153a5a1b3Sopenharmony_ci#!/usr/bin/env python3
253a5a1b3Sopenharmony_ci#    qpaeq is a equalizer interface for pulseaudio's equalizer sinks
353a5a1b3Sopenharmony_ci#    Copyright (C) 2009  Jason Newton <nevion@gmail.com
453a5a1b3Sopenharmony_ci#
553a5a1b3Sopenharmony_ci#    This program is free software: you can redistribute it and/or modify
653a5a1b3Sopenharmony_ci#    it under the terms of the GNU Lesser General Public License as
753a5a1b3Sopenharmony_ci#    published by the Free Software Foundation, either version 2.1 of the
853a5a1b3Sopenharmony_ci#    License, or (at your option) any later version.
953a5a1b3Sopenharmony_ci#
1053a5a1b3Sopenharmony_ci#    This program is distributed in the hope that it will be useful,
1153a5a1b3Sopenharmony_ci#    but WITHOUT ANY WARRANTY; without even the implied warranty of
1253a5a1b3Sopenharmony_ci#    MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
1353a5a1b3Sopenharmony_ci#    GNU Lesser General Public License for more details.
1453a5a1b3Sopenharmony_ci#
1553a5a1b3Sopenharmony_ci#    You should have received a copy of the GNU Lesser General Public License
1653a5a1b3Sopenharmony_ci#    along with this program.  If not, see <http://www.gnu.org/licenses/>.
1753a5a1b3Sopenharmony_ci
1853a5a1b3Sopenharmony_ci
1953a5a1b3Sopenharmony_ciimport os,math,sys
2053a5a1b3Sopenharmony_citry:
2153a5a1b3Sopenharmony_ci    from PyQt5 import QtWidgets,QtCore
2253a5a1b3Sopenharmony_ci    import dbus.mainloop.pyqt5
2353a5a1b3Sopenharmony_ci    import dbus
2453a5a1b3Sopenharmony_ciexcept ImportError as e:
2553a5a1b3Sopenharmony_ci    sys.stderr.write('There was an error importing needed libraries\n'
2653a5a1b3Sopenharmony_ci                     'Make sure you have qt5 and dbus-python installed\n'
2753a5a1b3Sopenharmony_ci                     'The error that occurred was:\n'
2853a5a1b3Sopenharmony_ci                     '\t%s\n' % (str(e)))
2953a5a1b3Sopenharmony_ci    sys.exit(-1)
3053a5a1b3Sopenharmony_ci
3153a5a1b3Sopenharmony_cifrom functools import partial
3253a5a1b3Sopenharmony_ci
3353a5a1b3Sopenharmony_ciimport signal
3453a5a1b3Sopenharmony_cisignal.signal(signal.SIGINT, signal.SIG_DFL)
3553a5a1b3Sopenharmony_ciSYNC_TIMEOUT = 4*1000
3653a5a1b3Sopenharmony_ci
3753a5a1b3Sopenharmony_ciCORE_PATH = "/org/pulseaudio/core1"
3853a5a1b3Sopenharmony_ciCORE_IFACE = "org.PulseAudio.Core1"
3953a5a1b3Sopenharmony_cidef connect():
4053a5a1b3Sopenharmony_ci    try:
4153a5a1b3Sopenharmony_ci        if 'PULSE_DBUS_SERVER' in os.environ:
4253a5a1b3Sopenharmony_ci            address = os.environ['PULSE_DBUS_SERVER']
4353a5a1b3Sopenharmony_ci        else:
4453a5a1b3Sopenharmony_ci            bus = dbus.SessionBus() # Should be UserBus, but D-Bus doesn't implement that yet.
4553a5a1b3Sopenharmony_ci            server_lookup = bus.get_object('org.PulseAudio1', '/org/pulseaudio/server_lookup1')
4653a5a1b3Sopenharmony_ci            address = server_lookup.Get('org.PulseAudio.ServerLookup1', 'Address', dbus_interface='org.freedesktop.DBus.Properties')
4753a5a1b3Sopenharmony_ci        return dbus.connection.Connection(address)
4853a5a1b3Sopenharmony_ci    except Exception as e:
4953a5a1b3Sopenharmony_ci        sys.stderr.write('There was an error connecting to pulseaudio, '
5053a5a1b3Sopenharmony_ci                         'please make sure you have the pulseaudio dbus '
5153a5a1b3Sopenharmony_ci                         'module loaded, exiting...\n')
5253a5a1b3Sopenharmony_ci        sys.exit(-1)
5353a5a1b3Sopenharmony_ci
5453a5a1b3Sopenharmony_ci
5553a5a1b3Sopenharmony_ci#TODO: signals: sink Filter changed, sink reconfigured (window size) (sink iface)
5653a5a1b3Sopenharmony_ci#TODO: manager signals: new sink, removed sink, new profile, removed profile
5753a5a1b3Sopenharmony_ci#TODO: add support for changing of window_size 1000-fft_size (adv option)
5853a5a1b3Sopenharmony_ci#TODO: reconnect support loop 1 second trying to reconnect
5953a5a1b3Sopenharmony_ci#TODO: just resample the filters for profiles when loading to different sizes
6053a5a1b3Sopenharmony_ci#TODO: add preamp
6153a5a1b3Sopenharmony_ciprop_iface='org.freedesktop.DBus.Properties'
6253a5a1b3Sopenharmony_cieq_iface='org.PulseAudio.Ext.Equalizing1.Equalizer'
6353a5a1b3Sopenharmony_cidevice_iface='org.PulseAudio.Core1.Device'
6453a5a1b3Sopenharmony_ciclass QPaeq(QtWidgets.QWidget):
6553a5a1b3Sopenharmony_ci    manager_path='/org/pulseaudio/equalizing1'
6653a5a1b3Sopenharmony_ci    manager_iface='org.PulseAudio.Ext.Equalizing1.Manager'
6753a5a1b3Sopenharmony_ci    core_iface='org.PulseAudio.Core1'
6853a5a1b3Sopenharmony_ci    core_path='/org/pulseaudio/core1'
6953a5a1b3Sopenharmony_ci    module_name='module-equalizer-sink'
7053a5a1b3Sopenharmony_ci
7153a5a1b3Sopenharmony_ci    def __init__(self):
7253a5a1b3Sopenharmony_ci        QtWidgets.QWidget.__init__(self)
7353a5a1b3Sopenharmony_ci        self.setWindowTitle('qpaeq')
7453a5a1b3Sopenharmony_ci        self.slider_widget=None
7553a5a1b3Sopenharmony_ci        self.sink_name=None
7653a5a1b3Sopenharmony_ci        self.filter_state=None
7753a5a1b3Sopenharmony_ci
7853a5a1b3Sopenharmony_ci        self.create_layout()
7953a5a1b3Sopenharmony_ci
8053a5a1b3Sopenharmony_ci        self.set_connection()
8153a5a1b3Sopenharmony_ci        self.connect_to_sink(self.sinks[0])
8253a5a1b3Sopenharmony_ci        self.set_callbacks()
8353a5a1b3Sopenharmony_ci        self.setMinimumSize(self.sizeHint())
8453a5a1b3Sopenharmony_ci
8553a5a1b3Sopenharmony_ci    def create_layout(self):
8653a5a1b3Sopenharmony_ci        self.main_layout=QtWidgets.QVBoxLayout()
8753a5a1b3Sopenharmony_ci        self.setLayout(self.main_layout)
8853a5a1b3Sopenharmony_ci        toprow_layout=QtWidgets.QHBoxLayout()
8953a5a1b3Sopenharmony_ci        sizePolicy = QtWidgets.QSizePolicy(QtWidgets.QSizePolicy.Preferred, QtWidgets.QSizePolicy.Fixed)
9053a5a1b3Sopenharmony_ci        sizePolicy.setHorizontalStretch(0)
9153a5a1b3Sopenharmony_ci        sizePolicy.setVerticalStretch(0)
9253a5a1b3Sopenharmony_ci        #sizePolicy.setHeightForWidth(self.profile_box.sizePolicy().hasHeightForWidth())
9353a5a1b3Sopenharmony_ci
9453a5a1b3Sopenharmony_ci        toprow_layout.addWidget(QtWidgets.QLabel('Sink'))
9553a5a1b3Sopenharmony_ci        self.sink_box = QtWidgets.QComboBox()
9653a5a1b3Sopenharmony_ci        self.sink_box.setSizePolicy(sizePolicy)
9753a5a1b3Sopenharmony_ci        self.sink_box.setDuplicatesEnabled(False)
9853a5a1b3Sopenharmony_ci        self.sink_box.setInsertPolicy(QtWidgets.QComboBox.InsertAlphabetically)
9953a5a1b3Sopenharmony_ci        #self.sink_box.setSizeAdjustPolicy(QtWidgets.QComboBox.AdjustToContents)
10053a5a1b3Sopenharmony_ci        toprow_layout.addWidget(self.sink_box)
10153a5a1b3Sopenharmony_ci
10253a5a1b3Sopenharmony_ci        toprow_layout.addWidget(QtWidgets.QLabel('Channel'))
10353a5a1b3Sopenharmony_ci        self.channel_box = QtWidgets.QComboBox()
10453a5a1b3Sopenharmony_ci        self.channel_box.setSizePolicy(sizePolicy)
10553a5a1b3Sopenharmony_ci        toprow_layout.addWidget(self.channel_box)
10653a5a1b3Sopenharmony_ci
10753a5a1b3Sopenharmony_ci        toprow_layout.addWidget(QtWidgets.QLabel('Preset'))
10853a5a1b3Sopenharmony_ci        self.profile_box = QtWidgets.QComboBox()
10953a5a1b3Sopenharmony_ci        self.profile_box.setSizePolicy(sizePolicy)
11053a5a1b3Sopenharmony_ci        self.profile_box.setInsertPolicy(QtWidgets.QComboBox.InsertAlphabetically)
11153a5a1b3Sopenharmony_ci        #self.profile_box.setSizeAdjustPolicy(QtWidgets.QComboBox.AdjustToContents)
11253a5a1b3Sopenharmony_ci        toprow_layout.addWidget(self.profile_box)
11353a5a1b3Sopenharmony_ci
11453a5a1b3Sopenharmony_ci        large_icon_size=self.style().pixelMetric(QtWidgets.QStyle.PM_LargeIconSize)
11553a5a1b3Sopenharmony_ci        large_icon_size=QtCore.QSize(large_icon_size,large_icon_size)
11653a5a1b3Sopenharmony_ci        save_profile=QtWidgets.QToolButton()
11753a5a1b3Sopenharmony_ci        save_profile.setIcon(self.style().standardIcon(QtWidgets.QStyle.SP_DriveFDIcon))
11853a5a1b3Sopenharmony_ci        save_profile.setIconSize(large_icon_size)
11953a5a1b3Sopenharmony_ci        save_profile.setToolButtonStyle(QtCore.Qt.ToolButtonIconOnly)
12053a5a1b3Sopenharmony_ci        save_profile.clicked.connect(self.save_profile)
12153a5a1b3Sopenharmony_ci        remove_profile=QtWidgets.QToolButton()
12253a5a1b3Sopenharmony_ci        remove_profile.setIcon(self.style().standardIcon(QtWidgets.QStyle.SP_TrashIcon))
12353a5a1b3Sopenharmony_ci        remove_profile.setIconSize(large_icon_size)
12453a5a1b3Sopenharmony_ci        remove_profile.setToolButtonStyle(QtCore.Qt.ToolButtonIconOnly)
12553a5a1b3Sopenharmony_ci        remove_profile.clicked.connect(self.remove_profile)
12653a5a1b3Sopenharmony_ci        toprow_layout.addWidget(save_profile)
12753a5a1b3Sopenharmony_ci        toprow_layout.addWidget(remove_profile)
12853a5a1b3Sopenharmony_ci
12953a5a1b3Sopenharmony_ci        reset_button = QtWidgets.QPushButton('Reset')
13053a5a1b3Sopenharmony_ci        reset_button.clicked.connect(self.reset)
13153a5a1b3Sopenharmony_ci        toprow_layout.addStretch()
13253a5a1b3Sopenharmony_ci        toprow_layout.addWidget(reset_button)
13353a5a1b3Sopenharmony_ci        self.layout().addLayout(toprow_layout)
13453a5a1b3Sopenharmony_ci
13553a5a1b3Sopenharmony_ci        self.profile_box.activated.connect(self.load_profile)
13653a5a1b3Sopenharmony_ci        self.channel_box.activated.connect(self.select_channel)
13753a5a1b3Sopenharmony_ci    def connect_to_sink(self,name):
13853a5a1b3Sopenharmony_ci        #TODO: clear slots for profile buttons
13953a5a1b3Sopenharmony_ci
14053a5a1b3Sopenharmony_ci        #flush any pending saves for other sinks
14153a5a1b3Sopenharmony_ci        if self.filter_state is not None:
14253a5a1b3Sopenharmony_ci            self.filter_state.flush_state()
14353a5a1b3Sopenharmony_ci        sink=self.connection.get_object(object_path=name)
14453a5a1b3Sopenharmony_ci        self.sink_props=dbus.Interface(sink,dbus_interface=prop_iface)
14553a5a1b3Sopenharmony_ci        self.sink=dbus.Interface(sink,dbus_interface=eq_iface)
14653a5a1b3Sopenharmony_ci        self.filter_state=FilterState(sink)
14753a5a1b3Sopenharmony_ci        #sample_rate,filter_rate,channels,channel)
14853a5a1b3Sopenharmony_ci
14953a5a1b3Sopenharmony_ci        self.channel_box.clear()
15053a5a1b3Sopenharmony_ci        self.channel_box.addItem('All',self.filter_state.channels)
15153a5a1b3Sopenharmony_ci        for i in range(self.filter_state.channels):
15253a5a1b3Sopenharmony_ci            self.channel_box.addItem('%d' %(i+1,),i)
15353a5a1b3Sopenharmony_ci        self.setMinimumSize(self.sizeHint())
15453a5a1b3Sopenharmony_ci
15553a5a1b3Sopenharmony_ci        self.set_slider_widget(SliderArray(self.filter_state))
15653a5a1b3Sopenharmony_ci
15753a5a1b3Sopenharmony_ci        self.sink_name=name
15853a5a1b3Sopenharmony_ci        #set the signal listener for this sink
15953a5a1b3Sopenharmony_ci        core=self._get_core()
16053a5a1b3Sopenharmony_ci        #temporary hack until signal filtering works properly
16153a5a1b3Sopenharmony_ci        core.ListenForSignal('',[dbus.ObjectPath(self.sink_name),dbus.ObjectPath(self.manager_path)])
16253a5a1b3Sopenharmony_ci        #for x in ['FilterChanged']:
16353a5a1b3Sopenharmony_ci        #    core.ListenForSignal("%s.%s" %(self.eq_iface,x),[dbus.ObjectPath(self.sink_name)])
16453a5a1b3Sopenharmony_ci        #core.ListenForSignal(self.eq_iface,[dbus.ObjectPath(self.sink_name)])
16553a5a1b3Sopenharmony_ci        self.sink.connect_to_signal('FilterChanged',self.read_filter)
16653a5a1b3Sopenharmony_ci
16753a5a1b3Sopenharmony_ci    def set_slider_widget(self,widget):
16853a5a1b3Sopenharmony_ci        layout=self.layout()
16953a5a1b3Sopenharmony_ci        if self.slider_widget is not None:
17053a5a1b3Sopenharmony_ci            i=layout.indexOf(self.slider_widget)
17153a5a1b3Sopenharmony_ci            layout.removeWidget(self.slider_widget)
17253a5a1b3Sopenharmony_ci            self.slider_widget.deleteLater()
17353a5a1b3Sopenharmony_ci            layout.insertWidget(i,self.slider_widget)
17453a5a1b3Sopenharmony_ci        else:
17553a5a1b3Sopenharmony_ci            layout.addWidget(widget)
17653a5a1b3Sopenharmony_ci        self.slider_widget=widget
17753a5a1b3Sopenharmony_ci        self.read_filter()
17853a5a1b3Sopenharmony_ci    def _get_core(self):
17953a5a1b3Sopenharmony_ci        core_obj=self.connection.get_object(object_path=self.core_path)
18053a5a1b3Sopenharmony_ci        core=dbus.Interface(core_obj,dbus_interface=self.core_iface)
18153a5a1b3Sopenharmony_ci        return core
18253a5a1b3Sopenharmony_ci    def sink_added(self,sink):
18353a5a1b3Sopenharmony_ci        #TODO: preserve selected sink
18453a5a1b3Sopenharmony_ci        self.update_sinks()
18553a5a1b3Sopenharmony_ci    def sink_removed(self,sink):
18653a5a1b3Sopenharmony_ci        #TODO: preserve selected sink, try connecting to backup otherwise
18753a5a1b3Sopenharmony_ci        if sink==self.sink_name:
18853a5a1b3Sopenharmony_ci            #connect to new sink?
18953a5a1b3Sopenharmony_ci            pass
19053a5a1b3Sopenharmony_ci        self.update_sinks()
19153a5a1b3Sopenharmony_ci    def save_profile(self):
19253a5a1b3Sopenharmony_ci        #popup dialog box for name
19353a5a1b3Sopenharmony_ci        current=self.profile_box.currentIndex()
19453a5a1b3Sopenharmony_ci        profile,ok=QtWidgets.QInputDialog.getItem(self,'Preset Name','Preset',self.profiles,current)
19553a5a1b3Sopenharmony_ci        if not ok or profile=='':
19653a5a1b3Sopenharmony_ci            return
19753a5a1b3Sopenharmony_ci        if profile in self.profiles:
19853a5a1b3Sopenharmony_ci            mbox=QtWidgets.QMessageBox(self)
19953a5a1b3Sopenharmony_ci            mbox.setText('%s preset already exists'%(profile,))
20053a5a1b3Sopenharmony_ci            mbox.setInformativeText('Do you want to save over it?')
20153a5a1b3Sopenharmony_ci            mbox.setStandardButtons(mbox.Save|mbox.Discard|mbox.Cancel)
20253a5a1b3Sopenharmony_ci            mbox.setDefaultButton(mbox.Save)
20353a5a1b3Sopenharmony_ci            ret=mbox.exec_()
20453a5a1b3Sopenharmony_ci            if ret!=mbox.Save:
20553a5a1b3Sopenharmony_ci                return
20653a5a1b3Sopenharmony_ci        self.sink.SaveProfile(self.filter_state.channel,dbus.String(profile))
20753a5a1b3Sopenharmony_ci        if self.filter_state.channel==self.filter_state.channels:
20853a5a1b3Sopenharmony_ci            for x in range(1,self.filter_state.channels):
20953a5a1b3Sopenharmony_ci                self.sink.LoadProfile(x,dbus.String(profile))
21053a5a1b3Sopenharmony_ci    def remove_profile(self):
21153a5a1b3Sopenharmony_ci        #find active profile name, remove it
21253a5a1b3Sopenharmony_ci        profile=self.profile_box.currentText()
21353a5a1b3Sopenharmony_ci        manager=dbus.Interface(self.manager_obj,dbus_interface=self.manager_iface)
21453a5a1b3Sopenharmony_ci        manager.RemoveProfile(dbus.String(profile))
21553a5a1b3Sopenharmony_ci    def load_profile(self,x):
21653a5a1b3Sopenharmony_ci        profile=self.profile_box.itemText(x)
21753a5a1b3Sopenharmony_ci        self.filter_state.load_profile(profile)
21853a5a1b3Sopenharmony_ci    def select_channel(self,x):
21953a5a1b3Sopenharmony_ci        self.filter_state.channel = self.channel_box.itemData(x)
22053a5a1b3Sopenharmony_ci        self._set_profile_name()
22153a5a1b3Sopenharmony_ci        self.filter_state.readback()
22253a5a1b3Sopenharmony_ci
22353a5a1b3Sopenharmony_ci    #TODO: add back in preamp!
22453a5a1b3Sopenharmony_ci    #print(frequencies)
22553a5a1b3Sopenharmony_ci    #main_layout.addLayout(self.create_slider(partial(self.update_coefficient,0),
22653a5a1b3Sopenharmony_ci    #    'Preamp')[0]
22753a5a1b3Sopenharmony_ci    #)
22853a5a1b3Sopenharmony_ci    def set_connection(self):
22953a5a1b3Sopenharmony_ci        self.connection=connect()
23053a5a1b3Sopenharmony_ci
23153a5a1b3Sopenharmony_ci        self.manager_obj=self.connection.get_object(object_path=self.manager_path)
23253a5a1b3Sopenharmony_ci        manager_props=dbus.Interface(self.manager_obj,dbus_interface=prop_iface)
23353a5a1b3Sopenharmony_ci        try:
23453a5a1b3Sopenharmony_ci            self.sinks=manager_props.Get(self.manager_iface,'EqualizedSinks')
23553a5a1b3Sopenharmony_ci        except dbus.exceptions.DBusException:
23653a5a1b3Sopenharmony_ci            # probably module not yet loaded, try to load it:
23753a5a1b3Sopenharmony_ci            try:
23853a5a1b3Sopenharmony_ci                core=self.connection.get_object(object_path=self.core_path)
23953a5a1b3Sopenharmony_ci                core.LoadModule(self.module_name,{},dbus_interface=self.core_iface)
24053a5a1b3Sopenharmony_ci                # yup, we don't need to re-create manager_obj and manager_props,
24153a5a1b3Sopenharmony_ci                # these are late-bound
24253a5a1b3Sopenharmony_ci                self.sinks=manager_props.Get(self.manager_iface,'EqualizedSinks')
24353a5a1b3Sopenharmony_ci            except dbus.exceptions.DBusException:
24453a5a1b3Sopenharmony_ci                sys.stderr.write('It seems that running pulseaudio does not support '
24553a5a1b3Sopenharmony_ci                                 'equalizer features and loading %s module failed.\n'
24653a5a1b3Sopenharmony_ci                                 'Exiting...\n' % self.module_name)
24753a5a1b3Sopenharmony_ci                sys.exit(-1)
24853a5a1b3Sopenharmony_ci
24953a5a1b3Sopenharmony_ci    def set_callbacks(self):
25053a5a1b3Sopenharmony_ci        manager=dbus.Interface(self.manager_obj,dbus_interface=self.manager_iface)
25153a5a1b3Sopenharmony_ci        manager.connect_to_signal('ProfilesChanged',self.update_profiles)
25253a5a1b3Sopenharmony_ci        manager.connect_to_signal('SinkAdded',self.sink_added)
25353a5a1b3Sopenharmony_ci        manager.connect_to_signal('SinkRemoved',self.sink_removed)
25453a5a1b3Sopenharmony_ci        #self._get_core().ListenForSignal(self.manager_iface,[])
25553a5a1b3Sopenharmony_ci        #self._get_core().ListenForSignal(self.manager_iface,[dbus.ObjectPath(self.manager_path)])
25653a5a1b3Sopenharmony_ci        #core=self._get_core()
25753a5a1b3Sopenharmony_ci        #for x in ['ProfilesChanged','SinkAdded','SinkRemoved']:
25853a5a1b3Sopenharmony_ci        #    core.ListenForSignal("%s.%s" %(self.manager_iface,x),[dbus.ObjectPath(self.manager_path)])
25953a5a1b3Sopenharmony_ci        self.update_profiles()
26053a5a1b3Sopenharmony_ci        self.update_sinks()
26153a5a1b3Sopenharmony_ci    def update_profiles(self):
26253a5a1b3Sopenharmony_ci        #print('update profiles called!')
26353a5a1b3Sopenharmony_ci        manager_props=dbus.Interface(self.manager_obj,dbus_interface=prop_iface)
26453a5a1b3Sopenharmony_ci        self.profiles=manager_props.Get(self.manager_iface,'Profiles')
26553a5a1b3Sopenharmony_ci        self.profile_box.blockSignals(True)
26653a5a1b3Sopenharmony_ci        self.profile_box.clear()
26753a5a1b3Sopenharmony_ci        self.profile_box.addItems(self.profiles)
26853a5a1b3Sopenharmony_ci        self.profile_box.blockSignals(False)
26953a5a1b3Sopenharmony_ci        self._set_profile_name()
27053a5a1b3Sopenharmony_ci    def update_sinks(self):
27153a5a1b3Sopenharmony_ci        self.sink_box.blockSignals(True)
27253a5a1b3Sopenharmony_ci        self.sink_box.clear()
27353a5a1b3Sopenharmony_ci        for x in self.sinks:
27453a5a1b3Sopenharmony_ci            sink=self.connection.get_object(object_path=x)
27553a5a1b3Sopenharmony_ci            sink_props=dbus.Interface(sink,dbus_interface=prop_iface)
27653a5a1b3Sopenharmony_ci            simple_name=sink_props.Get(device_iface,'Name')
27753a5a1b3Sopenharmony_ci            self.sink_box.addItem(simple_name,x)
27853a5a1b3Sopenharmony_ci        self.sink_box.blockSignals(False)
27953a5a1b3Sopenharmony_ci        self.sink_box.setMinimumSize(self.sink_box.sizeHint())
28053a5a1b3Sopenharmony_ci    def read_filter(self):
28153a5a1b3Sopenharmony_ci        #print(self.filter_frequencies)
28253a5a1b3Sopenharmony_ci        self.filter_state.readback()
28353a5a1b3Sopenharmony_ci    def reset(self):
28453a5a1b3Sopenharmony_ci        coefs=dbus.Array([1/math.sqrt(2.0)]*(self.filter_state.filter_rate//2+1))
28553a5a1b3Sopenharmony_ci        preamp=1.0
28653a5a1b3Sopenharmony_ci        self.filter_state.set_filter(preamp,coefs)
28753a5a1b3Sopenharmony_ci    def _set_profile_name(self):
28853a5a1b3Sopenharmony_ci        self.profile_box.blockSignals(True)
28953a5a1b3Sopenharmony_ci        profile_name=self.sink.BaseProfile(self.filter_state.channel)
29053a5a1b3Sopenharmony_ci        if profile_name is not None:
29153a5a1b3Sopenharmony_ci            i=self.profile_box.findText(profile_name)
29253a5a1b3Sopenharmony_ci            if i>=0:
29353a5a1b3Sopenharmony_ci                self.profile_box.setCurrentIndex(i)
29453a5a1b3Sopenharmony_ci        self.profile_box.blockSignals(False)
29553a5a1b3Sopenharmony_ci
29653a5a1b3Sopenharmony_ci
29753a5a1b3Sopenharmony_ciclass SliderArray(QtWidgets.QWidget):
29853a5a1b3Sopenharmony_ci    def __init__(self,filter_state,parent=None):
29953a5a1b3Sopenharmony_ci        super(SliderArray,self).__init__(parent)
30053a5a1b3Sopenharmony_ci        #self.setStyleSheet('padding: 0px; border-width: 0px; margin: 0px;')
30153a5a1b3Sopenharmony_ci        #self.setStyleSheet('font-family: monospace;'+outline%('blue'))
30253a5a1b3Sopenharmony_ci        self.filter_state=filter_state
30353a5a1b3Sopenharmony_ci        self.setLayout(QtWidgets.QHBoxLayout())
30453a5a1b3Sopenharmony_ci        self.sub_array=None
30553a5a1b3Sopenharmony_ci        self.set_sub_array(SliderArraySub(self.filter_state))
30653a5a1b3Sopenharmony_ci        self.inhibit_resize=0
30753a5a1b3Sopenharmony_ci    def set_sub_array(self,widget):
30853a5a1b3Sopenharmony_ci        if self.sub_array is not None:
30953a5a1b3Sopenharmony_ci            self.layout().removeWidget(self.sub_array)
31053a5a1b3Sopenharmony_ci            self.sub_array.disconnect_signals()
31153a5a1b3Sopenharmony_ci            self.sub_array.deleteLater()
31253a5a1b3Sopenharmony_ci        self.sub_array=widget
31353a5a1b3Sopenharmony_ci        self.layout().addWidget(self.sub_array)
31453a5a1b3Sopenharmony_ci        self.sub_array.connect_signals()
31553a5a1b3Sopenharmony_ci        self.filter_state.readback()
31653a5a1b3Sopenharmony_ci    def resizeEvent(self,event):
31753a5a1b3Sopenharmony_ci        super(SliderArray,self).resizeEvent(event)
31853a5a1b3Sopenharmony_ci        if self.inhibit_resize==0:
31953a5a1b3Sopenharmony_ci            self.inhibit_resize+=1
32053a5a1b3Sopenharmony_ci            #self.add_sliders_to_fit()
32153a5a1b3Sopenharmony_ci            t=QtCore.QTimer(self)
32253a5a1b3Sopenharmony_ci            t.setSingleShot(True)
32353a5a1b3Sopenharmony_ci            t.setInterval(0)
32453a5a1b3Sopenharmony_ci            t.timeout.connect(partial(self.add_sliders_to_fit,event))
32553a5a1b3Sopenharmony_ci            t.start()
32653a5a1b3Sopenharmony_ci    def add_sliders_to_fit(self,event):
32753a5a1b3Sopenharmony_ci        if event.oldSize().width()>0 and event.size().width()>0:
32853a5a1b3Sopenharmony_ci            i=len(self.filter_state.frequencies)*int(round(float(event.size().width())/event.oldSize().width()))
32953a5a1b3Sopenharmony_ci        else:
33053a5a1b3Sopenharmony_ci            i=len(self.filter_state.frequencies)
33153a5a1b3Sopenharmony_ci
33253a5a1b3Sopenharmony_ci        t_w=self.size().width()
33353a5a1b3Sopenharmony_ci        def evaluate(filter_state, target, variable):
33453a5a1b3Sopenharmony_ci            base_freqs=self.filter_state.freq_proper(self.filter_state.DEFAULT_FREQUENCIES)
33553a5a1b3Sopenharmony_ci            filter_state._set_frequency_values(subdivide(base_freqs,variable))
33653a5a1b3Sopenharmony_ci            new_widget=SliderArraySub(filter_state)
33753a5a1b3Sopenharmony_ci            w=new_widget.sizeHint().width()
33853a5a1b3Sopenharmony_ci            return w-target
33953a5a1b3Sopenharmony_ci        def searcher(initial,evaluator):
34053a5a1b3Sopenharmony_ci            i=initial
34153a5a1b3Sopenharmony_ci            def d(e): return 1 if e>=0 else -1
34253a5a1b3Sopenharmony_ci            error=evaluator(i)
34353a5a1b3Sopenharmony_ci            old_direction=d(error)
34453a5a1b3Sopenharmony_ci            i-=old_direction
34553a5a1b3Sopenharmony_ci            while True:
34653a5a1b3Sopenharmony_ci                error=evaluator(i)
34753a5a1b3Sopenharmony_ci                direction=d(error)
34853a5a1b3Sopenharmony_ci                if direction!=old_direction:
34953a5a1b3Sopenharmony_ci                    k=i-1
35053a5a1b3Sopenharmony_ci                    #while direction<0 and error!=0:
35153a5a1b3Sopenharmony_ci                    #    k-=1
35253a5a1b3Sopenharmony_ci                    #    error=evaluator(i)
35353a5a1b3Sopenharmony_ci                    #    direction=d(error)
35453a5a1b3Sopenharmony_ci                    return k, evaluator(k)
35553a5a1b3Sopenharmony_ci                i-=direction
35653a5a1b3Sopenharmony_ci                old_direction=direction
35753a5a1b3Sopenharmony_ci        searcher(i,partial(evaluate,self.filter_state,t_w))
35853a5a1b3Sopenharmony_ci        self.set_sub_array(SliderArraySub(self.filter_state))
35953a5a1b3Sopenharmony_ci        self.inhibit_resize-=1
36053a5a1b3Sopenharmony_ci
36153a5a1b3Sopenharmony_ciclass SliderArraySub(QtWidgets.QWidget):
36253a5a1b3Sopenharmony_ci    def __init__(self,filter_state,parent=None):
36353a5a1b3Sopenharmony_ci        super(SliderArraySub,self).__init__(parent)
36453a5a1b3Sopenharmony_ci        self.filter_state=filter_state
36553a5a1b3Sopenharmony_ci        self.setLayout(QtWidgets.QGridLayout())
36653a5a1b3Sopenharmony_ci        self.slider=[None]*len(self.filter_state.frequencies)
36753a5a1b3Sopenharmony_ci        self.label=[None]*len(self.slider)
36853a5a1b3Sopenharmony_ci        #self.setStyleSheet('padding: 0px; border-width: 0px; margin: 0px;')
36953a5a1b3Sopenharmony_ci        #self.setStyleSheet('font-family: monospace;'+outline%('blue'))
37053a5a1b3Sopenharmony_ci        qt=QtCore.Qt
37153a5a1b3Sopenharmony_ci        #self.layout().setHorizontalSpacing(1)
37253a5a1b3Sopenharmony_ci        def add_slider(slider,label, c):
37353a5a1b3Sopenharmony_ci            self.layout().addWidget(slider,0,c,qt.AlignHCenter)
37453a5a1b3Sopenharmony_ci            self.layout().addWidget(label,1,c,qt.AlignHCenter)
37553a5a1b3Sopenharmony_ci            self.layout().setColumnMinimumWidth(c,max(label.sizeHint().width(),slider.sizeHint().width()))
37653a5a1b3Sopenharmony_ci        def create_slider(slider_label):
37753a5a1b3Sopenharmony_ci            slider=QtWidgets.QSlider(QtCore.Qt.Vertical,self)
37853a5a1b3Sopenharmony_ci            label=SliderLabel(slider_label,filter_state,self)
37953a5a1b3Sopenharmony_ci            slider.setRange(-1000,2000)
38053a5a1b3Sopenharmony_ci            slider.setSingleStep(1)
38153a5a1b3Sopenharmony_ci            return (slider,label)
38253a5a1b3Sopenharmony_ci        self.preamp_slider,self.preamp_label=create_slider('Preamp')
38353a5a1b3Sopenharmony_ci        add_slider(self.preamp_slider,self.preamp_label,0)
38453a5a1b3Sopenharmony_ci        for i,hz in enumerate(self.filter_state.frequencies):
38553a5a1b3Sopenharmony_ci            slider,label=create_slider(self.hz2label(hz))
38653a5a1b3Sopenharmony_ci            self.slider[i]=slider
38753a5a1b3Sopenharmony_ci            #slider.setStyleSheet('font-family: monospace;'+outline%('red',))
38853a5a1b3Sopenharmony_ci            self.label[i]=label
38953a5a1b3Sopenharmony_ci            c=i+1
39053a5a1b3Sopenharmony_ci            add_slider(slider,label,i+1)
39153a5a1b3Sopenharmony_ci    def hz2label(self, hz):
39253a5a1b3Sopenharmony_ci        if hz==0:
39353a5a1b3Sopenharmony_ci            label_text='DC'
39453a5a1b3Sopenharmony_ci        elif hz==self.filter_state.sample_rate//2:
39553a5a1b3Sopenharmony_ci            label_text='Coda'
39653a5a1b3Sopenharmony_ci        else:
39753a5a1b3Sopenharmony_ci            label_text=hz2str(hz)
39853a5a1b3Sopenharmony_ci        return label_text
39953a5a1b3Sopenharmony_ci
40053a5a1b3Sopenharmony_ci    def connect_signals(self):
40153a5a1b3Sopenharmony_ci        def connect(writer,reader,slider,label):
40253a5a1b3Sopenharmony_ci            slider.valueChanged.connect(writer)
40353a5a1b3Sopenharmony_ci            self.filter_state.readFilter.connect(reader)
40453a5a1b3Sopenharmony_ci            label_cb=partial(slider.setValue,0)
40553a5a1b3Sopenharmony_ci            label.clicked.connect(label_cb)
40653a5a1b3Sopenharmony_ci            return label_cb
40753a5a1b3Sopenharmony_ci
40853a5a1b3Sopenharmony_ci        self.preamp_writer_cb=self.write_preamp
40953a5a1b3Sopenharmony_ci        self.preamp_reader_cb=self.sync_preamp
41053a5a1b3Sopenharmony_ci        self.preamp_label_cb=connect(self.preamp_writer_cb,
41153a5a1b3Sopenharmony_ci                self.preamp_reader_cb,
41253a5a1b3Sopenharmony_ci                self.preamp_slider,
41353a5a1b3Sopenharmony_ci                self.preamp_label)
41453a5a1b3Sopenharmony_ci        self.writer_callbacks=[None]*len(self.slider)
41553a5a1b3Sopenharmony_ci        self.reader_callbacks=[None]*len(self.slider)
41653a5a1b3Sopenharmony_ci        self.label_callbacks=[None]*len(self.label)
41753a5a1b3Sopenharmony_ci        for i in range(len(self.slider)):
41853a5a1b3Sopenharmony_ci            self.writer_callbacks[i]=partial(self.write_coefficient,i)
41953a5a1b3Sopenharmony_ci            self.reader_callbacks[i]=partial(self.sync_coefficient,i)
42053a5a1b3Sopenharmony_ci            self.label_callbacks[i]=connect(self.writer_callbacks[i],
42153a5a1b3Sopenharmony_ci                    self.reader_callbacks[i],
42253a5a1b3Sopenharmony_ci                    self.slider[i],
42353a5a1b3Sopenharmony_ci                    self.label[i])
42453a5a1b3Sopenharmony_ci    def disconnect_signals(self):
42553a5a1b3Sopenharmony_ci        def disconnect(writer,reader,label_cb,slider,label):
42653a5a1b3Sopenharmony_ci            slider.valueChanged.disconnect(writer)
42753a5a1b3Sopenharmony_ci            self.filter_state.readFilter.disconnect(reader)
42853a5a1b3Sopenharmony_ci            label.clicked.disconnect(label_cb)
42953a5a1b3Sopenharmony_ci        disconnect(self.preamp_writer_cb, self.preamp_reader_cb,
43053a5a1b3Sopenharmony_ci                self.preamp_label_cb, self.preamp_slider, self.preamp_label)
43153a5a1b3Sopenharmony_ci        for i in range(len(self.slider)):
43253a5a1b3Sopenharmony_ci            disconnect(self.writer_callbacks[i],
43353a5a1b3Sopenharmony_ci                    self.reader_callbacks[i],
43453a5a1b3Sopenharmony_ci                    self.label_callbacks[i],
43553a5a1b3Sopenharmony_ci                    self.slider[i],
43653a5a1b3Sopenharmony_ci                    self.label[i])
43753a5a1b3Sopenharmony_ci
43853a5a1b3Sopenharmony_ci    def write_preamp(self, v):
43953a5a1b3Sopenharmony_ci        self.filter_state.preamp=self.slider2coef(v)
44053a5a1b3Sopenharmony_ci        self.filter_state.seed()
44153a5a1b3Sopenharmony_ci    def sync_preamp(self):
44253a5a1b3Sopenharmony_ci        self.preamp_slider.blockSignals(True)
44353a5a1b3Sopenharmony_ci        self.preamp_slider.setValue(self.coef2slider(self.filter_state.preamp))
44453a5a1b3Sopenharmony_ci        self.preamp_slider.blockSignals(False)
44553a5a1b3Sopenharmony_ci
44653a5a1b3Sopenharmony_ci
44753a5a1b3Sopenharmony_ci    def write_coefficient(self,i,v):
44853a5a1b3Sopenharmony_ci        self.filter_state.coefficients[i]=self.slider2coef(v)/math.sqrt(2.0)
44953a5a1b3Sopenharmony_ci        self.filter_state.seed()
45053a5a1b3Sopenharmony_ci    def sync_coefficient(self,i):
45153a5a1b3Sopenharmony_ci        slider=self.slider[i]
45253a5a1b3Sopenharmony_ci        slider.blockSignals(True)
45353a5a1b3Sopenharmony_ci        slider.setValue(self.coef2slider(math.sqrt(2.0)*self.filter_state.coefficients[i]))
45453a5a1b3Sopenharmony_ci        slider.blockSignals(False)
45553a5a1b3Sopenharmony_ci    @staticmethod
45653a5a1b3Sopenharmony_ci    def slider2coef(x):
45753a5a1b3Sopenharmony_ci        return (1.0+(x/1000.0))
45853a5a1b3Sopenharmony_ci    @staticmethod
45953a5a1b3Sopenharmony_ci    def coef2slider(x):
46053a5a1b3Sopenharmony_ci        return int((x-1.0)*1000)
46153a5a1b3Sopenharmony_cioutline='border-width: 1px; border-style: solid; border-color: %s;'
46253a5a1b3Sopenharmony_ci
46353a5a1b3Sopenharmony_ciclass SliderLabel(QtWidgets.QLabel):
46453a5a1b3Sopenharmony_ci    clicked=QtCore.pyqtSignal()
46553a5a1b3Sopenharmony_ci    def __init__(self,label_text,filter_state,parent=None):
46653a5a1b3Sopenharmony_ci        super(SliderLabel,self).__init__(parent)
46753a5a1b3Sopenharmony_ci        self.setStyleSheet('font-family: monospace;')
46853a5a1b3Sopenharmony_ci        self.setText(label_text)
46953a5a1b3Sopenharmony_ci        self.setMinimumSize(self.sizeHint())
47053a5a1b3Sopenharmony_ci    def mouseDoubleClickEvent(self, event):
47153a5a1b3Sopenharmony_ci        self.clicked.emit()
47253a5a1b3Sopenharmony_ci        super(SliderLabel,self).mouseDoubleClickEvent(event)
47353a5a1b3Sopenharmony_ci
47453a5a1b3Sopenharmony_ci#until there are server side state savings, do it in the client but try and avoid
47553a5a1b3Sopenharmony_ci#simulaneous broadcasting situations
47653a5a1b3Sopenharmony_ciclass FilterState(QtCore.QObject):
47753a5a1b3Sopenharmony_ci    #DEFAULT_FREQUENCIES=map(float,[25,50,75,100,150,200,300,400,500,800,1e3,1.5e3,3e3,5e3,7e3,10e3,15e3,20e3])
47853a5a1b3Sopenharmony_ci    DEFAULT_FREQUENCIES=[31.75,63.5,125,250,500,1e3,2e3,4e3,8e3,16e3]
47953a5a1b3Sopenharmony_ci    readFilter=QtCore.pyqtSignal()
48053a5a1b3Sopenharmony_ci    def __init__(self,sink):
48153a5a1b3Sopenharmony_ci        super(FilterState,self).__init__()
48253a5a1b3Sopenharmony_ci        self.sink_props=dbus.Interface(sink,dbus_interface=prop_iface)
48353a5a1b3Sopenharmony_ci        self.sink=dbus.Interface(sink,dbus_interface=eq_iface)
48453a5a1b3Sopenharmony_ci        self.sample_rate=self.get_eq_attr('SampleRate')
48553a5a1b3Sopenharmony_ci        self.filter_rate=self.get_eq_attr('FilterSampleRate')
48653a5a1b3Sopenharmony_ci        self.channels=self.get_eq_attr('NChannels')
48753a5a1b3Sopenharmony_ci        self.channel=self.channels
48853a5a1b3Sopenharmony_ci        self.set_frequency_values(self.DEFAULT_FREQUENCIES)
48953a5a1b3Sopenharmony_ci        self.sync_timer=QtCore.QTimer()
49053a5a1b3Sopenharmony_ci        self.sync_timer.setSingleShot(True)
49153a5a1b3Sopenharmony_ci        self.sync_timer.timeout.connect(self.save_state)
49253a5a1b3Sopenharmony_ci
49353a5a1b3Sopenharmony_ci    def get_eq_attr(self,attr):
49453a5a1b3Sopenharmony_ci        return self.sink_props.Get(eq_iface,attr)
49553a5a1b3Sopenharmony_ci    def freq_proper(self,xs):
49653a5a1b3Sopenharmony_ci        return [0]+xs+[self.sample_rate//2]
49753a5a1b3Sopenharmony_ci    def _set_frequency_values(self,freqs):
49853a5a1b3Sopenharmony_ci        self.frequencies=freqs
49953a5a1b3Sopenharmony_ci        #print('base',self.frequencies)
50053a5a1b3Sopenharmony_ci        self.filter_frequencies=[int(round(x)) for x in self.translate_rates(self.filter_rate,self.sample_rate,
50153a5a1b3Sopenharmony_ci                    self.frequencies)]
50253a5a1b3Sopenharmony_ci        self.coefficients=[0.0]*len(self.frequencies)
50353a5a1b3Sopenharmony_ci        self.preamp=1.0
50453a5a1b3Sopenharmony_ci    def set_frequency_values(self,freqs):
50553a5a1b3Sopenharmony_ci        self._set_frequency_values(self.freq_proper(freqs))
50653a5a1b3Sopenharmony_ci    @staticmethod
50753a5a1b3Sopenharmony_ci    def translate_rates(dst,src,rates):
50853a5a1b3Sopenharmony_ci        return list([x*dst/src for x in rates])
50953a5a1b3Sopenharmony_ci    def seed(self):
51053a5a1b3Sopenharmony_ci        self.sink.SeedFilter(self.channel,self.filter_frequencies,self.coefficients,self.preamp)
51153a5a1b3Sopenharmony_ci        self.sync_timer.start(SYNC_TIMEOUT)
51253a5a1b3Sopenharmony_ci    def readback(self):
51353a5a1b3Sopenharmony_ci        coefs,preamp=self.sink.FilterAtPoints(self.channel,self.filter_frequencies)
51453a5a1b3Sopenharmony_ci        self.coefficients=coefs
51553a5a1b3Sopenharmony_ci        self.preamp=preamp
51653a5a1b3Sopenharmony_ci        self.readFilter.emit()
51753a5a1b3Sopenharmony_ci    def set_filter(self,preamp,coefs):
51853a5a1b3Sopenharmony_ci        self.sink.SetFilter(self.channel,dbus.Array(coefs),self.preamp)
51953a5a1b3Sopenharmony_ci        self.sync_timer.start(SYNC_TIMEOUT)
52053a5a1b3Sopenharmony_ci    def save_state(self):
52153a5a1b3Sopenharmony_ci        print('saving state')
52253a5a1b3Sopenharmony_ci        self.sink.SaveState()
52353a5a1b3Sopenharmony_ci    def load_profile(self,profile):
52453a5a1b3Sopenharmony_ci        self.sink.LoadProfile(self.channel,dbus.String(profile))
52553a5a1b3Sopenharmony_ci        self.sync_timer.start(SYNC_TIMEOUT)
52653a5a1b3Sopenharmony_ci    def flush_state(self):
52753a5a1b3Sopenharmony_ci        if self.sync_timer.isActive():
52853a5a1b3Sopenharmony_ci            self.sync_timer.stop()
52953a5a1b3Sopenharmony_ci            self.save_state()
53053a5a1b3Sopenharmony_ci
53153a5a1b3Sopenharmony_ci
53253a5a1b3Sopenharmony_cidef safe_log(k,b):
53353a5a1b3Sopenharmony_ci    i=0
53453a5a1b3Sopenharmony_ci    while k//b!=0:
53553a5a1b3Sopenharmony_ci        i+=1
53653a5a1b3Sopenharmony_ci        k=k//b
53753a5a1b3Sopenharmony_ci    return i
53853a5a1b3Sopenharmony_cidef hz2str(hz):
53953a5a1b3Sopenharmony_ci    p=safe_log(hz,10.0)
54053a5a1b3Sopenharmony_ci    if p<3:
54153a5a1b3Sopenharmony_ci        return '%dHz' %(hz,)
54253a5a1b3Sopenharmony_ci    elif hz%1000==0:
54353a5a1b3Sopenharmony_ci        return '%dKHz' %(hz/(10.0**3),)
54453a5a1b3Sopenharmony_ci    else:
54553a5a1b3Sopenharmony_ci        return '%.1fKHz' %(hz/(10.0**3),)
54653a5a1b3Sopenharmony_ci
54753a5a1b3Sopenharmony_cidef subdivide(xs, t_points):
54853a5a1b3Sopenharmony_ci    while len(xs)<t_points:
54953a5a1b3Sopenharmony_ci        m=[0]*(2*len(xs)-1)
55053a5a1b3Sopenharmony_ci        m[0:len(m):2]=xs
55153a5a1b3Sopenharmony_ci        for i in range(1,len(m),2):
55253a5a1b3Sopenharmony_ci            m[i]=(m[i-1]+m[i+1])//2
55353a5a1b3Sopenharmony_ci        xs=m
55453a5a1b3Sopenharmony_ci    p_drop=len(xs)-t_points
55553a5a1b3Sopenharmony_ci    p_drop_left=p_drop//2
55653a5a1b3Sopenharmony_ci    p_drop_right=p_drop-p_drop_left
55753a5a1b3Sopenharmony_ci    #print('xs',xs)
55853a5a1b3Sopenharmony_ci    #print('dropping %d, %d left, %d right' %(p_drop,p_drop_left,p_drop_right))
55953a5a1b3Sopenharmony_ci    c=len(xs)//2
56053a5a1b3Sopenharmony_ci    left=xs[0:p_drop_left*2:2]+xs[p_drop_left*2:c]
56153a5a1b3Sopenharmony_ci    right=list(reversed(xs[c:]))
56253a5a1b3Sopenharmony_ci    right=right[0:p_drop_right*2:2]+right[p_drop_right*2:]
56353a5a1b3Sopenharmony_ci    right=list(reversed(right))
56453a5a1b3Sopenharmony_ci    return left+right
56553a5a1b3Sopenharmony_ci
56653a5a1b3Sopenharmony_cidef main():
56753a5a1b3Sopenharmony_ci    dbus.mainloop.pyqt5.DBusQtMainLoop(set_as_default=True)
56853a5a1b3Sopenharmony_ci    app=QtWidgets.QApplication(sys.argv)
56953a5a1b3Sopenharmony_ci    qpaeq_main=QPaeq()
57053a5a1b3Sopenharmony_ci    qpaeq_main.show()
57153a5a1b3Sopenharmony_ci    sys.exit(app.exec_())
57253a5a1b3Sopenharmony_ci
57353a5a1b3Sopenharmony_ciif __name__=='__main__':
57453a5a1b3Sopenharmony_ci    main()
575