Module audian.databrowser

Functions

def marker_tip(x, y, data)

Classes

class DataBrowser (file_path, load_kwargs, plugins, channels, audio, acts, *args, **kwargs)

QWidget(parent: Optional[QWidget] = None, flags: Union[Qt.WindowFlags, Qt.WindowType] = Qt.WindowFlags())

Expand source code
class DataBrowser(QWidget):

    color_maps = [
                  'CET-R4',   # jet
                  'CET-L8',   # blue-pink-yellow
                  'CET-L16',  # black-blue-green-white
                  'CET-CBL2', # black-blue-yellow-white
                  'CET-L1',   # black-white
                  #pg.colormap.get('CET-L1').reverse(),   # white-black
                  'CET-L3',   # inferno
                  ]
    # see https://colorcet.holoviz.org/
    # and pyqtgraph.colormap module for useful functions.

    zoom_region = 0
    play_region = 1
    analyze_region = 2
    save_region = 3
    ask_region = 4
    
    sigRangesChanged = Signal(object, object)
    sigResolutionChanged = Signal()
    sigColorMapChanged = Signal()
    sigFilterChanged = Signal()
    sigEnvelopeChanged = Signal()
    sigTraceChanged = Signal(object, object, object)
    sigAudioChanged = Signal(object, object, object)

    
    def __init__(self, file_path, load_kwargs, plugins, channels,
                 audio, acts, *args, **kwargs):
        super().__init__(*args, **kwargs)

        # actions of main window:
        self.acts = acts

        # data:
        self.schannels = channels
        self.data = Data(file_path, **load_kwargs)
        self.plot_ranges = PlotRanges()
        self.trace_acts = []
        self.spec_acts = []
        
        # panels:
        self.panels = Panels()
        self.panels.add_trace()
        self.panels.add_spectrogram()

        # plugins:
        self.plugins = plugins
        self.analysis_table = None
        self.analyzers = []
        self.plugins.setup_traces(self)
        self.data.setup_traces()

        # channel selection:
        self.show_channels = None
        self.current_channel = 0
        self.selected_channels = []
        
        # view:
        self.setting = False
        
        self.trace_fracs = {0: 1, 1: 1, 2: 0.5, 3: 0.25, 4: 0.15}

        self.region_mode = DataBrowser.ask_region

        specs = self.data.get_trace_names(BufferedSpectrogram)
        self.spectrogram = specs[0] if len(specs) > 0 else ''
        self.spectrogram_power = ''
        
        self.grids = 0
        self.show_traces = True
        self.show_specs = 0
        self.show_powers = False
        self.show_cbars = False
        self.show_fulldata = True
        
        # auto scroll:
        self.scroll_step = 0.0
        self.scroll_timer = QTimer(self)
        self.scroll_timer.timeout.connect(self.scroll_further)

        # audio:
        self.audio = audio
        self.audio_timer = QTimer(self)
        self.audio_timer.timeout.connect(self.mark_audio)
        self.audio_time = 0.0
        self.audio_use_heterodyne = False
        self.audio_heterodyne_freq = 40000.0
        self.audio_rate_fac = 1.0
        self.audio_tmax = 0.0
        self.audio_markers = [] # vertical lines showing position while playing

        # window:
        self.vbox = QVBoxLayout(self)
        self.vbox.setContentsMargins(0, 0, 0, 0)
        self.vbox.setSpacing(0)
        self.setEnabled(False)
        self.toolbar = None
        self.audiofacw = None
        self.nfftw = None

        # cross hair:
        self.xpos_action = None
        self.ypos_action = None
        self.zpos_action = None
        self.cross_hair = False
        self.marker_data = MarkerData()
        self.marker_model = MarkerDataModel(self.marker_data)
        self.marker_labels = []
        self.marker_labels.append(MarkerLabel('start', 's', 'yellow'))
        self.marker_labels.append(MarkerLabel('end', 'e', 'blue'))
        self.marker_labels_model = MarkerLabelsModel(self.marker_labels,
                                                     self.acts)
        self.marker_orig_acts = []
        
        # plots:
        self.color_map = 0  # index into color_maps
        self.figs = []      # all GraphicsLayoutWidgets - one for each channel
        self.border_height = 1
        self.borders = []
        self.sig_proxies = []
        # nested lists (channel, panel):
        self.axs  = []      # all plots
        self.axgs = []      # plots with grids
        # lists with marker labels and regions:
        self.trace_labels = [] # labels on traces
        self.trace_region_labels = [] # regions with labels on traces
        self.spec_labels = []  # labels on spectrograms
        self.spec_region_labels = [] # regions with labels on spectrograms
        # full traces:
        self.datafig = None
        # default colors:
        text_color = self.palette().color(QPalette.WindowText)
        pg.setConfigOption('background', 'black')
        pg.setConfigOption('foreground', text_color)

        
    def __del__(self):
        self.close()


    def get_trace(self, name):
        return self.data[name]


    def add_trace(self, trace):
        self.data.add_trace(trace)


    def remove_trace(self, name):
        self.data.remove_trace(name)


    def clear_traces(self):
        self.data.clear_traces()


    def get_analyzer(self, name):
        for a in self.analyzers:
            if name.lower() == a.name.lower():
                return a
        return None


    def add_analyzer(self, analyzer):
        self.analyzers.append(analyzer)


    def remove_analyzer(self, name):
        for k, a in enumerate(self.analyzers):
            if name.lower() == a.name.lower():
                del self.analyzers[k]


    def clear_analyzer(self):
        self.analyzers = []


    def add_to_panel_trace(self, trace_name, channel, plot_item):
        panel_name = self.data[trace_name].panel
        self.panels[panel_name].add_item(plot_item, channel, False)


    def toggle_trace(self, checked, name):
        self.data.set_visible(name, checked)
        self.adjust_layout(self.width(), self.height())
        self.sigTraceChanged.emit(self, checked, name)

        
    def set_trace(self, checked, name):
        self.data.set_visible(name, checked)
        for act in self.trace_acts:
            if act.text() == name:
                act.blockSignals(True)
                act.setChecked(checked)
                act.blockSignals(False)
                
        
    def open(self, gui, unwrap, unwrap_clip, highpass_cutoff, lowpass_cutoff):
        # load data:
        self.data.open(unwrap, unwrap_clip)
        if self.data.data is None:
            return
        self.marker_data.file_path = self.data.file_path

        # add traces to menu:
        self.trace_acts = []
        for t in self.data.traces:
            act = QAction(t.name, self)
            act.setCheckable(True)
            act.setChecked(True)
            act.toggled.connect(lambda x, name=t.name: self.toggle_trace(x, name))
            self.trace_acts.append(act)
        # add spectrogram selection to menu:
        self.spec_acts = []
        for spec in self.data.get_trace_names(BufferedSpectrogram):
            act = QAction(spec, self)
            act.setCheckable(True)
            act.setChecked(False)
            act.toggled.connect(lambda x, name=spec: self.set_spectrogram(x, name))
            self.spec_acts.append(act)
            
        # ranges:
        self.plot_ranges.setup(self.data.channels)
        
        # requested filtering:
        if 'filtered' in self.data:
            filtered = self.data['filtered']
            filter_changed = False
            if highpass_cutoff is not None:
                filtered.highpass_cutoff = highpass_cutoff
                filter_changed = True
            if lowpass_cutoff is not None:
                filtered.lowpass_cutoff = lowpass_cutoff
                filter_changed = True
            if filter_changed:
                filtered.update()
                
        # setup channel selection:
        if self.show_channels is None:
            if len(self.schannels) == 0:
                self.show_channels = list(range(self.data.channels))
            else:
                self.show_channels = [c for c in self.schannels if c < self.data.channels]
        else:
            self.show_channels = [c for c in self.show_channels if c < self.data.channels]
        if len(self.show_channels) == 0:
            self.show_channels = [0]
        
        self.current_channel = self.show_channels[0]
        self.selected_channels = list(range(self.data.channels))

        # load marker data:
        locs, labels = self.data.data.markers()
        self.marker_data.set_markers(locs, labels, self.data.rate)
        if len(labels) > 0:
            lbls = np.unique(labels[:,0])
            for i, l in enumerate(lbls):
                self.marker_labels.append(MarkerLabel(l, l[0].lower(),
                                list(colors.keys())[i % len(colors.keys())]))

        # make panels:
        self.panels.fill(self.data)
        self.panels.insert_spacers()
            
        # setup plots:
        self.figs = []     # all GraphicsLayoutWidgets - one for each channel
        self.borders = []
        self.sig_proxies = []
        # nested lists (channel, panel):
        self.axs  = []      # all plots
        self.axgs = []      # plots with grids
        # lists with marker labels and regions:
        self.trace_labels = [] # labels on traces
        self.trace_region_labels = [] # regions with labels on traces
        self.spec_labels = []  # labels on spectrograms
        self.spec_region_labels = [] # regions with labels on spectrograms
        self.audio_markers = [] # vertical line showing position while playing
        # font size:
        xwidth = self.fontMetrics().averageCharWidth()
        xwidth2 = xwidth/2
        self.border_height = 0.5*xwidth
        for c in range(self.data.channels):
            self.axs.append([])
            self.axgs.append([])
            self.audio_markers.append([])
            
            # one figure per channel:
            fig = pg.GraphicsLayoutWidget()
            fig.setBackground(None)
            fig.ci.layout.setContentsMargins(xwidth2, xwidth2, xwidth2, xwidth2)
            fig.ci.layout.setVerticalSpacing(-1)
            fig.ci.layout.setHorizontalSpacing(xwidth2)
            fig.ci.layout.setHorizontalSpacing(0)
            fig.setVisible(c in self.show_channels)

            self.vbox.addWidget(fig)
            self.figs.append(fig)
            
            # border:
            border = QGraphicsRectItem()
            border.setZValue(-1000)
            border.setPen(pg.mkPen('#aaaaaa', width=self.border_height))
            fig.scene().addItem(border)
            fig.sigDeviceRangeChanged.connect(self.update_borders)
            self.borders.append(border)
                
            # setup plot panels:
            row = 0
            for name in reversed(self.panels):
                panel = self.panels[name]
                # spacer:
                if panel.is_spacer():
                    axsp = fig.addLayout(row=row, col=0)
                    axsp.setContentsMargins(0, 0, 0, 0)
                    panel.add_ax(row, axsp)
                # trace plot:
                elif panel.is_trace():
                    ylabel = panel.name if panel.name != 'trace' else ''
                    axt = TimePlot(panel.ax_spec, c, self, xwidth, ylabel)
                    axt.polish()
                    self.audio_markers[-1].append(axt.vmarker)
                    fig.addItem(axt, row=row, col=0)
                    self.axgs[-1].append(axt)
                    self.axs[-1].append(axt)
                    panel.add_ax(row, axt)
                    panel.add_traces(c, self.data)
                    self.plot_ranges.add_plot(axt)
                    # add marker labels:
                    labels = []
                    for l in self.marker_labels:
                        label = pg.ScatterPlotItem(size=10, hoverSize=20,
                                                   hoverable=True,
                                                   pen=pg.mkPen(None),
                                                   brush=pg.mkBrush(l.color))
                        axt.addItem(label)
                        labels.append(label)
                    self.trace_labels.append(labels)
                    self.trace_region_labels.append([])
                # spectrogram:
                elif panel.is_spectrogram():
                    axs = SpectrogramPlot(panel.ax_spec, c, self, xwidth,
                                          self.color_maps[self.color_map],
                                          self.show_cbars, self.show_powers)
                    axs.polish()
                    self.audio_markers[-1].append(axs.vmarker)
                    panel.add_ax(row, axs, axs.cbar)
                    panel.add_traces(c, self.data)
                    self.panels.add_power_ax(panel.name, row, axs.powerax)
                    self.plot_ranges.add_plot(axs)
                    self.plot_ranges.add_plot(axs.powerax)
                    fig.addItem(axs, row=row, col=0)
                    fig.addItem(axs.powerax, row=row, col=1)
                    fig.addItem(axs.cbar, row=row, col=2)
                    self.axgs[-1].append(axs)
                    self.axs[-1].append(axs)
                    # add marker labels:
                    labels = []
                    for l in self.marker_labels:
                        label = pg.ScatterPlotItem(size=10, pen=pg.mkPen(None),
                                                   brush=pg.mkBrush(l.color))
                        axs.addItem(label)
                        labels.append(label)
                    self.spec_labels.append(labels)
                    self.spec_region_labels.append([])
                # power:
                elif panel.is_power():
                    # was already set up with spectrogram
                    continue

                row += 1
                
            proxy = pg.SignalProxy(fig.scene().sigMouseMoved, rateLimit=60,
                                   slot=lambda x, c=c: self.mouse_moved(x, c))
            self.sig_proxies.append(proxy)
            proxy = pg.SignalProxy(fig.scene().sigMouseClicked, rateLimit=60,
                                   slot=lambda x, c=c: self.mouse_clicked(x, c))
            self.sig_proxies.append(proxy)
            
        self.setting = True
        self.plot_ranges.set_limits()
        self.plot_ranges.set_ranges()
        if not self.plot_ranges[Panel.amplitudes[0]].is_used():
            self.acts.zoom_xamplitude_in.setEnabled(False)
            self.acts.zoom_xamplitude_out.setEnabled(False)
            self.acts.zoom_xamplitude_in.setVisible(False)
            self.acts.zoom_xamplitude_out.setVisible(False)
        if not self.plot_ranges[Panel.amplitudes[1]].is_used():
            self.acts.zoom_yamplitude_in.setEnabled(False)
            self.acts.zoom_yamplitude_out.setEnabled(False)
            self.acts.zoom_yamplitude_in.setVisible(False)
            self.acts.zoom_yamplitude_out.setVisible(False)
        if not self.plot_ranges[Panel.amplitudes[2]].is_used():
            self.acts.zoom_uamplitude_in.setEnabled(False)
            self.acts.zoom_uamplitude_out.setEnabled(False)
            self.acts.zoom_uamplitude_in.setVisible(False)
            self.acts.zoom_uamplitude_out.setVisible(False)
        if not self.plot_ranges[Panel.frequencies[0]].is_used():
            self.acts.zoom_ffrequency_in.setEnabled(False)
            self.acts.zoom_ffrequency_out.setEnabled(False)
            self.acts.zoom_ffrequency_in.setVisible(False)
            self.acts.zoom_ffrequency_out.setVisible(False)
        if not self.plot_ranges[Panel.frequencies[1]].is_used():
            self.acts.zoom_wfrequency_in.setEnabled(False)
            self.acts.zoom_wfrequency_out.setEnabled(False)
            self.acts.zoom_wfrequency_in.setVisible(False)
            self.acts.zoom_wfrequency_out.setVisible(False)
        self.setting = False
        self.data.set_need_update()
        self.set_times()
        
        # tool bar:
        self.toolbar = QToolBar()
        self.toolbar.addAction(self.acts.time_home)
        self.toolbar.addAction(self.acts.time_up)
        self.toolbar.addAction(self.acts.time_down)
        self.toolbar.addAction(self.acts.time_end)
        self.toolbar.addSeparator()
        self.toolbar.addAction(self.acts.play_window)
        self.audiofacw = QComboBox(self)
        self.audiofacw.setToolTip('Audio time expansion factor')
        self.audiofacw.addItems(['0.1', '0.2', '0.5', '1', '2', '5', '10', '20', '50', '100'])
        self.audiofacw.setEditable(False)
        self.audiofacw.setCurrentText(f'{self.audio_rate_fac:g}')
        self.audiofacw.currentTextChanged.connect(lambda s: self.set_audio(rate_fac=float(s)))
        self.toolbar.addWidget(self.audiofacw)
        self.audiohetfw = pg.SpinBox(self, self.audio_heterodyne_freq,
                                     bounds=(10000, 100000),
                                     suffix='Hz', siPrefix=True,
                                     step=0.1, dec=True, decimals=3,
                                     minStep=5000)
        self.audiohetfw.setToolTip('Audio heterodyne frequency')
        self.audiohetfw.sigValueChanged.connect(lambda s: self.set_audio(heterodyne_freq=s.value()))
        if self.data.rate > 50000:
            self.toolbar.addWidget(self.audiohetfw)
            self.toolbar.addAction(self.acts.use_heterodyne)
        else:
            self.audiohetfw.setVisible(False)
        self.toolbar.addSeparator()
        self.toolbar.addAction(self.acts.zoom_home)
        self.toolbar.addAction(self.acts.zoom_back)
        self.toolbar.addAction(self.acts.zoom_forward)
        self.toolbar.addSeparator()

        if self.spectrogram:
            self.spectrogram_power = self.panels[self.data[self.spectrogram].panel].z()
        if 'spectrogram' in self.data:
            self.toolbar.addWidget(QLabel('N:'))
            self.nfftw = QComboBox(self)
            self.nfftw.tooltip = 'NFFT (R, Shift+R)'
            self.nfftw.setToolTip(self.nfftw.tooltip)
            self.nfftw.addItems([f'{2**i}' for i in range(3, 20)])
            self.nfftw.setEditable(False)
            self.nfftw.setCurrentText(f'{self.data["spectrogram"].nfft}')
            self.nfftw.currentTextChanged.connect(lambda s: self.set_resolution(nfft=int(s)))
            self.toolbar.addWidget(self.nfftw)

            self.toolbar.addWidget(QLabel('O:'))
            self.ofracw = pg.SpinBox(self, 100*(1 - self.data["spectrogram"].hop_frac),
                                     bounds=(0, 99.8),
                                     suffix='%', siPrefix=False,
                                     step=0.5, dec=True, decimals=3,
                                     minStep=0.01)
            self.ofracw.setToolTip('Overlap of Fourier segments (O, Shift+O)')
            self.ofracw.valueChanged.connect(lambda v: self.set_resolution(hop_frac=1-0.01*v))
            self.toolbar.addWidget(self.ofracw)
            self.toolbar.addSeparator()

        if 'filtered' in self.data:
            self.toolbar.addWidget(QLabel('H:'))
            self.hpfw = pg.SpinBox(self, self.data['filtered'].highpass_cutoff,
                                   bounds=(0, self.data.rate/2),
                                   suffix='Hz', siPrefix=True,
                                   step=0.5, dec=True, decimals=3,
                                   minStep=10**floor(log10(0.01*self.data.rate/2)))
            self.hpfw.setToolTip('High-pass filter cutoff frequency (H, Shift+H)')
            self.hpfw.sigValueChanged.connect(lambda s: self.update_filter(highpass_cutoff=s.value()))
            self.toolbar.addWidget(self.hpfw)        

            self.toolbar.addWidget(QLabel(' L:'))
            self.lpfw = pg.SpinBox(self, self.data['filtered'].lowpass_cutoff,
                                   bounds=(0.01*self.data.rate/2, self.data.rate/2),
                                   suffix='Hz', siPrefix=True,
                                   step=0.5, dec=True, decimals=3,
                                   minStep=10**floor(log10(0.01*self.data.rate/2)))
            self.lpfw.setToolTip('Low-pass filter cutoff frequency (L, Shift+L)')
            self.lpfw.sigValueChanged.connect(lambda s: self.update_filter(lowpass_cutoff=s.value()))
            self.toolbar.addWidget(self.lpfw)
        else:
            self.hpfw = None
            self.lpfw = None
            self.acts.link_filter.setEnabled(False)
            self.acts.highpass_up.setEnabled(False)
            self.acts.highpass_down.setEnabled(False)
            self.acts.lowpass_up.setEnabled(False)
            self.acts.lowpass_down.setEnabled(False)
        
        if 'envelope' in self.data:
            self.toolbar.addWidget(QLabel(' E:'))
            self.envfw = pg.SpinBox(self, self.data['envelope'].envelope_cutoff,
                                    bounds=(0, 0.5*self.data.rate/2),
                                    suffix='Hz', siPrefix=True,
                                    step=0.5, dec=True, decimals=3,
                                    minStep=10**np.floor(np.log10(0.00001*self.data.rate/2)))
            self.envfw.setToolTip('Envelope low-pass filter cutoff frequency (E, Shift+E)')
            self.envfw.sigValueChanged.connect(lambda s: self.update_envelope(envelope_cutoff=s.value()))
            self.toolbar.addWidget(self.envfw)
        else:
            self.envfw = None
            self.acts.link_envelope.setEnabled(False)
            self.acts.show_envelope.setEnabled(False)
            self.acts.envelope_up.setEnabled(False)
            self.acts.envelope_down.setEnabled(False)

        self.toolbar.addSeparator()
        self.toolbar.addWidget(QLabel('Channel:'))
        for c in range(max(self.data.channels, len(self.acts.channels))):
            gui.set_channel_action(c, self.data.channels,
                                   c in self.show_channels,
                                   gui.browser() is self)
            if c < self.data.channels:
                self.toolbar.addAction(self.acts.channels[c])
        spacer = QWidget()
        spacer.setSizePolicy(QSizePolicy.Expanding, QSizePolicy.Preferred)
        self.toolbar.addWidget(spacer)
        self.xpos_action = self.toolbar.addAction('xpos')
        self.xpos_action.setVisible(False)
        self.toolbar.widgetForAction(self.xpos_action).setFixedWidth(20*xwidth)
        self.ypos_action = self.toolbar.addAction('ypos')
        self.ypos_action.setVisible(False)
        self.toolbar.widgetForAction(self.ypos_action).setFixedWidth(10*xwidth)
        self.zpos_action = self.toolbar.addAction('zpos')
        self.zpos_action.setVisible(False)
        self.toolbar.widgetForAction(self.zpos_action).setFixedWidth(10*xwidth)
        self.vbox.addWidget(self.toolbar)
        
        # full data:
        self.datafig = FullTracePlot(self.data.data, self.panels['trace'].axs)
        self.datafig.polish()
        self.vbox.addWidget(self.datafig)

        self.setEnabled(True)
        self.adjust_layout(self.width(), self.height())

        # setup analyzers:
        PlainAnalyzer(self)
        StatisticsAnalyzer(self)
        self.plugins.setup_analyzer(self)
        if len(self.analyzers) == 0:
            self.acts.analyze_region.setEnabled(False)            
            self.acts.analyze_region.setVisible(False)

        # update visibility of traces:
        for name in self.data.keys():
            for act in self.trace_acts:
                if act.text() == name:
                    act.blockSignals(True)
                    act.setChecked(self.data.is_visible(name))
                    act.blockSignals(False)

        # add marker data to plot:
        labels = [l.label for l in self.marker_labels]
        for t1, ddt, ls, ts in zip(self.marker_data.times,
                                  self.marker_data.delta_times,
                                  self.marker_data.labels,
                                  self.marker_data.texts):
            lidx = labels.index(ls)
            for c, tl in enumerate(self.trace_labels):
                ds = ts if ts else ls
                t0 = t1 - ddt
                idx0 = int(t0*self.data.rate)
                idx1 = int(t1*self.data.rate)
                if ddt > 0:
                    region = pg.LinearRegionItem((t0, t1),
                                                 orientation='vertical',
                                                 pen=pg.mkPen(self.marker_labels[lidx].color),
                                                 brush=pg.mkBrush(self.marker_labels[lidx].color),
                                                 movable=False,
                                                 span=(0.02, 0.05))
                    region.setZValue(-10)
                    self.panels['trace'].add_item(region, c, False)
                    #text = pg.TextItem(ds, color='green', anchor=(0, 0))
                    #text.setPos(t0, 0)
                    #self.panels['trace'].add_item(text, c, False)
                    self.trace_region_labels[c].append(region)
                else:
                    tl[lidx].addPoints((t1,), (self.data.data[idx1, c],), data=(ds,), tip=marker_tip)
            for c, sl in enumerate(self.spec_labels):
                if ddt > 0:
                    # TODO: self.spec_region_labels
                    sl[lidx].addPoints((t0, t1), (0.0, 0.0),
                                       data=(f'start: {ds}', f'end: {ds}'))
                else:
                    sl[lidx].addPoints((t1,), (0.0,), data=(ds,), tip=marker_tip)


    def close(self):
        self.data.close()

                    
    def show_metadata(self):
        
        def format_dict(md, level):
            mdtable = ''
            for k in md:
                pads = ''
                if level > 0:
                    pads = f' style="padding-left: {level*30:d}px;"'
                if isinstance(md[k], dict):
                    # new section:
                    if level == 0:
                        mdtable += f'<tr><td colspan=2><font size="+1"><b>{k}:</b></font></td></tr>'
                    else:
                        mdtable += f'<tr><td colspan=2{pads}><b>{k}:</b></td></tr>'
                    mdtable += format_dict(md[k], level+1)
                    if level == 0:
                        mdtable += '<tr><td colspan=2></td></tr>'
                else:
                    # key-value pair:
                    value = md[k]
                    if isinstance(value, (list, tuple)):
                        value = ', '.join([f'{v}' for v in value])
                    else:
                        value = f'{value}'
                    value = value.replace('\r\n', '\n')
                    value = value.replace('\r', '\n')
                    value = value.replace('\n', '<br>')
                    mdtable += f'<tr><td{pads}><b>{k}</b></td><td>{value}</td></tr>'
            return mdtable

        w = xwidth = self.fontMetrics().averageCharWidth()
        mdtable = f'<style>td {{padding: 0 {w}px 0 0; }}</style><table>'
        mdtable += format_dict(self.data.meta_data, 0)
        mdtable += '</table>'
        dialog = QDialog(self)
        dialog.setWindowTitle('Meta data')
        vbox = QVBoxLayout()
        dialog.setLayout(vbox)
        label = QLabel(mdtable)
        label.setTextInteractionFlags(Qt.TextSelectableByMouse);
        scrollarea = QScrollArea()
        scrollarea.setWidget(label)
        vbox.addWidget(scrollarea)
        buttons = QDialogButtonBox(QDialogButtonBox.Close)
        buttons.rejected.connect(dialog.reject)
        vbox.addWidget(buttons)
        dialog.show()


    def set_cross_hair(self, checked):
        self.cross_hair = checked
        if self.cross_hair:
            # disable existing key shortcuts:
            self.marker_orig_acts = []
            for l in self.marker_labels:
                ks = QKeySequence(l.key_shortcut)
                for a in dir(self.acts):
                    act = getattr(self.acts, a)
                    if isinstance(act, QAction) and act.shortcut() == ks:
                        self.marker_orig_acts.append((act.shortcut(), act))
                        act.setShortcut(QKeySequence())
                        break
            # setup marker actions:            
            for l in self.marker_labels:
                if l.action is None:
                    l.action = QAction(l.label, self)
                    l.action.triggered.connect(lambda x, label=l.label: self.store_marker(label))
                    self.addAction(l.action)
                l.action.setShortcut(l.key_shortcut)
                l.action.setEnabled(True)
            self.plot_ranges.clear_marker()
            self.plot_ranges.clear_stored_marker()
        else:
            self.xpos_action.setVisible(False)
            self.ypos_action.setVisible(False)
            self.zpos_action.setVisible(False)
            self.plot_ranges.clear_marker()
            self.plot_ranges.clear_stored_marker()
            self.plot_ranges.update_crosshair()
            # disable marker actions:
            for l in self.marker_labels:
                l.action.setEnabled(False)
            # restore key shortcuts:
            for key, act in self.marker_orig_acts:
                act.setShortcuts(key)
            self.marker_orig_acts = []


    def set_marker(self):
        pass
        """
        if not self.marker_ax is None and not self.marker_time is None:
            if not self.marker_ampl is None:
                self.marker_ax.prev_marker.setData((self.marker_time,),
                                                   (self.marker_ampl,))
            if not self.marker_freq is None:
                self.marker_ax.prev_marker.setData((self.marker_time,),
                                                   (self.marker_freq,))
        """

            
    def store_marker(self, label=''):
        """
        self.marker_model.add_data(self.marker_channel,
                                   self.marker_time, self.marker_ampl,
                                   self.marker_freq,
                                   self.marker_power,self.delta_time,
                                   self.delta_ampl, self.delta_freq,
                                   self.delta_power, label)
        # add new label point to scatter plots:
        labels = [l.label for l in self.marker_labels]
        if len(label) > 0 and label in labels and \
           self.marker_time is not None:
            lidx = labels.index(label)
            for c, tl in enumerate(self.trace_labels):
                if c == self.marker_channel and self.marker_ampl is not None:
                    tl[lidx].addPoints((self.marker_time,),
                                      (self.marker_ampl,),
                                       tip=marker_tip)
                else:
                    tidx = int(self.marker_time*self.data.rate)
                    tl[lidx].addPoints((self.marker_time,),
                                       (self.data.data[tidx, c],),
                                       tip=marker_tip)
            for c, sl in enumerate(self.spec_labels):
                y = 0.0 if self.marker_freq is None else self.marker_freq
                sl[lidx].addPoints((self.marker_time,), (y,))
        """                
        
    def mouse_moved(self, evt, channel):
        if not self.cross_hair:
            return
            
        # find axes and position:
        pixel_pos = evt[0]
        self.plot_ranges.clear_marker()
        for panel in self.panels.values():
            if not panel.is_used() and not panel.is_visible(channel):
                continue
            ax = panel.axs[channel]
            if not ax.sceneBoundingRect().contains(pixel_pos):
                continue
            pos = ax.getViewBox().mapSceneToView(pixel_pos)
            pixel_pos.setX(pixel_pos.x() + 1)
            npos = ax.getViewBox().mapSceneToView(pixel_pos)
            x0 = pos.x()
            x1 = npos.x()
            y = pos.y()
            x, y, z = ax.get_marker_pos(x0, x1, y)
            self.plot_ranges[panel.x()].set_marker(channel, ax, x)
            self.plot_ranges[panel.y()].set_marker(channel, ax, y)
            if z is not None:
                self.plot_ranges[panel.z()].set_marker(channel, ax, z)
            """
            if not self.marker_time is None:
                self.marker_time, self.marker_ampl = \
                    panel.get_amplitude(channel, self.marker_time,
                                        pos.y(), npos.x())
            if panel.z():
                self.plot_ranges[panel.z()].set_marker(channel, ax, XXX)
            if self.marker_time is not None:
                self.marker_power = panel.get_power(channel,
                                                    self.marker_time,
                                                    self.marker_freq)
            """
            break
        # set cross-hair positions:
        self.plot_ranges.update_crosshair()
        
        # report time on toolbar:
        s = ''
        time, delta_time = self.plot_ranges.marker_delta_time()
        if delta_time is not None:
            sign = '-' if delta_time < 0 else ''
            s = f'\u0394{time}={sign}{secs_to_str(fabs(delta_time))}'
            if fabs(delta_time) > 1e-6:
                if 1/fabs(delta_time) > 1000:
                    s += f' ({0.001/fabs(delta_time):.4g}kHz)'
                elif 1/fabs(delta_time) < 1:
                    s += f' ({1000/fabs(delta_time):.4g}mHz)'
                else:
                    s += f' ({1/fabs(delta_time):.4g}Hz)'
        time, pos = self.plot_ranges.marker_time()
        if not s and pos is not None:
            sign = '-' if pos < 0 else ''
            s = f't={sign}{secs_to_str(fabs(pos))}'
        self.xpos_action.setText(s)
        self.xpos_action.setVisible(len(s) > 0)
        # report amplitude or frequency on toolbar:
        s = ''
        ampl, delta_ampl = self.plot_ranges.marker_delta_amplitude()
        freq, delta_freq = self.plot_ranges.marker_delta_frequency()
        if delta_ampl is not None:
            s = f'\u0394{ampl}={delta_ampl:6.3f}'
            self.ypos_action.setText(s)
        elif delta_freq is not None:
            if abs(delta_freq) > 1000:
                s = f'\u0394{freq}={delta_freq/1000:.4g}kHz'
            elif abs(delta_freq) < 1:
                s = f'\u0394{freq}={delta_freq*1000:.4g}mHz'
            else:
                s = f'\u0394{freq}={delta_freq:.4g}Hz'
        ampl, pos = self.plot_ranges.marker_amplitude()
        if not s and pos is not None:
            s = f'{ampl}={pos:.5g}'
        freq, pos = self.plot_ranges.marker_frequency()
        if not s and pos is not None:
            if pos > 1000:
                s = f'{freq}={pos/1000:.4g}kHz'
            elif pos < 1:
                s = f'{freq}={pos*1000:.4g}mHz'
            else:
                s = f'{freq}={pos:.4g}Hz'
        self.ypos_action.setText(s)
        self.ypos_action.setVisible(len(s) > 0)
        # report power on toolbar:
        s = ''
        pwr, delta_power = self.plot_ranges.marker_delta_power()
        if delta_power is not None:
            s = f'\u0394{pwr}={delta_power:6.1f}dB'
        pwr, pos = self.plot_ranges.marker_power()
        if not s and pos is not None:
            s = f'{pwr}={pos:6.1f}dB'
        self.zpos_action.setText(s)
        self.zpos_action.setVisible(len(s) > 0)

        
    def mouse_clicked(self, evt, channel):
        if not self.cross_hair:
            return
        
        # update position:
        self.mouse_moved((evt[0].scenePos(),), channel)

        """
        # store marker positions:
        if (evt[0].button() & Qt.LeftButton) > 0 and \
           (evt[0].modifiers() == Qt.NoModifier or \
            (evt[0].modifiers() & Qt.ShiftModifier) == Qt.ShiftModifier):
            menu = QMenu(self)
            acts = [menu.addAction(self.marker_labels_model.icons[l.color], l.label) for l in self.marker_labels]
            act = menu.exec(QCursor.pos())
            if act in acts:
                idx = acts.index(act)
                self.store_marker(self.marker_labels[idx].label)
        
        """
        # clear marker:
        if (evt[0].button() & Qt.RightButton) > 0:
            self.plot_ranges.clear_stored_marker()
            
        # store marker position:
        if (evt[0].button() & Qt.LeftButton) > 0: # and \
           #(evt[0].modifiers() & Qt.ControlModifier) == Qt.ControlModifier:
            self.plot_ranges.store_marker()

            
    def label_editor(self):
        self.marker_labels_model.set(self.marker_labels)
        self.marker_labels_model.edit(self)
        
        
    def marker_table(self):
        dialog = QDialog(self)
        dialog.setWindowTitle('Audian marker table')
        vbox = QVBoxLayout()
        dialog.setLayout(vbox)
        view = QTableView()
        view.setModel(self.marker_model)
        view.resizeColumnsToContents()
        width = view.verticalHeader().width() + 24
        for c in range(self.marker_model.columnCount()):
            width += view.columnWidth(c)
        dialog.setMaximumWidth(width)
        dialog.resize(width, 2*width//3)
        view.setSelectionMode(QAbstractItemView.ContiguousSelection)
        vbox.addWidget(view)
        buttons = QDialogButtonBox(QDialogButtonBox.Close |
                                   QDialogButtonBox.Save |
                                   QDialogButtonBox.Reset)
        buttons.rejected.connect(dialog.reject)
        buttons.button(QDialogButtonBox.Reset).clicked.connect(self.marker_model.clear)
        buttons.button(QDialogButtonBox.Save).clicked.connect(lambda x: self.marker_model.save(self))
        vbox.addWidget(buttons)
        dialog.show()
            

    def update_borders(self, rect=None):
        for c in range(len(self.figs)):
            self.borders[c].setRect(0, 0, self.figs[c].size().width(),
                                    self.figs[c].size().height())
            self.borders[c].setVisible(c in self.selected_channels)


    def showEvent(self, event):
        if self.data is None:
            return
        self.setting = True
        self.plot_ranges.set_ranges()
        self.data.set_need_update()
        self.panels.update_plots()
        self.plot_ranges.set_powers()
        self.setting = False

                
    def resizeEvent(self, event):
        if self.show_channels is None or len(self.show_channels) == 0:
            return
        self.adjust_layout(event.size().width(), event.size().height())
        self.data.set_need_update()
        
            
    def show_xticks(self):
        for c in range(self.data.channels):
            first = True
            for panel in self.panels.values():
                if panel.is_spacer() or panel.is_power():
                    continue
                if first and c == self.show_channels[-1] and \
                   panel.is_visible(c):
                    panel.axs[c].getAxis('bottom').showLabel(True)
                    panel.axs[c].getAxis('bottom').setStyle(showValues=True)
                    first = False
                else:
                    panel.axs[c].getAxis('bottom').showLabel(False)
                    panel.axs[c].getAxis('bottom').setStyle(showValues=False)


    def adjust_layout(self, width, height):
        if self.show_channels is None:
            return
        self.show_xticks()
        self.panels.show_spacers(self.show_channels[0])
        xwidth = self.fontMetrics().averageCharWidth()
        xheight = self.fontMetrics().ascent()
        # subtract full data plot:
        data_height = 5*xheight//2 if len(self.show_channels) <= 1 else 3*xheight//2
        if not self.show_fulldata:
            data_height = 0
        height -= len(self.show_channels)*data_height
        # subtract toolbar:
        height -= 2*xheight
        # subtract time axis:
        taxis_height = 2*xheight
        height -= taxis_height
        # what to plot:
        ntraces = 0
        nspecs = 0
        nspacers = 0
        c = self.show_channels[0]
        for panel in self.panels.values():
            if panel.is_visible(c) and (panel.is_spacer() or
                                        panel.has_visible_traces(c)):
                if panel.is_spacer():
                    nspacers += 1
                elif panel.is_spectrogram():
                    nspecs += 1
                elif panel.is_trace():
                    ntraces += 1
        nrows = len(self.show_channels)
        # subtract border height:
        border_height = self.border_height
        height -= nrows*border_height
        # subtract spacer height:
        spacer_height = 0*xheight
        height -= nspacers*spacer_height
        # set heights of panels and channels (figures):
        fig_height = height/nrows
        trace_frac = self.trace_fracs[self.show_specs]
        spec_height = fig_height/(nspecs + trace_frac*ntraces)
        trace_height = trace_frac*spec_height
        bottom_channel = self.show_channels[-1]
        for c in self.show_channels:
            if self.show_specs > 0 and self.show_powers:
                self.figs[c].ci.layout.setColumnFixedWidth(1, 0.1*width)
            else:
                self.figs[c].ci.layout.setColumnFixedWidth(1, 0)
            add_height = taxis_height if c == bottom_channel else 0
            self.vbox.setStretch(c, int(10*(border_height +
                                            nspecs*spec_height +
                                            nspacers*spacer_height +
                                            ntraces*trace_height +
                                            add_height)))
            for panel in self.panels.values():
                if panel.is_power():
                    continue
                if panel.is_visible(c) and (panel.is_spacer() or
                                            panel.has_visible_traces(c)):
                    if panel.is_spacer():
                        row_height = spacer_height
                    elif panel.is_spectrogram():
                        row_height = spec_height + add_height
                    elif panel.is_trace():
                        row_height = trace_height + add_height
                    else:
                        continue
                    self.figs[c].ci.layout.setRowFixedHeight(panel.row,
                                                             row_height)
                    add_height = 0
                else:
                    self.figs[c].ci.layout.setRowFixedHeight(panel.row, 0)
        # fix full data plot:
        if self.datafig is not None:
            self.datafig.update_layout(self.show_channels, data_height)
            self.datafig.setVisible(self.show_fulldata)
        # update:
        for c in self.show_channels:
            self.figs[c].update()

            
    def update_ranges(self, viewbox, arange):
        """
        TODO: a newer version of pyqtgraph might need:
        def update_ranges(self, viewbox, arange):
        """
        if self.setting:
            return
        panel = self.panels.get_panel(viewbox)
        if not panel:
            return
        axspec = panel.ax_spec
        for s in range(2):
            r0, r1 = arange[s]
            if axspec[s] in Panel.times:
                self.set_times(r0, r1 - r0)
            else:
                self.set_ranges(axspec[s], r0, r1)
        self.sigRangesChanged.emit(axspec, arange)


    def set_times(self, toffset=None, twindow=None):
        if self.setting:
            return
        self.setting = True
        trange = self.plot_ranges[Panel.times[0]]
        trange.set_ranges(toffset, None, twindow, None, True)
        self.data.update_times(trange.r0[0], trange.r1[0])
        self.panels.update_plots()
        self.plot_ranges.set_powers()
        self.setting = False
        

    def apply_time_ranges(self, timefunc):
        self.setting = True
        getattr(self.plot_ranges, timefunc)(Panel.times[0], None,
                                            self.isVisible())
        trange = self.plot_ranges[Panel.times[0]]
        self.data.update_times(trange.r0[0], trange.r1[0])
        # TODO: set time range here!
        self.panels.update_plots()
        self.plot_ranges.set_powers()
        self.setting = False
        

    def set_ranges(self, axspec, r0=None, r1=None):
        if self.setting:
            return
        self.setting = True
        self.plot_ranges[axspec].set_ranges(r0, r1, None,
                                            self.selected_channels,
                                            self.isVisible())
        self.setting = False


    def apply_ranges(self, amplitudefunc, axspec):
        self.setting = True
        getattr(self.plot_ranges, amplitudefunc)(axspec,
                                                 self.selected_channels,
                                                 self.isVisible())
        self.setting = False
        

    def auto_ampl(self, axspec=Panel.amplitudes):
        self.setting = True
        trange = self.plot_ranges[Panel.times[0]]
        t0 = trange.r0[0]
        t1 = trange.r1[0]
        self.plot_ranges.auto(axspec, t0, t1,
                              self.selected_channels, self.isVisible())
        self.setting = False


    def set_spectrogram(self, checked, spec):
        if checked:
            self.spectrogram = spec
            if self.spectrogram:
                self.spectrogram_power = self.panels[self.data[self.spectrogram].panel].z()
            self.set_resolution()

        
    def set_resolution(self, nfft=None, hop_frac=None, dispatch=True):
        if self.setting:
            return
        self.setting = True
        if not self.spectrogram and self.spectrogram not in self.data:
            return
        spectrogram = self.data[self.spectrogram]
        spectrogram.update(nfft, hop_frac)
        self.panels.update_plots()
        self.plot_ranges.set_powers()
        self.nfftw.setCurrentText(f'{spectrogram.nfft}')
        T = spectrogram.nfft/self.data.rate
        if T >= 1:
            self.nfftw.setToolTip(self.nfftw.tooltip +
                                  f'={spectrogram.nfft}, ' +
                                  f'T={T:.1f}s, \u0394f={1/T:.2f}Hz')
        elif T >= 0.1:
            self.nfftw.setToolTip(self.nfftw.tooltip +
                                  f'={spectrogram.nfft}, ' +
                                  f'T={1000*T:.0f}ms, \u0394f={1/T:.1f}Hz')
        elif T >= 0.01:
            self.nfftw.setToolTip(self.nfftw.tooltip +
                                  f'={spectrogram.nfft}, ' +
                                  f'T={1000*T:.0f}ms, \u0394f={1/T:.0f}Hz')
        elif T >= 0.001:
            self.nfftw.setToolTip(self.nfftw.tooltip +
                                  f'={spectrogram.nfft}, ' +
                                  f'T={1000*T:.1f}ms, \u0394f={1/T:.0f}Hz')
        else:
            self.nfftw.setToolTip(self.nfftw.tooltip +
                                  f'={spectrogram.nfft}, ' +
                                  f'T={1000*T:.2f}ms, \u0394f={0.001/T:.1f}kHz')
        self.ofracw.setValue(100*(1 - spectrogram.hop_frac))
        self.setting = False
        if dispatch:
            self.sigResolutionChanged.emit()

        
    def freq_resolution_down(self):
        if self.spectrogram in self.data:
            self.set_resolution(nfft=self.data[self.spectrogram].nfft//2)

        
    def freq_resolution_up(self):
        if self.spectrogram in self.data:
            self.set_resolution(nfft=2*self.data[self.spectrogram].nfft)


    def hop_frac_down(self):
        if self.spectrogram in self.data:
            self.set_resolution(hop_frac=self.data[self.spectrogram].hop_frac/2)


    def hop_frac_up(self):
        if self.spectrogram in self.data:
            self.set_resolution(hop_frac=2*self.data[self.spectrogram].hop_frac)

        
    def set_color_map(self, color_map=None, dispatch=True):
        if color_map is not None:
            self.color_map = color_map
        for panel in self.panels.values():
            if panel.is_spectrogram():
                panel.set_colormap(self.color_maps[self.color_map])
        if dispatch:
            self.sigColorMapChanged.emit()

            
    def color_map_cycler(self):
        self.color_map += 1
        if self.color_map >= len(self.color_maps):
            self.color_map = 0
        self.set_color_map()


    def update_filter(self, highpass_cutoff=None, lowpass_cutoff=None):
        """Called when filter cutoffs were changed by key shortcuts or handles
        in spectrum plots and when dispatching.

        """
        if self.setting:
            return
        self.setting = True
        if 'filtered' not in self.data:
            return
        filtered = self.data['filtered']
        if highpass_cutoff is not None:
            filtered.highpass_cutoff = highpass_cutoff
        if lowpass_cutoff is not None:
            filtered.lowpass_cutoff = lowpass_cutoff
        for ax in self.panels['spectrogram'].axs:
            ax.set_filter_handles(filtered.highpass_cutoff,
                                  filtered.lowpass_cutoff)
        self.hpfw.setValue(filtered.highpass_cutoff)
        self.lpfw.setValue(filtered.lowpass_cutoff)
        filtered.update()
        self.panels.update_plots()
        self.plot_ranges.set_powers()
        self.setting = False
        self.sigFilterChanged.emit()  # dispatch


    def update_envelope(self, envelope_cutoff=None, show_envelope=None,
                        dispatch=True):
        """Called when envelope cutoff was changed by key shortcuts or widget.
        """
        if self.setting:
            return
        self.setting = True
        if 'envelope' not in self.data:
            return
        if envelope_cutoff is not None:
            envelope = self.data['envelope']
            envelope.envelope_cutoff = envelope_cutoff
            envelope.update()
            self.data.set_need_update()
            self.panels.update_plots()
            self.envfw.setValue(envelope.envelope_cutoff)
        if show_envelope is not None:
            for name in self.data.keys():
                if name.startswith('env'):
                    self.set_trace(show_envelope, name)
            self.adjust_layout(self.width(), self.height())
        self.setting = False
        if dispatch:
            self.sigEnvelopeChanged.emit()


    def add_to_show_channels(self, channels):
        if isinstance(channels, int):
            channels = [channels]
        for channel in channels:
            if not channel in self.show_channels:
                self.show_channels.append(channel)
        self.show_channels.sort()


    def add_to_selected_channels(self, channels):
        if isinstance(channels, int):
            channels = [channels]
        for channel in channels:
            if not channel in self.selected_channels:
                self.selected_channels.append(channel)
        self.selected_channels.sort()

        
    def all_channels(self):
        if self.selected_channels == self.show_channels:
            self.selected_channels = list(range(self.data.channels))
        else:
            self.selected_channels = list(self.show_channels)
        self.update_borders()


    def next_channel(self):
        idx = self.show_channels.index(self.current_channel)
        if idx + 1 < len(self.show_channels):
            self.current_channel = self.show_channels[idx + 1]
            self.selected_channels = [self.current_channel]
            self.update_borders()
        else:
            if self.show_channels[-1] < self.data.channels - 1:
                n = len(self.show_channels)
                if n > 1:
                    n -= 1
                if self.show_channels[-1] + n >= self.data.channels:
                    n = self.data.channels - 1 - self.show_channels[-1]
                self.add_to_show_channels(list(range(self.show_channels[-1] + 1,
                                                     self.show_channels[-1] + 1 + n)))
                del self.show_channels[:n]
                self.current_channel += 1
            self.selected_channels = [self.current_channel]
            self.set_channels()


    def previous_channel(self):
        idx = self.show_channels.index(self.current_channel)
        if idx > 0:
            self.current_channel = self.show_channels[idx - 1]
            self.selected_channels = [self.current_channel]
            self.update_borders()
        else:
            if self.show_channels[0] > 0:
                n = len(self.show_channels)
                if n > 1:
                    n -= 1
                if self.show_channels[0] < n:
                    n = self.show_channels[0]
                self.add_to_show_channels(list(range(self.show_channels[0] - n,
                                                 self.show_channels[0])))
                del self.show_channels[-n:]
                self.current_channel -= 1
            self.selected_channels = [self.current_channel]
            self.set_channels()


    def select_next_channel(self):
        show_selected_channels = [c for c in range(self.data.channels) if c in self.show_channels and c in self.selected_channels]
        if len(show_selected_channels) > 0:
            self.current_channel = show_selected_channels[-1]
        idx = self.show_channels.index(self.current_channel)
        if idx + 1 < len(self.show_channels):
            self.current_channel = self.show_channels[idx + 1]
            self.add_to_selected_channels(self.current_channel)
            self.update_borders()
        else:
            if self.show_channels[-1] < self.data.channels - 1:
                n = len(self.show_channels)
                if self.show_channels[-1] + n >= self.data.channels:
                    n = self.data.channels - 1 - self.show_channels[-1]
                self.add_to_show_channels(list(range(self.show_channels[-1] + 1,
                                                     self.show_channels[-1] + 1 + n)))
                del self.show_channels[:n]
            if self.current_channel < self.data.channels - 1:
                self.current_channel += 1
                self.add_to_selected_channels(self.current_channel)
            self.set_channels()


    def select_previous_channel(self):
        show_selected_channels = [c for c in range(self.data.channels) if c in self.show_channels and c in self.selected_channels]
        if len(show_selected_channels) > 0:
            self.current_channel = show_selected_channels[0]
        idx = self.show_channels.index(self.current_channel)
        if idx > 0:
            self.current_channel = self.show_channels[idx - 1]
            self.add_to_selected_channels(self.current_channel)
            self.update_borders()
        else:
            if self.show_channels[0] > 0:
                n = len(self.show_channels)
                if self.show_channels[0] < n:
                    n = self.show_channels[0]
                self.add_to_show_channels(list(range(self.show_channels[0] - n,
                                                     self.show_channels[0])))
                del self.show_channels[-n:]
            if self.current_channel > 0:
                self.current_channel -= 1
                self.add_to_selected_channels(self.current_channel)
            self.set_channels()

            
    def set_channels(self, show_channels=None, selected_channels=None,
                     current_channel=None):
        if self.setting:
            return
        self.setting = True
        if show_channels is not None:
            if self.data is None:
                self.schannels = show_channels
                self.setting = False
                return
            self.show_channels = [c for c in show_channels if c < self.data.channels]
        if selected_channels is not None:
            self.selected_channels = [c for c in selected_channels if c < self.data.channels]
        if current_channel is not None:
            self.current_channel = current_channel
        # current channel must be in shown and selected channels:
        show_selected_channels = [c for c in range(self.data.channels) if c in self.show_channels and c in self.selected_channels]
        if not self.current_channel in show_selected_channels:
            for c in show_selected_channels:
                if c >= self.current_channel:
                    self.current_channel = c
                    break
            if not self.current_channel in show_selected_channels:
                self.current_channel = show_selected_channels[-1]
        for c in range(self.data.channels):
            self.figs[c].setVisible(c in self.show_channels)
            self.acts.channels[c].setChecked(c in self.show_channels)
        self.adjust_layout(self.width(), self.height())
        self.update_borders()
        self.setting = False
            
        
    def toggle_channel(self, channel):
        if self.setting:
            return
        if channel < 0 or channel >= self.data.channels:
            return
        if self.acts.channels[channel].isChecked():
            self.add_to_show_channels(channel)
            self.add_to_selected_channels(channel)
            self.set_channels()
        else:
            if channel in self.show_channels:
                self.show_channels.remove(channel)
                if len(self.show_channels) == 0:
                    c = channel + 1
                    if c >= self.data.channels:
                        c = 0
                    self.show_channels = [c]
                    self.add_to_selected_channels(c)
                if channel in self.selected_channels:
                    self.selected_channels.remove(channel)
                    if len(self.selected_channels) == 0:
                        for c in self.show_channels:
                            if c < channel:
                                self.current_channel = c
                            else:
                                break
                        self.selected_channels = [self.current_channel]
                #if len(self.show_channels) == 1:
                #    self.acts.channels[self.show_channels[0]].setCheckable(False)
                self.set_channels()
        self.setFocus()

        
    def show_channel(self, channel):
        if channel < 0 or channel >= self.data.channels:
            return
        if self.current_channel == channel and \
           self.show_channels == [channel]:
            self.set_channels(list(range(self.data.channels)))
        else:
            self.current_channel = channel
            self.add_to_selected_channels(channel)
            self.set_channels([channel])

        
    def hide_deselected_channels(self):
        show_channels = [c for c in self.show_channels if c in self.selected_channels]
        if len(show_channels) == 0:
            show_channels = [self.show_channels[0]]
        self.set_channels(show_channels)
        
        
    def set_panels(self, traces=None, specs=None, powers=None,
                   cbars=None, fulldata=None):
        if not traces is None:
            self.show_traces = traces
        if not specs is None:
            self.show_specs = specs
        if not powers is None:
            self.show_powers = powers
        if not cbars is None:
            self.show_cbars = cbars
        if not fulldata is None:
            self.show_fulldata = fulldata
        for panel in self.panels.values():
            if panel.is_trace():
                panel.set_visible(self.show_traces)
            elif panel.is_spectrogram():
                panel.set_visible(self.show_specs >  0)
                panel.set_cbar_visible(self.show_specs >  0 and
                                       self.show_cbars)
            elif panel.is_power():
                panel.set_visible(self.show_specs >  0 and
                                  self.show_powers)
        if self.datafig is not None:
            self.datafig.setVisible(self.show_fulldata)
        self.adjust_layout(self.width(), self.height())
        self.data.set_need_update()
        trange = self.plot_ranges[Panel.times[0]]
        self.data.update_times(trange.r0[0], trange.r1[0])
        self.panels.update_plots()
        self.plot_ranges.set_powers()
            

    def toggle_traces(self):
        self.show_traces = not self.show_traces
        if not self.show_traces:
            self.show_specs = 1
        self.set_panels()
            

    def toggle_spectrograms(self):
        self.show_specs += 1
        if self.show_specs > 4:
            self.show_specs = 0
        if self.show_specs == 0:
            self.show_traces = True
        self.set_panels()

                
    def toggle_colorbars(self):
        self.show_cbars = not self.show_cbars
        self.set_panels()
            
                
    def toggle_powers(self):
        self.show_powers = not self.show_powers
        self.set_panels()
            
                
    def toggle_fulldata(self):
        self.show_fulldata = not self.show_fulldata
        self.set_panels()
            
            
    def toggle_grids(self):
        self.grids -= 1
        if self.grids < 0:
            self.grids = 3
        self.panels.show_grid(self.grids)


    def set_zoom_mode(self, mode):
        for axs in self.axs:
            for ax in axs:
                ax.getViewBox().setMouseMode(mode)


    def zoom_back(self):
        for axs in self.axs:
            for ax in axs:
                ax.getViewBox().zoom_back()


    def zoom_forward(self):
        for axs in self.axs:
            for ax in axs:
                ax.getViewBox().zoom_forward()


    def zoom_home(self):
        for axs in self.axs:
            for ax in axs:
                ax.getViewBox().zoom_home()


    def set_region_mode(self, mode):
        self.region_mode = mode


    def region_menu(self, channel, vbox, rect):
        panel = self.panels.get_panel(vbox)
        if self.region_mode == DataBrowser.zoom_region or not panel.is_time():
            vbox.zoom_region(rect)
        elif self.region_mode == DataBrowser.play_region:
            self.play_region(rect.left(), rect.right())
        elif self.region_mode == DataBrowser.analyze_region:
            self.analyze_region(rect.left(), rect.right(), channel)
        elif self.region_mode == DataBrowser.save_region:
            self.save_region(rect.left(), rect.right())
        elif self.region_mode == DataBrowser.ask_region:
            menu = QMenu(self)
            zoom_act = menu.addAction('&Zoom')
            play_act = menu.addAction('&Play')
            analyze_act = menu.addAction('&Analyze')
            analyze_act.setEnabled(self.acts.analyze_region.isEnabled())
            analyze_act.setVisible(self.acts.analyze_region.isVisible())
            save_act = menu.addAction('&Save as')
            #crop_act = menu.addAction('&Crop')
            act = menu.exec(QCursor.pos())
            if act is zoom_act:
                vbox.zoom_region(rect)
            elif act is play_act:
                self.play_region(rect.left(), rect.right())
            elif act is analyze_act:
                self.analyze_region(rect.left(), rect.right(), channel)
            elif act is save_act:
                self.save_region(rect.left(), rect.right())
        vbox.hide_region()


    def play_scroll(self):
        if self.scroll_timer.isActive():
            self.scroll_timer.stop()
            self.scroll_step /= 2
        elif self.audio_timer.isActive():
            self.audio.stop()
            self.audio_timer.stop()
            for amarkers in self.audio_markers:
                for vmarker in amarkers:
                    vmarker.setValue(-1)
        else:
            self.play_window()
        

    def auto_scroll(self):
        if self.scroll_step == 0:
            self.scroll_step = 0.005
        elif self.scroll_step > 1.0:
            if self.scroll_timer.isActive():
                self.scroll_timer.stop()
            self.scroll_step = 0
            return
        else:
            self.scroll_step *= 2
        if not self.scroll_timer.isActive():
            self.scroll_timer.start(50)

        
    def scroll_further(self):
        trange = self.plot_ranges[Panel.times[0]]
        if trange.at_end():
            self.scroll_timer.stop()
            self.scroll_step /= 2
        else:
            twin = trange.r1[0] - trange.r0[0]
            self.set_times(trange.r0[0] + twin*self.scroll_step, twin)


    def set_audio(self, rate_fac=None,
                  use_heterodyne=None, heterodyne_freq=None,
                  dispatch=True):
        if rate_fac is not None:
            self.audio_rate_fac = rate_fac
            if not dispatch:
                self.audiofacw.setCurrentText(f'{self.audio_rate_fac:g}')
        if use_heterodyne is not None:
            self.audio_use_heterodyne = use_heterodyne
        if heterodyne_freq is not None:
            self.audio_heterodyne_freq = float(heterodyne_freq)
            if not dispatch:
                self.audiohetfw.setValue(self.audio_heterodyne_freq)
        if dispatch:
            self.sigAudioChanged.emit(self.audio_rate_fac,
                                      self.audio_use_heterodyne,
                                      self.audio_heterodyne_freq)


    def play_region(self, t0, t1):
        data = self.data['filtered'] if 'filtered' in self.data else self.data['data']
        rate = data.rate
        i0 = int(np.round(t0*rate))
        i1 = int(np.round(t1*rate))
        if i0 < 0:
            i0 = 0
            t0 = 0.0
        if i1 > len(data):
            i1 = len(data)
            t1 = i1/rate
        n2 = (len(self.selected_channels)+1)//2
        playdata = np.zeros((i1-i0, min(2, len(self.selected_channels))))
        playdata[:,0] = np.mean(data[i0:i1, self.selected_channels[:n2]], 1)
        if len(self.selected_channels) > 1:
            playdata[:,1] = np.mean(data[i0:i1, self.selected_channels[n2:]], 1)
        if self.audio_use_heterodyne:
            # multiply with heterodyne frequency:
            heterodyne = np.sin(2*np.pi*self.audio_heterodyne_freq*np.arange(len(playdata))/rate)
            playdata = (playdata.T * heterodyne).T
            # low-pass filter and downsample:
            fcutoff = 20000.0
            sos = butter(2, 20000, 'low', output='sos', fs=rate)
            nstep = int(np.round(rate/(2*fcutoff)))
            if nstep < 1:
                nstep = 1
            playdata = sosfiltfilt(sos, playdata, 0)[::nstep]
            rate /= nstep
        fade(playdata, rate/self.audio_rate_fac, 0.1)
        self.audio.play(playdata, rate/self.audio_rate_fac, blocking=False)
        self.audio_time = t0
        self.audio_tmax = t1
        self.audio_timer.start(50)
        for c in range(data.channels):
            atime = self.audio_time if c in self.selected_channels else -1
            for vmarker in self.audio_markers[c]:
                vmarker.setValue(atime)


    def play_window(self):
        trange = self.plot_ranges[Panel.times[0]]
        self.play_region(trange.r0[0], trange.r1[0])

        
    def mark_audio(self):
        self.audio_time += 0.05 / self.audio_rate_fac
        for amarkers in self.audio_markers:
            for vmarker in amarkers:
                if vmarker.value() >= 0:
                    vmarker.setValue(self.audio_time)
        if self.audio_time > self.audio_tmax:
            self.audio_timer.stop()
            for amarkers in self.audio_markers:
                for vmarker in amarkers:
                    vmarker.setValue(-1)

                    
    def analyze_region(self, t0, t1, channel):
        if t0 < 0:
            t0 = 0
        if t1 > self.data.data.frames/self.data.data.rate:
            t1 = self.data.data.frames/self.data.data.rate
        traces = self.data.get_region(t0, t1, channel)
        for a in self.analyzers:
            a.analyze(t0, t1, channel, traces)
        if self.analysis_table is None:
            self.analysis_results()
        else:
            self.analysis_table.setData(self.get_analysis_table())

            
    def get_analysis_table(self):
        table = []
        r = 0
        while True:
            row = {}
            for a in self.analyzers:
                if r < a.data.rows():
                    for c in range(len(a.data)):
                        us = f'/{a.data.unit(c)}' if a.data.unit(c) else ''
                        header = a.data.label(c) + us
                        row.update({header: a.data[r, c]})
            if len(row) == 0:
                break
            table.append(row)
            r += 1
        return table
    
            
    def analysis_results(self):
        if self.analysis_table is not None:
            return
        if len(self.analyzers) == 0:
            return
        dialog = QDialog(self)
        dialog.setWindowTitle('Audian analyis table')
        vbox = QVBoxLayout()
        dialog.setLayout(vbox)
        self.analysis_table = pg.TableWidget()
        self.analysis_table.setMinimumHeight(250)
        self.analysis_table.setData(self.get_analysis_table())
        c = 0
        for a in self.analyzers:
            for i in range(len(a.data)):
                self.analysis_table.setFormat(a.data.format(i), c)
                c += 1
        vbox.addWidget(self.analysis_table)
        buttons = QDialogButtonBox(QDialogButtonBox.Close |
                                   QDialogButtonBox.Save |
                                   QDialogButtonBox.Reset)
        buttons.rejected.connect(dialog.reject)
        buttons.button(QDialogButtonBox.Reset).clicked.connect(self.clear_analysis)
        buttons.button(QDialogButtonBox.Save).clicked.connect(self.save_analysis)
        vbox.addWidget(buttons)
        dialog.finished.connect(lambda x: [None for self.analysis_table in [None]])
        dialog.show()


    def clear_analysis(self):
        if self.analysis_table is not None:
            self.analysis_table.clear()
        for a in self.analyzers:
            a.clear()

            
    def save_analysis(self):
        if len(self.analyzers) == 0 or len(self.analyzers[0].data) == 0:
            return
        file_name, _ = QFileDialog.getSaveFileName(
            self, 'Save analysis as',
            os.path.splitext(self.data.file_path)[0] + '-analysis.csv',
            'comma-separated values (*.csv)')
        if not file_name:
            return
        table = self.analyzers[0].data
        for a in self.analyzers[1:]:
            for c in range(len(a.data)):
                table.append(a.data.label(c), a.data.unit(c),
                             a.data.format(c), a.data.data[c])
        table.write(file_name, table_format='csv', delimiter=';',
                    unit_style='header', column_numbers=None, sections=0)
     
                    
    def save_region(self, t0, t1):

        def secs_to_str(time):
            hours = time//3600
            time -= 3600*hours
            mins = time//60
            time -= 60*mins
            secs = int(np.floor(time))
            time -= secs
            msecs = f'{1000*time:03.0f}ms'
            if hours > 0:
                return f'{hours}h{mins}m{secs}s{msecs}'
            elif mins > 0:
                return f'{mins}m{secs}s{msecs}'
            elif secs > 0:
                return f'{secs}s{msecs}'
            else:
                return msecs

        i0 = int(np.round(t0*self.data.rate))
        i1 = int(np.round(t1*self.data.rate))
        if i0 < 0:
            i0 = 0
            t0 = 0.0
        if i1 > len(self.data.data):
            i1 = len(self.data.data)
            t1 = i1/rate
        name = os.path.splitext(os.path.basename(self.data.file_path))[0]
        #if self.channel > 0:
        #    filename = f'{name}-{channel:d}-{t0:.4g}s-{t1s:.4g}s.wav'
        t0s = secs_to_str(t0)
        t1s = secs_to_str(t1)
        file_name = f'{name}-{t0s}-{t1s}.wav'
        formats = available_formats()
        for f in ['MP3', 'OGG', 'WAV']:
            if f in formats:
                formats.remove(f)
                formats.insert(0, f)
        filters = ['All files (*)'] + [f'{f} files (*.{f}, *.{f.lower()})' for f in formats]
        file_path = os.path.join(os.path.dirname(self.data.file_path), file_name)
        file_path = QFileDialog.getSaveFileName(self, 'Save region as',
                                                file_path,
                                                ';;'.join(filters))[0]
        if file_path:
            md = deepcopy(self.data.data.metadata())
            update_starttime(md, t0, self.data.rate)
            hkey = 'CodingHistory'
            if 'BEXT' in md:
                hkey = 'BEXT.' + hkey
            bext_code = bext_history_str(self.data.data.encoding,
                                         self.data.rate, self.data.channels)
            add_history(md, bext_code + f',T=cut out {t0s}-{t1s}: {os.path.basename(file_path)}', hkey, bext_code + f',T={self.data.file_path}')
            locs, labels = self.marker_data.get_markers(self.data.rate)
            sel = (locs[:,0] + locs[:,1] >= i0) & (locs[:,0] <= i1)
            locs = locs[sel]
            labels = labels[sel]
            try:
                write_data(file_path,
                           self.data.data[i0:i1, self.selected_channels],
                           self.data.rate, self.data.data.ampl_max,
                           self.data.data.unit, md, locs, labels,
                           encoding=self.data.data.encoding)
                print(f'saved region to "{os.path.relpath(file_path)}"')
            except PermissionError as e:
                print(f'failed to save region to "{os.path.relpath(file_path)}": permission denied')

        
    def save_window(self):
        trange = self.plot_ranges[Panel.times[0]]
        self.save_region(trange.r0[0], trange.r1[0])

Ancestors

  • PyQt5.QtWidgets.QWidget
  • PyQt5.QtCore.QObject
  • sip.wrapper
  • PyQt5.QtGui.QPaintDevice
  • sip.simplewrapper

Class variables

var color_maps
var zoom_region
var ask_region

Methods

def play_region(self, t0, t1)
def analyze_region(self, t0, t1, channel)
def save_region(self, t0, t1)
def sigRangesChanged(...)

pyqtSignal(*types, name: str = …, revision: int = …, arguments: Sequence = …) -> PYQT_SIGNAL

types is normally a sequence of individual types. Each type is either a type object or a string that is the name of a C++ type. Alternatively each type could itself be a sequence of types each describing a different overloaded signal. name is the optional C++ name of the signal. If it is not specified then the name of the class attribute that is bound to the signal is used. revision is the optional revision of the signal that is exported to QML. If it is not specified then 0 is used. arguments is the optional sequence of the names of the signal's arguments.

def sigResolutionChanged(...)

pyqtSignal(*types, name: str = …, revision: int = …, arguments: Sequence = …) -> PYQT_SIGNAL

types is normally a sequence of individual types. Each type is either a type object or a string that is the name of a C++ type. Alternatively each type could itself be a sequence of types each describing a different overloaded signal. name is the optional C++ name of the signal. If it is not specified then the name of the class attribute that is bound to the signal is used. revision is the optional revision of the signal that is exported to QML. If it is not specified then 0 is used. arguments is the optional sequence of the names of the signal's arguments.

def sigColorMapChanged(...)

pyqtSignal(*types, name: str = …, revision: int = …, arguments: Sequence = …) -> PYQT_SIGNAL

types is normally a sequence of individual types. Each type is either a type object or a string that is the name of a C++ type. Alternatively each type could itself be a sequence of types each describing a different overloaded signal. name is the optional C++ name of the signal. If it is not specified then the name of the class attribute that is bound to the signal is used. revision is the optional revision of the signal that is exported to QML. If it is not specified then 0 is used. arguments is the optional sequence of the names of the signal's arguments.

def sigFilterChanged(...)

pyqtSignal(*types, name: str = …, revision: int = …, arguments: Sequence = …) -> PYQT_SIGNAL

types is normally a sequence of individual types. Each type is either a type object or a string that is the name of a C++ type. Alternatively each type could itself be a sequence of types each describing a different overloaded signal. name is the optional C++ name of the signal. If it is not specified then the name of the class attribute that is bound to the signal is used. revision is the optional revision of the signal that is exported to QML. If it is not specified then 0 is used. arguments is the optional sequence of the names of the signal's arguments.

def sigEnvelopeChanged(...)

pyqtSignal(*types, name: str = …, revision: int = …, arguments: Sequence = …) -> PYQT_SIGNAL

types is normally a sequence of individual types. Each type is either a type object or a string that is the name of a C++ type. Alternatively each type could itself be a sequence of types each describing a different overloaded signal. name is the optional C++ name of the signal. If it is not specified then the name of the class attribute that is bound to the signal is used. revision is the optional revision of the signal that is exported to QML. If it is not specified then 0 is used. arguments is the optional sequence of the names of the signal's arguments.

def sigTraceChanged(...)

pyqtSignal(*types, name: str = …, revision: int = …, arguments: Sequence = …) -> PYQT_SIGNAL

types is normally a sequence of individual types. Each type is either a type object or a string that is the name of a C++ type. Alternatively each type could itself be a sequence of types each describing a different overloaded signal. name is the optional C++ name of the signal. If it is not specified then the name of the class attribute that is bound to the signal is used. revision is the optional revision of the signal that is exported to QML. If it is not specified then 0 is used. arguments is the optional sequence of the names of the signal's arguments.

def sigAudioChanged(...)

pyqtSignal(*types, name: str = …, revision: int = …, arguments: Sequence = …) -> PYQT_SIGNAL

types is normally a sequence of individual types. Each type is either a type object or a string that is the name of a C++ type. Alternatively each type could itself be a sequence of types each describing a different overloaded signal. name is the optional C++ name of the signal. If it is not specified then the name of the class attribute that is bound to the signal is used. revision is the optional revision of the signal that is exported to QML. If it is not specified then 0 is used. arguments is the optional sequence of the names of the signal's arguments.

def get_trace(self, name)
def add_trace(self, trace)
def remove_trace(self, name)
def clear_traces(self)
def get_analyzer(self, name)
def add_analyzer(self, analyzer)
def remove_analyzer(self, name)
def clear_analyzer(self)
def add_to_panel_trace(self, trace_name, channel, plot_item)
def toggle_trace(self, checked, name)
def set_trace(self, checked, name)
def open(self, gui, unwrap, unwrap_clip, highpass_cutoff, lowpass_cutoff)
def close(self)

close(self) -> bool

def show_metadata(self)
def set_cross_hair(self, checked)
def set_marker(self)
def store_marker(self, label='')

self.marker_model.add_data(self.marker_channel, self.marker_time, self.marker_ampl, self.marker_freq, self.marker_power,self.delta_time, self.delta_ampl, self.delta_freq, self.delta_power, label)

add new label point to scatter plots:

labels = [l.label for l in self.marker_labels] if len(label) > 0 and label in labels and self.marker_time is not None: lidx = labels.index(label) for c, tl in enumerate(self.trace_labels): if c == self.marker_channel and self.marker_ampl is not None: tl[lidx].addPoints((self.marker_time,), (self.marker_ampl,), tip=marker_tip) else: tidx = int(self.marker_time*self.data.rate) tl[lidx].addPoints((self.marker_time,), (self.data.data[tidx, c],), tip=marker_tip) for c, sl in enumerate(self.spec_labels): y = 0.0 if self.marker_freq is None else self.marker_freq sl[lidx].addPoints((self.marker_time,), (y,))

def mouse_moved(self, evt, channel)
def mouse_clicked(self, evt, channel)
def label_editor(self)
def marker_table(self)
def update_borders(self, rect=None)
def showEvent(self, event)

showEvent(self, a0: Optional[QShowEvent])

def resizeEvent(self, event)

resizeEvent(self, a0: Optional[QResizeEvent])

def show_xticks(self)
def adjust_layout(self, width, height)
def update_ranges(self, viewbox, arange)

TODO: a newer version of pyqtgraph might need: def update_ranges(self, viewbox, arange):

def set_times(self, toffset=None, twindow=None)
def apply_time_ranges(self, timefunc)
def set_ranges(self, axspec, r0=None, r1=None)
def apply_ranges(self, amplitudefunc, axspec)
def auto_ampl(self, axspec='xyu')
def set_spectrogram(self, checked, spec)
def set_resolution(self, nfft=None, hop_frac=None, dispatch=True)
def freq_resolution_down(self)
def freq_resolution_up(self)
def hop_frac_down(self)
def hop_frac_up(self)
def set_color_map(self, color_map=None, dispatch=True)
def color_map_cycler(self)
def update_filter(self, highpass_cutoff=None, lowpass_cutoff=None)

Called when filter cutoffs were changed by key shortcuts or handles in spectrum plots and when dispatching.

def update_envelope(self, envelope_cutoff=None, show_envelope=None, dispatch=True)

Called when envelope cutoff was changed by key shortcuts or widget.

def add_to_show_channels(self, channels)
def add_to_selected_channels(self, channels)
def all_channels(self)
def next_channel(self)
def previous_channel(self)
def select_next_channel(self)
def select_previous_channel(self)
def set_channels(self, show_channels=None, selected_channels=None, current_channel=None)
def toggle_channel(self, channel)
def show_channel(self, channel)
def hide_deselected_channels(self)
def set_panels(self, traces=None, specs=None, powers=None, cbars=None, fulldata=None)
def toggle_traces(self)
def toggle_spectrograms(self)
def toggle_colorbars(self)
def toggle_powers(self)
def toggle_fulldata(self)
def toggle_grids(self)
def set_zoom_mode(self, mode)
def zoom_back(self)
def zoom_forward(self)
def zoom_home(self)
def set_region_mode(self, mode)
def region_menu(self, channel, vbox, rect)
def play_scroll(self)
def auto_scroll(self)
def scroll_further(self)
def set_audio(self, rate_fac=None, use_heterodyne=None, heterodyne_freq=None, dispatch=True)
def play_window(self)
def mark_audio(self)
def get_analysis_table(self)
def analysis_results(self)
def clear_analysis(self)
def save_analysis(self)
def save_window(self)