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