class Spectrogram_Widget(QtGui.QWidget): def __init__(self, parent, audiobackend, logger = PrintLogger()): QtGui.QWidget.__init__(self, parent) self.logger = logger self.setObjectName("Spectrogram_Widget") self.gridLayout = QtGui.QGridLayout(self) self.gridLayout.setObjectName("gridLayout") self.PlotZoneImage = ImagePlot(self, self.logger, audiobackend) #self.PlotZoneImage = GLRollingCanvasWidget(self, self.logger) self.PlotZoneImage.setObjectName("PlotZoneImage") self.gridLayout.addWidget(self.PlotZoneImage, 0, 1, 1, 1) self.audiobuffer = None self.audiobackend = audiobackend # initialize the class instance that will do the fft self.proc = audioproc(self.logger) self.maxfreq = DEFAULT_MAXFREQ self.proc.set_maxfreq(self.maxfreq) self.minfreq = DEFAULT_MINFREQ self.fft_size = 2**DEFAULT_FFT_SIZE*32 self.proc.set_fftsize(self.fft_size) self.spec_min = DEFAULT_SPEC_MIN self.spec_max = DEFAULT_SPEC_MAX self.weighting = DEFAULT_WEIGHTING self.update_weighting() self.freq = self.proc.get_freq_scale() self.timerange_s = DEFAULT_TIMERANGE self.canvas_width = 100. self.old_index = 0 self.overlap = 3./4. self.overlap_frac = Fraction(3, 4) self.dT_s = self.fft_size*(1. - self.overlap)/float(SAMPLING_RATE) self.PlotZoneImage.setlog10freqscale() #DEFAULT_FREQ_SCALE = 1 #log10 self.PlotZoneImage.setfreqrange(self.minfreq, self.maxfreq) self.PlotZoneImage.setspecrange(self.spec_min, self.spec_max) self.PlotZoneImage.setweighting(self.weighting) self.PlotZoneImage.settimerange(self.timerange_s, self.dT_s) sfft_rate_frac = Fraction(SAMPLING_RATE, self.fft_size)/(Fraction(1) - self.overlap_frac)/1000 self.PlotZoneImage.set_sfft_rate(sfft_rate_frac) # initialize the settings dialog self.settings_dialog = Spectrogram_Settings_Dialog(self, self.logger) # method def set_buffer(self, buffer): self.audiobuffer = buffer self.old_index = self.audiobuffer.ringbuffer.offset def log_spectrogram(self, sp): # Note: implementing the log10 of the array in Cython did not bring # any speedup. # Idea: Instead of computing the log of the data, I could pre-compute # a list of values associated with the colormap, and then do a search... epsilon = 1e-30 return 10.*log10(sp + epsilon) # scale the db spectrum from [- spec_range db ... 0 db] > [0..1] def scale_spectrogram(self, sp): return (sp.clip(min = self.spec_min, max = self.spec_max) - self.spec_min)/(self.spec_max - self.spec_min) # method def update(self): if not self.isVisible(): return # we need to maintain an index of where we are in the buffer index = self.audiobuffer.ringbuffer.offset available = index - self.old_index if available < 0: #ringbuffer must have grown or something... available = 0 self.old_index = index # if we have enough data to add a frequency column in the time-frequency plane, compute it needed = self.fft_size*(1. - self.overlap) realizable = int(floor(available/needed)) if realizable > 0: spn = zeros((len(self.freq), realizable), dtype=float64) for i in range(realizable): floatdata = self.audiobuffer.data_indexed(self.old_index, self.fft_size) # for now, take the first channel only floatdata = floatdata[0,:] # FIXME We should allow here for more intelligent transforms, especially when the log freq scale is selected spn[:, i] = self.proc.analyzelive(floatdata) self.old_index += int(needed) w = tile(self.w, (1, realizable)) norm_spectrogram = self.scale_spectrogram(self.log_spectrogram(spn) + w) self.PlotZoneImage.addData(self.freq, norm_spectrogram) self.PlotZoneImage.updatePlot() # thickness of a frequency column depends on FFT size and window overlap # hamming window with 75% overlap provides good quality (Perfect reconstruction, # aliasing from side lobes only, 42 dB channel isolation) # number of frequency columns that we keep depends on the time history that the user has chosen # actual displayed spectrogram is a scaled version of the time-frequency plane def setminfreq(self, freq): self.minfreq = freq self.PlotZoneImage.setfreqrange(self.minfreq, self.maxfreq) def setmaxfreq(self, freq): self.maxfreq = freq self.PlotZoneImage.setfreqrange(self.minfreq, self.maxfreq) self.proc.set_maxfreq(freq) self.update_weighting() self.freq = self.proc.get_freq_scale() def setfftsize(self, fft_size): self.fft_size = fft_size self.proc.set_fftsize(fft_size) self.update_weighting() self.freq = self.proc.get_freq_scale() self.dT_s = self.fft_size*(1. - self.overlap)/float(SAMPLING_RATE) self.PlotZoneImage.settimerange(self.timerange_s, self.dT_s) sfft_rate_frac = Fraction(SAMPLING_RATE, self.fft_size)/(Fraction(1) - self.overlap_frac)/1000 self.PlotZoneImage.set_sfft_rate(sfft_rate_frac) def setmin(self, value): self.spec_min = value self.PlotZoneImage.setspecrange(self.spec_min, self.spec_max) def setmax(self, value): self.spec_max = value self.PlotZoneImage.setspecrange(self.spec_min, self.spec_max) def setweighting(self, weighting): self.weighting = weighting self.PlotZoneImage.setweighting(weighting) self.update_weighting() def update_weighting(self): A, B, C = self.proc.get_freq_weighting() if self.weighting is 0: self.w = 0. elif self.weighting is 1: self.w = A elif self.weighting is 2: self.w = B else: self.w = C self.w.shape = (len(self.w), 1) def settings_called(self, checked): self.settings_dialog.show() def saveState(self, settings): self.settings_dialog.saveState(settings) def restoreState(self, settings): self.settings_dialog.restoreState(settings) # slot def timerangechanged(self, value): self.timerange_s = value self.PlotZoneImage.settimerange(self.timerange_s, self.dT_s) # slot def canvasWidthChanged(self, width): self.canvas_width = width
class Spectrogram_Widget(QtGui.QWidget): def __init__(self, parent, logger = None): QtGui.QWidget.__init__(self, parent) # store the logger instance if logger is None: self.logger = parent.parent().logger else: self.logger = logger self.parent = parent self.setObjectName("Spectrogram_Widget") self.gridLayout = QtGui.QGridLayout(self) self.gridLayout.setObjectName("gridLayout") self.PlotZoneImage = ImagePlot(self, self.logger) self.PlotZoneImage.setObjectName("PlotZoneImage") self.gridLayout.addWidget(self.PlotZoneImage, 0, 1, 1, 1) self.audiobuffer = None # initialize the class instance that will do the fft self.proc = audioproc(self.logger) self.maxfreq = DEFAULT_MAXFREQ self.minfreq = DEFAULT_MINFREQ self.fft_size = 2**DEFAULT_FFT_SIZE*32 self.spec_min = DEFAULT_SPEC_MIN self.spec_max = DEFAULT_SPEC_MAX self.weighting = DEFAULT_WEIGHTING self.spectrogram_timer_time = 0. self.timerange_s = DEFAULT_TIMERANGE self.canvas_width = 100. self.PlotZoneImage.setlog10freqscale() #DEFAULT_FREQ_SCALE = 1 #log10 self.PlotZoneImage.setfreqrange(self.minfreq, self.maxfreq) self.PlotZoneImage.setspecrange(self.spec_min, self.spec_max) self.PlotZoneImage.setweighting(self.weighting) self.PlotZoneImage.settimerange(self.timerange_s) # this timer is used to update the spectrogram widget, whose update period # is fixed by the time scale and the width of the widget canvas self.timer = QtCore.QTimer() self.period_ms = SMOOTH_DISPLAY_TIMER_PERIOD_MS self.timer.setInterval(self.period_ms) # variable timing # initialize the settings dialog self.settings_dialog = Spectrogram_Settings_Dialog(self, self.logger) # timer ticks self.connect(self.timer, QtCore.SIGNAL('timeout()'), self.timer_slot) # window resize self.connect(self.PlotZoneImage.plotImage.canvasscaledspectrogram, QtCore.SIGNAL("canvasWidthChanged"), self.canvasWidthChanged) # we do not use the display timer since we have a special one # tell the caller by setting this variable as None self.update = None self.timer_time = QtCore.QTime() # FIXME # for smoothness, the following shoudl be observed # - the FFT should be done with Hamming, or Hanning or Kaiser windows # with 50% or more overlap. # - the animation should be advanced according to the actual time elapsed # since the last update. Proper advancement is done through interpolation. # (Linear or quadratic (causal!) interpolation should be fine first) # - ideally timer should be removed altogether and replaced by bloacking OpenGL # paintings synchronized to vsync # method def set_buffer(self, buffer): self.audiobuffer = buffer # method def custom_update(self): if not self.isVisible(): return # FIXME We should allow here for more intelligent transforms, especially when the log freq scale is selected floatdata = self.audiobuffer.data(self.fft_size) # for now, take the first channel only floatdata = floatdata[0,:] sp, freq, A, B, C = self.proc.analyzelive(floatdata, self.fft_size, self.maxfreq) # scale the db spectrum from [- spec_range db ... 0 db] > [0..1] epsilon = 1e-30 if self.weighting is 0: w = 0. elif self.weighting is 1: w = A elif self.weighting is 2: w = B else: w = C db_spectrogram = 20*log10(sp + epsilon) + w norm_spectrogram = (db_spectrogram.clip(min = self.spec_min, max = self.spec_max) - self.spec_min)/(self.spec_max - self.spec_min) self.PlotZoneImage.addData(freq, norm_spectrogram) def setminfreq(self, freq): self.minfreq = freq self.PlotZoneImage.setfreqrange(self.minfreq, self.maxfreq) def setmaxfreq(self, freq): self.maxfreq = freq self.PlotZoneImage.setfreqrange(self.minfreq, self.maxfreq) def setfftsize(self, fft_size): self.fft_size = fft_size def setmin(self, value): self.spec_min = value self.PlotZoneImage.setspecrange(self.spec_min, self.spec_max) def setmax(self, value): self.spec_max = value self.PlotZoneImage.setspecrange(self.spec_min, self.spec_max) def setweighting(self, weighting): self.weighting = weighting self.PlotZoneImage.setweighting(weighting) def settings_called(self, checked): self.settings_dialog.show() def saveState(self, settings): self.settings_dialog.saveState(settings) def restoreState(self, settings): self.settings_dialog.restoreState(settings) # slot def timerangechanged(self, value): self.timerange_s = value self.PlotZoneImage.settimerange(value) self.reset_timer() # slot def canvasWidthChanged(self, width): self.canvas_width = width self.reset_timer() # method def reset_timer(self): # FIXME millisecond resolution is limiting ! # need to find a way to integrate this cleverly in the GUI # When the period is smaller than 25 ms, we can reasonably # try to draw as many columns at once as possible self.period_ms = 1000.*self.timerange_s/self.canvas_width self.logger.push("Resetting the timer, will fire every %d ms" %(self.period_ms)) self.timer.setInterval(self.period_ms) # slot def timer_slot(self): #(chunks, t) = self.audiobuffer.update(self.audiobackend.stream) #self.chunk_number += chunks #self.buffer_timer_time = (95.*self.buffer_timer_time + 5.*t)/100. t = QtCore.QTime() t.start() self.custom_update() self.spectrogram_timer_time = (95.*self.spectrogram_timer_time + 5.*t.elapsed())/100.
class Spectrogram_Widget(QtWidgets.QWidget): name = "Spectrogram" def __init__(self, parent, logger=PrintLogger()): super().__init__(parent) self.logger = logger self.setObjectName(self.name) self.gridLayout = QtWidgets.QGridLayout(self) self.gridLayout.setObjectName("gridLayout") self.PlotZoneImage = ImagePlot(self, self.logger) self.PlotZoneImage.setObjectName("PlotZoneImage") self.gridLayout.addWidget(self.PlotZoneImage, 0, 1, 1, 1) # initialize the class instance that will do the fft self.proc = audioproc(self.logger) self.maxfreq = DEFAULT_MAXFREQ self.proc.set_maxfreq(self.maxfreq) self.minfreq = DEFAULT_MINFREQ self.fft_size = 2 ** DEFAULT_FFT_SIZE * 32 self.proc.set_fftsize(self.fft_size) self.spec_min = DEFAULT_SPEC_MIN self.spec_max = DEFAULT_SPEC_MAX self.weighting = DEFAULT_WEIGHTING self.update_weighting() self.freq = self.proc.get_freq_scale() self.timerange_s = DEFAULT_TIMERANGE self.canvas_width = 100. self.overlap = 3. / 4. self.overlap_frac = Fraction(3, 4) self.dT_s = self.fft_size * (1. - self.overlap) / float(SAMPLING_RATE) self.data_buffer = RingBuffer(1, 4 * self.fft_size) self.smoothing_buffer = RingBuffer(1, self.fft_size * self.overlap) self.PlotZoneImage.setlog10freqscale() # DEFAULT_FREQ_SCALE = 1 #log10 self.PlotZoneImage.setfreqrange(self.minfreq, self.maxfreq) self.PlotZoneImage.setspecrange(self.spec_min, self.spec_max) self.PlotZoneImage.setweighting(self.weighting) self.PlotZoneImage.settimerange(self.timerange_s, self.dT_s) self.update_jitter() sfft_rate_frac = Fraction(SAMPLING_RATE, self.fft_size) / (Fraction(1) - self.overlap_frac) / 1000 self.PlotZoneImage.set_sfft_rate(sfft_rate_frac) # initialize the settings dialog self.settings_dialog = Spectrogram_Settings_Dialog(self, self.logger) @staticmethod def log_spectrogram(sp): # Note: implementing the log10 of the array in Cython did not bring # any speedup. # Idea: Instead of computing the log of the data, I could pre-compute # a list of values associated with the colormap, and then do a search... epsilon = 1e-30 return 10. * log10(sp + epsilon) # scale the db spectrum from [- spec_range db ... 0 db] to [0..1] (do not clip, will be down after resampling) def scale_spectrogram(self, sp): return (sp - self.spec_min) / (self.spec_max - self.spec_min) def handle_new_data(self, floatdata): self.data_buffer.push(floatdata) new_sample_points = self.data_buffer.num_unread_data_points() # if we have enough data to add a frequency column in the time-frequency plane, compute it needed = int(self.fft_size * (1. - self.overlap)) realizable = int(floor(new_sample_points / needed)) if realizable > 0: spn = zeros((len(self.freq), realizable), dtype=float64) data = append(self.smoothing_buffer.unwound_data(), self.data_buffer.pop(realizable * needed)) # append flattens 1d arrays, so reshape if necessary if data.size == data.shape[0]: data = data.reshape([1, data.size]) for i in range(realizable): floatdata = data[:, i * needed:i * needed + self.fft_size] self.smoothing_buffer.push(floatdata[:, (self.fft_size - needed):]) # for now, take the first channel only floatdata = floatdata[0, :] # FFT transform spn[:, i] = self.proc.analyzelive(floatdata, "Power") w = tile(self.w, (1, realizable)) norm_spectrogram = self.scale_spectrogram(self.log_spectrogram(spn) + w) self.PlotZoneImage.addData(self.freq, norm_spectrogram) # thickness of a frequency column depends on FFT size and window overlap # hamming window with 75% overlap provides good quality (Perfect reconstruction, # aliasing from side lobes only, 42 dB channel isolation) # number of frequency columns that we keep depends on the time history that the user has chosen # actual displayed spectrogram is a scaled version of the time-frequency plane def canvasUpdate(self): if not self.isVisible(): return self.PlotZoneImage.draw() def update_jitter(self): audio_jitter = 2 * float(FRAMES_PER_BUFFER) / SAMPLING_RATE analysis_jitter = self.fft_size * (1. - self.overlap) / SAMPLING_RATE canvas_jitter = audio_jitter + analysis_jitter # print audio_jitter, analysis_jitter, canvas_jitter self.PlotZoneImage.plotImage.set_jitter(canvas_jitter) def pause(self): pass def setminfreq(self, freq): self.minfreq = freq self.PlotZoneImage.setfreqrange(self.minfreq, self.maxfreq) def setmaxfreq(self, freq): self.maxfreq = freq self.PlotZoneImage.setfreqrange(self.minfreq, self.maxfreq) self.proc.set_maxfreq(freq) self.update_weighting() self.freq = self.proc.get_freq_scale() def setfftsize(self, fft_size): self.fft_size = fft_size self.proc.set_fftsize(fft_size) self.update_weighting() self.freq = self.proc.get_freq_scale() self.dT_s = self.fft_size * (1. - self.overlap) / float(SAMPLING_RATE) self.PlotZoneImage.settimerange(self.timerange_s, self.dT_s) sfft_rate_frac = Fraction(SAMPLING_RATE, self.fft_size) / (Fraction(1) - self.overlap_frac) / 1000 self.PlotZoneImage.set_sfft_rate(sfft_rate_frac) self.update_jitter() def set_spect_min(self, value): self.spec_min = value self.PlotZoneImage.setspecrange(self.spec_min, self.spec_max) def set_spect_max(self, value): self.spec_max = value self.PlotZoneImage.setspecrange(self.spec_min, self.spec_max) def setweighting(self, weighting): self.weighting = weighting self.PlotZoneImage.setweighting(weighting) self.update_weighting() def update_weighting(self): A, B, C = self.proc.get_freq_weighting() if self.weighting is 0: self.w = array([0.]) elif self.weighting is 1: self.w = A elif self.weighting is 2: self.w = B else: self.w = C self.w.shape = (len(self.w), 1) def settings_called(self, checked): self.settings_dialog.show() def saveState(self, settings): self.settings_dialog.saveState(settings) def restoreState(self, settings): self.settings_dialog.restoreState(settings) # slot def timerangechanged(self, value): self.timerange_s = value self.PlotZoneImage.settimerange(self.timerange_s, self.dT_s) # slot def canvasWidthChanged(self, width): self.canvas_width = width
class Spectrogram_Widget(QtWidgets.QWidget): def __init__(self, parent): super().__init__(parent) self.setObjectName("Spectrogram_Widget") self.gridLayout = QtWidgets.QGridLayout(self) self.gridLayout.setObjectName("gridLayout") self.PlotZoneImage = ImagePlot(self) self.PlotZoneImage.setObjectName("PlotZoneImage") self.gridLayout.addWidget(self.PlotZoneImage, 0, 1, 1, 1) self.audiobuffer = None # initialize the class instance that will do the fft self.proc = audioproc() self.maxfreq = DEFAULT_MAXFREQ self.proc.set_maxfreq(self.maxfreq) self.minfreq = DEFAULT_MINFREQ self.fft_size = 2**DEFAULT_FFT_SIZE * 32 self.proc.set_fftsize(self.fft_size) self.spec_min = DEFAULT_SPEC_MIN self.spec_max = DEFAULT_SPEC_MAX self.weighting = DEFAULT_WEIGHTING self.update_weighting() self.freq = self.proc.get_freq_scale() self.timerange_s = DEFAULT_TIMERANGE self.canvas_width = 100. self.old_index = 0 self.overlap = 3. / 4. self.overlap_frac = Fraction(3, 4) self.dT_s = self.fft_size * (1. - self.overlap) / float(SAMPLING_RATE) self.PlotZoneImage.setlog10freqscale() # DEFAULT_FREQ_SCALE = 1 #log10 self.PlotZoneImage.setfreqrange(self.minfreq, self.maxfreq) self.PlotZoneImage.setspecrange(self.spec_min, self.spec_max) self.PlotZoneImage.setweighting(self.weighting) self.PlotZoneImage.settimerange(self.timerange_s, self.dT_s) self.update_jitter() sfft_rate_frac = Fraction(SAMPLING_RATE, self.fft_size) / ( Fraction(1) - self.overlap_frac) / 1000 self.PlotZoneImage.set_sfft_rate(sfft_rate_frac) # initialize the settings dialog self.settings_dialog = Spectrogram_Settings_Dialog(self) AudioBackend().underflow.connect( self.PlotZoneImage.plotImage.canvasscaledspectrogram.syncOffsets) self.last_data_time = 0. self.mustRestart = False # method def set_buffer(self, buffer): self.audiobuffer = buffer self.old_index = self.audiobuffer.ringbuffer.offset def log_spectrogram(self, sp): # Note: implementing the log10 of the array in Cython did not bring # any speedup. # Idea: Instead of computing the log of the data, I could pre-compute # a list of values associated with the colormap, and then do a search... epsilon = 1e-30 return 10. * log10(sp + epsilon) # scale the db spectrum from [- spec_range db ... 0 db] to [0..1] (do not clip, will be down after resampling) def scale_spectrogram(self, sp): return (sp - self.spec_min) / (self.spec_max - self.spec_min) def handle_new_data(self, floatdata): # we need to maintain an index of where we are in the buffer index = self.audiobuffer.ringbuffer.offset self.last_data_time = self.audiobuffer.lastDataTime available = index - self.old_index if available < 0: # ringbuffer must have grown or something... available = 0 self.old_index = index # if we have enough data to add a frequency column in the time-frequency plane, compute it needed = self.fft_size * (1. - self.overlap) realizable = int(floor(available / needed)) if realizable > 0: spn = zeros((len(self.freq), realizable), dtype=float64) for i in range(realizable): floatdata = self.audiobuffer.data_indexed( self.old_index, self.fft_size) # for now, take the first channel only floatdata = floatdata[0, :] # FFT transform spn[:, i] = self.proc.analyzelive(floatdata) self.old_index += int(needed) w = tile(self.w, (1, realizable)) norm_spectrogram = self.scale_spectrogram( self.log_spectrogram(spn) + w) self.PlotZoneImage.addData(self.freq, norm_spectrogram, self.last_data_time) if self.mustRestart: self.PlotZoneImage.restart() self.mustRestart = False # thickness of a frequency column depends on FFT size and window overlap # hamming window with 75% overlap provides good quality (Perfect reconstruction, # aliasing from side lobes only, 42 dB channel isolation) # number of frequency columns that we keep depends on the time history that the user has chosen # actual displayed spectrogram is a scaled version of the time-frequency plane def canvasUpdate(self): if not self.isVisible(): return self.PlotZoneImage.draw() def update_jitter(self): audio_jitter = 2 * float(FRAMES_PER_BUFFER) / SAMPLING_RATE analysis_jitter = self.fft_size * (1. - self.overlap) / SAMPLING_RATE canvas_jitter = audio_jitter + analysis_jitter # print audio_jitter, analysis_jitter, canvas_jitter self.PlotZoneImage.plotImage.set_jitter(canvas_jitter) def pause(self): self.PlotZoneImage.pause() def restart(self): # defer the restart until we get data from the audio source (so that a fresh lastdatatime is passed to the spectrogram image) self.mustRestart = True def setminfreq(self, freq): self.minfreq = freq self.PlotZoneImage.setfreqrange(self.minfreq, self.maxfreq) def setmaxfreq(self, freq): self.maxfreq = freq self.PlotZoneImage.setfreqrange(self.minfreq, self.maxfreq) self.proc.set_maxfreq(freq) self.update_weighting() self.freq = self.proc.get_freq_scale() def setfftsize(self, fft_size): self.fft_size = fft_size self.proc.set_fftsize(fft_size) self.update_weighting() self.freq = self.proc.get_freq_scale() self.dT_s = self.fft_size * (1. - self.overlap) / float(SAMPLING_RATE) self.PlotZoneImage.settimerange(self.timerange_s, self.dT_s) sfft_rate_frac = Fraction(SAMPLING_RATE, self.fft_size) / ( Fraction(1) - self.overlap_frac) / 1000 self.PlotZoneImage.set_sfft_rate(sfft_rate_frac) self.update_jitter() def setmin(self, value): self.spec_min = value self.PlotZoneImage.setspecrange(self.spec_min, self.spec_max) def setmax(self, value): self.spec_max = value self.PlotZoneImage.setspecrange(self.spec_min, self.spec_max) def setweighting(self, weighting): self.weighting = weighting self.PlotZoneImage.setweighting(weighting) self.update_weighting() def update_weighting(self): A, B, C = self.proc.get_freq_weighting() if self.weighting is 0: self.w = array([0.]) elif self.weighting is 1: self.w = A elif self.weighting is 2: self.w = B else: self.w = C self.w.shape = (len(self.w), 1) def settings_called(self, checked): self.settings_dialog.show() def saveState(self, settings): self.settings_dialog.saveState(settings) def restoreState(self, settings): self.settings_dialog.restoreState(settings) # slot def timerangechanged(self, value): self.timerange_s = value self.PlotZoneImage.settimerange(self.timerange_s, self.dT_s) # slot def canvasWidthChanged(self, width): self.canvas_width = width