From f5122511f299fa81e1ac4229edbcf366964f2b5a Mon Sep 17 00:00:00 2001 From: inference Date: Sun, 6 Nov 2022 09:08:53 +0000 Subject: [PATCH] Add MPV equalizer script. --- mpv/scripts/equalizer.lua | 250 ++++++++++++++++++++++++++++++++++++++ 1 file changed, 250 insertions(+) create mode 100644 mpv/scripts/equalizer.lua diff --git a/mpv/scripts/equalizer.lua b/mpv/scripts/equalizer.lua new file mode 100644 index 0000000..6448394 --- /dev/null +++ b/mpv/scripts/equalizer.lua @@ -0,0 +1,250 @@ +--[[ +mpv 5-bands equalizer with visual feedback. +Copyright 2016 Avi Halachmi ( https://github.com/avih ) +License: public domain +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 +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 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