Base URL: http://{listenAddress} (default 127.0.0.1:8088)
GET /healthzHealth check.
Response:
{"ok": true}
GET /statusCurrent transmitter status (read-only snapshot).
Response:
{
"service": "fm-rds-tx",
"backend": "pluto",
"frequencyMHz": 100.0,
"stereoEnabled": true,
"rdsEnabled": true,
"preEmphasisTauUS": 50,
"limiterEnabled": true,
"fmModulationEnabled": true
}
GET /runtimeLive engine and driver telemetry. Only populated when TX is active.
Response:
{
"engine": {
"state": "running",
"chunksProduced": 12345,
"totalSamples": 1408950000,
"underruns": 0,
"lastError": "",
"uptimeSeconds": 3614.2
},
"driver": {
"txEnabled": true,
"streamActive": true,
"framesWritten": 12345,
"samplesWritten": 1408950000,
"underruns": 0,
"effectiveSampleRateHz": 2280000
}
}
GET /configFull current configuration (all fields, including non-patchable).
Response: Complete Config JSON object.
POST /configLive parameter update. Changes are applied to the running TX engine immediately — no restart required. Only include fields you want to change (PATCH semantics).
Request body: JSON with any subset of patchable fields.
Response:
{"ok": true, "live": true}
"live": true = changes were forwarded to the running engine.
"live": false = engine not active, changes saved for next start.
| Field | Type | Range | Description |
|---|---|---|---|
frequencyMHz |
float | 65–110 | TX center frequency. Tunes hardware LO live. |
outputDrive |
float | 0–3 | Composite output level multiplier. |
stereoEnabled |
bool | Enable/disable stereo (pilot + 38kHz subcarrier). | |
pilotLevel |
float | 0–0.2 | 19 kHz pilot injection level. |
rdsInjection |
float | 0–0.15 | 57 kHz RDS subcarrier injection level. |
rdsEnabled |
bool | Enable/disable RDS subcarrier. | |
limiterEnabled |
bool | Enable/disable MPX peak limiter. | |
limiterCeiling |
float | 0–2 | Limiter ceiling (max composite amplitude). |
| Field | Type | Max length | Description |
|---|---|---|---|
ps |
string | 8 chars | Program Service name (station name on receiver display). |
radioText |
string | 64 chars | RadioText message (scrolling text on receiver). |
When radioText is updated, the RDS A/B flag toggles automatically per spec, signaling receivers to refresh their display.
| Field | Type | Description |
|---|---|---|
toneLeftHz |
float | Left tone frequency (test generator). |
toneRightHz |
float | Right tone frequency (test generator). |
toneAmplitude |
float | Test tone amplitude (0–1). |
preEmphasisTauUS |
float | Pre-emphasis time constant. Requires restart. |
# Tune to 99.5 MHz
curl -X POST localhost:8088/config -d '{"frequencyMHz": 99.5}'
# Switch to mono
curl -X POST localhost:8088/config -d '{"stereoEnabled": false}'
# Update now-playing text
curl -X POST localhost:8088/config \
-d '{"ps": "MYRADIO", "radioText": "Artist - Song Title"}'
# Reduce power + disable limiter
curl -X POST localhost:8088/config \
-d '{"outputDrive": 0.8, "limiterEnabled": false}'
# Full update
curl -X POST localhost:8088/config -d '{
"frequencyMHz": 101.3,
"outputDrive": 2.2,
"stereoEnabled": true,
"pilotLevel": 0.041,
"rdsInjection": 0.021,
"rdsEnabled": true,
"limiterEnabled": true,
"limiterCeiling": 1.0,
"ps": "PIRATE",
"radioText": "Broadcasting from the attic"
}'
Invalid values return 400 Bad Request with a descriptive message:
curl -X POST localhost:8088/config -d '{"frequencyMHz": 200}'
# → 400: frequencyMHz out of range (65-110)
POST /tx/startStart transmission. Requires --tx mode with hardware.
Response:
{"ok": true, "action": "started"}
Errors:
405 if not POST503 if no TX controller (not in --tx mode)409 if already runningPOST /tx/stopStop transmission.
Response:
{"ok": true, "action": "stopped"}
GET /dry-runGenerate a synthetic frame summary without hardware. Useful for config verification.
Response: FrameSummary JSON with mode, rates, source info, preview samples.
All live updates are lock-free in the DSP path:
| What | Mechanism | Latency |
|---|---|---|
| DSP params | atomic.Pointer[LiveParams] loaded once per chunk |
≤ 50ms |
| RDS text | atomic.Value in encoder, read at group boundary |
≤ 88ms |
| TX frequency | atomic.Pointer in engine, driver.Tune() between chunks |
≤ 50ms |
No mutex, no channel, no allocation in the real-time path. The HTTP goroutine writes atomics, the DSP goroutine reads them.
These cannot be hot-reloaded (they affect DSP pipeline structure):
compositeRateHz — changes sample rate of entire DSP chaindeviceSampleRateHz — changes hardware rate / upsampler ratiomaxDeviationHz — changes FM modulator scalingpreEmphasisTauUS — changes filter coefficientsrds.pi / rds.pty — rarely change, baked into encoder initaudio.inputPath — audio source selectionbackend.kind / backend.device — hardware selection