├── .gitignore ├── LICENSE ├── Makefile ├── README.md ├── apply-equalizer ├── apply-equalizer.desktop └── apply-equalizer.py /.gitignore: -------------------------------------------------------------------------------- 1 | *.py[cod] 2 | 3 | # C extensions 4 | *.so 5 | 6 | # Packages 7 | *.egg 8 | *.egg-info 9 | dist 10 | build 11 | eggs 12 | parts 13 | bin 14 | var 15 | sdist 16 | develop-eggs 17 | .installed.cfg 18 | lib 19 | lib64 20 | __pycache__ 21 | 22 | # Installer logs 23 | pip-log.txt 24 | 25 | # Unit test / coverage reports 26 | .coverage 27 | .tox 28 | nosetests.xml 29 | 30 | # Translations 31 | *.mo 32 | 33 | # Mr Developer 34 | .mr.developer.cfg 35 | .project 36 | .pydevproject 37 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2014 nullEuro 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | NAME = apply-equalizer 2 | PREFIX = /usr/share/$(NAME) 3 | SCRIPT = $(NAME).py 4 | STARTER = $(NAME).desktop 5 | AUTOSTART_DIR = /etc/xdg/autostart 6 | 7 | install: 8 | mkdir -p $(PREFIX) 9 | cp -u $(SCRIPT) $(PREFIX) 10 | cp -u $(STARTER) $(AUTOSTART_DIR) 11 | cp -u --preserve=mode $(NAME) /usr/bin 12 | 13 | clean: 14 | rm $(PREFIX)/$(SCRIPT) 15 | rmdir $(PREFIX) 16 | rm $(AUTOSTART_DIR)/$(STARTER) 17 | rm /usr/bin/$(NAME) 18 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | apply-equalizer 2 | =============== 3 | 4 | A python programm that activates or deactivates pulseaudio-equalizer based on output port (headphones or speakers). 5 | 6 | ## Why ? ## 7 | 8 | It started on [pulseaudio - Automatically switch equalizer preset based on audio output (internal speaker or external) - Ask Ubuntu](http://askubuntu.com/questions/402275/automatically-switch-equalizer-preset-based-on-audio-output-internal-speaker-or) 9 | 10 | * **Problem:** laptop speakers have "highly unequal frequency response" as audio professional would say. Normal people say: "they sound very bad". 11 | * **Theory:** equalizing provides a valuable workaround, dramatically improving sound quality with a one-time effort. Laptop speakers needs heavy equalization, which is not needed by regular stereo or headphones. 12 | * **Practical (partial) solution:** use [pulseaudio-equalizer](https://launchpad.net/~nilarimogard/+archive/webupd8) 13 | * **Remaining problem:** when switching between laptop speakers and audio line out (concretely, plugging your external stereo or headphones on the jack), equalization needs to be switched on and off or adjusted. 14 | * **Solution:** this program automatically switches equalization profile based on active output. 15 | 16 | ## Installation ## 17 | 18 | Disable the equalizer via `pulseaudio-equalizer-gtk` and click on "Apply settings", now close the GUI. 19 | 20 | Download this repository, change to the directory and run 21 | 22 | sudo make install 23 | apply-equalizer 24 | 25 | Maybe you need to install some additional python modules. 26 | 27 | To uninstall, change to the directory where you downloaded the repository and run 28 | 29 | sudo make clean 30 | 31 | ## Usage ## 32 | The script creates per-port [1] equalizer-configurations under `~/.config/apply-equalizer` and symlinks them if a device changes the output port (i.e. headphones plugged in or out). 33 | 34 | [1]: many sound cards have different *ports*, e.g. one speaker-port and one headphone-port 35 | 36 | So: 37 | 38 | 1. Unplug headphones. 39 | 2. Open pulseaudio-equalizer GUI 40 | 3. Customize equalizer-settings until it sounds good 41 | 4. "Apply Settings" will then assign the configuration you made (including if the equalizer is enabled at all) to the *current port* (speakers in this case) 42 | 5. **close the GUI** and repeat from step 2 for every port you want to assign (headphones not plugged in) 43 | 44 | Now the equalizer settings get automatically adjusted whenever your switch between speakers and headphones. 45 | -------------------------------------------------------------------------------- /apply-equalizer: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | NAME=apply-equalizer 3 | PIDFILE=${HOME}/.${NAME}.pid 4 | LOGFILE=/tmp/${NAME}_${USER}.log 5 | 6 | if [ -f $PIDFILE ] && PID=$(cat $PIDFILE) && kill -0 `cat $PIDFILE` 2>/dev/null; then 7 | echo "Already running! PID=$PID" 8 | else 9 | python3 "/usr/share/$NAME/$NAME.py" > $LOGFILE & 10 | echo $! > $PIDFILE 11 | fi 12 | -------------------------------------------------------------------------------- /apply-equalizer.desktop: -------------------------------------------------------------------------------- 1 | [Desktop Entry] 2 | Version=1.0 3 | Encoding=UTF-8 4 | Name=Pulseaudio Equalizer Auto-Apply 5 | Exec=apply-equalizer 6 | Terminal=false 7 | Type=Application 8 | Categories= 9 | GenericName= 10 | X-GNOME-Autostart-Phase=Initialization 11 | NoDisplay=true 12 | -------------------------------------------------------------------------------- /apply-equalizer.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | # -*- coding: utf-8 -*- 3 | 4 | 5 | import dbus, os 6 | from dbus.mainloop.glib import DBusGMainLoop 7 | from gi.repository import GObject 8 | from subprocess import call, check_call 9 | from time import sleep 10 | from xdg.BaseDirectory import * 11 | 12 | config_dir = os.path.join(xdg_config_home, 'apply-equalizer') 13 | if not os.path.isdir(config_dir): 14 | os.mkdir(config_dir) 15 | 16 | debounceTime = 50 # milliseconds 17 | 18 | eq_config_path = os.path.join(xdg_config_home, 'pulse', 'equalizerrc') 19 | 20 | def get_bus_address(): 21 | """ lookup PA address """ 22 | return dbus.SessionBus()\ 23 | .get_object( 24 | 'org.PulseAudio1', 25 | '/org/pulseaudio/server_lookup1' 26 | ).Get( 27 | 'org.PulseAudio.ServerLookup1', 28 | 'Address', 29 | dbus_interface=dbus.PROPERTIES_IFACE 30 | ) 31 | 32 | def wait_for_pulseaudio (): 33 | """ wait for pulseaudio start (possibly forever) """ 34 | while call(['pulseaudio', '--check']) == 1: 35 | print ('pulseaudio is not running, retry...') 36 | sleep(1) 37 | 38 | def connect(srv_addr=None): 39 | """ 40 | load dbus-module into PA, lookup address and connect to the dbus interface 41 | """ 42 | 43 | # load dbus-module if not loaded already 44 | if call('pactl list modules short | grep module-dbus-protocol', shell=True) == 1: 45 | print('load dbus-module into PA') 46 | check_call(['pactl', 'load-module', 'module-dbus-protocol']) 47 | 48 | while not srv_addr: 49 | try: 50 | srv_addr = get_bus_address() 51 | print('Got pa-server address from dbus: {}'.format(srv_addr)) 52 | except dbus.exceptions.DBusException as err: 53 | if err.get_dbus_name() != 'org.freedesktop.DBus.Error.ServiceUnknown': 54 | raise 55 | print('cannot look up address!') 56 | sleep(1) 57 | 58 | return dbus.connection.Connection(srv_addr) 59 | 60 | 61 | def init (): 62 | """ connect to PA-DBus, set up event listeners and configure default sink eq """ 63 | global bus, core 64 | # connect to pulseaudio dbus 65 | wait_for_pulseaudio() 66 | bus = connect() 67 | core = bus.get_object(object_path='/org/pulseaudio/core1') 68 | bus.call_on_disconnection(on_disconnect) 69 | 70 | # listen for port change events i.e. headphone is plugged in or out 71 | bus.add_signal_receiver(on_port_change, 'ActivePortUpdated') 72 | core.ListenForSignal('org.PulseAudio.Core1.Device.ActivePortUpdated', dbus.Array(signature='o')) 73 | 74 | configure_default_sink() 75 | 76 | print('connected to pulseaudio') 77 | 78 | def configure_default_sink(): 79 | """ activates eq conf for current active sink """ 80 | # activate profile for current audio device and port 81 | fb_sink_addr = core.Get('org.PulseAudio.Core1', 'FallbackSink', dbus_interface=dbus.PROPERTIES_IFACE) 82 | fb_sink = bus.get_object(object_path=fb_sink_addr) 83 | fb_sink_name = getName(fb_sink, 'Device') 84 | try: 85 | port_addr = fb_sink.Get('org.PulseAudio.Core1.Device', 'ActivePort', dbus_interface=dbus.PROPERTIES_IFACE) 86 | port = bus.get_object(object_path=port_addr) 87 | port_name = port.Get('org.PulseAudio.Core1.DevicePort', 'Name', dbus_interface=dbus.PROPERTIES_IFACE) 88 | activate_profile(fb_sink_name, port_name) 89 | except dbus.exceptions.DBusException as e: 90 | print ("current device has no ports!") 91 | 92 | def on_disconnect (con): 93 | print ('disconnected from pulseaudio, try to reconnect...') 94 | init() 95 | 96 | class State: 97 | Clear, EventOccurred, End = range(3) 98 | 99 | burstState = State.Clear 100 | def end_burst(): 101 | """ apply last detected port change after some elapsed time """ 102 | global burstState 103 | print ("queue drained!") 104 | if burstState == State.EventOccurred: 105 | print ("event occurred, wait a bit...") 106 | burstState = State.End 107 | # give the event queue some time to collect more events 108 | sleep(debounceTime/1000) 109 | return True 110 | elif burstState == State.End: 111 | print ("now apply") 112 | apply_port_change(lastPortAddr) 113 | burstState = State.Clear 114 | return False 115 | 116 | 117 | lastPortAddr=None 118 | def on_port_change(port_addr): 119 | """ save last port change """ 120 | global burstState, lastPortAddr 121 | if burstState == State.Clear: 122 | GObject.idle_add(end_burst) 123 | burstState = State.EventOccurred 124 | lastPortAddr = port_addr 125 | 126 | 127 | def apply_port_change(port_addr): 128 | sink_addr = os.path.dirname(port_addr) 129 | sink = bus.get_object(object_path=sink_addr) 130 | sink_name = getName(sink, 'Device') 131 | 132 | port = bus.get_object(object_path=port_addr) 133 | port_name = getName(port, 'DevicePort') 134 | 135 | print ("change detected! new output port is '{}' on '{}'".format(port_name, sink_name)) 136 | 137 | activate_profile(sink_name, port_name) 138 | 139 | 140 | def getName (obj, itf): 141 | return obj.Get('org.PulseAudio.Core1.{}'.format(itf), 142 | 'Name', dbus_interface=dbus.PROPERTIES_IFACE) 143 | 144 | def make_conf_path(sink_name, port_name): 145 | """ get path to port-specific eq conf """ 146 | return os.path.join(config_dir, sink_name, port_name, 'equalizerrc') 147 | 148 | 149 | def activate_profile(sink_name, port_name): 150 | """ create symlink to port-specific eq conf """ 151 | # dump old configuration and save content 152 | os.system('pulseaudio-equalizer interface.getsettings') 153 | current_conf = open(eq_config_path).read() 154 | 155 | # remove old config 156 | try: 157 | os.remove(eq_config_path) 158 | except OSError: 159 | pass 160 | 161 | conf_file = make_conf_path(sink_name, port_name) 162 | conf_dir = os.path.dirname(conf_file) 163 | 164 | # create new config if it does not exist and fill with old config 165 | if not os.path.isdir(conf_dir): 166 | os.makedirs(conf_dir) 167 | open(conf_file, 'w').write(current_conf) 168 | 169 | # create symlink 170 | os.symlink(conf_file, eq_config_path) 171 | 172 | # apply new settings 173 | os.system('pulseaudio-equalizer interface.applysettings') 174 | 175 | 176 | DBusGMainLoop(set_as_default=True) 177 | loop = GObject.MainLoop() 178 | 179 | init() 180 | 181 | loop.run() 182 | --------------------------------------------------------------------------------