mirror of
https://github.com/jketterl/openwebrx.git
synced 2026-01-14 20:50:41 +01:00
Merge 5fa7e9d695 into 640c5b0b3e
This commit is contained in:
commit
0ebce00a27
|
|
@ -1,5 +1,5 @@
|
|||
from csdr.chain.demodulator import BaseDemodulatorChain, FixedIfSampleRateChain, FixedAudioRateChain, HdAudio, \
|
||||
MetaProvider, DabServiceSelector, DialFrequencyReceiver
|
||||
MetaProvider, AudioServiceSelector, DialFrequencyReceiver
|
||||
from csdr.module import PickleModule
|
||||
from csdreti.modules import EtiDecoder
|
||||
from owrx.dab.dablin import DablinModule
|
||||
|
|
@ -58,7 +58,7 @@ class MetaProcessor(PickleModule):
|
|||
self.shifter.setRate(0)
|
||||
|
||||
|
||||
class Dablin(BaseDemodulatorChain, FixedIfSampleRateChain, FixedAudioRateChain, HdAudio, MetaProvider, DabServiceSelector, DialFrequencyReceiver):
|
||||
class Dablin(BaseDemodulatorChain, FixedIfSampleRateChain, FixedAudioRateChain, HdAudio, MetaProvider, AudioServiceSelector, DialFrequencyReceiver):
|
||||
def __init__(self):
|
||||
shift = Shift(0)
|
||||
self.decoder = EtiDecoder()
|
||||
|
|
@ -99,7 +99,7 @@ class Dablin(BaseDemodulatorChain, FixedIfSampleRateChain, FixedAudioRateChain,
|
|||
def setMetaWriter(self, writer: Writer) -> None:
|
||||
self.processor.setWriter(writer)
|
||||
|
||||
def setDabServiceId(self, serviceId: int) -> None:
|
||||
def setAudioServiceId(self, serviceId: int) -> None:
|
||||
self.decoder.setServiceIdFilter([serviceId])
|
||||
self.dablin.setDabServiceId(serviceId)
|
||||
|
||||
|
|
|
|||
|
|
@ -55,9 +55,9 @@ class RdsChain(ABC):
|
|||
pass
|
||||
|
||||
|
||||
class DabServiceSelector(ABC):
|
||||
class AudioServiceSelector(ABC):
|
||||
@abstractmethod
|
||||
def setDabServiceId(self, serviceId: int) -> None:
|
||||
def setAudioServiceId(self, serviceId: int) -> None:
|
||||
pass
|
||||
|
||||
|
||||
|
|
|
|||
50
csdr/chain/hdradio.py
Normal file
50
csdr/chain/hdradio.py
Normal file
|
|
@ -0,0 +1,50 @@
|
|||
from csdr.chain.demodulator import FixedIfSampleRateChain, BaseDemodulatorChain, FixedAudioRateChain, DialFrequencyReceiver, HdAudio, MetaProvider, AudioServiceSelector
|
||||
from csdr.module.hdradio import HdRadioModule
|
||||
from pycsdr.modules import Convert, Agc, Downmix, Writer, Buffer, Throttle
|
||||
from pycsdr.types import Format
|
||||
from typing import Optional
|
||||
|
||||
import logging
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
logger.setLevel(logging.DEBUG)
|
||||
|
||||
|
||||
class HdRadio(BaseDemodulatorChain, FixedIfSampleRateChain, FixedAudioRateChain, HdAudio, MetaProvider, DialFrequencyReceiver, AudioServiceSelector):
|
||||
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,
|
||||
Throttle(Format.SHORT, 44100 * 2),
|
||||
Downmix(Format.SHORT),
|
||||
]
|
||||
super().__init__(workers)
|
||||
|
||||
def getFixedIfSampleRate(self) -> int:
|
||||
return self.hdradio.getFixedAudioRate()
|
||||
|
||||
def getFixedAudioRate(self) -> int:
|
||||
return 44100
|
||||
|
||||
def supportsSquelch(self) -> bool:
|
||||
return False
|
||||
|
||||
# Set metadata consumer
|
||||
def setMetaWriter(self, writer: Writer) -> None:
|
||||
self.hdradio.setMetaWriter(writer)
|
||||
|
||||
# Change program
|
||||
def setAudioServiceId(self, serviceId: int) -> None:
|
||||
self.hdradio.setProgram(serviceId)
|
||||
|
||||
def setDialFrequency(self, frequency: int) -> None:
|
||||
self.hdradio.setFrequency(frequency)
|
||||
|
||||
def _connect(self, w1, w2, buffer: Optional[Buffer] = None) -> None:
|
||||
if isinstance(w2, Throttle):
|
||||
# Audio data comes in in bursts, so we use a throttle
|
||||
# and 10x the default buffer size here
|
||||
buffer = Buffer(Format.SHORT, 2621440)
|
||||
return super()._connect(w1, w2, buffer)
|
||||
259
csdr/module/hdradio.py
Normal file
259
csdr/module/hdradio.py
Normal file
|
|
@ -0,0 +1,259 @@
|
|||
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
|
||||
from owrx.map import Map, LatLngLocation
|
||||
|
||||
import logging
|
||||
import threading
|
||||
import pickle
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
logger.setLevel(logging.DEBUG)
|
||||
|
||||
|
||||
class StationLocation(LatLngLocation):
|
||||
def __init__(self, data):
|
||||
super().__init__(data["lat"], data["lon"])
|
||||
# Complete station data
|
||||
self.data = data
|
||||
|
||||
def getSymbolData(self, symbol, table):
|
||||
return {"symbol": symbol, "table": table, "index": ord(symbol) - 33, "tableindex": ord(table) - 33}
|
||||
|
||||
def __dict__(self):
|
||||
# Return APRS-like dictionary object with "antenna tower" symbol
|
||||
res = super(StationLocation, self).__dict__()
|
||||
res["symbol"] = self.getSymbolData('r', '/')
|
||||
res.update(self.data)
|
||||
return res
|
||||
|
||||
|
||||
class HdRadioModule(ThreadModule):
|
||||
def __init__(self, program: int = 0, amMode: bool = False):
|
||||
self.program = program
|
||||
self.frequency = 0
|
||||
self.metaLock = threading.Lock()
|
||||
self.metaWriter = None
|
||||
self.meta = {}
|
||||
self._clearMeta()
|
||||
# Initialize and start NRSC5 decoder
|
||||
self.radio = NRSC5(lambda evt_type, evt: self.callback(evt_type, evt))
|
||||
self.radio.open_pipe()
|
||||
self.radio.start()
|
||||
# Crashes things?
|
||||
# self.radio.set_mode(Mode.AM if amMode else Mode.FM)
|
||||
super().__init__()
|
||||
|
||||
def __del__(self):
|
||||
# Make sure NRSC5 object is truly destroyed
|
||||
if self.radio is not None:
|
||||
self.radio.stop()
|
||||
self.radio.close()
|
||||
self.radio = None
|
||||
|
||||
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:
|
||||
if program != self.program:
|
||||
self.program = program
|
||||
logger.info("Now playing program #{0}".format(self.program))
|
||||
# Clear program metadata
|
||||
with self.metaLock:
|
||||
self.meta["program"] = self.program
|
||||
if "title" in self.meta:
|
||||
del self.meta["title"]
|
||||
if "artist" in self.meta:
|
||||
del self.meta["artist"]
|
||||
if "album" in self.meta:
|
||||
del self.meta["album"]
|
||||
if "genre" in self.meta:
|
||||
del self.meta["genre"]
|
||||
self._writeMeta()
|
||||
|
||||
# Change frequency
|
||||
def setFrequency(self, frequency: int) -> None:
|
||||
if frequency != self.frequency:
|
||||
self.frequency = frequency
|
||||
self.program = 0
|
||||
logger.info("Now playing program #{0} at {1}MHz".format(self.program, self.frequency / 1000000))
|
||||
self._clearMeta()
|
||||
|
||||
# Set metadata consumer
|
||||
def setMetaWriter(self, writer: Writer) -> None:
|
||||
self.metaWriter = writer
|
||||
|
||||
# Write metadata
|
||||
def _writeMeta(self) -> None:
|
||||
if self.meta and self.metaWriter:
|
||||
logger.debug("Metadata: {0}".format(self.meta))
|
||||
self.metaWriter.write(pickle.dumps(self.meta))
|
||||
|
||||
# Clear all metadata
|
||||
def _clearMeta(self) -> None:
|
||||
with self.metaLock:
|
||||
self.meta = {
|
||||
"mode" : "HDR",
|
||||
"frequency" : self.frequency,
|
||||
"program" : self.program
|
||||
}
|
||||
self._writeMeta()
|
||||
|
||||
# Update existing metadata
|
||||
def _updateMeta(self, data) -> None:
|
||||
# Update station location on the map
|
||||
if "station" in data and "lat" in data and "lon" in data:
|
||||
loc = StationLocation(data)
|
||||
Map.getSharedInstance().updateLocation(data["station"], loc, "HDR")
|
||||
# Update any new or different values
|
||||
with self.metaLock:
|
||||
changes = 0
|
||||
for key in data.keys():
|
||||
if key not in self.meta or self.meta[key] != data[key]:
|
||||
self.meta[key] = data[key]
|
||||
changes = changes + 1
|
||||
# If anything changed, write metadata to the buffer
|
||||
if changes > 0:
|
||||
self._writeMeta()
|
||||
|
||||
def run(self):
|
||||
# Start NRSC5 decoder
|
||||
logger.debug("Starting NRSC5 decoder...")
|
||||
|
||||
# Main loop
|
||||
logger.debug("Running the loop...")
|
||||
while self.doRun:
|
||||
data = self.reader.read()
|
||||
if data is None or len(data) == 0:
|
||||
self.doRun = False
|
||||
else:
|
||||
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()
|
||||
self.radio = None
|
||||
logger.debug("DONE.")
|
||||
|
||||
def callback(self, evt_type, evt):
|
||||
if evt_type == EventType.LOST_DEVICE:
|
||||
logger.info("Lost device")
|
||||
self.doRun = False
|
||||
elif evt_type == EventType.AUDIO:
|
||||
if evt.program == self.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.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 new 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.genre
|
||||
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)
|
||||
# Update existing metadata
|
||||
self._updateMeta(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 new metadata
|
||||
meta = {
|
||||
"audio_services" : [],
|
||||
"data_services" : []
|
||||
}
|
||||
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["altitude"] = round(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)
|
||||
meta["audio_services"] += [{
|
||||
"id" : audio_service.program,
|
||||
"type" : audio_service.type.value,
|
||||
"name" : self.radio.program_type_name(audio_service.type),
|
||||
"public" : audio_service.access == Access.PUBLIC,
|
||||
"experience" : 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)
|
||||
meta["data_services"] += [{
|
||||
"mime" : data_service.mime_type,
|
||||
"type" : data_service.type.value,
|
||||
"name" : self.radio.service_data_type_name(data_service.type),
|
||||
"public" : data_service.access == Access.PUBLIC
|
||||
}]
|
||||
# Update existing metadata
|
||||
self._updateMeta(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.")
|
||||
|
|
@ -1381,6 +1381,52 @@ img.openwebrx-mirror-img
|
|||
content: "🔗 ";
|
||||
}
|
||||
|
||||
#openwebrx-panel-metadata-hdr {
|
||||
width: 350px;
|
||||
max-height: 300px;
|
||||
padding: 10px 10px 10px 10px;
|
||||
}
|
||||
|
||||
.hdr-container {
|
||||
width: 100%;
|
||||
text-align: center;
|
||||
overflow: hidden auto;
|
||||
}
|
||||
|
||||
.hdr-container .hdr-station {
|
||||
font-weight: bold;
|
||||
font-size: 18pt;
|
||||
}
|
||||
|
||||
.hdr-container .hdr-top-line,
|
||||
.hdr-container .hdr-bottom-line,
|
||||
.hdr-container .hdr-station,
|
||||
.hdr-container .hdr-message {
|
||||
min-height: 1lh;
|
||||
}
|
||||
|
||||
.hdr-container .hdr-top-line {
|
||||
padding: 0 0 5px 0;
|
||||
}
|
||||
|
||||
.hdr-container .hdr-bottom-line {
|
||||
padding: 5px 0 0 0;
|
||||
}
|
||||
|
||||
.hdr-container .hdr-station,
|
||||
.hdr-container .hdr-message {
|
||||
padding: 5px 0 5px 0;
|
||||
}
|
||||
|
||||
.hdr-container .hdr-selector,
|
||||
.hdr-container .hdr-genre {
|
||||
float: left;
|
||||
}
|
||||
|
||||
.hdr-container .hdr-identifier {
|
||||
float: right;
|
||||
}
|
||||
|
||||
#openwebrx-panel-metadata-dab {
|
||||
width: 300px;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -153,6 +153,7 @@
|
|||
</div>
|
||||
</div>
|
||||
<div class="openwebrx-panel openwebrx-meta-panel disabled" id="openwebrx-panel-metadata-wfm" style="display: none;" data-panel-name="metadata-wfm"></div>
|
||||
<div class="openwebrx-panel openwebrx-meta-panel" id="openwebrx-panel-metadata-hdr" style="display: none;" data-panel-name="metadata-hdr"></div>
|
||||
<div class="openwebrx-panel openwebrx-meta-panel" id="openwebrx-panel-metadata-dab" style="display: none;" data-panel-name="metadata-dab"></div>
|
||||
<div class="openwebrx-panel" id="openwebrx-panel-log" data-panel-name="debug" style="width: 619px;">
|
||||
<div class="openwebrx-panel-inner nano" id="openwebrx-log-scroll">
|
||||
|
|
|
|||
|
|
@ -239,7 +239,7 @@ function Demodulator(offset_frequency, modulation) {
|
|||
this.filter = new Filter(this);
|
||||
this.squelch_level = -150;
|
||||
this.dmr_filter = 3;
|
||||
this.dab_service_id = 0;
|
||||
this.audio_service_id = 0;
|
||||
this.started = false;
|
||||
this.state = {};
|
||||
this.secondary_demod = false;
|
||||
|
|
@ -328,7 +328,7 @@ Demodulator.prototype.set = function () { //this function sends demodulator par
|
|||
"offset_freq": this.offset_frequency,
|
||||
"mod": this.modulation,
|
||||
"dmr_filter": this.dmr_filter,
|
||||
"dab_service_id": this.dab_service_id,
|
||||
"audio_service_id": this.audio_service_id,
|
||||
"squelch_level": this.squelch_level,
|
||||
"secondary_mod": this.secondary_demod,
|
||||
"secondary_offset_freq": this.secondary_offset_freq
|
||||
|
|
@ -366,8 +366,8 @@ Demodulator.prototype.setDmrFilter = function(dmr_filter) {
|
|||
this.set();
|
||||
};
|
||||
|
||||
Demodulator.prototype.setDabServiceId = function(dab_service_id) {
|
||||
this.dab_service_id = dab_service_id;
|
||||
Demodulator.prototype.setAudioServiceId = function(audio_service_id) {
|
||||
this.audio_service_id = audio_service_id;
|
||||
this.set();
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -471,7 +471,6 @@ WfmMetaPanel.prototype.update = function(data) {
|
|||
if ('info.weather' in tags) {
|
||||
this.radiotext_plus.weather = tags['info.weather'];
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
if ('radiotext' in data && !this.radiotext_plus) {
|
||||
|
|
@ -550,6 +549,80 @@ WfmMetaPanel.prototype.clear = function() {
|
|||
this.radiotext_plus = false;
|
||||
};
|
||||
|
||||
function HdrMetaPanel(el) {
|
||||
MetaPanel.call(this, el);
|
||||
this.modes = ['HDR'];
|
||||
|
||||
// Create info panel
|
||||
var $container = $(
|
||||
'<div class="hdr-container">' +
|
||||
'<div class="hdr-top-line">' +
|
||||
'<select id="hdr-program-id" class="hdr-selector"></select>' +
|
||||
'<span class="hdr-identifier"></span>' +
|
||||
'</div>' +
|
||||
'<div class="hdr-station"></div>' +
|
||||
'<div class="hdr-message"></div>' +
|
||||
'<div class="hdr-title"></div>' +
|
||||
'<div class="hdr-artist"></div>' +
|
||||
'<div class="hdr-album"></div>' +
|
||||
'<div class="hdr-bottom-line">' +
|
||||
'<span class="hdr-genre"></span>' +
|
||||
'</div>' +
|
||||
'</div>'
|
||||
);
|
||||
|
||||
$(this.el).append($container);
|
||||
|
||||
var $select = $('#hdr-program-id');
|
||||
$select.hide();
|
||||
$select.on("change", function() {
|
||||
var id = parseInt($(this).val());
|
||||
$('#openwebrx-panel-receiver').demodulatorPanel().getDemodulator().setAudioServiceId(id);
|
||||
});
|
||||
}
|
||||
|
||||
HdrMetaPanel.prototype = new MetaPanel();
|
||||
|
||||
HdrMetaPanel.prototype.update = function(data) {
|
||||
if (!this.isSupported(data)) return;
|
||||
|
||||
// Convert FCC ID to hexadecimal
|
||||
var fcc_id = '';
|
||||
if ('fcc_id' in data) {
|
||||
fcc_id = data.fcc_id.toString(16).toUpperCase();
|
||||
fcc_id = '0x' + ('0000' + fcc_id).slice(-4);
|
||||
fcc_id = ('country' in data? data.country + ':' : '') + fcc_id;
|
||||
}
|
||||
|
||||
// Update panel
|
||||
var $el = $(this.el);
|
||||
$el.find('.hdr-identifier').text(fcc_id);
|
||||
$el.find('.hdr-station').text(data.station || '');
|
||||
$el.find('.hdr-message').text(data.alert || data.message || data.slogan || '');
|
||||
$el.find('.hdr-title').text(data.title || '');
|
||||
$el.find('.hdr-artist').text(data.artist || '');
|
||||
$el.find('.hdr-genre').text(data.genre || '');
|
||||
$el.find('.hdr-album').text(data.album || '');
|
||||
|
||||
// Update program selector
|
||||
var $select = $('#hdr-program-id');
|
||||
if (data.audio_services && data.audio_services.length) {
|
||||
$select.html(data.audio_services.map(function(pgm) {
|
||||
var selected = data.program == pgm.id? ' selected' : '';
|
||||
return '<option value="' + pgm.id + '"' + selected + '>P' +
|
||||
(pgm.id + 1) + ' - ' + pgm.name + '</option>';
|
||||
}).join());
|
||||
$select.show();
|
||||
} else {
|
||||
$select.html('');
|
||||
$select.hide();
|
||||
}
|
||||
};
|
||||
|
||||
HdrMetaPanel.prototype.isSupported = function(data) {
|
||||
return this.modes.includes(data.mode);
|
||||
};
|
||||
|
||||
function DabMetaPanel(el) {
|
||||
MetaPanel.call(this, el);
|
||||
var me = this;
|
||||
|
|
@ -558,7 +631,7 @@ function DabMetaPanel(el) {
|
|||
this.$select = $('<select id="dab-service-id"></select>');
|
||||
this.$select.on("change", function() {
|
||||
me.service_id = parseInt($(this).val());
|
||||
$('#openwebrx-panel-receiver').demodulatorPanel().getDemodulator().setDabServiceId(me.service_id);
|
||||
$('#openwebrx-panel-receiver').demodulatorPanel().getDemodulator().setAudioServiceId(me.service_id);
|
||||
});
|
||||
var $container = $(
|
||||
'<div class="dab-container">' +
|
||||
|
|
@ -580,7 +653,6 @@ DabMetaPanel.prototype.isSupported = function(data) {
|
|||
return this.modes.includes(data.mode);
|
||||
}
|
||||
|
||||
|
||||
DabMetaPanel.prototype.update = function(data) {
|
||||
if (!this.isSupported(data)) return;
|
||||
|
||||
|
|
@ -633,6 +705,7 @@ MetaPanel.types = {
|
|||
m17: M17MetaPanel,
|
||||
wfm: WfmMetaPanel,
|
||||
dab: DabMetaPanel,
|
||||
hdr: HdrMetaPanel,
|
||||
};
|
||||
|
||||
$.fn.metaPanel = function() {
|
||||
|
|
|
|||
15
owrx/dsp.py
15
owrx/dsp.py
|
|
@ -5,7 +5,7 @@ from owrx.modes import Modes, DigitalMode
|
|||
from csdr.chain import Chain
|
||||
from csdr.chain.demodulator import BaseDemodulatorChain, FixedIfSampleRateChain, FixedAudioRateChain, HdAudio, \
|
||||
SecondaryDemodulator, DialFrequencyReceiver, MetaProvider, SlotFilterChain, SecondarySelectorChain, \
|
||||
DeemphasisTauChain, DemodulatorError, RdsChain, DabServiceSelector
|
||||
DeemphasisTauChain, DemodulatorError, RdsChain, AudioServiceSelector
|
||||
from csdr.chain.selector import Selector, SecondarySelector
|
||||
from csdr.chain.clientaudio import ClientAudioChain
|
||||
from csdr.chain.fft import FftChain
|
||||
|
|
@ -330,10 +330,10 @@ class ClientDemodulatorChain(Chain):
|
|||
return
|
||||
self.demodulator.setSlotFilter(filter)
|
||||
|
||||
def setDabServiceId(self, serviceId: int) -> None:
|
||||
if not isinstance(self.demodulator, DabServiceSelector):
|
||||
def setAudioServiceId(self, serviceId: int) -> None:
|
||||
if not isinstance(self.demodulator, AudioServiceSelector):
|
||||
return
|
||||
self.demodulator.setDabServiceId(serviceId)
|
||||
self.demodulator.setAudioServiceId(serviceId)
|
||||
|
||||
def setSecondaryFftSize(self, size: int) -> None:
|
||||
if size == self.secondaryFftSize:
|
||||
|
|
@ -429,7 +429,7 @@ class DspManager(SdrSourceEventClient, ClientDemodulatorSecondaryDspEventClient)
|
|||
"mod": ModulationValidator(),
|
||||
"secondary_offset_freq": "int",
|
||||
"dmr_filter": "int",
|
||||
"dab_service_id": "int",
|
||||
"audio_service_id": "int",
|
||||
}
|
||||
self.localProps = PropertyValidator(PropertyLayer().filter(*validators.keys()), validators)
|
||||
|
||||
|
|
@ -510,7 +510,7 @@ class DspManager(SdrSourceEventClient, ClientDemodulatorSecondaryDspEventClient)
|
|||
self.props.wireProperty("high_cut", self.setHighCut),
|
||||
self.props.wireProperty("mod", self.setDemodulator),
|
||||
self.props.wireProperty("dmr_filter", self.chain.setSlotFilter),
|
||||
self.props.wireProperty("dab_service_id", self.chain.setDabServiceId),
|
||||
self.props.wireProperty("audio_service_id", self.chain.setAudioServiceId),
|
||||
self.props.wireProperty("wfm_deemphasis_tau", self.chain.setWfmDeemphasisTau),
|
||||
self.props.wireProperty("wfm_rds_rbds", self.chain.setRdsRbds),
|
||||
self.props.wireProperty("secondary_mod", self.setSecondaryDemodulator),
|
||||
|
|
@ -573,6 +573,9 @@ class DspManager(SdrSourceEventClient, ClientDemodulatorSecondaryDspEventClient)
|
|||
elif demod == "nxdn":
|
||||
from csdr.chain.digiham import Nxdn
|
||||
return Nxdn(self.props["digital_voice_codecserver"])
|
||||
elif demod == "hdr":
|
||||
from csdr.chain.hdradio import HdRadio
|
||||
return HdRadio()
|
||||
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):
|
||||
|
|
@ -715,3 +716,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")
|
||||
|
|
|
|||
|
|
@ -134,7 +134,8 @@ class Modes(object):
|
|||
"freedv", "FreeDV", bandpass=Bandpass(300, 3000), requirements=["digital_voice_freedv"], squelch=False
|
||||
),
|
||||
AnalogMode("drm", "DRM", bandpass=Bandpass(-5000, 5000), requirements=["drm"], squelch=False),
|
||||
AnalogMode("dab", "DAB", bandpass=None, ifRate=2.048e6, requirements=["dab"], squelch=False),
|
||||
AnalogMode("dab", "DAB", bandpass=None, ifRate=2048000, requirements=["dab"], squelch=False),
|
||||
AnalogMode("hdr", "HDR", 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