OneScript
Foundation

audio

Record from the microphone to a file and read live levels and elapsed time.

audio records microphone sound directly to a file with AVAudioRecorder. record returns an AudioRecording that is already recording, and stop ends it; levels is an async iterable of live levels for meters and timers. OneScript configures the audio session for you, so scripts don't set it up.

import { audio } from "onescript";

const recording = await audio.record("memo.m4a");
await new Promise(resolve => setTimeout(resolve, 5_000));
const { path, duration } = await recording.stop();
console.log(path, duration);

Types

interface AudioPermission {
  status: "notDetermined" | "denied" | "authorized";
}

interface AudioRecording {
  readonly path: string;                              // absolute path of the recording
  readonly state: "recording" | "paused" | "ended";
  readonly finished: Promise<AudioRecordingResult>;
  pause(): Promise<void>;
  resume(): Promise<void>;
  stop(): Promise<AudioRecordingResult>;
  levels(options?: { interval?: number; signal?: AbortSignal }): AsyncIterable<AudioLevel>;
}

interface AudioRecordingResult {
  path: string;
  duration: number;                                   // seconds
  reason: "stopped" | "maxDuration" | "interrupted";
}

interface AudioLevel {
  currentTime: number;                                // seconds recorded, excluding pauses
  average: number;                                    // average power (dBFS)
  peak: number;                                       // peak power (dBFS)
  channels: { average: number; peak: number }[];
}

Permission

function audio.getPermission(): Promise<AudioPermission>;
function audio.requestPermission(): Promise<AudioPermission>;

The first record call asks for microphone access, so you usually don't need to ask yourself. Call requestPermission to ask ahead of time; once the user has decided, it returns the current status.

statusMeaning
notDeterminedThe user hasn't been asked yet.
deniedThe user denied microphone access.
authorizedScripts can record.

audio.record

function audio.record(path: string, options?: {
  sampleRate?: number;
  channels?: 1 | 2;
  bitRate?: number;
  maxDuration?: number;
}): Promise<AudioRecording>;

Starts recording; when the promise fulfills, recording has begun. path works like fs: relative paths resolve against the project directory, an existing file is overwritten, and the parent directory must exist. The extension picks the format:

ExtensionFormat
.m4aAAC. Small files, good for speech and sharing.
.wav16-bit PCM. Lossless, good for further processing.
OptionDescription
sampleRateSample rate in Hz. Defaults to 48000; range 8000–192000.
channelsNumber of channels. Defaults to 1.
bitRateAAC bit rate in bit/s, .m4a only. The encoder chooses by default.
maxDurationMaximum length in seconds, excluding pauses. Recording ends when it's reached.
const recording = await audio.record("recordings/interview.m4a", {
  bitRate: 64_000,
  maxDuration: 10 * 60
});
const result = await recording.finished;
console.log(result.reason); // "maxDuration"

Only one recording can run at a time; calling record while recording throws AUDIO_UNAVAILABLE.

Controlling a recording

pause pauses and resume continues; each does nothing if the recording is already in that state. stop ends the recording and finishes the file. You can call it more than once, and it always returns the same result.

finished fulfills however the recording ends, and reason tells you why:

reasonMeaning
stoppedstop was called.
maxDurationmaxDuration was reached.
interruptedA call, Siri, or similar interrupted it, or OneScript moved to the background.

An interrupted recording ends and saves immediately, so the file is complete; call record again to keep going. If recording fails, finished and stop reject with AUDIO_RECORDING_FAILED.

The script keeps running until the recording ends. When the script ends (or the worker thread that started the recording ends), the recording ends too: what was recorded stays in the file, and finished rejects.

Levels

recording.levels(options?: { interval?: number; signal?: AbortSignal }): AsyncIterable<AudioLevel>;

Samples the level and elapsed time every interval milliseconds (default 50, minimum 16) until you break, the signal aborts, or the recording ends. Levels are current state: if the loop body is slower than the updates, intermediate values are skipped and only the latest level is delivered. No levels are produced while paused.

Power is in dBFS, from -160 (silence) to 0 (full scale). average and peak are means across channels. To convert to a linear 0–1 value, use 10 ** (db / 20).

const recording = await audio.record("memo.m4a", { maxDuration: 30 });

for await (const level of recording.levels({ interval: 100 })) {
  const bar = "█".repeat(Math.round(10 ** (level.peak / 20) * 30));
  console.log(level.currentTime.toFixed(1), bar);
}
// The loop ends when the recording reaches maxDuration.
console.log(await recording.finished);

Availability

APIAppAction extensionWidget
getPermission✓✓✓
requestPermission, record✓——

Errors

Errors have a code property:

codeMeaning
AUDIO_PERMISSION_DENIEDMicrophone access was denied.
AUDIO_UNAVAILABLENo input device is available, the audio session couldn't be activated, or another recording is running.
AUDIO_CONTEXT_UNAVAILABLEThe operation isn't supported here, for example recording in a widget.
AUDIO_RECORDING_FAILEDRecording couldn't start, or a system error such as an encoder failure stopped it.
ENOENTThe recording's parent directory doesn't exist.

Invalid arguments (such as an unsupported extension, or bitRate with .wav) throw a TypeError. Calling pause or resume after the recording has ended throws ERR_INVALID_STATE.

How is this guide?

Last updated on

On this page