2 # qpaeq is a equalizer interface for pulseaudio's equalizer sinks
3 # Copyright (C) 2009 Jason Newton <nevion@gmail.com
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.
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.
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/>.
22 from PyQt4
import QtGui
,QtCore
23 import dbus
.mainloop
.qt
26 print 'There was an error importing need libraries'
27 print 'Make sure you haveqt4 and dbus forthon installed'
28 print 'The error that occured was'
29 print '\t%s' %(str(e
))
33 from functools
import partial
36 signal
.signal(signal
.SIGINT
, signal
.SIG_DFL
)
39 CORE_PATH
= "/org/pulseaudio/core1"
40 CORE_IFACE
= "org.PulseAudio.Core1"
43 if 'PULSE_DBUS_SERVER' in os
.environ
:
44 address
= os
.environ
['PULSE_DBUS_SERVER']
46 bus
= dbus
.SessionBus() # Should be UserBus, but D-Bus doesn't implement that yet.
47 server_lookup
= bus
.get_object('org.PulseAudio1', '/org/pulseaudio/server_lookup1')
48 address
= server_lookup
.Get('org.PulseAudio.ServerLookup1', 'Address', dbus_interface
='org.freedesktop.DBus.Properties')
49 return dbus
.connection
.Connection(address
)
51 print 'There was an error connecting to pulseaudio, please make sure you have the pulseaudio dbus'
52 print 'and equalizer modules loaded, exiting...'
57 #TODO: signals: sink Filter changed, sink reconfigured (window size) (sink iface)
58 #TODO: manager signals: new sink, removed sink, new profile, removed profile
59 #TODO: add support for changing of window_size 1000-fft_size (adv option)
60 #TODO: reconnect support loop 1 second trying to reconnect
61 #TODO: just resample the filters for profiles when loading to different sizes
63 prop_iface
='org.freedesktop.DBus.Properties'
64 eq_iface
='org.PulseAudio.Ext.Equalizing1.Equalizer'
65 device_iface
='org.PulseAudio.Core1.Device'
66 class QPaeq(QtGui
.QWidget
):
67 manager_path
='/org/pulseaudio/equalizing1'
68 manager_iface
='org.PulseAudio.Ext.Equalizing1.Manager'
69 core_iface
='org.PulseAudio.Core1'
70 core_path
='/org/pulseaudio/core1'
72 QtGui
.QWidget
.__init
__(self
)
73 self
.setWindowTitle('qpaeq')
74 self
.slider_widget
=None
76 self
.filter_state
=None
81 self
.connect_to_sink(self
.sinks
[0])
83 self
.setMinimumSize(self
.sizeHint())
85 def create_layout(self
):
86 self
.main_layout
=QtGui
.QVBoxLayout()
87 self
.setLayout(self
.main_layout
)
88 toprow_layout
=QtGui
.QHBoxLayout()
89 sizePolicy
= QtGui
.QSizePolicy(QtGui
.QSizePolicy
.Preferred
, QtGui
.QSizePolicy
.Fixed
)
90 sizePolicy
.setHorizontalStretch(0)
91 sizePolicy
.setVerticalStretch(0)
92 #sizePolicy.setHeightForWidth(self.profile_box.sizePolicy().hasHeightForWidth())
94 toprow_layout
.addWidget(QtGui
.QLabel('Sink'))
95 self
.sink_box
= QtGui
.QComboBox()
96 self
.sink_box
.setSizePolicy(sizePolicy
)
97 self
.sink_box
.setDuplicatesEnabled(False)
98 self
.sink_box
.setInsertPolicy(QtGui
.QComboBox
.InsertAlphabetically
)
99 #self.sink_box.setSizeAdjustPolicy(QtGui.QComboBox.AdjustToContents)
100 toprow_layout
.addWidget(self
.sink_box
)
102 toprow_layout
.addWidget(QtGui
.QLabel('Channel'))
103 self
.channel_box
= QtGui
.QComboBox()
104 self
.channel_box
.setSizePolicy(sizePolicy
)
105 toprow_layout
.addWidget(self
.channel_box
)
107 toprow_layout
.addWidget(QtGui
.QLabel('Preset'))
108 self
.profile_box
= QtGui
.QComboBox()
109 self
.profile_box
.setSizePolicy(sizePolicy
)
110 self
.profile_box
.setInsertPolicy(QtGui
.QComboBox
.InsertAlphabetically
)
111 #self.profile_box.setSizeAdjustPolicy(QtGui.QComboBox.AdjustToContents)
112 toprow_layout
.addWidget(self
.profile_box
)
114 large_icon_size
=self
.style().pixelMetric(QtGui
.QStyle
.PM_LargeIconSize
)
115 large_icon_size
=QtCore
.QSize(large_icon_size
,large_icon_size
)
116 save_profile
=QtGui
.QToolButton()
117 save_profile
.setIcon(self
.style().standardIcon(QtGui
.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
=QtGui
.QToolButton()
122 remove_profile
.setIcon(self
.style().standardIcon(QtGui
.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
)
129 reset_button
= QtGui
.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
)
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
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)
149 self
.channel_box
.clear()
150 self
.channel_box
.addItem('All',self
.filter_state
.channels
)
151 for i
in xrange(self
.filter_state
.channels
):
152 self
.channel_box
.addItem('%d' %(i
+1,),i
)
153 self
.setMinimumSize(self
.sizeHint())
155 self
.set_slider_widget(SliderArray(self
.filter_state
))
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
)
167 def set_slider_widget(self
,widget
):
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
)
175 layout
.addWidget(widget
)
176 self
.slider_widget
=widget
179 core_obj
=self
.connection
.get_object(object_path
=self
.core_path
)
180 core
=dbus
.Interface(core_obj
,dbus_interface
=self
.core_iface
)
182 def sink_added(self
,sink
):
183 #TODO: preserve selected sink
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?
191 def save_profile(self
):
192 #popup dialog box for name
193 current
=self
.profile_box
.currentIndex()
194 profile
,ok
=QtGui
.QInputDialog
.getItem(self
,'Preset Name','Preset',self
.profiles
,current
)
195 if not ok
or profile
=='':
197 if profile
in self
.profiles
:
198 mbox
=QtGui
.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
)
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
).toPyObject()
220 self
._set
_profile
_name
()
221 self
.filter_state
.readback()
223 #TODO: add back in preamp!
225 #main_layout.addLayout(self.create_slider(partial(self.update_coefficient,0),
228 def set_connection(self
):
229 self
.connection
=connect()
230 self
.manager_obj
=self
.connection
.get_object(object_path
=self
.manager_path
)
231 manager_props
=dbus
.Interface(self
.manager_obj
,dbus_interface
=prop_iface
)
232 self
.sinks
=manager_props
.Get(self
.manager_iface
,'EqualizedSinks')
233 def set_callbacks(self
):
234 manager
=dbus
.Interface(self
.manager_obj
,dbus_interface
=self
.manager_iface
)
235 manager
.connect_to_signal('ProfilesChanged',self
.update_profiles
)
236 manager
.connect_to_signal('SinkAdded',self
.sink_added
)
237 manager
.connect_to_signal('SinkRemoved',self
.sink_removed
)
238 #self._get_core().ListenForSignal(self.manager_iface,[])
239 #self._get_core().ListenForSignal(self.manager_iface,[dbus.ObjectPath(self.manager_path)])
240 #core=self._get_core()
241 #for x in ['ProfilesChanged','SinkAdded','SinkRemoved']:
242 # core.ListenForSignal("%s.%s" %(self.manager_iface,x),[dbus.ObjectPath(self.manager_path)])
243 self
.update_profiles()
245 def update_profiles(self
):
246 #print 'update profiles called!'
247 manager_props
=dbus
.Interface(self
.manager_obj
,dbus_interface
=prop_iface
)
248 self
.profiles
=manager_props
.Get(self
.manager_iface
,'Profiles')
249 self
.profile_box
.blockSignals(True)
250 self
.profile_box
.clear()
251 self
.profile_box
.addItems(self
.profiles
)
252 self
.profile_box
.blockSignals(False)
253 self
._set
_profile
_name
()
254 def update_sinks(self
):
255 self
.sink_box
.blockSignals(True)
256 self
.sink_box
.clear()
258 sink
=self
.connection
.get_object(object_path
=x
)
259 sink_props
=dbus
.Interface(sink
,dbus_interface
=prop_iface
)
260 simple_name
=sink_props
.Get(device_iface
,'Name')
261 self
.sink_box
.addItem(simple_name
,x
)
262 self
.sink_box
.blockSignals(False)
263 self
.sink_box
.setMinimumSize(self
.sink_box
.sizeHint())
264 def read_filter(self
):
265 #print self.filter_frequencies
266 self
.filter_state
.readback()
268 coefs
=dbus
.Array([1/math
.sqrt(2.0)]*(self
.filter_state
.filter_rate
//2+1))
270 self
.filter_state
.set_filter(preamp
,coefs
)
271 def _set_profile_name(self
):
272 self
.profile_box
.blockSignals(True)
273 profile_name
=self
.sink
.BaseProfile(self
.filter_state
.channel
)
274 if profile_name
is not None:
275 i
=self
.profile_box
.findText(profile_name
)
277 self
.profile_box
.setCurrentIndex(i
)
278 self
.profile_box
.blockSignals(False)
281 class SliderArray(QtGui
.QWidget
):
282 def __init__(self
,filter_state
,parent
=None):
283 super(SliderArray
,self
).__init
__(parent
)
284 #self.setStyleSheet('padding: 0px; border-width: 0px; margin: 0px;')
285 #self.setStyleSheet('font-size: 7pt; font-family: monospace;'+outline%('blue'))
286 self
.filter_state
=filter_state
287 self
.setLayout(QtGui
.QHBoxLayout())
289 self
.set_sub_array(SliderArraySub(self
.filter_state
))
290 self
.inhibit_resize
=0
291 def set_sub_array(self
,widget
):
292 if self
.sub_array
is not None:
293 self
.layout().removeWidget(self
.sub_array
)
294 self
.sub_array
.disconnect_signals()
295 self
.sub_array
.deleteLater()
296 self
.sub_array
=widget
297 self
.layout().addWidget(self
.sub_array
)
298 self
.sub_array
.connect_signals()
299 self
.filter_state
.readback()
300 def resizeEvent(self
,event
):
301 super(SliderArray
,self
).resizeEvent(event
)
302 if self
.inhibit_resize
==0:
303 self
.inhibit_resize
+=1
304 #self.add_sliders_to_fit()
305 t
=QtCore
.QTimer(self
)
306 t
.setSingleShot(True)
308 t
.timeout
.connect(partial(self
.add_sliders_to_fit
,event
))
310 def add_sliders_to_fit(self
,event
):
311 if event
.oldSize().width()>0 and event
.size().width()>0:
312 i
=len(self
.filter_state
.frequencies
)*int(round(float(event
.size().width())/event
.oldSize().width()))
314 i
=len(self
.filter_state
.frequencies
)
316 t_w
=self
.size().width()
317 def evaluate(filter_state
, target
, variable
):
318 base_freqs
=self
.filter_state
.freq_proper(self
.filter_state
.DEFAULT_FREQUENCIES
)
319 filter_state
._set
_frequency
_values
(subdivide(base_freqs
,variable
))
320 new_widget
=SliderArraySub(filter_state
)
321 w
=new_widget
.sizeHint().width()
323 def searcher(initial
,evaluator
):
325 def d(e
): return 1 if e
>=0 else -1
327 old_direction
=d(error
)
332 if direction
!=old_direction
:
334 #while direction<0 and error!=0:
338 return k
, evaluator(k
)
340 old_direction
=direction
341 searcher(i
,partial(evaluate
,self
.filter_state
,t_w
))
342 self
.set_sub_array(SliderArraySub(self
.filter_state
))
343 self
.inhibit_resize
-=1
345 class SliderArraySub(QtGui
.QWidget
):
346 def __init__(self
,filter_state
,parent
=None):
347 super(SliderArraySub
,self
).__init
__(parent
)
348 self
.filter_state
=filter_state
349 self
.setLayout(QtGui
.QGridLayout())
350 self
.slider
=[None]*len(self
.filter_state
.frequencies
)
351 self
.label
=[None]*len(self
.slider
)
352 #self.setStyleSheet('padding: 0px; border-width: 0px; margin: 0px;')
353 #self.setStyleSheet('font-size: 7pt; font-family: monospace;'+outline%('blue'))
355 #self.layout().setHorizontalSpacing(1)
356 def add_slider(slider
,label
, c
):
357 self
.layout().addWidget(slider
,0,c
,qt
.AlignHCenter
)
358 self
.layout().addWidget(label
,1,c
,qt
.AlignHCenter
)
359 self
.layout().setColumnMinimumWidth(c
,max(label
.sizeHint().width(),slider
.sizeHint().width()))
360 def create_slider(slider_label
):
361 slider
=QtGui
.QSlider(QtCore
.Qt
.Vertical
,self
)
362 label
=SliderLabel(slider_label
,filter_state
,self
)
363 slider
.setRange(-1000,2000)
364 slider
.setSingleStep(1)
365 return (slider
,label
)
366 self
.preamp_slider
,self
.preamp_label
=create_slider('Preamp')
367 add_slider(self
.preamp_slider
,self
.preamp_label
,0)
368 for i
,hz
in enumerate(self
.filter_state
.frequencies
):
369 slider
,label
=create_slider(self
.hz2label(hz
))
370 self
.slider
[i
]=slider
371 #slider.setStyleSheet('font-size: 7pt; font-family: monospace;'+outline%('red',))
374 add_slider(slider
,label
,i
+1)
375 def hz2label(self
, hz
):
378 elif hz
==self
.filter_state
.sample_rate
//2:
381 label_text
=hz2str(hz
)
384 def connect_signals(self
):
385 def connect(writer
,reader
,slider
,label
):
386 slider
.valueChanged
.connect(writer
)
387 self
.filter_state
.readFilter
.connect(reader
)
388 label_cb
=partial(slider
.setValue
,0)
389 label
.clicked
.connect(label_cb
)
392 self
.preamp_writer_cb
=self
.write_preamp
393 self
.preamp_reader_cb
=self
.sync_preamp
394 self
.preamp_label_cb
=connect(self
.preamp_writer_cb
,
395 self
.preamp_reader_cb
,
398 self
.writer_callbacks
=[None]*len(self
.slider
)
399 self
.reader_callbacks
=[None]*len(self
.slider
)
400 self
.label_callbacks
=[None]*len(self
.label
)
401 for i
in range(len(self
.slider
)):
402 self
.writer_callbacks
[i
]=partial(self
.write_coefficient
,i
)
403 self
.reader_callbacks
[i
]=partial(self
.sync_coefficient
,i
)
404 self
.label_callbacks
[i
]=connect(self
.writer_callbacks
[i
],
405 self
.reader_callbacks
[i
],
408 def disconnect_signals(self
):
409 def disconnect(writer
,reader
,label_cb
,slider
,label
):
410 slider
.valueChanged
.disconnect(writer
)
411 self
.filter_state
.readFilter
.disconnect(reader
)
412 label
.clicked
.disconnect(label_cb
)
413 disconnect(self
.preamp_writer_cb
, self
.preamp_reader_cb
,
414 self
.preamp_label_cb
, self
.preamp_slider
, self
.preamp_label
)
415 for i
in range(len(self
.slider
)):
416 disconnect(self
.writer_callbacks
[i
],
417 self
.reader_callbacks
[i
],
418 self
.label_callbacks
[i
],
422 def write_preamp(self
, v
):
423 self
.filter_state
.preamp
=self
.slider2coef(v
)
424 self
.filter_state
.seed()
425 def sync_preamp(self
):
426 self
.preamp_slider
.blockSignals(True)
427 self
.preamp_slider
.setValue(self
.coef2slider(self
.filter_state
.preamp
))
428 self
.preamp_slider
.blockSignals(False)
431 def write_coefficient(self
,i
,v
):
432 self
.filter_state
.coefficients
[i
]=self
.slider2coef(v
)/math
.sqrt(2.0)
433 self
.filter_state
.seed()
434 def sync_coefficient(self
,i
):
435 slider
=self
.slider
[i
]
436 slider
.blockSignals(True)
437 slider
.setValue(self
.coef2slider(math
.sqrt(2.0)*self
.filter_state
.coefficients
[i
]))
438 slider
.blockSignals(False)
441 return (1.0+(x
/1000.0))
444 return int((x
-1.0)*1000)
445 outline
='border-width: 1px; border-style: solid; border-color: %s;'
447 class SliderLabel(QtGui
.QLabel
):
448 clicked
=QtCore
.pyqtSignal()
449 def __init__(self
,label_text
,filter_state
,parent
=None):
450 super(SliderLabel
,self
).__init
__(parent
)
451 self
.setStyleSheet('font-size: 7pt; font-family: monospace;')
452 self
.setText(label_text
)
453 self
.setMinimumSize(self
.sizeHint())
454 def mouseDoubleClickEvent(self
, event
):
456 super(SliderLabel
,self
).mouseDoubleClickEvent(event
)
458 #until there are server side state savings, do it in the client but try and avoid
459 #simulaneous broadcasting situations
460 class FilterState(QtCore
.QObject
):
461 #DEFAULT_FREQUENCIES=map(float,[25,50,75,100,150,200,300,400,500,800,1e3,1.5e3,3e3,5e3,7e3,10e3,15e3,20e3])
462 DEFAULT_FREQUENCIES
=[31.75,63.5,125,250,500,1e3
,2e3
,4e3
,8e3
,16e3
]
463 readFilter
=QtCore
.pyqtSignal()
464 def __init__(self
,sink
):
465 super(FilterState
,self
).__init
__()
466 self
.sink_props
=dbus
.Interface(sink
,dbus_interface
=prop_iface
)
467 self
.sink
=dbus
.Interface(sink
,dbus_interface
=eq_iface
)
468 self
.sample_rate
=self
.get_eq_attr('SampleRate')
469 self
.filter_rate
=self
.get_eq_attr('FilterSampleRate')
470 self
.channels
=self
.get_eq_attr('NChannels')
471 self
.channel
=self
.channels
472 self
.set_frequency_values(self
.DEFAULT_FREQUENCIES
)
473 self
.sync_timer
=QtCore
.QTimer()
474 self
.sync_timer
.setSingleShot(True)
475 self
.sync_timer
.timeout
.connect(self
.save_state
)
477 def get_eq_attr(self
,attr
):
478 return self
.sink_props
.Get(eq_iface
,attr
)
479 def freq_proper(self
,xs
):
480 return [0]+xs
+[self
.sample_rate
//2]
481 def _set_frequency_values(self
,freqs
):
482 self
.frequencies
=freqs
483 #print 'base',self.frequencies
484 self
.filter_frequencies
=map(lambda x
: int(round(x
)), \
485 self
.translate_rates(self
.filter_rate
,self
.sample_rate
,
488 self
.coefficients
=[0.0]*len(self
.frequencies
)
490 def set_frequency_values(self
,freqs
):
491 self
._set
_frequency
_values
(self
.freq_proper(freqs
))
493 def translate_rates(dst
,src
,rates
):
494 return list(map(lambda x
: x
*dst
/src
,rates
))
496 self
.sink
.SeedFilter(self
.channel
,self
.filter_frequencies
,self
.coefficients
,self
.preamp
)
497 self
.sync_timer
.start(SYNC_TIMEOUT
)
499 coefs
,preamp
=self
.sink
.FilterAtPoints(self
.channel
,self
.filter_frequencies
)
500 self
.coefficients
=coefs
502 self
.readFilter
.emit()
503 def set_filter(self
,preamp
,coefs
):
504 self
.sink
.SetFilter(self
.channel
,dbus
.Array(coefs
),self
.preamp
)
505 self
.sync_timer
.start(SYNC_TIMEOUT
)
506 def save_state(self
):
508 self
.sink
.SaveState()
509 def load_profile(self
,profile
):
510 self
.sink
.LoadProfile(self
.channel
,dbus
.String(profile
))
511 self
.sync_timer
.start(SYNC_TIMEOUT
)
512 def flush_state(self
):
513 if self
.sync_timer
.isActive():
514 self
.sync_timer
.stop()
529 return '%dKHz' %(hz
/(10.0**3),)
531 return '%.1fKHz' %(hz
/(10.0**3),)
533 def subdivide(xs
, t_points
):
534 while len(xs
)<t_points
:
537 for i
in range(1,len(m
),2):
538 m
[i
]=(m
[i
-1]+m
[i
+1])//2
540 p_drop
=len(xs
)-t_points
541 p_drop_left
=p_drop
//2
542 p_drop_right
=p_drop
-p_drop_left
544 #print 'dropping %d, %d left, %d right' %(p_drop,p_drop_left,p_drop_right)
546 left
=xs
[0:p_drop_left
*2:2]+xs
[p_drop_left
*2:c
]
547 right
=list(reversed(xs
[c
:]))
548 right
=right
[0:p_drop_right
*2:2]+right
[p_drop_right
*2:]
549 right
=list(reversed(right
))
553 dbus
.mainloop
.qt
.DBusQtMainLoop(set_as_default
=True)
554 app
=QtGui
.QApplication(sys
.argv
)
557 sys
.exit(app
.exec_())
559 if __name__
=='__main__':