-
-
Save disco0/702dedc033abfe8c499661f7d230901b to your computer and use it in GitHub Desktop.
Visual equalizer script for mpv
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
--[[ | |
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' | |
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 | |
-- 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 | |
-- ------ 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() -- 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 | |
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 |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment