fun

Add MPV skipsilence plugin

Author
Maarten Vangeneugden
Date
Dec. 4, 2020, 2: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