| #!/usr/bin/env python3 | |
| # qpaeq is a equalizer interface for pulseaudio's equalizer sinks | |
| # Copyright (C) 2009 Jason Newton <nevion@gmail.com | |
| # | |
| # This program is free software: you can redistribute it and/or modify | |
| # it under the terms of the GNU Lesser General Public License as | |
| # published by the Free Software Foundation, either version 2.1 of the | |
| # License, or (at your option) any later version. | |
| # | |
| # This program is distributed in the hope that it will be useful, | |
| # but WITHOUT ANY WARRANTY; without even the implied warranty of | |
| # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the | |
| # GNU Lesser General Public License for more details. | |
| # | |
| # You should have received a copy of the GNU Lesser General Public License | |
| # along with this program. If not, see <http://www.gnu.org/licenses/>. | |
| import os,math,sys | |
| try: | |
| from PyQt5 import QtWidgets,QtCore | |
| import dbus.mainloop.pyqt5 | |
| import dbus | |
| except ImportError as e: | |
| sys.stderr.write('There was an error importing needed libraries\n' | |
| 'Make sure you have qt5 and dbus-python installed\n' | |
| 'The error that occurred was:\n' | |
| '\t%s\n' % (str(e))) | |
| sys.exit(-1) | |
| from functools import partial | |
| import signal | |
| signal.signal(signal.SIGINT, signal.SIG_DFL) | |
| SYNC_TIMEOUT = 4*1000 | |
| CORE_PATH = "/org/pulseaudio/core1" | |
| CORE_IFACE = "org.PulseAudio.Core1" | |
| def connect(): | |
| try: | |
| if 'PULSE_DBUS_SERVER' in os.environ: | |
| address = os.environ['PULSE_DBUS_SERVER'] | |
| else: | |
| bus = dbus.SessionBus() # Should be UserBus, but D-Bus doesn't implement that yet. | |
| server_lookup = bus.get_object('org.PulseAudio1', '/org/pulseaudio/server_lookup1') | |
| address = server_lookup.Get('org.PulseAudio.ServerLookup1', 'Address', dbus_interface='org.freedesktop.DBus.Properties') | |
| return dbus.connection.Connection(address) | |
| except Exception as e: | |
| sys.stderr.write('There was an error connecting to pulseaudio, ' | |
| 'please make sure you have the pulseaudio dbus ' | |
| 'module loaded, exiting...\n') | |
| sys.exit(-1) | |
| #TODO: signals: sink Filter changed, sink reconfigured (window size) (sink iface) | |
| #TODO: manager signals: new sink, removed sink, new profile, removed profile | |
| #TODO: add support for changing of window_size 1000-fft_size (adv option) | |
| #TODO: reconnect support loop 1 second trying to reconnect | |
| #TODO: just resample the filters for profiles when loading to different sizes | |
| #TODO: add preamp | |
| prop_iface='org.freedesktop.DBus.Properties' | |
| eq_iface='org.PulseAudio.Ext.Equalizing1.Equalizer' | |
| device_iface='org.PulseAudio.Core1.Device' | |
| class QPaeq(QtWidgets.QWidget): | |
| manager_path='/org/pulseaudio/equalizing1' | |
| manager_iface='org.PulseAudio.Ext.Equalizing1.Manager' | |
| core_iface='org.PulseAudio.Core1' | |
| core_path='/org/pulseaudio/core1' | |
| module_name='module-equalizer-sink' | |
| def __init__(self): | |
| QtWidgets.QWidget.__init__(self) | |
| self.setWindowTitle('qpaeq') | |
| self.slider_widget=None | |
| self.sink_name=None | |
| self.filter_state=None | |
| self.create_layout() | |
| self.set_connection() | |
| self.connect_to_sink(self.sinks[0]) | |
| self.set_callbacks() | |
| self.setMinimumSize(self.sizeHint()) | |
| def create_layout(self): | |
| self.main_layout=QtWidgets.QVBoxLayout() | |
| self.setLayout(self.main_layout) | |
| toprow_layout=QtWidgets.QHBoxLayout() | |
| sizePolicy = QtWidgets.QSizePolicy(QtWidgets.QSizePolicy.Preferred, QtWidgets.QSizePolicy.Fixed) | |
| sizePolicy.setHorizontalStretch(0) | |
| sizePolicy.setVerticalStretch(0) | |
| #sizePolicy.setHeightForWidth(self.profile_box.sizePolicy().hasHeightForWidth()) | |
| toprow_layout.addWidget(QtWidgets.QLabel('Sink')) | |
| self.sink_box = QtWidgets.QComboBox() | |
| self.sink_box.setSizePolicy(sizePolicy) | |
| self.sink_box.setDuplicatesEnabled(False) | |
| self.sink_box.setInsertPolicy(QtWidgets.QComboBox.InsertAlphabetically) | |
| #self.sink_box.setSizeAdjustPolicy(QtWidgets.QComboBox.AdjustToContents) | |
| toprow_layout.addWidget(self.sink_box) | |
| toprow_layout.addWidget(QtWidgets.QLabel('Channel')) | |
| self.channel_box = QtWidgets.QComboBox() | |
| self.channel_box.setSizePolicy(sizePolicy) | |
| toprow_layout.addWidget(self.channel_box) | |
| toprow_layout.addWidget(QtWidgets.QLabel('Preset')) | |
| self.profile_box = QtWidgets.QComboBox() | |
| self.profile_box.setSizePolicy(sizePolicy) | |
| self.profile_box.setInsertPolicy(QtWidgets.QComboBox.InsertAlphabetically) | |
| #self.profile_box.setSizeAdjustPolicy(QtWidgets.QComboBox.AdjustToContents) | |
| toprow_layout.addWidget(self.profile_box) | |
| large_icon_size=self.style().pixelMetric(QtWidgets.QStyle.PM_LargeIconSize) | |
| large_icon_size=QtCore.QSize(large_icon_size,large_icon_size) | |
| save_profile=QtWidgets.QToolButton() | |
| save_profile.setIcon(self.style().standardIcon(QtWidgets.QStyle.SP_DriveFDIcon)) | |
| save_profile.setIconSize(large_icon_size) | |
| save_profile.setToolButtonStyle(QtCore.Qt.ToolButtonIconOnly) | |
| save_profile.clicked.connect(self.save_profile) | |
| remove_profile=QtWidgets.QToolButton() | |
| remove_profile.setIcon(self.style().standardIcon(QtWidgets.QStyle.SP_TrashIcon)) | |
| remove_profile.setIconSize(large_icon_size) | |
| remove_profile.setToolButtonStyle(QtCore.Qt.ToolButtonIconOnly) | |
| remove_profile.clicked.connect(self.remove_profile) | |
| toprow_layout.addWidget(save_profile) | |
| toprow_layout.addWidget(remove_profile) | |
| reset_button = QtWidgets.QPushButton('Reset') | |
| reset_button.clicked.connect(self.reset) | |
| toprow_layout.addStretch() | |
| toprow_layout.addWidget(reset_button) | |
| self.layout().addLayout(toprow_layout) | |
| self.profile_box.activated.connect(self.load_profile) | |
| self.channel_box.activated.connect(self.select_channel) | |
| def connect_to_sink(self,name): | |
| #TODO: clear slots for profile buttons | |
| #flush any pending saves for other sinks | |
| if self.filter_state is not None: | |
| self.filter_state.flush_state() | |
| sink=self.connection.get_object(object_path=name) | |
| self.sink_props=dbus.Interface(sink,dbus_interface=prop_iface) | |
| self.sink=dbus.Interface(sink,dbus_interface=eq_iface) | |
| self.filter_state=FilterState(sink) | |
| #sample_rate,filter_rate,channels,channel) | |
| self.channel_box.clear() | |
| self.channel_box.addItem('All',self.filter_state.channels) | |
| for i in range(self.filter_state.channels): | |
| self.channel_box.addItem('%d' %(i+1,),i) | |
| self.setMinimumSize(self.sizeHint()) | |
| self.set_slider_widget(SliderArray(self.filter_state)) | |
| self.sink_name=name | |
| #set the signal listener for this sink | |
| core=self._get_core() | |
| #temporary hack until signal filtering works properly | |
| core.ListenForSignal('',[dbus.ObjectPath(self.sink_name),dbus.ObjectPath(self.manager_path)]) | |
| #for x in ['FilterChanged']: | |
| # core.ListenForSignal("%s.%s" %(self.eq_iface,x),[dbus.ObjectPath(self.sink_name)]) | |
| #core.ListenForSignal(self.eq_iface,[dbus.ObjectPath(self.sink_name)]) | |
| self.sink.connect_to_signal('FilterChanged',self.read_filter) | |
| def set_slider_widget(self,widget): | |
| layout=self.layout() | |
| if self.slider_widget is not None: | |
| i=layout.indexOf(self.slider_widget) | |
| layout.removeWidget(self.slider_widget) | |
| self.slider_widget.deleteLater() | |
| layout.insertWidget(i,self.slider_widget) | |
| else: | |
| layout.addWidget(widget) | |
| self.slider_widget=widget | |
| self.read_filter() | |
| def _get_core(self): | |
| core_obj=self.connection.get_object(object_path=self.core_path) | |
| core=dbus.Interface(core_obj,dbus_interface=self.core_iface) | |
| return core | |
| def sink_added(self,sink): | |
| #TODO: preserve selected sink | |
| self.update_sinks() | |
| def sink_removed(self,sink): | |
| #TODO: preserve selected sink, try connecting to backup otherwise | |
| if sink==self.sink_name: | |
| #connect to new sink? | |
| pass | |
| self.update_sinks() | |
| def save_profile(self): | |
| #popup dialog box for name | |
| current=self.profile_box.currentIndex() | |
| profile,ok=QtWidgets.QInputDialog.getItem(self,'Preset Name','Preset',self.profiles,current) | |
| if not ok or profile=='': | |
| return | |
| if profile in self.profiles: | |
| mbox=QtWidgets.QMessageBox(self) | |
| mbox.setText('%s preset already exists'%(profile,)) | |
| mbox.setInformativeText('Do you want to save over it?') | |
| mbox.setStandardButtons(mbox.Save|mbox.Discard|mbox.Cancel) | |
| mbox.setDefaultButton(mbox.Save) | |
| ret=mbox.exec_() | |
| if ret!=mbox.Save: | |
| return | |
| self.sink.SaveProfile(self.filter_state.channel,dbus.String(profile)) | |
| if self.filter_state.channel==self.filter_state.channels: | |
| for x in range(1,self.filter_state.channels): | |
| self.sink.LoadProfile(x,dbus.String(profile)) | |
| def remove_profile(self): | |
| #find active profile name, remove it | |
| profile=self.profile_box.currentText() | |
| manager=dbus.Interface(self.manager_obj,dbus_interface=self.manager_iface) | |
| manager.RemoveProfile(dbus.String(profile)) | |
| def load_profile(self,x): | |
| profile=self.profile_box.itemText(x) | |
| self.filter_state.load_profile(profile) | |
| def select_channel(self,x): | |
| self.filter_state.channel = self.channel_box.itemData(x) | |
| self._set_profile_name() | |
| self.filter_state.readback() | |
| #TODO: add back in preamp! | |
| #print(frequencies) | |
| #main_layout.addLayout(self.create_slider(partial(self.update_coefficient,0), | |
| # 'Preamp')[0] | |
| #) | |
| def set_connection(self): | |
| self.connection=connect() | |
| self.manager_obj=self.connection.get_object(object_path=self.manager_path) | |
| manager_props=dbus.Interface(self.manager_obj,dbus_interface=prop_iface) | |
| try: | |
| self.sinks=manager_props.Get(self.manager_iface,'EqualizedSinks') | |
| except dbus.exceptions.DBusException: | |
| # probably module not yet loaded, try to load it: | |
| try: | |
| core=self.connection.get_object(object_path=self.core_path) | |
| core.LoadModule(self.module_name,{},dbus_interface=self.core_iface) | |
| # yup, we don't need to re-create manager_obj and manager_props, | |
| # these are late-bound | |
| self.sinks=manager_props.Get(self.manager_iface,'EqualizedSinks') | |
| except dbus.exceptions.DBusException: | |
| sys.stderr.write('It seems that running pulseaudio does not support ' | |
| 'equalizer features and loading %s module failed.\n' | |
| 'Exiting...\n' % self.module_name) | |
| sys.exit(-1) | |
| def set_callbacks(self): | |
| manager=dbus.Interface(self.manager_obj,dbus_interface=self.manager_iface) | |
| manager.connect_to_signal('ProfilesChanged',self.update_profiles) | |
| manager.connect_to_signal('SinkAdded',self.sink_added) | |
| manager.connect_to_signal('SinkRemoved',self.sink_removed) | |
| #self._get_core().ListenForSignal(self.manager_iface,[]) | |
| #self._get_core().ListenForSignal(self.manager_iface,[dbus.ObjectPath(self.manager_path)]) | |
| #core=self._get_core() | |
| #for x in ['ProfilesChanged','SinkAdded','SinkRemoved']: | |
| # core.ListenForSignal("%s.%s" %(self.manager_iface,x),[dbus.ObjectPath(self.manager_path)]) | |
| self.update_profiles() | |
| self.update_sinks() | |
| def update_profiles(self): | |
| #print('update profiles called!') | |
| manager_props=dbus.Interface(self.manager_obj,dbus_interface=prop_iface) | |
| self.profiles=manager_props.Get(self.manager_iface,'Profiles') | |
| self.profile_box.blockSignals(True) | |
| self.profile_box.clear() | |
| self.profile_box.addItems(self.profiles) | |
| self.profile_box.blockSignals(False) | |
| self._set_profile_name() | |
| def update_sinks(self): | |
| self.sink_box.blockSignals(True) | |
| self.sink_box.clear() | |
| for x in self.sinks: | |
| sink=self.connection.get_object(object_path=x) | |
| sink_props=dbus.Interface(sink,dbus_interface=prop_iface) | |
| simple_name=sink_props.Get(device_iface,'Name') | |
| self.sink_box.addItem(simple_name,x) | |
| self.sink_box.blockSignals(False) | |
| self.sink_box.setMinimumSize(self.sink_box.sizeHint()) | |
| def read_filter(self): | |
| #print(self.filter_frequencies) | |
| self.filter_state.readback() | |
| def reset(self): | |
| coefs=dbus.Array([1/math.sqrt(2.0)]*(self.filter_state.filter_rate//2+1)) | |
| preamp=1.0 | |
| self.filter_state.set_filter(preamp,coefs) | |
| def _set_profile_name(self): | |
| self.profile_box.blockSignals(True) | |
| profile_name=self.sink.BaseProfile(self.filter_state.channel) | |
| if profile_name is not None: | |
| i=self.profile_box.findText(profile_name) | |
| if i>=0: | |
| self.profile_box.setCurrentIndex(i) | |
| self.profile_box.blockSignals(False) | |
| class SliderArray(QtWidgets.QWidget): | |
| def __init__(self,filter_state,parent=None): | |
| super(SliderArray,self).__init__(parent) | |
| #self.setStyleSheet('padding: 0px; border-width: 0px; margin: 0px;') | |
| #self.setStyleSheet('font-family: monospace;'+outline%('blue')) | |
| self.filter_state=filter_state | |
| self.setLayout(QtWidgets.QHBoxLayout()) | |
| self.sub_array=None | |
| self.set_sub_array(SliderArraySub(self.filter_state)) | |
| self.inhibit_resize=0 | |
| def set_sub_array(self,widget): | |
| if self.sub_array is not None: | |
| self.layout().removeWidget(self.sub_array) | |
| self.sub_array.disconnect_signals() | |
| self.sub_array.deleteLater() | |
| self.sub_array=widget | |
| self.layout().addWidget(self.sub_array) | |
| self.sub_array.connect_signals() | |
| self.filter_state.readback() | |
| def resizeEvent(self,event): | |
| super(SliderArray,self).resizeEvent(event) | |
| if self.inhibit_resize==0: | |
| self.inhibit_resize+=1 | |
| #self.add_sliders_to_fit() | |
| t=QtCore.QTimer(self) | |
| t.setSingleShot(True) | |
| t.setInterval(0) | |
| t.timeout.connect(partial(self.add_sliders_to_fit,event)) | |
| t.start() | |
| def add_sliders_to_fit(self,event): | |
| if event.oldSize().width()>0 and event.size().width()>0: | |
| i=len(self.filter_state.frequencies)*int(round(float(event.size().width())/event.oldSize().width())) | |
| else: | |
| i=len(self.filter_state.frequencies) | |
| t_w=self.size().width() | |
| def evaluate(filter_state, target, variable): | |
| base_freqs=self.filter_state.freq_proper(self.filter_state.DEFAULT_FREQUENCIES) | |
| filter_state._set_frequency_values(subdivide(base_freqs,variable)) | |
| new_widget=SliderArraySub(filter_state) | |
| w=new_widget.sizeHint().width() | |
| return w-target | |
| def searcher(initial,evaluator): | |
| i=initial | |
| def d(e): return 1 if e>=0 else -1 | |
| error=evaluator(i) | |
| old_direction=d(error) | |
| i-=old_direction | |
| while True: | |
| error=evaluator(i) | |
| direction=d(error) | |
| if direction!=old_direction: | |
| k=i-1 | |
| #while direction<0 and error!=0: | |
| # k-=1 | |
| # error=evaluator(i) | |
| # direction=d(error) | |
| return k, evaluator(k) | |
| i-=direction | |
| old_direction=direction | |
| searcher(i,partial(evaluate,self.filter_state,t_w)) | |
| self.set_sub_array(SliderArraySub(self.filter_state)) | |
| self.inhibit_resize-=1 | |
| class SliderArraySub(QtWidgets.QWidget): | |
| def __init__(self,filter_state,parent=None): | |
| super(SliderArraySub,self).__init__(parent) | |
| self.filter_state=filter_state | |
| self.setLayout(QtWidgets.QGridLayout()) | |
| self.slider=[None]*len(self.filter_state.frequencies) | |
| self.label=[None]*len(self.slider) | |
| #self.setStyleSheet('padding: 0px; border-width: 0px; margin: 0px;') | |
| #self.setStyleSheet('font-family: monospace;'+outline%('blue')) | |
| qt=QtCore.Qt | |
| #self.layout().setHorizontalSpacing(1) | |
| def add_slider(slider,label, c): | |
| self.layout().addWidget(slider,0,c,qt.AlignHCenter) | |
| self.layout().addWidget(label,1,c,qt.AlignHCenter) | |
| self.layout().setColumnMinimumWidth(c,max(label.sizeHint().width(),slider.sizeHint().width())) | |
| def create_slider(slider_label): | |
| slider=QtWidgets.QSlider(QtCore.Qt.Vertical,self) | |
| label=SliderLabel(slider_label,filter_state,self) | |
| slider.setRange(-1000,2000) | |
| slider.setSingleStep(1) | |
| return (slider,label) | |
| self.preamp_slider,self.preamp_label=create_slider('Preamp') | |
| add_slider(self.preamp_slider,self.preamp_label,0) | |
| for i,hz in enumerate(self.filter_state.frequencies): | |
| slider,label=create_slider(self.hz2label(hz)) | |
| self.slider[i]=slider | |
| #slider.setStyleSheet('font-family: monospace;'+outline%('red',)) | |
| self.label[i]=label | |
| c=i+1 | |
| add_slider(slider,label,i+1) | |
| def hz2label(self, hz): | |
| if hz==0: | |
| label_text='DC' | |
| elif hz==self.filter_state.sample_rate//2: | |
| label_text='Coda' | |
| else: | |
| label_text=hz2str(hz) | |
| return label_text | |
| def connect_signals(self): | |
| def connect(writer,reader,slider,label): | |
| slider.valueChanged.connect(writer) | |
| self.filter_state.readFilter.connect(reader) | |
| label_cb=partial(slider.setValue,0) | |
| label.clicked.connect(label_cb) | |
| return label_cb | |
| self.preamp_writer_cb=self.write_preamp | |
| self.preamp_reader_cb=self.sync_preamp | |
| self.preamp_label_cb=connect(self.preamp_writer_cb, | |
| self.preamp_reader_cb, | |
| self.preamp_slider, | |
| self.preamp_label) | |
| self.writer_callbacks=[None]*len(self.slider) | |
| self.reader_callbacks=[None]*len(self.slider) | |
| self.label_callbacks=[None]*len(self.label) | |
| for i in range(len(self.slider)): | |
| self.writer_callbacks[i]=partial(self.write_coefficient,i) | |
| self.reader_callbacks[i]=partial(self.sync_coefficient,i) | |
| self.label_callbacks[i]=connect(self.writer_callbacks[i], | |
| self.reader_callbacks[i], | |
| self.slider[i], | |
| self.label[i]) | |
| def disconnect_signals(self): | |
| def disconnect(writer,reader,label_cb,slider,label): | |
| slider.valueChanged.disconnect(writer) | |
| self.filter_state.readFilter.disconnect(reader) | |
| label.clicked.disconnect(label_cb) | |
| disconnect(self.preamp_writer_cb, self.preamp_reader_cb, | |
| self.preamp_label_cb, self.preamp_slider, self.preamp_label) | |
| for i in range(len(self.slider)): | |
| disconnect(self.writer_callbacks[i], | |
| self.reader_callbacks[i], | |
| self.label_callbacks[i], | |
| self.slider[i], | |
| self.label[i]) | |
| def write_preamp(self, v): | |
| self.filter_state.preamp=self.slider2coef(v) | |
| self.filter_state.seed() | |
| def sync_preamp(self): | |
| self.preamp_slider.blockSignals(True) | |
| self.preamp_slider.setValue(self.coef2slider(self.filter_state.preamp)) | |
| self.preamp_slider.blockSignals(False) | |
| def write_coefficient(self,i,v): | |
| self.filter_state.coefficients[i]=self.slider2coef(v)/math.sqrt(2.0) | |
| self.filter_state.seed() | |
| def sync_coefficient(self,i): | |
| slider=self.slider[i] | |
| slider.blockSignals(True) | |
| slider.setValue(self.coef2slider(math.sqrt(2.0)*self.filter_state.coefficients[i])) | |
| slider.blockSignals(False) | |
| @staticmethod | |
| def slider2coef(x): | |
| return (1.0+(x/1000.0)) | |
| @staticmethod | |
| def coef2slider(x): | |
| return int((x-1.0)*1000) | |
| outline='border-width: 1px; border-style: solid; border-color: %s;' | |
| class SliderLabel(QtWidgets.QLabel): | |
| clicked=QtCore.pyqtSignal() | |
| def __init__(self,label_text,filter_state,parent=None): | |
| super(SliderLabel,self).__init__(parent) | |
| self.setStyleSheet('font-family: monospace;') | |
| self.setText(label_text) | |
| self.setMinimumSize(self.sizeHint()) | |
| def mouseDoubleClickEvent(self, event): | |
| self.clicked.emit() | |
| super(SliderLabel,self).mouseDoubleClickEvent(event) | |
| #until there are server side state savings, do it in the client but try and avoid | |
| #simulaneous broadcasting situations | |
| class FilterState(QtCore.QObject): | |
| #DEFAULT_FREQUENCIES=map(float,[25,50,75,100,150,200,300,400,500,800,1e3,1.5e3,3e3,5e3,7e3,10e3,15e3,20e3]) | |
| DEFAULT_FREQUENCIES=[31.75,63.5,125,250,500,1e3,2e3,4e3,8e3,16e3] | |
| readFilter=QtCore.pyqtSignal() | |
| def __init__(self,sink): | |
| super(FilterState,self).__init__() | |
| self.sink_props=dbus.Interface(sink,dbus_interface=prop_iface) | |
| self.sink=dbus.Interface(sink,dbus_interface=eq_iface) | |
| self.sample_rate=self.get_eq_attr('SampleRate') | |
| self.filter_rate=self.get_eq_attr('FilterSampleRate') | |
| self.channels=self.get_eq_attr('NChannels') | |
| self.channel=self.channels | |
| self.set_frequency_values(self.DEFAULT_FREQUENCIES) | |
| self.sync_timer=QtCore.QTimer() | |
| self.sync_timer.setSingleShot(True) | |
| self.sync_timer.timeout.connect(self.save_state) | |
| def get_eq_attr(self,attr): | |
| return self.sink_props.Get(eq_iface,attr) | |
| def freq_proper(self,xs): | |
| return [0]+xs+[self.sample_rate//2] | |
| def _set_frequency_values(self,freqs): | |
| self.frequencies=freqs | |
| #print('base',self.frequencies) | |
| self.filter_frequencies=[int(round(x)) for x in self.translate_rates(self.filter_rate,self.sample_rate, | |
| self.frequencies)] | |
| self.coefficients=[0.0]*len(self.frequencies) | |
| self.preamp=1.0 | |
| def set_frequency_values(self,freqs): | |
| self._set_frequency_values(self.freq_proper(freqs)) | |
| @staticmethod | |
| def translate_rates(dst,src,rates): | |
| return list([x*dst/src for x in rates]) | |
| def seed(self): | |
| self.sink.SeedFilter(self.channel,self.filter_frequencies,self.coefficients,self.preamp) | |
| self.sync_timer.start(SYNC_TIMEOUT) | |
| def readback(self): | |
| coefs,preamp=self.sink.FilterAtPoints(self.channel,self.filter_frequencies) | |
| self.coefficients=coefs | |
| self.preamp=preamp | |
| self.readFilter.emit() | |
| def set_filter(self,preamp,coefs): | |
| self.sink.SetFilter(self.channel,dbus.Array(coefs),self.preamp) | |
| self.sync_timer.start(SYNC_TIMEOUT) | |
| def save_state(self): | |
| print('saving state') | |
| self.sink.SaveState() | |
| def load_profile(self,profile): | |
| self.sink.LoadProfile(self.channel,dbus.String(profile)) | |
| self.sync_timer.start(SYNC_TIMEOUT) | |
| def flush_state(self): | |
| if self.sync_timer.isActive(): | |
| self.sync_timer.stop() | |
| self.save_state() | |
| def safe_log(k,b): | |
| i=0 | |
| while k//b!=0: | |
| i+=1 | |
| k=k//b | |
| return i | |
| def hz2str(hz): | |
| p=safe_log(hz,10.0) | |
| if p<3: | |
| return '%dHz' %(hz,) | |
| elif hz%1000==0: | |
| return '%dKHz' %(hz/(10.0**3),) | |
| else: | |
| return '%.1fKHz' %(hz/(10.0**3),) | |
| def subdivide(xs, t_points): | |
| while len(xs)<t_points: | |
| m=[0]*(2*len(xs)-1) | |
| m[0:len(m):2]=xs | |
| for i in range(1,len(m),2): | |
| m[i]=(m[i-1]+m[i+1])//2 | |
| xs=m | |
| p_drop=len(xs)-t_points | |
| p_drop_left=p_drop//2 | |
| p_drop_right=p_drop-p_drop_left | |
| #print('xs',xs) | |
| #print('dropping %d, %d left, %d right' %(p_drop,p_drop_left,p_drop_right)) | |
| c=len(xs)//2 | |
| left=xs[0:p_drop_left*2:2]+xs[p_drop_left*2:c] | |
| right=list(reversed(xs[c:])) | |
| right=right[0:p_drop_right*2:2]+right[p_drop_right*2:] | |
| right=list(reversed(right)) | |
| return left+right | |
| def main(): | |
| dbus.mainloop.pyqt5.DBusQtMainLoop(set_as_default=True) | |
| app=QtWidgets.QApplication(sys.argv) | |
| qpaeq_main=QPaeq() | |
| qpaeq_main.show() | |
| sys.exit(app.exec_()) | |
| if __name__=='__main__': | |
| main() |