High‑performance, thin, idiomatic Odin bindings for librtlsdr (RTL2832U / RTL-SDR USB dongles). Focus: zero fluff, immediate samples to your buffers, sync & async paths, ergonomic iteration while staying 1:1 with the C API.
warning: only testing on macOS.
You want raw IQ from a $20 dongle inside an Odin app (DSP, spectrum, decoding, visualization) without detouring through C glue. This repo gives you:
- Direct foreign imports (no wrappers hiding power)
- Sync + async streaming
- Full device + tuner control (freq, gains, PPM, direct sampling, offset tuning, IF gains, AGC)
- Works on macOS, Linux, (Should work on Windows with the included libraries, but haven't tested)
Core enumeration, open/close, frequency, sample rate, gain & streaming calls are bound. (Everything in the public header that matters to realtime IQ capture.)
macOS (Homebrew):
brew install librtlsdr odin
Linux (Debian/Ubuntu):
sudo apt install rtl-sdr odin
Arch:
sudo pacman -S rtl-sdr odin
Windows: NOT TESTED
Precompiled libraries on in rtlsdr/windows/x86 and rtlsdr/windows/x64.
If you want different ones, just replace them with your own.
- Install Zadig drivers for the dongle (WinUSB)
Minimal example (see examples/get_samples for a runnable variant):
package open_rtlsdr
import "core:fmt"
import "core:time"
import rtlsdr "../../rtlsdr"
main :: proc () {
using rtlsdr
num_devices := rtlsdr_get_device_count()
if num_devices == 0 {
fmt.println("No RTL-SDR devices found. Is it plugged in?")
return
}
dev: ^rtlsdr_dev
device_id: u32 = 0
ok := rtlsdr_open(&dev, device_id)
if ok != 0 {
fmt.println("Failed to open device", device_id)
return
} else {
fmt.println("Opened device", device_id, ":", rtlsdr_get_device_name(device_id))
}
rtlsdr_set_center_freq(dev, 101.1e6)
rtlsdr_set_sample_rate(dev, 1024000)
rtlsdr_set_agc_mode(dev, 0)
rtlsdr_reset_buffer(dev)
// define buffer and recv data
buffer_size: i32 = 1024 * 16 // 16KB buffer
buffer := make([]u8, buffer_size)
defer delete(buffer)
bytes_read: i32
read_result := rtlsdr_read_sync(dev, raw_data(buffer), buffer_size, &bytes_read)
fmt.println("Read result code:", read_result)
fmt.println("Bytes read:", bytes_read)
if read_result == 0 {
fmt.println("Successfully read", bytes_read, "bytes from RTL-SDR")
// Process the data in buffer here
fmt.println("First few samples:", buffer[:min(10, len(buffer))])
} else {
fmt.println("Failed to read from device, error:", read_result)
}
ok = rtlsdr_close(dev)
}Example Output:
Found Rafael Micro R828D tuner
RTL-SDR Blog V4 Detected
Opened device 0 : Generic RTL2832U OEM
Read result code: 0
Bytes read: 16384
Successfully read 16384 bytes from RTL-SDR
First few samples: [124, 127, 124, 127, 124, 126, 124, 126, 124, 126]
First few samples (converted to f32): [-0.027450979, -0.0039215684, -0.027450979, -0.0039215684, -0.027450979, -0.011764705, -0.027450979, -0.011764705, -0.027450979, -0.011764705]
Asynchronous mode spawns internal worker threads in the driver and invokes your callback continuously until cancelled:
read_cb :: proc "c" (data: ^u8, length: u32, ctx: rawptr) {
// length bytes: interleaved unsigned IQ (I,Q,I,Q,...)
}
// After configuring the device:
rtlsdr.rtlsdr_read_async(dev^, read_cb, nil, 0, 0) // 0 => driver defaults (e.g. 32 x 16k)
// Elsewhere to stop:
rtlsdr.rtlsdr_cancel_async(dev^)Use rtlsdr_wait_async if you set up the callback from another thread and want to block until start.
Device Management:
rtlsdr_get_device_count() -> u32- Get number of connected RTL-SDR devicesrtlsdr_get_device_name(index: u32) -> cstring- Get device name string by indexrtlsdr_open(dev: ^^rtlsdr_dev, index: u32) -> i32- Open device by indexrtlsdr_close(dev: ^rtlsdr_dev) -> i32- Close device
Hardware Info:
rtlsdr_get_usb_strings(dev: ^rtlsdr_dev, vendor: ^u8, product: ^u8, serial: ^u8) -> i32- Get USB device stringsrtlsdr_read_eeprom(dev: ^rtlsdr_dev, data: ^u8, offset: u8, len: u16) -> i32- Read EEPROM datartlsdr_write_eeprom(dev: ^rtlsdr_dev, data: ^u8, offset: u8, len: u16) -> i32- Write EEPROM datartlsdr_get_xtal_freq(dev: ^rtlsdr_dev, rtl_freq: ^u32, tuner_freq: ^u32) -> i32- Get crystal frequenciesrtlsdr_set_xtal_freq(dev: ^rtlsdr_dev, rtl_freq: u32, tuner_freq: u32) -> i32- Set crystal frequencies
Frequency Control:
rtlsdr_set_center_freq(dev: ^rtlsdr_dev, freq: u32) -> i32- Set center frequency in Hzrtlsdr_get_center_freq(dev: ^rtlsdr_dev) -> u32- Get current center frequencyrtlsdr_set_freq_correction(dev: ^rtlsdr_dev, ppm: i32) -> i32- Set frequency correction in PPMrtlsdr_get_freq_correction(dev: ^rtlsdr_dev) -> i32- Get frequency correction
Sample Rate:
rtlsdr_set_sample_rate(dev: ^rtlsdr_dev, rate: u32) -> i32- Set sample rate in Hzrtlsdr_get_sample_rate(dev: ^rtlsdr_dev) -> u32- Get current sample ratertlsdr_reset_buffer(dev: ^rtlsdr_dev) -> i32- Reset internal buffers
Gain Control:
rtlsdr_get_tuner_gains(dev: ^rtlsdr_dev, gains: ^i32) -> i32- Get supported gain valuesrtlsdr_set_tuner_gain_mode(dev: ^rtlsdr_dev, manual: i32) -> i32- Enable/disable manual gainrtlsdr_set_tuner_gain(dev: ^rtlsdr_dev, gain: i32) -> i32- Set tuner gainrtlsdr_get_tuner_gain(dev: ^rtlsdr_dev) -> i32- Get current tuner gainrtlsdr_set_agc_mode(dev: ^rtlsdr_dev, on: i32) -> i32- Enable/disable AGC
Advanced Settings:
rtlsdr_set_direct_sampling(dev: ^rtlsdr_dev, on: i32) -> i32- Enable direct sampling modertlsdr_set_offset_tuning(dev: ^rtlsdr_dev, on: i32) -> i32- Enable offset tuning
Data Streaming:
rtlsdr_read_sync(dev: ^rtlsdr_dev, buf: rawptr, len: i32, n_read: ^i32) -> i32- Synchronous readrtlsdr_read_async(dev: ^rtlsdr_dev, cb: rtlsdr_read_async_cb_t, ctx: rawptr, buf_num: u32, buf_len: u32) -> i32- Start async readingrtlsdr_cancel_async(dev: ^rtlsdr_dev) -> i32- Stop async readingrtlsdr_wait_async(dev: ^rtlsdr_dev, cb: rtlsdr_read_async_cb_t, ctx: rawptr) -> i32- Wait for async completion
Data Types:
rtlsdr_dev- Opaque device handlertlsdr_read_async_cb_t :: proc "c" (buf: ^u8, len: u32, ctx: rawptr)- Async callback type
Utility Functions:
utils.mhz_to_hz(freq: f64) -> u32- Convert MHz to Hzutils.hz_to_mhz(freq: u32) -> f64- Convert Hz to MHzutils.mhz_to_khz(freq: f64) -> f64- Convert MHz to kHzutils.khz_to_mhz(freq: u32) -> f64- Convert kHz to MHzutils.u8_to_f32(input: ^u8, len: u32) -> []f32- Convert interleaved u8 IQ data to normalized f32 array [-1, 1]
- Enumerate devices & pick index
rtlsdr_open- Configure: freq -> sample rate -> PPM -> gain mode/gain -> direct/offset options
rtlsdr_reset_buffer- Start sync loop OR async callback
- Process IQ (FFT, demod, etc.)
- Optional: adjust gain/frequency live
- Cancel async (if used) &
rtlsdr_close
Driver functions return 0 on success (or positive values for queries) and negative on error. Keep it simple:
if rtlsdr.rtlsdr_set_center_freq(dev^, freq) < 0 { /* handle */ }You can wrap return codes in your own helper for nicer error messages if desired.
- Use async for sustained high rates; it pre-fills multiple buffers.
- Keep your callback lean: push to a lock-free ring or channel for downstream DSP.
- Batch FFTs: convert unsigned IQ (0..255) -> float centered at 0 via
(f32(x)-127.5)/127.5. - Larger synchronous reads reduce syscall overhead but increase latency.
From repo root:
odin build examples/get_samples -out:build/get_samples
./build/get_samples
Or use provided script (if any) / copy into your project.
Copy the rtlsdr directory into your project. Reference it in your Odin code:
import "path/to/rtlsdr"Thats it.
Unsigned 8-bit interleaved IQ: [I0, Q0, I1, Q1, ...]. Convert to floats before DSP. Expect DC spike at center; apply DC removal / window / FFT.
These bindings: MIT (add MIT file if distributing). Underlying librtlsdr is LGPL;
RTL-SDR authors & Odin language community.
Have fun pulling RF out of thin air.