diff options
Diffstat (limited to 'src')
| -rw-r--r-- | src/modules/module-equalizer-sink.c | 87 | ||||
| -rwxr-xr-x | src/utils/qpaeq.py | 546 | 
2 files changed, 611 insertions, 22 deletions
| diff --git a/src/modules/module-equalizer-sink.c b/src/modules/module-equalizer-sink.c index 7c0ccd3a..0a2860b0 100644 --- a/src/modules/module-equalizer-sink.c +++ b/src/modules/module-equalizer-sink.c @@ -113,8 +113,11 @@ struct userdata {      float **Xs;      float ***Hs;//thread updatable copies of the freq response filters (magintude based)      pa_aupdate **a_H; -    pa_memchunk conv_buffer;      pa_memblockq *input_q; +    char *output_buffer; +    size_t output_buffer_length; +    size_t output_buffer_max_length; +    pa_memblockq *output_q;      pa_bool_t first_iteration;      pa_dbus_protocol *dbus_protocol; @@ -250,10 +253,11 @@ static int sink_process_msg_cb(pa_msgobject *o, int code, void *data, int64_t of                  pa_sink_get_latency_within_thread(u->sink_input->sink) +                  /* Add the latency internal to our sink input on top */ -                pa_bytes_to_usec(pa_memblockq_get_length(u->sink_input->thread_info.render_memblockq), &u->sink_input->sink->sample_spec); +                pa_bytes_to_usec(pa_memblockq_get_length(u->output_q), &u->sink_input->sink->sample_spec) + +                pa_bytes_to_usec(pa_memblockq_get_length(u->sink_input->thread_info.render_memblockq), &u->sink_input->sink->sample_spec) + +                pa_bytes_to_usec(pa_memblockq_get_length(u->input_q), &u->sink_input->sink->sample_spec);              //    pa_bytes_to_usec(u->samples_gathered * fs, &u->sink->sample_spec);              //+ pa_bytes_to_usec(u->latency * fs, ss) -            //+ pa_bytes_to_usec(pa_memblockq_get_length(u->input_q), ss);              return 0;          }      } @@ -337,7 +341,7 @@ static void sink_set_mute_cb(pa_sink *s) {      pa_sink_input_set_mute(u->sink_input, s->muted, s->save_muted);  } -#ifndef __SSE2__ +#if 1  //reference implementation  static void dsp_logic(      float * restrict dst,//used as a temp array too, needs to be fft_length! @@ -489,18 +493,42 @@ static void dsp_logic(  }  #endif -static void process_samples(struct userdata *u, pa_memchunk *tchunk){ +static void flatten_to_memblockq(struct userdata *u){ +    size_t mbs = pa_mempool_block_size_max(u->sink->core->mempool); +    pa_memchunk tchunk; +    char *dst; +    size_t i = 0; +    while(i < u->output_buffer_length){ +        tchunk.index = 0; +        tchunk.length = PA_MIN((u->output_buffer_length - i), mbs); +        tchunk.memblock = pa_memblock_new(u->sink->core->mempool, tchunk.length); +        //pa_log_debug("pushing %ld into the q", tchunk.length); +        dst = pa_memblock_acquire(tchunk.memblock); +        memcpy(dst, u->output_buffer + i, tchunk.length); +        pa_memblock_release(tchunk.memblock); +        pa_memblockq_push(u->output_q, &tchunk); +        pa_memblock_unref(tchunk.memblock); +        i += tchunk.length; +    } +} + +static void process_samples(struct userdata *u){      size_t fs = pa_frame_size(&(u->sink->sample_spec)); -    float *dst;      unsigned a_i;      float *H, X;      size_t iterations, offset;      pa_assert(u->samples_gathered >= u->window_size);      iterations = (u->samples_gathered - u->overlap_size) / u->R; -    tchunk->index = 0; -    tchunk->length = iterations * u->R * fs; -    tchunk->memblock = pa_memblock_new(u->sink->core->mempool, tchunk->length); -    dst = ((float*) pa_memblock_acquire(tchunk->memblock)); +    //make sure there is enough buffer memory allocated +    if(iterations * u->R * fs > u->output_buffer_max_length){ +        u->output_buffer_max_length = iterations * u->R * fs; +        if(u->output_buffer){ +            pa_xfree(u->output_buffer); +        } +        u->output_buffer = pa_xmalloc(u->output_buffer_max_length); +    } +    u->output_buffer_length = iterations * u->R * fs; +      for(size_t iter = 0; iter < iterations; ++iter){          offset = iter * u->R * fs;          for(size_t c = 0;c < u->channels; c++) { @@ -526,14 +554,14 @@ static void process_samples(struct userdata *u, pa_memchunk *tchunk){                      u->work_buffer[i] = u->W[i] <= FLT_EPSILON ? u->work_buffer[i] : u->work_buffer[i] / u->W[i];                  }              } -            pa_sample_clamp(PA_SAMPLE_FLOAT32NE, (uint8_t *) (dst + c) + offset, fs, u->work_buffer, sizeof(float), u->R); +            pa_sample_clamp(PA_SAMPLE_FLOAT32NE, (uint8_t *) (((float *)u->output_buffer) + c) + offset, fs, u->work_buffer, sizeof(float), u->R);          }          if(u->first_iteration){              u->first_iteration = FALSE;          }          u->samples_gathered -= u->R;      } -    pa_memblock_release(tchunk->memblock); +    flatten_to_memblockq(u);  }  static void input_buffer(struct userdata *u, pa_memchunk *in){ @@ -556,7 +584,8 @@ static void input_buffer(struct userdata *u, pa_memchunk *in){  /* Called from I/O thread context */  static int sink_input_pop_cb(pa_sink_input *i, size_t nbytes, pa_memchunk *chunk) {      struct userdata *u; -    size_t fs, target_samples, mbs; +    size_t fs, target_samples; +    size_t mbs;      //struct timeval start, end;      pa_memchunk tchunk;      pa_sink_input_assert_ref(i); @@ -564,13 +593,17 @@ static int sink_input_pop_cb(pa_sink_input *i, size_t nbytes, pa_memchunk *chunk      pa_assert(chunk);      pa_assert(u->sink);      fs = pa_frame_size(&(u->sink->sample_spec)); -    nbytes = PA_MIN(nbytes, pa_mempool_block_size_max(u->sink->core->mempool)); -    target_samples = PA_ROUND_UP(nbytes / fs, u->R);      mbs = pa_mempool_block_size_max(u->sink->core->mempool); -    //pa_log_debug("vanilla mbs = %ld",mbs); -    mbs = PA_ROUND_DOWN(mbs / fs, u->R); -    mbs = PA_MAX(mbs, u->R); -    target_samples = PA_MAX(target_samples, mbs); +    if(pa_memblockq_get_length(u->output_q) > 0){ +        //pa_log_debug("qsize is %ld", pa_memblockq_get_length(u->output_q)); +        goto END; +    } +    //nbytes = PA_MIN(nbytes, pa_mempool_block_size_max(u->sink->core->mempool)); +    target_samples = PA_ROUND_UP(nbytes / fs, u->R); +    ////pa_log_debug("vanilla mbs = %ld",mbs); +    //mbs = PA_ROUND_DOWN(mbs / fs, u->R); +    //mbs = PA_MAX(mbs, u->R); +    //target_samples = PA_MAX(target_samples, mbs);      //pa_log_debug("target samples: %ld", target_samples);      if(u->first_iteration){          //allocate request_size @@ -594,7 +627,7 @@ static int sink_input_pop_cb(pa_sink_input *i, size_t nbytes, pa_memchunk *chunk          pa_assert(input_remaining > 0);          while(pa_memblockq_peek(u->input_q, &tchunk) < 0){              //pa_sink_render(u->sink, input_remaining * fs, &tchunk); -            pa_sink_render_full(u->sink, input_remaining * fs, &tchunk); +            pa_sink_render_full(u->sink, PA_MIN(input_remaining * fs, mbs), &tchunk);              pa_assert(tchunk.memblock);              pa_memblockq_push(u->input_q, &tchunk);              pa_memblock_unref(tchunk.memblock); @@ -619,11 +652,13 @@ static int sink_input_pop_cb(pa_sink_input *i, size_t nbytes, pa_memchunk *chunk      pa_assert(u->R < u->window_size);      //pa_rtclock_get(&start);      /* process a block */ -    process_samples(u, chunk); +    process_samples(u);      //pa_rtclock_get(&end);      //pa_log_debug("Took %0.6f seconds to process", (double) pa_timeval_diff(&end, &start) / PA_USEC_PER_SEC); - +END: +    pa_assert_se(pa_memblockq_peek(u->output_q, chunk) >= 0);      pa_assert(chunk->memblock); +    pa_memblockq_drop(u->output_q, chunk->length);      //pa_log_debug("gave %ld", chunk->length/fs);      //pa_log_debug("end pop");      return 0; @@ -1143,6 +1178,10 @@ int pa__init(pa_module*m) {      u->sink->set_mute = sink_set_mute_cb;      u->sink->userdata = u;      u->input_q = pa_memblockq_new(0,  MEMBLOCKQ_MAXLENGTH, 0, fs, 1, 1, 0, &u->sink->silence); +    u->output_q = pa_memblockq_new(0,  MEMBLOCKQ_MAXLENGTH, 0, fs, 1, 1, 0, NULL); +    u->output_buffer = NULL; +    u->output_buffer_length = 0; +    u->output_buffer_max_length = 0;      pa_sink_set_asyncmsgq(u->sink, master->asyncmsgq);      //pa_sink_set_fixed_latency(u->sink, pa_bytes_to_usec(u->R*fs, &ss)); @@ -1255,6 +1294,10 @@ void pa__done(pa_module*m) {      if (u->sink)          pa_sink_unref(u->sink); +    if(u->output_buffer){ +        pa_xfree(u->output_buffer); +    } +    pa_memblockq_free(u->output_q);      pa_memblockq_free(u->input_q);      fftwf_destroy_plan(u->inverse_plan); diff --git a/src/utils/qpaeq.py b/src/utils/qpaeq.py new file mode 100755 index 00000000..dca0701d --- /dev/null +++ b/src/utils/qpaeq.py @@ -0,0 +1,546 @@ +#!/usr/bin/env python +#    qpaeq is a equalizer interface for pulseaudio's equalizer sinks +#    Copyright (C) 2009  Jason Newton <nevion@gmail.com +# +#    This program is free software: you can redistribute it and/or modify +#    it under the terms of the GNU Affero General Public License as +#    published by the Free Software Foundation, either version 3 of the +#    License, or (at your option) any later version. +# +#    This program is distributed in the hope that it will be useful, +#    but WITHOUT ANY WARRANTY; without even the implied warranty of +#    MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the +#    GNU Affero General Public License for more details. +# +#    You should have received a copy of the GNU Affero General Public License +#    along with this program.  If not, see <http://www.gnu.org/licenses/>. + + +import os,math,sys +import PyQt4,sip +from PyQt4 import QtGui,QtCore +from functools import partial + +import dbus.mainloop.qt +import dbus + +import signal +signal.signal(signal.SIGINT, signal.SIG_DFL) +SYNC_TIMEOUT = 4*1000 + +CORE_PATH = "/org/pulseaudio/core1" +CORE_IFACE = "org.PulseAudio.Core1" +def connect(): +    if 'PULSE_DBUS_SERVER' in os.environ: +        address = os.environ['PULSE_DBUS_SERVER'] +    else: +        bus = dbus.SessionBus() # Should be UserBus, but D-Bus doesn't implement that yet. +        server_lookup = bus.get_object('org.PulseAudio1', '/org/pulseaudio/server_lookup1') +        address = server_lookup.Get('org.PulseAudio.ServerLookup1', 'Address', dbus_interface='org.freedesktop.DBus.Properties') +    return dbus.connection.Connection(address) + + +#TODO: signals: sink Filter changed, sink reconfigured (window size) (sink iface) +#TODO: manager signals: new sink, removed sink, new profile, removed profile +#TODO: add support for changing of window_size 1000-fft_size (adv option) +#TODO: reconnect support loop 1 second trying to reconnect +#TODO: just resample the filters for profiles when loading to different sizes +#TODO: add preamp +prop_iface='org.freedesktop.DBus.Properties' +eq_iface='org.PulseAudio.Ext.Equalizing1.Equalizer' +device_iface='org.PulseAudio.Core1.Device' +class QPaeq(QtGui.QWidget): +    manager_path='/org/pulseaudio/equalizing1' +    manager_iface='org.PulseAudio.Ext.Equalizing1.Manager' +    core_iface='org.PulseAudio.Core1' +    core_path='/org/pulseaudio/core1' +    def __init__(self): +        QtGui.QWidget.__init__(self) +        self.setWindowTitle('qpaeq') +        self.slider_widget=None +        self.sink_name=None +        self.filter_state=None + +        self.create_layout() + +        self.set_connection() +        self.connect_to_sink(self.sinks[0]) +        self.set_callbacks() +        self.setMinimumSize(self.sizeHint()) + +    def create_layout(self): +        self.main_layout=QtGui.QVBoxLayout() +        self.setLayout(self.main_layout) +        toprow_layout=QtGui.QHBoxLayout() +        sizePolicy = QtGui.QSizePolicy(QtGui.QSizePolicy.Preferred, QtGui.QSizePolicy.Fixed) +        sizePolicy.setHorizontalStretch(0) +        sizePolicy.setVerticalStretch(0) +        #sizePolicy.setHeightForWidth(self.profile_box.sizePolicy().hasHeightForWidth()) + +        toprow_layout.addWidget(QtGui.QLabel('Sink')) +        self.sink_box = QtGui.QComboBox() +        self.sink_box.setSizePolicy(sizePolicy) +        self.sink_box.setDuplicatesEnabled(False) +        self.sink_box.setInsertPolicy(QtGui.QComboBox.InsertAlphabetically) +        #self.sink_box.setSizeAdjustPolicy(QtGui.QComboBox.AdjustToContents) +        toprow_layout.addWidget(self.sink_box) + +        toprow_layout.addWidget(QtGui.QLabel('Channel')) +        self.channel_box = QtGui.QComboBox() +        self.channel_box.setSizePolicy(sizePolicy) +        toprow_layout.addWidget(self.channel_box) + +        toprow_layout.addWidget(QtGui.QLabel('Preset')) +        self.profile_box = QtGui.QComboBox() +        self.profile_box.setSizePolicy(sizePolicy) +        self.profile_box.setInsertPolicy(QtGui.QComboBox.InsertAlphabetically) +        #self.profile_box.setSizeAdjustPolicy(QtGui.QComboBox.AdjustToContents) +        toprow_layout.addWidget(self.profile_box) + +        large_icon_size=self.style().pixelMetric(QtGui.QStyle.PM_LargeIconSize) +        large_icon_size=QtCore.QSize(large_icon_size,large_icon_size) +        save_profile=QtGui.QToolButton() +        save_profile.setIcon(self.style().standardIcon(QtGui.QStyle.SP_DriveFDIcon)) +        save_profile.setIconSize(large_icon_size) +        save_profile.setToolButtonStyle(QtCore.Qt.ToolButtonIconOnly) +        save_profile.clicked.connect(self.save_profile) +        remove_profile=QtGui.QToolButton() +        remove_profile.setIcon(self.style().standardIcon(QtGui.QStyle.SP_TrashIcon)) +        remove_profile.setIconSize(large_icon_size) +        remove_profile.setToolButtonStyle(QtCore.Qt.ToolButtonIconOnly) +        remove_profile.clicked.connect(self.remove_profile) +        toprow_layout.addWidget(save_profile) +        toprow_layout.addWidget(remove_profile) + +        reset_button = QtGui.QPushButton('Reset') +        reset_button.clicked.connect(self.reset) +        toprow_layout.addStretch() +        toprow_layout.addWidget(reset_button) +        self.layout().addLayout(toprow_layout) + +        self.profile_box.activated.connect(self.load_profile) +        self.channel_box.activated.connect(self.select_channel) +    def connect_to_sink(self,name): +        #TODO: clear slots for profile buttons + +        #flush any pending saves for other sinks +        if self.filter_state is not None: +            self.filter_state.flush_state() +        sink=self.connection.get_object(object_path=name) +        self.sink_props=dbus.Interface(sink,dbus_interface=prop_iface) +        self.sink=dbus.Interface(sink,dbus_interface=eq_iface) +        self.filter_state=FilterState(sink) +        #sample_rate,filter_rate,channels,channel) + +        self.channel_box.clear() +        self.channel_box.addItem('All',self.filter_state.channels) +        for i in xrange(self.filter_state.channels): +            self.channel_box.addItem('%d' %(i+1,),i) +        self.setMinimumSize(self.sizeHint()) + +        self.set_slider_widget(SliderArray(self.filter_state)) + +        self.sink_name=name +        #set the signal listener for this sink +        core=self._get_core() +        #temporary hack until signal filtering works properly +        core.ListenForSignal('',[dbus.ObjectPath(self.sink_name),dbus.ObjectPath(self.manager_path)]) +        #for x in ['FilterChanged']: +        #    core.ListenForSignal("%s.%s" %(self.eq_iface,x),[dbus.ObjectPath(self.sink_name)]) +        #core.ListenForSignal(self.eq_iface,[dbus.ObjectPath(self.sink_name)]) +        self.sink.connect_to_signal('FilterChanged',self.read_filter) + +    def set_slider_widget(self,widget): +        layout=self.layout() +        if self.slider_widget is not None: +            i=layout.indexOf(self.slider_widget) +            layout.removeWidget(self.slider_widget) +            self.slider_widget.deleteLater() +            layout.insertWidget(i,self.slider_widget) +        else: +            layout.addWidget(widget) +        self.slider_widget=widget +        self.read_filter() +    def _get_core(self): +        core_obj=self.connection.get_object(object_path=self.core_path) +        core=dbus.Interface(core_obj,dbus_interface=self.core_iface) +        return core +    def sink_added(self,sink): +        #TODO: preserve selected sink +        self.update_sinks() +    def sink_removed(self,sink): +        #TODO: preserve selected sink, try connecting to backup otherwise +        if sink==self.sink_name: +            #connect to new sink? +            pass +        self.update_sinks() +    def save_profile(self): +        #popup dialog box for name +        current=self.profile_box.currentIndex() +        profile,ok=QtGui.QInputDialog.getItem(self,'Preset Name','Preset',self.profiles,current) +        if not ok or profile=='': +            return +        if profile in self.profiles: +            mbox=QtGui.QMessageBox(self) +            mbox.setText('%s preset already exists'%(profile,)) +            mbox.setInformativeText('Do you want to save over it?') +            mbox.setStandardButtons(mbox.Save|mbox.Discard|mbox.Cancel) +            mbox.setDefaultButton(mbox.Save) +            ret=mbox.exec_() +            if ret!=mbox.Save: +                return +        self.sink.SaveProfile(self.filter_state.channel,dbus.String(profile)) +        if self.filter_state.channel==self.filter_state.channels: +            for x in range(1,self.filter_state.channels): +                self.sink.LoadProfile(x,dbus.String(profile)) +    def remove_profile(self): +        #find active profile name, remove it +        profile=self.profile_box.currentText() +        manager=dbus.Interface(self.manager_obj,dbus_interface=self.manager_iface) +        manager.RemoveProfile(dbus.String(profile)) +    def load_profile(self,x): +        profile=self.profile_box.itemText(x) +        self.filter_state.load_profile(profile) +    def select_channel(self,x): +        self.filter_state.channel = self.channel_box.itemData(x).toPyObject() +        self._set_profile_name() +        self.filter_state.readback() + +    #TODO: add back in preamp! +    #print frequencies +    #main_layout.addLayout(self.create_slider(partial(self.update_coefficient,0), +    #    'Preamp')[0] +    #) +    def set_connection(self): +        self.connection=connect() +        self.manager_obj=self.connection.get_object(object_path=self.manager_path) +        manager_props=dbus.Interface(self.manager_obj,dbus_interface=prop_iface) +        self.sinks=manager_props.Get(self.manager_iface,'EqualizedSinks') +    def set_callbacks(self): +        manager=dbus.Interface(self.manager_obj,dbus_interface=self.manager_iface) +        manager.connect_to_signal('ProfilesChanged',self.update_profiles) +        manager.connect_to_signal('SinkAdded',self.sink_added) +        manager.connect_to_signal('SinkRemoved',self.sink_removed) +        #self._get_core().ListenForSignal(self.manager_iface,[]) +        #self._get_core().ListenForSignal(self.manager_iface,[dbus.ObjectPath(self.manager_path)]) +        #core=self._get_core() +        #for x in ['ProfilesChanged','SinkAdded','SinkRemoved']: +        #    core.ListenForSignal("%s.%s" %(self.manager_iface,x),[dbus.ObjectPath(self.manager_path)]) +        self.update_profiles() +        self.update_sinks() +    def update_profiles(self): +        #print 'update profiles called!' +        manager_props=dbus.Interface(self.manager_obj,dbus_interface=prop_iface) +        self.profiles=manager_props.Get(self.manager_iface,'Profiles') +        self.profile_box.blockSignals(True) +        self.profile_box.clear() +        self.profile_box.addItems(self.profiles) +        self.profile_box.blockSignals(False) +        self._set_profile_name() +    def update_sinks(self): +        self.sink_box.blockSignals(True) +        self.sink_box.clear() +        for x in self.sinks: +            sink=self.connection.get_object(object_path=x) +            sink_props=dbus.Interface(sink,dbus_interface=prop_iface) +            simple_name=sink_props.Get(device_iface,'Name') +            self.sink_box.addItem(simple_name,x) +        self.sink_box.blockSignals(False) +        self.sink_box.setMinimumSize(self.sink_box.sizeHint()) +    def read_filter(self): +        #print self.filter_frequencies +        self.filter_state.readback() +    def reset(self): +        coefs=dbus.Array([1/math.sqrt(2.0)]*(self.filter_state.filter_rate//2+1)) +        preamp=1.0 +        self.filter_state.set_filter(preamp,coefs) +    def _set_profile_name(self): +        self.profile_box.blockSignals(True) +        profile_name=self.sink.BaseProfile(self.filter_state.channel) +        if profile_name is not None: +            i=self.profile_box.findText(profile_name) +            if i>=0: +                self.profile_box.setCurrentIndex(i) +        self.profile_box.blockSignals(False) + + +class SliderArray(QtGui.QWidget): +    def __init__(self,filter_state,parent=None): +        super(SliderArray,self).__init__(parent) +        #self.setStyleSheet('padding: 0px; border-width: 0px; margin: 0px;') +        #self.setStyleSheet('font-size: 7pt; font-family: monospace;'+outline%('blue')) +        self.filter_state=filter_state +        self.setLayout(QtGui.QHBoxLayout()) +        self.sub_array=None +        self.set_sub_array(SliderArraySub(self.filter_state)) +        self.inhibit_resize=0 +    def set_sub_array(self,widget): +        if self.sub_array is not None: +            self.layout().removeWidget(self.sub_array) +            self.sub_array.disconnect_signals() +            self.sub_array.deleteLater() +        self.sub_array=widget +        self.layout().addWidget(self.sub_array) +        self.sub_array.connect_signals() +        self.filter_state.readback() +    def resizeEvent(self,event): +        super(SliderArray,self).resizeEvent(event) +        if self.inhibit_resize==0: +            self.inhibit_resize+=1 +            #self.add_sliders_to_fit() +            t=QtCore.QTimer(self) +            t.setSingleShot(True) +            t.setInterval(0) +            t.timeout.connect(partial(self.add_sliders_to_fit,event)) +            t.start() +    def add_sliders_to_fit(self,event): +        if event.oldSize().width()>0 and event.size().width()>0: +            i=len(self.filter_state.frequencies)*int(round(float(event.size().width())/event.oldSize().width())) +        else: +            i=len(self.filter_state.frequencies) + +        t_w=self.size().width() +        def evaluate(filter_state, target, variable): +            base_freqs=self.filter_state.freq_proper(self.filter_state.DEFAULT_FREQUENCIES) +            filter_state._set_frequency_values(subdivide(base_freqs,variable)) +            new_widget=SliderArraySub(filter_state) +            w=new_widget.sizeHint().width() +            return w-target +        def searcher(initial,evaluator): +            i=initial +            def d(e): return 1 if e>=0 else -1 +            error=evaluator(i) +            old_direction=d(error) +            i-=old_direction +            while True: +                error=evaluator(i) +                direction=d(error) +                if direction!=old_direction: +                    k=i-1 +                    #while direction<0 and error!=0: +                    #    k-=1 +                    #    error=evaluator(i) +                    #    direction=d(error) +                    return k, evaluator(k) +                i-=direction +                old_direction=direction +        searcher(i,partial(evaluate,self.filter_state,t_w)) +        self.set_sub_array(SliderArraySub(self.filter_state)) +        self.inhibit_resize-=1 + +class SliderArraySub(QtGui.QWidget): +    def __init__(self,filter_state,parent=None): +        super(SliderArraySub,self).__init__(parent) +        self.filter_state=filter_state +        self.setLayout(QtGui.QGridLayout()) +        self.slider=[None]*len(self.filter_state.frequencies) +        self.label=[None]*len(self.slider) +        #self.setStyleSheet('padding: 0px; border-width: 0px; margin: 0px;') +        #self.setStyleSheet('font-size: 7pt; font-family: monospace;'+outline%('blue')) +        qt=QtCore.Qt +        #self.layout().setHorizontalSpacing(1) +        def add_slider(slider,label, c): +            self.layout().addWidget(slider,0,c,qt.AlignHCenter) +            self.layout().addWidget(label,1,c,qt.AlignHCenter) +            self.layout().setColumnMinimumWidth(c,max(label.sizeHint().width(),slider.sizeHint().width())) +        def create_slider(slider_label): +            slider=QtGui.QSlider(QtCore.Qt.Vertical,self) +            label=SliderLabel(slider_label,filter_state,self) +            slider.setRange(-1000,2000) +            slider.setSingleStep(1) +            return (slider,label) +        self.preamp_slider,self.preamp_label=create_slider('Preamp') +        add_slider(self.preamp_slider,self.preamp_label,0) +        for i,hz in enumerate(self.filter_state.frequencies): +            slider,label=create_slider(self.hz2label(hz)) +            self.slider[i]=slider +            #slider.setStyleSheet('font-size: 7pt; font-family: monospace;'+outline%('red',)) +            self.label[i]=label +            c=i+1 +            add_slider(slider,label,i+1) +    def hz2label(self, hz): +        if hz==0: +            label_text='DC' +        elif hz==self.filter_state.sample_rate//2: +            label_text='Coda' +        else: +            label_text=hz2str(hz) +        return label_text + +    def connect_signals(self): +        def connect(writer,reader,slider,label): +            slider.valueChanged.connect(writer) +            self.filter_state.readFilter.connect(reader) +            label_cb=partial(slider.setValue,0) +            label.clicked.connect(label_cb) +            return label_cb + +        self.preamp_writer_cb=self.write_preamp +        self.preamp_reader_cb=self.sync_preamp +        self.preamp_label_cb=connect(self.preamp_writer_cb, +                self.preamp_reader_cb, +                self.preamp_slider, +                self.preamp_label) +        self.writer_callbacks=[None]*len(self.slider) +        self.reader_callbacks=[None]*len(self.slider) +        self.label_callbacks=[None]*len(self.label) +        for i in range(len(self.slider)): +            self.writer_callbacks[i]=partial(self.write_coefficient,i) +            self.reader_callbacks[i]=partial(self.sync_coefficient,i) +            self.label_callbacks[i]=connect(self.writer_callbacks[i], +                    self.reader_callbacks[i], +                    self.slider[i], +                    self.label[i]) +    def disconnect_signals(self): +        def disconnect(writer,reader,label_cb,slider,label): +            slider.valueChanged.disconnect(writer) +            self.filter_state.readFilter.disconnect(reader) +            label.clicked.disconnect(label_cb) +        disconnect(self.preamp_writer_cb, self.preamp_reader_cb, +                self.preamp_label_cb, self.preamp_slider, self.preamp_label) +        for i in range(len(self.slider)): +            disconnect(self.writer_callbacks[i], +                    self.reader_callbacks[i], +                    self.label_callbacks[i], +                    self.slider[i], +                    self.label[i]) + +    def write_preamp(self, v): +        self.filter_state.preamp=self.slider2coef(v) +        self.filter_state.seed() +    def sync_preamp(self): +        self.preamp_slider.blockSignals(True) +        self.preamp_slider.setValue(self.coef2slider(self.filter_state.preamp)) +        self.preamp_slider.blockSignals(False) + + +    def write_coefficient(self,i,v): +        self.filter_state.coefficients[i]=self.slider2coef(v)/math.sqrt(2.0) +        self.filter_state.seed() +    def sync_coefficient(self,i): +        slider=self.slider[i] +        slider.blockSignals(True) +        slider.setValue(self.coef2slider(math.sqrt(2.0)*self.filter_state.coefficients[i])) +        slider.blockSignals(False) +    @staticmethod +    def slider2coef(x): +        return (1.0+(x/1000.0)) +    @staticmethod +    def coef2slider(x): +        return int((x-1.0)*1000) +outline='border-width: 1px; border-style: solid; border-color: %s;' + +class SliderLabel(QtGui.QLabel): +    clicked=QtCore.pyqtSignal() +    def __init__(self,label_text,filter_state,parent=None): +        super(SliderLabel,self).__init__(parent) +        self.setStyleSheet('font-size: 7pt; font-family: monospace;') +        self.setText(label_text) +        self.setMinimumSize(self.sizeHint()) +    def mouseDoubleClickEvent(self, event): +        self.clicked.emit() +        super(SliderLabel,self).mouseDoubleClickEvent(event) + +#until there are server side state savings, do it in the client but try and avoid +#simulaneous broadcasting situations +class FilterState(QtCore.QObject): +    #DEFAULT_FREQUENCIES=map(float,[25,50,75,100,150,200,300,400,500,800,1e3,1.5e3,3e3,5e3,7e3,10e3,15e3,20e3]) +    DEFAULT_FREQUENCIES=[31.75,63.5,125,250,500,1e3,2e3,4e3,8e3,16e3] +    readFilter=QtCore.pyqtSignal() +    def __init__(self,sink): +        super(FilterState,self).__init__() +        self.sink_props=dbus.Interface(sink,dbus_interface=prop_iface) +        self.sink=dbus.Interface(sink,dbus_interface=eq_iface) +        self.sample_rate=self.get_eq_attr('SampleRate') +        self.filter_rate=self.get_eq_attr('FilterSampleRate') +        self.channels=self.get_eq_attr('NChannels') +        self.channel=self.channels +        self.set_frequency_values(self.DEFAULT_FREQUENCIES) +        self.sync_timer=QtCore.QTimer() +        self.sync_timer.setSingleShot(True) +        self.sync_timer.timeout.connect(self.save_state) + +    def get_eq_attr(self,attr): +        return self.sink_props.Get(eq_iface,attr) +    def freq_proper(self,xs): +        return [0]+xs+[self.sample_rate//2] +    def _set_frequency_values(self,freqs): +        self.frequencies=freqs +        #print 'base',self.frequencies +        self.filter_frequencies=map(lambda x: int(round(x)), \ +                self.translate_rates(self.filter_rate,self.sample_rate, +                    self.frequencies) \ +                ) +        self.coefficients=[0.0]*len(self.frequencies) +        self.preamp=1.0 +    def set_frequency_values(self,freqs): +        self._set_frequency_values(self.freq_proper(freqs)) +    @staticmethod +    def translate_rates(dst,src,rates): +        return list(map(lambda x: x*dst/src,rates)) +    def seed(self): +        self.sink.SeedFilter(self.channel,self.filter_frequencies,self.coefficients,self.preamp) +        self.sync_timer.start(SYNC_TIMEOUT) +    def readback(self): +        coefs,preamp=self.sink.FilterAtPoints(self.channel,self.filter_frequencies) +        self.coefficients=coefs +        self.preamp=preamp +        self.readFilter.emit() +    def set_filter(self,preamp,coefs): +        self.sink.SetFilter(self.channel,dbus.Array(coefs),self.preamp) +        self.sync_timer.start(SYNC_TIMEOUT) +    def save_state(self): +        print 'saving state' +        self.sink.SaveState() +    def load_profile(self,profile): +        self.sink.LoadProfile(self.channel,dbus.String(profile)) +        self.sync_timer.start(SYNC_TIMEOUT) +    def flush_state(self): +        if self.sync_timer.isActive(): +            self.sync_timer.stop() +            self.save_state() + + +def safe_log(k,b): +    i=0 +    while k//b!=0: +        i+=1 +        k=k//b +    return i +def hz2str(hz): +    p=safe_log(hz,10.0) +    if p<3: +        return '%dHz' %(hz,) +    elif hz%1000==0: +        return '%dKHz' %(hz/(10.0**3),) +    else: +        return '%.1fKHz' %(hz/(10.0**3),) + +def subdivide(xs, t_points): +    while len(xs)<t_points: +        m=[0]*(2*len(xs)-1) +        m[0:len(m):2]=xs +        for i in range(1,len(m),2): +            m[i]=(m[i-1]+m[i+1])//2 +        xs=m +    p_drop=len(xs)-t_points +    p_drop_left=p_drop//2 +    p_drop_right=p_drop-p_drop_left +    #print 'xs',xs +    #print 'dropping %d, %d left, %d right' %(p_drop,p_drop_left,p_drop_right) +    c=len(xs)//2 +    left=xs[0:p_drop_left*2:2]+xs[p_drop_left*2:c] +    right=list(reversed(xs[c:])) +    right=right[0:p_drop_right*2:2]+right[p_drop_right*2:] +    right=list(reversed(right)) +    return left+right + +def main(): +    dbus.mainloop.qt.DBusQtMainLoop(set_as_default=True) +    app=QtGui.QApplication(sys.argv) +    qpaeq_main=QPaeq() +    qpaeq_main.show() +    sys.exit(app.exec_()) + +if __name__=='__main__': +    main() | 
