Updating with the latest OWRX+ HDRadio changes.

This commit is contained in:
Marat Fayzullin 2024-09-02 12:33:41 -04:00
parent 5b7397da46
commit 5fa7e9d695
10 changed files with 301 additions and 80 deletions

View file

@ -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)

View file

@ -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

View file

@ -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)

View file

@ -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)

View file

@ -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;
}

View file

@ -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">

View file

@ -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();
}

View file

@ -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() {

View file

@ -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,18 +573,9 @@ class DspManager(SdrSourceEventClient, ClientDemodulatorSecondaryDspEventClient)
elif demod == "nxdn":
from csdr.chain.digiham import Nxdn
return Nxdn(self.props["digital_voice_codecserver"])
elif demod == "hdr1":
elif demod == "hdr":
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)
return HdRadio()
elif demod == "m17":
from csdr.chain.m17 import M17
return M17()

View file

@ -134,11 +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("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),
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"]),