Implementerer lydpads (inspirert av RødeCaster Pro II) i mixeren: - mixer.ts: Nytt pad-system med AudioBuffer-caching, GainNode per pad, og one-shot AudioBufferSourceNode-avspilling. Funksjoner for load, play, stop, og gain-kontroll. - livekit.ts: Data message-støtte (sendDataMessage, onDataMessage) for synkronisert pad-avspilling på tvers av LiveKit-deltakere. Bruker reliable delivery med topic-filtrering. - SoundPadGrid.svelte: 4×2 responsivt pad-grid med fargede knapper. Forhåndslaster lydfiler fra CAS til AudioBuffer. Visuell feedback ved avspilling (scale-animasjon). Konfigurasjonsmodus for å sette label, farge og laste opp lydfil per pad. Pad-konfig lagres i metadata.mixer.pads på samlingsnoden. - MixerTrait.svelte: Integrerer SoundPadGrid mellom kanalstriper og master-seksjon. Sender isViewer-prop for tilgangskontroll. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
249 lines
11 KiB
Markdown
249 lines
11 KiB
Markdown
# 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 0–150% | `GainNode` (0.0–1.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, +6–12dB) | `BiquadFilterNode` lowshelf |
|
||
| **Exciter** | Harmonisk tilstedeværelse (3–5kHz) | `WaveShaperNode` + highshelf |
|
||
| **Sparkle** | Høyfrekvent luft (~10–16kHz, +3–6dB) | `BiquadFilterNode` highshelf |
|
||
| **Monsterstemme** | Pitch ned 4–8 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.0–1.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.0–1.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
|
||
på lyd-tråden:
|
||
- **Algoritme:** Phase vocoder med identity phase locking
|
||
- **Bibliotek:** Vurder `phaze` eller `SoundTouchJS` AudioWorklet
|
||
- **Latens:** ~20–40ms avhengig av FFT-vindu (1024–2048 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, 50–200Hz) 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 på 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 på 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 på 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
|
||
- [x] Pad-grid UI (4×2 grid med fargede knapper)
|
||
- [x] Last lydfiler fra CAS → AudioBuffer
|
||
- [x] Avspilling ved trykk (AudioBufferSourceNode)
|
||
- [x] Pad-konfigurasjon: velg lydfil, farge, label (lagres i node metadata)
|
||
- [x] LiveKit Data Message for synkronisert avspilling på 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` må 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
|