RTL-SDR Ruby Gem

A comprehensive Ruby gem for interfacing with RTL-SDR (Software Defined Radio) devices. This gem provides both low-level FFI bindings that closely match the C API and high-level Ruby-idiomatic classes for easy use.

gem | docs

Features

  • Complete C API Coverage: All librtlsdr functions are exposed through FFI
  • Ruby Idiomatic Interface: High-level classes with Ruby conventions
  • Async/Sync Reading: Both synchronous and asynchronous sample reading
  • Signal Processing: Built-in DSP functions for common operations
  • Frequency Scanning: Sweep frequencies and find active signals
  • Enumerable Interface: Use Ruby's enumerable methods on sample streams
  • Error Handling: Proper Ruby exceptions for different error conditions

Installation

First, make sure you have librtlsdr installed on your system:

Ubuntu/Debian

sudo apt-get install librtlsdr-dev

macOS (Homebrew)

brew install librtlsdr

Optional: FFTW3 for FFT Support

To enable FFT features (fast spectrum analysis, frequency response, etc.), install FFTW3:

# Ubuntu/Debian
sudo apt-get install libfftw3-dev

# macOS
brew install fftw

FFT features are optional - the gem works without FFTW3, but FFT-related functions will raise an error if called.

From Source

If you need to build librtlsdr from source, the gem will automatically try to build it:

git clone https://github.com/your-repo/rtlsdr-ruby.git
cd rtlsdr-ruby
bundle install
rake build_librtlsdr  # This will build librtlsdr in librtlsdr/build/install/

Then install the gem:

gem install rtlsdr

Or add to your Gemfile:

gem 'rtlsdr'

Quick Start

require 'rtlsdr'

# List available devices
puts "Found #{RTLSDR.device_count} RTL-SDR devices:"
RTLSDR.devices.each_with_index do |device, i|
  puts "#{i}: #{device[:name]} (#{device[:usb_strings][:product]})"
end

# Open first device
device = RTLSDR.open(0)

# Configure for FM radio
device.configure(
  frequency: 100_500_000,    # 100.5 MHz
  sample_rate: 2_048_000,    # 2.048 MHz
  gain: 400                  # 40.0 dB
)

# Read some samples
samples = device.read_samples(1024)
puts "Read #{samples.length} complex samples"

# Calculate average power
power = RTLSDR::DSP.average_power(samples)
power_db = 10 * Math.log10(power + 1e-10)
puts "Signal power: #{power_db.round(2)} dB"

device.close

Device Control

Opening and Configuring Devices

# Open specific device by index
device = RTLSDR.open(0)

# Get device information
puts device.name          # Device name
puts device.tuner_name    # Tuner chip name
puts device.usb_strings   # USB manufacturer, product, serial

# Configure all at once
device.configure(
  frequency: 433_920_000,     # 433.92 MHz
  sample_rate: 2_048_000,     # 2.048 MHz
  gain: 300,                  # 30.0 dB (gains are in tenths of dB)
  freq_correction: 15,        # 15 PPM frequency correction
  agc_mode: false,           # Disable AGC
  test_mode: false,          # Disable test mode
  bias_tee: false            # Disable bias tee
)

# Or configure individually
device.center_freq = 433_920_000
device.sample_rate = 2_048_000
device.manual_gain_mode!        # Enable manual gain
device.tuner_gain = 300         # 30.0 dB

# Check available gains
puts device.tuner_gains.map { |g| g / 10.0 }  # [0.0, 0.9, 1.4, 2.7, ...]

Reading Samples

# Synchronous reading
samples = device.read_samples(4096)  # Returns array of Complex numbers

# Asynchronous reading
device.read_samples_async do |samples|
  # Process samples in real-time
  power = RTLSDR::DSP.average_power(samples)
  puts "Power: #{10 * Math.log10(power + 1e-10)} dB"
end

# Stop async reading
device.cancel_async

# Enumerable interface - read continuously
device.each(samples_per_read: 1024) do |samples|
  # Process each batch of samples
  break if some_condition
end

Signal Processing

The gem includes built-in DSP functions:

samples = device.read_samples(8192)

# Power calculations
avg_power = RTLSDR::DSP.average_power(samples)
power_db = 10 * Math.log10(avg_power + 1e-10)

# Convert to magnitude and phase
magnitude = RTLSDR::DSP.magnitude(samples)
phase = RTLSDR::DSP.phase(samples)

# Remove DC component
filtered = RTLSDR::DSP.remove_dc(samples)

# Frequency estimation
freq_offset = RTLSDR::DSP.estimate_frequency(samples, device.sample_rate)

# Power spectrum (simple implementation)
power_spectrum = RTLSDR::DSP.power_spectrum(samples, 1024)
peak_bin, peak_power = RTLSDR::DSP.find_peak(power_spectrum)

FFT Analysis (requires FFTW3)

# Check if FFT is available
if RTLSDR::DSP.fft_available?
  # Forward FFT
  spectrum = RTLSDR::DSP.fft(samples)

  # Power spectrum in dB with windowing
  power_db = RTLSDR::DSP.fft_power_db(samples, window: :hanning)

  # Shift DC to center (like numpy.fft.fftshift)
  centered = RTLSDR::DSP.fft_shift(spectrum)

  # Inverse FFT
  reconstructed = RTLSDR::DSP.ifft(spectrum)
end

# Available window functions: :hanning, :hamming, :blackman, :none
windowed = RTLSDR::DSP.apply_window(samples, :blackman)

Digital Filters

# Create lowpass filter (100 kHz cutoff at 2.048 MHz sample rate)
lpf = RTLSDR::DSP::Filter.lowpass(
  cutoff: 100_000,
  sample_rate: 2_048_000,
  taps: 63,
  window: :hamming
)

# Create highpass filter
hpf = RTLSDR::DSP::Filter.highpass(cutoff: 1000, sample_rate: 48_000)

# Create bandpass filter (voice: 300-3000 Hz)
bpf = RTLSDR::DSP::Filter.bandpass(low: 300, high: 3000, sample_rate: 48_000)

# Create bandstop (notch) filter
notch = RTLSDR::DSP::Filter.bandstop(low: 50, high: 60, sample_rate: 48_000)

# Apply filter
filtered = lpf.apply(samples)

# Zero-phase filtering (no phase distortion)
filtered = lpf.apply_zero_phase(samples)

# Get filter properties
puts lpf.group_delay  # Delay in samples
puts lpf.taps         # Number of filter taps

# Get frequency response (requires FFTW3)
response = lpf.frequency_response(256)

Decimation and Resampling

# Decimate by factor of 4 (with anti-aliasing filter)
decimated = RTLSDR::DSP.decimate(samples, 4)

# Interpolate by factor of 2
interpolated = RTLSDR::DSP.interpolate(samples, 2)

# Resample from 2.4 MHz to 48 kHz
audio = RTLSDR::DSP.resample(samples, from_rate: 2_400_000, to_rate: 48_000)

Demodulation

The gem includes demodulators for common radio signals:

FM Demodulation

# Wideband FM (broadcast radio 88-108 MHz)
audio = RTLSDR::Demod.fm(samples, sample_rate: 2_048_000, audio_rate: 48_000)

# With European de-emphasis (50µs instead of US 75µs)
audio = RTLSDR::Demod.fm(samples, sample_rate: 2_048_000, tau: 50e-6)

# Narrowband FM (voice radio, ham, FRS)
audio = RTLSDR::Demod.nfm(samples, sample_rate: 2_048_000)

AM Demodulation

# Envelope detection (simple AM)
audio = RTLSDR::Demod.am(samples, sample_rate: 2_048_000)

# Synchronous AM (better quality)
audio = RTLSDR::Demod.am_sync(samples, sample_rate: 2_048_000)

SSB Demodulation

# Upper Sideband (ham radio above 10 MHz)
audio = RTLSDR::Demod.usb(samples, sample_rate: 2_048_000, bfo_offset: 1500)

# Lower Sideband (ham radio below 10 MHz)
audio = RTLSDR::Demod.lsb(samples, sample_rate: 2_048_000, bfo_offset: 1500)

Helper Functions

# Generate complex oscillator for mixing
osc = RTLSDR::Demod.complex_oscillator(1024, 1000, 48_000)

# Frequency shift a signal
shifted = RTLSDR::Demod.mix(samples, -10_000, 2_048_000)

# FM discriminator (phase difference)
baseband = RTLSDR::Demod.phase_diff(samples)

# De-emphasis filter for FM audio
filtered = RTLSDR::Demod.deemphasis(audio, 75e-6, 48_000)

Frequency Scanning

Scan frequency ranges to find active signals:

# Create a scanner
scanner = RTLSDR::Scanner.new(device,
  start_freq: 88_000_000,      # 88 MHz
  end_freq: 108_000_000,       # 108 MHz
  step_size: 200_000,          # 200 kHz steps
  dwell_time: 0.1              # 100ms per frequency
)

# Perform power sweep
results = scanner.power_sweep(samples_per_freq: 2048)
results.each do |freq, power_db|
  puts "#{freq/1e6} MHz: #{power_db} dB" if power_db > -60
end

# Find peaks above threshold
peaks = scanner.find_peaks(threshold: -50, samples_per_freq: 4096)
peaks.each do |peak|
  puts "#{peak[:frequency]/1e6} MHz: #{peak[:power_db]} dB"
end

# Real-time scanning with callback
scanner.scan(samples_per_freq: 1024) do |result|
  if result[:power] > some_threshold
    puts "Signal found at #{result[:frequency]/1e6} MHz"
  end
end

Advanced Features

EEPROM Access

# Read EEPROM
data = device.read_eeprom(0, 256)  # Read 256 bytes from offset 0

# Write EEPROM (be careful!)
device.write_eeprom([0x01, 0x02, 0x03], 0)  # Write 3 bytes at offset 0

Crystal Frequency Adjustment

# Set custom crystal frequencies
device.set_xtal_freq(28_800_000, 28_800_000)  # RTL and tuner crystal freqs

# Get current crystal frequencies
rtl_freq, tuner_freq = device.xtal_freq

Direct Sampling Mode

# Enable direct sampling (for HF reception)
device.direct_sampling = 1  # I-ADC input
device.direct_sampling = 2  # Q-ADC input
device.direct_sampling = 0  # Disabled

Bias Tee Control

# Enable bias tee on GPIO 0
device.bias_tee = true

# Enable bias tee on specific GPIO
device.set_bias_tee_gpio(1, true)

Error Handling

The gem provides specific exception types:

begin
  device = RTLSDR.open(99)  # Non-existent device
rescue RTLSDR::DeviceNotFoundError => e
  puts "Device not found: #{e.message}"
rescue RTLSDR::DeviceOpenError => e
  puts "Could not open device: #{e.message}"
rescue RTLSDR::Error => e
  puts "RTL-SDR error: #{e.message}"
end

Exception types:

  • RTLSDR::Error - Base error class
  • RTLSDR::DeviceNotFoundError - Device doesn't exist
  • RTLSDR::DeviceOpenError - Can't open device (in use, permissions, etc.)
  • RTLSDR::DeviceNotOpenError - Device is not open
  • RTLSDR::InvalidArgumentError - Invalid parameter
  • RTLSDR::OperationFailedError - Operation failed
  • RTLSDR::EEPROMError - EEPROM access error

Examples

See the examples/ directory for complete examples:

  • basic_usage.rb - Basic device control and sample reading
  • spectrum_analyzer.rb - Advanced spectrum analysis and scanning

Run examples:

ruby examples/basic_usage.rb
ruby examples/spectrum_analyzer.rb

API Reference

Module Methods

  • RTLSDR.device_count - Number of connected devices
  • RTLSDR.device_name(index) - Get device name
  • RTLSDR.device_usb_strings(index) - Get USB strings
  • RTLSDR.find_device_by_serial(serial) - Find device by serial number
  • RTLSDR.open(index) - Open device and return Device instance
  • RTLSDR.devices - List all devices with info

Device Methods

Device Control

  • #open?, #closed? - Check device state
  • #close - Close device
  • #configure(options) - Configure multiple settings at once
  • #info - Get device information hash

Frequency Control

  • #center_freq, #center_freq= - Center frequency (Hz)
  • #frequency, #frequency= - Alias for center_freq
  • #freq_correction, #freq_correction= - Frequency correction (PPM)
  • #set_xtal_freq(rtl_freq, tuner_freq) - Set crystal frequencies
  • #xtal_freq - Get crystal frequencies

Gain Control

  • #tuner_gains - Available gain values (tenths of dB)
  • #tuner_gain, #tuner_gain= - Current gain (tenths of dB)
  • #gain, #gain= - Alias for tuner_gain
  • #tuner_gain_mode= - Set manual (true) or auto (false) gain
  • #manual_gain_mode!, #auto_gain_mode! - Convenience methods
  • #set_tuner_if_gain(stage, gain) - Set IF gain for specific stage
  • #tuner_bandwidth= - Set tuner bandwidth

Sample Rate and Modes

  • #sample_rate, #sample_rate= - Sample rate (Hz)
  • #test_mode=, #test_mode! - Enable test mode
  • #agc_mode=, #agc_mode! - Enable AGC
  • #direct_sampling, #direct_sampling= - Direct sampling mode
  • #offset_tuning, #offset_tuning=, #offset_tuning! - Offset tuning
  • #bias_tee=, #bias_tee! - Bias tee control
  • #set_bias_tee_gpio(gpio, enabled) - GPIO-specific bias tee

Sample Reading

  • #read_samples(count) - Read complex samples synchronously
  • #read_sync(length) - Read raw bytes synchronously
  • #read_samples_async(&block) - Read samples asynchronously
  • #read_async(&block) - Read raw bytes asynchronously
  • #cancel_async - Stop async reading
  • #streaming? - Check if async reading is active
  • #reset_buffer - Reset device buffer
  • #each(options, &block) - Enumerable interface

EEPROM Access

  • #read_eeprom(offset, length) - Read EEPROM data
  • #write_eeprom(data, offset) - Write EEPROM data

DSP Functions

Basic Functions

  • RTLSDR::DSP.iq_to_complex(data) - Convert IQ bytes to complex samples
  • RTLSDR::DSP.average_power(samples) - Calculate average power
  • RTLSDR::DSP.power_spectrum(samples, window_size) - Power spectrum
  • RTLSDR::DSP.find_peak(power_spectrum) - Find peak in spectrum
  • RTLSDR::DSP.remove_dc(samples, alpha) - DC removal filter
  • RTLSDR::DSP.magnitude(samples) - Convert to magnitude
  • RTLSDR::DSP.phase(samples) - Extract phase information
  • RTLSDR::DSP.estimate_frequency(samples, sample_rate) - Frequency estimation

FFT Functions (requires FFTW3)

  • RTLSDR::DSP.fft_available? - Check if FFTW3 is available
  • RTLSDR::DSP.fft(samples) - Forward FFT
  • RTLSDR::DSP.ifft(spectrum) - Inverse FFT
  • RTLSDR::DSP.fft_power_db(samples, window:) - Power spectrum in dB
  • RTLSDR::DSP.fft_shift(spectrum) - Shift DC to center
  • RTLSDR::DSP.ifft_shift(spectrum) - Reverse fft_shift
  • RTLSDR::DSP.apply_window(samples, type) - Apply window function

Decimation and Resampling

  • RTLSDR::DSP.decimate(samples, factor) - Decimate with anti-aliasing
  • RTLSDR::DSP.interpolate(samples, factor) - Interpolate samples
  • RTLSDR::DSP.resample(samples, from_rate:, to_rate:) - Rational resampling

Filter Class

  • Filter.lowpass(cutoff:, sample_rate:, taps:, window:) - Design lowpass filter
  • Filter.highpass(cutoff:, sample_rate:, taps:, window:) - Design highpass filter
  • Filter.bandpass(low:, high:, sample_rate:, taps:, window:) - Design bandpass filter
  • Filter.bandstop(low:, high:, sample_rate:, taps:, window:) - Design bandstop filter
  • #apply(samples) - Apply filter to samples
  • #apply_zero_phase(samples) - Zero-phase filtering
  • #frequency_response(points) - Get filter frequency response
  • #group_delay - Get filter group delay
  • #coefficients - Get filter coefficients
  • #taps - Number of filter taps

Demod Module

FM Demodulation

  • Demod.fm(samples, sample_rate:, audio_rate:, deviation:, tau:) - Wideband FM
  • Demod.nfm(samples, sample_rate:, audio_rate:, deviation:) - Narrowband FM

AM Demodulation

  • Demod.am(samples, sample_rate:, audio_rate:, audio_bandwidth:) - Envelope detection
  • Demod.am_sync(samples, sample_rate:, audio_rate:, audio_bandwidth:) - Synchronous AM

SSB Demodulation

  • Demod.usb(samples, sample_rate:, audio_rate:, bfo_offset:) - Upper Sideband
  • Demod.lsb(samples, sample_rate:, audio_rate:, bfo_offset:) - Lower Sideband

Helper Functions

  • Demod.complex_oscillator(length, frequency, sample_rate) - Generate carrier
  • Demod.mix(samples, frequency, sample_rate) - Frequency shift signal
  • Demod.phase_diff(samples) - FM discriminator
  • Demod.deemphasis(samples, tau, sample_rate) - De-emphasis filter

Scanner Class

  • Scanner.new(device, options) - Create frequency scanner
  • #scan(&block) - Perform frequency sweep with callback
  • #scan_async(&block) - Async frequency sweep
  • #power_sweep(options) - Get power measurements across frequencies
  • #find_peaks(options) - Find signal peaks above threshold
  • #stop - Stop scanning
  • #configure(options) - Update scan parameters

Contributing

  1. Fork the repository
  2. Create your feature branch (git checkout -b my-new-feature)
  3. Write tests for your changes
  4. Make sure all tests pass (rake spec)
  5. Commit your changes (git commit -am 'Add some feature')
  6. Push to the branch (git push origin my-new-feature)
  7. Create a Pull Request

License

This gem is licensed under the MIT license.

Requirements

  • Ruby 3.3 or later
  • librtlsdr (installed system-wide or built locally)
  • FFI gem
  • libfftw3 (optional, for FFT functions)

Supported Platforms

  • Linux (tested on Ubuntu)
  • macOS (tested on macOS 10.15+)
  • Windows (should work but not extensively tested)

Credits

This gem provides Ruby bindings for librtlsdr, the excellent RTL-SDR library by Steve Markgraf and contributors.