# fm-rds-tx — DSP Signal Chain & Konfiguration

## Übersicht

fm-rds-tx ist ein broadcast-konformer FM-Stereo-MPX-Encoder mit RDS für PlutoSDR/SoapySDR.
Die DSP-Kette folgt dem Industriestandard (Omnia, Orban, Stereo Tool) mit Clip-Filter-Clip-
Architektur und ITU-R BS.412 MPX Power Limiting.

---

## Signalkette

```
┌─────────────────────────────────────────────────────────────────┐
│  AUDIO INPUT                                                     │
│  S16LE Stereo PCM via stdin (ffmpeg) oder interner Tongenerator  │
└──────────────────────────┬──────────────────────────────────────┘
                           │
                           ▼
┌──────────────────────────────────────────────────────────────────┐
│  STAGE 1: Pre-Emphasis + Band-Limiting (pro Kanal L/R)           │
│                                                                   │
│  Audio × gain ──→ Pre-Emphasis (50µs EU / 75µs US)               │
│    ──→ 15kHz LPF (8th-order Chebyshev Type I, 0.5dB Ripple)     │
│    ──→ 19kHz Notch (Q=15, double-cascade)                        │
│                                                                   │
│  Frequenzantwort (verifiziert):                                   │
│    10kHz: +0.1dB (flat)     15kHz: -0.2dB                        │
│    17kHz: -21dB             18.5kHz: -40dB                        │
│    19kHz: -155dB (tot)      22kHz: -51dB                          │
└──────────────────────────┬──────────────────────────────────────┘
                           │
                           ▼
┌──────────────────────────────────────────────────────────────────┐
│  STAGE 2: Drive + Kompression + Clip₁                            │
│                                                                   │
│  × OutputDrive                                                    │
│    ──→ StereoLimiter (5ms Attack / 200ms Release, ceiling)       │
│    ──→ HardClip₁ (ceiling)                                       │
│                                                                   │
│  Der Limiter komprimiert die Dynamik (bringt Average hoch).      │
│  Der Clip fängt Peaks die der Limiter's Attack verpasst.         │
│  "Slow-to-fast Progression" — Broadcast-Standard.                │
└──────────────────────────┬──────────────────────────────────────┘
                           │
                           ▼
┌──────────────────────────────────────────────────────────────────┐
│  STAGE 3: Cleanup LPF + Clip₂ (Overshoot-Kompensator)           │
│                                                                   │
│  ──→ 15kHz LPF (8th-order Chebyshev, identisch zu Stage 1)      │
│  ──→ HardClip₂ (ceiling)                                         │
│                                                                   │
│  Der zweite LPF-Pass entfernt Clip₁-Harmonische.                │
│  Clip₂ fängt die LPF-Overshoots (IIR-Filter erzeugen diese).   │
│  Doppelter LPF-Pass verdoppelt die Guard-Band-Dämpfung.         │
└──────────────────────────┬──────────────────────────────────────┘
                           │
                           ▼
┌──────────────────────────────────────────────────────────────────┐
│  STAGE 4: Stereo-Encode                                          │
│                                                                   │
│  L/R ──→ Mono: (L+R)/2         (0–15kHz Baseband)               │
│      ──→ Stereo: (L-R)/2 × sin(38kHz)  (23–53kHz DSB-SC)       │
│      ──→ Pilot: sin(19kHz)     (phase-locked, kohärent)         │
│      ──→ RDS Carrier: sin(57kHz) (3× Pilot-Phase, kohärent)     │
└──────────────────────────┬──────────────────────────────────────┘
                           │
                           ▼
┌──────────────────────────────────────────────────────────────────┐
│  STAGE 5: Composite Clip + Schutzfilter                          │
│                                                                   │
│  Audio-MPX (Mono + Stereo-Sub)                                   │
│    ──→ HardClip₃ (ceiling)     — finale Deviations-Kontrolle    │
│    ──→ 19kHz Notch (Q=10, double) — Clip-Harmonische bei Pilot  │
│    ──→ 57kHz Notch (Q=10, double) — Clip-Harmonische bei RDS    │
│                                                                   │
│  Guard Bands (total, 2× LPF + Notches):                         │
│    19kHz: >-80dB broadband, >-90dB exakt                         │
│    57kHz: >-100dB                                                 │
│    (Omnia 11 Spezifikation: >80dB — wir sind on par)            │
└──────────────────────────┬──────────────────────────────────────┘
                           │
                           ▼
┌──────────────────────────────────────────────────────────────────┐
│  STAGE 6: BS.412 MPX Power Limiter (optional)                    │
│                                                                   │
│  ──→ × BS412 Gain                                                │
│                                                                   │
│  Rolling 60-Sekunden RMS-Messung auf dem Audio-Composite.        │
│  Langsamer Gain-Regler (2s Attack / 5s Release).                 │
│  Zieht Pilot+RDS-Power automatisch vom Budget ab.               │
│  Pflicht in CH, DE, NL, FR für lizenzierte FM-Sender.            │
│  ~5dB Lautheitsverlust bei 0 dBr Threshold.                     │
└──────────────────────────┬──────────────────────────────────────┘
                           │
                           ▼
┌──────────────────────────────────────────────────────────────────┐
│  STAGE 7: Pilot + RDS Injection (fixe Amplitude)                 │
│                                                                   │
│  composite = audioMPX                                             │
│            + pilotLevel × sin(19kHz)     — IMMER 9%              │
│            + rdsInjection × rdsWaveform  — IMMER 4%              │
│                                                                   │
│  Pilot und RDS werden NIE geclippt, NIE gefiltert, NIE vom      │
│  BS.412-Limiter berührt. Konstante Amplitude, immer.             │
│                                                                   │
│  Peak Composite = ceiling + pilotLevel + rdsInjection ≈ 113%    │
│  (Standard-Broadcast-Praxis — Pilot/RDS werden von den meisten  │
│   Regulierungsbehörden aus dem Modulationslimit ausgenommen)     │
└──────────────────────────┬──────────────────────────────────────┘
                           │
                           ▼
┌──────────────────────────────────────────────────────────────────┐
│  STAGE 8: FM-Modulation                                          │
│                                                                   │
│  Split-Rate: Composite @ 228kHz ──→ FMUpsampler ──→ IQ @ 2.28MHz│
│  maxDeviation × mpxGain = effektive Deviation                    │
│  composite=1.0 → ±75kHz Deviation (bei mpxGain=1.0)             │
│                                                                   │
│  Ausgabe: IQ-Samples (float32) an PlutoSDR via libiio            │
└──────────────────────────────────────────────────────────────────┘
```

---

## Konfiguration

### Audio

| Parameter | Typ | Default | Beschreibung |
|---|---|---|---|
| `audio.gain` | float | 1.0 | Eingangsverstärkung vor Pre-Emphasis. 1.0 = unity. |
| `audio.inputPath` | string | "" | WAV-Datei als Quelle (leer = stdin oder Tongenerator) |

**Empfehlung:** `gain: 1.0`. Pegel-Kontrolle über `outputDrive`.

### FM — Audio-Processing

| Parameter | Typ | Default | Bereich | Beschreibung |
|---|---|---|---|---|
| `outputDrive` | float | 0.5 | 0–10 | Eingangsverstärkung vor Limiter/Clip. Bestimmt wie aggressiv Limiter und nachfolgende Clip-Stufen arbeiten. |
| `limiterEnabled` | bool | true | — | Aktiviert nur die StereoLimiter-Stufe (5ms/200ms) im L/R-Pfad. Die Hard-Clip-Stufen bleiben aktiv. |
| `limiterCeiling` | float | 1.0 | 0–2 | Maximum-Amplitude für Audio L/R und Composite. 1.0 = ±75kHz. |
| `preEmphasisTauUS` | float | 50 | 0/50/75 | Pre-Emphasis Zeitkonstante. 50µs = Europa/CH, 75µs = USA, 0 = aus. |

**outputDrive im Detail:**

Der Drive bestimmt den *Klangcharakter*, nicht die Lautstärke (wenn BS.412 aktiv ist):

| Drive | Effekt | Einsatz |
|---|---|---|
| 1–2 | Wenig Kompression, dynamisch, sauber | Klassik, Jazz, Wortbeiträge |
| 3–4 | Moderate Kompression, ausgewogen | **Empfohlen für die meisten Formate** |
| 5–7 | Aggressive Kompression, dichter Sound | Pop/Rock-Formatradio |
| 8–10 | Maximale Dichte, hörbare Clip-Artefakte | Nicht empfohlen |

**Empfehlung:** `outputDrive: 3.0` für sauberen, broadcast-fähigen Sound.

### FM — Pilot & RDS

| Parameter | Typ | Default | Bereich | Beschreibung |
|---|---|---|---|---|
| `pilotLevel` | float | 0.09 | 0–0.2 | 19kHz Pilot-Amplitude. **Direkte Prozentangabe von ±75kHz.** 0.09 = 9% = ITU-Standard. |
| `rdsInjection` | float | 0.04 | 0–0.15 | RDS-Amplitude. **Direkte Prozentangabe.** 0.04 = 4%. Waveform ist unity-normalisiert. |
| `stereoEnabled` | bool | true | — | Stereo-Encode an/aus. Aus = nur Mono (L+R)/2, kein Pilot. |

**Empfehlung:** `pilotLevel: 0.09`, `rdsInjection: 0.04`. Nicht ändern ausser es gibt einen guten Grund.

Pilot und RDS sind **unabhängig vom OutputDrive** — sie bleiben immer bei der konfigurierten Amplitude, egal wie hart das Audio komprimiert wird.

### FM — BS.412 (ITU-R MPX Power Limiter)

| Parameter | Typ | Default | Beschreibung |
|---|---|---|---|
| `bs412Enabled` | bool | false | Aktiviert den BS.412 MPX Power Limiter. **Pflicht in CH, DE, NL, FR.** |
| `bs412ThresholdDBr` | float | 0 | Power-Limit in dBr. 0 = Standard. +3 = relaxiert. |

**Was BS.412 macht:**
Begrenzt die durchschnittliche MPX-Leistung über ein rollendes 60-Sekunden-Fenster.
Reduziert die Audio-Amplitude langsam wenn die Power den Threshold überschreitet.
Pilot und RDS werden automatisch vom Power-Budget abgezogen.

**Effekt auf den Sound:**
- ~5dB Lautheitsverlust bei 0 dBr mit aggressiver Kompression
- Weniger Verlust bei dynamischerem Material
- OutputDrive beeinflusst bei aktivem BS.412 nur den Klangcharakter, nicht die Lautheit

**Empfehlung:** `bs412Enabled: true`, `bs412ThresholdDBr: 0` für BAKOM-Compliance.

### FM — Hardware-Kalibrierung

| Parameter | Typ | Default | Bereich | Beschreibung |
|---|---|---|---|---|
| `mpxGain` | float | 1.0 | 0.1–5 | Skaliert die FM-Deviation (nicht den Composite!). Kompensiert DAC/SDR-Hardware-Faktoren. |
| `maxDeviationHz` | float | 75000 | 0–150000 | Maximale FM-Deviation in Hz. 75000 = Standard. |
| `compositeRateHz` | int | 228000 | — | Interne DSP-Sample-Rate. 228000 = 12×19kHz (optimal für Pilot-Kohärenz). |

**MpxTool-Kalibrierung:**
1. `mpxGain: 1.0` setzen (keine Skalierung)
2. MpxTool Ref Level so einstellen dass **Pilot Level = 9.0%** anzeigt
3. Für PlutoSDR typisch: Ref Level ca. **-7.5 dBFS**
4. Einmal kalibrieren, nie wieder anfassen

**Empfehlung:** `mpxGain: 1.0`, `maxDeviationHz: 75000`. Kalibrierung über MpxTool Ref Level.

### RDS

| Parameter | Typ | Default | Beschreibung |
|---|---|---|---|
| `rds.enabled` | bool | true | RDS an/aus |
| `rds.pi` | string | "1234" | Programme Identification (4-stellig hex). Muss mit BAKOM-Zuteilung übereinstimmen. |
| `rds.ps` | string | "FMRTX" | Programme Service Name (max 8 Zeichen). Stationsname auf dem Display. |
| `rds.radioText` | string | "" | Radio Text (max 64 Zeichen). Scrolltext auf dem Display. |
| `rds.pty` | int | 0 | Programme Type. 0=undefined, 1=News, 3=Info, 10=Pop, 15=Other Music, etc. |

### Backend

| Parameter | Typ | Default | Beschreibung |
|---|---|---|---|
| `backend.kind` | string | "file" | `"pluto"` für PlutoSDR, `"soapy"` für SoapySDR, `"file"` für Dateiausgabe |
| `backend.device` | string | "" | Device-String. PlutoSDR: `"usb:"` oder `"ip:192.168.2.1"` |
| `backend.deviceSampleRateHz` | float | 0 | SDR-Device-Rate. 2280000 = 10× compositeRate (optimal). |

---

## Referenz-Konfiguration (BAKOM-konform, PlutoSDR)

```json
{
  "audio": {
    "gain": 1.0
  },
  "rds": {
    "enabled": true,
    "pi": "BEEF",
    "ps": "RADIO-ZH",
    "radioText": "Ihr Zürcher Kurzradio",
    "pty": 0
  },
  "fm": {
    "frequencyMHz": 100.0,
    "stereoEnabled": true,
    "pilotLevel": 0.09,
    "rdsInjection": 0.04,
    "preEmphasisTauUS": 50,
    "outputDrive": 3.0,
    "limiterEnabled": true,
    "limiterCeiling": 1.0,
    "bs412Enabled": true,
    "bs412ThresholdDBr": 0,
    "mpxGain": 1.0,
    "compositeRateHz": 228000,
    "maxDeviationHz": 75000,
    "fmModulationEnabled": true
  },
  "backend": {
    "kind": "pluto",
    "device": "usb:",
    "deviceSampleRateHz": 2280000
  },
  "control": {
    "listenAddress": "127.0.0.1:8088"
  }
}
```

---

## Audio-Streaming (Produktionsbetrieb)

```bash
ffmpeg -i http://stream-url/stream -f s16le -ar 44100 -ac 2 - | fmrtx.exe --tx --tx-auto-start --audio-stdin --audio-rate 44100 --config config.json
```

**Hinweis:** Unter Windows `cmd.exe` verwenden, nicht PowerShell (korrumpiert die Binary-Pipe).

---

## Verifizierte Messwerte (MpxTool, PlutoSDR @ 100MHz)

| Parameter | Messung | Soll |
|---|---|---|
| Pilot Level | 9.0% | 9% ✓ |
| RDS Injection | 3.4% | 4% (≈, BPSK-Mittelung) |
| MPX Peak | 105–110% | 100–113% ✓ |
| Guard Band 19kHz | >-80dB | >-80dB (Omnia 11: >80dB) ✓ |
| Audio Bandwidth | flat bis 15kHz | 15kHz ✓ |
