# Atmos validation

Validates a Dolby Atmos master against Apple's delivery requirements and Sony's operational QC. A
validation request pairs the Atmos ADM master with its stereo reference; the result is an
`is_valid` verdict plus metadata and a set of findings.

Findings come in two severities:

- **Errors** fail the delivery (`is_valid = false`). These are structural: wrong container, codec,
  bit depth, sample rate, a truncated file, or an Atmos/stereo duration mismatch beyond 2 s.
- **Warnings** are advisory — they are recorded and surfaced but never fail the delivery. Every
  signal-analysis check below is a warning.

```mermaid
flowchart TD
    msg[/"SFN message:<br/>atmos + stereo reference (S3)"/]
    msg --> mediainfo["mediainfo checks<br/>(container, codec, bit depth,<br/>sample rate, truncation, duration)"]

    mediainfo -->|"structural error"| fail["is_valid = false<br/>(errors block the delivery)"]
    mediainfo -->|"otherwise valid"| render["render checks<br/>(one spawned worker process)"]

    render --> report["result: is_valid + metadata<br/>+ advisory warnings"]
    fail --> report

    classDef err fill:#fdd
    classDef warn fill:#ffd
    class fail err
    class render warn
```

## Render-based checks: one shared pass

The signal-analysis checks all need the Atmos objects **rendered to speaker channels** — measuring
the raw stored channels is incorrect for object-based audio. The EBU ADM Renderer (vendored `ear`,
implementing ITU-R BS.2127) is the only open-source renderer that does object-aware ADM rendering.

Rendering is expensive: the ADM parse/preprocess dominates (~2.2 GB, ~24 s on a dense master). So
the worker parses **once** and runs **one pass** over the audio blocks, with every check hanging off
that single pass rather than re-parsing or re-rendering. The render runs in a spawned worker process
so its memory peak is isolated from the long-running poller and a render crash can't take the worker
down. `run_render_checks` is the picklable entry point; `measure_render_checks` is the pipeline.

```mermaid
flowchart TD
    atmos[("Atmos ADM master<br/>(S3, seekable)")]
    stereo[("Stereo reference<br/>(S3, presigned URL)")]

    subgraph worker["Render worker — 1 process, 1 vCPU (~2.2 GB peak)"]
        direction TB
        parse["Parse ADM once (~24 s)<br/>fix_dolby → validate → preprocess<br/>classify LFE / object / bed-height channels"]
        loop["ONE pass over the audio blocks<br/>(both renderers share the parse)"]
        parse --> loop
    end

    atmos --> parse

    loop -->|"each block → 5.1 render, streamed live"| loudnorm["ffmpeg loudnorm"]
    loudnorm --> lt["LOUDNESS_TOO_HIGH (> -18 LKFS)<br/>TRUE_PEAK_TOO_HIGH (> -1 dBTP)"]

    loop -->|"each block until 60 s → 2.0 render → mono, buffered"| buf["mono buffer → resample 8 kHz"]
    stereo --> dec["ffmpeg decode 60 s → mono 8 kHz"]
    buf --> xcorr["z-normalized cross-correlate"]
    dec --> xcorr
    xcorr --> align["SYNC_MISMATCH (|lag| > 50 ms)<br/>CONTENT_MISMATCH (|r| < 0.50)"]

    loop -->|"each block, raw source channels"| scans["LFE channels → streaming Hann FFT<br/>all channels → running peak"]
    scans --> lfe["LFE_LEVEL_ABOVE_400HZ_TOO_HIGH (> -60 dBFS ≥400 Hz)<br/>LFE_LEVEL_ABOVE_2000HZ_TOO_HIGH (> -100 dBFS ≥2 kHz)"]
    scans --> sob["SILENT_OBJECT (object peak ≤ -120 dBFS)<br/>SILENT_HEIGHT (silent bed heights,<br/>no active objects)"]

    classDef warn fill:#ffd
    class lt,align,lfe,sob warn
```

Three consumers ride the one pass, each taking exactly what it needs from every block:

- **Loudness / true-peak** (`loudness_check.py`) — the 5.1 (BS.2051 `0+5+0`) render is streamed
  block-by-block, live, into ffmpeg `loudnorm`, which reports integrated loudness (`input_i`) and
  true peak (`input_tp`) per ITU-R BS.1770-4. Measured on the 5.1 render per Apple/Netflix/Dolby
  convention; the layout is declared to ffmpeg so it excludes the LFE from loudness and weights the
  surrounds correctly. Warns above Apple's published limits (−18 LKFS, −1 dBTP).
- **Alignment** (`alignment_check.py`) — the stereo (`0+2+0`) render's first 60 s is downmixed to
  mono, buffered, and cross-correlated (z-normalized, 8 kHz) against the ffmpeg-decoded stereo
  reference. The global lag gives `SYNC_MISMATCH` (Sony QC parity, ±50 ms); the signed Pearson r
  gives `CONTENT_MISMATCH` (below |r| = 0.50 the reference can't be confirmed as the same program,
  so the meaningless lag is suppressed). Only 60 s is needed because a conform offset is a global
  constant, not a drift.
- **LFE + silent channels** (`lfe_check.py`, `silent_object_check.py`) — these read the **raw
  source channels** of every block (the whole track, not just the head). `AtmosRender` classifies
  the source channels from the parsed ADM (LFE by frequency element or BS.2051 label; objects by
  Objects-type rendering items; bed height channels by position above the listener). The LFE
  channel is streamed through a Hann-windowed FFT (Sony's exact method) to catch full-frequency
  content; every channel's running peak feeds the silent-object and silent-height scans.

## Checks

| Finding | Severity | Source | Meaning |
|---|---|---|---|
| `NOT_WAVE` / `NOT_PCM` / `NOT_LPCM` / `WRONG_BIT_DEPTH` / `WRONG_SAMPLE_RATE` | error | mediainfo | Container/format must be a 24-bit 48 kHz LPCM Wave file. |
| `NOT_DOLBY_ATMOS` | error | mediainfo | Must be a Dolby Atmos Master ADM profile. |
| `ATMOS_FILE_TRUNCATED` / `STEREO_FILE_TRUNCATED` | error | mediainfo | The declared duration exceeds the actual audio data. |
| `DURATION_MISMATCH` | error | mediainfo | Atmos and stereo durations differ by more than 2 s. |
| `LOUDNESS_TOO_HIGH` | warning | loudness_check | Integrated loudness of the 5.1 render exceeds −18 LKFS. |
| `TRUE_PEAK_TOO_HIGH` | warning | loudness_check | True peak of the 5.1 render exceeds −1 dBTP. |
| `SYNC_MISMATCH` | warning | alignment_check | Atmos and stereo reference are misaligned by more than 50 ms. |
| `CONTENT_MISMATCH` | warning | alignment_check | The stereo render and reference don't correlate well enough to confirm the same program (|r| < 0.50). |
| `LFE_LEVEL_ABOVE_400HZ_TOO_HIGH` | warning | lfe_check | The LFE channel's peak level at/above 400 Hz exceeds −60 dBFS — substantial out-of-band content. |
| `LFE_LEVEL_ABOVE_2000HZ_TOO_HIGH` | warning | lfe_check | The LFE channel's peak level at/above 2 kHz exceeds −100 dBFS — any high-frequency content (40 dB stricter than the 400 Hz gate). |
| `SILENT_OBJECT` | warning | silent_object_check | An object channel is blank (peak ≤ −120 dBFS). |
| `SILENT_HEIGHT` | warning | silent_object_check | The bed's height channels are silent and no object is active — the master carries no height content. |

Thresholds are Apple's published values (loudness / true peak) and Sony QC's operational values
(LFE, silence, sync window, content-match floor); each lives next to its enforcing code. The full
rationale and reconciliation is in the ticket spec (CDAM-3806).
