Module: RTLSDR::DSP

Defined in:
lib/rtlsdr/dsp.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
  • Power spectrum analysis with windowing
  • Peak detection and frequency estimation
  • DC removal and filtering
  • Magnitude and phase extraction
  • Average power calculation

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)

Class Method Summary collapse

Class Method Details

.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)



95
96
97
98
99
100
# File 'lib/rtlsdr/dsp.rb', line 95

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

.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



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

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

.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



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

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

.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



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

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



152
153
154
# File 'lib/rtlsdr/dsp.rb', line 152

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 π)



166
167
168
# File 'lib/rtlsdr/dsp.rb', line 166

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



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

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



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

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