Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -2,3 +2,8 @@
__pycache__/
/.vscode/
/apps/hd_generated.cu8
CMakeLists.txt.bak*
.claude/
.history/
/examples/*
!/examples/README
7 changes: 4 additions & 3 deletions CMakeLists.txt
Original file line number Diff line number Diff line change
Expand Up @@ -158,15 +158,16 @@ else()
include (ExternalProject)

set (FDK_AAC_PREFIX "${CMAKE_BINARY_DIR}/fdk-aac-prefix")

ExternalProject_Add (
fdk_aac_external
GIT_REPOSITORY "https://github.com/argilo/fdk-aac.git"
GIT_TAG 3b63dab59416a629f3de82463eb3875319a086d5
GIT_REPOSITORY "https://github.com/secludedhusky/fdk-aac.git"
GIT_TAG edits-parametric
PREFIX ${FDK_AAC_PREFIX}

BINARY_DIR ${FDK_AAC_PREFIX}/src/fdk_aac_external
UPDATE_COMMAND ""
CONFIGURE_COMMAND ./autogen.sh && ./configure --prefix=${FDK_AAC_PREFIX} --disable-shared --enable-static CXXFLAGS=-fPIC
CONFIGURE_COMMAND chmod +x autogen.sh && ./autogen.sh && ./configure --prefix=${FDK_AAC_PREFIX} --disable-shared --enable-static CXXFLAGS=-fPIC
BUILD_COMMAND make
)

Expand Down
76 changes: 69 additions & 7 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -30,17 +30,45 @@ This block encodes audio into High-Definition Coding (HDC) frames. The input sam

### PSD encoder

This block encodes Program Service Data PDUs, as described in https://www.nrscstandards.org/standards-and-guidelines/documents/standards/nrsc-5-d/reference-docs/1028s.pdf. PSD conveys information (e.g. track title & artist) about the audio that is currently playing.
This block encodes Program Service Data PDUs, as described in https://www.nrscstandards.org/standards-and-guidelines/documents/standards/nrsc-5-d/reference-docs/1028s.pdf. PSD conveys information (e.g. track title & artist) about the audio that is currently playing. The encoder supports all ID3 frames defined in NRSC-5 Table 5-1.

To control latency, connect the "clock" output of the Layer 1 encoder to the "clock" input of the PSD encoder, and set "Bytes/frame limit" to 128 (if the L2 frame size is 24000 or larger) or 64 (if the L2 frame size is smaller than 24000).

To dynamically update title, artist, and XHDR data, connect a Socket PDU (TCP Server) block to the "set_meta" input, and send any of the following commands via TCP, followed by a carriage return:
To dynamically update metadata, connect a Socket PDU (TCP Server) block to the "set_meta" input, and send any of the following commands via TCP, followed by a carriage return:

* `titleExample Title` — set title to `Example Title`
* `artistExample Artist` — set artist to `Example Artist`
**Basic Metadata (PSD Types 1-4):**
* `titleExample Title` — set title to `Example Title` (TIT2)
* `artistExample Artist` — set artist to `Example Artist` (TPE1)
* `albumExample Album` — set album to `Example Album` (TALB)
* `genreRock` — set genre to `Rock` (TCON)

**Comment (PSD Type 5 - COMM):**
* `commentRecorded live at Madison Square Garden` — set comment text
* `comment_shortInfo` — set comment short description
* `comment_languageeng` — set comment language (3-byte ISO 639-2 code, default: eng)

**Commercial Frame (PSD Type 6 - COMR):**
* `commercial_price$9.99` — set price string
* `commercial_valid20261231` — set expiration date (YYYYMMDD format)
* `commercial_urlhttps://example.com/buy` — set contact URL for purchase
* `commercial_received0` — set received-as byte (how merchandise is received)
* `commercial_sellerExample Store` — set seller name
* `commercial_descBuy now and save 20%!` — set advertisement description

**Reference Identifier (PSD Type 7 - UFID):**
* `ufid_ownerhttp://musicbrainz.org` — set owner identifier URL
* `ufid_idabc123def456` — set unique identifier (up to 64 bytes)

**Album Art (XHDR):**
* `lot1337` — display album art contained in LOT file 1337
* `lot-1` — display station logo

**Notes:**
* All metadata fields are optional except Title and Artist (per NRSC-5 requirements)
* Maximum PSD message size is 1,018 bytes (excluding 6-byte overhead)
* Title, Artist, Album, and Genre frames should be limited to less than 128 characters
* Binary pictures are not supported in PSD; use LOT protocol for album art transmission

### SIS & SIG encoder

This block encodes Station Information Service PDUs, as described in https://www.nrscstandards.org/standards-and-guidelines/documents/standards/nrsc-5-d/reference-docs/1020s.pdf, and assembles them into the PIDS and SIDS logical channels. SIS provides information about the station.
Expand Down Expand Up @@ -89,23 +117,57 @@ The "clock" output of the Layer 1 encoder must be connected to the "clock" input

This block sends files to the receiver (for instance, containing album art or a station logo) by encoding them as Advanced Application Services (AAS) PDUs, according to the Large Object Transfer (LOT) protocol. The "aas" output must be connected to the Layer 2 encoder's "aas" input, and the "ready" output of the Layer 2 encoder must be connected to the "ready" input of the LOT encoder to tell it when it should produce output.

#### File Expiry

Files transmitted via LOT include an expiry date/time. The expiry can be set in the GRC block's "Expiry" parameter (default: "45 minutes") or specified per-file via TCP commands. The expiry parameter supports two formats:

**Relative Time Format** (recommended):
* `15 milliseconds`, `15ms`, `15mil`
* `120 seconds`, `120s`, `120sec`
* `15 minutes`, `15m`, `15min`
* `2 hours`, `2h`, `2hr`
* `7 days`, `7d`
* `2 weeks`, `2w`, `2wk`
* `6 months`, `6mn`, `6month`
* `1 year`, `1y`, `1yr`

**ISO 8601 / TZ Format:**
* `2026-12-31T23:59:59+00:00` (with timezone offset)
* `2026-12-31T23:59:59Z` (UTC with 'Z' suffix)
* `2026-12-31 23:59:59+00:00` (space separator also supported)

#### TCP Commands

To allow new files to be sent at runtime, connect a Socket PDU (TCP Server) block to the "file" input. To read a new file from disk, send the following command, followed by a carriage return:

```
file|<lot_id>|<filename>
file|<lot_id>|<filename>|<expiry>
```

Example:
```
file|1337|album_art.png|15 minutes
```

To stream in a file over the network connection, send the following command, followed by a carriage return:

```
streamfile|<lot_id>|<size>|<filename>
streamfile|<lot_id>|<size>|<filename>|<expiry>
```

Example:
```
streamfile|1337|4096|album_art.png|2 hours
```

Then send the file itself over the same network connection.

The `apps/send_album_art.py` script demonstrates how to stream an album art file and request for it to be displayed by the receiver.

Note: Station logo and album art files must use PNG or JPEG format, and be 200x200 pixels in size.
**Notes:**
* The expiry parameter is optional in TCP commands; if omitted, the default expiry from the GRC block is used
* Station logo and album art files must use PNG or JPEG format, and be 200x200 pixels in size
* Expiry dates are encoded in the LOT header and transmitted to the receiver

### Layer 2 encoder

Expand Down
40 changes: 39 additions & 1 deletion apps/hd_tx_hackrf.py
100755 → 100644
Original file line number Diff line number Diff line change
Expand Up @@ -81,11 +81,45 @@ def __init__(self):
self.osmosdr_sink_0.set_bb_gain(20, 0)
self.osmosdr_sink_0.set_antenna('', 0)
self.osmosdr_sink_0.set_bandwidth(0, 0)
self.nrsc5_sis_encoder_0 = nrsc5.sis_encoder(mode=nrsc5.pids_mode.FM, short_name='ABCD-FM', slogan='This is ABCD-FM', message='Generated by GNU Radio', program_names=['HD1'], program_types=[nrsc5.program_type.NEWS], data_types=[], data_mime_types=[], latitude=40.6892, longitude=(-74.0445), altitude=93.0, country_code='US', fcc_facility_id=0)
# Data channels: port 0x1010 (LOT ID 1010, Text) and port 0x1011 (LOT ID 1011, Text)
# These are advertised in SIG so decoders reassemble LOT on these ports
self.nrsc5_sis_encoder_0 = nrsc5.sis_encoder(
mode=nrsc5.pids_mode.FM,
short_name='ABCD-FM',
slogan='This is ABCD-FM',
message='Generated by GNU Radio',
program_names=['HD1'],
program_types=[nrsc5.program_type.NEWS],
data_types=[],
data_mime_types=[],
latitude=40.6892,
longitude=(-74.0445),
altitude=93.0,
country_code='US',
fcc_facility_id=0,
data_channels=[
nrsc5.data_channel_config(0x1010, 1010, int(nrsc5.service_data_type.TEXT), int(nrsc5.mime_hash.TEXT)),
nrsc5.data_channel_config(0x1011, 1011, int(nrsc5.service_data_type.TEXT), int(nrsc5.mime_hash.TEXT)),
],
exciter_manufacturer_id='CS',
importer_manufacturer_id='CS',
exciter_core_version=[1, 0, 0, 0],
exciter_mfr_version=[1, 0, 0, 0],
importer_core_version=[1, 0, 0, 0],
importer_mfr_version=[1, 0, 0, 0],
importer_configuration_number=0)
self.nrsc5_psd_encoder_0 = nrsc5.psd_encoder(0, 'Title', 'Artist', 128)
self.nrsc5_lot_encoder_0_0 = nrsc5.lot_encoder('album_art.jpg', 1337, 0x1000)
self.nrsc5_lot_encoder_0 = nrsc5.lot_encoder('SLABCD$$010000.png', 42, 0x1001)
<<<<<<< Updated upstream
self.nrsc5_l2_encoder_0 = nrsc5.l2_encoder(1, 0, 146176, 2000, nrsc5.blend.ENABLE, ccc_width=24)
=======
# Data-only LOT encoders on ports 0x1010 and 0x1011
# These match the data_channels configured above in the SIS encoder
self.nrsc5_lot_encoder_dc0 = nrsc5.lot_encoder('STRWRI_Richmond_20260203_034254.txt', 1010, 0x1010)
self.nrsc5_lot_encoder_dc1 = nrsc5.lot_encoder('STRWRI_Richmond_20260203_034254.txt', 1011, 0x1011)
self.nrsc5_l2_encoder_0 = nrsc5.l2_encoder(1, 0, 146176, 2000, nrsc5.blend.ENABLE)
>>>>>>> Stashed changes
self.nrsc5_l1_fm_encoder_mp1_0 = nrsc5.l1_fm_encoder(1)
self.nrsc5_hdc_encoder_0 = nrsc5.hdc_encoder(2, 64000)
self.network_socket_pdu_1 = network.socket_pdu('TCP_SERVER', '', '52002', 10000, False)
Expand Down Expand Up @@ -132,9 +166,13 @@ def __init__(self):
self.msg_connect((self.nrsc5_l1_fm_encoder_mp1_0, 'clock'), (self.nrsc5_sis_encoder_0, 'clock'))
self.msg_connect((self.nrsc5_l2_encoder_0, 'ready'), (self.nrsc5_lot_encoder_0, 'ready'))
self.msg_connect((self.nrsc5_l2_encoder_0, 'ready'), (self.nrsc5_lot_encoder_0_0, 'ready'))
self.msg_connect((self.nrsc5_l2_encoder_0, 'ready'), (self.nrsc5_lot_encoder_dc0, 'ready'))
self.msg_connect((self.nrsc5_l2_encoder_0, 'ready'), (self.nrsc5_lot_encoder_dc1, 'ready'))
self.msg_connect((self.nrsc5_l2_encoder_0, 'ready'), (self.nrsc5_sis_encoder_0, 'ready'))
self.msg_connect((self.nrsc5_lot_encoder_0, 'aas'), (self.nrsc5_l2_encoder_0, 'aas'))
self.msg_connect((self.nrsc5_lot_encoder_0_0, 'aas'), (self.nrsc5_l2_encoder_0, 'aas'))
self.msg_connect((self.nrsc5_lot_encoder_dc0, 'aas'), (self.nrsc5_l2_encoder_0, 'aas'))
self.msg_connect((self.nrsc5_lot_encoder_dc1, 'aas'), (self.nrsc5_l2_encoder_0, 'aas'))
self.msg_connect((self.nrsc5_sis_encoder_0, 'aas'), (self.nrsc5_l2_encoder_0, 'aas'))
self.connect((self.analog_wfm_tx_0, 0), (self.rational_resampler_xxx_0_0, 0))
self.connect((self.blocks_add_xx_0, 0), (self.blocks_multiply_const_vxx_1, 0))
Expand Down
14 changes: 13 additions & 1 deletion grc/nrsc5_hdc_encoder.block.yml
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,17 @@ parameters:
label: Bitrate
dtype: int
default: 64000
- id: use_parametric_stereo
label: Stereo Mode
dtype: enum
default: 'False'
options: ['False', 'True']
option_labels: ['Regular Stereo (AOT 127)', 'Parametric Stereo (AOT 128)']
hide: ${ 'all' if channels == 1 else 'none' }
- id: tx_digital_gain
label: TX Digital Audio Gain (dB)
dtype: int
default: 0

inputs:
- domain: stream
Expand All @@ -25,9 +36,10 @@ outputs:
asserts:
- ${ 1 <= channels <= 2 }
- ${ 0 < bitrate }
- ${ -8 <= tx_digital_gain <= 6 }

templates:
imports: import nrsc5
make: nrsc5.hdc_encoder(${channels}, ${bitrate})
make: nrsc5.hdc_encoder(${channels}, ${bitrate}, ${use_parametric_stereo}, ${tx_digital_gain})

file_format: 1
11 changes: 10 additions & 1 deletion grc/nrsc5_l2_encoder.block.yml
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,14 @@ parameters:
option_labels: ["Disable", "Select", "Enable"]
default: nrsc5.blend.ENABLE
hide: ${ ('none' if num_progs > 0 and first_prog == 0 else 'all') }
- id: tx_digital_gain
label: TX Digital Audio Gain (dB)
dtype: int
default: 0
- id: debug_logs
label: Debug Logs
dtype: bool
default: 'False'

inputs:
- label: hdc
Expand Down Expand Up @@ -63,9 +71,10 @@ asserts:
- ${ 0 <= first_prog <= 7 }
- ${ 0 <= num_progs <= 8 - first_prog }
- ${ data_bytes <= (size - 22) // 8 - 1 - ccc_width }
- ${ -8 <= tx_digital_gain <= 6 }

templates:
imports: import nrsc5
make: nrsc5.l2_encoder(${num_progs}, ${first_prog}, ${size}, ${data_bytes}, ${blend_control}, ccc_width=${ccc_width})
make: nrsc5.l2_encoder(${num_progs}, ${first_prog}, ${size}, ${data_bytes}, ${blend_control}, ${tx_digital_gain}, ${debug_logs}, ccc_width=${ccc_width})

file_format: 1
24 changes: 23 additions & 1 deletion grc/nrsc5_lot_encoder.block.yml
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ category: '[NRSC-5]'

templates:
imports: import nrsc5
make: nrsc5.lot_encoder(${filename}, ${lot_id}, ${port})
make: nrsc5.lot_encoder(${filename}, ${lot_id}, ${port}, ${expiry})

parameters:
- id: filename
Expand All @@ -19,6 +19,10 @@ parameters:
label: Port
dtype: int
default: '0x1001'
- id: expiry
label: Expiry
dtype: string
default: '45 minutes'

inputs:
- label: file
Expand All @@ -33,4 +37,22 @@ outputs:
domain: message
optional: false

documentation: |-
Large Object Transfer (LOT) encoder. Transmits files as AAS PDUs.

Supported file types (auto-detected by magic bytes or extension):
- PNG (.png)
- JPEG (.jpg, .jpeg)
- GIF (.gif) -- both GIF87a and GIF89a
- Text (.txt)

For data-only channels (ports not tied to an audio subchannel's album art
or station logo), you must configure a matching data_channel entry in the
SIS & SIG encoder block with the same port and LOT ID. Without that SIG
advertisement the decoder will not know to reassemble the LOT object.

Expiry supports relative time strings (e.g. "45 minutes", "2 hours",
"1 week") or ISO 8601 datetime with timezone (e.g.
"2026-12-31T23:59:59+00:00").

file_format: 1
33 changes: 29 additions & 4 deletions grc/nrsc5_psd_encoder.block.yml
Original file line number Diff line number Diff line change
Expand Up @@ -12,11 +12,31 @@ parameters:
- id: title
label: Title
dtype: string
default: Title
default: ''
- id: artist
label: Artist
dtype: string
default: Artist
default: ''
- id: album
label: Album
dtype: string
default: ''
- id: genre
label: Genre
dtype: string
default: ''
- id: comment_language
label: Comment Language
dtype: string
default: 'eng'
- id: comment_short_desc
label: Comment Short Description
dtype: string
default: ''
- id: comment_text
label: Comment Text
dtype: string
default: ''
- id: bytes_per_frame
label: Bytes/frame limit
dtype: int
Expand All @@ -39,9 +59,14 @@ asserts:

templates:
imports: import nrsc5
make: nrsc5.psd_encoder(${prog_num}, ${title}, ${artist}, ${bytes_per_frame})
make: nrsc5.psd_encoder(${prog_num}, ${title}, ${artist}, ${album}, ${genre}, ${comment_language}, ${comment_short_desc}, ${comment_text}, ${bytes_per_frame})

documentation: |-
Accepts messages starting with 'title', 'artist', or 'lot' to set the respective fields.
Accepts messages starting with the following to set the respective fields:
- Basic metadata: 'title', 'artist', 'album', 'genre'
- Comment: 'comment' (text), 'comment_short' (short description), 'comment_language' (ISO 639-2 code)
- Commercial: 'commercial_price', 'commercial_valid', 'commercial_url', 'commercial_received', 'commercial_seller', 'commercial_desc'
- Reference ID: 'ufid_owner', 'ufid_id'
- Album art: 'lot'

file_format: 1
Loading