---
title: "microphone"
description: "Capture from an input device and read what is arriving: a loudness, a magnitude spectrum, and the raw PCM."
section: "API Reference"
slug: "api-microphone"
canonical: "https://origozero.ai/docs/api-microphone"
updated: "2026-10-16T20:06:47.099843308+00:00"
tags: ["api", "reference"]
---

# microphone

Capture from an input device and read what is arriving: a loudness, a magnitude spectrum, and the raw PCM.

```lua
require("@builtin::modules.api.engine.microphone") -- microphone (also available as global 'microphone')
```

The `microphone` namespace: 30 functions.

## globals/microphone/awaitRunning {#globals-microphone-awaitrunning}

```lua
microphone.awaitRunning(timeout: number?) -> (MicState, string?)
```

Wait until the capture settles out of `starting` and
`permissionPending`, and report where it landed. Returns as soon as the
state settles, or when `timeout` seconds have passed, whichever comes
first — a browser permission prompt nobody answers never settles, so
the wait is always bounded.

**Parameters**

- `timeout` `number` _(optional)_ — Seconds to wait at most. Defaults to 10.

**Returns** `(MicState, string?)` — The state reached, and its reason where it has one.

```lua
microphone.start(); local state, why = microphone.awaitRunning()
```

## globals/microphone/devices {#globals-microphone-devices}

```lua
microphone.devices() -> { MicDevice }
```

Every input device the platform offers. `id` is what
`microphone.start` takes to select one and is stable across reboots
where the platform provides a stable identifier; `name` is the label a
person recognises.

An empty list is a legitimate answer, not a failure: a machine with no
input hardware offers none, and a browser names none until microphone
access has been granted at least once — the labels are part of what the
permission protects.

**Returns** `{ MicDevice }` — Array of `{ id, name, default }`.

```lua
for _, d in ipairs(microphone.devices()) do print(d.name, d.default) end
```

## globals/microphone/frequencies {#globals-microphone-frequencies}

```lua
microphone.frequencies() -> { number }
```

The frequency each spectrum bin is centred on, in Hz, as an array
parallel to `microphone.spectrum()`. Derived from the capture's rate and
transform size, so it changes only when a capture is started with
different ones. Empty while no capture is running.

**Returns** `{ number }` — Array of centre frequencies, one per bin.

```lua
local hz = microphone.frequencies(); print(hz[#hz]) -- the Nyquist frequency
```

## globals/microphone/level {#globals-microphone-level}

```lua
microphone.level() -> number
```

Loudness of the most recent analysis window, as an RMS amplitude in
0..1. A full-scale sine reads about 0.707 and silence reads 0.

Measured over only the samples that have arrived, so a capture that has
just started reports the loudness of what it holds rather than a level
diluted by a window it has not filled yet. 0 while no capture is
running.

**Returns** `number` — RMS amplitude, 0..1.

```lua
if microphone.level() > 0.05 then print("someone is talking") end
```

## globals/microphone/peak {#globals-microphone-peak}

```lua
microphone.peak() -> { [string]: number }?
```

The bin carrying the most energy and what it says: the frequency it
is centred on, its amplitude, and the loudness of the whole window.
A capture reading silence answers with amplitude 0 at bin 1.

**Returns** `{ [string]: number }?` — `{ bin, hz, amplitude, level }`, or nil while no capture is running.

```lua
local p = microphone.peak(); if p and p.amplitude > 0.05 then print(p.hz) end
```

## globals/microphone/samples {#globals-microphone-samples}

```lua
microphone.samples(max: number?) -> buffer?
```

Captured mono PCM no caller has taken yet, oldest sample first, as a
buffer of little-endian f32 read with `buffer.readf32`. The samples are
removed, so successive calls walk forward through the capture and a
caller doing its own analysis sees every frame once.

nil while no capture is running, and a zero-length buffer when the
capture is running and nothing new has arrived. Samples nobody takes
are discarded once the queue fills, and `status().overruns` counts
every one.

**Parameters**

- `max` `number` _(optional)_ — How many samples to take at most. Omitted, everything held comes back.

**Returns** `buffer?` — Buffer of f32 samples, or nil when no capture is running.

```lua
local pcm = microphone.samples(); if pcm then print(buffer.len(pcm) // 4) end
```

## globals/microphone/spectrum {#globals-microphone-spectrum}

```lua
microphone.spectrum() -> { number }
```

Amplitude per frequency bin over the most recent analysis window:
`fftSize / 2 + 1` numbers, DC at index 1 through the Nyquist frequency
at the last. Bin `i` covers `(i - 1) * status().binHz` Hz.

Each value is an amplitude estimate rather than a raw transform
magnitude, so a full-scale tone sitting on a bin centre reads about 1.0
and the numbers stay comparable across transform sizes.

The window is multiplied by a **Hann** taper before the transform. An
untapered window ends abruptly at both edges and the transform reads
that as energy spread across every bin, smearing one tone into a skirt
that buries quieter tones beside it. Hann trades a slightly wider main
lobe — a tone occupies about three bins rather than one — for sidelobes
that fall away steeply, which is what lets neighbouring tones be told
apart. Read a peak as "a tone near here", not "a tone exactly here".

Reading this takes no samples away from `microphone.samples()`. Empty
while no capture is running.

**Returns** `{ number }` — Array of amplitudes, one per bin.

```lua
local bins = microphone.spectrum(); print(#bins, bins[1])
```

## globals/microphone/start {#globals-microphone-start}

```lua
microphone.start(opts: MicOpts?) -> (MicState?, string?)
```

Open an input device and begin capturing. Returns the state the
capture reached — `"running"` once a device is delivering, or
`"permissionPending"` where the platform must ask for access first,
which is the browser's normal path. Poll `microphone.status()` from
there, or use `microphone.awaitRunning()`. Where access is refused
without asking (a native engine's fake microphone presented as `deny`),
it returns `"denied"`.

A request that cannot be made at all returns nil and the reason: an
`fftSize` that is not a whole power of two between 64 and 16384, a
device no machine here offers, a rate the device does not capture at,
or a capture that is already running.

Omitting `device` opens the platform default. Omitting `sampleRate`
takes the device's own rate, which is what avoids a resample.
`fftSize` is how many samples one analysis window covers and defaults
to 1024 — at 48 kHz that spans ~21 ms and resolves ~47 Hz per bin.

**Parameters**

- `opts` `MicOpts` _(optional)_ — `{ device, sampleRate, fftSize }`.

**Returns** `(MicState?, string?)` — The state reached, or nil and the reason the request was refused.

```lua
local state, why = microphone.start({ fftSize = 2048 })
```

## globals/microphone/status {#globals-microphone-status}

```lua
microphone.status() -> MicStatus
```

Where the capture stands.

`reason` carries the platform's own message: the refusal for `denied`,
the device's message for `failed`, what is being waited on for
`permissionPending`. `binHz` is the width of one spectrum bin and
`bins` how many `microphone.spectrum()` returns.

`framesCaptured` counts every mono frame the device delivered whether
or not anything drained it, so a silent room reads differently from a
stalled device. `overruns` counts samples discarded because a consumer
did not keep up — it standing still is what says the readings are
continuous, and it climbing is why a caller sees gaps.

**Returns** `MicStatus` — `{ state, reason, device, sampleRate, fftSize, binHz, bins, framesCaptured, overruns }`.

```lua
local s = microphone.status(); print(s.state, s.framesCaptured, s.overruns)
```

## globals/microphone/stop {#globals-microphone-stop}

```lua
microphone.stop() -> boolean
```

Stop the capture and release the device. True when a capture was
open or being opened at call time. The device is let go before this
returns, so a stop followed by a start opens it again rather than
finding it held.

**Returns** `boolean` — Whether a capture was active.

```lua
microphone.stop()
```

## modules/microphone/awaitRunning {#modules-microphone-awaitrunning}

```lua
awaitRunning(timeout: number?): (MicState, string?)
```

Wait until the capture settles out of `starting` and
`permissionPending`, and report where it landed. Returns as soon as the
state settles, or when `timeout` seconds have passed, whichever comes
first — a browser permission prompt nobody answers never settles, so
the wait is always bounded.

**Parameters**

- `timeout` `number?` _(optional)_ — Seconds to wait at most. Defaults to 10.

```lua
microphone.start(); local state, why = microphone.awaitRunning()
```

## modules/microphone/devices {#modules-microphone-devices}

```lua
devices(): { MicDevice }
```

Every input device the platform offers. `id` is what
`microphone.start` takes to select one and is stable across reboots
where the platform provides a stable identifier; `name` is the label a
person recognises.

An empty list is a legitimate answer, not a failure: a machine with no
input hardware offers none, and a browser names none until microphone
access has been granted at least once — the labels are part of what the
permission protects.

```lua
for _, d in ipairs(microphone.devices()) do print(d.name, d.default) end
```

## modules/microphone/frequencies {#modules-microphone-frequencies}

```lua
frequencies(): { number }
```

The frequency each spectrum bin is centred on, in Hz, as an array
parallel to `microphone.spectrum()`. Derived from the capture's rate and
transform size, so it changes only when a capture is started with
different ones. Empty while no capture is running.

```lua
local hz = microphone.frequencies(); print(hz[#hz]) -- the Nyquist frequency
```

## modules/microphone/level {#modules-microphone-level}

```lua
level(): number
```

Loudness of the most recent analysis window, as an RMS amplitude in
0..1. A full-scale sine reads about 0.707 and silence reads 0.

Measured over only the samples that have arrived, so a capture that has
just started reports the loudness of what it holds rather than a level
diluted by a window it has not filled yet. 0 while no capture is
running.

```lua
if microphone.level() > 0.05 then print("someone is talking") end
```

## modules/microphone/peak {#modules-microphone-peak}

```lua
peak(): { [string]: number }?
```

The bin carrying the most energy and what it says: the frequency it
is centred on, its amplitude, and the loudness of the whole window.
A capture reading silence answers with amplitude 0 at bin 1.

```lua
local p = microphone.peak(); if p and p.amplitude > 0.05 then print(p.hz) end
```

## modules/microphone/samples {#modules-microphone-samples}

```lua
samples(max: number?): buffer?
```

Captured mono PCM no caller has taken yet, oldest sample first, as a
buffer of little-endian f32 read with `buffer.readf32`. The samples are
removed, so successive calls walk forward through the capture and a
caller doing its own analysis sees every frame once.

nil while no capture is running, and a zero-length buffer when the
capture is running and nothing new has arrived. Samples nobody takes
are discarded once the queue fills, and `status().overruns` counts
every one.

**Parameters**

- `max` `number?` _(optional)_ — How many samples to take at most. Omitted, everything held comes back.

```lua
local pcm = microphone.samples(); if pcm then print(buffer.len(pcm) // 4) end
```

## modules/microphone/spectrum {#modules-microphone-spectrum}

```lua
spectrum(): { number }
```

Amplitude per frequency bin over the most recent analysis window:
`fftSize / 2 + 1` numbers, DC at index 1 through the Nyquist frequency
at the last. Bin `i` covers `(i - 1) * status().binHz` Hz.

Each value is an amplitude estimate rather than a raw transform
magnitude, so a full-scale tone sitting on a bin centre reads about 1.0
and the numbers stay comparable across transform sizes.

The window is multiplied by a **Hann** taper before the transform. An
untapered window ends abruptly at both edges and the transform reads
that as energy spread across every bin, smearing one tone into a skirt
that buries quieter tones beside it. Hann trades a slightly wider main
lobe — a tone occupies about three bins rather than one — for sidelobes
that fall away steeply, which is what lets neighbouring tones be told
apart. Read a peak as "a tone near here", not "a tone exactly here".

Reading this takes no samples away from `microphone.samples()`. Empty
while no capture is running.

```lua
local bins = microphone.spectrum(); print(#bins, bins[1])
```

## modules/microphone/start {#modules-microphone-start}

```lua
start(opts: MicOpts?): (MicState?, string?)
```

Open an input device and begin capturing. Returns the state the
capture reached — `"running"` once a device is delivering, or
`"permissionPending"` where the platform must ask for access first,
which is the browser's normal path. Poll `microphone.status()` from
there, or use `microphone.awaitRunning()`. Where access is refused
without asking (a native engine's fake microphone presented as `deny`),
it returns `"denied"`.

A request that cannot be made at all returns nil and the reason: an
`fftSize` that is not a whole power of two between 64 and 16384, a
device no machine here offers, a rate the device does not capture at,
or a capture that is already running.

Omitting `device` opens the platform default. Omitting `sampleRate`
takes the device's own rate, which is what avoids a resample.
`fftSize` is how many samples one analysis window covers and defaults
to 1024 — at 48 kHz that spans ~21 ms and resolves ~47 Hz per bin.

**Parameters**

- `opts` `MicOpts?` _(optional)_ — `{ device, sampleRate, fftSize }`.

```lua
local state, why = microphone.start({ fftSize = 2048 })
```

## modules/microphone/status {#modules-microphone-status}

```lua
status(): MicStatus
```

Where the capture stands.

`reason` carries the platform's own message: the refusal for `denied`,
the device's message for `failed`, what is being waited on for
`permissionPending`. `binHz` is the width of one spectrum bin and
`bins` how many `microphone.spectrum()` returns.

`framesCaptured` counts every mono frame the device delivered whether
or not anything drained it, so a silent room reads differently from a
stalled device. `overruns` counts samples discarded because a consumer
did not keep up — it standing still is what says the readings are
continuous, and it climbing is why a caller sees gaps.

```lua
local s = microphone.status(); print(s.state, s.framesCaptured, s.overruns)
```

## modules/microphone/stop {#modules-microphone-stop}

```lua
stop(): boolean
```

Stop the capture and release the device. True when a capture was
open or being opened at call time. The device is let go before this
returns, so a stop followed by a start opens it again rather than
finding it held.

```lua
microphone.stop()
```

## typed/builtin//modules/api/engine/microphone/microphone/awaitRunning {#typed-builtin-modules-api-engine-microphone-microphone-awaitrunning}

```lua
microphone.awaitRunning(timeout: number?) -> (MicState, string?)
```

Wait until the capture settles out of `starting` and
`permissionPending`, and report where it landed. Returns as soon as the
state settles, or when `timeout` seconds have passed, whichever comes
first — a browser permission prompt nobody answers never settles, so
the wait is always bounded.

**Parameters**

- `timeout` `number` _(optional)_ — Seconds to wait at most. Defaults to 10.

**Returns** `(MicState, string?)` — The state reached, and its reason where it has one.

```lua
microphone.start(); local state, why = microphone.awaitRunning()
```

## typed/builtin//modules/api/engine/microphone/microphone/devices {#typed-builtin-modules-api-engine-microphone-microphone-devices}

```lua
microphone.devices() -> { MicDevice }
```

Every input device the platform offers. `id` is what
`microphone.start` takes to select one and is stable across reboots
where the platform provides a stable identifier; `name` is the label a
person recognises.

An empty list is a legitimate answer, not a failure: a machine with no
input hardware offers none, and a browser names none until microphone
access has been granted at least once — the labels are part of what the
permission protects.

**Returns** `{ MicDevice }` — Array of `{ id, name, default }`.

```lua
for _, d in ipairs(microphone.devices()) do print(d.name, d.default) end
```

## typed/builtin//modules/api/engine/microphone/microphone/frequencies {#typed-builtin-modules-api-engine-microphone-microphone-frequencies}

```lua
microphone.frequencies() -> { number }
```

The frequency each spectrum bin is centred on, in Hz, as an array
parallel to `microphone.spectrum()`. Derived from the capture's rate and
transform size, so it changes only when a capture is started with
different ones. Empty while no capture is running.

**Returns** `{ number }` — Array of centre frequencies, one per bin.

```lua
local hz = microphone.frequencies(); print(hz[#hz]) -- the Nyquist frequency
```

## typed/builtin//modules/api/engine/microphone/microphone/level {#typed-builtin-modules-api-engine-microphone-microphone-level}

```lua
microphone.level() -> number
```

Loudness of the most recent analysis window, as an RMS amplitude in
0..1. A full-scale sine reads about 0.707 and silence reads 0.

Measured over only the samples that have arrived, so a capture that has
just started reports the loudness of what it holds rather than a level
diluted by a window it has not filled yet. 0 while no capture is
running.

**Returns** `number` — RMS amplitude, 0..1.

```lua
if microphone.level() > 0.05 then print("someone is talking") end
```

## typed/builtin//modules/api/engine/microphone/microphone/peak {#typed-builtin-modules-api-engine-microphone-microphone-peak}

```lua
microphone.peak() -> { [string]: number }?
```

The bin carrying the most energy and what it says: the frequency it
is centred on, its amplitude, and the loudness of the whole window.
A capture reading silence answers with amplitude 0 at bin 1.

**Returns** `{ [string]: number }?` — `{ bin, hz, amplitude, level }`, or nil while no capture is running.

```lua
local p = microphone.peak(); if p and p.amplitude > 0.05 then print(p.hz) end
```

## typed/builtin//modules/api/engine/microphone/microphone/samples {#typed-builtin-modules-api-engine-microphone-microphone-samples}

```lua
microphone.samples(max: number?) -> buffer?
```

Captured mono PCM no caller has taken yet, oldest sample first, as a
buffer of little-endian f32 read with `buffer.readf32`. The samples are
removed, so successive calls walk forward through the capture and a
caller doing its own analysis sees every frame once.

nil while no capture is running, and a zero-length buffer when the
capture is running and nothing new has arrived. Samples nobody takes
are discarded once the queue fills, and `status().overruns` counts
every one.

**Parameters**

- `max` `number` _(optional)_ — How many samples to take at most. Omitted, everything held comes back.

**Returns** `buffer?` — Buffer of f32 samples, or nil when no capture is running.

```lua
local pcm = microphone.samples(); if pcm then print(buffer.len(pcm) // 4) end
```

## typed/builtin//modules/api/engine/microphone/microphone/spectrum {#typed-builtin-modules-api-engine-microphone-microphone-spectrum}

```lua
microphone.spectrum() -> { number }
```

Amplitude per frequency bin over the most recent analysis window:
`fftSize / 2 + 1` numbers, DC at index 1 through the Nyquist frequency
at the last. Bin `i` covers `(i - 1) * status().binHz` Hz.

Each value is an amplitude estimate rather than a raw transform
magnitude, so a full-scale tone sitting on a bin centre reads about 1.0
and the numbers stay comparable across transform sizes.

The window is multiplied by a **Hann** taper before the transform. An
untapered window ends abruptly at both edges and the transform reads
that as energy spread across every bin, smearing one tone into a skirt
that buries quieter tones beside it. Hann trades a slightly wider main
lobe — a tone occupies about three bins rather than one — for sidelobes
that fall away steeply, which is what lets neighbouring tones be told
apart. Read a peak as "a tone near here", not "a tone exactly here".

Reading this takes no samples away from `microphone.samples()`. Empty
while no capture is running.

**Returns** `{ number }` — Array of amplitudes, one per bin.

```lua
local bins = microphone.spectrum(); print(#bins, bins[1])
```

## typed/builtin//modules/api/engine/microphone/microphone/start {#typed-builtin-modules-api-engine-microphone-microphone-start}

```lua
microphone.start(opts: MicOpts?) -> (MicState?, string?)
```

Open an input device and begin capturing. Returns the state the
capture reached — `"running"` once a device is delivering, or
`"permissionPending"` where the platform must ask for access first,
which is the browser's normal path. Poll `microphone.status()` from
there, or use `microphone.awaitRunning()`. Where access is refused
without asking (a native engine's fake microphone presented as `deny`),
it returns `"denied"`.

A request that cannot be made at all returns nil and the reason: an
`fftSize` that is not a whole power of two between 64 and 16384, a
device no machine here offers, a rate the device does not capture at,
or a capture that is already running.

Omitting `device` opens the platform default. Omitting `sampleRate`
takes the device's own rate, which is what avoids a resample.
`fftSize` is how many samples one analysis window covers and defaults
to 1024 — at 48 kHz that spans ~21 ms and resolves ~47 Hz per bin.

**Parameters**

- `opts` `MicOpts` _(optional)_ — `{ device, sampleRate, fftSize }`.

**Returns** `(MicState?, string?)` — The state reached, or nil and the reason the request was refused.

```lua
local state, why = microphone.start({ fftSize = 2048 })
```

## typed/builtin//modules/api/engine/microphone/microphone/status {#typed-builtin-modules-api-engine-microphone-microphone-status}

```lua
microphone.status() -> MicStatus
```

Where the capture stands.

`reason` carries the platform's own message: the refusal for `denied`,
the device's message for `failed`, what is being waited on for
`permissionPending`. `binHz` is the width of one spectrum bin and
`bins` how many `microphone.spectrum()` returns.

`framesCaptured` counts every mono frame the device delivered whether
or not anything drained it, so a silent room reads differently from a
stalled device. `overruns` counts samples discarded because a consumer
did not keep up — it standing still is what says the readings are
continuous, and it climbing is why a caller sees gaps.

**Returns** `MicStatus` — `{ state, reason, device, sampleRate, fftSize, binHz, bins, framesCaptured, overruns }`.

```lua
local s = microphone.status(); print(s.state, s.framesCaptured, s.overruns)
```

## typed/builtin//modules/api/engine/microphone/microphone/stop {#typed-builtin-modules-api-engine-microphone-microphone-stop}

```lua
microphone.stop() -> boolean
```

Stop the capture and release the device. True when a capture was
open or being opened at call time. The device is let go before this
returns, so a stop followed by a start opens it again rather than
finding it held.

**Returns** `boolean` — Whether a capture was active.

```lua
microphone.stop()
```
