first commit
This commit is contained in:
@@ -0,0 +1,83 @@
|
||||
# `game-audio.types`
|
||||
|
||||
## Purpose
|
||||
|
||||
TypeScript-only type definitions for `GameAudioPlugin`'s vector math types, registry entries, configuration, and preset payload. Co-located with the implementation so the public type surface stays aligned with the Zod validator in `schemas/schemas.ts`.
|
||||
|
||||
The interfaces here form the structural contract that the browser-engine fast path (`createAudioNode`) will read in a future revision — keeping them stable here is essential for that integration.
|
||||
|
||||
## Features
|
||||
|
||||
- `Vector3` — `{ x, y, z }` finite-number 3-vector.
|
||||
- `ListenerOrientation` — `{ forward: Vector3, up: Vector3 }`.
|
||||
- `SpatialAudioSource` — `{ id, position, velocity, occlusion }`.
|
||||
- `VoiceChatParticipant` — `{ id, position }`.
|
||||
- `ProximityVoiceChatConfig` — `{ minDistance, maxDistance, rolloffFactor }`.
|
||||
- `MusicLayer` — `{ name, buffer, currentGain, targetGain }`.
|
||||
- `GameState` — textual enum `"exploration" | "combat" | "victory" | "menu"`.
|
||||
- `GameVoiceEffect` — `{ id, pitchShift, distortion }`.
|
||||
- `GameAudioPluginOptions` — optional listener + voice-chat options.
|
||||
- `GameAudioPresetData` — full preset payload.
|
||||
|
||||
## Usage: Basic Example
|
||||
|
||||
```ts
|
||||
import type {
|
||||
GameAudioPluginOptions,
|
||||
Vector3,
|
||||
} from "@suaveplan/audio-plugins-fx/game";
|
||||
|
||||
const position: Vector3 = { x: 1, y: 0, z: 0 };
|
||||
const opts: GameAudioPluginOptions = { listenerPosition: position };
|
||||
```
|
||||
|
||||
## Usage: Advanced Example
|
||||
|
||||
```ts
|
||||
import type {
|
||||
GameState,
|
||||
MusicLayer,
|
||||
ProximityVoiceChatConfig,
|
||||
Vector3,
|
||||
} from "@suaveplan/audio-plugins-fx/game";
|
||||
|
||||
function magnitude(v: Vector3): number {
|
||||
return Math.sqrt(v.x * v.x + v.y * v.y + v.z * v.z);
|
||||
}
|
||||
|
||||
const states: ReadonlyArray<GameState> = [
|
||||
"exploration",
|
||||
"combat",
|
||||
"victory",
|
||||
"menu",
|
||||
];
|
||||
|
||||
function describeLayer(layer: MusicLayer): string {
|
||||
return `${layer.name} @ ${(layer.currentGain * 100).toFixed(0)}%`;
|
||||
}
|
||||
|
||||
const tight: ProximityVoiceChatConfig = {
|
||||
minDistance: 1,
|
||||
maxDistance: 20,
|
||||
rolloffFactor: 2,
|
||||
};
|
||||
```
|
||||
|
||||
## API Reference
|
||||
|
||||
- `Vector3` — `{ readonly x, y, z: number }`
|
||||
- `ListenerOrientation` — `{ readonly forward, up: Vector3 }`
|
||||
- `SpatialAudioSource` — `{ readonly id, position, velocity, occlusion }`
|
||||
- `VoiceChatParticipant` — `{ readonly id, position }`
|
||||
- `ProximityVoiceChatConfig` — `{ readonly minDistance, maxDistance, rolloffFactor }`
|
||||
- `MusicLayer` — `{ readonly name, buffer, currentGain, targetGain }`
|
||||
- `GameState = "exploration" | "combat" | "victory" | "menu"`
|
||||
- `GameVoiceEffect` — `{ readonly id, pitchShift, distortion }`
|
||||
- `GameAudioPluginOptions` — `{ readonly listenerPosition?, listenerOrientation?, voiceChat? }`
|
||||
- `GameAudioPresetData` — `{ listenerPosition, listenerOrientation, voiceChat: … | null, gameState }`
|
||||
|
||||
## Implementation Notes
|
||||
|
||||
`GameAudioPresetData.voiceChat` is `ProximityVoiceChatConfig | null` (not `?:`) because a disabled voice-chat state is a legitimate value that the preset must round-trip. Using `?` would let the field be omitted, which is ambiguous between "no voice chat configured" and "voice chat at the default". The explicit `null` makes the disabled state unambiguous.
|
||||
|
||||
`SpatialAudioSource` and `MusicLayer` are returned as read-only views from the plugin's accessors; the live state inside the plugin is mutable (it has to be, for per-block gain updates), but consumers see snapshots that they cannot mutate to corrupt the registry. The cost is one shallow object copy per accessor call — negligible compared to the per-block work the plugin does.
|
||||
@@ -0,0 +1,83 @@
|
||||
# `game-audio`
|
||||
|
||||
<!-- begin:auto-docs -->
|
||||
_(no submodules)_
|
||||
<!-- end:auto-docs -->
|
||||
|
||||
## 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`.
|
||||
@@ -0,0 +1,72 @@
|
||||
# `game/schemas`
|
||||
|
||||
<!-- begin:auto-docs -->
|
||||
_(no submodules)_
|
||||
<!-- end:auto-docs -->
|
||||
|
||||
## Purpose
|
||||
|
||||
Zod schemas validating the construction-time options, vector types, voice-chat configuration, game-state enum, and preset payload of `GameAudioPlugin`. These run on the public boundary so any malformed object is rejected with `AUDIO_FX.INVALID_OPTIONS` / `AUDIO_FX.INVALID_PRESET` before it can corrupt runtime state.
|
||||
|
||||
Separating schemas from the plugin module lets `index.ts` re-export them so callers can introspect the option surface (for forms, RPC validation, or storybook controls) without importing the audio implementation.
|
||||
|
||||
## Features
|
||||
|
||||
- `Vector3Schema` — `{ x, y, z: finite }`. Reused by every position/velocity/orientation field in the package.
|
||||
- `ListenerOrientationSchema` — `{ forward, up: Vector3 }`.
|
||||
- `ProximityVoiceChatConfigSchema` — `{ minDistance, maxDistance, rolloffFactor }` with documented ranges.
|
||||
- `GameStateSchema` — `z.enum(["exploration", "combat", "victory", "menu"])`.
|
||||
- `GameAudioPluginOptionsSchema` — optional listener position, orientation, and voice-chat config.
|
||||
- `GameAudioPresetDataSchema` — `.strict()` object with all four persistent state fields (voice-chat nullable for the disabled state).
|
||||
|
||||
## Usage: Basic Example
|
||||
|
||||
```ts
|
||||
import { GameAudioPluginOptionsSchema } from "@suaveplan/audio-plugins-fx/game";
|
||||
|
||||
const parsed = GameAudioPluginOptionsSchema.parse({
|
||||
listenerPosition: { x: 0, y: 0, z: 0 },
|
||||
});
|
||||
```
|
||||
|
||||
## Usage: Advanced Example
|
||||
|
||||
```ts
|
||||
import {
|
||||
GameAudioPresetDataSchema,
|
||||
ProximityVoiceChatConfigSchema,
|
||||
Vector3Schema,
|
||||
} from "@suaveplan/audio-plugins-fx/game";
|
||||
|
||||
function isPosition(blob: unknown): boolean {
|
||||
return Vector3Schema.safeParse(blob).success;
|
||||
}
|
||||
|
||||
function isVoiceConfig(blob: unknown): boolean {
|
||||
return ProximityVoiceChatConfigSchema.safeParse(blob).success;
|
||||
}
|
||||
|
||||
function safeApplyPreset(plugin: GameAudioPlugin, payload: unknown) {
|
||||
const parsed = GameAudioPresetDataSchema.safeParse(payload);
|
||||
if (!parsed.success) return false;
|
||||
plugin.applyPreset({ version: 1, parameters: {}, presetData: parsed.data });
|
||||
return true;
|
||||
}
|
||||
```
|
||||
|
||||
## API Reference
|
||||
|
||||
- `Vector3Schema: z.ZodObject<{ x, y, z }>`
|
||||
- `ListenerOrientationSchema: z.ZodObject<{ forward, up }>`
|
||||
- `ProximityVoiceChatConfigSchema: z.ZodObject<{ minDistance, maxDistance, rolloffFactor }>`
|
||||
- `GameStateSchema: z.ZodEnum<["exploration", "combat", "victory", "menu"]>`
|
||||
- `GameAudioPluginOptionsSchema: z.ZodObject<{ listenerPosition?, listenerOrientation?, voiceChat? }>`
|
||||
- `GameAudioPresetDataSchema: z.ZodObject<{ listenerPosition, listenerOrientation, voiceChat, gameState }, "strict">`
|
||||
|
||||
## Implementation Notes
|
||||
|
||||
`Vector3Schema` is exported as a named schema and reused throughout the file. That guarantees every position/velocity/orientation field in the package agrees on the legal vector shape — a finite 3-tuple — and downstream code that wants to validate a vector independently (e.g., a network message carrying position updates) can pull the same schema.
|
||||
|
||||
`GameAudioPresetDataSchema` uses `.strict()` so unknown fields are rejected. `voiceChat` is `.nullable()` rather than `.optional()` because a disabled voice-chat state is a meaningful value the preset must round-trip; making it optional would lose the distinction between "absent" and "explicitly disabled".
|
||||
|
||||
The numeric ranges on `ProximityVoiceChatConfigSchema` match the `SpatialAudioPlugin` bounds (`minDistance` 0.01..1000, `maxDistance` 0.01..100 000, `rolloffFactor` 0..10) so a developer migrating from one plugin to the other can copy values directly without re-scaling.
|
||||
Reference in New Issue
Block a user