# `game-audio` _(no submodules)_ ## Purpose `GameAudioPlugin` is the universal composite plugin for game audio. It maintains three registries — spatial sources, music layers, and proximity voice-chat participants — and exposes a transport-level API for managing each. The plugin is a source (`io.inputs[0].count === 0`, `io.outputs[0].count === 1`); it owns its audio output rather than colouring upstream audio. The plugin extends `AbstractAudioFxPlugin` and runs identically across browser / server / worklet renderers. The audio path is intentionally a silent placeholder — the listener data, source registry, voice attenuation, and music-layer crossfade scheduling are pure bookkeeping that the browser-engine fast path consumes via `createAudioNode()` in a follow-up. The universal block path produces silence by design; consumers route the registry state into their own mixing graph. ## Features - **Spatial sources**: `createSpatialSource`, `updateSpatialSourcePosition`, `applyDopplerEffect`, `applyOcclusion(0..1)`, `cullDistantSources(maxDistance)`, `getActiveSpatialSourceCount`, `getSpatialSource(id)`. - **Proximity voice chat**: `enableProximityVoiceChat(config)`, `disableProximityVoiceChat`, `addVoiceChatParticipant`, `removeVoiceChatParticipant`, `updateVoiceChatParticipantPosition`, `calculateVoiceAttenuation`, `optimizeVoiceChatBandwidth`, `getVoiceChatOptimizationLevel`. - **Music layers**: `addMusicLayer`, `removeMusicLayer`, `getMusicLayers`, `crossfadeMusicLayers(from, to, durationMs)`, `isCrossfading`. - **Voice effects**: `applyGameEffect`, `removeGameEffect`, `getCurrentEffect`, `blendToEffect(effect, durationMs)`, `isBlending`. - **Listener**: `setListenerPosition`, `setListenerOrientation`, `getListenerPosition`, `getListenerOrientation`. - **Game state**: `setGameState("exploration" | "combat" | "victory" | "menu")`, `getGameState`. Voice attenuation uses the same inverse-distance model as `SpatialAudioPlugin`: `gain = minDistance / (minDistance + rolloffFactor · (clamp(d, minDistance, maxDistance) − minDistance))`. Distance grows ⇒ gain decreases monotonically, exactly as the spec requires. ## Usage: Basic Example ```ts import { GameAudioPlugin } from "@suaveplan/audio-plugins-fx/game"; const game = new GameAudioPlugin({ listenerPosition: { x: 0, y: 0, z: 0 }, voiceChat: { minDistance: 1, maxDistance: 50, rolloffFactor: 1 }, }); chain.add(game); ``` ## Usage: Advanced Example ```ts import { GameAudioPlugin } from "@suaveplan/audio-plugins-fx/game"; const game = new GameAudioPlugin({ voiceChat: { minDistance: 1, maxDistance: 100, rolloffFactor: 2 } }); await game.setup( { sampleRate: 48_000, channels: 1, blockSize: 512 }, ctx ); // Register a footstep emitter and a voice-chat peer. game.createSpatialSource("footstep", { x: 5, y: 0, z: 0 }); game.addVoiceChatParticipant("teammate", { x: 10, y: 0, z: 0 }); // Adaptive music: crossfade from exploration to combat over 1s. game.addMusicLayer("exploration", explorationBuffer); game.addMusicLayer("combat", combatBuffer); game.crossfadeMusicLayers("exploration", "combat", 1_000); game.setGameState("combat"); // Cull distant sources every frame to keep the voice-chat budget tight. game.optimizeVoiceChatBandwidth(); game.cullDistantSources(80); ``` ## API Reference - `GameAudioPlugin` (class extends `AbstractAudioFxPlugin`) - Construction and lifecycle: `new GameAudioPlugin(options?)`, `setup`, `reset`. - Listener: `setListenerPosition`, `setListenerOrientation`, `getListenerPosition`, `getListenerOrientation`. - Spatial sources: see Features above. - Voice chat: see Features above. - Music layers: see Features above. - Voice effects: see Features above. - Game state: `setGameState`, `getGameState`. ## Implementation Notes The audio path is silent by design. Implementing the full mix of spatial sources + music layers + voice chat into a single mono output stream would require this plugin to embed three sibling-plugin engines (`SpatialAudioPlugin`, `MixerPlugin`, plus a voice-chat router) — which would either duplicate code or pull every plugin into this one's import graph. Instead, the plugin owns the *registry* of game-audio state; the browser engine reads that state via `createAudioNode()` and renders it into a Web Audio graph in a future revision. The universal block path produces silence but advances the internal sample clock so `isCrossfading` / `isBlending` work correctly under any renderer. Crossfade scheduling stores the start sample (`processedSamples` at the time of the call) plus the duration in samples. `isCrossfading` is `elapsed < durationSamples`. The check is exact at the sample level — the test "isCrossfading true immediately; false after enough samples processed" exercises that by processing exactly the configured number of samples. `cullDistantSources` walks the spatial-source map and deletes entries whose distance to the listener exceeds the threshold. The cost is `O(numSources)` per call; for the typical game-audio case (under 100 sources) this is below 1 ms even at 60 fps cull frequency. A spatial-hash variant would amortise this to `O(log n)` but adds bookkeeping that the MVP does not need. `optimizeVoiceChatBandwidth` returns a coarse 0..3 tier rather than per-participant codec parameters because the actual codec selection lives in the transport layer (WebRTC / similar). The plugin's job is to *advise*; the transport applies the advice. Currently the heuristic is just a participant-count threshold (>8 ⇒ level 3, otherwise level 1) — a future revision can layer on signal-strength and ping data. The plugin maintains an internal `processedSamples` counter incremented by `output.length` each `processBlock`. This is the same clock crossfade and effect-blend countdowns reference — using `BlockContext.blockOffsetSamples` directly would not work because the chain may slice or align blocks differently, and the consumer expects clock advancement per actual rendered sample. The counter resets on `setup`/`reset`.