Skip to content
233 changes: 148 additions & 85 deletions skiptosilence.lua
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
--[[
* skiptosilence.lua v.2022-02-27
* skiptosilence.lua v.2023-08-27
*
* AUTHORS: detuur, microraptor
* AUTHORS: detuur, microraptor, Eisa01
* License: MIT
* link: https://github.com/detuur/mpv-scripts
*
Expand All @@ -19,110 +19,173 @@
* script-opts/skiptosilence.conf in mpv's user folder. The
* parameters will be automatically loaded on start.
*
* Dev note about the used filters:
Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Minor nitpick: the dev note should stay in the code. For userscripts, the header is aimed at users, not devs.

* - `silencedetect` is an audio filter that listens for silence and
* emits text output with details whenever silence is detected.
* Filter documentation: https://ffmpeg.org/ffmpeg-filters.html
****************** TEMPLATE FOR skiptosilence.conf ******************
# Maximum amount of noise to trigger, in terms of dB.
# The default is -30 (yes, negative). -60 is very sensitive,
# -10 is more tolerant to noise.
quietness = -30
#--(#number). Maximum amount of noise to trigger, in terms of dB. Lower is more sensitive.
silence_audio_level=-40
Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Avoid changing variable names unless they're outright wrong or conflict with new variables. Changing them results in needless noise in the git diff/history.

# Minimum duration of silence to trigger.
duration = 0.1
#--(#number). Duration of the silence that will be detected to trigger skipping.
silence_duration=0.7
# The fast-forwarded audio can sound jarring. Set to 'yes'
# to mute it while skipping.
mutewhileskipping = no
#--(0/#number). The first detcted silence_duration will be ignored for the defined seconds in this option, and it will continue skipping until the next silence_duration.
# (0 for disabled, or specify seconds).
ignore_silence_duration=1
#--(0/#number). Minimum amount of seconds accepted to skip until the configured silence_duration.
# (0 for disabled, or specify seconds)
min_skip_duration=0
#--(0/#number). Maximum amount of seconds accepted to skip until the configured silence_duration.
# (0 for disabled, or specify seconds)
max_skip_duration=120
#--(yes/no). Default is muted, however if audio was enabled due to custom mpv settings, the fast-forwarded audio can sound jarring.
force_mute_on_skip=no
#--(yes/no). Display osd messages when actions occur.
osd_msg=yes
Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm on the fence about this option. I don't like it from the perspective of hiding/showing debug messages (like "silence was less than minimum" on line 102). These messages don't belong in the OSD; users can find them in the terminal.

On the other hand, this option is valuable for people who want to hide "skipped to silence at XXX" (line 116).

Additionally, on line 108 I've found another use for this option. Weigh in with your opinion.

************************** END OF TEMPLATE **************************
--]]

local opts = {
quietness = -30,
duration = 0.1,
mutewhileskipping = false
local o = {
Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Don't use single-character variable names. This variable is impossible to search for in the code.

This is also an instance of "don't change variable names".

silence_audio_level = -40,
silence_duration = 0.7,
ignore_silence_duration=1,
min_skip_duration = 0,
max_skip_duration = 120,
force_mute_on_skip = false,
osd_msg = true,
}

(require 'mp.options').read_options(o)
local mp = require 'mp'
local msg = require 'mp.msg'
local options = require 'mp.options'

old_speed = 1
was_paused = false
was_muted = false
speed_state = 1
pause_state = false
mute_state = false
sub_state = nil
secondary_sub_state = nil
vid_state = nil
window_state = nil
skip_flag = false
initial_skip_time = 0

function restoreProp(timepos,pause)
if not timepos then timepos = mp.get_property_number("time-pos") end
if not pause then pause = pause_state end

mp.set_property("vid", vid_state)
mp.set_property("force-window", window_state)
mp.set_property_bool("mute", mute_state)
mp.set_property("speed", speed_state)
mp.unobserve_property(foundSilence)
mp.command("no-osd af remove @skiptosilence")
mp.set_property_bool("pause", pause)
mp.set_property_number("time-pos", timepos)
mp.set_property("sub-visibility", sub_state)
mp.set_property("secondary-sub-visibility", secondary_sub_state)
timer:kill()
skip_flag = false
end

--[[
Dev note about the used filters:
- `silencedetect` is an audio filter that listens for silence and
emits text output with details whenever silence is detected.
- `nullsink` interrupts the video stream requests to the decoder,
which stops it from bogging down the fast-forward.
- `color` generates a blank image, which renders very quickly and is
good for fast-forwarding.
- Filter documentation: https://ffmpeg.org/ffmpeg-filters.html
--]]
function handleMinMaxDuration(timepos)
if not skip_flag then return end
if not timepos then timepos = mp.get_property_number("time-pos") end

skip_duration = timepos - initial_skip_time
if o.min_skip_duration > 0 and skip_duration <= o.min_skip_duration then
restoreProp(initial_skip_time)
if o.osd_msg then mp.osd_message('Skipping Cancelled\nSilence is less than configured minimum') end
msg.info('Skipping Cancelled\nSilence is less than configured minimum')
return true
end
if o.max_skip_duration > 0 and skip_duration >= o.max_skip_duration then
restoreProp(initial_skip_time)
if o.osd_msg then mp.osd_message('Skipping Cancelled\nSilence is more than configured maximum') end
Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Could be changed to:

No silence found in the next XXX seconds.
See max_skip_duration in skiptosilence.lua

This message in particular will be seen by many users who won't have bothered to read the header. In this case we could even change it to something like this?

No silence found in the next XXX seconds.
See README section in skiptosilence.lua

What do you think? I want to move away from osd_msg but it could be repurposed as a yes_I_have_read_the_readme sort of variable. When set to yes it would only show the first line.

msg.info('Skipping Cancelled\nSilence is more than configured maximum')
return true
end
return false
end

function skippedMessage()
if o.osd_msg then mp.osd_message("Skipped to silence at " .. mp.get_property_osd("time-pos")) end
msg.info("Skipped to silence at " .. mp.get_property_osd("time-pos"))
end

function doSkip()
-- Get video dimensions
local width = mp.get_property_native("width");
local height = mp.get_property_native("height")

-- Create audio and video filters
mp.command(
"no-osd af add @skiptosilence:lavfi=[silencedetect=noise=" ..
opts.quietness .. "dB:d=" .. opts.duration .. "]"
)
mp.command(
"no-osd vf add @skiptosilence-blackout:lavfi=" ..
"[nullsink,color=c=black:s=" .. width .. "x" .. height .. "]"
)

-- Triggers whenever the `silencedetect` filter emits output
mp.observe_property("af-metadata/skiptosilence", "string", foundSilence)

was_muted = mp.get_property_native("mute")
if opts.mutewhileskipping then
if skip_flag then return end
initial_skip_time = (mp.get_property_native("time-pos") or 0)
if math.floor(initial_skip_time) == math.floor(mp.get_property_native('duration') or 0) then return end

local width = mp.get_property_native("osd-width")
local height = mp.get_property_native("osd-height")
mp.set_property_native("geometry", ("%dx%d"):format(width, height))

mp.command(
"no-osd af add @skiptosilence:lavfi=[silencedetect=noise=" ..
o.silence_audio_level .. "dB:d=" .. o.silence_duration .. "]"
)

mp.observe_property("af-metadata/skiptosilence", "string", foundSilence)

sub_state = mp.get_property("sub-visibility")
mp.set_property("sub-visibility", "no")
secondary_sub_state = mp.get_property("secondary-sub-visibility")
mp.set_property("secondary-sub-visibility", "no")
window_state = mp.get_property("force-window")
mp.set_property("force-window", "yes")
vid_state = mp.get_property("vid")
mp.set_property("vid", "no")
mute_state = mp.get_property_native("mute")
if o.force_mute_on_skip then
mp.set_property_bool("mute", true)
end

was_paused = mp.get_property_native("pause")
mp.set_property_bool("pause", false)
old_speed = mp.get_property_native("speed")
mp.set_property("speed", 100)
pause_state = mp.get_property_native("pause")
mp.set_property_bool("pause", false)
speed_state = mp.get_property_native("speed")
mp.set_property("speed", 100)
skip_flag = true

timer = mp.add_periodic_timer(0.5, function()
local video_time = (mp.get_property_native("time-pos") or 0)
handleMinMaxDuration(video_time)
end)
end

function foundSilence(name, value)
if value == "{}" or value == nil then
return -- For some reason these are sometimes emitted. Ignore.
end

timecode = tonumber(string.match(value, "%d+%.?%d+"))
time_pos = mp.get_property_native("time-pos")
if timecode == nil or timecode < time_pos + 1 then
return -- Ignore anything less than a second ahead.
end

mp.set_property_bool("mute", was_muted)
mp.set_property_bool("pause", was_paused)
mp.set_property("speed", old_speed)
mp.unobserve_property(foundSilence)

-- Remove used audio and video filters
mp.command("no-osd af remove @skiptosilence")
mp.command("no-osd vf remove @skiptosilence-blackout")

-- Seeking to the exact moment even though we've already
-- fast forwarded here allows the video decoder to skip
-- the missed video. This prevents massive A-V lag.
mp.set_property_number("time-pos", timecode)

-- If we don't wait at least 50ms before messaging the user, we
-- end up displaying an old value for time-pos.
mp.add_timeout(0.05, skippedMessage)
if value == "{}" or value == nil then
Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Don't change the code style. By changing spaces to tabs for the entire file, this line shows up in the diff while nothing actually changed. In general, code style changes are pretty disruptive to git history and should be avoided unless technically required.

return
Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

In a similar vein to the previous note, avoid removing (or even spell-checking!) comments unless something about them is wrong. This is another source of noise in git diff/history and reviews.

end

timecode = tonumber(string.match(value, "%d+%.?%d+"))
if timecode == nil or timecode < initial_skip_time + o.ignore_silence_duration then
return
end

if handleMinMaxDuration(timecode) then return end

restoreProp(timecode)

mp.add_timeout(0.05, skippedMessage)
skip_flag = false
end

function skippedMessage()
msg.info("Skipped to silence at " .. mp.get_property_osd("time-pos"))
mp.osd_message("Skipped to silence at " .. mp.get_property_osd("time-pos"))
end
mp.observe_property('pause', 'bool', function(name, value)
if value and skip_flag then
restoreProp(initial_skip_time, true)
end
end)


options.read_options(opts)
mp.add_hook('on_unload', 9, function()
if skip_flag then
restoreProp()
end
end)

mp.add_key_binding("F3", "skip-to-silence", doSkip)