153a5a1b3Sopenharmony_ci#!/usr/bin/env python3 253a5a1b3Sopenharmony_ci# qpaeq is a equalizer interface for pulseaudio's equalizer sinks 353a5a1b3Sopenharmony_ci# Copyright (C) 2009 Jason Newton <nevion@gmail.com 453a5a1b3Sopenharmony_ci# 553a5a1b3Sopenharmony_ci# This program is free software: you can redistribute it and/or modify 653a5a1b3Sopenharmony_ci# it under the terms of the GNU Lesser General Public License as 753a5a1b3Sopenharmony_ci# published by the Free Software Foundation, either version 2.1 of the 853a5a1b3Sopenharmony_ci# License, or (at your option) any later version. 953a5a1b3Sopenharmony_ci# 1053a5a1b3Sopenharmony_ci# This program is distributed in the hope that it will be useful, 1153a5a1b3Sopenharmony_ci# but WITHOUT ANY WARRANTY; without even the implied warranty of 1253a5a1b3Sopenharmony_ci# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 1353a5a1b3Sopenharmony_ci# GNU Lesser General Public License for more details. 1453a5a1b3Sopenharmony_ci# 1553a5a1b3Sopenharmony_ci# You should have received a copy of the GNU Lesser General Public License 1653a5a1b3Sopenharmony_ci# along with this program. If not, see <http://www.gnu.org/licenses/>. 1753a5a1b3Sopenharmony_ci 1853a5a1b3Sopenharmony_ci 1953a5a1b3Sopenharmony_ciimport os,math,sys 2053a5a1b3Sopenharmony_citry: 2153a5a1b3Sopenharmony_ci from PyQt5 import QtWidgets,QtCore 2253a5a1b3Sopenharmony_ci import dbus.mainloop.pyqt5 2353a5a1b3Sopenharmony_ci import dbus 2453a5a1b3Sopenharmony_ciexcept ImportError as e: 2553a5a1b3Sopenharmony_ci sys.stderr.write('There was an error importing needed libraries\n' 2653a5a1b3Sopenharmony_ci 'Make sure you have qt5 and dbus-python installed\n' 2753a5a1b3Sopenharmony_ci 'The error that occurred was:\n' 2853a5a1b3Sopenharmony_ci '\t%s\n' % (str(e))) 2953a5a1b3Sopenharmony_ci sys.exit(-1) 3053a5a1b3Sopenharmony_ci 3153a5a1b3Sopenharmony_cifrom functools import partial 3253a5a1b3Sopenharmony_ci 3353a5a1b3Sopenharmony_ciimport signal 3453a5a1b3Sopenharmony_cisignal.signal(signal.SIGINT, signal.SIG_DFL) 3553a5a1b3Sopenharmony_ciSYNC_TIMEOUT = 4*1000 3653a5a1b3Sopenharmony_ci 3753a5a1b3Sopenharmony_ciCORE_PATH = "/org/pulseaudio/core1" 3853a5a1b3Sopenharmony_ciCORE_IFACE = "org.PulseAudio.Core1" 3953a5a1b3Sopenharmony_cidef connect(): 4053a5a1b3Sopenharmony_ci try: 4153a5a1b3Sopenharmony_ci if 'PULSE_DBUS_SERVER' in os.environ: 4253a5a1b3Sopenharmony_ci address = os.environ['PULSE_DBUS_SERVER'] 4353a5a1b3Sopenharmony_ci else: 4453a5a1b3Sopenharmony_ci bus = dbus.SessionBus() # Should be UserBus, but D-Bus doesn't implement that yet. 4553a5a1b3Sopenharmony_ci server_lookup = bus.get_object('org.PulseAudio1', '/org/pulseaudio/server_lookup1') 4653a5a1b3Sopenharmony_ci address = server_lookup.Get('org.PulseAudio.ServerLookup1', 'Address', dbus_interface='org.freedesktop.DBus.Properties') 4753a5a1b3Sopenharmony_ci return dbus.connection.Connection(address) 4853a5a1b3Sopenharmony_ci except Exception as e: 4953a5a1b3Sopenharmony_ci sys.stderr.write('There was an error connecting to pulseaudio, ' 5053a5a1b3Sopenharmony_ci 'please make sure you have the pulseaudio dbus ' 5153a5a1b3Sopenharmony_ci 'module loaded, exiting...\n') 5253a5a1b3Sopenharmony_ci sys.exit(-1) 5353a5a1b3Sopenharmony_ci 5453a5a1b3Sopenharmony_ci 5553a5a1b3Sopenharmony_ci#TODO: signals: sink Filter changed, sink reconfigured (window size) (sink iface) 5653a5a1b3Sopenharmony_ci#TODO: manager signals: new sink, removed sink, new profile, removed profile 5753a5a1b3Sopenharmony_ci#TODO: add support for changing of window_size 1000-fft_size (adv option) 5853a5a1b3Sopenharmony_ci#TODO: reconnect support loop 1 second trying to reconnect 5953a5a1b3Sopenharmony_ci#TODO: just resample the filters for profiles when loading to different sizes 6053a5a1b3Sopenharmony_ci#TODO: add preamp 6153a5a1b3Sopenharmony_ciprop_iface='org.freedesktop.DBus.Properties' 6253a5a1b3Sopenharmony_cieq_iface='org.PulseAudio.Ext.Equalizing1.Equalizer' 6353a5a1b3Sopenharmony_cidevice_iface='org.PulseAudio.Core1.Device' 6453a5a1b3Sopenharmony_ciclass QPaeq(QtWidgets.QWidget): 6553a5a1b3Sopenharmony_ci manager_path='/org/pulseaudio/equalizing1' 6653a5a1b3Sopenharmony_ci manager_iface='org.PulseAudio.Ext.Equalizing1.Manager' 6753a5a1b3Sopenharmony_ci core_iface='org.PulseAudio.Core1' 6853a5a1b3Sopenharmony_ci core_path='/org/pulseaudio/core1' 6953a5a1b3Sopenharmony_ci module_name='module-equalizer-sink' 7053a5a1b3Sopenharmony_ci 7153a5a1b3Sopenharmony_ci def __init__(self): 7253a5a1b3Sopenharmony_ci QtWidgets.QWidget.__init__(self) 7353a5a1b3Sopenharmony_ci self.setWindowTitle('qpaeq') 7453a5a1b3Sopenharmony_ci self.slider_widget=None 7553a5a1b3Sopenharmony_ci self.sink_name=None 7653a5a1b3Sopenharmony_ci self.filter_state=None 7753a5a1b3Sopenharmony_ci 7853a5a1b3Sopenharmony_ci self.create_layout() 7953a5a1b3Sopenharmony_ci 8053a5a1b3Sopenharmony_ci self.set_connection() 8153a5a1b3Sopenharmony_ci self.connect_to_sink(self.sinks[0]) 8253a5a1b3Sopenharmony_ci self.set_callbacks() 8353a5a1b3Sopenharmony_ci self.setMinimumSize(self.sizeHint()) 8453a5a1b3Sopenharmony_ci 8553a5a1b3Sopenharmony_ci def create_layout(self): 8653a5a1b3Sopenharmony_ci self.main_layout=QtWidgets.QVBoxLayout() 8753a5a1b3Sopenharmony_ci self.setLayout(self.main_layout) 8853a5a1b3Sopenharmony_ci toprow_layout=QtWidgets.QHBoxLayout() 8953a5a1b3Sopenharmony_ci sizePolicy = QtWidgets.QSizePolicy(QtWidgets.QSizePolicy.Preferred, QtWidgets.QSizePolicy.Fixed) 9053a5a1b3Sopenharmony_ci sizePolicy.setHorizontalStretch(0) 9153a5a1b3Sopenharmony_ci sizePolicy.setVerticalStretch(0) 9253a5a1b3Sopenharmony_ci #sizePolicy.setHeightForWidth(self.profile_box.sizePolicy().hasHeightForWidth()) 9353a5a1b3Sopenharmony_ci 9453a5a1b3Sopenharmony_ci toprow_layout.addWidget(QtWidgets.QLabel('Sink')) 9553a5a1b3Sopenharmony_ci self.sink_box = QtWidgets.QComboBox() 9653a5a1b3Sopenharmony_ci self.sink_box.setSizePolicy(sizePolicy) 9753a5a1b3Sopenharmony_ci self.sink_box.setDuplicatesEnabled(False) 9853a5a1b3Sopenharmony_ci self.sink_box.setInsertPolicy(QtWidgets.QComboBox.InsertAlphabetically) 9953a5a1b3Sopenharmony_ci #self.sink_box.setSizeAdjustPolicy(QtWidgets.QComboBox.AdjustToContents) 10053a5a1b3Sopenharmony_ci toprow_layout.addWidget(self.sink_box) 10153a5a1b3Sopenharmony_ci 10253a5a1b3Sopenharmony_ci toprow_layout.addWidget(QtWidgets.QLabel('Channel')) 10353a5a1b3Sopenharmony_ci self.channel_box = QtWidgets.QComboBox() 10453a5a1b3Sopenharmony_ci self.channel_box.setSizePolicy(sizePolicy) 10553a5a1b3Sopenharmony_ci toprow_layout.addWidget(self.channel_box) 10653a5a1b3Sopenharmony_ci 10753a5a1b3Sopenharmony_ci toprow_layout.addWidget(QtWidgets.QLabel('Preset')) 10853a5a1b3Sopenharmony_ci self.profile_box = QtWidgets.QComboBox() 10953a5a1b3Sopenharmony_ci self.profile_box.setSizePolicy(sizePolicy) 11053a5a1b3Sopenharmony_ci self.profile_box.setInsertPolicy(QtWidgets.QComboBox.InsertAlphabetically) 11153a5a1b3Sopenharmony_ci #self.profile_box.setSizeAdjustPolicy(QtWidgets.QComboBox.AdjustToContents) 11253a5a1b3Sopenharmony_ci toprow_layout.addWidget(self.profile_box) 11353a5a1b3Sopenharmony_ci 11453a5a1b3Sopenharmony_ci large_icon_size=self.style().pixelMetric(QtWidgets.QStyle.PM_LargeIconSize) 11553a5a1b3Sopenharmony_ci large_icon_size=QtCore.QSize(large_icon_size,large_icon_size) 11653a5a1b3Sopenharmony_ci save_profile=QtWidgets.QToolButton() 11753a5a1b3Sopenharmony_ci save_profile.setIcon(self.style().standardIcon(QtWidgets.QStyle.SP_DriveFDIcon)) 11853a5a1b3Sopenharmony_ci save_profile.setIconSize(large_icon_size) 11953a5a1b3Sopenharmony_ci save_profile.setToolButtonStyle(QtCore.Qt.ToolButtonIconOnly) 12053a5a1b3Sopenharmony_ci save_profile.clicked.connect(self.save_profile) 12153a5a1b3Sopenharmony_ci remove_profile=QtWidgets.QToolButton() 12253a5a1b3Sopenharmony_ci remove_profile.setIcon(self.style().standardIcon(QtWidgets.QStyle.SP_TrashIcon)) 12353a5a1b3Sopenharmony_ci remove_profile.setIconSize(large_icon_size) 12453a5a1b3Sopenharmony_ci remove_profile.setToolButtonStyle(QtCore.Qt.ToolButtonIconOnly) 12553a5a1b3Sopenharmony_ci remove_profile.clicked.connect(self.remove_profile) 12653a5a1b3Sopenharmony_ci toprow_layout.addWidget(save_profile) 12753a5a1b3Sopenharmony_ci toprow_layout.addWidget(remove_profile) 12853a5a1b3Sopenharmony_ci 12953a5a1b3Sopenharmony_ci reset_button = QtWidgets.QPushButton('Reset') 13053a5a1b3Sopenharmony_ci reset_button.clicked.connect(self.reset) 13153a5a1b3Sopenharmony_ci toprow_layout.addStretch() 13253a5a1b3Sopenharmony_ci toprow_layout.addWidget(reset_button) 13353a5a1b3Sopenharmony_ci self.layout().addLayout(toprow_layout) 13453a5a1b3Sopenharmony_ci 13553a5a1b3Sopenharmony_ci self.profile_box.activated.connect(self.load_profile) 13653a5a1b3Sopenharmony_ci self.channel_box.activated.connect(self.select_channel) 13753a5a1b3Sopenharmony_ci def connect_to_sink(self,name): 13853a5a1b3Sopenharmony_ci #TODO: clear slots for profile buttons 13953a5a1b3Sopenharmony_ci 14053a5a1b3Sopenharmony_ci #flush any pending saves for other sinks 14153a5a1b3Sopenharmony_ci if self.filter_state is not None: 14253a5a1b3Sopenharmony_ci self.filter_state.flush_state() 14353a5a1b3Sopenharmony_ci sink=self.connection.get_object(object_path=name) 14453a5a1b3Sopenharmony_ci self.sink_props=dbus.Interface(sink,dbus_interface=prop_iface) 14553a5a1b3Sopenharmony_ci self.sink=dbus.Interface(sink,dbus_interface=eq_iface) 14653a5a1b3Sopenharmony_ci self.filter_state=FilterState(sink) 14753a5a1b3Sopenharmony_ci #sample_rate,filter_rate,channels,channel) 14853a5a1b3Sopenharmony_ci 14953a5a1b3Sopenharmony_ci self.channel_box.clear() 15053a5a1b3Sopenharmony_ci self.channel_box.addItem('All',self.filter_state.channels) 15153a5a1b3Sopenharmony_ci for i in range(self.filter_state.channels): 15253a5a1b3Sopenharmony_ci self.channel_box.addItem('%d' %(i+1,),i) 15353a5a1b3Sopenharmony_ci self.setMinimumSize(self.sizeHint()) 15453a5a1b3Sopenharmony_ci 15553a5a1b3Sopenharmony_ci self.set_slider_widget(SliderArray(self.filter_state)) 15653a5a1b3Sopenharmony_ci 15753a5a1b3Sopenharmony_ci self.sink_name=name 15853a5a1b3Sopenharmony_ci #set the signal listener for this sink 15953a5a1b3Sopenharmony_ci core=self._get_core() 16053a5a1b3Sopenharmony_ci #temporary hack until signal filtering works properly 16153a5a1b3Sopenharmony_ci core.ListenForSignal('',[dbus.ObjectPath(self.sink_name),dbus.ObjectPath(self.manager_path)]) 16253a5a1b3Sopenharmony_ci #for x in ['FilterChanged']: 16353a5a1b3Sopenharmony_ci # core.ListenForSignal("%s.%s" %(self.eq_iface,x),[dbus.ObjectPath(self.sink_name)]) 16453a5a1b3Sopenharmony_ci #core.ListenForSignal(self.eq_iface,[dbus.ObjectPath(self.sink_name)]) 16553a5a1b3Sopenharmony_ci self.sink.connect_to_signal('FilterChanged',self.read_filter) 16653a5a1b3Sopenharmony_ci 16753a5a1b3Sopenharmony_ci def set_slider_widget(self,widget): 16853a5a1b3Sopenharmony_ci layout=self.layout() 16953a5a1b3Sopenharmony_ci if self.slider_widget is not None: 17053a5a1b3Sopenharmony_ci i=layout.indexOf(self.slider_widget) 17153a5a1b3Sopenharmony_ci layout.removeWidget(self.slider_widget) 17253a5a1b3Sopenharmony_ci self.slider_widget.deleteLater() 17353a5a1b3Sopenharmony_ci layout.insertWidget(i,self.slider_widget) 17453a5a1b3Sopenharmony_ci else: 17553a5a1b3Sopenharmony_ci layout.addWidget(widget) 17653a5a1b3Sopenharmony_ci self.slider_widget=widget 17753a5a1b3Sopenharmony_ci self.read_filter() 17853a5a1b3Sopenharmony_ci def _get_core(self): 17953a5a1b3Sopenharmony_ci core_obj=self.connection.get_object(object_path=self.core_path) 18053a5a1b3Sopenharmony_ci core=dbus.Interface(core_obj,dbus_interface=self.core_iface) 18153a5a1b3Sopenharmony_ci return core 18253a5a1b3Sopenharmony_ci def sink_added(self,sink): 18353a5a1b3Sopenharmony_ci #TODO: preserve selected sink 18453a5a1b3Sopenharmony_ci self.update_sinks() 18553a5a1b3Sopenharmony_ci def sink_removed(self,sink): 18653a5a1b3Sopenharmony_ci #TODO: preserve selected sink, try connecting to backup otherwise 18753a5a1b3Sopenharmony_ci if sink==self.sink_name: 18853a5a1b3Sopenharmony_ci #connect to new sink? 18953a5a1b3Sopenharmony_ci pass 19053a5a1b3Sopenharmony_ci self.update_sinks() 19153a5a1b3Sopenharmony_ci def save_profile(self): 19253a5a1b3Sopenharmony_ci #popup dialog box for name 19353a5a1b3Sopenharmony_ci current=self.profile_box.currentIndex() 19453a5a1b3Sopenharmony_ci profile,ok=QtWidgets.QInputDialog.getItem(self,'Preset Name','Preset',self.profiles,current) 19553a5a1b3Sopenharmony_ci if not ok or profile=='': 19653a5a1b3Sopenharmony_ci return 19753a5a1b3Sopenharmony_ci if profile in self.profiles: 19853a5a1b3Sopenharmony_ci mbox=QtWidgets.QMessageBox(self) 19953a5a1b3Sopenharmony_ci mbox.setText('%s preset already exists'%(profile,)) 20053a5a1b3Sopenharmony_ci mbox.setInformativeText('Do you want to save over it?') 20153a5a1b3Sopenharmony_ci mbox.setStandardButtons(mbox.Save|mbox.Discard|mbox.Cancel) 20253a5a1b3Sopenharmony_ci mbox.setDefaultButton(mbox.Save) 20353a5a1b3Sopenharmony_ci ret=mbox.exec_() 20453a5a1b3Sopenharmony_ci if ret!=mbox.Save: 20553a5a1b3Sopenharmony_ci return 20653a5a1b3Sopenharmony_ci self.sink.SaveProfile(self.filter_state.channel,dbus.String(profile)) 20753a5a1b3Sopenharmony_ci if self.filter_state.channel==self.filter_state.channels: 20853a5a1b3Sopenharmony_ci for x in range(1,self.filter_state.channels): 20953a5a1b3Sopenharmony_ci self.sink.LoadProfile(x,dbus.String(profile)) 21053a5a1b3Sopenharmony_ci def remove_profile(self): 21153a5a1b3Sopenharmony_ci #find active profile name, remove it 21253a5a1b3Sopenharmony_ci profile=self.profile_box.currentText() 21353a5a1b3Sopenharmony_ci manager=dbus.Interface(self.manager_obj,dbus_interface=self.manager_iface) 21453a5a1b3Sopenharmony_ci manager.RemoveProfile(dbus.String(profile)) 21553a5a1b3Sopenharmony_ci def load_profile(self,x): 21653a5a1b3Sopenharmony_ci profile=self.profile_box.itemText(x) 21753a5a1b3Sopenharmony_ci self.filter_state.load_profile(profile) 21853a5a1b3Sopenharmony_ci def select_channel(self,x): 21953a5a1b3Sopenharmony_ci self.filter_state.channel = self.channel_box.itemData(x) 22053a5a1b3Sopenharmony_ci self._set_profile_name() 22153a5a1b3Sopenharmony_ci self.filter_state.readback() 22253a5a1b3Sopenharmony_ci 22353a5a1b3Sopenharmony_ci #TODO: add back in preamp! 22453a5a1b3Sopenharmony_ci #print(frequencies) 22553a5a1b3Sopenharmony_ci #main_layout.addLayout(self.create_slider(partial(self.update_coefficient,0), 22653a5a1b3Sopenharmony_ci # 'Preamp')[0] 22753a5a1b3Sopenharmony_ci #) 22853a5a1b3Sopenharmony_ci def set_connection(self): 22953a5a1b3Sopenharmony_ci self.connection=connect() 23053a5a1b3Sopenharmony_ci 23153a5a1b3Sopenharmony_ci self.manager_obj=self.connection.get_object(object_path=self.manager_path) 23253a5a1b3Sopenharmony_ci manager_props=dbus.Interface(self.manager_obj,dbus_interface=prop_iface) 23353a5a1b3Sopenharmony_ci try: 23453a5a1b3Sopenharmony_ci self.sinks=manager_props.Get(self.manager_iface,'EqualizedSinks') 23553a5a1b3Sopenharmony_ci except dbus.exceptions.DBusException: 23653a5a1b3Sopenharmony_ci # probably module not yet loaded, try to load it: 23753a5a1b3Sopenharmony_ci try: 23853a5a1b3Sopenharmony_ci core=self.connection.get_object(object_path=self.core_path) 23953a5a1b3Sopenharmony_ci core.LoadModule(self.module_name,{},dbus_interface=self.core_iface) 24053a5a1b3Sopenharmony_ci # yup, we don't need to re-create manager_obj and manager_props, 24153a5a1b3Sopenharmony_ci # these are late-bound 24253a5a1b3Sopenharmony_ci self.sinks=manager_props.Get(self.manager_iface,'EqualizedSinks') 24353a5a1b3Sopenharmony_ci except dbus.exceptions.DBusException: 24453a5a1b3Sopenharmony_ci sys.stderr.write('It seems that running pulseaudio does not support ' 24553a5a1b3Sopenharmony_ci 'equalizer features and loading %s module failed.\n' 24653a5a1b3Sopenharmony_ci 'Exiting...\n' % self.module_name) 24753a5a1b3Sopenharmony_ci sys.exit(-1) 24853a5a1b3Sopenharmony_ci 24953a5a1b3Sopenharmony_ci def set_callbacks(self): 25053a5a1b3Sopenharmony_ci manager=dbus.Interface(self.manager_obj,dbus_interface=self.manager_iface) 25153a5a1b3Sopenharmony_ci manager.connect_to_signal('ProfilesChanged',self.update_profiles) 25253a5a1b3Sopenharmony_ci manager.connect_to_signal('SinkAdded',self.sink_added) 25353a5a1b3Sopenharmony_ci manager.connect_to_signal('SinkRemoved',self.sink_removed) 25453a5a1b3Sopenharmony_ci #self._get_core().ListenForSignal(self.manager_iface,[]) 25553a5a1b3Sopenharmony_ci #self._get_core().ListenForSignal(self.manager_iface,[dbus.ObjectPath(self.manager_path)]) 25653a5a1b3Sopenharmony_ci #core=self._get_core() 25753a5a1b3Sopenharmony_ci #for x in ['ProfilesChanged','SinkAdded','SinkRemoved']: 25853a5a1b3Sopenharmony_ci # core.ListenForSignal("%s.%s" %(self.manager_iface,x),[dbus.ObjectPath(self.manager_path)]) 25953a5a1b3Sopenharmony_ci self.update_profiles() 26053a5a1b3Sopenharmony_ci self.update_sinks() 26153a5a1b3Sopenharmony_ci def update_profiles(self): 26253a5a1b3Sopenharmony_ci #print('update profiles called!') 26353a5a1b3Sopenharmony_ci manager_props=dbus.Interface(self.manager_obj,dbus_interface=prop_iface) 26453a5a1b3Sopenharmony_ci self.profiles=manager_props.Get(self.manager_iface,'Profiles') 26553a5a1b3Sopenharmony_ci self.profile_box.blockSignals(True) 26653a5a1b3Sopenharmony_ci self.profile_box.clear() 26753a5a1b3Sopenharmony_ci self.profile_box.addItems(self.profiles) 26853a5a1b3Sopenharmony_ci self.profile_box.blockSignals(False) 26953a5a1b3Sopenharmony_ci self._set_profile_name() 27053a5a1b3Sopenharmony_ci def update_sinks(self): 27153a5a1b3Sopenharmony_ci self.sink_box.blockSignals(True) 27253a5a1b3Sopenharmony_ci self.sink_box.clear() 27353a5a1b3Sopenharmony_ci for x in self.sinks: 27453a5a1b3Sopenharmony_ci sink=self.connection.get_object(object_path=x) 27553a5a1b3Sopenharmony_ci sink_props=dbus.Interface(sink,dbus_interface=prop_iface) 27653a5a1b3Sopenharmony_ci simple_name=sink_props.Get(device_iface,'Name') 27753a5a1b3Sopenharmony_ci self.sink_box.addItem(simple_name,x) 27853a5a1b3Sopenharmony_ci self.sink_box.blockSignals(False) 27953a5a1b3Sopenharmony_ci self.sink_box.setMinimumSize(self.sink_box.sizeHint()) 28053a5a1b3Sopenharmony_ci def read_filter(self): 28153a5a1b3Sopenharmony_ci #print(self.filter_frequencies) 28253a5a1b3Sopenharmony_ci self.filter_state.readback() 28353a5a1b3Sopenharmony_ci def reset(self): 28453a5a1b3Sopenharmony_ci coefs=dbus.Array([1/math.sqrt(2.0)]*(self.filter_state.filter_rate//2+1)) 28553a5a1b3Sopenharmony_ci preamp=1.0 28653a5a1b3Sopenharmony_ci self.filter_state.set_filter(preamp,coefs) 28753a5a1b3Sopenharmony_ci def _set_profile_name(self): 28853a5a1b3Sopenharmony_ci self.profile_box.blockSignals(True) 28953a5a1b3Sopenharmony_ci profile_name=self.sink.BaseProfile(self.filter_state.channel) 29053a5a1b3Sopenharmony_ci if profile_name is not None: 29153a5a1b3Sopenharmony_ci i=self.profile_box.findText(profile_name) 29253a5a1b3Sopenharmony_ci if i>=0: 29353a5a1b3Sopenharmony_ci self.profile_box.setCurrentIndex(i) 29453a5a1b3Sopenharmony_ci self.profile_box.blockSignals(False) 29553a5a1b3Sopenharmony_ci 29653a5a1b3Sopenharmony_ci 29753a5a1b3Sopenharmony_ciclass SliderArray(QtWidgets.QWidget): 29853a5a1b3Sopenharmony_ci def __init__(self,filter_state,parent=None): 29953a5a1b3Sopenharmony_ci super(SliderArray,self).__init__(parent) 30053a5a1b3Sopenharmony_ci #self.setStyleSheet('padding: 0px; border-width: 0px; margin: 0px;') 30153a5a1b3Sopenharmony_ci #self.setStyleSheet('font-family: monospace;'+outline%('blue')) 30253a5a1b3Sopenharmony_ci self.filter_state=filter_state 30353a5a1b3Sopenharmony_ci self.setLayout(QtWidgets.QHBoxLayout()) 30453a5a1b3Sopenharmony_ci self.sub_array=None 30553a5a1b3Sopenharmony_ci self.set_sub_array(SliderArraySub(self.filter_state)) 30653a5a1b3Sopenharmony_ci self.inhibit_resize=0 30753a5a1b3Sopenharmony_ci def set_sub_array(self,widget): 30853a5a1b3Sopenharmony_ci if self.sub_array is not None: 30953a5a1b3Sopenharmony_ci self.layout().removeWidget(self.sub_array) 31053a5a1b3Sopenharmony_ci self.sub_array.disconnect_signals() 31153a5a1b3Sopenharmony_ci self.sub_array.deleteLater() 31253a5a1b3Sopenharmony_ci self.sub_array=widget 31353a5a1b3Sopenharmony_ci self.layout().addWidget(self.sub_array) 31453a5a1b3Sopenharmony_ci self.sub_array.connect_signals() 31553a5a1b3Sopenharmony_ci self.filter_state.readback() 31653a5a1b3Sopenharmony_ci def resizeEvent(self,event): 31753a5a1b3Sopenharmony_ci super(SliderArray,self).resizeEvent(event) 31853a5a1b3Sopenharmony_ci if self.inhibit_resize==0: 31953a5a1b3Sopenharmony_ci self.inhibit_resize+=1 32053a5a1b3Sopenharmony_ci #self.add_sliders_to_fit() 32153a5a1b3Sopenharmony_ci t=QtCore.QTimer(self) 32253a5a1b3Sopenharmony_ci t.setSingleShot(True) 32353a5a1b3Sopenharmony_ci t.setInterval(0) 32453a5a1b3Sopenharmony_ci t.timeout.connect(partial(self.add_sliders_to_fit,event)) 32553a5a1b3Sopenharmony_ci t.start() 32653a5a1b3Sopenharmony_ci def add_sliders_to_fit(self,event): 32753a5a1b3Sopenharmony_ci if event.oldSize().width()>0 and event.size().width()>0: 32853a5a1b3Sopenharmony_ci i=len(self.filter_state.frequencies)*int(round(float(event.size().width())/event.oldSize().width())) 32953a5a1b3Sopenharmony_ci else: 33053a5a1b3Sopenharmony_ci i=len(self.filter_state.frequencies) 33153a5a1b3Sopenharmony_ci 33253a5a1b3Sopenharmony_ci t_w=self.size().width() 33353a5a1b3Sopenharmony_ci def evaluate(filter_state, target, variable): 33453a5a1b3Sopenharmony_ci base_freqs=self.filter_state.freq_proper(self.filter_state.DEFAULT_FREQUENCIES) 33553a5a1b3Sopenharmony_ci filter_state._set_frequency_values(subdivide(base_freqs,variable)) 33653a5a1b3Sopenharmony_ci new_widget=SliderArraySub(filter_state) 33753a5a1b3Sopenharmony_ci w=new_widget.sizeHint().width() 33853a5a1b3Sopenharmony_ci return w-target 33953a5a1b3Sopenharmony_ci def searcher(initial,evaluator): 34053a5a1b3Sopenharmony_ci i=initial 34153a5a1b3Sopenharmony_ci def d(e): return 1 if e>=0 else -1 34253a5a1b3Sopenharmony_ci error=evaluator(i) 34353a5a1b3Sopenharmony_ci old_direction=d(error) 34453a5a1b3Sopenharmony_ci i-=old_direction 34553a5a1b3Sopenharmony_ci while True: 34653a5a1b3Sopenharmony_ci error=evaluator(i) 34753a5a1b3Sopenharmony_ci direction=d(error) 34853a5a1b3Sopenharmony_ci if direction!=old_direction: 34953a5a1b3Sopenharmony_ci k=i-1 35053a5a1b3Sopenharmony_ci #while direction<0 and error!=0: 35153a5a1b3Sopenharmony_ci # k-=1 35253a5a1b3Sopenharmony_ci # error=evaluator(i) 35353a5a1b3Sopenharmony_ci # direction=d(error) 35453a5a1b3Sopenharmony_ci return k, evaluator(k) 35553a5a1b3Sopenharmony_ci i-=direction 35653a5a1b3Sopenharmony_ci old_direction=direction 35753a5a1b3Sopenharmony_ci searcher(i,partial(evaluate,self.filter_state,t_w)) 35853a5a1b3Sopenharmony_ci self.set_sub_array(SliderArraySub(self.filter_state)) 35953a5a1b3Sopenharmony_ci self.inhibit_resize-=1 36053a5a1b3Sopenharmony_ci 36153a5a1b3Sopenharmony_ciclass SliderArraySub(QtWidgets.QWidget): 36253a5a1b3Sopenharmony_ci def __init__(self,filter_state,parent=None): 36353a5a1b3Sopenharmony_ci super(SliderArraySub,self).__init__(parent) 36453a5a1b3Sopenharmony_ci self.filter_state=filter_state 36553a5a1b3Sopenharmony_ci self.setLayout(QtWidgets.QGridLayout()) 36653a5a1b3Sopenharmony_ci self.slider=[None]*len(self.filter_state.frequencies) 36753a5a1b3Sopenharmony_ci self.label=[None]*len(self.slider) 36853a5a1b3Sopenharmony_ci #self.setStyleSheet('padding: 0px; border-width: 0px; margin: 0px;') 36953a5a1b3Sopenharmony_ci #self.setStyleSheet('font-family: monospace;'+outline%('blue')) 37053a5a1b3Sopenharmony_ci qt=QtCore.Qt 37153a5a1b3Sopenharmony_ci #self.layout().setHorizontalSpacing(1) 37253a5a1b3Sopenharmony_ci def add_slider(slider,label, c): 37353a5a1b3Sopenharmony_ci self.layout().addWidget(slider,0,c,qt.AlignHCenter) 37453a5a1b3Sopenharmony_ci self.layout().addWidget(label,1,c,qt.AlignHCenter) 37553a5a1b3Sopenharmony_ci self.layout().setColumnMinimumWidth(c,max(label.sizeHint().width(),slider.sizeHint().width())) 37653a5a1b3Sopenharmony_ci def create_slider(slider_label): 37753a5a1b3Sopenharmony_ci slider=QtWidgets.QSlider(QtCore.Qt.Vertical,self) 37853a5a1b3Sopenharmony_ci label=SliderLabel(slider_label,filter_state,self) 37953a5a1b3Sopenharmony_ci slider.setRange(-1000,2000) 38053a5a1b3Sopenharmony_ci slider.setSingleStep(1) 38153a5a1b3Sopenharmony_ci return (slider,label) 38253a5a1b3Sopenharmony_ci self.preamp_slider,self.preamp_label=create_slider('Preamp') 38353a5a1b3Sopenharmony_ci add_slider(self.preamp_slider,self.preamp_label,0) 38453a5a1b3Sopenharmony_ci for i,hz in enumerate(self.filter_state.frequencies): 38553a5a1b3Sopenharmony_ci slider,label=create_slider(self.hz2label(hz)) 38653a5a1b3Sopenharmony_ci self.slider[i]=slider 38753a5a1b3Sopenharmony_ci #slider.setStyleSheet('font-family: monospace;'+outline%('red',)) 38853a5a1b3Sopenharmony_ci self.label[i]=label 38953a5a1b3Sopenharmony_ci c=i+1 39053a5a1b3Sopenharmony_ci add_slider(slider,label,i+1) 39153a5a1b3Sopenharmony_ci def hz2label(self, hz): 39253a5a1b3Sopenharmony_ci if hz==0: 39353a5a1b3Sopenharmony_ci label_text='DC' 39453a5a1b3Sopenharmony_ci elif hz==self.filter_state.sample_rate//2: 39553a5a1b3Sopenharmony_ci label_text='Coda' 39653a5a1b3Sopenharmony_ci else: 39753a5a1b3Sopenharmony_ci label_text=hz2str(hz) 39853a5a1b3Sopenharmony_ci return label_text 39953a5a1b3Sopenharmony_ci 40053a5a1b3Sopenharmony_ci def connect_signals(self): 40153a5a1b3Sopenharmony_ci def connect(writer,reader,slider,label): 40253a5a1b3Sopenharmony_ci slider.valueChanged.connect(writer) 40353a5a1b3Sopenharmony_ci self.filter_state.readFilter.connect(reader) 40453a5a1b3Sopenharmony_ci label_cb=partial(slider.setValue,0) 40553a5a1b3Sopenharmony_ci label.clicked.connect(label_cb) 40653a5a1b3Sopenharmony_ci return label_cb 40753a5a1b3Sopenharmony_ci 40853a5a1b3Sopenharmony_ci self.preamp_writer_cb=self.write_preamp 40953a5a1b3Sopenharmony_ci self.preamp_reader_cb=self.sync_preamp 41053a5a1b3Sopenharmony_ci self.preamp_label_cb=connect(self.preamp_writer_cb, 41153a5a1b3Sopenharmony_ci self.preamp_reader_cb, 41253a5a1b3Sopenharmony_ci self.preamp_slider, 41353a5a1b3Sopenharmony_ci self.preamp_label) 41453a5a1b3Sopenharmony_ci self.writer_callbacks=[None]*len(self.slider) 41553a5a1b3Sopenharmony_ci self.reader_callbacks=[None]*len(self.slider) 41653a5a1b3Sopenharmony_ci self.label_callbacks=[None]*len(self.label) 41753a5a1b3Sopenharmony_ci for i in range(len(self.slider)): 41853a5a1b3Sopenharmony_ci self.writer_callbacks[i]=partial(self.write_coefficient,i) 41953a5a1b3Sopenharmony_ci self.reader_callbacks[i]=partial(self.sync_coefficient,i) 42053a5a1b3Sopenharmony_ci self.label_callbacks[i]=connect(self.writer_callbacks[i], 42153a5a1b3Sopenharmony_ci self.reader_callbacks[i], 42253a5a1b3Sopenharmony_ci self.slider[i], 42353a5a1b3Sopenharmony_ci self.label[i]) 42453a5a1b3Sopenharmony_ci def disconnect_signals(self): 42553a5a1b3Sopenharmony_ci def disconnect(writer,reader,label_cb,slider,label): 42653a5a1b3Sopenharmony_ci slider.valueChanged.disconnect(writer) 42753a5a1b3Sopenharmony_ci self.filter_state.readFilter.disconnect(reader) 42853a5a1b3Sopenharmony_ci label.clicked.disconnect(label_cb) 42953a5a1b3Sopenharmony_ci disconnect(self.preamp_writer_cb, self.preamp_reader_cb, 43053a5a1b3Sopenharmony_ci self.preamp_label_cb, self.preamp_slider, self.preamp_label) 43153a5a1b3Sopenharmony_ci for i in range(len(self.slider)): 43253a5a1b3Sopenharmony_ci disconnect(self.writer_callbacks[i], 43353a5a1b3Sopenharmony_ci self.reader_callbacks[i], 43453a5a1b3Sopenharmony_ci self.label_callbacks[i], 43553a5a1b3Sopenharmony_ci self.slider[i], 43653a5a1b3Sopenharmony_ci self.label[i]) 43753a5a1b3Sopenharmony_ci 43853a5a1b3Sopenharmony_ci def write_preamp(self, v): 43953a5a1b3Sopenharmony_ci self.filter_state.preamp=self.slider2coef(v) 44053a5a1b3Sopenharmony_ci self.filter_state.seed() 44153a5a1b3Sopenharmony_ci def sync_preamp(self): 44253a5a1b3Sopenharmony_ci self.preamp_slider.blockSignals(True) 44353a5a1b3Sopenharmony_ci self.preamp_slider.setValue(self.coef2slider(self.filter_state.preamp)) 44453a5a1b3Sopenharmony_ci self.preamp_slider.blockSignals(False) 44553a5a1b3Sopenharmony_ci 44653a5a1b3Sopenharmony_ci 44753a5a1b3Sopenharmony_ci def write_coefficient(self,i,v): 44853a5a1b3Sopenharmony_ci self.filter_state.coefficients[i]=self.slider2coef(v)/math.sqrt(2.0) 44953a5a1b3Sopenharmony_ci self.filter_state.seed() 45053a5a1b3Sopenharmony_ci def sync_coefficient(self,i): 45153a5a1b3Sopenharmony_ci slider=self.slider[i] 45253a5a1b3Sopenharmony_ci slider.blockSignals(True) 45353a5a1b3Sopenharmony_ci slider.setValue(self.coef2slider(math.sqrt(2.0)*self.filter_state.coefficients[i])) 45453a5a1b3Sopenharmony_ci slider.blockSignals(False) 45553a5a1b3Sopenharmony_ci @staticmethod 45653a5a1b3Sopenharmony_ci def slider2coef(x): 45753a5a1b3Sopenharmony_ci return (1.0+(x/1000.0)) 45853a5a1b3Sopenharmony_ci @staticmethod 45953a5a1b3Sopenharmony_ci def coef2slider(x): 46053a5a1b3Sopenharmony_ci return int((x-1.0)*1000) 46153a5a1b3Sopenharmony_cioutline='border-width: 1px; border-style: solid; border-color: %s;' 46253a5a1b3Sopenharmony_ci 46353a5a1b3Sopenharmony_ciclass SliderLabel(QtWidgets.QLabel): 46453a5a1b3Sopenharmony_ci clicked=QtCore.pyqtSignal() 46553a5a1b3Sopenharmony_ci def __init__(self,label_text,filter_state,parent=None): 46653a5a1b3Sopenharmony_ci super(SliderLabel,self).__init__(parent) 46753a5a1b3Sopenharmony_ci self.setStyleSheet('font-family: monospace;') 46853a5a1b3Sopenharmony_ci self.setText(label_text) 46953a5a1b3Sopenharmony_ci self.setMinimumSize(self.sizeHint()) 47053a5a1b3Sopenharmony_ci def mouseDoubleClickEvent(self, event): 47153a5a1b3Sopenharmony_ci self.clicked.emit() 47253a5a1b3Sopenharmony_ci super(SliderLabel,self).mouseDoubleClickEvent(event) 47353a5a1b3Sopenharmony_ci 47453a5a1b3Sopenharmony_ci#until there are server side state savings, do it in the client but try and avoid 47553a5a1b3Sopenharmony_ci#simulaneous broadcasting situations 47653a5a1b3Sopenharmony_ciclass FilterState(QtCore.QObject): 47753a5a1b3Sopenharmony_ci #DEFAULT_FREQUENCIES=map(float,[25,50,75,100,150,200,300,400,500,800,1e3,1.5e3,3e3,5e3,7e3,10e3,15e3,20e3]) 47853a5a1b3Sopenharmony_ci DEFAULT_FREQUENCIES=[31.75,63.5,125,250,500,1e3,2e3,4e3,8e3,16e3] 47953a5a1b3Sopenharmony_ci readFilter=QtCore.pyqtSignal() 48053a5a1b3Sopenharmony_ci def __init__(self,sink): 48153a5a1b3Sopenharmony_ci super(FilterState,self).__init__() 48253a5a1b3Sopenharmony_ci self.sink_props=dbus.Interface(sink,dbus_interface=prop_iface) 48353a5a1b3Sopenharmony_ci self.sink=dbus.Interface(sink,dbus_interface=eq_iface) 48453a5a1b3Sopenharmony_ci self.sample_rate=self.get_eq_attr('SampleRate') 48553a5a1b3Sopenharmony_ci self.filter_rate=self.get_eq_attr('FilterSampleRate') 48653a5a1b3Sopenharmony_ci self.channels=self.get_eq_attr('NChannels') 48753a5a1b3Sopenharmony_ci self.channel=self.channels 48853a5a1b3Sopenharmony_ci self.set_frequency_values(self.DEFAULT_FREQUENCIES) 48953a5a1b3Sopenharmony_ci self.sync_timer=QtCore.QTimer() 49053a5a1b3Sopenharmony_ci self.sync_timer.setSingleShot(True) 49153a5a1b3Sopenharmony_ci self.sync_timer.timeout.connect(self.save_state) 49253a5a1b3Sopenharmony_ci 49353a5a1b3Sopenharmony_ci def get_eq_attr(self,attr): 49453a5a1b3Sopenharmony_ci return self.sink_props.Get(eq_iface,attr) 49553a5a1b3Sopenharmony_ci def freq_proper(self,xs): 49653a5a1b3Sopenharmony_ci return [0]+xs+[self.sample_rate//2] 49753a5a1b3Sopenharmony_ci def _set_frequency_values(self,freqs): 49853a5a1b3Sopenharmony_ci self.frequencies=freqs 49953a5a1b3Sopenharmony_ci #print('base',self.frequencies) 50053a5a1b3Sopenharmony_ci self.filter_frequencies=[int(round(x)) for x in self.translate_rates(self.filter_rate,self.sample_rate, 50153a5a1b3Sopenharmony_ci self.frequencies)] 50253a5a1b3Sopenharmony_ci self.coefficients=[0.0]*len(self.frequencies) 50353a5a1b3Sopenharmony_ci self.preamp=1.0 50453a5a1b3Sopenharmony_ci def set_frequency_values(self,freqs): 50553a5a1b3Sopenharmony_ci self._set_frequency_values(self.freq_proper(freqs)) 50653a5a1b3Sopenharmony_ci @staticmethod 50753a5a1b3Sopenharmony_ci def translate_rates(dst,src,rates): 50853a5a1b3Sopenharmony_ci return list([x*dst/src for x in rates]) 50953a5a1b3Sopenharmony_ci def seed(self): 51053a5a1b3Sopenharmony_ci self.sink.SeedFilter(self.channel,self.filter_frequencies,self.coefficients,self.preamp) 51153a5a1b3Sopenharmony_ci self.sync_timer.start(SYNC_TIMEOUT) 51253a5a1b3Sopenharmony_ci def readback(self): 51353a5a1b3Sopenharmony_ci coefs,preamp=self.sink.FilterAtPoints(self.channel,self.filter_frequencies) 51453a5a1b3Sopenharmony_ci self.coefficients=coefs 51553a5a1b3Sopenharmony_ci self.preamp=preamp 51653a5a1b3Sopenharmony_ci self.readFilter.emit() 51753a5a1b3Sopenharmony_ci def set_filter(self,preamp,coefs): 51853a5a1b3Sopenharmony_ci self.sink.SetFilter(self.channel,dbus.Array(coefs),self.preamp) 51953a5a1b3Sopenharmony_ci self.sync_timer.start(SYNC_TIMEOUT) 52053a5a1b3Sopenharmony_ci def save_state(self): 52153a5a1b3Sopenharmony_ci print('saving state') 52253a5a1b3Sopenharmony_ci self.sink.SaveState() 52353a5a1b3Sopenharmony_ci def load_profile(self,profile): 52453a5a1b3Sopenharmony_ci self.sink.LoadProfile(self.channel,dbus.String(profile)) 52553a5a1b3Sopenharmony_ci self.sync_timer.start(SYNC_TIMEOUT) 52653a5a1b3Sopenharmony_ci def flush_state(self): 52753a5a1b3Sopenharmony_ci if self.sync_timer.isActive(): 52853a5a1b3Sopenharmony_ci self.sync_timer.stop() 52953a5a1b3Sopenharmony_ci self.save_state() 53053a5a1b3Sopenharmony_ci 53153a5a1b3Sopenharmony_ci 53253a5a1b3Sopenharmony_cidef safe_log(k,b): 53353a5a1b3Sopenharmony_ci i=0 53453a5a1b3Sopenharmony_ci while k//b!=0: 53553a5a1b3Sopenharmony_ci i+=1 53653a5a1b3Sopenharmony_ci k=k//b 53753a5a1b3Sopenharmony_ci return i 53853a5a1b3Sopenharmony_cidef hz2str(hz): 53953a5a1b3Sopenharmony_ci p=safe_log(hz,10.0) 54053a5a1b3Sopenharmony_ci if p<3: 54153a5a1b3Sopenharmony_ci return '%dHz' %(hz,) 54253a5a1b3Sopenharmony_ci elif hz%1000==0: 54353a5a1b3Sopenharmony_ci return '%dKHz' %(hz/(10.0**3),) 54453a5a1b3Sopenharmony_ci else: 54553a5a1b3Sopenharmony_ci return '%.1fKHz' %(hz/(10.0**3),) 54653a5a1b3Sopenharmony_ci 54753a5a1b3Sopenharmony_cidef subdivide(xs, t_points): 54853a5a1b3Sopenharmony_ci while len(xs)<t_points: 54953a5a1b3Sopenharmony_ci m=[0]*(2*len(xs)-1) 55053a5a1b3Sopenharmony_ci m[0:len(m):2]=xs 55153a5a1b3Sopenharmony_ci for i in range(1,len(m),2): 55253a5a1b3Sopenharmony_ci m[i]=(m[i-1]+m[i+1])//2 55353a5a1b3Sopenharmony_ci xs=m 55453a5a1b3Sopenharmony_ci p_drop=len(xs)-t_points 55553a5a1b3Sopenharmony_ci p_drop_left=p_drop//2 55653a5a1b3Sopenharmony_ci p_drop_right=p_drop-p_drop_left 55753a5a1b3Sopenharmony_ci #print('xs',xs) 55853a5a1b3Sopenharmony_ci #print('dropping %d, %d left, %d right' %(p_drop,p_drop_left,p_drop_right)) 55953a5a1b3Sopenharmony_ci c=len(xs)//2 56053a5a1b3Sopenharmony_ci left=xs[0:p_drop_left*2:2]+xs[p_drop_left*2:c] 56153a5a1b3Sopenharmony_ci right=list(reversed(xs[c:])) 56253a5a1b3Sopenharmony_ci right=right[0:p_drop_right*2:2]+right[p_drop_right*2:] 56353a5a1b3Sopenharmony_ci right=list(reversed(right)) 56453a5a1b3Sopenharmony_ci return left+right 56553a5a1b3Sopenharmony_ci 56653a5a1b3Sopenharmony_cidef main(): 56753a5a1b3Sopenharmony_ci dbus.mainloop.pyqt5.DBusQtMainLoop(set_as_default=True) 56853a5a1b3Sopenharmony_ci app=QtWidgets.QApplication(sys.argv) 56953a5a1b3Sopenharmony_ci qpaeq_main=QPaeq() 57053a5a1b3Sopenharmony_ci qpaeq_main.show() 57153a5a1b3Sopenharmony_ci sys.exit(app.exec_()) 57253a5a1b3Sopenharmony_ci 57353a5a1b3Sopenharmony_ciif __name__=='__main__': 57453a5a1b3Sopenharmony_ci main() 575