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