Module: RTLSDR::DSP

Defined in:
lib/rtlsdr/dsp.rb,
lib/rtlsdr/dsp/filter.rb

Overview

Digital Signal Processing utilities for RTL-SDR

The DSP module provides essential signal processing functions for working with RTL-SDR sample data. It includes utilities for converting raw IQ data to complex samples, calculating power spectra, performing filtering operations, and extracting signal characteristics.

All methods are designed to work with Ruby's Complex number type and standard Array collections, making them easy to integrate into Ruby applications and pipelines.

Features:

  • IQ data conversion to complex samples
  • FFT and IFFT via FFTW3 (when available)
  • Power spectrum analysis with windowing
  • Peak detection and frequency estimation
  • DC removal and filtering
  • Magnitude and phase extraction
  • Average power calculation
  • Decimation and resampling

Examples:

Basic signal analysis

raw_data = device.read_sync(2048)
samples = RTLSDR::DSP.iq_to_complex(raw_data)
power = RTLSDR::DSP.average_power(samples)
spectrum = RTLSDR::DSP.power_spectrum(samples)
peak_idx, peak_power = RTLSDR::DSP.find_peak(spectrum)

Signal conditioning

filtered = RTLSDR::DSP.remove_dc(samples)
magnitudes = RTLSDR::DSP.magnitude(filtered)
phases = RTLSDR::DSP.phase(filtered)

Defined Under Namespace

Classes: Filter

Class Method Summary collapse

Class Method Details

.apply_window(samples, window_type = :hanning) ⇒ Array<Complex>

Apply window function to samples

Applies a window function to reduce spectral leakage in FFT analysis. Supported windows: :hanning, :hamming, :blackman, :none

Examples:

Apply Hanning window

windowed = RTLSDR::DSP.apply_window(samples, :hanning)

Parameters:

  • samples (Array<Complex>)

    Input samples

  • window_type (Symbol) (defaults to: :hanning)

    Window function to apply

Returns:

  • (Array<Complex>)

    Windowed samples



304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
# File 'lib/rtlsdr/dsp.rb', line 304

def self.apply_window(samples, window_type = :hanning)
  n = samples.length
  return samples if n.zero? || window_type == :none

  samples.each_with_index.map do |sample, i|
    window = case window_type
             when :hanning
               0.5 * (1 - Math.cos(2 * Math::PI * i / (n - 1)))
             when :hamming
               0.54 - (0.46 * Math.cos(2 * Math::PI * i / (n - 1)))
             when :blackman
               0.42 - (0.5 * Math.cos(2 * Math::PI * i / (n - 1))) +
               (0.08 * Math.cos(4 * Math::PI * i / (n - 1)))
             else
               1.0
             end
    sample * window
  end
end

.average_power(samples) ⇒ Float

Calculate average power of complex samples

Computes the mean power (magnitude squared) across all samples. This is useful for signal strength measurements and AGC calculations.

Examples:

Measure signal power

power = RTLSDR::DSP.average_power(samples)
power_db = 10 * Math.log10(power + 1e-10)

Parameters:

  • samples (Array<Complex>)

    Array of complex samples

Returns:

  • (Float)

    Average power value (0.0 if no samples)



97
98
99
100
101
102
# File 'lib/rtlsdr/dsp.rb', line 97

def self.average_power(samples)
  return 0.0 if samples.empty?

  total_power = samples.reduce(0.0) { |sum, sample| sum + sample.abs2 }
  total_power / samples.length
end

.decimate(samples, factor, filter_taps: 31) ⇒ Array<Complex>

Decimate samples by an integer factor

Reduces the sample rate by applying a lowpass anti-aliasing filter and then downsampling. The cutoff frequency is automatically set to prevent aliasing.

Examples:

Decimate by 4

decimated = RTLSDR::DSP.decimate(samples, 4)

Parameters:

  • samples (Array<Complex>)

    Input samples

  • factor (Integer)

    Decimation factor (must be >= 1)

  • filter_taps (Integer) (defaults to: 31)

    Number of filter taps (more = sharper rolloff)

Returns:

  • (Array<Complex>)

    Decimated samples



340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
# File 'lib/rtlsdr/dsp.rb', line 340

def self.decimate(samples, factor, filter_taps: 31)
  return samples if factor <= 1

  # Design lowpass filter with cutoff at 0.5/factor of Nyquist
  cutoff = 0.5 / factor
  filter = design_lowpass(cutoff, filter_taps)

  # Apply filter
  filtered = convolve(samples, filter)

  # Downsample
  result = []
  (0...filtered.length).step(factor) { |i| result << filtered[i] }
  result
end

.estimate_frequency(samples, sample_rate) ⇒ Float

Estimate frequency using zero-crossing detection

Provides a rough frequency estimate by counting zero crossings in the magnitude signal. This is a simple method that works reasonably well for single-tone signals but may be inaccurate for complex signals.

Examples:

Estimate carrier frequency

freq_hz = RTLSDR::DSP.estimate_frequency(samples, 2_048_000)
puts "Estimated frequency: #{freq_hz} Hz"

Parameters:

  • samples (Array<Complex>)

    Array of complex samples

  • sample_rate (Integer)

    Sample rate in Hz

Returns:

  • (Float)

    Estimated frequency in Hz



184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
# File 'lib/rtlsdr/dsp.rb', line 184

def self.estimate_frequency(samples, sample_rate)
  return 0.0 if samples.length < 2

  magnitudes = magnitude(samples)
  zero_crossings = 0

  (1...magnitudes.length).each do |i|
    if (magnitudes[i - 1] >= 0 && magnitudes[i].negative?) ||
       (magnitudes[i - 1].negative? && magnitudes[i] >= 0)
      zero_crossings += 1
    end
  end

  # Frequency = (zero crossings / 2) / time_duration
  time_duration = samples.length.to_f / sample_rate
  (zero_crossings / 2.0) / time_duration
end

.fft(samples) ⇒ Array<Complex>

Compute forward FFT of complex samples

Performs a Fast Fourier Transform using FFTW3. The result is an array of complex frequency bins from DC to Nyquist to negative frequencies.

Examples:

Compute FFT

spectrum = RTLSDR::DSP.fft(samples)
magnitudes = spectrum.map(&:abs)

Parameters:

  • samples (Array<Complex>)

    Input complex time-domain samples

Returns:

  • (Array<Complex>)

    Complex frequency-domain bins

Raises:

  • (RuntimeError)

    if FFTW3 is not available



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

def self.fft(samples)
  raise "FFTW3 not available. Install libfftw3." unless fft_available?

  RTLSDR::FFTW.forward(samples)
end

.fft_available?Boolean

Check if FFT is available (FFTW3 loaded)

Examples:

Check FFT availability

if RTLSDR::DSP.fft_available?
  spectrum = RTLSDR::DSP.fft(samples)
end

Returns:

  • (Boolean)

    true if FFTW3 is available for FFT operations



213
214
215
# File 'lib/rtlsdr/dsp.rb', line 213

def self.fft_available?
  defined?(RTLSDR::FFTW) && RTLSDR::FFTW.available?
end

.fft_power_db(samples, window: :hanning) ⇒ Array<Float>

Compute power spectrum in decibels

Calculates the power spectrum using FFT and returns values in dB. Applies optional windowing to reduce spectral leakage.

Examples:

Get dB spectrum

power_db = RTLSDR::DSP.fft_power_db(samples, window: :hanning)

Parameters:

  • samples (Array<Complex>)

    Input complex samples

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

    Window type (:hanning, :hamming, :blackman, :none)

Returns:

  • (Array<Float>)

    Power spectrum in dB



260
261
262
263
264
# File 'lib/rtlsdr/dsp.rb', line 260

def self.fft_power_db(samples, window: :hanning)
  windowed = apply_window(samples, window)
  spectrum = fft(windowed)
  spectrum.map { |s| 10 * Math.log10(s.abs2 + 1e-20) }
end

.fft_shift(spectrum) ⇒ Array

Shift FFT output to center DC component

Rearranges FFT output so that DC (0 Hz) is in the center, with negative frequencies on the left and positive on the right. Similar to numpy.fft.fftshift.

Examples:

Center the spectrum

centered = RTLSDR::DSP.fft_shift(spectrum)

Parameters:

  • spectrum (Array)

    FFT output array

Returns:

  • (Array)

    Shifted spectrum with DC centered



276
277
278
279
280
# File 'lib/rtlsdr/dsp.rb', line 276

def self.fft_shift(spectrum)
  n = spectrum.length
  mid = n / 2
  spectrum[mid..] + spectrum[0...mid]
end

.find_peak(power_spectrum) ⇒ Array<Integer, Float>

Find peak power and frequency bin in spectrum

Locates the frequency bin with maximum power in a power spectrum. Returns both the bin index and the power value at that bin.

Examples:

Find strongest signal

spectrum = RTLSDR::DSP.power_spectrum(samples)
peak_bin, peak_power = RTLSDR::DSP.find_peak(spectrum)
freq_offset = (peak_bin - spectrum.length/2) * sample_rate / spectrum.length

Parameters:

  • power_spectrum (Array<Float>)

    Array of power values

Returns:

  • (Array<Integer, Float>)

    [bin_index, peak_power] or [0, 0.0] if empty



115
116
117
118
119
120
121
# File 'lib/rtlsdr/dsp.rb', line 115

def self.find_peak(power_spectrum)
  return [0, 0.0] if power_spectrum.empty?

  max_power = power_spectrum.max
  max_index = power_spectrum.index(max_power)
  [max_index, max_power]
end

.ifft(spectrum) ⇒ Array<Complex>

Compute inverse FFT of complex spectrum

Performs an Inverse Fast Fourier Transform using FFTW3. Converts frequency-domain data back to time-domain samples.

Examples:

Reconstruct time domain

reconstructed = RTLSDR::DSP.ifft(spectrum)

Parameters:

  • spectrum (Array<Complex>)

    Input complex frequency-domain bins

Returns:

  • (Array<Complex>)

    Complex time-domain samples

Raises:

  • (RuntimeError)

    if FFTW3 is not available



244
245
246
247
248
# File 'lib/rtlsdr/dsp.rb', line 244

def self.ifft(spectrum)
  raise "FFTW3 not available. Install libfftw3." unless fft_available?

  RTLSDR::FFTW.backward(spectrum)
end

.ifft_shift(spectrum) ⇒ Array

Inverse of fft_shift

Reverses the fft_shift operation to restore original FFT ordering.

Parameters:

  • spectrum (Array)

    Shifted spectrum

Returns:

  • (Array)

    Unshifted spectrum



288
289
290
291
292
# File 'lib/rtlsdr/dsp.rb', line 288

def self.ifft_shift(spectrum)
  n = spectrum.length
  mid = (n + 1) / 2
  spectrum[mid..] + spectrum[0...mid]
end

.interpolate(samples, factor, filter_taps: 31) ⇒ Array<Complex>

Interpolate samples by an integer factor

Increases the sample rate by inserting zeros and then applying a lowpass interpolation filter.

Examples:

Interpolate by 2

interpolated = RTLSDR::DSP.interpolate(samples, 2)

Parameters:

  • samples (Array<Complex>)

    Input samples

  • factor (Integer)

    Interpolation factor (must be >= 1)

  • filter_taps (Integer) (defaults to: 31)

    Number of filter taps

Returns:

  • (Array<Complex>)

    Interpolated samples



367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
# File 'lib/rtlsdr/dsp.rb', line 367

def self.interpolate(samples, factor, filter_taps: 31)
  return samples if factor <= 1

  # Insert zeros (upsample)
  upsampled = []
  samples.each do |sample|
    upsampled << sample
    (factor - 1).times { upsampled << Complex(0, 0) }
  end

  # Design lowpass filter
  cutoff = 0.5 / factor
  filter = design_lowpass(cutoff, filter_taps)

  # Apply filter and scale
  filtered = convolve(upsampled, filter)
  filtered.map { |s| s * factor }
end

.iq_to_complex(data) ⇒ Array<Complex>

Convert raw IQ data to complex samples

Converts raw 8-bit IQ data from RTL-SDR devices to Ruby Complex numbers. The RTL-SDR outputs unsigned 8-bit integers centered at 128, which are converted to floating point values in the range [-1.0, 1.0].

Examples:

Convert device samples

raw_data = [127, 130, 125, 135, 120, 140]  # 3 IQ pairs
samples = RTLSDR::DSP.iq_to_complex(raw_data)
# => [(-0.008+0.016i), (-0.024+0.055i), (-0.063+0.094i)]

Parameters:

  • data (Array<Integer>)

    Array of 8-bit unsigned integers (I, Q, I, Q, ...)

Returns:

  • (Array<Complex>)

    Array of Complex numbers representing I+jQ samples



49
50
51
52
53
54
55
56
57
# File 'lib/rtlsdr/dsp.rb', line 49

def self.iq_to_complex(data)
  samples = []
  (0...data.length).step(2) do |i|
    i_sample = (data[i] - 128) / 128.0
    q_sample = (data[i + 1] - 128) / 128.0
    samples << Complex(i_sample, q_sample)
  end
  samples
end

.magnitude(samples) ⇒ Array<Float>

Extract magnitude from complex samples

Calculates the magnitude (absolute value) of each complex sample. This converts I+jQ samples to their envelope/amplitude values.

Examples:

Get signal envelope

magnitudes = RTLSDR::DSP.magnitude(samples)
peak_amplitude = magnitudes.max

Parameters:

  • samples (Array<Complex>)

    Array of complex samples

Returns:

  • (Array<Float>)

    Array of magnitude values



154
155
156
# File 'lib/rtlsdr/dsp.rb', line 154

def self.magnitude(samples)
  samples.map(&:abs)
end

.phase(samples) ⇒ Array<Float>

Extract phase from complex samples

Calculates the phase angle (argument) of each complex sample in radians. The phase represents the angle between the I and Q components.

Examples:

Get phase information

phases = RTLSDR::DSP.phase(samples)
phase_degrees = phases.map { |p| p * 180 / Math::PI }

Parameters:

  • samples (Array<Complex>)

    Array of complex samples

Returns:

  • (Array<Float>)

    Array of phase values in radians (-π to π)



168
169
170
# File 'lib/rtlsdr/dsp.rb', line 168

def self.phase(samples)
  samples.map { |s| Math.atan2(s.imag, s.real) }
end

.power_spectrum(samples, window_size = 1024) ⇒ Array<Float>

Calculate power spectral density

Computes a basic power spectrum from complex samples using windowing. This applies a Hanning window to reduce spectral leakage and then calculates the power (magnitude squared) for each sample. A proper FFT implementation would require an external library.

Examples:

Calculate spectrum

spectrum = RTLSDR::DSP.power_spectrum(samples, 512)
max_power = spectrum.max

Parameters:

  • samples (Array<Complex>)

    Array of complex samples

  • window_size (Integer) (defaults to: 1024)

    Size of the analysis window (default 1024)

Returns:

  • (Array<Float>)

    Power spectrum values



72
73
74
75
76
77
78
79
80
81
82
83
84
85
# File 'lib/rtlsdr/dsp.rb', line 72

def self.power_spectrum(samples, window_size = 1024)
  return [] if samples.length < window_size

  windowed_samples = samples.take(window_size)

  # Apply Hanning window
  windowed_samples = windowed_samples.each_with_index.map do |sample, i|
    window_factor = 0.5 * (1 - Math.cos(2 * Math::PI * i / (window_size - 1)))
    sample * window_factor
  end

  # Simple magnitude calculation (real FFT would require external library)
  windowed_samples.map { |s| ((s.real**2) + (s.imag**2)) }
end

.remove_dc(samples, alpha = 0.995) ⇒ Array<Complex>

Remove DC component using high-pass filter

Applies a simple first-order high-pass filter to remove DC bias from the signal. This is useful for RTL-SDR devices which often have a DC offset in their I/Q samples.

Examples:

Remove DC bias

clean_samples = RTLSDR::DSP.remove_dc(samples, 0.99)

Parameters:

  • samples (Array<Complex>)

    Array of complex samples

  • alpha (Float) (defaults to: 0.995)

    Filter coefficient (0.995 = ~160Hz cutoff at 2.4MHz sample rate)

Returns:

  • (Array<Complex>)

    Filtered samples with DC component removed



134
135
136
137
138
139
140
141
142
# File 'lib/rtlsdr/dsp.rb', line 134

def self.remove_dc(samples, alpha = 0.995)
  return samples if samples.empty?

  filtered = [samples.first]
  (1...samples.length).each do |i|
    filtered[i] = samples[i] - samples[i - 1] + (alpha * filtered[i - 1])
  end
  filtered
end

.resample(samples, from_rate:, to_rate:, filter_taps: 31) ⇒ Array<Complex>

Resample to a new sample rate using rational resampling

Resamples by first interpolating then decimating. The interpolation and decimation factors are determined by the ratio of sample rates.

Examples:

Resample from 2.4 MHz to 48 kHz

resampled = RTLSDR::DSP.resample(samples, from_rate: 2_400_000, to_rate: 48_000)

Parameters:

  • samples (Array<Complex>)

    Input samples

  • from_rate (Integer)

    Original sample rate in Hz

  • to_rate (Integer)

    Target sample rate in Hz

  • filter_taps (Integer) (defaults to: 31)

    Number of filter taps

Returns:

  • (Array<Complex>)

    Resampled samples



398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
# File 'lib/rtlsdr/dsp.rb', line 398

def self.resample(samples, from_rate:, to_rate:, filter_taps: 31)
  return samples if from_rate == to_rate

  # Find GCD to minimize interpolation/decimation factors
  gcd = from_rate.gcd(to_rate)
  interp_factor = to_rate / gcd
  decim_factor = from_rate / gcd

  # Limit factors to reasonable values
  max_factor = 100
  if interp_factor > max_factor || decim_factor > max_factor
    # Fall back to simple linear interpolation for large ratios
    return linear_resample(samples, from_rate, to_rate)
  end

  # Interpolate then decimate
  result = samples
  result = interpolate(result, interp_factor, filter_taps: filter_taps) if interp_factor > 1
  result = decimate(result, decim_factor, filter_taps: filter_taps) if decim_factor > 1
  result
end