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 selectionPOST /audio/streamPush raw audio data into the live stream buffer. Format: S16LE stereo PCM at the configured --audio-rate (default 44100 Hz).
Requires --audio-stdin or a configured stream source.
Request: Binary body, application/octet-stream, raw S16LE stereo PCM bytes.
Response:
{
"ok": true,
"frames": 4096,
"stats": {
"available": 12000,
"capacity": 131072,
"buffered": 0.09,
"written": 890000,
"underruns": 0,
"overflows": 0
}
}
Example:
# Push a file
ffmpeg -i song.mp3 -f s16le -ar 44100 -ac 2 - | \
curl -X POST --data-binary @- http://pluto:8088/audio/stream
Errors:
405 if not POST503 if no audio stream configuredPipe any audio source through ffmpeg into the transmitter:
# Internet radio stream
ffmpeg -i "http://stream.example.com/radio.mp3" -f s16le -ar 44100 -ac 2 - | \
fmrtx --tx --tx-auto-start --audio-stdin --config config.json
# Local music file
ffmpeg -i music.flac -f s16le -ar 44100 -ac 2 - | \
fmrtx --tx --tx-auto-start --audio-stdin
# Playlist (ffmpeg concat)
ffmpeg -f concat -i playlist.txt -f s16le -ar 44100 -ac 2 - | \
fmrtx --tx --tx-auto-start --audio-stdin
# PulseAudio / ALSA capture (Linux)
parecord --format=s16le --rate=44100 --channels=2 - | \
fmrtx --tx --tx-auto-start --audio-stdin
# Custom sample rate (e.g. 48kHz source)
ffmpeg -i source.wav -f s16le -ar 48000 -ac 2 - | \
fmrtx --tx --tx-auto-start --audio-stdin --audio-rate 48000
Push audio from a remote machine via the HTTP API:
# From another machine on the network
ffmpeg -i music.mp3 -f s16le -ar 44100 -ac 2 - | \
curl -X POST --data-binary @- http://pluto-host:8088/audio/stream
The stream uses a lock-free ring buffer (default: 2 seconds at input rate). Buffer stats are available in GET /runtime under audioStream:
{
"audioStream": {
"available": 12000,
"capacity": 131072,
"buffered": 0.09,
"written": 890000,
"underruns": 0,
"overflows": 0
}
}
When no audio is streaming, the transmitter falls back to the configured tone generator or silence.