Go-based FM stereo transmitter with RDS, Windows-first and cross-platform
Você não pode selecionar mais de 25 tópicos Os tópicos devem começar com uma letra ou um número, podem incluir traços ('-') e podem ter até 35 caracteres.

7.9KB

fm-rds-tx HTTP Control API

Base URL: http://{listenAddress} (default 127.0.0.1:8088)


Endpoints

GET /healthz

Health check.

Response:

{"ok": true}

GET /status

Current 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 /runtime

Live 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 /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:

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

# 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:

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:

{"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:

{"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:

{
  "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 POST
  • 503 if no audio stream configured

Audio Streaming

Stdin pipe (primary method)

Pipe 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

HTTP audio push

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

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:

{
  "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.