@cloak-software/bot-sdk for voice. For the concepts behind these, start at Voice overview.
The interface behind
client.voiceKeys is called VoiceKeyAccess in the source and is not exported. Do not write an import for it. Reach the lane through client.voiceKeys.Client surface
These live onClient, not in the voice modules. Documented in full on Joining voice.
client.guild(serverId) binds joinVoice(channelId, opts?) and refreshVoiceRoster().
voiceConnectionReporter
VoiceMediaOptions.connectionReporter. The returned function sends the bot’s own media connection state to the backend (0 up, 1 reconnecting); on a current backend the 0 a server-voice session sends before it publishes is what admits the microphone. It rides the send chain, is a no-op when the bot is not in voice or is in a DM call, and never throws or rejects. Sending it twice is harmless. See Microphone admission.
Events: voiceJoin, voiceLeave, voiceStateUpdate, voiceConnectionState, voiceDisconnect, voiceKeyUpdate. See Events.
livekitUrlFor
wss: (or ws: when the transport itself is insecure). Returns '' for an unparseable URL. This is what VoiceConnection.url defaults to. Override it with ClientOptions.livekitUrl.
The SDK never connects to that URL. It is passed through for the media layer.
Types
VoiceConnection
VoiceParticipant
serverMuted and serverDeafened are best-effort in the initial roster snapshot: the backend appends them from a separate query whose row order is not guaranteed to line up. The delta events correct them.
VoiceKey
VoiceKeyScope
VoiceStateFlag
state field on voiceStateUpdate. The last two are deprecated and never change.
VoiceMediaSession
Promise<VoiceMediaSession>
Connects, installs frame keys, reports the connection up through
connectionReporter and waits for the microphone admission, then publishes one microphone track. Fails closed on every precondition. Full walkthrough on Publishing and receiving audio.string
The identity the SFU knows this bot by. Asserted equal to the normalized bot id at attach time.
'participant' | 'room'
The scope the key arrived with. Read-only.
readonly string[]
Every identity this session has filed a key for. In participant scope this is exactly one, the bot’s own.
Promise<void>
Re-pin after a
voiceKeyUpdate. Detaches the session and throws when the new scope differs from the attached one.Promise<void>
Stops the player, unpublishes, and disconnects. Idempotent. Ends every receive iterable with a normal return.
this
Room scope only. Fires once per subscribed remote audio track with a
ParticipantAudio. A re-subscribe for the same identity is a new event with a new iterable. Throws in participant scope: that session holds only its own frame key and can decrypt nobody. off() removes a listener.AsyncIterable<ParticipantFrame>
Room scope only. Every live participant’s frames merged into one iterable, each tagged with its identity. Later arrivals join, leavers drop out, and any number of concurrent consumers each see every frame. Ends with a normal return on
detach(). return() (a break) releases every underlying stream this consumer held. Throws in participant scope.Promise<void>
Room scope only. Subscribe or unsubscribe this bot from one identity’s audio tracks.
false ends that identity’s live iterable immediately with a normal return; a later true produces a fresh audio event. Rejects when no participant with that identity is in the room. Throws in participant scope.voice_listen holds only its own frame key and is cryptographically unable to decrypt any member; a bot granted voice_listen can decrypt everyone in the channel, by design. See Receiving audio.
VoiceMediaOptions
receiveSampleRate and receiveChannels set the format of every received PcmFrame. They are fixed at attach, and the engine does the resampling and channel mixing.
connectionReporter is not optional in practice. Pass client.voiceConnectionReporter(). attach() calls it with 0 before the publish, once the identity and key checks have passed, then waits for the microphone admission by polling the local participant’s canPublish permission every 100 ms and re-sending the report every second, up to admissionTimeoutMs. It also calls it with 1 then 0 around an engine reconnect, and never from detach(). Without it there is no wait and a server-voice credential stays microphone-clamped on a current backend, so the publish is silent, with a warning only under debug.
admissionTimeoutMs bounds that wait. Default 5000. On timeout attach() still publishes once, because an older single-phase backend never flips the flag but its credential already allows publishing; if that publish rejects, attach() fails closed with an error naming the two-phase admission, connectionReporter, and admissionTimeoutMs. Ignored without a connectionReporter. See Microphone admission.
VoiceMediaSessionEvents
PcmFrame
receiveSampleRate and receiveChannels. data is interleaved signed 16-bit, samplesPerChannel * channels long, and a copy out of the engine, so it is safe to keep after the next frame arrives. Consumers sharing a session see the same data: read it or convert it, never write into it.
ParticipantAudio
audio event carries. identity is the participant’s normalized Cloak user id. frames ends with a normal return when that track is unsubscribed or the session detaches. A muted track yields nothing and stays open. More than one consumer may iterate it, and each sees every frame. A consumer slower than real time is capped at 500 queued frames before the oldest is dropped.
ParticipantFrame
session.audio() yields.
VoiceKeyValue
VoiceKey satisfies it structurally.
VoiceFrameKeySource
client.voiceKeys satisfies it. It exists so the publish path never sees a scope.
VoicePlayer
session.player. Constructing your own is only for testing.
Promise<void>
Pumps a resource to completion. Resolves when the resource ends or
stop() is called. Rejects when the source throws, including a non-zero decoder exit. Throws immediately if a resource is already playing.void
Ends the current resource. The in-flight frame is still awaited.
VoicePlayerEvents
VoicePlayerDeps
Audio resources
AudioResource
close() is called once when the pump ends for any reason, including stop() and errors.
pcmChunks
pcmFrameBytes
PcmFrame as interleaved little-endian signed 16-bit bytes: the inverse of pcmChunks, and the layout ffmpeg -f s16le, a WAV data chunk, and raw-PCM speech-to-text inputs expect. Pure and endian-fixed. It copies exactly samplesPerChannel * channels samples and never aliases frame.data.
This is the whole of what the SDK does with received audio. There is no recorder, no file sink, and no transcription helper, on purpose. See Receiving audio.
ffmpegResource
CloakEnvironmentError at play time when the decoder cannot start or exits non-zero.
ytdlpResource
ffmpegResource.
--no-playlist is always passed.
resolveMediaUrl
DecoderChild, DecoderSpawn, ExtractorRun
DecoderChild is a narrow structural view of a spawned process. Not version-stable.
Runtime
probeVoiceRuntime
voiceEnvError
Runtime constants
Encryption
These are a cross-platform contract shared with the Cloak desktop, web, and mobile clients. Treat them like a wire format, not like configuration.deriveParticipantVoiceKey
roomKey is exactly 32 bytes. Derives downward only: there is no inverse, and that absence is what makes a publish-only bot structurally deaf. Pinned by a golden test vector shared across repos.
voiceKeyMaterial
voiceKeyProviderOptions
Constants
Next
Voice overview
What a voice bot can and cannot do.
Publishing and receiving audio
The media session in practice, both directions.
Voice runtime
Engine setup and the symptom-to-cause table.
Client
The rest of the client surface.