259 lines
8.6 KiB
Lua
259 lines
8.6 KiB
Lua
-- Inferencium
|
||
-- MPV - Script - Equalizer
|
||
|
||
-- Copyright 2022-2023 Jake Winters
|
||
-- SPDX-License-Identifier: BSD-3-Clause-Clear
|
||
|
||
-- Version: 2.0.0.4
|
||
|
||
|
||
--[[
|
||
Default config:
|
||
- Enter/exit equilizer keys mode: ctrl+e
|
||
- Equalizer keys: 2/w control bass ... 6/y control treble, and middles in between
|
||
- Toggle equalizer without changing its values: ctrl+E (ctrl+shift+e)
|
||
- Reset equalizer values: alt+ctrl+e
|
||
- See ffmpeg filter description below the config section
|
||
--]]
|
||
|
||
|
||
-- ------ config -------
|
||
local start_keys_enabled = false -- if true then choose the up/down keys wisely
|
||
local key_toggle_bindings = 'ctrl+e' -- enable/disable equalizer key bindings
|
||
local key_toggle_equalizer = 'ctrl+E' -- enable/disable equalizer
|
||
local key_reset_equalizer = 'alt+ctrl+e' -- sets all bands to gain 0
|
||
|
||
-- reduce clicks (update the filter chain inplace). requires ffmpeg >= 4.0
|
||
local inplace = true
|
||
|
||
-- configure the equalizer keys, bands, and initial gain value for each band
|
||
local bands = {
|
||
-- octave is x2. e.g. two octaves range around f is from f/2 to f*2
|
||
-- {up down}
|
||
{keys = {'2', 'w'}, filter = {'equalizer=f=64:width_type=o:w=3.3:g=', 0}}, -- 20-200
|
||
{keys = {'3', 'e'}, filter = {'equalizer=f=400:width_type=o:w=2.0:g=', 0}}, -- 200-800
|
||
{keys = {'4', 'r'}, filter = {'equalizer=f=1250:width_type=o:w=1.3:g=', 0}}, -- 800-2k
|
||
{keys = {'5', 't'}, filter = {'equalizer=f=2830:width_type=o:w=1.0:g=', 0}}, -- 2k-4k
|
||
{keys = {'6', 'y'}, filter = {'equalizer=f=5600:width_type=o:w=1.0:g=', 0}}, -- 4k-8k
|
||
--{keys = {'7', 'u'}, filter = {'equalizer=f=12500:width_type=o:w=1.3:g=', 0}} -- - 20k
|
||
}
|
||
|
||
--[[
|
||
https://ffmpeg.org/ffmpeg-filters.html#equalizer
|
||
Apply a two-pole peaking equalisation (EQ) filter. With this filter, the signal-level
|
||
at and around a selected frequency can be increased or decreased, whilst (unlike
|
||
bandpass and bandreject filters) that at all other frequencies is unchanged.
|
||
In order to produce complex equalisation curves, this filter can be given several
|
||
times, each with a different central frequency.
|
||
The filter accepts the following options:
|
||
frequency, f: Set the filter’s central frequency in Hz.
|
||
width_type: Set method to specify band-width of filter.
|
||
h Hz
|
||
q Q-Factor
|
||
o octave
|
||
s slope
|
||
width, w: Specify the band-width of a filter in width_type units.
|
||
gain, g: Set the required gain or attenuation in dB. Beware of clipping when
|
||
using a positive gain.
|
||
--]]
|
||
|
||
|
||
-- ------- utils --------
|
||
function iff(cc, a, b) if cc then return a else return b end end
|
||
function ss(s, from, to) return s:sub(from, to - 1) end
|
||
--[[-- utils
|
||
local mp_msg = require 'mp.msg'
|
||
function midwidth(min, max) -- range --> middle freq and width in octaves
|
||
local wo = math.log(max / min) / math.log(2)
|
||
mp_msg.info(min, max / (2 ^ (wo / 2)) .. ' <' .. wo .. '>', max)
|
||
end
|
||
function range(f, wo) -- middle freq and width in octaves --> range
|
||
local h = 2 ^ (wo / 2)
|
||
mp_msg.info(f / h, '' .. f .. ' <' .. wo .. '>' , f * h)
|
||
end
|
||
--]]
|
||
|
||
-- return the filter as numbers {frequency, gain}
|
||
local function filter_data(filter)
|
||
return { tonumber(ss(filter[1], 13, filter[1]:find(':', 14, true))), filter[2] }
|
||
end
|
||
|
||
-- the mpv command string for adding the filter (only used when gain != 0)
|
||
local function get_cmd(filter)
|
||
return 'no-osd af add lavfi=[' .. filter[1] .. filter[2] .. ']'
|
||
end
|
||
|
||
-- setup named filter equalizer<band>
|
||
local function get_cmd_band_inplace_setup(filter, band, reset)
|
||
local v = reset and 0 or filter[2]
|
||
return 'no-osd af add @equalizer'.. band ..':lavfi=[' .. filter[1] .. v .. ']'
|
||
end
|
||
|
||
-- update gain of named filter equalizer<band> inplace
|
||
local function get_cmd_band_inplace(filter, band, reset)
|
||
local v = reset and 0 or filter[2]
|
||
return 'no-osd af-command equalizer'.. band ..' g '.. v
|
||
end
|
||
|
||
-- these two vars are used globally
|
||
local bindings_enabled = start_keys_enabled
|
||
local eq_enabled = true -- but af is not touched before the equalizer is modified
|
||
|
||
local inplace_init = false
|
||
|
||
|
||
-- ------ OSD handling -------
|
||
local function ass(x)
|
||
-- local gpo = mp.get_property_osd
|
||
-- return gpo('osd-ass-cc/0') .. x .. gpo('osd-ass-cc/1')
|
||
|
||
-- seemingly it's impossible to enable ass escaping with mp.set_osd_ass,
|
||
-- so we're already in ass mode, and no need to unescape first.
|
||
return x
|
||
end
|
||
|
||
local function fsize(s) -- 100 is the normal font size
|
||
return ass('{\\fscx' .. s .. '\\fscy' .. s ..'}')
|
||
end
|
||
|
||
local function color(c) -- c is RRGGBB
|
||
return ass('{\\1c&H' .. ss(c, 5, 7) .. ss(c, 3, 5) .. ss(c, 1, 3) .. '&}')
|
||
end
|
||
|
||
local function cnorm() return color('ffffff') end -- white
|
||
local function cdis() return color('909090') end -- grey
|
||
local function ceq() return iff(eq_enabled, color('ffff90'), cdis()) end -- yellow-ish
|
||
local function ckeys() return iff(bindings_enabled, color('90FF90'), cdis()) end -- green-ish
|
||
|
||
local DUR_DEFAULT = 1.5 -- seconds
|
||
local osd_timer = nil
|
||
-- duration: seconds, or default if missing/nil, or infinite if 0 (or negative)
|
||
local function ass_osd(msg, duration) -- empty or missing msg -> just clears the OSD
|
||
duration = duration or DUR_DEFAULT
|
||
if not msg or msg == '' then
|
||
msg = '{}' -- the API ignores empty string, but '{}' works to clean it up
|
||
duration = 0
|
||
end
|
||
mp.set_osd_ass(0, 0, msg)
|
||
if osd_timer then
|
||
osd_timer:kill()
|
||
osd_timer = nil
|
||
end
|
||
if duration > 0 then
|
||
osd_timer = mp.add_timeout(duration, ass_osd) -- ass_osd() clears without a timer
|
||
end
|
||
end
|
||
|
||
-- some visual messing about
|
||
local function updateOSD()
|
||
local msg1 = fsize(70) .. 'Equalizer: ' .. ceq() .. iff(eq_enabled, 'On', 'Off')
|
||
.. ' [' .. key_toggle_equalizer .. ']' .. cnorm()
|
||
local msg2 = fsize(70)
|
||
.. 'Key-bindings: ' .. ckeys() .. iff(bindings_enabled, 'On', 'Off')
|
||
.. ' [' .. key_toggle_bindings .. ']' .. cnorm()
|
||
local msg3 = ''
|
||
|
||
for i = 1, #bands do
|
||
local data = filter_data(bands[i].filter)
|
||
local info =
|
||
ceq() .. fsize(50) .. data[1] .. ' hz ' .. fsize(100)
|
||
.. iff(data[2] ~= 0 and eq_enabled, '', cdis()) .. data[2] .. ceq()
|
||
.. fsize(50) .. ckeys() .. ' [' .. bands[i].keys[1] .. '/' .. bands[i].keys[2] .. ']'
|
||
.. ceq() .. fsize(100) .. cnorm()
|
||
|
||
msg3 = msg3 .. iff(i > 1, ' ', '') .. info
|
||
end
|
||
|
||
local nlb = '\n' .. ass('{\\an1}') -- new line and "align bottom for next"
|
||
local msg = ass('{\\an1}') .. msg3 .. nlb .. msg2 .. nlb .. msg1
|
||
local duration = iff(start_keys_enabled, iff(bindings_enabled and eq_enabled, 5, nil)
|
||
, iff(bindings_enabled, 0, nil))
|
||
ass_osd(msg, duration)
|
||
end
|
||
|
||
|
||
-- ------- actual functionality ------
|
||
local function updateAF_simple() -- setup an audio filter chain which applies the equalizer
|
||
mp.command('no-osd af clr ""') -- af clr must have two double-quotes
|
||
if not eq_enabled then return end
|
||
|
||
for i = 1, #bands do
|
||
local f = bands[i].filter
|
||
if f[2] ~= 0 then -- insert filters only were the gain is non default
|
||
mp.command(get_cmd(f))
|
||
end
|
||
end
|
||
end
|
||
|
||
-- update gains of the whole equalizer inplace, also setup on first time
|
||
local function updateAF_inplace()
|
||
for i = 1, #bands do
|
||
local f = bands[i].filter
|
||
if not inplace_init then
|
||
mp.command(get_cmd_band_inplace_setup(f, i, not eq_enabled))
|
||
end
|
||
mp.command(get_cmd_band_inplace(f, i, not eq_enabled))
|
||
end
|
||
inplace_init = true
|
||
end
|
||
|
||
if inplace then
|
||
updateAF = updateAF_inplace
|
||
else
|
||
updateAF = updateAF_simple
|
||
end
|
||
|
||
local function getBind(filter, delta)
|
||
return function() -- onKey
|
||
filter[2] = filter[2] + delta
|
||
updateAF()
|
||
updateOSD()
|
||
end
|
||
end
|
||
|
||
local function update_key_binding(enable, key, name, fn)
|
||
if enable then
|
||
mp.add_forced_key_binding(key, name, fn, 'repeatable')
|
||
else
|
||
mp.remove_key_binding(name)
|
||
end
|
||
end
|
||
|
||
local function toggle_bindings(explicit, no_osd)
|
||
bindings_enabled = iff(explicit ~= nil, explicit, not bindings_enabled)
|
||
for i = 1, #bands do
|
||
local k = bands[i].keys
|
||
local f = bands[i].filter
|
||
update_key_binding(bindings_enabled, k[1], 'eq' .. k[1], getBind(f, 1)) -- up
|
||
update_key_binding(bindings_enabled, k[2], 'eq' .. k[2], getBind(f, -1)) -- down
|
||
end
|
||
if not no_osd then updateOSD() end
|
||
end
|
||
|
||
local function toggle_equalizer()
|
||
eq_enabled = not eq_enabled
|
||
updateAF()
|
||
updateOSD()
|
||
end
|
||
|
||
local function reset_equalizer()
|
||
for i = 1, #bands do
|
||
bands[i].filter[2] = 0
|
||
end
|
||
updateAF()
|
||
updateOSD()
|
||
end
|
||
|
||
mp.add_forced_key_binding(key_toggle_equalizer, toggle_equalizer)
|
||
mp.add_forced_key_binding(key_toggle_bindings, toggle_bindings)
|
||
mp.add_forced_key_binding(key_reset_equalizer, reset_equalizer)
|
||
if bindings_enabled then toggle_bindings(true, true) end
|
||
|
||
-- init: setup the equalizer if the initial gain is not 0
|
||
for i = 1, #bands do
|
||
if bands[i].filter[2] ~= 0 then
|
||
updateAF()
|
||
break
|
||
end
|
||
end
|