Class: RTLSDR::DSP::Filter

Inherits:
Object
  • Object
show all
Defined in:
lib/rtlsdr/dsp/filter.rb

Overview

FIR (Finite Impulse Response) filter class

Provides methods for designing and applying FIR filters to complex or real-valued samples. Supports lowpass, highpass, and bandpass filter types using the windowed sinc design method.

Examples:

Create and apply a lowpass filter

filter = RTLSDR::DSP::Filter.lowpass(cutoff: 100_000, sample_rate: 2_048_000)
filtered = filter.apply(samples)

Chain multiple filters

lpf = Filter.lowpass(cutoff: 100_000, sample_rate: 2_048_000)
hpf = Filter.highpass(cutoff: 1000, sample_rate: 2_048_000)
filtered = hpf.apply(lpf.apply(samples))

Instance Attribute Summary collapse

Class Method Summary collapse

Instance Method Summary collapse

Constructor Details

#initialize(coefficients, filter_type: :custom, window: :hamming) ⇒ Filter

Create a filter from existing coefficients

Parameters:

  • coefficients (Array<Float>)

    Filter coefficients

  • filter_type (Symbol) (defaults to: :custom)

    Type of filter

  • window (Symbol) (defaults to: :hamming)

    Window function used



159
160
161
162
163
164
# File 'lib/rtlsdr/dsp/filter.rb', line 159

def initialize(coefficients, filter_type: :custom, window: :hamming)
  @coefficients = coefficients.freeze
  @taps = coefficients.length
  @filter_type = filter_type
  @window = window
end

Instance Attribute Details

#coefficientsArray<Float> (readonly)

Returns Filter coefficients.

Returns:

  • (Array<Float>)

    Filter coefficients



21
22
23
# File 'lib/rtlsdr/dsp/filter.rb', line 21

def coefficients
  @coefficients
end

#filter_typeSymbol (readonly)

Returns Filter type (:lowpass, :highpass, :bandpass).

Returns:

  • (Symbol)

    Filter type (:lowpass, :highpass, :bandpass)



27
28
29
# File 'lib/rtlsdr/dsp/filter.rb', line 27

def filter_type
  @filter_type
end

#tapsInteger (readonly)

Returns Number of filter taps.

Returns:

  • (Integer)

    Number of filter taps



24
25
26
# File 'lib/rtlsdr/dsp/filter.rb', line 24

def taps
  @taps
end

#windowSymbol (readonly)

Returns Window function used (:hamming, :hanning, :blackman, :kaiser).

Returns:

  • (Symbol)

    Window function used (:hamming, :hanning, :blackman, :kaiser)



30
31
32
# File 'lib/rtlsdr/dsp/filter.rb', line 30

def window
  @window
end

Class Method Details

.bandpass(low:, high:, sample_rate:, taps: 63, window: :hamming) ⇒ Filter

Design a bandpass FIR filter

Creates a bandpass filter that passes frequencies between low and high cutoffs and attenuates frequencies outside that range.

Examples:

300-3000 Hz bandpass (voice) at 48 kHz

filter = Filter.bandpass(low: 300, high: 3000, sample_rate: 48_000)

Parameters:

  • low (Numeric)

    Lower cutoff frequency in Hz

  • high (Numeric)

    Upper cutoff frequency in Hz

  • sample_rate (Numeric)

    Sample rate in Hz

  • taps (Integer) (defaults to: 63)

    Number of filter taps

  • window (Symbol) (defaults to: :hamming)

    Window function

Returns:

  • (Filter)

    Configured bandpass filter

Raises:

  • (ArgumentError)


96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
# File 'lib/rtlsdr/dsp/filter.rb', line 96

def self.bandpass(low:, high:, sample_rate:, taps: 63, window: :hamming)
  raise ArgumentError, "low must be less than high" if low >= high

  # Ensure odd number of taps
  taps += 1 unless taps.odd?

  # Design as difference of two lowpass filters
  norm_low = low.to_f / sample_rate
  norm_high = high.to_f / sample_rate

  low_coeffs = design_sinc_filter(norm_low, taps, window)
  high_coeffs = design_sinc_filter(norm_high, taps, window)

  # Bandpass = highpass(low) convolved with lowpass(high)
  # Simpler: lowpass(high) - lowpass(low) then spectral shift
  # Even simpler: subtract lowpass from highpass equivalent
  coeffs = high_coeffs.zip(low_coeffs).map { |h, l| h - l }

  new(coeffs, filter_type: :bandpass, window: window)
end

.bandstop(low:, high:, sample_rate:, taps: 63, window: :hamming) ⇒ Filter

Design a bandstop (notch) FIR filter

Creates a filter that attenuates frequencies between low and high cutoffs and passes frequencies outside that range.

Parameters:

  • low (Numeric)

    Lower cutoff frequency in Hz

  • high (Numeric)

    Upper cutoff frequency in Hz

  • sample_rate (Numeric)

    Sample rate in Hz

  • taps (Integer) (defaults to: 63)

    Number of filter taps

  • window (Symbol) (defaults to: :hamming)

    Window function

Returns:

  • (Filter)

    Configured bandstop filter

Raises:

  • (ArgumentError)


128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
# File 'lib/rtlsdr/dsp/filter.rb', line 128

def self.bandstop(low:, high:, sample_rate:, taps: 63, window: :hamming)
  raise ArgumentError, "low must be less than high" if low >= high

  taps += 1 unless taps.odd?

  norm_low = low.to_f / sample_rate
  norm_high = high.to_f / sample_rate

  low_coeffs = design_sinc_filter(norm_low, taps, window)
  high_coeffs = design_sinc_filter(norm_high, taps, window)

  # Bandstop = allpass - bandpass = lowpass(low) + highpass(high)
  # highpass = spectral inversion of lowpass
  mid = taps / 2

  # Create highpass from high cutoff
  hp_coeffs = high_coeffs.map.with_index do |c, i|
    i == mid ? 1.0 - c : -c
  end

  # Add lowpass(low) + highpass(high)
  coeffs = low_coeffs.zip(hp_coeffs).map { |l, h| l + h }

  new(coeffs, filter_type: :bandstop, window: window)
end

.highpass(cutoff:, sample_rate:, taps: 63, window: :hamming) ⇒ Filter

Design a highpass FIR filter

Creates a highpass filter that passes frequencies above the cutoff and attenuates frequencies below it. Implemented via spectral inversion of a lowpass filter.

Examples:

1 kHz highpass at 48 kHz sample rate

filter = Filter.highpass(cutoff: 1000, sample_rate: 48_000, taps: 63)

Parameters:

  • cutoff (Numeric)

    Cutoff frequency in Hz

  • sample_rate (Numeric)

    Sample rate in Hz

  • taps (Integer) (defaults to: 63)

    Number of filter taps (must be odd for highpass)

  • window (Symbol) (defaults to: :hamming)

    Window function (:hamming, :hanning, :blackman)

Returns:

  • (Filter)

    Configured highpass filter



63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
# File 'lib/rtlsdr/dsp/filter.rb', line 63

def self.highpass(cutoff:, sample_rate:, taps: 63, window: :hamming)
  # Ensure odd number of taps for highpass
  taps += 1 unless taps.odd?

  normalized_cutoff = cutoff.to_f / sample_rate
  coeffs = design_sinc_filter(normalized_cutoff, taps, window)

  # Spectral inversion: negate all coefficients, add 1 to center tap
  mid = taps / 2
  coeffs = coeffs.map.with_index do |c, i|
    if i == mid
      1.0 - c
    else
      -c
    end
  end

  new(coeffs, filter_type: :highpass, window: window)
end

.lowpass(cutoff:, sample_rate:, taps: 63, window: :hamming) ⇒ Filter

Design a lowpass FIR filter

Creates a lowpass filter that passes frequencies below the cutoff and attenuates frequencies above it.

Examples:

100 kHz lowpass at 2.048 MHz sample rate

filter = Filter.lowpass(cutoff: 100_000, sample_rate: 2_048_000, taps: 64)

Parameters:

  • cutoff (Numeric)

    Cutoff frequency in Hz

  • sample_rate (Numeric)

    Sample rate in Hz

  • taps (Integer) (defaults to: 63)

    Number of filter taps (more = sharper rolloff, more delay)

  • window (Symbol) (defaults to: :hamming)

    Window function (:hamming, :hanning, :blackman)

Returns:

  • (Filter)

    Configured lowpass filter



44
45
46
47
48
# File 'lib/rtlsdr/dsp/filter.rb', line 44

def self.lowpass(cutoff:, sample_rate:, taps: 63, window: :hamming)
  normalized_cutoff = cutoff.to_f / sample_rate
  coeffs = design_sinc_filter(normalized_cutoff, taps, window)
  new(coeffs, filter_type: :lowpass, window: window)
end

Instance Method Details

#apply(samples) ⇒ Array<Complex, Float>

Apply the filter to samples using convolution

Examples:

Filter complex IQ samples

filtered = filter.apply(iq_samples)

Parameters:

  • samples (Array<Complex, Float>)

    Input samples

Returns:

  • (Array<Complex, Float>)

    Filtered samples



172
173
174
175
176
# File 'lib/rtlsdr/dsp/filter.rb', line 172

def apply(samples)
  return [] if samples.empty?

  convolve(samples, @coefficients)
end

#apply_zero_phase(samples) ⇒ Array<Complex, Float>

Apply filter with zero-phase (forward-backward filtering)

Filters the signal twice (forward then backward) to eliminate phase distortion. The effective filter order is doubled.

Parameters:

  • samples (Array<Complex, Float>)

    Input samples

Returns:

  • (Array<Complex, Float>)

    Zero-phase filtered samples



185
186
187
188
189
190
191
192
193
194
195
196
# File 'lib/rtlsdr/dsp/filter.rb', line 185

def apply_zero_phase(samples)
  return [] if samples.empty?

  # Forward filter
  forward = convolve(samples, @coefficients)
  # Reverse
  reversed = forward.reverse
  # Backward filter
  backward = convolve(reversed, @coefficients)
  # Reverse again
  backward.reverse
end

#frequency_response(points = 512) ⇒ Array<Float>

Get the frequency response of the filter

Computes the magnitude response at the specified number of frequency points. Requires FFTW3 to be available.

Parameters:

  • points (Integer) (defaults to: 512)

    Number of frequency points

Returns:

  • (Array<Float>)

    Magnitude response (linear scale)

Raises:

  • (RuntimeError)

    if FFTW3 is not available



206
207
208
209
210
211
212
213
214
215
216
217
# File 'lib/rtlsdr/dsp/filter.rb', line 206

def frequency_response(points = 512)
  raise "FFTW3 required for frequency response" unless DSP.fft_available?

  # Zero-pad coefficients to desired length
  padded = @coefficients + Array.new(points - @taps, 0.0)
  # Convert to complex
  complex_padded = padded.map { |c| Complex(c, 0) }
  # FFT
  spectrum = DSP.fft(complex_padded)
  # Return magnitude
  spectrum.map(&:abs)
end

#group_delayFloat

Get the group delay of the filter

For a symmetric FIR filter, the group delay is constant and equal to (taps - 1) / 2 samples.

Returns:

  • (Float)

    Group delay in samples



225
226
227
# File 'lib/rtlsdr/dsp/filter.rb', line 225

def group_delay
  (@taps - 1) / 2.0
end

#to_sString

Returns Human-readable filter description.

Returns:

  • (String)

    Human-readable filter description



230
231
232
# File 'lib/rtlsdr/dsp/filter.rb', line 230

def to_s
  "#{@filter_type.capitalize} FIR filter (#{@taps} taps, #{@window} window)"
end