skipsilence2.lua
1 |
|
2 |
* skipsilence.lua versie 2 |
3 |
* |
4 |
* AUTHOR: Maarten Vangeneugden |
5 |
* License: GNU GPLv3+ |
6 |
* link: https://maartenv.be/gitar/fun/master/ |
7 |
* |
8 |
* This script can be used to skip all silent parts while watching |
9 |
* a video. I made this during the COVID-19 pandemic because all |
10 |
* my courses were being taught online. I noticed that up to a third |
11 |
* of what I "watched" was actually silence, which I could skip, |
12 |
* saving me a lot of time. However, doing this manually is error- |
13 |
* prone and not very convenient. |
14 |
* |
15 |
* The way this works is actually kind of a hack: It uses ffmpeg's |
16 |
* detectsilence filter, but that doesn't give reliable results as to |
17 |
* how much I can skip. So when you activate this script, it rapidly |
18 |
* goes over the entire video, storing all silent parts in a Lua table. |
19 |
* (Which, should you be somebody that thinks Lua tables are incredibly |
20 |
* weird, they're definitely not). After that, it returns to the point |
21 |
* where it was called, and resumes playback. When a silent part pops up, |
22 |
* the data is retrieved from the table, and the silence is skipped |
23 |
* immediately. I had to do it this way, because there was literally no |
24 |
* other way to do this, except for writing a custom ffmpeg filter, but |
25 |
* let's not resort to radicalism. |
26 |
|
27 |
* The default keybind is F3, but change that to your liking below. |
28 |
* You can change this by adding |
29 |
* the following line to your input.conf: |
30 |
* KEY script-binding skip-to-silence |
31 |
* |
32 |
* In order to tweak the script parameters, you can place the |
33 |
* text below, between the template markers, in a new file at |
34 |
* script-opts/skiptosilence.conf in mpv's user folder. The |
35 |
* parameters will be automatically loaded on start. |
36 |
* |
37 |
|
38 |
*** FUCKING USEFUL INFORMATION FOR WORKING WITH THE SILENCEDETECT FILTER |
39 |
This filter fires a string at TWO moments: |
40 |
1. When the silence starts |
41 |
2. The frame after the silence ends |
42 |
The first time, it sends a message like this: |
43 |
{"lavfi.silence_start":"1095.34"} |
44 |
The second time, it sends like this: |
45 |
{"lavfi.silence_start":"1095.34","lavfi.silence_end":"1101.16","lavfi.silence_duration":"5.81419"} |
46 |
Notice how the start tag has remained the same, but there are two tags that have been added: |
47 |
The duration and the end tag. |
48 |
|
49 |
WHY IS THIS IMPORTANT? Because the next time a silence is detected, it sends this shit out: |
50 |
{"lavfi.silence_start":"1102.37","lavfi.silence_end":"1101.16","lavfi.silence_duration":"5.81419"} |
51 |
Notice how suddenly, even though this is step 1, it does have an end and duration! |
52 |
BUT THESE ARE JUST THE NOT-UPDATED VALUES OF THE PREVIOUS SILENCE! So you ought to ignore these, |
53 |
since IT IS NOT INFO ABOUT THIS SILENCE! |
54 |
|
55 |
I'm pretty pissed off about this because apparently nobody at ffmpeg thought |
56 |
it was important to document this, and I've been spending quite some time |
57 |
trying to find out why my own script wouldn't fucking work. But it's because |
58 |
some twat didn't document per shit properly, got it now. |
59 |
This is version two, a version with less than half the amount of code from |
60 |
the previous version, and one that works a thousand times better. |
61 |
|
62 |
|
63 |
****************** TEMPLATE FOR skiptosilence.conf ****************** |
64 |
# Maximum amount of noise to trigger, in terms of dB. |
65 |
# The default is -30 (yes, negative). -60 is very sensitive, |
66 |
# -10 is more tolerant to noise. |
67 |
quietness = -30 |
68 |
|
69 |
# Minimum duration of silence to trigger. |
70 |
duration = 0.1 |
71 |
|
72 |
# The fast-forwarded audio can sound jarring. Set to 'yes' |
73 |
# to mute it while skipping. |
74 |
mutewhileskipping = no |
75 |
************************** END OF TEMPLATE ************************** |
76 |
--]] |
77 |
|
78 |
local opts = { |
79 |
quietness = -40, |
80 |
duration = 1.5, |
81 |
mutewhileskipping = true |
82 |
} |
83 |
|
84 |
local mp = require 'mp' |
85 |
local msg = require 'mp.msg' |
86 |
local options = require 'mp.options' |
87 |
|
88 |
local enabled = false |
89 |
|
90 |
-- I added resetSkip because the skipper sometimes goes haywire and skips over |
91 |
-- parts that are clearly not silent. Don't know why, but if this happens, |
92 |
-- resetSkip takes you back 10 seconds and resets everything. |
93 |
function resetSkip() |
94 |
setAudioFilter(false) |
95 |
enabled=false |
96 |
mp.unobserve_property(silenceDetected) |
97 |
if old_speed ~= 10 then |
98 |
mp.set_property("speed", old_speed) |
99 |
else |
100 |
mp.set_property("speed", 1) |
101 |
end |
102 |
mp.set_property_bool("mute", false) |
103 |
in_silence = false |
104 |
last_silence_start = 0 |
105 |
last_silence_end = 0 |
106 |
last_silence_duration = 0 |
107 |
-- Go back 10 seconds because there's probably a part that was skipped over |
108 |
current_time = mp.get_property_native("time-pos") |
109 |
mp.set_property_number("time-pos", current_time - 10) |
110 |
print("RESET") |
111 |
end |
112 |
|
113 |
|
114 |
function toggleSkip() |
115 |
if enabled then |
116 |
mp.unobserve_property(silenceDetected) |
117 |
print("TOGGLE OFF") |
118 |
else |
119 |
print("TOGGLE ON") |
120 |
mp.observe_property("af-metadata/skipsilence", "string", silenceDetected) -- Start listening |
121 |
|
122 |
end |
123 |
setAudioFilter(not enabled) |
124 |
--setVideoFilter(not enabled, mp.get_property_native("width"), mp.get_property_native("height")) |
125 |
enabled = not enabled |
126 |
|
127 |
end |
128 |
|
129 |
|
130 |
in_silence = false |
131 |
-- Normally I'd use a simple in_silence state variable, but because this ffmpeg |
132 |
-- filter was written by people who can't even log shit correctly, I have to |
133 |
-- manually check every log entry for the values to see if they changed. |
134 |
last_silence_start = 0 |
135 |
last_silence_end = 0 |
136 |
last_silence_duration = 0 |
137 |
old_speed = 1 |
138 |
|
139 |
-- This function is triggered whenever the skip feature is enabled and a silent |
140 |
-- part occurs in the stream. |
141 |
function silenceDetected(name, value) |
142 |
--print("SPEED UP") |
143 |
--mp.set_property("speed", 10) |
144 |
if value == "{}" or value == nil then |
145 |
return -- For some reason these are sometimes emitted. Ignore. |
146 |
end |
147 |
i = 1 |
148 |
for silence in string.gmatch(value, "%d+%.?%d+") do |
149 |
if i==1 then |
150 |
silence_start = silence |
151 |
elseif i==2 then |
152 |
silence_end = silence |
153 |
elseif i==3 then |
154 |
silence_duration = silence |
155 |
end |
156 |
end |
157 |
-- If we're in a silence, then we need to reduce the speed again |
158 |
--if silence_start==last_silence_start then |
159 |
if in_silence then |
160 |
mp.set_property("speed", old_speed) |
161 |
print("SPEED DOWN") |
162 |
--mp.unobserve_property(silenceDetected) |
163 |
--now = mp.get_property_native("time-pos") -- get current time |
164 |
--mp.set_property_number("time-pos", now) -- A little bit before the end of the silence |
165 |
--mp.add_timeout(2.0, function() mp.observe_property("af-metadata/skipsilence", "string", silenceDetected) end) -- Wait half a second before observing silences again |
166 |
mp.set_property_bool("mute", false) |
167 |
in_silence = false -- And finally, disable the in-silence state |
168 |
else |
169 |
in_silence = true |
170 |
mp.set_property_bool("mute", true) |
171 |
old_speed = mp.get_property_native("speed") -- get current speed |
172 |
mp.set_property("speed", 10) |
173 |
print("SPEED UP") |
174 |
end |
175 |
print(value) --DEBUG |
176 |
last_silence_start = silence_start |
177 |
last_silence_end= silence_end |
178 |
last_silence_duration = silence_duration |
179 |
|
180 |
end |
181 |
|
182 |
|
183 |
|
184 |
|
185 |
-- Adds the filters to the filtergraph on mpv init |
186 |
-- in a disabled state. |
187 |
-- Filter documentation: https://ffmpeg.org/ffmpeg-filters.html |
188 |
function init() |
189 |
-- `silencedetect` is an audio filter that listens for silence |
190 |
-- and emits text output with details whenever silence is detected. |
191 |
local af_table = mp.get_property_native("af") |
192 |
af_table[#af_table + 1] = { |
193 |
enabled=false, |
194 |
label="skipsilence", |
195 |
name="lavfi", |
196 |
params= { |
197 |
graph = "silencedetect=noise="..opts.quietness.."dB:d="..opts.duration |
198 |
} |
199 |
} |
200 |
mp.set_property_native("af", af_table) |
201 |
|
202 |
-- `nullsink` interrupts the video stream requests to the decoder, |
203 |
-- which stops it from bogging down the fast-forward. |
204 |
-- `color` generates a blank image, which renders very quickly and |
205 |
-- is good for fast-forwarding. |
206 |
-- The graph is not actually filled in now, but when toggled on, |
207 |
-- as it needs the resolution information. |
208 |
local vf_table = mp.get_property_native("vf") |
209 |
vf_table[#vf_table + 1] = { |
210 |
enabled=false, |
211 |
label="skipsilence-blackout", |
212 |
name="lavfi", |
213 |
params= { |
214 |
graph = "" --"nullsink,color=c=black:s=1920x1080" |
215 |
} |
216 |
} |
217 |
mp.set_property_native("vf", vf_table) |
218 |
end |
219 |
|
220 |
function setAudioFilter(state) |
221 |
local af_table = mp.get_property_native("af") |
222 |
if #af_table > 0 then |
223 |
for i = #af_table, 1, -1 do |
224 |
if af_table[i].label == "skipsilence" then |
225 |
af_table[i].enabled = state |
226 |
mp.set_property_native("af", af_table) |
227 |
return |
228 |
end |
229 |
end |
230 |
end |
231 |
end |
232 |
|
233 |
function setVideoFilter(state, width, height) |
234 |
local vf_table = mp.get_property_native("vf") |
235 |
if #vf_table > 0 then |
236 |
for i = #vf_table, 1, -1 do |
237 |
if vf_table[i].label == "skipsilence-blackout" then |
238 |
vf_table[i].enabled = state |
239 |
vf_table[i].params = { |
240 |
graph = "nullsink,color=c=black:s="..width.."x"..height |
241 |
} |
242 |
mp.set_property_native("vf", vf_table) |
243 |
return |
244 |
end |
245 |
end |
246 |
end |
247 |
end |
248 |
|
249 |
options.read_options(opts) |
250 |
init() |
251 |
|
252 |
mp.add_key_binding("F3", "toggle-skip-silence", toggleSkip) |
253 |
mp.add_key_binding("F4", "reset-skip-silence", resetSkip) |
254 |