dot-voice is voice chat: capture, a pluggable codec, a jitter buffer per
speaker, and routing decided on the server.
Requires dot-core. It works with
dot-moderation and
dot-server without importing either.
| Capture | Push-to-talk, or voice activation with hysteresis and a hangover so words are not clipped. |
| Codecs | pcm16 (exact) and adpcm (a quarter of the bandwidth). Register your own and nothing else changes. |
| Wire | A twelve-byte header carrying a full 32-bit speaker peer id, sequence, frame length, channel and codec. Every field bounds-checked, because a voice packet arrives before its sender has done anything trustworthy. |
| Playback | A jitter buffer per speaker that reorders, drops duplicates, caps its own growth, and fades over a lost frame rather than cutting to silence. |
| Routing | All, team, proximity, or a filter you write. Decided on the server. |
| Muting | A moderator’s mute is enforced on the server. A player’s own mute list is applied on their machine. Those are different features and it matters. |
Three things worth knowing first
Nothing opens a microphone on its own. Not on ready, not because a config
flag was true in an exported default. start_capture() is an explicit call you
make after the player has agreed, and it returns a failure you can put on
screen.
The devices are behind an interface, which is why there is a test suite at
all. DotVoiceSource and DotVoiceSink have a real implementation and a
buffer implementation, so the suite writes samples in one end and reads audio
out of the other with no audio device in the process — the gate, the codec, the
packet, the router and the jitter buffer all run the code a player runs.
Both ends must agree on the format. Sample rate, frame length and codec.
DotVoiceConfig.format_fingerprint() is one number to exchange at handshake,
because a mismatch is not an error — it is noise, or speech at the wrong speed.
The server’s switch and the game’s channel
sv_voice_enable turns voice off on the router, which then relays nothing (quietly, and counted), and sv_voice_mode forces a channel over the game’s own. Register them with DotVoiceRouter.bind_cvars(add, FLAG_REPLICATED) on the authoritative router; clients call DotVoiceManager.follow_cvars(link) and only hide their key, because the enforcement is the server’s. If the shell has not published its link yet, follow_cvars(null) waits for it.
Every packet is relayed on the router’s default_channel, never the one the client wrote. That field was once never read, so every game that chose team or proximity voice relayed to everybody. honour_packet_channel opts back into the packet’s field.
Proximity, gag and mute
Proximity voice is a gain the server works out per listener and carries in the packet’s flags byte (the version stays 2); a packet past the radius is not sent at all, and the router resets any gain bits a client wrote. The cvars are sv_voice_proximity_range, _min, _falloff, _occlusion, _occlusion_gain, _team_bypass and _global_bypass. Occlusion stays off until the game sets occlusion_fn and turns the cvar on. DotVoiceProximity holds the falloff curves and a ray occluder.
DotVoiceLookMute lets a player look at a speaker: a tap on voice_gag gags them, a double tap on voice_mute mutes them. Both are rebindable and local to that player’s machine, kept apart from local_mutes so a chat sm and a look-mute do not undo each other. The server only says whether they are allowed: sv_voice_gag_allowed, sv_voice_mute_allowed and sv_voice_gag_default.
The microphone opens on the first press
DotVoiceManager.update_talk(held) opens the microphone on the first press of the talk key (TALK_ACTION, dot_voice_talk, V by default). That press is the player’s consent, and it is the user gesture a browser insists on before it will grant a microphone. A voice build in a browser needs the project’s audio/driver/enable_input on, and for a delivered game it is the client shell’s setting that counts. Positional playback needs speaker_position_fn; without it voices are flat rather than sitting at the origin.
Where a game plugs in
| To change | Where |
|---|---|
| Where bytes go | DotVoiceManager.send_fn, DotVoiceRouter.send_fn |
| Where audio comes from | DotVoiceSource subclass |
| Where audio goes | DotVoiceSink subclass, or DotVoiceManager.sink_factory |
| The codec | DotVoiceCodec subclass plus DotVoiceCodec.register |
| Who hears whom | DotVoiceRouter.team_fn / position_fn / listener_filter |
| Whether a listener in proximity range can actually hear — a wall, a closed door | DotVoiceRouter.can_hear_fn(listener, speaker, listener_at, speaker_at), asked only of listeners already inside the range |
| Who may be heard at all | Anything with is_voice_muted(peer) registered as dot_mute_source |
| Feel and bandwidth | DotVoiceConfig |
- DotVoiceCodec subclass
id(),encode(),decode(),bytes_for(),is_stateful(),reset(),duplicate_codec(). ThenDotVoiceCodec.register(...). This is the seam an Opus GDExtension drops into — Godot exposes no Opus encoder to GDScript, and the two shipped codecs are what the language can do.- DotVoiceSource / DotVoiceSink subclass
start,stop,has_frame/can_accept,read_frame/write_frame,is_running,describe.- Signals
talking_changed,speaker_changed,frame_sent,speech_relayed,speech_refused,capture_failed. Those plusinput_level()are what a voice HUD needs; drawing it is your game’s ordot-ui’s.
What is deliberately not here
- Opus. The codec interface exists so a GDExtension can be dropped in.
- Echo cancellation and noise suppression. Both are real signal processing and belong in a GDExtension. Push-to-talk is the reason this is usable without them.
- A voice UI.
- A transport.
send_fntakes bytes, exactly as indot-net. - Proof that the microphone works. The device layer needs an audio device, so the headless suite cannot cover it and says so rather than leaving you to find out.
`AudioServer` reports a working sound card when there is none
In a headless run get_mix_rate() is 44100, get_input_device_list() is
["Default"] and get_output_latency() is 0.0. Only get_driver_name() says
"Dummy". A capability check built on any of the others passes on a machine
with no audio at all, and the symptom is a capture returning silence for ever
with nothing reporting a problem.
