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 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)