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
Defined Under Namespace
Classes: Filter
Class Method Summary collapse
-
.apply_window(samples, window_type = :hanning) ⇒ Array<Complex>
Apply window function to samples.
-
.average_power(samples) ⇒ Float
Calculate average power of complex samples.
-
.decimate(samples, factor, filter_taps: 31) ⇒ Array<Complex>
Decimate samples by an integer factor.
-
.estimate_frequency(samples, sample_rate) ⇒ Float
Estimate frequency using zero-crossing detection.
-
.fft(samples) ⇒ Array<Complex>
Compute forward FFT of complex samples.
-
.fft_available? ⇒ Boolean
Check if FFT is available (FFTW3 loaded).
-
.fft_power_db(samples, window: :hanning) ⇒ Array<Float>
Compute power spectrum in decibels.
-
.fft_shift(spectrum) ⇒ Array
Shift FFT output to center DC component.
-
.find_peak(power_spectrum) ⇒ Array<Integer, Float>
Find peak power and frequency bin in spectrum.
-
.ifft(spectrum) ⇒ Array<Complex>
Compute inverse FFT of complex spectrum.
-
.ifft_shift(spectrum) ⇒ Array
Inverse of fft_shift.
-
.interpolate(samples, factor, filter_taps: 31) ⇒ Array<Complex>
Interpolate samples by an integer factor.
-
.iq_to_complex(data) ⇒ Array<Complex>
Convert raw IQ data to complex samples.
-
.magnitude(samples) ⇒ Array<Float>
Extract magnitude from complex samples.
-
.phase(samples) ⇒ Array<Float>
Extract phase from complex samples.
-
.power_spectrum(samples, window_size = 1024) ⇒ Array<Float>
Calculate power spectral density.
-
.remove_dc(samples, alpha = 0.995) ⇒ Array<Complex>
Remove DC component using high-pass filter.
-
.resample(samples, from_rate:, to_rate:, filter_taps: 31) ⇒ Array<Complex>
Resample to a new sample rate using rational resampling.
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
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.
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.
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.
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.
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)
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.
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.
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.
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.
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.
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.
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].
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.
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.
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.
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.
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.
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 |