Skip to content

Instantly share code, notes, and snippets.

@avih
Last active April 17, 2024 23:56
Show Gist options
  • Star 32 You must be signed in to star a gist
  • Fork 6 You must be signed in to fork a gist
  • Save avih/41acff712abd32e1f436235388c8b523 to your computer and use it in GitHub Desktop.
Save avih/41acff712abd32e1f436235388c8b523 to your computer and use it in GitHub Desktop.
Visual equalizer script for mpv
--[[
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<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
if inplace then
-- inplace changes (using af-command LABEL ...) don't recreate the filter
-- chain and can reduce perceptible discontinuities, however, they also
-- don't seem to persist across playlist items (probably reset on re-init).
-- this hook persists the current values between files, by replacing the
-- filters with new ones with the current values (not sub-values inplace).
mp.add_hook("on_before_start_file", 50, function()
inplace_init = false -- ensure the filters are recreated if exist
updateAF()
end)
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
@avih
Copy link
Author

avih commented Feb 5, 2023

2023-02-05: updated to persist between playlist items.

@escapezn thanks for the report. Should now be fixed.

@alek3y
Copy link

alek3y commented Oct 11, 2023

It seems the equalizer stops working when seeking if inplace = true, and starts working again after pressing Ctrl+E twice.
Is this intended or is there a problem with my setup (mpv 0.36.0 and ffmpeg 6.0)?

@Erikcb91
Copy link

Erikcb91 commented Apr 17, 2024

Hi, is there any way to set the gains by default?
I'm trying to edit it in {keys = {'2', 'w'}, filter = {'equalizer=f=64:width_type=o:w=3.3:g=', 0}}, -- 20-200 for example but it does nothing
I tried to add the gain after "g=" and I also tried changing the 0 at the end, but no luck so far, am I doing it wrong? or maybe I'm missing something?
The script works fine with the keyboard, it's just I use the player to watch anime and it's very inconvenient having to set it up every time
Thanks in advance

edit: I just saw that the info says there's a superequalizer, not sure if there's part of your script, but I can't find any other equalizer in my folders...
(https://i.imgur.com/6kQ1pFt.png)

@avih
Copy link
Author

avih commented Apr 17, 2024

@Erikcb91 The only thing which this script does is allow to configure the equalizer using the KB.

If you don't need that, and you just want a fixed equalizer preset, then you don't need the script. Just configure mpv with the preset you want, either with your mpv.conf file, or using command line options.

See above: https://gist.github.com/avih/41acff712abd32e1f436235388c8b523?permalink_comment_id=2978042#gistcomment-2978042

@Erikcb91
Copy link

I also tried that, for some reason it's not working for me :S
Anyway, here where I live is late, so I'll try some more stuff tomorrow, thanks

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment