# Voice lines

Everything in this directory is **drop-in**: `AudioBackend::init` scans
`voice/*.wav` once at startup and keys each file by its filename, so adding
or swapping a line needs **no code changes** — just restart the client.
Missing files are silently absent, so this directory can stay empty.

Two categories share the directory.

## 1. Super-activation lines (optional)

    assets/audio/voice/{character}_super.wav

When a unit activates its super, the client plays the `super_activate`
SFX and, if a wav matching the activating unit's **character** exists
here, layers it on top (music is ducked for ~1.4 s so both carry).

- `{character}` is the sprite character key from
  `game/client/src/gfx/mod.rs::character_for_unit` — the SAME
  class→character mapping the super cut-in art uses (`operator` →
  **maid**, so `maid_super.wav`; every other class uses its class name:
  `striker_super.wav`, `sentinel_super.wav`, `phantom_super.wav`,
  `support_super.wav`).
- Keep these ~0.8-1.5 s; the activation SFX is mixed dark/mid-heavy on
  purpose so the speech band is free after the slash accent (~0.6 s in).

**NONE ARE SHIPPED.** `maid_super.wav` (the "shadows" call) was cut on
2026-08-17 — the generated read was poor enough to hurt the moment it was
meant to sell. The path stays live: drop a wav in and it plays, no code
change. If this is revisited, generate it as a CHARACTER take rather than
an announcer one — the announcer persona in `pipeline/out/voice/` is a
detached commentator, which is exactly the wrong register for a line the
character says about her own power.

## 2. Result lines

    assets/audio/voice/announcer_{line}.wav

Played by `AudioCtl::play_announcer("announcer_{line}", duck_ms)` — the key
is the **file stem**, so a new line is a new file plus one call. `duck_ms`
dips the music while the line plays (roughly the line's own length; `0.0`
skips the duck).

Shipped today — **just the two match-result calls**:

| key                  | line        | voice   | duration  |
| -------------------- | ----------- | ------- | --------- |
| `announcer_you_win`  | "YOU WIN!"  | Despina | 1333.7 ms |
| `announcer_you_lose` | "YOU LOSE!" | Despina | 1565.0 ms |

A warm, composed **feminine** delivery — pleasant and human, deliberately
*not* an arcade announcer. An earlier arcade-announcer set (GAME START,
DOMINATION, ROUND ONE, FIGHT, K.O., PERFECT and per-character name calls)
was cut; its takes and renders are kept under `pipeline/out/voice/` if any
of it is ever wanted again.

`client/src/scenes/battle/mod.rs::announcer_len_ms` holds these lengths so
queued lines cannot overlap — **if you replace a wav, re-measure it and
update that table**.

Unlike the super path there is **no rollback guard** on these lines: they
fire on scene/UI transitions rather than sim ticks, so call
`play_announcer` once at the moment of the transition.

## Format (both categories)

44.1 kHz, 16-bit, mono, peaks **~-2.5 dBFS** to sit with the v6.1/v7 SFX
loudness. Verify with:

    python3 pipeline/out/voice/measure.py --check assets/audio/voice/*.wav

## Regenerating / swapping the voice

Reproducible from `pipeline/out/voice/`:

    cd pipeline/out/voice
    GEMINI_API_KEY=... node gen_voice.mjs --audition --persona=pleasant you_win
    GEMINI_TTS_VOICE=Despina node gen_voice.mjs --persona=pleasant --takes=3 you_win you_lose
    ./make_pleasant.sh --ship Despina

`gen_voice.mjs` uses Gemini TTS (`gemini-2.5-pro-preview-tts`). The
`pleasant` persona prompts for a warm, natural, unhurried read and picks
from several takes.

Four voices were rendered as candidates — **Despina** (shipped), `Kore`,
`Sulafat`, `Achernar` — in `pipeline/out/voice/candidates/`. To swap:

    ./make_pleasant.sh --ship Kore     # then re-measure and update announcer_len_ms

The `pleasant` DSP chain in `dsp.sh` is deliberately near-transparent:
**no pitch shift, no saturation, no slapback** — just a high-pass, a small
low-mid trim, a gentle de-esser (~6 dB max at 7 kHz), light 2:1
compression, a whisper of room, and clean normalisation to -2.5 dBFS.
Over-processing is what made the earlier arcade set read as a robot.

> **Heads-up:** `pipeline/.gitignore` ignores all of `out/`, so the pipeline
> scripts (`gen_voice.mjs`, `dsp.sh`, `f0.py`, `measure.py`,
> `make_pleasant.sh`, `render_all.sh`) are **not tracked by git** and will
> not survive a fresh clone. Move them somewhere tracked, or add a
> `!out/voice/*.{mjs,sh,py}` negation, if this pipeline should be kept.
