# fm-rds-tx HTTP Control API Base URL: `http://{listenAddress}` (default `127.0.0.1:8088`) --- ## Endpoints ### `GET /healthz` Health check. **Response:** ```json {"ok": true} ``` --- ### `GET /status` Current transmitter status (read-only snapshot). **Response:** ```json { "service": "fm-rds-tx", "backend": "pluto", "frequencyMHz": 100.0, "stereoEnabled": true, "rdsEnabled": true, "preEmphasisTauUS": 50, "limiterEnabled": true, "fmModulationEnabled": true } ``` --- ### `GET /runtime` Live engine and driver telemetry. Only populated when TX is active. **Response:** ```json { "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 /config` Full current configuration (all fields, including non-patchable). **Response:** Complete `Config` JSON object. --- ### `POST /config` **Live 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:** ```json {"ok": true, "live": true} ``` `"live": true` = changes were forwarded to the running engine. `"live": false` = engine not active, changes saved for next start. #### Patchable fields — DSP (applied within ~50ms) | 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). | #### Patchable fields — RDS text (applied within ~88ms) | 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. #### Patchable fields — other (saved, not live-applied) | 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.** | #### Examples ```bash # 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" }' ``` #### Error handling Invalid values return `400 Bad Request` with a descriptive message: ```bash curl -X POST localhost:8088/config -d '{"frequencyMHz": 200}' # → 400: frequencyMHz out of range (65-110) ``` --- ### `POST /tx/start` Start transmission. Requires `--tx` mode with hardware. **Response:** ```json {"ok": true, "action": "started"} ``` **Errors:** - `405` if not POST - `503` if no TX controller (not in `--tx` mode) - `409` if already running --- ### `POST /tx/stop` Stop transmission. **Response:** ```json {"ok": true, "action": "stopped"} ``` --- ### `GET /dry-run` Generate a synthetic frame summary without hardware. Useful for config verification. **Response:** `FrameSummary` JSON with mode, rates, source info, preview samples. --- ## Live update architecture 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. ## Parameters that require restart These cannot be hot-reloaded (they affect DSP pipeline structure): - `compositeRateHz` — changes sample rate of entire DSP chain - `deviceSampleRateHz` — changes hardware rate / upsampler ratio - `maxDeviationHz` — changes FM modulator scaling - `preEmphasisTauUS` — changes filter coefficients - `rds.pi` / `rds.pty` — rarely change, baked into encoder init - `audio.inputPath` — audio source selection - `backend.kind` / `backend.device` — hardware selection --- ### `POST /audio/stream` Push 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:** ```json { "ok": true, "frames": 4096, "stats": { "available": 12000, "capacity": 131072, "buffered": 0.09, "written": 890000, "underruns": 0, "overflows": 0 } } ``` **Example:** ```bash # 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 POST - `503` if no audio stream configured --- ## Audio Streaming ### Stdin pipe (primary method) Pipe any audio source through ffmpeg into the transmitter: ```bash # 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 ``` ### HTTP audio push Push audio from a remote machine via the HTTP API: ```bash # 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 ``` ### Audio buffer The stream uses a lock-free ring buffer (default: 2 seconds at input rate). Buffer stats are available in `GET /runtime` under `audioStream`: ```json { "audioStream": { "available": 12000, "capacity": 131072, "buffered": 0.09, "written": 890000, "underruns": 0, "overflows": 0 } } ``` - **underruns**: DSP consumed faster than audio arrived (silence inserted) - **overflows**: Audio arrived faster than DSP consumed (data dropped) - **buffered**: Fill ratio (0.0 = empty, 1.0 = full) When no audio is streaming, the transmitter falls back to the configured tone generator or silence.