import type { HelixVoiceMicMode, HelixVoiceSettings, HelixVoiceState } from './protocol.js'; export interface VoiceHost { isInitialized(): boolean; getToken(): string | null; getWorldId(): string | null; /** The joined Colyseus room instance id (multiplayer.currentRoomId()) — null before joinRoom. */ getRoomId(): string | null; /** API-base resolution shared with the multiplayer module (configure() pin, else the token's iss). */ resolveApiBase(token: string | null): string | null; /** Shell publish hook (helix:voice-state): every real change, null on leave/teardown. */ publishVoiceState?(state: HelixVoiceState | null): void; /** Shell publish hook (helix:voice-level): live mic meter, ~10 Hz while captured, 0 on release. */ publishVoiceLevel?(level: number): void; } export interface JoinVoiceOptions { /** Override the paired Colyseus room id. Default: the currently joined multiplayer room. */ roomId?: string; /** Override the platform API base URL (same semantics as JoinRoomOptions.apiBaseUrl). */ apiBaseUrl?: string; /** Same-origin reservation adapter route for a non-World runtime. */ tokenPath?: string; /** Push-to-talk key (KeyboardEvent.key, compared lowercase). Default 'n', matching FiveM's * standard push-to-talk control while B remains available for character pointing. */ pttKey?: string; /** Initial exclusive voice channel (equivalent to calling setChannel before join). */ channel?: string; } /** A world-space position/direction for spatial voice (three.js and WebAudio share right-handed axes). */ export type HelixVoiceVec3 = { x: number; y: number; z: number; }; /** * Fixed-size, receiver-local speech features. These are derived from a subscribed playback track, * never serialized, and intentionally contain no recoverable PCM or arbitrary channel data. */ export interface HelixVoiceAudioFeatures { /** Root-mean-square energy, normalized to 0..1. */ rms: number; /** Time-domain zero-crossing rate, normalized to 0..1. */ zeroCrossingRate: number; /** Mean spectral energy in fixed speech bands: <350, 350-700, 700-1400, 1400-2800, 2800-5600 Hz. */ bands: readonly [number, number, number, number, number]; } /** Platform default for KeyboardEvent.key while push-to-talk is active. */ export declare const DEFAULT_PUSH_TO_TALK_KEY = "n"; export declare class HelixVoice { private readonly host; private room; private events; private connected; private micMode; private captured; private capturePromise; private permission; private requestedDeviceId; private requestedDeviceLabel; private activeDeviceId; private audioCtx; private micStream; private micSource; private volumeGain; private compressor; private analyser; private analyserBuf; private analyserSpectrum; private gateGain; private muteGain; private levelTimer; private levelTick; private micLevel; private gateOpenUntil; private micVolume; private autoSensitivity; private sensitivity; private outputVolume; private participantBase; private channelId; private pairing; private pendingJoin; private joinSeq; private mixCtx; private spatialTracks; private panners; private remoteAnalysers; private remoteLevelListeners; private remoteFeatureListeners; private localLevelListeners; private localFeatureListeners; private localWasAudible; private remoteLevelTimer; private positions; private listenerPose; private devices; private muted; private speaking; private audioBlocked; private error; private available; private pttKey; private pttHeld; private stateSig; private audioRoot; private unlockInstalled; private speakingListeners; private participantListeners; constructor(host: VoiceHost); join(options?: JoinVoiceOptions): Promise; private connectVoice; leave(): Promise; detachForRoomSwitch(): Promise<(() => Promise) | null>; /** The shell-owned settings push (helix:voice-settings) — also callable directly by a world. */ applySettings(settings: HelixVoiceSettings): Promise; /** Outgoing mic gain (0..2, Discord-style 0–200%). Applies live when captured. */ setMicVolume(volume: number): void; /** Incoming voice master gain (0..2, Discord-style 0–200%). Reapplies to every participant live. */ setOutputVolume(volume: number): void; setMicMode(mode: HelixVoiceMicMode): Promise; getMicMode(): HelixVoiceMicMode; setPttDown(down: boolean): void; private captureForPtt; setDevice(deviceId: string, label?: string): Promise; private swapSource; private reconcileDeviceLabel; setChannel(channelId: string | null): Promise; /** The local player's current exclusive voice channel (null = the ambient space). */ channel(): string | null; /** The channel a remote participant is in (null = ambient; engine render policy reads this). */ channelOf(userId: string): string | null; setPlayerMuted(userId: string, muted: boolean): void; mutedUserIds(): string[]; /** Playback gain for one remote player (engine proximity hook; 0 = silent, 1 = as-sent). */ setParticipantVolume(userId: string, volume: number): void; /** * Spatialize one remote player's voice: heard from `position` (world coords) relative to the listener * pose. Direction ONLY — loudness still flows through setParticipantVolume × outputVolume. null returns * them to center-panned render (the engine passes null for channel — radio/phone — voices). Engine * proximity hook, called per-frame; silently no-ops where the client audio hook is unavailable. */ setParticipantPosition(userId: string, position: HelixVoiceVec3 | null): void; /** The listener (camera) pose driving spatial voice. Engine hook, called per-frame. */ setListenerPose(position: HelixVoiceVec3, forward: HelixVoiceVec3, up?: HelixVoiceVec3): void; /** Connected remote voice participants (platform userIds). */ participantIds(): string[]; /** Currently speaking userIds (muted-by-me players never appear). Fires on every change. */ onSpeakingChanged(cb: (userIds: string[]) => void): () => void; /** Remote participant set changes (join/leave). Fires with the current userId list. */ onParticipantsChanged(cb: (userIds: string[]) => void): () => void; /** * Receiver-local audio energy for presentation such as avatar jaw motion. The callback receives * platform userId plus a clamped 0..1 RMS value at at most about 30 Hz. It is intentionally not exposed * to the shell protocol or any network transport. Analysis exists only while at least one listener * is registered and degrades to silence when LiveKit's WebAudio plugin hook is unavailable. */ onParticipantAudioLevel(cb: (userId: string, level: number) => void): () => void; /** * Receiver-local, bounded speech features for replaceable lip-sync solvers. Analysis runs at at * most about 30 Hz for at most 32 audible tracks. The five fixed bands and two scalars are ephemeral; * raw samples are neither exposed nor retained after each tick. */ onParticipantAudioFeatures(cb: (userId: string, features: HelixVoiceAudioFeatures) => void): () => void; /** * The LOCAL speaker's own audio energy, for driving their own avatar's mouth. Nobody plays their * own voice back, so `onParticipantAudioLevel` structurally cannot cover this seat. * * Sampled off the microphone analyser that already drives the meter and the sensitivity gate — no * second analyser, no extra timer — and reported only while the room can actually hear you, so a * muted or un-held mic leaves the mouth closed. Never networked; nothing is retained. */ onLocalAudioLevel(cb: (level: number) => void): () => void; /** * The local speaker's own speech features, in the same fixed shape and units as the receiver-local * ones, so one solver serves both seats. Preferred over `onLocalAudioLevel` when consumed. */ onLocalAudioFeatures(cb: (features: HelixVoiceAudioFeatures) => void): () => void; startAudio(): Promise; private requestVoiceToken; private wireRoomEvents; private ensureMic; private captureMic; private setMicEnabled; private shouldTransmit; private startLevelLoop; /** * The local speaker's own face signal, off the same analyser the meter and the gate already use. * * Gated on what the room can actually HEAR — the publish mute (mode/PTT) times the sensitivity gate * — so a muted or un-held microphone leaves the avatar's mouth closed instead of mouthing silently * at everyone. The transition to inaudible emits one explicit silent frame and then stops, so a * consumer settles at rest rather than freezing on the last syllable. */ private emitLocalAudio; private emitLocalSilence; private applyGate; private applyMicMode; private sameChannel; private participantAudible; private applyAudibility; private applyVolumes; private findParticipant; private ensurePanner; private applyTrackPlugins; private ensureRemoteAnalyser; private installRemoteAnalysers; private startRemoteLevelLoop; private emitRemoteLevel; private emitRemoteFeatures; private stopRemoteLevelAnalysis; private applyParticipantPosition; private applyListenerPose; private refreshDevices; private attachInputListeners; private detachInputListeners; private onKeyDown; private onKeyUp; private releasePtt; private onVisibility; private ensureAudioRoot; private installUnlock; private micSuspended; private resumeAudio; private kickContext; private visibleSpeaking; private emitSpeaking; private emitParticipants; private publishState; private teardown; }