mirror of
https://github.com/jketterl/openwebrx.git
synced 2026-01-24 17:40:39 +01:00
Porting initial HDRadio support to 1.3-devel.
This commit is contained in:
parent
10c642e102
commit
c7f0d52a21
34
csdr/chain/hdradio.py
Normal file
34
csdr/chain/hdradio.py
Normal file
|
|
@ -0,0 +1,34 @@
|
|||
from csdr.chain.demodulator import FixedIfSampleRateChain, BaseDemodulatorChain, FixedAudioRateChain, DialFrequencyReceiver
|
||||
from csdr.module.hdradio import HdRadioModule
|
||||
from pycsdr.modules import Convert, Agc, Downmix, Writer
|
||||
from pycsdr.types import Format
|
||||
|
||||
|
||||
class HdRadio(BaseDemodulatorChain, FixedIfSampleRateChain, FixedAudioRateChain, DialFrequencyReceiver):
|
||||
def __init__(self, program: int = 0):
|
||||
self.hdradio = HdRadioModule(program = program)
|
||||
workers = [
|
||||
Agc(Format.COMPLEX_FLOAT),
|
||||
Convert(Format.COMPLEX_FLOAT, Format.COMPLEX_SHORT),
|
||||
self.hdradio,
|
||||
Downmix(Format.SHORT),
|
||||
]
|
||||
super().__init__(workers)
|
||||
|
||||
def getFixedIfSampleRate(self):
|
||||
return self.hdradio.getFixedAudioRate()
|
||||
|
||||
def getFixedAudioRate(self):
|
||||
return 44100
|
||||
|
||||
# Set metadata consumer
|
||||
def setMetaWriter(self, writer: Writer) -> None:
|
||||
self.hdradio.setMetaWriter(writer)
|
||||
|
||||
# Change program
|
||||
def setProgram(self, program: int) -> None:
|
||||
self.hdradio.setProgram(program)
|
||||
|
||||
def setDialFrequency(self, frequency: int) -> None:
|
||||
# Clear station metadata when changing frequency
|
||||
pass
|
||||
162
csdr/module/hdradio.py
Normal file
162
csdr/module/hdradio.py
Normal file
|
|
@ -0,0 +1,162 @@
|
|||
from csdr.module.nrsc5 import NRSC5, Mode, EventType, ComponentType, Access
|
||||
from csdr.module import ThreadModule
|
||||
from pycsdr.modules import Writer
|
||||
from pycsdr.types import Format
|
||||
|
||||
import logging
|
||||
import threading
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
logger.setLevel(logging.DEBUG)
|
||||
|
||||
|
||||
class HdRadioModule(ThreadModule):
|
||||
def __init__(self, program: int = 0, amMode: bool = False):
|
||||
self.metaWriter = None
|
||||
self.program = program
|
||||
self.radio = NRSC5(lambda evt_type, evt: self.callback(evt_type, evt))
|
||||
# Crashes things?
|
||||
# self.radio.set_mode(Mode.AM if amMode else Mode.FM)
|
||||
super().__init__()
|
||||
|
||||
def getInputFormat(self) -> Format:
|
||||
return Format.COMPLEX_SHORT
|
||||
|
||||
def getOutputFormat(self) -> Format:
|
||||
return Format.SHORT
|
||||
|
||||
def getFixedAudioRate(self) -> int:
|
||||
return 744188 # 744187.5
|
||||
|
||||
# Change program
|
||||
def setProgram(self, program: int) -> None:
|
||||
self.program = program
|
||||
|
||||
# Set metadata consumer
|
||||
def setMetaWriter(self, writer: Writer) -> None:
|
||||
self.metaWriter = writer
|
||||
|
||||
# Write metadata
|
||||
def _writeMeta(self, data) -> None:
|
||||
if data and self.metaWriter:
|
||||
self.metaWriter.write(data)
|
||||
|
||||
def run(self):
|
||||
# Start NRSC5 decoder
|
||||
logger.debug("Starting NRSC5 decoder...")
|
||||
self.radio.open_pipe()
|
||||
self.radio.start()
|
||||
|
||||
# Main loop
|
||||
logger.debug("Running the loop...")
|
||||
while self.doRun:
|
||||
data = self.reader.read()
|
||||
if data is None:
|
||||
self.doRun = False
|
||||
break
|
||||
try:
|
||||
self.radio.pipe_samples_cs16(data.tobytes())
|
||||
except Exception as exptn:
|
||||
logger.debug("Exception: %s" % str(exptn))
|
||||
|
||||
# Stop NRSC5 decoder
|
||||
logger.debug("Stopping NRSC5 decoder...")
|
||||
self.radio.stop()
|
||||
self.radio.close()
|
||||
logger.debug("DONE.")
|
||||
|
||||
def callback(self, evt_type, evt):
|
||||
if evt_type == EventType.AUDIO:
|
||||
if evt.program == self.program:
|
||||
#logger.info("Audio data for program %d", evt.program)
|
||||
self.writer.write(evt.data)
|
||||
elif evt_type == EventType.HDC:
|
||||
if evt.program == self.program:
|
||||
#logger.info("HDC data for program %d", evt.program)
|
||||
pass
|
||||
elif evt_type == EventType.LOST_DEVICE:
|
||||
logger.info("Lost device")
|
||||
self.doRun = False
|
||||
elif evt_type == EventType.IQ:
|
||||
logger.info("IQ data")
|
||||
elif evt_type == EventType.SYNC:
|
||||
logger.info("Synchronized")
|
||||
elif evt_type == EventType.LOST_SYNC:
|
||||
logger.info("Lost synchronization")
|
||||
elif evt_type == EventType.MER:
|
||||
logger.info("MER: %.1f dB (lower), %.1f dB (upper)", evt.lower, evt.upper)
|
||||
elif evt_type == EventType.BER:
|
||||
logger.info("BER: %.6f", evt.cber)
|
||||
elif evt_type == EventType.ID3:
|
||||
if evt.program == self.program:
|
||||
# Collect metadata
|
||||
meta = {}
|
||||
if evt.title:
|
||||
meta["title"] = evt.title
|
||||
if evt.artist:
|
||||
meta["artist"] = evt.artist
|
||||
if evt.album:
|
||||
meta["album"] = evt.album
|
||||
if evt.genre:
|
||||
meta["genre"] = evt.album
|
||||
if evt.ufid:
|
||||
logger.info("Unique file identifier: %s %s", evt.ufid.owner, evt.ufid.id)
|
||||
if evt.xhdr:
|
||||
logger.info("XHDR: param=%s mime=%s lot=%s", evt.xhdr.param, evt.xhdr.mime, evt.xhdr.lot)
|
||||
# Output collected metadata
|
||||
self._writeMeta(meta)
|
||||
elif evt_type == EventType.SIG:
|
||||
for service in evt:
|
||||
logger.info("SIG Service: type=%s number=%s name=%s",
|
||||
service.type, service.number, service.name)
|
||||
for component in service.components:
|
||||
if component.type == ComponentType.AUDIO:
|
||||
logger.info(" Audio component: id=%s port=%04X type=%s mime=%s",
|
||||
component.id, component.audio.port,
|
||||
component.audio.type, component.audio.mime)
|
||||
elif component.type == ComponentType.DATA:
|
||||
logger.info(" Data component: id=%s port=%04X service_data_type=%s type=%s mime=%s",
|
||||
component.id, component.data.port,
|
||||
component.data.service_data_type,
|
||||
component.data.type, component.data.mime)
|
||||
elif evt_type == EventType.STREAM:
|
||||
logger.info("Stream data: port=%04X seq=%04X mime=%s size=%s",
|
||||
evt.port, evt.seq, evt.mime, len(evt.data))
|
||||
elif evt_type == EventType.PACKET:
|
||||
logger.info("Packet data: port=%04X seq=%04X mime=%s size=%s",
|
||||
evt.port, evt.seq, evt.mime, len(evt.data))
|
||||
elif evt_type == EventType.LOT:
|
||||
time_str = evt.expiry_utc.strftime("%Y-%m-%dT%H:%M:%SZ")
|
||||
logger.info("LOT file: port=%04X lot=%s name=%s size=%s mime=%s expiry=%s",
|
||||
evt.port, evt.lot, evt.name, len(evt.data), evt.mime, time_str)
|
||||
elif evt_type == EventType.SIS:
|
||||
# Collect metadata
|
||||
meta = {}
|
||||
if evt.country_code:
|
||||
meta["country"] = evt.country_code
|
||||
meta["fcc_id"] = evt.fcc_facility_id
|
||||
if evt.name:
|
||||
meta["station"] = evt.name
|
||||
if evt.slogan:
|
||||
meta["slogan"] = evt.slogan
|
||||
if evt.message:
|
||||
meta["message"] = evt.message
|
||||
if evt.alert:
|
||||
meta["alert"] = evt.alert
|
||||
if evt.latitude:
|
||||
meta["lat"] = evt.latitude
|
||||
meta["lon"] = evt.longitude
|
||||
meta["alt"] = evt.altitude
|
||||
for audio_service in evt.audio_services:
|
||||
logger.info("Audio program %s: %s, type: %s, sound experience %s",
|
||||
audio_service.program,
|
||||
"public" if audio_service.access == Access.PUBLIC else "restricted",
|
||||
self.radio.program_type_name(audio_service.type),
|
||||
audio_service.sound_exp)
|
||||
for data_service in evt.data_services:
|
||||
logger.info("Data service: %s, type: %s, MIME type %03x",
|
||||
"public" if data_service.access == Access.PUBLIC else "restricted",
|
||||
self.radio.service_data_type_name(data_service.type),
|
||||
data_service.mime_type)
|
||||
# Output collected metadata
|
||||
self._writeMeta(meta)
|
||||
617
csdr/module/nrsc5.py
Normal file
617
csdr/module/nrsc5.py
Normal file
|
|
@ -0,0 +1,617 @@
|
|||
import collections
|
||||
import ctypes
|
||||
import datetime
|
||||
import enum
|
||||
import math
|
||||
import platform
|
||||
import socket
|
||||
|
||||
|
||||
class Mode(enum.Enum):
|
||||
FM = 0
|
||||
AM = 1
|
||||
|
||||
|
||||
class EventType(enum.Enum):
|
||||
LOST_DEVICE = 0
|
||||
IQ = 1
|
||||
SYNC = 2
|
||||
LOST_SYNC = 3
|
||||
MER = 4
|
||||
BER = 5
|
||||
HDC = 6
|
||||
AUDIO = 7
|
||||
ID3 = 8
|
||||
SIG = 9
|
||||
LOT = 10
|
||||
SIS = 11
|
||||
STREAM = 12
|
||||
PACKET = 13
|
||||
|
||||
|
||||
class ServiceType(enum.Enum):
|
||||
AUDIO = 0
|
||||
DATA = 1
|
||||
|
||||
|
||||
class ComponentType(enum.Enum):
|
||||
AUDIO = 0
|
||||
DATA = 1
|
||||
|
||||
|
||||
class MIMEType(enum.Enum):
|
||||
PRIMARY_IMAGE = 0xBE4B7536
|
||||
STATION_LOGO = 0xD9C72536
|
||||
NAVTEQ = 0x2D42AC3E
|
||||
HERE_TPEG = 0x82F03DFC
|
||||
HERE_IMAGE = 0xB7F03DFC
|
||||
HD_TMC = 0xEECB55B6
|
||||
HDC = 0x4DC66C5A
|
||||
TEXT = 0xBB492AAC
|
||||
JPEG = 0x1E653E9C
|
||||
PNG = 0x4F328CA0
|
||||
TTN_TPEG_1 = 0xB39EBEB2
|
||||
TTN_TPEG_2 = 0x4EB03469
|
||||
TTN_TPEG_3 = 0x52103469
|
||||
TTN_STM_TRAFFIC = 0xFF8422D7
|
||||
TTN_STM_WEATHER = 0xEF042E96
|
||||
|
||||
|
||||
class Access(enum.Enum):
|
||||
PUBLIC = 0
|
||||
RESTRICTED = 1
|
||||
|
||||
|
||||
class ServiceDataType(enum.Enum):
|
||||
NON_SPECIFIC = 0
|
||||
NEWS = 1
|
||||
SPORTS = 3
|
||||
WEATHER = 29
|
||||
EMERGENCY = 31
|
||||
TRAFFIC = 65
|
||||
IMAGE_MAPS = 66
|
||||
TEXT = 80
|
||||
ADVERTISING = 256
|
||||
FINANCIAL = 257
|
||||
STOCK_TICKER = 258
|
||||
NAVIGATION = 259
|
||||
ELECTRONIC_PROGRAM_GUIDE = 260
|
||||
AUDIO = 261
|
||||
PRIVATE_DATA_NETWORK = 262
|
||||
SERVICE_MAINTENANCE = 263
|
||||
HD_RADIO_SYSTEM_SERVICES = 264
|
||||
AUDIO_RELATED_DATA = 265
|
||||
RESERVED_FOR_SPECIAL_TESTS = 511
|
||||
|
||||
|
||||
class ProgramType(enum.Enum):
|
||||
UNDEFINED = 0
|
||||
NEWS = 1
|
||||
INFORMATION = 2
|
||||
SPORTS = 3
|
||||
TALK = 4
|
||||
ROCK = 5
|
||||
CLASSIC_ROCK = 6
|
||||
ADULT_HITS = 7
|
||||
SOFT_ROCK = 8
|
||||
TOP_40 = 9
|
||||
COUNTRY = 10
|
||||
OLDIES = 11
|
||||
SOFT = 12
|
||||
NOSTALGIA = 13
|
||||
JAZZ = 14
|
||||
CLASSICAL = 15
|
||||
RHYTHM_AND_BLUES = 16
|
||||
SOFT_RHYTHM_AND_BLUES = 17
|
||||
FOREIGN_LANGUAGE = 18
|
||||
RELIGIOUS_MUSIC = 19
|
||||
RELIGIOUS_TALK = 20
|
||||
PERSONALITY = 21
|
||||
PUBLIC = 22
|
||||
COLLEGE = 23
|
||||
SPANISH_TALK = 24
|
||||
SPANISH_MUSIC = 25
|
||||
HIP_HOP = 26
|
||||
WEATHER = 29
|
||||
EMERGENCY_TEST = 30
|
||||
EMERGENCY = 31
|
||||
TRAFFIC = 65
|
||||
SPECIAL_READING_SERVICES = 76
|
||||
|
||||
|
||||
IQ = collections.namedtuple("IQ", ["data"])
|
||||
MER = collections.namedtuple("MER", ["lower", "upper"])
|
||||
BER = collections.namedtuple("BER", ["cber"])
|
||||
HDC = collections.namedtuple("HDC", ["program", "data"])
|
||||
Audio = collections.namedtuple("Audio", ["program", "data"])
|
||||
UFID = collections.namedtuple("UFID", ["owner", "id"])
|
||||
XHDR = collections.namedtuple("XHDR", ["mime", "param", "lot"])
|
||||
ID3 = collections.namedtuple("ID3", ["program", "title", "artist", "album", "genre", "ufid", "xhdr"])
|
||||
SIGAudioComponent = collections.namedtuple("SIGAudioComponent", ["port", "type", "mime"])
|
||||
SIGDataComponent = collections.namedtuple("SIGDataComponent", ["port", "service_data_type", "type", "mime"])
|
||||
SIGComponent = collections.namedtuple("SIGComponent", ["type", "id", "audio", "data"])
|
||||
SIGService = collections.namedtuple("SIGService", ["type", "number", "name", "components"])
|
||||
SIG = collections.namedtuple("SIG", ["services"])
|
||||
STREAM = collections.namedtuple("STREAM", ["port", "seq", "mime", "data"])
|
||||
PACKET = collections.namedtuple("PACKET", ["port", "seq", "mime", "data"])
|
||||
LOT = collections.namedtuple("LOT", ["port", "lot", "mime", "name", "data", "expiry_utc"])
|
||||
SISAudioService = collections.namedtuple("SISAudioService", ["program", "access", "type", "sound_exp"])
|
||||
SISDataService = collections.namedtuple("SISDataService", ["access", "type", "mime_type"])
|
||||
SIS = collections.namedtuple("SIS", ["country_code", "fcc_facility_id", "name", "slogan", "message", "alert",
|
||||
"latitude", "longitude", "altitude", "audio_services", "data_services"])
|
||||
|
||||
|
||||
class _IQ(ctypes.Structure):
|
||||
_fields_ = [
|
||||
("data", ctypes.POINTER(ctypes.c_char)),
|
||||
("count", ctypes.c_size_t),
|
||||
]
|
||||
|
||||
|
||||
class _MER(ctypes.Structure):
|
||||
_fields_ = [
|
||||
("lower", ctypes.c_float),
|
||||
("upper", ctypes.c_float),
|
||||
]
|
||||
|
||||
|
||||
class _BER(ctypes.Structure):
|
||||
_fields_ = [
|
||||
("cber", ctypes.c_float),
|
||||
]
|
||||
|
||||
|
||||
class _HDC(ctypes.Structure):
|
||||
_fields_ = [
|
||||
("program", ctypes.c_uint),
|
||||
("data", ctypes.POINTER(ctypes.c_char)),
|
||||
("count", ctypes.c_size_t),
|
||||
]
|
||||
|
||||
|
||||
class _Audio(ctypes.Structure):
|
||||
_fields_ = [
|
||||
("program", ctypes.c_uint),
|
||||
("data", ctypes.POINTER(ctypes.c_char)),
|
||||
("count", ctypes.c_size_t),
|
||||
]
|
||||
|
||||
|
||||
class _UFID(ctypes.Structure):
|
||||
_fields_ = [
|
||||
("owner", ctypes.c_char_p),
|
||||
("id", ctypes.c_char_p),
|
||||
]
|
||||
|
||||
|
||||
class _XHDR(ctypes.Structure):
|
||||
_fields_ = [
|
||||
("mime", ctypes.c_uint32),
|
||||
("param", ctypes.c_int),
|
||||
("lot", ctypes.c_int),
|
||||
]
|
||||
|
||||
|
||||
class _ID3(ctypes.Structure):
|
||||
_fields_ = [
|
||||
("program", ctypes.c_uint),
|
||||
("title", ctypes.c_char_p),
|
||||
("artist", ctypes.c_char_p),
|
||||
("album", ctypes.c_char_p),
|
||||
("genre", ctypes.c_char_p),
|
||||
("ufid", _UFID),
|
||||
("xhdr", _XHDR),
|
||||
]
|
||||
|
||||
|
||||
class _SIGData(ctypes.Structure):
|
||||
_fields_ = [
|
||||
("port", ctypes.c_uint16),
|
||||
("service_data_type", ctypes.c_uint16),
|
||||
("type", ctypes.c_uint8),
|
||||
("mime", ctypes.c_uint32),
|
||||
]
|
||||
|
||||
|
||||
class _SIGAudio(ctypes.Structure):
|
||||
_fields_ = [
|
||||
("port", ctypes.c_uint8),
|
||||
("type", ctypes.c_uint8),
|
||||
("mime", ctypes.c_uint32),
|
||||
]
|
||||
|
||||
|
||||
class _SIGUnion(ctypes.Union):
|
||||
_fields_ = [
|
||||
("audio", _SIGAudio),
|
||||
("data", _SIGData),
|
||||
]
|
||||
|
||||
|
||||
class _SIGComponent(ctypes.Structure):
|
||||
pass
|
||||
|
||||
|
||||
_SIGComponent._fields_ = [
|
||||
("next", ctypes.POINTER(_SIGComponent)),
|
||||
("type", ctypes.c_uint8),
|
||||
("id", ctypes.c_uint8),
|
||||
("u", _SIGUnion),
|
||||
]
|
||||
|
||||
|
||||
class _SIGService(ctypes.Structure):
|
||||
pass
|
||||
|
||||
|
||||
_SIGService._fields_ = [
|
||||
("next", ctypes.POINTER(_SIGService)),
|
||||
("type", ctypes.c_uint8),
|
||||
("number", ctypes.c_uint16),
|
||||
("name", ctypes.c_char_p),
|
||||
("components", ctypes.POINTER(_SIGComponent)),
|
||||
]
|
||||
|
||||
|
||||
class _SIG(ctypes.Structure):
|
||||
_fields_ = [
|
||||
("services", ctypes.POINTER(_SIGService)),
|
||||
]
|
||||
|
||||
|
||||
class _STREAM(ctypes.Structure):
|
||||
_fields_ = [
|
||||
("port", ctypes.c_uint16),
|
||||
("seq", ctypes.c_uint16),
|
||||
("size", ctypes.c_uint),
|
||||
("mime", ctypes.c_uint32),
|
||||
("data", ctypes.POINTER(ctypes.c_char)),
|
||||
]
|
||||
|
||||
|
||||
class _PACKET(ctypes.Structure):
|
||||
_fields_ = [
|
||||
("port", ctypes.c_uint16),
|
||||
("seq", ctypes.c_uint16),
|
||||
("size", ctypes.c_uint),
|
||||
("mime", ctypes.c_uint32),
|
||||
("data", ctypes.POINTER(ctypes.c_char)),
|
||||
]
|
||||
|
||||
|
||||
class _TimeStruct(ctypes.Structure):
|
||||
_fields_ = [
|
||||
("tm_sec", ctypes.c_int),
|
||||
("tm_min", ctypes.c_int),
|
||||
("tm_hour", ctypes.c_int),
|
||||
("tm_mday", ctypes.c_int),
|
||||
("tm_mon", ctypes.c_int),
|
||||
("tm_year", ctypes.c_int),
|
||||
("tm_wday", ctypes.c_int),
|
||||
("tm_yday", ctypes.c_int),
|
||||
("tm_isdst", ctypes.c_int),
|
||||
]
|
||||
|
||||
|
||||
class _LOT(ctypes.Structure):
|
||||
_fields_ = [
|
||||
("port", ctypes.c_uint16),
|
||||
("lot", ctypes.c_uint),
|
||||
("size", ctypes.c_uint),
|
||||
("mime", ctypes.c_uint32),
|
||||
("name", ctypes.c_char_p),
|
||||
("data", ctypes.POINTER(ctypes.c_char)),
|
||||
("expiry_utc", ctypes.POINTER(_TimeStruct)),
|
||||
]
|
||||
|
||||
|
||||
class _SISAudioService(ctypes.Structure):
|
||||
pass
|
||||
|
||||
|
||||
_SISAudioService._fields_ = [
|
||||
("next", ctypes.POINTER(_SISAudioService)),
|
||||
("program", ctypes.c_uint),
|
||||
("access", ctypes.c_uint),
|
||||
("type", ctypes.c_uint),
|
||||
("sound_exp", ctypes.c_uint),
|
||||
]
|
||||
|
||||
|
||||
class _SISDataService(ctypes.Structure):
|
||||
pass
|
||||
|
||||
|
||||
_SISDataService._fields_ = [
|
||||
("next", ctypes.POINTER(_SISDataService)),
|
||||
("access", ctypes.c_uint),
|
||||
("type", ctypes.c_uint),
|
||||
("mime_type", ctypes.c_uint32),
|
||||
]
|
||||
|
||||
|
||||
class _SIS(ctypes.Structure):
|
||||
_fields_ = [
|
||||
("country_code", ctypes.c_char_p),
|
||||
("fcc_facility_id", ctypes.c_int),
|
||||
("name", ctypes.c_char_p),
|
||||
("slogan", ctypes.c_char_p),
|
||||
("message", ctypes.c_char_p),
|
||||
("alert", ctypes.c_char_p),
|
||||
("latitude", ctypes.c_float),
|
||||
("longitude", ctypes.c_float),
|
||||
("altitude", ctypes.c_int),
|
||||
("audio_services", ctypes.POINTER(_SISAudioService)),
|
||||
("data_services", ctypes.POINTER(_SISDataService)),
|
||||
]
|
||||
|
||||
|
||||
class _EventUnion(ctypes.Union):
|
||||
_fields_ = [
|
||||
("iq", _IQ),
|
||||
("mer", _MER),
|
||||
("ber", _BER),
|
||||
("hdc", _HDC),
|
||||
("audio", _Audio),
|
||||
("id3", _ID3),
|
||||
("sig", _SIG),
|
||||
("stream", _STREAM),
|
||||
("packet", _PACKET),
|
||||
("lot", _LOT),
|
||||
("sis", _SIS),
|
||||
]
|
||||
|
||||
|
||||
class _Event(ctypes.Structure):
|
||||
_fields_ = [
|
||||
("event", ctypes.c_uint),
|
||||
("u", _EventUnion),
|
||||
]
|
||||
|
||||
|
||||
class NRSC5Error(Exception):
|
||||
pass
|
||||
|
||||
|
||||
class NRSC5:
|
||||
libnrsc5 = None
|
||||
|
||||
def _load_library(self):
|
||||
if NRSC5.libnrsc5 is None:
|
||||
if platform.system() == "Windows":
|
||||
lib_name = "libnrsc5.dll"
|
||||
elif platform.system() == "Linux":
|
||||
lib_name = "libnrsc5.so"
|
||||
elif platform.system() == "Darwin":
|
||||
lib_name = "libnrsc5.dylib"
|
||||
else:
|
||||
raise NRSC5Error("Unsupported platform: " + platform.system())
|
||||
NRSC5.libnrsc5 = ctypes.cdll.LoadLibrary(lib_name)
|
||||
self.radio = ctypes.c_void_p()
|
||||
|
||||
@staticmethod
|
||||
def _decode(string):
|
||||
if string is None:
|
||||
return string
|
||||
return string.decode()
|
||||
|
||||
def _callback_wrapper(self, c_evt):
|
||||
c_evt = c_evt.contents
|
||||
evt = None
|
||||
|
||||
try:
|
||||
evt_type = EventType(c_evt.event)
|
||||
except ValueError:
|
||||
return
|
||||
|
||||
if evt_type == EventType.IQ:
|
||||
iq = c_evt.u.iq
|
||||
evt = IQ(iq.data[:iq.count])
|
||||
elif evt_type == EventType.MER:
|
||||
mer = c_evt.u.mer
|
||||
evt = MER(mer.lower, mer.upper)
|
||||
elif evt_type == EventType.BER:
|
||||
ber = c_evt.u.ber
|
||||
evt = BER(ber.cber)
|
||||
elif evt_type == EventType.HDC:
|
||||
hdc = c_evt.u.hdc
|
||||
evt = HDC(hdc.program, hdc.data[:hdc.count])
|
||||
elif evt_type == EventType.AUDIO:
|
||||
audio = c_evt.u.audio
|
||||
evt = Audio(audio.program, audio.data[:audio.count * 2])
|
||||
elif evt_type == EventType.ID3:
|
||||
id3 = c_evt.u.id3
|
||||
|
||||
ufid = None
|
||||
if id3.ufid.owner or id3.ufid.id:
|
||||
ufid = UFID(self._decode(id3.ufid.owner), self._decode(id3.ufid.id))
|
||||
|
||||
xhdr = None
|
||||
if id3.xhdr.mime != 0 or id3.xhdr.param != -1 or id3.xhdr.lot != -1:
|
||||
xhdr = XHDR(None if id3.xhdr.mime == 0 else MIMEType(id3.xhdr.mime),
|
||||
None if id3.xhdr.param == -1 else id3.xhdr.param,
|
||||
None if id3.xhdr.lot == -1 else id3.xhdr.lot)
|
||||
|
||||
evt = ID3(id3.program, self._decode(id3.title), self._decode(id3.artist),
|
||||
self._decode(id3.album), self._decode(id3.genre), ufid, xhdr)
|
||||
elif evt_type == EventType.SIG:
|
||||
evt = []
|
||||
service_ptr = c_evt.u.sig.services
|
||||
while service_ptr:
|
||||
service = service_ptr.contents
|
||||
components = []
|
||||
component_ptr = service.components
|
||||
while component_ptr:
|
||||
component = component_ptr.contents
|
||||
component_type = ComponentType(component.type)
|
||||
if component_type == ComponentType.AUDIO:
|
||||
audio = SIGAudioComponent(component.u.audio.port, ProgramType(component.u.audio.type),
|
||||
MIMEType(component.u.audio.mime))
|
||||
components.append(SIGComponent(component_type, component.id, audio, None))
|
||||
if component_type == ComponentType.DATA:
|
||||
data = SIGDataComponent(component.u.data.port,
|
||||
ServiceDataType(component.u.data.service_data_type),
|
||||
component.u.data.type, MIMEType(component.u.data.mime))
|
||||
components.append(SIGComponent(component_type, component.id, None, data))
|
||||
component_ptr = component.next
|
||||
evt.append(SIGService(ServiceType(service.type), service.number,
|
||||
self._decode(service.name), components))
|
||||
service_ptr = service.next
|
||||
elif evt_type == EventType.STREAM:
|
||||
stream = c_evt.u.stream
|
||||
evt = STREAM(stream.port, stream.seq, MIMEType(stream.mime), stream.data[:stream.size])
|
||||
elif evt_type == EventType.PACKET:
|
||||
packet = c_evt.u.packet
|
||||
evt = PACKET(packet.port, packet.seq, MIMEType(packet.mime), packet.data[:packet.size])
|
||||
elif evt_type == EventType.LOT:
|
||||
lot = c_evt.u.lot
|
||||
expiry_struct = lot.expiry_utc.contents
|
||||
expiry_time = datetime.datetime(
|
||||
expiry_struct.tm_year + 1900,
|
||||
expiry_struct.tm_mon + 1,
|
||||
expiry_struct.tm_mday,
|
||||
expiry_struct.tm_hour,
|
||||
expiry_struct.tm_min,
|
||||
expiry_struct.tm_sec,
|
||||
tzinfo=datetime.timezone.utc
|
||||
)
|
||||
evt = LOT(lot.port, lot.lot, MIMEType(lot.mime), self._decode(lot.name), lot.data[:lot.size], expiry_time)
|
||||
elif evt_type == EventType.SIS:
|
||||
sis = c_evt.u.sis
|
||||
|
||||
latitude, longitude, altitude = None, None, None
|
||||
if not math.isnan(sis.latitude):
|
||||
latitude, longitude, altitude = sis.latitude, sis.longitude, sis.altitude
|
||||
|
||||
audio_services = []
|
||||
audio_service_ptr = sis.audio_services
|
||||
while audio_service_ptr:
|
||||
asd = audio_service_ptr.contents
|
||||
audio_services.append(SISAudioService(asd.program, Access(asd.access),
|
||||
ProgramType(asd.type), asd.sound_exp))
|
||||
audio_service_ptr = asd.next
|
||||
|
||||
data_services = []
|
||||
data_service_ptr = sis.data_services
|
||||
while data_service_ptr:
|
||||
dsd = data_service_ptr.contents
|
||||
data_services.append(SISDataService(Access(dsd.access), ServiceDataType(dsd.type), dsd.mime_type))
|
||||
data_service_ptr = dsd.next
|
||||
|
||||
evt = SIS(self._decode(sis.country_code), sis.fcc_facility_id, self._decode(sis.name),
|
||||
self._decode(sis.slogan), self._decode(sis.message), self._decode(sis.alert),
|
||||
latitude, longitude, altitude, audio_services, data_services)
|
||||
self.callback(evt_type, evt)
|
||||
|
||||
def __init__(self, callback):
|
||||
self._load_library()
|
||||
self.radio = ctypes.c_void_p()
|
||||
self.callback = callback
|
||||
|
||||
@staticmethod
|
||||
def get_version():
|
||||
version = ctypes.c_char_p()
|
||||
NRSC5.libnrsc5.nrsc5_get_version(ctypes.byref(version))
|
||||
return version.value.decode()
|
||||
|
||||
@staticmethod
|
||||
def service_data_type_name(service_data_type):
|
||||
name = ctypes.c_char_p()
|
||||
NRSC5.libnrsc5.nrsc5_service_data_type_name(service_data_type.value, ctypes.byref(name))
|
||||
return name.value.decode()
|
||||
|
||||
@staticmethod
|
||||
def program_type_name(program_type):
|
||||
name = ctypes.c_char_p()
|
||||
NRSC5.libnrsc5.nrsc5_program_type_name(program_type.value, ctypes.byref(name))
|
||||
return name.value.decode()
|
||||
|
||||
def open(self, device_index):
|
||||
result = NRSC5.libnrsc5.nrsc5_open(ctypes.byref(self.radio), device_index)
|
||||
if result != 0:
|
||||
raise NRSC5Error("Failed to open RTL-SDR.")
|
||||
self._set_callback()
|
||||
|
||||
def open_pipe(self):
|
||||
result = NRSC5.libnrsc5.nrsc5_open_pipe(ctypes.byref(self.radio))
|
||||
if result != 0:
|
||||
raise NRSC5Error("Failed to open pipe.")
|
||||
self._set_callback()
|
||||
|
||||
def open_rtltcp(self, host, port):
|
||||
s = socket.create_connection((host, port))
|
||||
result = NRSC5.libnrsc5.nrsc5_open_rtltcp(ctypes.byref(self.radio), s.detach())
|
||||
if result != 0:
|
||||
raise NRSC5Error("Failed to open rtl_tcp.")
|
||||
self._set_callback()
|
||||
|
||||
def close(self):
|
||||
NRSC5.libnrsc5.nrsc5_close(self.radio)
|
||||
|
||||
def start(self):
|
||||
NRSC5.libnrsc5.nrsc5_start(self.radio)
|
||||
|
||||
def stop(self):
|
||||
NRSC5.libnrsc5.nrsc5_stop(self.radio)
|
||||
|
||||
def set_mode(self, mode):
|
||||
NRSC5.libnrsc5.nrsc5_set_mode(self.radio, mode.value)
|
||||
|
||||
def set_bias_tee(self, on):
|
||||
result = NRSC5.libnrsc5.nrsc5_set_bias_tee(self.radio, on)
|
||||
if result != 0:
|
||||
raise NRSC5Error("Failed to set bias-T.")
|
||||
|
||||
def set_direct_sampling(self, on):
|
||||
result = NRSC5.libnrsc5.nrsc5_set_direct_sampling(self.radio, on)
|
||||
if result != 0:
|
||||
raise NRSC5Error("Failed to set direct sampling.")
|
||||
|
||||
def set_freq_correction(self, ppm_error):
|
||||
result = NRSC5.libnrsc5.nrsc5_set_freq_correction(self.radio, ppm_error)
|
||||
if result != 0:
|
||||
raise NRSC5Error("Failed to set frequency correction.")
|
||||
|
||||
def get_frequency(self):
|
||||
frequency = ctypes.c_float()
|
||||
NRSC5.libnrsc5.nrsc5_get_frequency(self.radio, ctypes.byref(frequency))
|
||||
return frequency.value
|
||||
|
||||
def set_frequency(self, freq):
|
||||
result = NRSC5.libnrsc5.nrsc5_set_frequency(self.radio, ctypes.c_float(freq))
|
||||
if result != 0:
|
||||
raise NRSC5Error("Failed to set frequency.")
|
||||
|
||||
def get_gain(self):
|
||||
gain = ctypes.c_float()
|
||||
NRSC5.libnrsc5.nrsc5_get_gain(self.radio, ctypes.byref(gain))
|
||||
return gain.value
|
||||
|
||||
def set_gain(self, gain):
|
||||
result = NRSC5.libnrsc5.nrsc5_set_gain(self.radio, ctypes.c_float(gain))
|
||||
if result != 0:
|
||||
raise NRSC5Error("Failed to set gain.")
|
||||
|
||||
def set_auto_gain(self, enabled):
|
||||
NRSC5.libnrsc5.nrsc5_set_auto_gain(self.radio, int(enabled))
|
||||
|
||||
def _set_callback(self):
|
||||
def callback_closure(evt, opaque):
|
||||
self._callback_wrapper(evt)
|
||||
|
||||
self.callback_func = ctypes.CFUNCTYPE(None, ctypes.POINTER(_Event), ctypes.c_void_p)(callback_closure)
|
||||
NRSC5.libnrsc5.nrsc5_set_callback(self.radio, self.callback_func, None)
|
||||
|
||||
def pipe_samples_cu8(self, samples):
|
||||
if len(samples) % 4 != 0:
|
||||
raise NRSC5Error("len(samples) must be a multiple of 4.")
|
||||
result = NRSC5.libnrsc5.nrsc5_pipe_samples_cu8(self.radio, samples, len(samples))
|
||||
if result != 0:
|
||||
raise NRSC5Error("Failed to pipe samples.")
|
||||
|
||||
def pipe_samples_cs16(self, samples):
|
||||
if len(samples) % 4 != 0:
|
||||
raise NRSC5Error("len(samples) must be a multiple of 4.")
|
||||
result = NRSC5.libnrsc5.nrsc5_pipe_samples_cs16(self.radio, samples, len(samples) // 2)
|
||||
if result != 0:
|
||||
raise NRSC5Error("Failed to pipe samples.")
|
||||
12
owrx/dsp.py
12
owrx/dsp.py
|
|
@ -573,6 +573,18 @@ class DspManager(SdrSourceEventClient, ClientDemodulatorSecondaryDspEventClient)
|
|||
elif demod == "nxdn":
|
||||
from csdr.chain.digiham import Nxdn
|
||||
return Nxdn(self.props["digital_voice_codecserver"])
|
||||
elif demod == "hdr1":
|
||||
from csdr.chain.hdradio import HdRadio
|
||||
return HdRadio(program = 0)
|
||||
elif demod == "hdr2":
|
||||
from csdr.chain.hdradio import HdRadio
|
||||
return HdRadio(program = 1)
|
||||
elif demod == "hdr3":
|
||||
from csdr.chain.hdradio import HdRadio
|
||||
return HdRadio(program = 2)
|
||||
elif demod == "hdr4":
|
||||
from csdr.chain.hdradio import HdRadio
|
||||
return HdRadio(program = 3)
|
||||
elif demod == "m17":
|
||||
from csdr.chain.m17 import M17
|
||||
return M17()
|
||||
|
|
|
|||
|
|
@ -92,6 +92,7 @@ class FeatureDetector(object):
|
|||
"redsea": ["redsea"],
|
||||
"dab": ["csdreti", "dablin"],
|
||||
"mqtt": ["paho_mqtt"],
|
||||
"hdradio": ["nrsc5"],
|
||||
}
|
||||
|
||||
def feature_availability(self):
|
||||
|
|
@ -694,3 +695,11 @@ class FeatureDetector(object):
|
|||
return True
|
||||
except ImportError:
|
||||
return False
|
||||
|
||||
def has_nrsc5(self):
|
||||
"""
|
||||
OpenWebRX uses the [nrsc5](https://github.com/theori-io/nrsc5) tool to decode HDRadio
|
||||
FM broadcasts. Nrsc5 is not yet available as a package and thus you will have
|
||||
to compile it from source.
|
||||
"""
|
||||
return self.command_is_runnable("nrsc5 -v")
|
||||
|
|
|
|||
|
|
@ -135,6 +135,10 @@ class Modes(object):
|
|||
),
|
||||
AnalogMode("drm", "DRM", bandpass=Bandpass(-5000, 5000), requirements=["drm"], squelch=False),
|
||||
AnalogMode("dab", "DAB", bandpass=None, ifRate=2.048e6, requirements=["dab"], squelch=False),
|
||||
AnalogMode("hdr1", "HDR1", bandpass=None, ifRate=744188, requirements=["hdradio"], squelch=False),
|
||||
AnalogMode("hdr2", "HDR2", bandpass=Bandpass(-200000, 200000), requirements=["hdradio"], squelch=False),
|
||||
AnalogMode("hdr3", "HDR3", bandpass=Bandpass(-200000, 200000), requirements=["hdradio"], squelch=False),
|
||||
AnalogMode("hdr4", "HDR4", bandpass=Bandpass(-200000, 200000), requirements=["hdradio"], squelch=False),
|
||||
DigitalMode("bpsk31", "BPSK31", underlying=["usb"]),
|
||||
DigitalMode("bpsk63", "BPSK63", underlying=["usb"]),
|
||||
DigitalMode("rtty170", "RTTY 45/170", underlying=["usb", "lsb"]),
|
||||
|
|
|
|||
Loading…
Reference in a new issue