Module audian.databrowser
Functions
def marker_tip(x, y, data)
-
Expand source code
def marker_tip(x, y, data): s = '' if data: s += data + '\n' s += 'time=' + secs_to_str(x) return s
Classes
class DataBrowser (file_path, load_kwargs, plugins, channels, audio, acts, *args, **kwargs)
-
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 name(self): if isinstance(self.data.file_path, (list, tuple, np.ndarray)): return os.path.basename(self.data.file_path[0]) else: return os.path.basename(self.data.file_path) 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])
QWidget(parent: Optional[QWidget] = None, flags: Union[Qt.WindowFlags, Qt.WindowType] = Qt.WindowFlags())
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)
-
Expand source code
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 analyze_region(self, t0, t1, channel)
-
Expand source code
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 save_region(self, t0, t1)
-
Expand source code
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 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 name(self)
-
Expand source code
def name(self): if isinstance(self.data.file_path, (list, tuple, np.ndarray)): return os.path.basename(self.data.file_path[0]) else: return os.path.basename(self.data.file_path)
def get_trace(self, name)
-
Expand source code
def get_trace(self, name): return self.data[name]
def add_trace(self, trace)
-
Expand source code
def add_trace(self, trace): self.data.add_trace(trace)
def remove_trace(self, name)
-
Expand source code
def remove_trace(self, name): self.data.remove_trace(name)
def clear_traces(self)
-
Expand source code
def clear_traces(self): self.data.clear_traces()
def get_analyzer(self, name)
-
Expand source code
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)
-
Expand source code
def add_analyzer(self, analyzer): self.analyzers.append(analyzer)
def remove_analyzer(self, name)
-
Expand source code
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)
-
Expand source code
def clear_analyzer(self): self.analyzers = []
def add_to_panel_trace(self, trace_name, channel, plot_item)
-
Expand source code
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)
-
Expand source code
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)
-
Expand source code
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)
-
Expand source code
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)
-
Expand source code
def close(self): self.data.close()
close(self) -> bool
def show_metadata(self)
-
Expand source code
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)
-
Expand source code
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)
-
Expand source code
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='')
-
Expand source code
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,)) """
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)
-
Expand source code
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)
-
Expand source code
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)
-
Expand source code
def label_editor(self): self.marker_labels_model.set(self.marker_labels) self.marker_labels_model.edit(self)
def marker_table(self)
-
Expand source code
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)
-
Expand source code
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)
-
Expand source code
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
showEvent(self, a0: Optional[QShowEvent])
def resizeEvent(self, event)
-
Expand source code
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()
resizeEvent(self, a0: Optional[QResizeEvent])
def show_xticks(self)
-
Expand source code
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)
-
Expand source code
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)
-
Expand source code
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)
TODO: a newer version of pyqtgraph might need: def update_ranges(self, viewbox, arange):
def set_times(self, toffset=None, twindow=None)
-
Expand source code
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)
-
Expand source code
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)
-
Expand source code
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)
-
Expand source code
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='xyu')
-
Expand source code
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)
-
Expand source code
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)
-
Expand source code
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)
-
Expand source code
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)
-
Expand source code
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)
-
Expand source code
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)
-
Expand source code
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)
-
Expand source code
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)
-
Expand source code
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)
-
Expand source code
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
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)
-
Expand source code
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()
Called when envelope cutoff was changed by key shortcuts or widget.
def add_to_show_channels(self, channels)
-
Expand source code
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)
-
Expand source code
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)
-
Expand source code
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)
-
Expand source code
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)
-
Expand source code
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)
-
Expand source code
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)
-
Expand source code
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)
-
Expand source code
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)
-
Expand source code
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)
-
Expand source code
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)
-
Expand source code
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)
-
Expand source code
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)
-
Expand source code
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)
-
Expand source code
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)
-
Expand source code
def toggle_colorbars(self): self.show_cbars = not self.show_cbars self.set_panels()
def toggle_powers(self)
-
Expand source code
def toggle_powers(self): self.show_powers = not self.show_powers self.set_panels()
def toggle_fulldata(self)
-
Expand source code
def toggle_fulldata(self): self.show_fulldata = not self.show_fulldata self.set_panels()
def toggle_grids(self)
-
Expand source code
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)
-
Expand source code
def set_zoom_mode(self, mode): for axs in self.axs: for ax in axs: ax.getViewBox().setMouseMode(mode)
def zoom_back(self)
-
Expand source code
def zoom_back(self): for axs in self.axs: for ax in axs: ax.getViewBox().zoom_back()
def zoom_forward(self)
-
Expand source code
def zoom_forward(self): for axs in self.axs: for ax in axs: ax.getViewBox().zoom_forward()
def zoom_home(self)
-
Expand source code
def zoom_home(self): for axs in self.axs: for ax in axs: ax.getViewBox().zoom_home()
def set_region_mode(self, mode)
-
Expand source code
def set_region_mode(self, mode): self.region_mode = mode
-
Expand source code
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)
-
Expand source code
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)
-
Expand source code
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)
-
Expand source code
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)
-
Expand source code
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_window(self)
-
Expand source code
def play_window(self): trange = self.plot_ranges[Panel.times[0]] self.play_region(trange.r0[0], trange.r1[0])
def mark_audio(self)
-
Expand source code
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 get_analysis_table(self)
-
Expand source code
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)
-
Expand source code
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)
-
Expand source code
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)
-
Expand source code
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_window(self)
-
Expand source code
def save_window(self): trange = self.plot_ranges[Panel.times[0]] self.save_region(trange.r0[0], trange.r1[0])