#!/usr/bin/env python3
import subprocess
import os

r"""
Version 0.1.0

15-band parametric equalizer. Set the preset in CONTROL_VALUES below and run. Run again with a new preset to replace the virtual sink with it instead.

Created on 13 January 2026 (Tuesday), with ChatGPT's assistance. Tested on Xubuntu 22.04.5 LTS (Jammy Jellyfish).

Pre-requisits:
• Linux
• PulseAudio
• swh-plugins (sudo apt install swh-plugins)
    Verify it exists with
        ls /usr/lib/ladspa | grep mbeq
    Should show mbeq_1197.so
• Python 3.x

The MIT License (MIT)

Copyright © 2026 Mark

Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the “Software”), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:

The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.

THE SOFTWARE IS PROVIDED “AS IS”, WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.


"""

# ===== Configuration =====
# Full name of the output sink (replace with your device)
#MASTER_SINK="alsa_output.pci-0000_00_14.2.analog-stereo"
#MASTER_SINK="alsa_output.usb-K66_K66_20190805V001-00.analog-stereo"
MASTER_SINK = "@DEFAULT_SINK@"
SINK_NAME = "custom_eq_sink"
PLUGIN = "mbeq_1197"
LABEL = "mbeq"
STATE_FILE = "/tmp/eq_sink_last_module_id"

# Hardcoded 15-band EQ values; EDIT THIS AND RUN (subsequent runs will remove the previously created virtual sink and replace it with this one)
PRESETS = {
    "movies": "-3,-2,0,1,2,3,4,5,4,3,2,1,0,-1,-2",
    "podcasts": "-2,-1,0,1,3,4,4,4,3,2,1,0,0,0,0",
    "music": "0,0,1,2,3,4,5,5,4,3,2,1,0,0,0",
    "exp1": "11,10,9,8,7,6,10,11,10,6,7,8,9,10,11",
    "exp2": "8,6,10,8,6,4,2,0,2,4,6,8,10,6,8",
    "pop": "-2,-1,0,1,2,3,2,1,2,2,1,2,3,3,2",
    "sweep": "12,10,8,6,4,2,0,2,4,6,8,10,12,10,8",
    "surge": "0,2,4,6,8,10,12,10,8,6,4,2,0,2,4",
    "jcurve": "5,12,3,10,6,12,2,8,4,10,6,11,3,12,5",
    "shimber": "15,14,13,5,4,3,0,1,2,3,6,8,12,14,15",
    "mcoast": "0,12,2,10,4,8,6,6,4,8,2,10,0,12,2",
    "acresc": "0,1,2,3,4,5,6,7,8,9,10,11,12,13,14",
    "cunch": "14,13,12,11,10,9,8,7,6,5,4,3,2,1,0",
    "jalarity": "10,0,10,0,10,0,10,0,10,0,10,0,10,0,10",
    "lope": "15,14,13,12,11,10,5,0,5,10,11,12,13,14,15", #cool, but too loud
    "interlope": "7,8,8,3,6,5,5,9,5,5,6,3,8,8,7",
    "zeros": "0,0,0,0,0,0,0,0,0,0,0,0,0,0,0"
}
CONTROL_VALUES = PRESETS["music"]

# ===== Ensure PulseAudio can be found =====
if "XDG_RUNTIME_DIR" not in os.environ:
    os.environ["XDG_RUNTIME_DIR"] = f"/run/user/{os.getuid()}"

# ===== Function to run shell commands =====
def run_cmd(cmd):
    subprocess.run(cmd, shell=True, check=False)

# ===== Unload previous sink if it exists =====
if os.path.exists(STATE_FILE):
    with open(STATE_FILE, "r") as f:
        last_module = f.read().strip()
    # Check if the module still exists
    result = subprocess.run("pactl list short modules", shell=True, capture_output=True, text=True)
    if last_module in [line.split()[0] for line in result.stdout.splitlines()]:
        print(f"Unloading previous sink (module {last_module})...")
        run_cmd(f"pactl unload-module {last_module}")

# ===== Load new LADSPA EQ sink =====
cmd = (
    f"pactl load-module module-ladspa-sink "
    f"sink_name={SINK_NAME} "
    f"master={MASTER_SINK} "
    f"plugin={PLUGIN} "
    f"label={LABEL} "
    f"control={CONTROL_VALUES}"
)

# Run without capturing stdout to avoid PIPE issues
ret = subprocess.run(cmd, shell=True)
if ret.returncode == 0:
    # Get the latest module ID (assumes it’s the last one listed)
    result = subprocess.run("pactl list short modules | tail -n1 | awk '{print $1}'",
                            shell=True, capture_output=True, text=True)
    module_id = result.stdout.strip()
    with open(STATE_FILE, "w") as f:
        f.write(module_id)
    print(f"New EQ sink '{SINK_NAME}' loaded with module ID {module_id}.")

    # Set it as the default sink
    run_cmd(f"pactl set-default-sink {SINK_NAME}")
    print(f"Sink '{SINK_NAME}' is now the default output.")
else:
    print("Failed to load the EQ sink.")

#See eq_sink for the bash version.

#To unload a sink manually (switch 35 below is the number returned to the terminal emulator when initializing the sink; you'll need to replace that with the number you receive from it instead):
#pactl list short sinks
#pactl unload-module 35