class MonitorSink(gr.hier_block2, ExportedState): """Convenience wrapper around all the bits and pieces to display the signal spectrum to the client. The units of the FFT output are dB power/Hz (power spectral density) relative to unit amplitude (i.e. dBFS assuming the source clips at +/-1). Note this is different from the standard logpwrfft result of power _per bin_, which would be undesirably dependent on the sample rate and bin size. """ def __init__(self, signal_type=None, enable_scope=False, freq_resolution=4096, time_length=2048, window_type=windows.WIN_BLACKMAN_HARRIS, frame_rate=30.0, input_center_freq=0.0, paused=False, context=None): assert isinstance(signal_type, SignalType) assert context is not None itemsize = signal_type.get_itemsize() gr.hier_block2.__init__( self, type(self).__name__, gr.io_signature(1, 1, itemsize), gr.io_signature(0, 0, 0), ) # constant parameters self.__power_offset = 40 # TODO autoset or controllable self.__itemsize = itemsize self.__context = context self.__enable_scope = enable_scope # settable parameters self.__signal_type = signal_type self.__freq_resolution = int(freq_resolution) self.__time_length = int(time_length) self.__window_type = _window_type_enum(window_type) self.__frame_rate = float(frame_rate) self.__input_center_freq = float(input_center_freq) self.__paused = bool(paused) # interest tracking # this is indirect because we ignore interest when paused self.__interested_cell = LooseCell(type=bool, value=False, writable=False, persists=False) self.__has_subscriptions = False self.__interest = InterestTracker(self.__cell_interest_callback) self.__fft_cell = ElementSinkCell(info_getter=self._get_fft_info, type=BulkDataT(array_format='b', info_format='dff'), interest_tracker=self.__interest, label='Spectrum') self.__scope_cell = ElementSinkCell(info_getter=self._get_scope_info, type=BulkDataT(array_format='f', info_format='d'), interest_tracker=self.__interest, label='Scope') # stuff created by __do_connect self.__gate = None self.__frame_dec = None self.__frame_rate_to_decimation_conversion = 0.0 self.__do_connect() def state_def(self): for d in super(MonitorSink, self).state_def(): yield d # TODO make this possible to be decorator style yield 'fft', self.__fft_cell yield 'scope', self.__scope_cell def __do_connect(self): itemsize = self.__itemsize if self.__signal_type.is_analytic(): input_length = self.__freq_resolution output_length = self.__freq_resolution self.__after_fft = None else: # use vector_to_streams to cut the output in half and discard the redundant part input_length = self.__freq_resolution * 2 output_length = self.__freq_resolution self.__after_fft = blocks.vector_to_streams( itemsize=output_length * gr.sizeof_float, nstreams=2) sample_rate = self.__signal_type.get_sample_rate() overlap_factor = int( math.ceil(_maximum_fft_rate * input_length / sample_rate)) # sanity limit -- OverlapGimmick is not free overlap_factor = min(16, overlap_factor) self.__frame_rate_to_decimation_conversion = sample_rate * overlap_factor / input_length self.__gate = blocks.copy(itemsize) self.__gate.set_enabled(not self.__paused) overlapper = _OverlappedStreamToVector(size=input_length, factor=overlap_factor, itemsize=itemsize) self.__frame_dec = blocks.keep_one_in_n( itemsize=itemsize * input_length, n=max( 1, int( round(self.__frame_rate_to_decimation_conversion / self.__frame_rate)))) # the actual FFT logic, which is similar to GR's logpwrfft_c window = windows.build(self.__window_type, input_length, 6.76) window_power = sum(x * x for x in window) # TODO: use fft_vfc when applicable fft_block = (fft_vcc if itemsize == gr.sizeof_gr_complex else fft_vfc)( fft_size=input_length, forward=True, window=window) mag_squared = blocks.complex_to_mag_squared(input_length) logarithmizer = blocks.nlog10_ff( n=10, # the "deci" in "decibel" vlen=input_length, k=( -to_dB(window_power) + # compensate for window -to_dB(sample_rate) + # convert from power-per-sample to power-per-Hz self.__power_offset # offset for packing into bytes )) # It would make slightly more sense to use unsigned chars, but blocks.float_to_uchar does not support vlen. self.__fft_converter = blocks.float_to_char( vlen=self.__freq_resolution, scale=1.0) fft_sink = self.__fft_cell.create_sink_internal( numpy.dtype((numpy.int8, output_length))) scope_sink = self.__scope_cell.create_sink_internal( numpy.dtype(('c8', self.__time_length))) scope_chunker = blocks.stream_to_vector_decimator( item_size=gr.sizeof_gr_complex, sample_rate=sample_rate, vec_rate=self.__frame_rate, # TODO doesn't need to be coupled vec_len=self.__time_length) # connect everything self.__context.lock() try: self.disconnect_all() self.connect(self, self.__gate, overlapper, self.__frame_dec, fft_block, mag_squared, logarithmizer) if self.__after_fft is not None: self.connect(logarithmizer, self.__after_fft) self.connect(self.__after_fft, self.__fft_converter, fft_sink) self.connect( (self.__after_fft, 1), blocks.null_sink(gr.sizeof_float * self.__freq_resolution)) else: self.connect(logarithmizer, self.__fft_converter, fft_sink) if self.__enable_scope: self.connect(self.__gate, scope_chunker, scope_sink) finally: self.__context.unlock() # non-exported # TODO: now that InterestTracker exists maybe use that interface instead def get_interested_cell(self): return self.__interested_cell def __cell_interest_callback(self, interested): self.__has_subscriptions = interested self.__update_interested() def __update_interested(self): self.__interested_cell.set_internal(not self.__paused and self.__has_subscriptions) @exported_value(type=SignalType, changes='explicit') def get_signal_type(self): return self.__signal_type # non-exported def set_signal_type(self, value): # TODO: don't rebuild if the rate did not change and the spectrum-sidedness of the type did not change assert self.__signal_type.compatible_items(value) self.__signal_type = value self.__do_connect() self.state_changed('signal_type') # non-exported def set_input_center_freq(self, value): self.__input_center_freq = float(value) @exported_value( type=RangeT([(2, 4096)], logarithmic=True, integer=True), changes='this_setter', label='Resolution', description='Frequency domain resolution; number of FFT bins.') def get_freq_resolution(self): return self.__freq_resolution @setter def set_freq_resolution(self, freq_resolution): self.__freq_resolution = freq_resolution self.__do_connect() @exported_value(type=RangeT([(1, 4096)], logarithmic=True, integer=True), changes='this_setter') def get_time_length(self): return self.__time_length @setter def set_time_length(self, value): self.__time_length = value self.__do_connect() @exported_value(type=_window_type_enum, changes='this_setter', label='Window', description='Window function applied before the FFT') def get_window_type(self): return self.__window_type @setter def set_window_type(self, value): self.__window_type = value # Updating window requires a reconnect because the nlog10 block does not allow changing its parameters. This could be fixed by using a separate regular add block. self.__do_connect() @exported_value(type=RangeT([(1, _maximum_fft_rate)], unit=units.Hz, logarithmic=True, integer=False), changes='this_setter', label='Rate', description='Number of FFT frames per second.') def get_frame_rate(self): return self.__frame_rate @setter def set_frame_rate(self, value): n = int(round(self.__frame_rate_to_decimation_conversion / value)) self.__frame_dec.set_n(n) # derive effective value by calculating inverse self.__frame_rate = self.__frame_rate_to_decimation_conversion / n @exported_value(type=bool, changes='this_setter', label='Pause') def get_paused(self): return self.__paused @setter def set_paused(self, value): self.__paused = value self.__gate.set_enabled(not value) self.__update_interested() # exported via state_def def _get_fft_info(self): return (self.__input_center_freq, self.__signal_type.get_sample_rate(), self.__power_offset) def _get_scope_info(self): return (self.__signal_type.get_sample_rate(), )
class MonitorSink(gr.hier_block2, ExportedState): """Convenience wrapper around all the bits and pieces to display the signal spectrum to the client. The units of the FFT output are dB power/Hz (power spectral density) relative to unit amplitude (i.e. dBFS assuming the source clips at +/-1). Note this is different from the standard logpwrfft result of power _per bin_, which would be undesirably dependent on the sample rate and bin size. """ implements(IMonitor) def __init__(self, signal_type=None, enable_scope=False, freq_resolution=4096, time_length=2048, frame_rate=30.0, input_center_freq=0.0, paused=False, context=None): assert isinstance(signal_type, SignalType) assert context is not None itemsize = signal_type.get_itemsize() gr.hier_block2.__init__( self, type(self).__name__, gr.io_signature(1, 1, itemsize), gr.io_signature(0, 0, 0), ) # constant parameters self.__power_offset = 40 # TODO autoset or controllable self.__itemsize = itemsize self.__context = context self.__enable_scope = enable_scope # settable parameters self.__signal_type = signal_type self.__freq_resolution = int(freq_resolution) self.__time_length = int(time_length) self.__frame_rate = float(frame_rate) self.__input_center_freq = float(input_center_freq) self.__paused = bool(paused) self.__interested_cell = LooseCell(key='interested', type=bool, value=False, writable=False, persists=False) # blocks self.__gate = None self.__fft_sink = None self.__scope_sink = None self.__scope_chunker = None self.__before_fft = None self.__logpwrfft = None self.__overlapper = None self.__rebuild() self.__connect() def state_def(self, callback): super(MonitorSink, self).state_def(callback) # TODO make this possible to be decorator style callback(StreamCell(self, 'fft', type=BulkDataT(array_format='b', info_format='dff'), label='Spectrum')) callback(StreamCell(self, 'scope', type=BulkDataT(array_format='f', info_format='d'), label='Scope')) def __rebuild(self): if self.__signal_type.is_analytic(): input_length = self.__freq_resolution output_length = self.__freq_resolution self.__after_fft = None else: # use vector_to_streams to cut the output in half and discard the redundant part input_length = self.__freq_resolution * 2 output_length = self.__freq_resolution self.__after_fft = blocks.vector_to_streams(itemsize=output_length * gr.sizeof_float, nstreams=2) sample_rate = self.__signal_type.get_sample_rate() overlap_factor = int(math.ceil(_maximum_fft_rate * input_length / sample_rate)) # sanity limit -- OverlapGimmick is not free overlap_factor = min(16, overlap_factor) self.__gate = blocks.copy(gr.sizeof_gr_complex) self.__gate.set_enabled(not self.__paused) self.__fft_sink = MessageDistributorSink( itemsize=output_length * gr.sizeof_char, context=self.__context, migrate=self.__fft_sink, notify=self.__update_interested) self.__overlapper = _OverlapGimmick( size=input_length, factor=overlap_factor, itemsize=self.__itemsize) # Adjusts units so displayed level is independent of resolution and sample rate. Also throw in the packing offset compensation = to_dB(input_length / sample_rate) + self.__power_offset # TODO: Consider not using the logpwrfft block self.__logpwrfft = logpwrfft.logpwrfft_c( sample_rate=sample_rate * overlap_factor, fft_size=input_length, ref_scale=10.0 ** (-compensation / 20.0) * 2, # not actually using this as a reference scale value but avoiding needing to use a separate add operation to apply the unit change -- this expression is the inverse of what logpwrfft does internally frame_rate=self.__frame_rate, avg_alpha=1.0, average=False) # It would make slightly more sense to use unsigned chars, but blocks.float_to_uchar does not support vlen. self.__fft_converter = blocks.float_to_char(vlen=self.__freq_resolution, scale=1.0) self.__scope_sink = MessageDistributorSink( itemsize=self.__time_length * gr.sizeof_gr_complex, context=self.__context, migrate=self.__scope_sink, notify=self.__update_interested) self.__scope_chunker = blocks.stream_to_vector_decimator( item_size=gr.sizeof_gr_complex, sample_rate=sample_rate, vec_rate=self.__frame_rate, # TODO doesn't need to be coupled vec_len=self.__time_length) def __connect(self): self.__context.lock() try: self.disconnect_all() self.connect( self, self.__gate, self.__overlapper, self.__logpwrfft) if self.__after_fft is not None: self.connect(self.__logpwrfft, self.__after_fft) self.connect(self.__after_fft, self.__fft_converter, self.__fft_sink) self.connect((self.__after_fft, 1), blocks.null_sink(gr.sizeof_float * self.__freq_resolution)) else: self.connect(self.__logpwrfft, self.__fft_converter, self.__fft_sink) if self.__enable_scope: self.connect( self.__gate, self.__scope_chunker, self.__scope_sink) finally: self.__context.unlock() # non-exported def get_interested_cell(self): return self.__interested_cell def __update_interested(self): self.__interested_cell.set_internal(not self.__paused and ( self.__fft_sink.get_subscription_count() > 0 or self.__scope_sink.get_subscription_count() > 0)) @exported_value(type=SignalType, changes='explicit') def get_signal_type(self): return self.__signal_type # non-exported def set_signal_type(self, value): # TODO: don't rebuild if the rate did not change and the spectrum-sidedness of the type did not change assert self.__signal_type.compatible_items(value) self.__signal_type = value self.__rebuild() self.__connect() self.state_changed('signal_type') # non-exported def set_input_center_freq(self, value): self.__input_center_freq = float(value) @exported_value( type=RangeT([(2, 4096)], logarithmic=True, integer=True), changes='this_setter', label='Resolution', description='Frequency domain resolution; number of FFT bins.') def get_freq_resolution(self): return self.__freq_resolution @setter def set_freq_resolution(self, freq_resolution): self.__freq_resolution = freq_resolution self.__rebuild() self.__connect() @exported_value(type=RangeT([(1, 4096)], logarithmic=True, integer=True), changes='this_setter') def get_time_length(self): return self.__time_length @setter def set_time_length(self, value): self.__time_length = value self.__rebuild() self.__connect() @exported_value( type=RangeT([(1, _maximum_fft_rate)], logarithmic=True, integer=False), changes='this_setter', label='Rate', description='Number of FFT frames per second.') def get_frame_rate(self): return self.__frame_rate @setter def set_frame_rate(self, value): self.__logpwrfft.set_vec_rate(float(value)) self.__frame_rate = self.__logpwrfft.frame_rate() @exported_value(type=bool, changes='this_setter', label='Pause') def get_paused(self): return self.__paused @setter def set_paused(self, value): self.__paused = value self.__gate.set_enabled(not value) self.__update_interested() # exported via state_def def get_fft_info(self): return (self.__input_center_freq, self.__signal_type.get_sample_rate(), self.__power_offset) def get_fft_distributor(self): return self.__fft_sink # exported via state_def def get_scope_info(self): return (self.__signal_type.get_sample_rate(),) def get_scope_distributor(self): return self.__scope_sink
class MonitorSink(gr.hier_block2, ExportedState): """Convenience wrapper around all the bits and pieces to display the signal spectrum to the client. The units of the FFT output are dB power/Hz (power spectral density) relative to unit amplitude (i.e. dBFS assuming the source clips at +/-1). Note this is different from the standard logpwrfft result of power _per bin_, which would be undesirably dependent on the sample rate and bin size. """ def __init__(self, signal_type=None, enable_scope=False, freq_resolution=4096, time_length=2048, frame_rate=30.0, input_center_freq=0.0, paused=False, context=None): assert isinstance(signal_type, SignalType) assert context is not None itemsize = signal_type.get_itemsize() gr.hier_block2.__init__( self, type(self).__name__, gr.io_signature(1, 1, itemsize), gr.io_signature(0, 0, 0), ) # constant parameters self.__power_offset = 40 # TODO autoset or controllable self.__itemsize = itemsize self.__context = context self.__enable_scope = enable_scope # settable parameters self.__signal_type = signal_type self.__freq_resolution = int(freq_resolution) self.__time_length = int(time_length) self.__frame_rate = float(frame_rate) self.__input_center_freq = float(input_center_freq) self.__paused = bool(paused) self.__interested_cell = LooseCell(type=bool, value=False, writable=False, persists=False) # stuff created by __do_connect self.__gate = None self.__fft_sink = None self.__scope_sink = None self.__frame_dec = None self.__frame_rate_to_decimation_conversion = 0.0 self.__do_connect() def state_def(self): for d in super(MonitorSink, self).state_def(): yield d # TODO make this possible to be decorator style yield 'fft', StreamCell(self, 'fft', type=BulkDataT(array_format='b', info_format='dff'), label='Spectrum') yield 'scope', StreamCell(self, 'scope', type=BulkDataT(array_format='f', info_format='d'), label='Scope') def __do_connect(self): itemsize = self.__itemsize if self.__signal_type.is_analytic(): input_length = self.__freq_resolution output_length = self.__freq_resolution self.__after_fft = None else: # use vector_to_streams to cut the output in half and discard the redundant part input_length = self.__freq_resolution * 2 output_length = self.__freq_resolution self.__after_fft = blocks.vector_to_streams(itemsize=output_length * gr.sizeof_float, nstreams=2) sample_rate = self.__signal_type.get_sample_rate() overlap_factor = int(math.ceil(_maximum_fft_rate * input_length / sample_rate)) # sanity limit -- OverlapGimmick is not free overlap_factor = min(16, overlap_factor) self.__frame_rate_to_decimation_conversion = sample_rate * overlap_factor / input_length self.__gate = blocks.copy(itemsize) self.__gate.set_enabled(not self.__paused) overlapper = _OverlappedStreamToVector( size=input_length, factor=overlap_factor, itemsize=itemsize) self.__frame_dec = blocks.keep_one_in_n( itemsize=itemsize * input_length, n=int(round(self.__frame_rate_to_decimation_conversion / self.__frame_rate))) # the actual FFT logic, which is similar to GR's logpwrfft_c window = windows.blackmanharris(input_length) window_power = sum(x * x for x in window) # TODO: use fft_vfc when applicable fft_block = (fft_vcc if itemsize == gr.sizeof_gr_complex else fft_vfc)( fft_size=input_length, forward=True, window=window) mag_squared = blocks.complex_to_mag_squared(input_length) logarithmizer = blocks.nlog10_ff( n=10, # the "deci" in "decibel" vlen=input_length, k=( -to_dB(window_power) + # compensate for window -to_dB(sample_rate) + # convert from power-per-sample to power-per-Hz self.__power_offset # offset for packing into bytes )) # It would make slightly more sense to use unsigned chars, but blocks.float_to_uchar does not support vlen. self.__fft_converter = blocks.float_to_char(vlen=self.__freq_resolution, scale=1.0) self.__fft_sink = MessageDistributorSink( itemsize=output_length * gr.sizeof_char, context=self.__context, migrate=self.__fft_sink, notify=self.__update_interested) self.__scope_sink = MessageDistributorSink( itemsize=self.__time_length * gr.sizeof_gr_complex, context=self.__context, migrate=self.__scope_sink, notify=self.__update_interested) scope_chunker = blocks.stream_to_vector_decimator( item_size=gr.sizeof_gr_complex, sample_rate=sample_rate, vec_rate=self.__frame_rate, # TODO doesn't need to be coupled vec_len=self.__time_length) # connect everything self.__context.lock() try: self.disconnect_all() self.connect( self, self.__gate, overlapper, self.__frame_dec, fft_block, mag_squared, logarithmizer) if self.__after_fft is not None: self.connect(logarithmizer, self.__after_fft) self.connect(self.__after_fft, self.__fft_converter, self.__fft_sink) self.connect((self.__after_fft, 1), blocks.null_sink(gr.sizeof_float * self.__freq_resolution)) else: self.connect(logarithmizer, self.__fft_converter, self.__fft_sink) if self.__enable_scope: self.connect( self.__gate, scope_chunker, self.__scope_sink) finally: self.__context.unlock() # non-exported def get_interested_cell(self): return self.__interested_cell def __update_interested(self): self.__interested_cell.set_internal(not self.__paused and ( self.__fft_sink.get_subscription_count() > 0 or self.__scope_sink.get_subscription_count() > 0)) @exported_value(type=SignalType, changes='explicit') def get_signal_type(self): return self.__signal_type # non-exported def set_signal_type(self, value): # TODO: don't rebuild if the rate did not change and the spectrum-sidedness of the type did not change assert self.__signal_type.compatible_items(value) self.__signal_type = value self.__do_connect() self.state_changed('signal_type') # non-exported def set_input_center_freq(self, value): self.__input_center_freq = float(value) @exported_value( type=RangeT([(2, 4096)], logarithmic=True, integer=True), changes='this_setter', label='Resolution', description='Frequency domain resolution; number of FFT bins.') def get_freq_resolution(self): return self.__freq_resolution @setter def set_freq_resolution(self, freq_resolution): self.__freq_resolution = freq_resolution self.__do_connect() @exported_value(type=RangeT([(1, 4096)], logarithmic=True, integer=True), changes='this_setter') def get_time_length(self): return self.__time_length @setter def set_time_length(self, value): self.__time_length = value self.__do_connect() @exported_value( type=RangeT([(1, _maximum_fft_rate)], unit=units.Hz, logarithmic=True, integer=False), changes='this_setter', label='Rate', description='Number of FFT frames per second.') def get_frame_rate(self): return self.__frame_rate @setter def set_frame_rate(self, value): n = int(round(self.__frame_rate_to_decimation_conversion / value)) self.__frame_dec.set_n(n) # derive effective value by calculating inverse self.__frame_rate = self.__frame_rate_to_decimation_conversion / n @exported_value(type=bool, changes='this_setter', label='Pause') def get_paused(self): return self.__paused @setter def set_paused(self, value): self.__paused = value self.__gate.set_enabled(not value) self.__update_interested() # exported via state_def def get_fft_info(self): return (self.__input_center_freq, self.__signal_type.get_sample_rate(), self.__power_offset) def get_fft_distributor(self): return self.__fft_sink # exported via state_def def get_scope_info(self): return (self.__signal_type.get_sample_rate(),) def get_scope_distributor(self): return self.__scope_sink