diff --git a/csdr/chain/dablin.py b/csdr/chain/dablin.py index c5a32116..2f6a2794 100644 --- a/csdr/chain/dablin.py +++ b/csdr/chain/dablin.py @@ -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) diff --git a/csdr/chain/demodulator.py b/csdr/chain/demodulator.py index 7b4506cc..40a6c015 100644 --- a/csdr/chain/demodulator.py +++ b/csdr/chain/demodulator.py @@ -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 diff --git a/csdr/chain/hdradio.py b/csdr/chain/hdradio.py index ceaa519b..7584bc89 100644 --- a/csdr/chain/hdradio.py +++ b/csdr/chain/hdradio.py @@ -1,34 +1,50 @@ -from csdr.chain.demodulator import FixedIfSampleRateChain, BaseDemodulatorChain, FixedAudioRateChain, DialFrequencyReceiver +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 +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, DialFrequencyReceiver): +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): + def getFixedIfSampleRate(self) -> int: return self.hdradio.getFixedAudioRate() - def getFixedAudioRate(self): + 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 setProgram(self, program: int) -> None: - self.hdradio.setProgram(program) + def setAudioServiceId(self, serviceId: int) -> None: + self.hdradio.setProgram(serviceId) def setDialFrequency(self, frequency: int) -> None: - # Clear station metadata when changing frequency - pass + 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) diff --git a/csdr/module/hdradio.py b/csdr/module/hdradio.py index a7dde19a..1aa08ef0 100644 --- a/csdr/module/hdradio.py +++ b/csdr/module/hdradio.py @@ -2,23 +2,56 @@ 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.program = program - self.radio = NRSC5(lambda evt_type, evt: self.callback(evt_type, evt)) + 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 @@ -30,53 +63,101 @@ class HdRadioModule(ThreadModule): # Change program def setProgram(self, program: int) -> None: - self.program = program + 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, data) -> None: - if data and self.metaWriter: - self.metaWriter.write(data) + 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...") - 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: + if data is None or len(data) == 0: self.doRun = False - break - try: - self.radio.pipe_samples_cs16(data.tobytes()) - except Exception as exptn: - logger.debug("Exception: %s" % str(exptn)) + 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.AUDIO: + if evt_type == EventType.LOST_DEVICE: + logger.info("Lost device") + self.doRun = False + elif 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: @@ -89,7 +170,7 @@ class HdRadioModule(ThreadModule): logger.info("BER: %.6f", evt.cber) elif evt_type == EventType.ID3: if evt.program == self.program: - # Collect metadata + # Collect new metadata meta = {} if evt.title: meta["title"] = evt.title @@ -98,13 +179,13 @@ class HdRadioModule(ThreadModule): if evt.album: meta["album"] = evt.album if evt.genre: - meta["genre"] = evt.album + 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) - # Output collected metadata - self._writeMeta(meta) + # 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", @@ -130,8 +211,11 @@ class HdRadioModule(ThreadModule): 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 = {} + # Collect new metadata + meta = { + "audio_services" : [], + "data_services" : [] + } if evt.country_code: meta["country"] = evt.country_code meta["fcc_id"] = evt.fcc_facility_id @@ -146,17 +230,30 @@ class HdRadioModule(ThreadModule): if evt.latitude: meta["lat"] = evt.latitude meta["lon"] = evt.longitude - meta["alt"] = evt.altitude + 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) + #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) - # Output collected metadata - self._writeMeta(meta) + #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) diff --git a/htdocs/css/openwebrx.css b/htdocs/css/openwebrx.css index fae76378..dde1487a 100644 --- a/htdocs/css/openwebrx.css +++ b/htdocs/css/openwebrx.css @@ -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; } diff --git a/htdocs/index.html b/htdocs/index.html index 1e1c4c43..be713261 100644 --- a/htdocs/index.html +++ b/htdocs/index.html @@ -153,6 +153,7 @@
+