• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
1#!/usr/bin/env python3
2#    qpaeq is a equalizer interface for pulseaudio's equalizer sinks
3#    Copyright (C) 2009  Jason Newton <nevion@gmail.com
4#
5#    This program is free software: you can redistribute it and/or modify
6#    it under the terms of the GNU Lesser General Public License as
7#    published by the Free Software Foundation, either version 2.1 of the
8#    License, or (at your option) any later version.
9#
10#    This program is distributed in the hope that it will be useful,
11#    but WITHOUT ANY WARRANTY; without even the implied warranty of
12#    MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
13#    GNU Lesser General Public License for more details.
14#
15#    You should have received a copy of the GNU Lesser General Public License
16#    along with this program.  If not, see <http://www.gnu.org/licenses/>.
17
18
19import os,math,sys
20try:
21    from PyQt5 import QtWidgets,QtCore
22    import dbus.mainloop.pyqt5
23    import dbus
24except ImportError as e:
25    sys.stderr.write('There was an error importing needed libraries\n'
26                     'Make sure you have qt5 and dbus-python installed\n'
27                     'The error that occurred was:\n'
28                     '\t%s\n' % (str(e)))
29    sys.exit(-1)
30
31from functools import partial
32
33import signal
34signal.signal(signal.SIGINT, signal.SIG_DFL)
35SYNC_TIMEOUT = 4*1000
36
37CORE_PATH = "/org/pulseaudio/core1"
38CORE_IFACE = "org.PulseAudio.Core1"
39def connect():
40    try:
41        if 'PULSE_DBUS_SERVER' in os.environ:
42            address = os.environ['PULSE_DBUS_SERVER']
43        else:
44            bus = dbus.SessionBus() # Should be UserBus, but D-Bus doesn't implement that yet.
45            server_lookup = bus.get_object('org.PulseAudio1', '/org/pulseaudio/server_lookup1')
46            address = server_lookup.Get('org.PulseAudio.ServerLookup1', 'Address', dbus_interface='org.freedesktop.DBus.Properties')
47        return dbus.connection.Connection(address)
48    except Exception as e:
49        sys.stderr.write('There was an error connecting to pulseaudio, '
50                         'please make sure you have the pulseaudio dbus '
51                         'module loaded, exiting...\n')
52        sys.exit(-1)
53
54
55#TODO: signals: sink Filter changed, sink reconfigured (window size) (sink iface)
56#TODO: manager signals: new sink, removed sink, new profile, removed profile
57#TODO: add support for changing of window_size 1000-fft_size (adv option)
58#TODO: reconnect support loop 1 second trying to reconnect
59#TODO: just resample the filters for profiles when loading to different sizes
60#TODO: add preamp
61prop_iface='org.freedesktop.DBus.Properties'
62eq_iface='org.PulseAudio.Ext.Equalizing1.Equalizer'
63device_iface='org.PulseAudio.Core1.Device'
64class QPaeq(QtWidgets.QWidget):
65    manager_path='/org/pulseaudio/equalizing1'
66    manager_iface='org.PulseAudio.Ext.Equalizing1.Manager'
67    core_iface='org.PulseAudio.Core1'
68    core_path='/org/pulseaudio/core1'
69    module_name='module-equalizer-sink'
70
71    def __init__(self):
72        QtWidgets.QWidget.__init__(self)
73        self.setWindowTitle('qpaeq')
74        self.slider_widget=None
75        self.sink_name=None
76        self.filter_state=None
77
78        self.create_layout()
79
80        self.set_connection()
81        self.connect_to_sink(self.sinks[0])
82        self.set_callbacks()
83        self.setMinimumSize(self.sizeHint())
84
85    def create_layout(self):
86        self.main_layout=QtWidgets.QVBoxLayout()
87        self.setLayout(self.main_layout)
88        toprow_layout=QtWidgets.QHBoxLayout()
89        sizePolicy = QtWidgets.QSizePolicy(QtWidgets.QSizePolicy.Preferred, QtWidgets.QSizePolicy.Fixed)
90        sizePolicy.setHorizontalStretch(0)
91        sizePolicy.setVerticalStretch(0)
92        #sizePolicy.setHeightForWidth(self.profile_box.sizePolicy().hasHeightForWidth())
93
94        toprow_layout.addWidget(QtWidgets.QLabel('Sink'))
95        self.sink_box = QtWidgets.QComboBox()
96        self.sink_box.setSizePolicy(sizePolicy)
97        self.sink_box.setDuplicatesEnabled(False)
98        self.sink_box.setInsertPolicy(QtWidgets.QComboBox.InsertAlphabetically)
99        #self.sink_box.setSizeAdjustPolicy(QtWidgets.QComboBox.AdjustToContents)
100        toprow_layout.addWidget(self.sink_box)
101
102        toprow_layout.addWidget(QtWidgets.QLabel('Channel'))
103        self.channel_box = QtWidgets.QComboBox()
104        self.channel_box.setSizePolicy(sizePolicy)
105        toprow_layout.addWidget(self.channel_box)
106
107        toprow_layout.addWidget(QtWidgets.QLabel('Preset'))
108        self.profile_box = QtWidgets.QComboBox()
109        self.profile_box.setSizePolicy(sizePolicy)
110        self.profile_box.setInsertPolicy(QtWidgets.QComboBox.InsertAlphabetically)
111        #self.profile_box.setSizeAdjustPolicy(QtWidgets.QComboBox.AdjustToContents)
112        toprow_layout.addWidget(self.profile_box)
113
114        large_icon_size=self.style().pixelMetric(QtWidgets.QStyle.PM_LargeIconSize)
115        large_icon_size=QtCore.QSize(large_icon_size,large_icon_size)
116        save_profile=QtWidgets.QToolButton()
117        save_profile.setIcon(self.style().standardIcon(QtWidgets.QStyle.SP_DriveFDIcon))
118        save_profile.setIconSize(large_icon_size)
119        save_profile.setToolButtonStyle(QtCore.Qt.ToolButtonIconOnly)
120        save_profile.clicked.connect(self.save_profile)
121        remove_profile=QtWidgets.QToolButton()
122        remove_profile.setIcon(self.style().standardIcon(QtWidgets.QStyle.SP_TrashIcon))
123        remove_profile.setIconSize(large_icon_size)
124        remove_profile.setToolButtonStyle(QtCore.Qt.ToolButtonIconOnly)
125        remove_profile.clicked.connect(self.remove_profile)
126        toprow_layout.addWidget(save_profile)
127        toprow_layout.addWidget(remove_profile)
128
129        reset_button = QtWidgets.QPushButton('Reset')
130        reset_button.clicked.connect(self.reset)
131        toprow_layout.addStretch()
132        toprow_layout.addWidget(reset_button)
133        self.layout().addLayout(toprow_layout)
134
135        self.profile_box.activated.connect(self.load_profile)
136        self.channel_box.activated.connect(self.select_channel)
137    def connect_to_sink(self,name):
138        #TODO: clear slots for profile buttons
139
140        #flush any pending saves for other sinks
141        if self.filter_state is not None:
142            self.filter_state.flush_state()
143        sink=self.connection.get_object(object_path=name)
144        self.sink_props=dbus.Interface(sink,dbus_interface=prop_iface)
145        self.sink=dbus.Interface(sink,dbus_interface=eq_iface)
146        self.filter_state=FilterState(sink)
147        #sample_rate,filter_rate,channels,channel)
148
149        self.channel_box.clear()
150        self.channel_box.addItem('All',self.filter_state.channels)
151        for i in range(self.filter_state.channels):
152            self.channel_box.addItem('%d' %(i+1,),i)
153        self.setMinimumSize(self.sizeHint())
154
155        self.set_slider_widget(SliderArray(self.filter_state))
156
157        self.sink_name=name
158        #set the signal listener for this sink
159        core=self._get_core()
160        #temporary hack until signal filtering works properly
161        core.ListenForSignal('',[dbus.ObjectPath(self.sink_name),dbus.ObjectPath(self.manager_path)])
162        #for x in ['FilterChanged']:
163        #    core.ListenForSignal("%s.%s" %(self.eq_iface,x),[dbus.ObjectPath(self.sink_name)])
164        #core.ListenForSignal(self.eq_iface,[dbus.ObjectPath(self.sink_name)])
165        self.sink.connect_to_signal('FilterChanged',self.read_filter)
166
167    def set_slider_widget(self,widget):
168        layout=self.layout()
169        if self.slider_widget is not None:
170            i=layout.indexOf(self.slider_widget)
171            layout.removeWidget(self.slider_widget)
172            self.slider_widget.deleteLater()
173            layout.insertWidget(i,self.slider_widget)
174        else:
175            layout.addWidget(widget)
176        self.slider_widget=widget
177        self.read_filter()
178    def _get_core(self):
179        core_obj=self.connection.get_object(object_path=self.core_path)
180        core=dbus.Interface(core_obj,dbus_interface=self.core_iface)
181        return core
182    def sink_added(self,sink):
183        #TODO: preserve selected sink
184        self.update_sinks()
185    def sink_removed(self,sink):
186        #TODO: preserve selected sink, try connecting to backup otherwise
187        if sink==self.sink_name:
188            #connect to new sink?
189            pass
190        self.update_sinks()
191    def save_profile(self):
192        #popup dialog box for name
193        current=self.profile_box.currentIndex()
194        profile,ok=QtWidgets.QInputDialog.getItem(self,'Preset Name','Preset',self.profiles,current)
195        if not ok or profile=='':
196            return
197        if profile in self.profiles:
198            mbox=QtWidgets.QMessageBox(self)
199            mbox.setText('%s preset already exists'%(profile,))
200            mbox.setInformativeText('Do you want to save over it?')
201            mbox.setStandardButtons(mbox.Save|mbox.Discard|mbox.Cancel)
202            mbox.setDefaultButton(mbox.Save)
203            ret=mbox.exec_()
204            if ret!=mbox.Save:
205                return
206        self.sink.SaveProfile(self.filter_state.channel,dbus.String(profile))
207        if self.filter_state.channel==self.filter_state.channels:
208            for x in range(1,self.filter_state.channels):
209                self.sink.LoadProfile(x,dbus.String(profile))
210    def remove_profile(self):
211        #find active profile name, remove it
212        profile=self.profile_box.currentText()
213        manager=dbus.Interface(self.manager_obj,dbus_interface=self.manager_iface)
214        manager.RemoveProfile(dbus.String(profile))
215    def load_profile(self,x):
216        profile=self.profile_box.itemText(x)
217        self.filter_state.load_profile(profile)
218    def select_channel(self,x):
219        self.filter_state.channel = self.channel_box.itemData(x)
220        self._set_profile_name()
221        self.filter_state.readback()
222
223    #TODO: add back in preamp!
224    #print(frequencies)
225    #main_layout.addLayout(self.create_slider(partial(self.update_coefficient,0),
226    #    'Preamp')[0]
227    #)
228    def set_connection(self):
229        self.connection=connect()
230
231        self.manager_obj=self.connection.get_object(object_path=self.manager_path)
232        manager_props=dbus.Interface(self.manager_obj,dbus_interface=prop_iface)
233        try:
234            self.sinks=manager_props.Get(self.manager_iface,'EqualizedSinks')
235        except dbus.exceptions.DBusException:
236            # probably module not yet loaded, try to load it:
237            try:
238                core=self.connection.get_object(object_path=self.core_path)
239                core.LoadModule(self.module_name,{},dbus_interface=self.core_iface)
240                # yup, we don't need to re-create manager_obj and manager_props,
241                # these are late-bound
242                self.sinks=manager_props.Get(self.manager_iface,'EqualizedSinks')
243            except dbus.exceptions.DBusException:
244                sys.stderr.write('It seems that running pulseaudio does not support '
245                                 'equalizer features and loading %s module failed.\n'
246                                 'Exiting...\n' % self.module_name)
247                sys.exit(-1)
248
249    def set_callbacks(self):
250        manager=dbus.Interface(self.manager_obj,dbus_interface=self.manager_iface)
251        manager.connect_to_signal('ProfilesChanged',self.update_profiles)
252        manager.connect_to_signal('SinkAdded',self.sink_added)
253        manager.connect_to_signal('SinkRemoved',self.sink_removed)
254        #self._get_core().ListenForSignal(self.manager_iface,[])
255        #self._get_core().ListenForSignal(self.manager_iface,[dbus.ObjectPath(self.manager_path)])
256        #core=self._get_core()
257        #for x in ['ProfilesChanged','SinkAdded','SinkRemoved']:
258        #    core.ListenForSignal("%s.%s" %(self.manager_iface,x),[dbus.ObjectPath(self.manager_path)])
259        self.update_profiles()
260        self.update_sinks()
261    def update_profiles(self):
262        #print('update profiles called!')
263        manager_props=dbus.Interface(self.manager_obj,dbus_interface=prop_iface)
264        self.profiles=manager_props.Get(self.manager_iface,'Profiles')
265        self.profile_box.blockSignals(True)
266        self.profile_box.clear()
267        self.profile_box.addItems(self.profiles)
268        self.profile_box.blockSignals(False)
269        self._set_profile_name()
270    def update_sinks(self):
271        self.sink_box.blockSignals(True)
272        self.sink_box.clear()
273        for x in self.sinks:
274            sink=self.connection.get_object(object_path=x)
275            sink_props=dbus.Interface(sink,dbus_interface=prop_iface)
276            simple_name=sink_props.Get(device_iface,'Name')
277            self.sink_box.addItem(simple_name,x)
278        self.sink_box.blockSignals(False)
279        self.sink_box.setMinimumSize(self.sink_box.sizeHint())
280    def read_filter(self):
281        #print(self.filter_frequencies)
282        self.filter_state.readback()
283    def reset(self):
284        coefs=dbus.Array([1/math.sqrt(2.0)]*(self.filter_state.filter_rate//2+1))
285        preamp=1.0
286        self.filter_state.set_filter(preamp,coefs)
287    def _set_profile_name(self):
288        self.profile_box.blockSignals(True)
289        profile_name=self.sink.BaseProfile(self.filter_state.channel)
290        if profile_name is not None:
291            i=self.profile_box.findText(profile_name)
292            if i>=0:
293                self.profile_box.setCurrentIndex(i)
294        self.profile_box.blockSignals(False)
295
296
297class SliderArray(QtWidgets.QWidget):
298    def __init__(self,filter_state,parent=None):
299        super(SliderArray,self).__init__(parent)
300        #self.setStyleSheet('padding: 0px; border-width: 0px; margin: 0px;')
301        #self.setStyleSheet('font-family: monospace;'+outline%('blue'))
302        self.filter_state=filter_state
303        self.setLayout(QtWidgets.QHBoxLayout())
304        self.sub_array=None
305        self.set_sub_array(SliderArraySub(self.filter_state))
306        self.inhibit_resize=0
307    def set_sub_array(self,widget):
308        if self.sub_array is not None:
309            self.layout().removeWidget(self.sub_array)
310            self.sub_array.disconnect_signals()
311            self.sub_array.deleteLater()
312        self.sub_array=widget
313        self.layout().addWidget(self.sub_array)
314        self.sub_array.connect_signals()
315        self.filter_state.readback()
316    def resizeEvent(self,event):
317        super(SliderArray,self).resizeEvent(event)
318        if self.inhibit_resize==0:
319            self.inhibit_resize+=1
320            #self.add_sliders_to_fit()
321            t=QtCore.QTimer(self)
322            t.setSingleShot(True)
323            t.setInterval(0)
324            t.timeout.connect(partial(self.add_sliders_to_fit,event))
325            t.start()
326    def add_sliders_to_fit(self,event):
327        if event.oldSize().width()>0 and event.size().width()>0:
328            i=len(self.filter_state.frequencies)*int(round(float(event.size().width())/event.oldSize().width()))
329        else:
330            i=len(self.filter_state.frequencies)
331
332        t_w=self.size().width()
333        def evaluate(filter_state, target, variable):
334            base_freqs=self.filter_state.freq_proper(self.filter_state.DEFAULT_FREQUENCIES)
335            filter_state._set_frequency_values(subdivide(base_freqs,variable))
336            new_widget=SliderArraySub(filter_state)
337            w=new_widget.sizeHint().width()
338            return w-target
339        def searcher(initial,evaluator):
340            i=initial
341            def d(e): return 1 if e>=0 else -1
342            error=evaluator(i)
343            old_direction=d(error)
344            i-=old_direction
345            while True:
346                error=evaluator(i)
347                direction=d(error)
348                if direction!=old_direction:
349                    k=i-1
350                    #while direction<0 and error!=0:
351                    #    k-=1
352                    #    error=evaluator(i)
353                    #    direction=d(error)
354                    return k, evaluator(k)
355                i-=direction
356                old_direction=direction
357        searcher(i,partial(evaluate,self.filter_state,t_w))
358        self.set_sub_array(SliderArraySub(self.filter_state))
359        self.inhibit_resize-=1
360
361class SliderArraySub(QtWidgets.QWidget):
362    def __init__(self,filter_state,parent=None):
363        super(SliderArraySub,self).__init__(parent)
364        self.filter_state=filter_state
365        self.setLayout(QtWidgets.QGridLayout())
366        self.slider=[None]*len(self.filter_state.frequencies)
367        self.label=[None]*len(self.slider)
368        #self.setStyleSheet('padding: 0px; border-width: 0px; margin: 0px;')
369        #self.setStyleSheet('font-family: monospace;'+outline%('blue'))
370        qt=QtCore.Qt
371        #self.layout().setHorizontalSpacing(1)
372        def add_slider(slider,label, c):
373            self.layout().addWidget(slider,0,c,qt.AlignHCenter)
374            self.layout().addWidget(label,1,c,qt.AlignHCenter)
375            self.layout().setColumnMinimumWidth(c,max(label.sizeHint().width(),slider.sizeHint().width()))
376        def create_slider(slider_label):
377            slider=QtWidgets.QSlider(QtCore.Qt.Vertical,self)
378            label=SliderLabel(slider_label,filter_state,self)
379            slider.setRange(-1000,2000)
380            slider.setSingleStep(1)
381            return (slider,label)
382        self.preamp_slider,self.preamp_label=create_slider('Preamp')
383        add_slider(self.preamp_slider,self.preamp_label,0)
384        for i,hz in enumerate(self.filter_state.frequencies):
385            slider,label=create_slider(self.hz2label(hz))
386            self.slider[i]=slider
387            #slider.setStyleSheet('font-family: monospace;'+outline%('red',))
388            self.label[i]=label
389            c=i+1
390            add_slider(slider,label,i+1)
391    def hz2label(self, hz):
392        if hz==0:
393            label_text='DC'
394        elif hz==self.filter_state.sample_rate//2:
395            label_text='Coda'
396        else:
397            label_text=hz2str(hz)
398        return label_text
399
400    def connect_signals(self):
401        def connect(writer,reader,slider,label):
402            slider.valueChanged.connect(writer)
403            self.filter_state.readFilter.connect(reader)
404            label_cb=partial(slider.setValue,0)
405            label.clicked.connect(label_cb)
406            return label_cb
407
408        self.preamp_writer_cb=self.write_preamp
409        self.preamp_reader_cb=self.sync_preamp
410        self.preamp_label_cb=connect(self.preamp_writer_cb,
411                self.preamp_reader_cb,
412                self.preamp_slider,
413                self.preamp_label)
414        self.writer_callbacks=[None]*len(self.slider)
415        self.reader_callbacks=[None]*len(self.slider)
416        self.label_callbacks=[None]*len(self.label)
417        for i in range(len(self.slider)):
418            self.writer_callbacks[i]=partial(self.write_coefficient,i)
419            self.reader_callbacks[i]=partial(self.sync_coefficient,i)
420            self.label_callbacks[i]=connect(self.writer_callbacks[i],
421                    self.reader_callbacks[i],
422                    self.slider[i],
423                    self.label[i])
424    def disconnect_signals(self):
425        def disconnect(writer,reader,label_cb,slider,label):
426            slider.valueChanged.disconnect(writer)
427            self.filter_state.readFilter.disconnect(reader)
428            label.clicked.disconnect(label_cb)
429        disconnect(self.preamp_writer_cb, self.preamp_reader_cb,
430                self.preamp_label_cb, self.preamp_slider, self.preamp_label)
431        for i in range(len(self.slider)):
432            disconnect(self.writer_callbacks[i],
433                    self.reader_callbacks[i],
434                    self.label_callbacks[i],
435                    self.slider[i],
436                    self.label[i])
437
438    def write_preamp(self, v):
439        self.filter_state.preamp=self.slider2coef(v)
440        self.filter_state.seed()
441    def sync_preamp(self):
442        self.preamp_slider.blockSignals(True)
443        self.preamp_slider.setValue(self.coef2slider(self.filter_state.preamp))
444        self.preamp_slider.blockSignals(False)
445
446
447    def write_coefficient(self,i,v):
448        self.filter_state.coefficients[i]=self.slider2coef(v)/math.sqrt(2.0)
449        self.filter_state.seed()
450    def sync_coefficient(self,i):
451        slider=self.slider[i]
452        slider.blockSignals(True)
453        slider.setValue(self.coef2slider(math.sqrt(2.0)*self.filter_state.coefficients[i]))
454        slider.blockSignals(False)
455    @staticmethod
456    def slider2coef(x):
457        return (1.0+(x/1000.0))
458    @staticmethod
459    def coef2slider(x):
460        return int((x-1.0)*1000)
461outline='border-width: 1px; border-style: solid; border-color: %s;'
462
463class SliderLabel(QtWidgets.QLabel):
464    clicked=QtCore.pyqtSignal()
465    def __init__(self,label_text,filter_state,parent=None):
466        super(SliderLabel,self).__init__(parent)
467        self.setStyleSheet('font-family: monospace;')
468        self.setText(label_text)
469        self.setMinimumSize(self.sizeHint())
470    def mouseDoubleClickEvent(self, event):
471        self.clicked.emit()
472        super(SliderLabel,self).mouseDoubleClickEvent(event)
473
474#until there are server side state savings, do it in the client but try and avoid
475#simulaneous broadcasting situations
476class FilterState(QtCore.QObject):
477    #DEFAULT_FREQUENCIES=map(float,[25,50,75,100,150,200,300,400,500,800,1e3,1.5e3,3e3,5e3,7e3,10e3,15e3,20e3])
478    DEFAULT_FREQUENCIES=[31.75,63.5,125,250,500,1e3,2e3,4e3,8e3,16e3]
479    readFilter=QtCore.pyqtSignal()
480    def __init__(self,sink):
481        super(FilterState,self).__init__()
482        self.sink_props=dbus.Interface(sink,dbus_interface=prop_iface)
483        self.sink=dbus.Interface(sink,dbus_interface=eq_iface)
484        self.sample_rate=self.get_eq_attr('SampleRate')
485        self.filter_rate=self.get_eq_attr('FilterSampleRate')
486        self.channels=self.get_eq_attr('NChannels')
487        self.channel=self.channels
488        self.set_frequency_values(self.DEFAULT_FREQUENCIES)
489        self.sync_timer=QtCore.QTimer()
490        self.sync_timer.setSingleShot(True)
491        self.sync_timer.timeout.connect(self.save_state)
492
493    def get_eq_attr(self,attr):
494        return self.sink_props.Get(eq_iface,attr)
495    def freq_proper(self,xs):
496        return [0]+xs+[self.sample_rate//2]
497    def _set_frequency_values(self,freqs):
498        self.frequencies=freqs
499        #print('base',self.frequencies)
500        self.filter_frequencies=[int(round(x)) for x in self.translate_rates(self.filter_rate,self.sample_rate,
501                    self.frequencies)]
502        self.coefficients=[0.0]*len(self.frequencies)
503        self.preamp=1.0
504    def set_frequency_values(self,freqs):
505        self._set_frequency_values(self.freq_proper(freqs))
506    @staticmethod
507    def translate_rates(dst,src,rates):
508        return list([x*dst/src for x in rates])
509    def seed(self):
510        self.sink.SeedFilter(self.channel,self.filter_frequencies,self.coefficients,self.preamp)
511        self.sync_timer.start(SYNC_TIMEOUT)
512    def readback(self):
513        coefs,preamp=self.sink.FilterAtPoints(self.channel,self.filter_frequencies)
514        self.coefficients=coefs
515        self.preamp=preamp
516        self.readFilter.emit()
517    def set_filter(self,preamp,coefs):
518        self.sink.SetFilter(self.channel,dbus.Array(coefs),self.preamp)
519        self.sync_timer.start(SYNC_TIMEOUT)
520    def save_state(self):
521        print('saving state')
522        self.sink.SaveState()
523    def load_profile(self,profile):
524        self.sink.LoadProfile(self.channel,dbus.String(profile))
525        self.sync_timer.start(SYNC_TIMEOUT)
526    def flush_state(self):
527        if self.sync_timer.isActive():
528            self.sync_timer.stop()
529            self.save_state()
530
531
532def safe_log(k,b):
533    i=0
534    while k//b!=0:
535        i+=1
536        k=k//b
537    return i
538def hz2str(hz):
539    p=safe_log(hz,10.0)
540    if p<3:
541        return '%dHz' %(hz,)
542    elif hz%1000==0:
543        return '%dKHz' %(hz/(10.0**3),)
544    else:
545        return '%.1fKHz' %(hz/(10.0**3),)
546
547def subdivide(xs, t_points):
548    while len(xs)<t_points:
549        m=[0]*(2*len(xs)-1)
550        m[0:len(m):2]=xs
551        for i in range(1,len(m),2):
552            m[i]=(m[i-1]+m[i+1])//2
553        xs=m
554    p_drop=len(xs)-t_points
555    p_drop_left=p_drop//2
556    p_drop_right=p_drop-p_drop_left
557    #print('xs',xs)
558    #print('dropping %d, %d left, %d right' %(p_drop,p_drop_left,p_drop_right))
559    c=len(xs)//2
560    left=xs[0:p_drop_left*2:2]+xs[p_drop_left*2:c]
561    right=list(reversed(xs[c:]))
562    right=right[0:p_drop_right*2:2]+right[p_drop_right*2:]
563    right=list(reversed(right))
564    return left+right
565
566def main():
567    dbus.mainloop.pyqt5.DBusQtMainLoop(set_as_default=True)
568    app=QtWidgets.QApplication(sys.argv)
569    qpaeq_main=QPaeq()
570    qpaeq_main.show()
571    sys.exit(app.exec_())
572
573if __name__=='__main__':
574    main()
575