fun

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