auditok 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Amine SEHILI
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md ADDED
@@ -0,0 +1,236 @@
1
+ # auditok.js
2
+
3
+ **Audio activity detection and segmentation for JavaScript** — the
4
+ [auditok](https://github.com/amsehili/auditok) Python package, ported to run
5
+ in Node and in the browser. Zero dependencies.
6
+
7
+ `auditok` finds where the sound is. It splits audio into *events* — regions
8
+ of acoustic activity shaped by duration and silence rules — rather than
9
+ emitting raw per-frame speech/no-speech booleans:
10
+
11
+ - **Event semantics, not frame booleans**: minimum and maximum event
12
+ duration, tolerated silence inside an event, leading/trailing silence
13
+ handling for natural onsets and fadeouts.
14
+ - **Offline file segmentation and online streaming** with the same API:
15
+ events are yielded as soon as their end is decided, while the input is
16
+ still being read, decoded or recorded.
17
+ - **Generic acoustic activity**, not speech-only — music, claps, beeps,
18
+ speech alike. For speech-specific detection, plug any frame-level VAD into
19
+ the tokenizer as a custom validator and get proper event shaping on top of
20
+ its decisions.
21
+ - **A few KB of plain JS** — no ONNX runtime, no model downloads, no native
22
+ modules.
23
+
24
+ The detection engine, energy scale and parameters are those of Python
25
+ auditok: thresholds are in dB relative to 16-bit full scale, and the test
26
+ suite replays fixtures generated by the Python implementation to keep the
27
+ two in lockstep — same tokens, same event boundaries, same estimated
28
+ thresholds.
29
+
30
+ **[Try it in your browser](https://amsehili.github.io/auditok.js/)** — drop
31
+ an audio file or record your microphone, tune the threshold, listen to the
32
+ detected events. Everything runs client-side.
33
+
34
+ ## Install
35
+
36
+ ```sh
37
+ npm install auditok
38
+ ```
39
+
40
+ Node ≥ 18. In Node, WAV files are parsed natively; every other format
41
+ (mp3, ogg, flac, aac, …) and microphone capture go through
42
+ [ffmpeg](https://ffmpeg.org/), which must be on the `PATH`. In the browser,
43
+ decoding and microphone input use the Web Audio API — nothing to install.
44
+
45
+ ## Quick start (Node)
46
+
47
+ ```js
48
+ import { load, split } from "auditok";
49
+
50
+ const audio = await load("audio.mp3"); // WAV parsed natively, rest via ffmpeg
51
+
52
+ for await (const event of split(audio)) {
53
+ console.log(`event: ${event.start.toFixed(3)}s -> ${event.end.toFixed(3)}s`);
54
+ }
55
+ ```
56
+
57
+ Split a large compressed file *while it is being decoded*:
58
+
59
+ ```js
60
+ import { decodeFileStream, split } from "auditok";
61
+
62
+ const stream = decodeFileStream("podcast.mp3", { sampleRate: 16000 });
63
+ for await (const event of split(stream.chunks, { sampleRate: stream.sampleRate })) {
64
+ console.log(event.start, event.end);
65
+ }
66
+ ```
67
+
68
+ Detect events from the microphone (captured through ffmpeg):
69
+
70
+ ```js
71
+ import { microphone, split } from "auditok";
72
+
73
+ const mic = microphone({ sampleRate: 16000 });
74
+ for await (const event of split(mic.chunks, { sampleRate: mic.sampleRate })) {
75
+ console.log(`heard something: ${event.start.toFixed(2)}s, ${event.duration.toFixed(2)}s`);
76
+ }
77
+ ```
78
+
79
+ Save events or processed audio as WAV:
80
+
81
+ ```js
82
+ import { load, fixPauses, save } from "auditok";
83
+
84
+ const cleaned = await fixPauses(await load("interview.wav"), 0.4);
85
+ await save("cleaned.wav", cleaned);
86
+ ```
87
+
88
+ ## Quick start (browser)
89
+
90
+ Same core API; `load` uses the browser's own decoder and `microphone` uses
91
+ getUserMedia + AudioWorklet:
92
+
93
+ ```js
94
+ import { load, microphone, split } from "auditok";
95
+
96
+ // from an <input type="file"> or drag-and-drop
97
+ const audio = await load(fileInput.files[0]);
98
+ for await (const event of split(audio)) {
99
+ highlight(event.start, event.end);
100
+ }
101
+
102
+ // live from the microphone (call from a user gesture)
103
+ const mic = await microphone();
104
+ for await (const event of split(mic.chunks, { sampleRate: mic.sampleRate })) {
105
+ console.log("speech-like activity", event.start, event.end);
106
+ }
107
+ // later: mic.stop()
108
+ ```
109
+
110
+ A Web Audio `AudioBuffer` can be passed to `split` directly.
111
+
112
+ ## API
113
+
114
+ ### `split(input, options)` → `AsyncGenerator<AudioRegion>`
115
+
116
+ `input` is one of: `Float32Array` (mono samples), `Float32Array[]` (planar
117
+ channels), an `AudioRegion`, `{ channelData, sampleRate }`, an
118
+ `AudioBuffer`, or a sync/async iterable of interleaved `Float32Array`
119
+ chunks (the streaming path). Bare samples and chunk streams need
120
+ `options.sampleRate`.
121
+
122
+ | Option | Default | Meaning |
123
+ | --- | --- | --- |
124
+ | `minDur` | `0.2` | Minimum event duration in seconds |
125
+ | `maxDur` | `5` | Maximum event duration (`null` = unlimited); longer events are truncated |
126
+ | `maxSilence` | `0.3` | Maximum continuous silence *within* an event — controls when an event ends |
127
+ | `maxLeadingSilence` | `0` | Silence kept before each event (natural attack) |
128
+ | `maxTrailingSilence` | `null` | Trailing silence kept at the end of each event: `null` keeps all (up to `maxSilence`), `0` drops it, larger values extend collection past the boundary (natural fadeout) |
129
+ | `strictMinDur` | `false` | Reject events shorter than `minDur` even right after a truncated event |
130
+ | `analysisWindow` | `0.05` | Analysis window duration in seconds |
131
+ | `energyThreshold` | `50` | Detection threshold in dB (relative to int16 full scale), used when no `validator` is given |
132
+ | `validator` | — | `"otsu"`, `"percentile"`, `"pXX"` or a function — see below |
133
+ | `useChannel` | `"any"` | Channel used for detection on multichannel audio: `"any"` (max over channels), `"mix"`, or a channel index |
134
+
135
+ `splitAll(input, options)` returns the events as an array.
136
+
137
+ ### Automatic thresholding and custom validators
138
+
139
+ If `validator` is set it is the whole story and `energyThreshold` is
140
+ overlooked:
141
+
142
+ - `"otsu"` splits the energy histogram in two classes (balanced,
143
+ parameter-free);
144
+ - `"percentile"` (alias `"p10"`) or `"pXX"` reads the noise floor at the
145
+ XXth percentile of window energies and adds a 6 dB margin
146
+ (recall-oriented).
147
+
148
+ For in-memory input the threshold is estimated from the whole input before
149
+ detection starts. For chunk-stream input (e.g., a live microphone) it is
150
+ **calibrated on the first `calibrationDur` seconds** (default 3), clamped to
151
+ `minEnergyThreshold` (default 40 dB); the calibration audio is replayed, so
152
+ no data is lost. A digitally silent calibration window (muted microphone)
153
+ falls back to the floor and detection keeps running.
154
+
155
+ A **function** validator receives each analysis window as planar
156
+ per-channel samples and decides its validity — this is how you put proper
157
+ event shaping on top of any frame-level VAD (an energy rule of your own, a
158
+ WASM model, Silero frame decisions, …):
159
+
160
+ ```js
161
+ for await (const event of split(audio, {
162
+ validator: ([samples]) => myFrameVad(samples), // true = active frame
163
+ maxSilence: 0.1,
164
+ })) { ... }
165
+ ```
166
+
167
+ To detect *speech* specifically rather than any acoustic activity, the
168
+ [auditok-webrtcvad](https://www.npmjs.com/package/auditok-webrtcvad)
169
+ add-on (in this repository under `webrtcvad/`) provides the WebRTC voice
170
+ activity detector as such a validator — libfvad compiled to a ~30 KB
171
+ WASM binary, the counterpart of Python auditok's `validator="webrtc:N"`:
172
+
173
+ ```js
174
+ import { createWebrtcValidator } from "auditok-webrtcvad";
175
+
176
+ const validator = await createWebrtcValidator({ sampleRate: 16000, mode: 1 });
177
+ for await (const event of split(audio, { validator, maxSilence: 0.1 })) { ... }
178
+ ```
179
+
180
+ ### `trim(input, options)` → `Promise<AudioRegion>`
181
+
182
+ Audio from the start of the first detection to the end of the last —
183
+ leading and trailing silence removed. Streams are read once and recorded
184
+ while being consumed.
185
+
186
+ ### `fixPauses(input, silenceDuration, options)` → `Promise<AudioRegion>`
187
+
188
+ All detected events (never truncated) joined with exactly
189
+ `silenceDuration` seconds of silence between them.
190
+
191
+ ### `AudioRegion`
192
+
193
+ What `split` yields: `channelData` (planar `Float32Array` per channel),
194
+ `sampleRate`, `start`, `end`, `duration`, `channels`, `sampleCount`, and
195
+ `toWav()` for a WAV-encoded `Uint8Array`.
196
+
197
+ ### Lower level
198
+
199
+ - `StreamTokenizer` — the generic event-shaping state machine, usable over
200
+ any frame type;
201
+ - `calculateEnergy`, `windowEnergy`, `computeFrameEnergies`,
202
+ `estimateEnergyThreshold` — the energy/threshold toolbox;
203
+ - `decodeWav`, `encodeWav` — dependency-free WAV codec (Node and browser);
204
+ - Node: `load`, `save`, `decodeFileStream`, `microphone`;
205
+ - browser: `load`, `microphone`.
206
+
207
+ `import ... from "auditok/core"` gives the environment-free core only.
208
+
209
+ ## Demo
210
+
211
+ The playground lives in `docs/` and is served by GitHub Pages straight from
212
+ the main branch — plain ES modules, no bundler. It imports the built
213
+ library from `docs/lib`; refresh that copy after changing the source with
214
+ `npm run demo:lib`. To try it locally, serve the folder with any static
215
+ server, e.g. `npm run demo:serve`.
216
+
217
+ ## Relationship to Python auditok
218
+
219
+ Same algorithm, same parameters (camelCased), same dB scale — a threshold
220
+ that works with one implementation works with the other, and the
221
+ [Python documentation](https://auditok.readthedocs.io/) is a valid
222
+ reference for the detection semantics. Parity is enforced by fixtures
223
+ generated with the Python package (`tools/generate_fixtures.py`): the JS
224
+ test suite replays ~300 recorded cases through the tokenizer, `split` and
225
+ the threshold estimators.
226
+
227
+ The WebRTC VAD validator (`validator="webrtc:N"` in Python) is available
228
+ as the separate opt-in
229
+ [auditok-webrtcvad](https://www.npmjs.com/package/auditok-webrtcvad)
230
+ package, so the core keeps its size claim honest; its frame decisions are
231
+ parity-tested bit for bit against Python's `webrtcvad`. Not (yet) ported:
232
+ live threshold calibration for streams and the command-line tool.
233
+
234
+ ## License
235
+
236
+ MIT — © Amine Sehili
@@ -0,0 +1,44 @@
1
+ /**
2
+ * Browser entry point: everything from the core, plus decoding of
3
+ * compressed audio through the browser's own decoder
4
+ * (`decodeAudioData` handles mp3/aac/ogg/flac/wav in every browser)
5
+ * and microphone capture through getUserMedia + AudioWorklet. No
6
+ * external assets, no dependencies.
7
+ */
8
+ import { AudioRegion } from "../region.js";
9
+ export * from "../index.js";
10
+ export interface BrowserLoadOptions {
11
+ /** Target sample rate for compressed formats (the browser resamples
12
+ * while decoding), default 16000. WAV files are parsed natively at
13
+ * their own rate unless `forceDecode` is set. */
14
+ sampleRate?: number;
15
+ /** Decode WAV files through the browser decoder too (resampling them
16
+ * to `sampleRate`) instead of parsing them natively. */
17
+ forceDecode?: boolean;
18
+ }
19
+ /**
20
+ * Load audio in the browser into an {@link AudioRegion}. Accepts a
21
+ * `File`/`Blob` (e.g. from a file input or drag-and-drop), an
22
+ * `ArrayBuffer`/`Uint8Array`, or a URL string (fetched with CORS
23
+ * rules applying).
24
+ */
25
+ export declare function load(source: File | Blob | ArrayBuffer | Uint8Array | string, options?: BrowserLoadOptions): Promise<AudioRegion>;
26
+ export interface BrowserMicrophoneOptions {
27
+ /** Sample rate of the capture AudioContext, default 16000. */
28
+ sampleRate?: number;
29
+ }
30
+ export interface BrowserAudioStream {
31
+ /** Mono Float32Array chunks — feed this to `split`. */
32
+ chunks: AsyncIterable<Float32Array>;
33
+ sampleRate: number;
34
+ channels: 1;
35
+ /** Stop the capture and release the microphone. */
36
+ stop(): void;
37
+ }
38
+ /**
39
+ * Capture the microphone as a stream of mono Float32Array chunks via
40
+ * an AudioWorklet (the worklet module is inlined — no asset files).
41
+ * Must be called from a user gesture in most browsers. Call `stop()`
42
+ * to release the microphone.
43
+ */
44
+ export declare function microphone(options?: BrowserMicrophoneOptions): Promise<BrowserAudioStream>;
@@ -0,0 +1,118 @@
1
+ /**
2
+ * Browser entry point: everything from the core, plus decoding of
3
+ * compressed audio through the browser's own decoder
4
+ * (`decodeAudioData` handles mp3/aac/ogg/flac/wav in every browser)
5
+ * and microphone capture through getUserMedia + AudioWorklet. No
6
+ * external assets, no dependencies.
7
+ */
8
+ import { AudioRegion } from "../region.js";
9
+ import { decodeWav, isWav } from "../wav.js";
10
+ export * from "../index.js";
11
+ /**
12
+ * Load audio in the browser into an {@link AudioRegion}. Accepts a
13
+ * `File`/`Blob` (e.g. from a file input or drag-and-drop), an
14
+ * `ArrayBuffer`/`Uint8Array`, or a URL string (fetched with CORS
15
+ * rules applying).
16
+ */
17
+ export async function load(source, options = {}) {
18
+ let bytes;
19
+ if (typeof source === "string") {
20
+ const response = await fetch(source);
21
+ if (!response.ok) {
22
+ throw new Error(`failed to fetch '${source}': ${response.status}`);
23
+ }
24
+ bytes = await response.arrayBuffer();
25
+ }
26
+ else if (source instanceof Blob) {
27
+ bytes = await source.arrayBuffer();
28
+ }
29
+ else if (source instanceof Uint8Array) {
30
+ bytes = source.buffer.slice(source.byteOffset, source.byteOffset + source.byteLength);
31
+ }
32
+ else {
33
+ bytes = source;
34
+ }
35
+ if (options.forceDecode !== true && isWav(bytes)) {
36
+ const { channelData, sampleRate } = decodeWav(bytes);
37
+ return new AudioRegion(channelData, sampleRate);
38
+ }
39
+ const sampleRate = options.sampleRate ?? 16000;
40
+ const context = new OfflineAudioContext({
41
+ numberOfChannels: 1,
42
+ length: 1,
43
+ sampleRate,
44
+ });
45
+ const buffer = await context.decodeAudioData(bytes);
46
+ const channelData = Array.from({ length: buffer.numberOfChannels }, (_, c) => buffer.getChannelData(c));
47
+ return new AudioRegion(channelData, buffer.sampleRate);
48
+ }
49
+ const CAPTURE_WORKLET = `
50
+ class AuditokCapture extends AudioWorkletProcessor {
51
+ process(inputs) {
52
+ const input = inputs[0];
53
+ if (input && input[0] && input[0].length > 0) {
54
+ this.port.postMessage(input[0].slice());
55
+ }
56
+ return true;
57
+ }
58
+ }
59
+ registerProcessor("auditok-capture", AuditokCapture);
60
+ `;
61
+ /**
62
+ * Capture the microphone as a stream of mono Float32Array chunks via
63
+ * an AudioWorklet (the worklet module is inlined — no asset files).
64
+ * Must be called from a user gesture in most browsers. Call `stop()`
65
+ * to release the microphone.
66
+ */
67
+ export async function microphone(options = {}) {
68
+ const sampleRate = options.sampleRate ?? 16000;
69
+ const media = await navigator.mediaDevices.getUserMedia({
70
+ audio: { channelCount: 1 },
71
+ });
72
+ const context = new AudioContext({ sampleRate });
73
+ const workletUrl = URL.createObjectURL(new Blob([CAPTURE_WORKLET], { type: "application/javascript" }));
74
+ try {
75
+ await context.audioWorklet.addModule(workletUrl);
76
+ }
77
+ finally {
78
+ URL.revokeObjectURL(workletUrl);
79
+ }
80
+ const sourceNode = context.createMediaStreamSource(media);
81
+ const captureNode = new AudioWorkletNode(context, "auditok-capture");
82
+ sourceNode.connect(captureNode);
83
+ const pending = [];
84
+ let notify;
85
+ let done = false;
86
+ captureNode.port.onmessage = (event) => {
87
+ pending.push(event.data);
88
+ notify?.();
89
+ };
90
+ async function* chunks() {
91
+ while (!done || pending.length > 0) {
92
+ if (pending.length === 0) {
93
+ await new Promise((resolve) => {
94
+ notify = resolve;
95
+ });
96
+ notify = undefined;
97
+ continue;
98
+ }
99
+ yield pending.shift();
100
+ }
101
+ }
102
+ return {
103
+ chunks: chunks(),
104
+ sampleRate: context.sampleRate,
105
+ channels: 1,
106
+ stop() {
107
+ done = true;
108
+ captureNode.port.onmessage = null;
109
+ sourceNode.disconnect();
110
+ captureNode.disconnect();
111
+ for (const track of media.getTracks()) {
112
+ track.stop();
113
+ }
114
+ void context.close();
115
+ notify?.();
116
+ },
117
+ };
118
+ }
@@ -0,0 +1,14 @@
1
+ /**
2
+ * auditok core: audio activity detection and segmentation over
3
+ * Float32Array samples. Environment-free and dependency-free — this
4
+ * entry point works in Node, browsers, workers and edge runtimes.
5
+ *
6
+ * Importing the package root (`import ... from "auditok"`) adds the
7
+ * platform io helpers (file loading, microphone) on top of this core;
8
+ * `import ... from "auditok/core"` gives exactly this module.
9
+ */
10
+ export { split, splitAll, trim, fixPauses, type AudioInput, type AudioFrame, type AudioBufferLike, type SplitOptions, } from "./split.js";
11
+ export { AudioRegion, makeSilence, concatRegions } from "./region.js";
12
+ export { StreamTokenizer, type FrameValidator, type Token, type StreamTokenizerOptions, } from "./tokenizer.js";
13
+ export { calculateEnergy, windowEnergy, computeFrameEnergies, estimateEnergyThreshold, EPSILON, INT16_SCALE, SILENCE_ENERGY, type ChannelSelection, type ThresholdMethod, type ThresholdMethodOptions, } from "./signal.js";
14
+ export { decodeWav, encodeWav, isWav, type DecodedAudio, type WavEncodeOptions, } from "./wav.js";
package/dist/index.js ADDED
@@ -0,0 +1,14 @@
1
+ /**
2
+ * auditok core: audio activity detection and segmentation over
3
+ * Float32Array samples. Environment-free and dependency-free — this
4
+ * entry point works in Node, browsers, workers and edge runtimes.
5
+ *
6
+ * Importing the package root (`import ... from "auditok"`) adds the
7
+ * platform io helpers (file loading, microphone) on top of this core;
8
+ * `import ... from "auditok/core"` gives exactly this module.
9
+ */
10
+ export { split, splitAll, trim, fixPauses, } from "./split.js";
11
+ export { AudioRegion, makeSilence, concatRegions } from "./region.js";
12
+ export { StreamTokenizer, } from "./tokenizer.js";
13
+ export { calculateEnergy, windowEnergy, computeFrameEnergies, estimateEnergyThreshold, EPSILON, INT16_SCALE, SILENCE_ENERGY, } from "./signal.js";
14
+ export { decodeWav, encodeWav, isWav, } from "./wav.js";
@@ -0,0 +1,47 @@
1
+ /**
2
+ * ffmpeg-backed decoding and capture for Node. ffmpeg is the single
3
+ * external tool auditok relies on outside WAV files — the same policy
4
+ * as the Python package: it decodes every compressed format and also
5
+ * captures audio devices, so the package itself keeps zero runtime
6
+ * dependencies.
7
+ */
8
+ export interface FfmpegStreamOptions {
9
+ /** Output sample rate in Hz (ffmpeg resamples), default 16000. */
10
+ sampleRate?: number;
11
+ /** Output channel count (ffmpeg up/downmixes), default 1. */
12
+ channels?: number;
13
+ /** ffmpeg executable, default "ffmpeg" (must be on PATH). */
14
+ ffmpegPath?: string;
15
+ }
16
+ export interface AudioStream {
17
+ /** Interleaved Float32Array chunks — feed this to `split`. */
18
+ chunks: AsyncIterable<Float32Array>;
19
+ sampleRate: number;
20
+ channels: number;
21
+ /** Stop reading and terminate ffmpeg (for device capture). */
22
+ stop(): void;
23
+ }
24
+ /**
25
+ * Decode an audio file with ffmpeg into a stream of interleaved
26
+ * Float32Array chunks. Handles every format ffmpeg can read (mp3, ogg,
27
+ * flac, aac, video containers, ...), with optional resampling and
28
+ * channel mixing. The stream starts as soon as ffmpeg produces data,
29
+ * so `split` can emit events while the file is still being decoded.
30
+ */
31
+ export declare function decodeFileStream(path: string, options?: FfmpegStreamOptions): AudioStream;
32
+ export interface MicrophoneOptions extends FfmpegStreamOptions {
33
+ /**
34
+ * Capture device. Defaults per platform: `"default"` with PulseAudio
35
+ * on Linux, `":default"` with AVFoundation on macOS. On Windows
36
+ * (DirectShow) there is no default: pass the device name shown by
37
+ * `ffmpeg -list_devices true -f dshow -i dummy`, without the
38
+ * `audio=` prefix.
39
+ */
40
+ device?: string;
41
+ }
42
+ /**
43
+ * Capture the microphone through ffmpeg's device support (PulseAudio /
44
+ * AVFoundation / DirectShow). Returns a stream of interleaved
45
+ * Float32Array chunks; call `stop()` to end the capture.
46
+ */
47
+ export declare function microphone(options?: MicrophoneOptions): AudioStream;
@@ -0,0 +1,117 @@
1
+ /**
2
+ * ffmpeg-backed decoding and capture for Node. ffmpeg is the single
3
+ * external tool auditok relies on outside WAV files — the same policy
4
+ * as the Python package: it decodes every compressed format and also
5
+ * captures audio devices, so the package itself keeps zero runtime
6
+ * dependencies.
7
+ */
8
+ import { spawn } from "node:child_process";
9
+ function runFfmpeg(inputArgs, options) {
10
+ const sampleRate = options.sampleRate ?? 16000;
11
+ const channels = options.channels ?? 1;
12
+ const ffmpegPath = options.ffmpegPath ?? "ffmpeg";
13
+ const args = [
14
+ "-hide_banner",
15
+ "-loglevel",
16
+ "error",
17
+ ...inputArgs,
18
+ "-f",
19
+ "f32le",
20
+ "-acodec",
21
+ "pcm_f32le",
22
+ "-ac",
23
+ String(channels),
24
+ "-ar",
25
+ String(sampleRate),
26
+ "-",
27
+ ];
28
+ const child = spawn(ffmpegPath, args, {
29
+ stdio: ["ignore", "pipe", "pipe"],
30
+ });
31
+ let stderr = "";
32
+ child.stderr.setEncoding("utf8");
33
+ child.stderr.on("data", (text) => {
34
+ stderr += text;
35
+ });
36
+ let stopped = false;
37
+ async function* chunks() {
38
+ // carry bytes that don't end on a float32 boundary to the next chunk
39
+ let carry = Buffer.alloc(0);
40
+ try {
41
+ for await (const data of child.stdout) {
42
+ const buffer = carry.length > 0 ? Buffer.concat([carry, data]) : data;
43
+ const usable = buffer.length - (buffer.length % 4);
44
+ carry = buffer.subarray(usable);
45
+ if (usable === 0) {
46
+ continue;
47
+ }
48
+ const samples = new Float32Array(usable / 4);
49
+ for (let i = 0; i < samples.length; i++) {
50
+ samples[i] = buffer.readFloatLE(i * 4);
51
+ }
52
+ yield samples;
53
+ }
54
+ }
55
+ finally {
56
+ child.kill("SIGKILL");
57
+ }
58
+ const exitCode = await new Promise((resolve) => {
59
+ if (child.exitCode !== null || stopped) {
60
+ resolve(child.exitCode);
61
+ }
62
+ else {
63
+ child.once("close", resolve);
64
+ }
65
+ });
66
+ if (!stopped && exitCode !== 0) {
67
+ throw new Error(`ffmpeg exited with code ${exitCode}: ${stderr.trim()}`);
68
+ }
69
+ }
70
+ return {
71
+ chunks: chunks(),
72
+ sampleRate,
73
+ channels,
74
+ stop() {
75
+ stopped = true;
76
+ child.kill("SIGTERM");
77
+ },
78
+ };
79
+ }
80
+ /**
81
+ * Decode an audio file with ffmpeg into a stream of interleaved
82
+ * Float32Array chunks. Handles every format ffmpeg can read (mp3, ogg,
83
+ * flac, aac, video containers, ...), with optional resampling and
84
+ * channel mixing. The stream starts as soon as ffmpeg produces data,
85
+ * so `split` can emit events while the file is still being decoded.
86
+ */
87
+ export function decodeFileStream(path, options = {}) {
88
+ return runFfmpeg(["-i", path], options);
89
+ }
90
+ /**
91
+ * Capture the microphone through ffmpeg's device support (PulseAudio /
92
+ * AVFoundation / DirectShow). Returns a stream of interleaved
93
+ * Float32Array chunks; call `stop()` to end the capture.
94
+ */
95
+ export function microphone(options = {}) {
96
+ let inputArgs;
97
+ const { device } = options;
98
+ switch (process.platform) {
99
+ case "linux":
100
+ inputArgs = ["-f", "pulse", "-i", device ?? "default"];
101
+ break;
102
+ case "darwin":
103
+ inputArgs = ["-f", "avfoundation", "-i", device ?? ":default"];
104
+ break;
105
+ case "win32":
106
+ if (device === undefined) {
107
+ throw new Error("on Windows, pass the capture device name (see " +
108
+ "`ffmpeg -list_devices true -f dshow -i dummy`)");
109
+ }
110
+ inputArgs = ["-f", "dshow", "-i", `audio=${device}`];
111
+ break;
112
+ default:
113
+ throw new Error(`microphone capture is not supported on platform ` +
114
+ `'${process.platform}'`);
115
+ }
116
+ return runFfmpeg(inputArgs, options);
117
+ }
@@ -0,0 +1,32 @@
1
+ /**
2
+ * Node entry point: everything from the core, plus file loading
3
+ * (native WAV parser, ffmpeg for everything else), microphone capture
4
+ * through ffmpeg, and WAV saving. Zero npm runtime dependencies.
5
+ */
6
+ import { AudioRegion } from "../region.js";
7
+ import { type WavEncodeOptions } from "../wav.js";
8
+ import { type FfmpegStreamOptions } from "./ffmpeg.js";
9
+ export * from "../index.js";
10
+ export { decodeFileStream, microphone, type AudioStream, type FfmpegStreamOptions, type MicrophoneOptions, } from "./ffmpeg.js";
11
+ export interface LoadOptions extends FfmpegStreamOptions {
12
+ /**
13
+ * By default WAV files are parsed natively at their own sample rate
14
+ * and channel count, and other formats are decoded with ffmpeg
15
+ * (resampled to `sampleRate`, mixed to `channels`). Set to true to
16
+ * force the ffmpeg path for WAV files too, e.g. to resample them.
17
+ */
18
+ forceFfmpeg?: boolean;
19
+ }
20
+ /**
21
+ * Load an audio file into an {@link AudioRegion}. WAV files are parsed
22
+ * natively; every other format is decoded with ffmpeg (which must be
23
+ * on the PATH). To detect events while a large compressed file is
24
+ * still being decoded, use {@link decodeFileStream} with `split`
25
+ * instead of loading it fully.
26
+ */
27
+ export declare function load(path: string, options?: LoadOptions): Promise<AudioRegion>;
28
+ /** Save a region (or any `{ channelData, sampleRate }`) as a WAV file. */
29
+ export declare function save(path: string, audio: {
30
+ channelData: readonly Float32Array[];
31
+ sampleRate: number;
32
+ }, options?: WavEncodeOptions): Promise<void>;