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