synops/docs/features/lydmixer.md
vegard 453ec4fb59 Fullfører oppgave 16.4: Delt mixer-kontroll via SpacetimeDB
Implementerer sanntids mixer-synkronisering mellom alle deltakere i et
LiveKit-rom via SpacetimeDB. Når én deltaker justerer gain eller muter
en kanal, oppdateres alle klienters Web Audio-graf og UI umiddelbart.

SpacetimeDB-modul (Rust):
- MixerChannel-tabell med room_id/target_user_id-indekser
- Reducers: set_gain (clamped 0.0-1.5), set_mute, toggle_effect,
  create/delete_mixer_channel, set_mixer_role (editor/viewer)
- Viewer-sjekk i reducers — viewer kan ikke endre andres kanaler
- Opprydding av mixer-kanaler i close_live_room og clear_all

Frontend (SvelteKit):
- mixerChannelStore med reaktive callbacks og room_id-indeks
- MixerTrait leser delt state fra STDB, skriver endringer via reducers
- suppressRemoteSync-flagg forhindrer feedback-loop ved egne endringer
- Viewer-modus: disabled sliders/knapper for låste deltakere
- Visuell (V)-indikator for viewer-kanaler

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-18 05:08:23 +00:00

249 lines
11 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

# Feature: Lydmixer (Virtuelt studioverktøy)
**Filsti:** `docs/features/lydmixer.md`
## 1. Konsept
En nettleserbasert lydmixer inspirert av RødeCaster Pro II. Gir programledere
og møtedeltakere kontroll over lydnivåer, lydeffekter og stemmeeffekter
direkte i Synops — uten behov for ekstern maskinvare.
Bygget på Web Audio API med LiveKit-integrasjon. Alle lydstrømmer (mikrofon,
remote-deltakere, sound pads) rutes gjennom en Web Audio-graf som gir
per-kanal prosessering før avspilling.
## 2. Brukeropplevelse
1. Brukeren åpner studioet/møterommet og kobler seg til LiveKit-rommet.
2. I bunnen av skjermen vises en **mixerstripe** med én kanal per deltaker.
3. Hver kanal har: volumslider, mute-knapp, og valgfri effektkjede.
4. Over mixeren ligger et **pad-brett** med konfigurerbare lydeffekter.
5. En **effektvelger** per kanal lar brukeren slå av/på stemmeeffekter
(monster, robot) og lydbehandling (fat bottom, exciter, sparkle).
## 3. Kjernekomponenter
### 3.1 Kanalstripe (per deltaker)
Hver LiveKit-deltaker får en dedikert kanalstripe i mixeren:
| Element | Funksjon | Web Audio |
|---|---|---|
| **Volumslider** | Visuell fader 0150% | `GainNode` (0.01.5) |
| **Nød-mute** | Stor, rød knapp — umiddelbar demping | `gain.setValueAtTime(0, now)` |
| **Nivåmeter** | VU-meter som viser live lydnivå | `AnalyserNode` → canvas |
| **Navnelabel** | Deltakerens display_name | Fra LiveKit participant |
### 3.2 Effektkjede (per kanal, valgfri)
Hver kanal kan ha en kjede av prosesseringsmoduler som slås av/på individuelt:
| Effekt | Beskrivelse | Web Audio |
|---|---|---|
| **Fat bottom** | Lavfrekvent fylde (~200Hz, +612dB) | `BiquadFilterNode` lowshelf |
| **Exciter** | Harmonisk tilstedeværelse (35kHz) | `WaveShaperNode` + highshelf |
| **Sparkle** | Høyfrekvent luft (~1016kHz, +36dB) | `BiquadFilterNode` highshelf |
| **Monsterstemme** | Pitch ned 48 halvtoner | `AudioWorkletNode` (phase vocoder) |
| **Robotstemme** | Metallisk ring-modulasjon | `OscillatorNode``GainNode.gain` |
Signalflyt per kanal:
```
Kilde → HighPass(80Hz) → FatBottom → Exciter → Sparkle → PitchShift → GainNode(fader) → Master
```
### 3.3 Sound Pads
Et grid med konfigurerbare lyd-pads (inspirert av RødeCaster Pro II sine 8×8 pads):
| Egenskap | Detalj |
|---|---|
| **Layout** | Grid, f.eks. 4×2 (utvidbart) |
| **Avspilling** | Trykk → spill fra start til slutt |
| **Lydkilde** | Forhåndslastede `AudioBuffer` fra CAS |
| **Volum** | Egen `GainNode` per pad |
| **Synkronisering** | LiveKit Data Message → alle klienter spiller samtidig |
| **Konfigurasjon** | Velg lydfil, farge, label per pad |
Eksempler på standard-pads: jingle/intro, applaus, latter, dramatisk pause,
"breaking news"-sting, rim shot, sad trombone, airhorn.
### 3.4 Delt mixer-kontroll (flerbruker)
Alle deltakere i rommet kan se og bruke mixeren samtidig. Mixer-state
synkroniseres i sanntid via SpacetimeDB, slik at volumendringer, mutes,
effekttogles og pad-avspilling reflekteres hos alle klienter umiddelbart.
| Element | Synkronisering |
|---|---|
| **Volumslider** | STDB: `MixerChannel`-tabell med `gain`-verdi per kanal |
| **Mute** | STDB: `is_muted` boolean per kanal |
| **Effekt av/på** | STDB: `active_effects` JSON per kanal |
| **Pad-trigger** | LiveKit Data Message (lav latens) |
| **Pad-konfig** | Node metadata (persistent, sjelden endring) |
**Tilgangsnivåer:**
- **Full kontroll** (default): alle deltakere kan justere alle kanaler,
trigge pads og endre effekter. Tilsvarer at alle sitter rundt samme
RødeCaster.
- **Begrenset**: eier/admin kan låse enkeltdeltakere til kun "viewer" —
de ser mixeren live men kan ikke interagere. Bruker eksisterende
rolle-system (owner/admin/member-edges).
**Konflikthåndtering:** Last-write-wins. Volumslidere er kontinuerlige
verdier som oppdateres via STDB-reducers. Ved samtidig endring av samme
kanal vinner siste skriving — i praksis uproblematisk fordi endringer
er visuelt synlige for alle og deltakerne koordinerer naturlig.
**SpacetimeDB-tabeller:**
```rust
#[spacetimedb::table(accessor = mixer_channel, public)]
pub struct MixerChannel {
#[primary_key]
pub id: String, // "{room_id}:{target_user_id}"
pub room_id: String, // "communication_{node_uuid}"
pub target_user_id: String, // hvem kanalen tilhører
pub gain: f64, // 0.01.5
pub is_muted: bool,
pub active_effects: String, // JSON: {"fat_bottom": true, "robot": false, ...}
pub role: String, // "editor" | "viewer" — tilgangskontroll per kanal
pub updated_by: String, // hvem som sist endret
pub updated_at: Timestamp,
}
```
**STDB Reducers:**
- `create_mixer_channel(room_id, target_user_id, updated_by)` — idempotent opprettelse
- `set_gain(room_id, target_user_id, gain, updated_by)` — clamped 0.01.5, viewer-sjekk
- `set_mute(room_id, target_user_id, is_muted, updated_by)` — viewer-sjekk
- `toggle_effect(room_id, target_user_id, effect_name, updated_by)` — JSON-toggle
- `delete_mixer_channel(room_id, target_user_id)` — opprydding ved disconnect
- `set_mixer_role(room_id, target_user_id, role, updated_by)` — sett editor/viewer
Mixer-kanaler ryddes automatisk ved `close_live_room` og `clear_all`.
### 3.5 Master-seksjon
| Element | Funksjon |
|---|---|
| **Master fader** | Samlet utgangsnivå |
| **Master mute** | Demper all lyd |
| **Master VU** | Stereo nivåmeter for utgangen |
## 4. Teknisk arkitektur
### 4.1 Web Audio-graf
```
┌─────────────────────────────────┐
│ AudioContext │
│ │
Remote Track 1 ──→ MediaStreamSource → EffektKjede → GainNode ─┐
Remote Track 2 ──→ MediaStreamSource → EffektKjede → GainNode ─┤
Lokal mikrofon ──→ MediaStreamSource → EffektKjede → GainNode ─┼→ MasterGain → destination
Sound Pad ──→ AudioBufferSource ─────────────→ GainNode ─┘
│ │
└─────────────────────────────────┘
```
### 4.2 LiveKit-integrasjon
- **Innkommende lyd:** Deaktiver LiveKit sin auto-attach av `<audio>`-elementer.
Hent `MediaStreamTrack` fra remote participant, opprett
`MediaStreamSourceNode`, rut gjennom Web Audio-kjeden.
- **Utgående lyd:** Mikrofon → Web Audio-kjede → `MediaStreamAudioDestinationNode`
→ publiser den prosesserte tracken via LiveKit.
- **Sound pads over nett:** Send LiveKit Data Message (reliable) med pad-ID
og tidsstempel. Alle klienter trigge samme pad lokalt. Akseptabel synk: <50ms.
### 4.3 Stemmeeffekter (AudioWorklet)
Pitch-shifting (monsterstemme) krever en `AudioWorkletProcessor` som kjører
lyd-tråden:
- **Algoritme:** Phase vocoder med identity phase locking
- **Bibliotek:** Vurder `phaze` eller `SoundTouchJS` AudioWorklet
- **Latens:** ~2040ms avhengig av FFT-vindu (10242048 samples ved 48kHz)
- **Parametere:** Pitch-faktor (0.5 = oktav ned, 0.7 = monster, 1.0 = normal)
Robot-stemme bruker ring-modulasjon ingen AudioWorklet nødvendig:
- `OscillatorNode` (sinus, 50200Hz) kobles til `GainNode.gain` AudioParam
- Stemmekilden kobles til samme `GainNode`
- Resultatet er metallisk, Dalek-aktig stemme
### 4.4 Pad-konfigurasjon (lagring)
Sound pads lagres i samlingsnoden sin metadata:
```json
{
"traits": ["recording", "mixer"],
"mixer": {
"pads": [
{ "label": "Jingle", "cas_hash": "abc123...", "color": "#FF6B6B" },
{ "label": "Applaus", "cas_hash": "def456...", "color": "#4ECDC4" }
]
}
}
```
## 5. Avhengigheter
| Avhengighet | Type | Lisens | Formål |
|---|---|---|---|
| `livekit-client` | npm | Apache-2.0 | WebRTC-klient for LiveKit |
| Web Audio API | nettleser | | All lydprosessering (innebygd) |
| CAS | eksisterende | | Lagring av pad-lydfiler |
**Ingen betalte tjenester.** All lydprosessering skjer lokalt i nettleseren
via Web Audio API. Stemmeeffekter (robot, monster) og EQ (fat bottom, exciter,
sparkle) bygges med innebygde Web Audio-noder og en egenutviklet
`AudioWorkletProcessor` for pitch shifting. Phase vocoder-algoritmen er
veldokumentert og implementeres direkte ingen tredjepartslisens nødvendig.
## 6. Node/trait-integrasjon
Lydmixeren aktiveres via `mixer`-traitet en samlings-node. Krever at
`recording`-traitet også er aktivt (LiveKit-avhengighet).
- **Trait:** `mixer` (kategori: Lyd & video)
- **Frontend:** MixerTrait-komponent i samlingssiden
- **Backend:** Ingen nye endepunkter bruker eksisterende LiveKit token-generering
og CAS for pad-lydfiler
## 7. Avgrensning
- Mixeren er for **live-bruk**, ikke postproduksjon/redigering
- Effektene prosesseres **lokalt** i nettleseren ikke serveren
- Hver bruker kontrollerer sin egen mixer (personlig mix)
- Pad-synkronisering er best-effort (~50ms) akseptabelt for lydeffekter
- Støtter ikke multitrack-opptak (det er en separat feature)
## 8. Utviklingsfaser
### Fase A: Grunnleggende mixer (MVP)
- [x] `livekit-client` integrasjon i frontend (koble til rom, vise deltakere)
- [x] Web Audio-graf: MediaStreamSource per remote track GainNode destination
- [x] Kanalstripe-UI: volumslider + mute-knapp per deltaker
- [x] VU-meter (AnalyserNode canvas/CSS)
- [x] Master fader og master mute
### Fase B: Delt mixer-kontroll
- [x] SpacetimeDB: `MixerChannel`-tabell + reducers (set_gain, set_mute, toggle_effect, set_mixer_role)
- [x] Frontend abonnerer mixer-state, oppdaterer Web Audio-graf ved endringer
- [x] Visuell feedback: alle ser sliders bevege seg i sanntid
- [x] Tilgangskontroll: eier/admin kan sette deltaker til "viewer" (kun observere)
### Fase C: Sound Pads
- [ ] Pad-grid UI (4×2 grid med fargede knapper)
- [ ] Last lydfiler fra CAS AudioBuffer
- [ ] Avspilling ved trykk (AudioBufferSourceNode)
- [ ] Pad-konfigurasjon: velg lydfil, farge, label (lagres i node metadata)
- [ ] LiveKit Data Message for synkronisert avspilling tvers av deltakere
### Fase D: Lydbehandling (EQ)
- [ ] Fat bottom (lowshelf filter)
- [ ] Sparkle (highshelf filter)
- [ ] Exciter (WaveShaperNode + filter)
- [ ] Per-kanal av/på-toggles for hver effekt (synkronisert via STDB)
- [ ] Preset-konfigurasjon (f.eks. "Podcast-stemme", "Radio-stemme")
### Fase E: Stemmeeffekter
- [ ] Robotstemme (ring-modulasjon med OscillatorNode)
- [ ] Monsterstemme (pitch shift via egenutviklet AudioWorklet)
- [ ] Effektvelger-UI per kanal
- [ ] Parameterjustering (pitch-faktor, oscillator-frekvens)
## 9. Instruks for Claude Code
- Lydmixeren er **ren frontend** ingen nye Rust-endepunkter nødvendig
- Bruk Web Audio API, IKKE `<audio>`-elementer for prosessering
- AudioWorklet for pitch shifting aldri ScriptProcessorNode (deprecated)
- `AudioContext` opprettes fra brukergest (klikk/tap) nettleserkrav
- Pad-lydfiler lagres i CAS via eksisterende `upload_media` intention
- Pad-konfigurasjon i samlingsnoden sin `metadata.mixer`-nøkkel
- Stemmeeffekter prosesseres lokalt ikke send prosessert lyd over LiveKit
med mindre brukeren eksplisitt vil at andre skal høre effekten