sdrplay-fm-radio/radio.py

207 lines
6.5 KiB
Python

from threading import Thread
import SoapySDR as soapy
import prefixed
from formats import *
import sys
import subprocess
import struct
from soapyhelpers import *
from samplerates import *
from streamer import Streamer, is_alive
class Radio(Streamer):
REST_PATH = "radio"
FORMAT = "CS16"
SAMPLES = 8192
def __init__(self, name, device_info):
super().__init__()
self.name = name
self.device_info = device_info
self.device = soapy.Device(device_info)
if self.device is None:
raise RuntimeError("Failed to connect to radio device")
self.capabilities = self._get_capabilities()
def configure(self, frequency):
if self.is_streaming():
raise RuntimeError("Cannot configure radio while a stream is active")
frequency = int(prefixed.Float(frequency))
bandwidth = 200000
sample_rates = preferred_sample_rates(self.capabilities["rx"]["sample-rates"])
if len(sample_rates) == 0:
raise RuntimeError("No suitable sample rates are available")
self.sample_rate, self.output_rate = sample_rates[0]
self.device.setFrequency(soapy.SOAPY_SDR_RX, 0, frequency)
self.device.setSampleRate(soapy.SOAPY_SDR_RX, 0, self.sample_rate)
self.device.setBandwidth(soapy.SOAPY_SDR_RX, 0, bandwidth)
# Set automatic gain
self.device.setGainMode(soapy.SOAPY_SDR_RX, 0, True)
return {
"frequency": self.device.getFrequency(soapy.SOAPY_SDR_RX, 0),
"sample-rate": self.device.getSampleRate(soapy.SOAPY_SDR_RX, 0),
"bandwidth": self.device.getBandwidth(soapy.SOAPY_SDR_RX, 0),
"gain-mode": "auto"
if self.device.getGainMode(soapy.SOAPY_SDR_RX, 0)
else "manual",
}
def get_info(self):
return {
"name": self.name,
"device": self.device_info,
"capabilities": self.capabilities,
"stream-path": self._stream_path(),
"streaming": self.is_streaming(),
}
def _get_capabilities(self):
def get_direction_capabilities(direction):
return {
"antennas": self.device.listAntennas(direction, 0),
"gains": self.device.listGains(direction, 0),
"frequencies": self.device.listFrequencies(direction, 0),
"sample-rates": self.device.listSampleRates(direction, 0),
"bandwidths": self.device.listBandwidths(direction, 0),
"sensors": self.device.listSensors(direction, 0),
"formats": self.device.getStreamFormats(direction, 0),
}
return {
"rx": get_direction_capabilities(soapy.SOAPY_SDR_RX),
"tx": get_direction_capabilities(soapy.SOAPY_SDR_TX),
"clock-sources": self.device.listClockSources(),
"time-sources": self.device.listTimeSources(),
"register-interfaces": self.device.listRegisterInterfaces(),
"gpios": self.device.listGPIOBanks(),
"uarts": self.device.listUARTs(),
}
def _stream_thread(self):
self._init_stream()
while self.run:
# Check that the child processes are still running
if (not is_alive(self.demod)) or (not is_alive(self.playback)):
print("DSP chain failed, aborting stream.", file=sys.stderr)
break
result = self.device.readStream(self.stream, [self.buffer], Radio.SAMPLES)
if result == 0:
continue
elif result.ret < 0:
error = SoapyError(result.ret)
if error is not SoapyError.Timeout:
print(
"Stream read failed, aborting stream:", error, file=sys.stderr
)
break
continue
else:
read_size = int(result.ret * 2)
self.demod.stdin.write(
struct.pack(
FORMATS[Radio.FORMAT].packing % read_size,
*self.buffer[:read_size],
)
)
self._cleanup_stream()
def _init_stream(self):
self.playback = subprocess.Popen(
[
"/usr/bin/ffmpeg",
"-f",
"s16le",
"-ar",
str(self.output_rate),
"-ac",
"2",
"-i",
"-",
"-f",
"rtsp",
self.stream_address(),
],
stdin=subprocess.PIPE,
stdout=subprocess.DEVNULL,
stderr=subprocess.DEVNULL,
)
self.demod = subprocess.Popen(
[
"/usr/bin/python3",
"fm_demod.py",
"-f",
"CS16",
"-s",
str(self.sample_rate),
"-d",
str(self.output_rate),
],
stdin=subprocess.PIPE,
stdout=self.playback.stdin,
stderr=subprocess.DEVNULL,
)
self.buffer = np.array([0] * Radio.SAMPLES * 2, FORMATS[Radio.FORMAT].numpy)
self.stream = self.device.setupStream(
soapy.SOAPY_SDR_RX, FORMATS[Radio.FORMAT].soapy
)
result = self.device.activateStream(self.stream)
if result != 0:
raise RuntimeError(f"Error activating stream: {result}")
def _cleanup_stream(self):
self.run = False
# Cleanup the streaming objects in the reverse order to the _init method.
self.device.deactivateStream(self.stream)
self.device.closeStream(self.stream)
self.buffer = None
# .terminate()/.kill() followed by .wait() is required to properly clear
# killed zombie processes from the process table.
# https://stackoverflow.com/a/41961462
self.demod.kill()
self.demod.wait()
self.demod = None
self.playback.kill()
self.playback.wait()
self.playback = None
"""
Quick and dirty test of the Radio class.
"""
if __name__ == "__main__":
import time
sdr = Radio("demo", {"driver": "sdrplay"})
print("Configuring...")
sdr.configure("105.5M")
print("Configured.")
print("Starting stream...")
sdr.start_stream()
print("Stream started.")
# Let the stream play for a while
time.sleep(15)
print("Ending stream...")
sdr.end_stream()
print("Stream ended.")