Beispiel #1
0
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(), )
Beispiel #2
0
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
Beispiel #3
0
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