I didn't look deeper into FutureSDR yet, but I was able to successfully receive radio transmissions (commercial FM broadcast and amateur radio) using these components:
I used tokio
to make the overall processing asynchronous. I had to use tokio::task::spawn_blocking
to use the blocking interface of soapysdr
with tokio
's async runtime. Looks something like that:
let dev = soapysdr::Device::new("").unwrap();
dev.set_frequency(Rx, 0, 433.5e6, "").unwrap();
dev.set_sample_rate(Rx, 0, 1024000.0).unwrap();
dev.set_bandwidth(Rx, 0, 1024000.0).unwrap();
let mut rx = dev.rx_stream::<Sample>(&[0]).unwrap();
let mtu = rx.mtu().unwrap();
rx.activate(None).unwrap();
let (rx_rf_send, rx_rf_recv) = channel::<Sample>(queue);
let join_handle = spawn_blocking(move || {
let mut buf_pool = ChunkBufPool::<Sample>::new();
loop {
let mut buffer = buf_pool.get();
buffer.resize_with(mtu, Default::default);
let count = rx.read(&[&mut buffer], 1000000).unwrap();
buffer.truncate(count);
rx_rf_send.send(buffer.finalize());
}
});
I then connect several futures (which get spawned) with some asynchronous channels:
let (rx_base_send, rx_base_recv) = channel::<Sample>(queue);
spawn(blocks::freq_shift(rx_rf_recv, rx_base_send, -75, 2 * 1024));
let (rx_down_send, rx_down_recv) = channel::<Sample>(queue);
spawn(blocks::downsample(
rx_base_recv,
rx_down_send,
1024000.0,
384000.0,
12500.0,
blocks::DownsampleOpts {
chunk_size: 4096,
..Default::default()
},
));
/* … */
cpal
requires me to provide a callback which writes the audio data into a buffer. Since that callback is invoked by a thread that I don't control, I used tokio::runtime::Handle::block_on
to be able to await
new data:
let rt = tokio::runtime::Handle::current();
/* … */
let host = cpal::default_host();
let device = host
.default_output_device()
.expect("no output device available");
let supported_ranges = device
.supported_output_configs()
.expect("no supported audio config");
let range = supported_ranges
.filter(|range| {
range.channels() == 1
&& range.min_sample_rate().0 <= 48000
&& range.max_sample_rate().0 >= 48000
&& range.sample_format() == cpal::SampleFormat::F32
})
.next()
.expect("no suitable audio config found");
let supported_config = range.with_sample_rate(cpal::SampleRate(48000));
let mut buffer_size = 2 * 4096;
match supported_config.buffer_size() {
&cpal::SupportedBufferSize::Range { min, max } => {
buffer_size = buffer_size.min(max).max(min)
}
&cpal::SupportedBufferSize::Unknown => (),
}
let config = cpal::StreamConfig {
channels: 1,
sample_rate: cpal::SampleRate(48000),
buffer_size: cpal::BufferSize::Fixed(buffer_size),
};
let err_fn = |err| eprintln!("an error occurred on the output audio stream: {}", err);
/* … */
let write_audio = move |data: &mut [f32], _: &cpal::OutputCallbackInfo| {
for sample in data.iter_mut() {
// method `recv_realtime` is async, i.e. it returns a future
/* … */ rt.block_on(rx_audio_down_recv.recv_realtime(0)) /* … */
*sample = /* … */;
/* … */
}
};
let stream = device
.build_output_stream(&config, write_audio, err_fn)
.unwrap();
stream.play().unwrap();
It's still work in progress and a bit ugly, but I'm happy it works. And I'm especially happy that the audio delay is low (which is relevant for realtime radio applications). To keep the audio delay low, I manually set a small buffer (might be good to test automatically how low it can be without causing underflows), and I monitor the len
of the last tokio::sync::broadcast::Receiver
to be small and discard chunks if there is congestion (which might happen because the time basis of the receiver and the audio device are not exactly synchronous).
So cpal
and rustfft
do fine!
Only thing that I'm missing in rustfft
is some specialized transforms, e.g. for real-valued signals or chunks where half of the data is zero, etc. I currently work almost entirely in the complex domain, which makes the code a bit easier to overlook but might come with a bit of unnecessary overhead when the imaginary part is known to be zero.
P.S.: I tested this workflow on FreeBSD and with this SDR stick, but also want to try out the LimeSDR Mini to be able to transmit (the RTL SDR can only receive). Haven't tested this on Windows or Mac yet.