ctx.res.Mic() captures live microphone audio and hands your script either a rolling MicClip or drained packet-ready bytes. The engine owns capture, denoise, and the compact PMIC byte codec; your game owns transport, recipients, and playback. Use it for proximity voice chat, voice recording, and any feature that turns the player's mic into gameplay.
- Mic selection menu: list every connected input with
mic_devices!, show label, and start capture on the pick with mic_start_on!.
- Remembering the player's mic: save the chosen
name in your settings file and reopen it next launch with mic_resolve_device!, which falls back to the OS default when the device is unplugged.
- Push-to-talk voice chat:
mic_start_stream! while the key is held, drain packets with mic_get_bytes!, and send them over your own transport.
- Playing received voice: decode a peer's packet with
mic_unpack! and hand the clip to the audio bus with audio_play_clip!.
- Voice memos / clip recording:
mic_start!, then mic_stop! to take the full buffer, and mic_save_wav! to store it.
- Noise-gated voice: capture with
MicDenoiseSettings::voice() (or ::strong() for noisy rooms) or clean an existing clip with MicClip::denoised.
- Sensitivity slider + level bar in the audio settings menu: set input gain with
MicSettings::with_gain, enable with_auto_gain(true) for hands-off leveling, and drive the meter each frame with mic_level!.
- "Why is my mic silent?" support flow: show
mic_diagnostic! text when the level bar stays at zero β it flags an OS microphone-permission block.
- Voice-driven mechanics: read the rolling buffer with
mic_clip! to measure loudness for a "shout to scare enemies" or lip-sync feature.
- Bandwidth-friendly networking: pack a clip with
mic_pack! to the smallest PMIC codec before sending.
The microphone module owns capture lifetime and byte delivery; a game system owns consent, UI state, encoding, and network/storage policy. Use it for explicit voice or audio-input features. Do not start capture as a hidden side effect of an unrelated scene. Start once, consume bounded chunks, stop when the feature ends, and treat absent permission/device data as normal failure.
Use ctx.res.Mic() for live microphone bytes and optional recorded clips.
Mic clips are MicClip values:
- PCM16 samples
- input sample rate
- channel count
- optional denoise pass
- compressed packable bytes for UDP, TCP, HTTP, or save data
- WAV save support
The mic is a live stream while capture is active.
Call get_clip or get_bytes when your game decides it is time to send.
Those calls drain new audio since the last stream/get read.
clip returns the full rolling recording buffer.
stop_listening stops capture and returns that full buffer.
Packed mic bytes use PMIC.
Unpack supports raw v1 and compressed v2.
Pack chooses the smallest engine codec from raw PCM, zlib PCM, delta PCM, and zlib delta PCM.
They are engine bytes, not Opus voice-chat bytes.
Use them for simple send/store first.
Add Opus later for real voice chat bandwidth.
Proximity chat split:
- engine owns mic capture and encode/decode
- audio API owns clip playback
- game/server owns who hears whom, room/team rules, auth, mute, push-to-talk, and net relay
- client captures while push-to-talk or VAD is active
- client drains live bytes and sends packed bytes to server
- server filters recipients by position, team, room, or other game rules
- receiving client decodes frames and gives clips to the audio API
- Script context path:
ctx.res
- Module access:
ctx.res.Mic()
- Native backend:
cpal (WASAPI on Windows, CoreAudio on macOS, ALSA on Linux)
- Devices: any OS input works, so USB, XLR through an interface, headset, wireless, and virtual-cable mics all list and open
- Format: rate, channel count, and sample format come from the device; f32, i16, and u16 streams all convert to the
MicClip i16 format
- Default capture never gives up after one device: when no name is set and the OS default fails to open (missing ALSA default, per-endpoint privacy block), every other visible input on every available backend is tried before erroring
- All-fail errors and the silent-capture diagnostic both point at the OS microphone permission pages, the most common reason a shipped game "has no mic"
- Optional pro-audio backends: build the game with the
mic_asio feature (Windows ASIO, needs the Steinberg SDK + LLVM at build time) or mic_jack (Linux JACK, needs the jack development library). Their devices join the scan with names prefixed ASIO: / JACK: , and a cached prefixed name reopens on that backend. Most interfaces do not need this - vendors ship WASAPI paths that already work
- Wasm backend: unsupported, device scan returns an empty list and capture returns an error or empty clip
- Audio output: use
ctx.res.Audio() with MicClip
Scan first, cache the name, start on the cached name.
ctx.res.Mic().devices() rescans on every call because wireless and USB mics come and go. Each entry carries a name (the selection key), a label for the menu, and is_default. Duplicate hardware gets a #2 suffix on the label while both keep their own name.
Build the menu:
let devices = mic_devices!(ctx.res).unwrap_or_default();
for device in &devices {
// device.label for the row text, device.name for the value you store.
let _ = (&device.label, &device.name, device.is_default);
}
Start on the pick and store the name in your own settings:
if let Err(err) = mic_start_on!(ctx.res, &chosen_name) {
// Device unplugged between the scan and the click.
let _ = err;
let _ = mic_start!(ctx.res);
}
Reopen a cached name next launch. resolve_device returns the cached mic when it is still connected and the OS default when it is gone, so a missing mic never blocks the feature:
if let Some(device) = mic_resolve_device!(ctx.res, saved_name.as_deref()) {
let _ = mic_start_with!(ctx.res, device.settings());
}
Settings-struct form, for capture options plus a device:
let settings = MicSettings::default()
.with_device(&chosen_name)
.with_max_seconds(8.0)
.with_denoise(MicDenoiseSettings::voice());
let _ = mic_start!(ctx.res, settings);
Rules that keep selection working:
- An empty or absent
device opens the OS default.
- A name that no longer exists returns
Err; nothing silently swaps to another mic.
- Matching is by name, never by list position, so a rescan or reorder keeps the cached pick.
- Read the live name with
mic_device! and the failure text with mic_last_error!.
- A mic yanked mid-capture drops
mic_is_listening! to false; mic_stop! still returns the audio captured before the loss.
Hold R to record and stream mic bytes, press T to stop and play the clip back. The stop handler is split into a methods! helper.
lifecycle!({
fn on_update(&self, ctx: &mut ScriptContext<'_, API>) {
if key_pressed!(ctx.ipt, KeyCode::KeyR) {
let _ = mic_start!(ctx.res);
}
if key_down!(ctx.ipt, KeyCode::KeyR) {
if let Some(bytes) = mic_get_bytes!(ctx.res) {
// send bytes over UDP/TCP/HTTP here if you want
let _ = bytes;
}
}
if key_pressed!(ctx.ipt, KeyCode::KeyT) {
self.finish_recording(ctx);
}
}
});
methods!({
fn finish_recording(&self, ctx: &mut ScriptContext<'_, API>) {
if let Some(clip) = mic_stop!(ctx.res) {
let _ = audio_play!(ctx.res, &clip);
let bytes = mic_pack!(ctx.res, &clip);
let copy = mic_unpack!(ctx.res, &bytes).ok();
let _ = copy;
let _ = mic_save_wav!(ctx.res, "user://recordings/last.wav", &clip);
}
}
});
With settings:
let settings = MicSettings {
max_seconds: 8.0,
..Default::default()
};
let _ = mic_start!(ctx.res, settings);
With denoise:
let settings = MicSettings {
max_seconds: 8.0,
denoise: MicDenoiseSettings::voice(),
..Default::default()
};
let _ = mic_start!(ctx.res, settings);
Clip cleanup:
if let Some(clip) = mic_clip!(ctx.res) {
let clean = clip.denoised(MicDenoiseSettings::voice());
let _ = audio_play!(ctx.res, &clean);
}
Audio bus playback:
if let Some(clip) = mic_clip!(ctx.res) {
let voice = audio_bus!("voice");
let _ = audio_play_clip!(ctx.res, voice, &clip, 0.8);
}
Live receive:
// bytes came from a remote speaker packet.
if let Ok(clip) = mic_unpack!(ctx.res, &bytes) {
let _ = audio_play_clip!(ctx.res, audio_bus!("voice"), &clip, 1.0);
}
Mic networking is game-owned.
The engine gives packet-ready bytes.
Your game chooses transport, server relay, recipients, and playback position.
The engine does not tick, schedule, or send mic packets.
Your script calls mic_get_bytes! at the send rate you choose.
Client talk loop:
if push_to_talk {
let _ = mic_start_stream!(ctx.res);
if let Some(bytes) = mic_get_bytes!(ctx.res) {
// UdpEndpoint / NetworkWorld send call owned by game net state.
let _ = voice_udp.send_to(&bytes, server_addr);
}
} else if mic_is_listening!(ctx.res) {
let _ = mic_stop_stream!(ctx.res);
}
Server relay loop:
// Decode only if server needs metadata from the bytes.
// Otherwise relay bytes as-is to chosen recipients.
if player_can_hear_talker {
let _ = voice_udp.send_to(&bytes, listener_addr);
}
Client receive loop:
if let Ok(clip) = mic_unpack!(ctx.res, &bytes) {
// Pick speaker position from replicated game state.
let _ = audio_play_clip!(ctx.res, audio_bus!("voice"), &clip, 1.0);
}
UDP notes:
- drain every net tick
- keep frames small
- prefer ~20ms to ~60ms ticks for voice
- drop old frames over reliable resend
- use TCP/HTTP only for recorded clips or non-realtime upload
| Field |
Detail |
| Access |
ctx.res.Mic() |
| Signature |
pub fn devices(&self) -> Result<Vec<MicDevice>, String> |
| Returns |
Result<Vec<MicDevice>, String> |
| Use when |
List the connected input devices for a selection menu. Rescans on every call. |
| Field |
Detail |
| Access |
ctx.res.Mic() |
| Signature |
pub fn scan(&self) -> Result<Vec<MicDevice>, String> |
| Returns |
Result<Vec<MicDevice>, String> |
| Use when |
Same as devices, named for a refresh button. |
| Field |
Type |
Detail |
name |
String |
Selection key. Store this to remember the player's choice. |
label |
String |
Menu text. Suffixed #2 when two devices share a name. |
is_default |
bool |
OS default input. |
sample_rate |
u32 |
Device default rate, 0 when the backend hides it. |
channels |
u16 |
Device default channel count, 0 when unknown. |
Call device.settings() for default capture settings already aimed at that device.
| Field |
Detail |
| Access |
ctx.res.Mic() |
| Signature |
pub fn default_device(&self) -> Option<MicDevice> |
| Returns |
Option<MicDevice> |
| Use when |
Preselect the OS default row in a menu. |
| Field |
Detail |
| Access |
ctx.res.Mic() |
| Signature |
pub fn find_device(&self, name: &str) -> Option<MicDevice> |
| Returns |
Option<MicDevice> |
| Use when |
Check whether a cached name is still connected, with no fallback. |
| Field |
Detail |
| Access |
ctx.res.Mic() |
| Signature |
pub fn resolve_device(&self, name: Option<&str>) -> Option<MicDevice> |
| Returns |
Option<MicDevice> |
| Use when |
Reopen a cached pick, falling back to the OS default when it is unplugged. |
| Field |
Detail |
| Access |
ctx.res.Mic() |
| Signature |
pub fn has_device(&self, name: &str) -> bool |
| Returns |
bool |
| Use when |
Grey out a saved device row that is not plugged in. |
| Field |
Detail |
| Access |
ctx.res.Mic() |
| Signature |
pub fn start_on<S: Into<String>>(&self, device: S) -> Result<(), String> |
| Returns |
Result<(), String> |
| Use when |
Capture from one named device. Errs when that device is gone. |
| Field |
Detail |
| Access |
ctx.res.Mic() |
| Signature |
pub fn start_on_with<S: Into<String>>(&self, device: S, settings: MicSettings) -> Result<(), String> |
| Returns |
Result<(), String> |
| Use when |
Capture from a named device with custom length and denoise. |
| Field |
Detail |
| Access |
ctx.res.Mic() |
| Signature |
pub fn device(&self) -> Option<String> |
| Returns |
Option<String> |
| Use when |
Show which mic the current capture runs on. |
| Field |
Detail |
| Access |
ctx.res.Mic() |
| Signature |
pub fn last_error(&self) -> Option<String> |
| Returns |
Option<String> |
| Use when |
Report why capture stopped, including a mic unplugged mid-stream. |
| Field |
Detail |
| Access |
ctx.res.Mic() |
| Signature |
pub fn level(&self) -> f32 |
| Returns |
f32 in 0.0..=1.0 |
| Use when |
Draw the live input meter next to the sensitivity slider. Reads the post-gain peak, so the bar matches what gets recorded. |
| Field |
Detail |
| Access |
ctx.res.Mic() |
| Signature |
pub fn diagnostic(&self) -> Option<String> |
| Returns |
Option<String> |
| Use when |
Capture runs but stays silent. Set after ~2s of bit-perfect zero input β the classic OS microphone-permission block β and cleared as soon as real signal arrives. |
| Field |
Detail |
| Access |
ctx.res.Mic() |
| Signature |
pub fn start_listening(&self) -> Result<(), String> |
| Returns |
Result<(), String> |
| Use when |
Start mic capture with default settings. |
| Field |
Detail |
| Access |
ctx.res.Mic() |
| Signature |
pub fn start_stream(&self) -> Result<(), String> |
| Returns |
Result<(), String> |
| Use when |
Start live mic stream capture. |
| Field |
Detail |
| Access |
ctx.res.Mic() |
| Signature |
pub fn start_with(&self, settings: MicSettings) -> Result<(), String> |
| Returns |
Result<(), String> |
| Use when |
Start mic capture with max clip seconds and optional denoise. |
| Field |
Type |
Detail |
max_seconds |
f32 |
Rolling capture length. |
denoise |
MicDenoiseSettings |
Capture-time denoise settings. |
device |
Option<String> |
Device name from devices(). None or blank opens the OS default. |
channels |
MicChannels |
Clip channel layout. |
gain |
f32 |
Input sensitivity. Linear, 1.0 is unity, clamped to 0.0..=8.0. Wire it to the settings-menu slider. |
auto_gain |
bool |
Slow automatic level on top of gain: boosts quiet mics toward speech level, capped at 4x, never cuts. Good default for voice chat. |
Builders: with_device, with_default_device, with_max_seconds, with_denoise, with_channels, with_gain, with_auto_gain.
| Variant |
Detail |
Auto |
Default. Mono and stereo mics stay as-is, wider interfaces fold to mono. |
Mono |
Always fold to one channel. Smallest voice packets. |
Device |
Keep the device layout, including 4 or 8 channel interfaces. |
Folding averages the channels of each frame, so an 8-in interface with one live XLR input records quieter than the same mic on a mono device. Pick Device when the game wants every input channel.
| Field |
Type |
Detail |
enabled |
bool |
Enable denoise pass. |
noise_floor |
f32 |
Noise-gate threshold, linear 0.0..=1.0. Samples below it get reduced. |
reduction |
f32 |
Quiet-sample gain cut, from 0.0 (off) to 1.0 (full mute). |
high_pass |
bool |
Remove low rumble/DC drift. |
high_pass_hz |
f32 |
Rumble-cut corner frequency in Hz. Tracks the device's real rate, so the cut lands the same on a 44.1 kHz headset and a 96 kHz interface. |
Presets: MicDenoiseSettings::off(), ::voice() for a default voice gate, ::strong() for noisy rooms and laptop mics.
Builders for the settings-menu knobs: with_noise_floor, with_reduction, with_high_pass_hz.
| Field |
Detail |
| Access |
MicClip |
| Signature |
pub fn denoised(&self, settings: MicDenoiseSettings) -> MicClip |
| Returns |
MicClip |
| Use when |
Clean a captured clip without changing the active capture stream. |
| Field |
Detail |
| Access |
MicClip |
| Signature |
pub fn compressed_bytes(&self) -> Vec<u8> |
| Returns |
Vec<u8> |
| Use when |
Pack with the smallest available PMIC byte codec. |
| Field |
Detail |
| Access |
MicClip |
| Signature |
pub fn raw_bytes(&self) -> Vec<u8> |
| Returns |
Vec<u8> |
| Use when |
Force legacy raw PMIC v1 bytes. |
| Field |
Detail |
| Access |
MicClip |
| Signature |
pub fn compression_ratio(&self) -> f32 |
| Returns |
f32 |
| Use when |
Compare packed byte length to raw v1 length. |
| Field |
Detail |
| Access |
ctx.res.Mic() |
| Signature |
pub fn stop_listening(&self) -> Option<MicClip> |
| Returns |
Option<MicClip> |
| Use when |
Stop capture and take the recorded clip. |
| Field |
Detail |
| Access |
ctx.res.Mic() |
| Signature |
pub fn stop_stream(&self) -> Option<MicClip> |
| Returns |
Option<MicClip> |
| Use when |
Stop live mic stream and take the rolling recorded clip. |
| Field |
Detail |
| Access |
ctx.res.Mic() |
| Signature |
pub fn clip(&self) -> Option<MicClip> |
| Returns |
Option<MicClip> |
| Use when |
Read a copy of the current clip without stopping capture. |
| Field |
Detail |
| Access |
ctx.res.Mic() |
| Signature |
pub fn get_clip(&self) -> Option<MicClip> |
| Returns |
Option<MicClip> |
| Use when |
Drain new live mic samples since the last stream read. |
| Field |
Detail |
| Access |
ctx.res.Mic() |
| Signature |
pub fn get_bytes(&self) -> Option<Vec<u8>> |
| Returns |
Option<Vec<u8>> |
| Use when |
Drain new live mic samples as compressed PMIC bytes for networking. |
| Field |
Detail |
| Access |
ctx.res.Mic() |
| Signature |
pub fn is_listening(&self) -> bool |
| Returns |
bool |
| Use when |
Check whether capture is active. |
| Field |
Detail |
| Access |
ctx.res.Mic() |
| Signature |
pub fn save_wav<S: ResPathSource>(&self, source: S, clip: &MicClip) -> Result<(), String> |
| Returns |
Result<(), String> |
| Use when |
Save a recorded clip as .wav. |
| Field |
Detail |
| Access |
ctx.res.Mic() |
| Signature |
pub fn pack(&self, clip: &MicClip) -> Vec<u8> |
| Returns |
Vec<u8> |
| Use when |
Convert clip to smallest PMIC bytes for network or storage. |
| Field |
Detail |
| Access |
ctx.res.Mic() |
| Signature |
pub fn unpack(&self, bytes: &[u8]) -> Result<MicClip, String> |
| Returns |
Result<MicClip, String> |
| Use when |
Convert raw v1 or compressed v2 PMIC bytes back to a MicClip. |
| Macro |
Expands to |
mic_devices!(ctx.res) |
ctx.res.Mic().devices() |
mic_scan!(ctx.res) |
ctx.res.Mic().scan() |
mic_default_device!(ctx.res) |
ctx.res.Mic().default_device() |
mic_find_device!(ctx.res, name) |
ctx.res.Mic().find_device(name) |
mic_resolve_device!(ctx.res, name) |
ctx.res.Mic().resolve_device(name) |
mic_has_device!(ctx.res, name) |
ctx.res.Mic().has_device(name) |
mic_start_on!(ctx.res, name) |
ctx.res.Mic().start_on(name) |
mic_start_on!(ctx.res, name, settings) |
ctx.res.Mic().start_on_with(name, settings) |
mic_device!(ctx.res) |
ctx.res.Mic().device() |
mic_last_error!(ctx.res) |
ctx.res.Mic().last_error() |
mic_level!(ctx.res) |
ctx.res.Mic().level() |
mic_diagnostic!(ctx.res) |
ctx.res.Mic().diagnostic() |
mic_start!(ctx.res) |
ctx.res.Mic().start_listening() |
mic_start!(ctx.res, settings) |
ctx.res.Mic().start_with(settings) |
mic_start_listening!(ctx.res) |
ctx.res.Mic().start_listening() |
mic_start_stream!(ctx.res) |
ctx.res.Mic().start_stream() |
mic_start_with!(ctx.res, settings) |
ctx.res.Mic().start_with(settings) |
mic_record!(ctx.res) |
ctx.res.Mic().record() |
mic_stop!(ctx.res) |
ctx.res.Mic().stop_listening() |
mic_stop_listening!(ctx.res) |
ctx.res.Mic().stop_listening() |
mic_stop_stream!(ctx.res) |
ctx.res.Mic().stop_stream() |
mic_clip!(ctx.res) |
ctx.res.Mic().clip() |
mic_get_clip!(ctx.res) |
ctx.res.Mic().get_clip() |
mic_get_bytes!(ctx.res) |
ctx.res.Mic().get_bytes() |
mic_stream_clip!(ctx.res) |
ctx.res.Mic().stream_clip() |
mic_stream_bytes!(ctx.res) |
ctx.res.Mic().stream_bytes() |
mic_frame!(ctx.res) |
ctx.res.Mic().stream_clip() |
mic_frame_bytes!(ctx.res) |
ctx.res.Mic().stream_bytes() |
mic_is_listening!(ctx.res) |
ctx.res.Mic().is_listening() |
mic_save_wav!(ctx.res, path, &clip) |
ctx.res.Mic().save_wav(path, &clip) |
mic_pack!(ctx.res, &clip) |
ctx.res.Mic().pack(&clip) |
mic_unpack!(ctx.res, &bytes) |
ctx.res.Mic().unpack(&bytes) |