@happyvertical/speech 0.102.4 → 0.102.5

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.
@@ -0,0 +1,89 @@
1
+ /**
2
+ * Hands-free microphone capture: opens the microphone, runs its audio through
3
+ * the on-device voice activity detector (`vad.ts`) and emits one finished
4
+ * utterance at a time as 16 kHz mono Float32 PCM. Audio stays in memory; the
5
+ * microphone is released by `stop()`. Nothing here touches Node APIs; the Web
6
+ * Audio globals are read when `createVadCapture` is called, so importing this
7
+ * module is SSR-safe.
8
+ */
9
+ import { type PcmCapture, type PcmCaptureOptions } from './pcm-capture.js';
10
+ import { type VadEndReason, type VadOptions } from './vad.js';
11
+ /** Rate of the PCM in `speechend` events. */
12
+ export declare const VAD_CAPTURE_SAMPLE_RATE = 16000;
13
+ /** Events emitted by a `VadCapture`. */
14
+ export interface VadCaptureEventMap {
15
+ /** An utterance began. */
16
+ speechstart: Record<string, never>;
17
+ /** An utterance ended (a pause, the length cap, or `stop()`). */
18
+ speechend: {
19
+ /** Mono samples, -1..1, at `sampleRate`, including the pre-roll. */
20
+ samples: Float32Array;
21
+ sampleRate: number;
22
+ durationMs: number;
23
+ reason: VadEndReason;
24
+ };
25
+ /** Loudness 0..1 for each analysed frame, for a meter. */
26
+ level: {
27
+ level: number;
28
+ };
29
+ }
30
+ export type VadCaptureEventType = keyof VadCaptureEventMap;
31
+ export interface VadCaptureOptions extends VadOptions {
32
+ /**
33
+ * Use this stream instead of opening the microphone. The caller keeps
34
+ * ownership: `stop()` does not stop its tracks.
35
+ */
36
+ stream?: MediaStream;
37
+ /**
38
+ * Constraints for the microphone when no `stream` is given. Default:
39
+ * `{ audio: { echoCancellation, noiseSuppression, autoGainControl: true } }`,
40
+ * a secondary defence against hearing the page's own playback. Browsers do
41
+ * not reliably cancel `speechSynthesis`; use `suspend()` while it plays.
42
+ */
43
+ constraints?: MediaStreamConstraints;
44
+ /**
45
+ * After `resume()`, audio of this length is still ignored so the tail of the
46
+ * playback and its room reverb are not heard as speech. Default 350.
47
+ */
48
+ resumeGuardMs?: number;
49
+ /** Test seam: replaces `createPcmCapture`. */
50
+ createCapture?: (stream: MediaStream, options: PcmCaptureOptions) => PcmCapture;
51
+ }
52
+ export interface VadCapture {
53
+ /** Subscribes to an event; returns the unsubscribe function. */
54
+ on<T extends VadCaptureEventType>(type: T, listener: (event: VadCaptureEventMap[T]) => void): () => void;
55
+ /** Currently inside an utterance. */
56
+ readonly speaking: boolean;
57
+ /** Half-duplex gate is closed (between `suspend()` and `resume()`). */
58
+ readonly suspended: boolean;
59
+ /**
60
+ * Half-duplex gate: stops listening while the page itself is playing audio
61
+ * (for example `speechSynthesis`). Frames are dropped, an utterance in
62
+ * progress is discarded (no `speechend`), the noise floor does not adapt to
63
+ * the played audio, and the microphone stays open. Idempotent.
64
+ */
65
+ suspend(): void;
66
+ /**
67
+ * Reopens the gate after `resumeGuardMs` of further audio has been ignored.
68
+ * No-op when not suspended.
69
+ */
70
+ resume(): void;
71
+ /**
72
+ * Ends an utterance in progress (a final `speechend` with reason `flush`),
73
+ * then releases the microphone. Idempotent.
74
+ */
75
+ stop(): Promise<void>;
76
+ /** Releases the microphone and discards any utterance in progress. Idempotent. */
77
+ cancel(): void;
78
+ }
79
+ /** Whether this runtime can open a microphone and tap it with Web Audio. */
80
+ export declare function vadCaptureSupported(): boolean;
81
+ /**
82
+ * Opens the microphone (or uses `options.stream`) and starts detecting speech.
83
+ * Rejects with `PcmCaptureError` (`unsupported`) when Web Audio or the
84
+ * microphone API is missing, and with the browser's own error when permission
85
+ * is denied.
86
+ */
87
+ export declare function createVadCapture(options?: VadCaptureOptions): Promise<VadCapture>;
88
+ export type { VadEndReason, VadOptions } from './vad.js';
89
+ //# sourceMappingURL=vad-capture.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"vad-capture.d.ts","sourceRoot":"","sources":["../../src/shared/vad-capture.ts"],"names":[],"mappings":"AAAA;;;;;;;GAOG;AAGH,OAAO,EAEL,KAAK,UAAU,EACf,KAAK,iBAAiB,EAEvB,MAAM,kBAAkB,CAAC;AAE1B,OAAO,EAGL,KAAK,YAAY,EACjB,KAAK,UAAU,EAChB,MAAM,UAAU,CAAC;AAElB,6CAA6C;AAC7C,eAAO,MAAM,uBAAuB,QAAS,CAAC;AAE9C,wCAAwC;AACxC,MAAM,WAAW,kBAAkB;IACjC,0BAA0B;IAC1B,WAAW,EAAE,MAAM,CAAC,MAAM,EAAE,KAAK,CAAC,CAAC;IACnC,iEAAiE;IACjE,SAAS,EAAE;QACT,oEAAoE;QACpE,OAAO,EAAE,YAAY,CAAC;QACtB,UAAU,EAAE,MAAM,CAAC;QACnB,UAAU,EAAE,MAAM,CAAC;QACnB,MAAM,EAAE,YAAY,CAAC;KACtB,CAAC;IACF,0DAA0D;IAC1D,KAAK,EAAE;QAAE,KAAK,EAAE,MAAM,CAAA;KAAE,CAAC;CAC1B;AAED,MAAM,MAAM,mBAAmB,GAAG,MAAM,kBAAkB,CAAC;AAE3D,MAAM,WAAW,iBAAkB,SAAQ,UAAU;IACnD;;;OAGG;IACH,MAAM,CAAC,EAAE,WAAW,CAAC;IACrB;;;;;OAKG;IACH,WAAW,CAAC,EAAE,sBAAsB,CAAC;IACrC;;;OAGG;IACH,aAAa,CAAC,EAAE,MAAM,CAAC;IACvB,8CAA8C;IAC9C,aAAa,CAAC,EAAE,CACd,MAAM,EAAE,WAAW,EACnB,OAAO,EAAE,iBAAiB,KACvB,UAAU,CAAC;CACjB;AAED,MAAM,WAAW,UAAU;IACzB,gEAAgE;IAChE,EAAE,CAAC,CAAC,SAAS,mBAAmB,EAC9B,IAAI,EAAE,CAAC,EACP,QAAQ,EAAE,CAAC,KAAK,EAAE,kBAAkB,CAAC,CAAC,CAAC,KAAK,IAAI,GAC/C,MAAM,IAAI,CAAC;IACd,qCAAqC;IACrC,QAAQ,CAAC,QAAQ,EAAE,OAAO,CAAC;IAC3B,uEAAuE;IACvE,QAAQ,CAAC,SAAS,EAAE,OAAO,CAAC;IAC5B;;;;;OAKG;IACH,OAAO,IAAI,IAAI,CAAC;IAChB;;;OAGG;IACH,MAAM,IAAI,IAAI,CAAC;IACf;;;OAGG;IACH,IAAI,IAAI,OAAO,CAAC,IAAI,CAAC,CAAC;IACtB,kFAAkF;IAClF,MAAM,IAAI,IAAI,CAAC;CAChB;AAED,4EAA4E;AAC5E,wBAAgB,mBAAmB,IAAI,OAAO,CAM7C;AAKD;;;;;GAKG;AACH,wBAAsB,gBAAgB,CACpC,OAAO,GAAE,iBAAsB,GAC9B,OAAO,CAAC,UAAU,CAAC,CAoJrB;AAED,YAAY,EAAE,YAAY,EAAE,UAAU,EAAE,MAAM,UAAU,CAAC"}
@@ -0,0 +1,122 @@
1
+ /**
2
+ * Voice activity detection for hands-free capture.
3
+ *
4
+ * Energy based, no model and no download: the loudness (RMS) of short audio
5
+ * frames is compared with a noise floor learned from the room. Speech onset
6
+ * starts an utterance and a pause ends it. No Node APIs, no DOM; safe in
7
+ * browsers, workers and Node.
8
+ *
9
+ * Two pure pieces:
10
+ * - `createVadSegmenter`: fed one frame energy at a time, answers with
11
+ * `start` / `end` / `split` events and a live level;
12
+ * - `createUtteranceCollector`: fed raw mono PCM, keeps a pre-roll so the
13
+ * first syllable is not clipped and hands over each finished utterance.
14
+ *
15
+ * `createVadCapture` (`@happyvertical/speech/browser`) drives the collector
16
+ * from a microphone.
17
+ */
18
+ /** Tuning for the detector. Every field is optional. */
19
+ export interface VadOptions {
20
+ /** Silence that ends an utterance, in ms. Default 800. */
21
+ silenceMs?: number;
22
+ /** Speech shorter than this is a click or a cough, not an utterance. Default 150. */
23
+ minSpeechMs?: number;
24
+ /** Audio kept from before the onset so the first syllable is not clipped. Default 300. */
25
+ preRollMs?: number;
26
+ /** An utterance this long is cut and the next one starts at once. Default 30000. */
27
+ maxUtteranceMs?: number;
28
+ /**
29
+ * 0 (needs loud, close speech) to 1 (picks up quiet speech, and more
30
+ * noise). Default 0.5.
31
+ */
32
+ sensitivity?: number;
33
+ /**
34
+ * How long the room is listened to before the floor is settled, in ms.
35
+ * Default 300. Speech is still detected during this window (with a
36
+ * conservative floor), and calibration only uses a low percentile of the
37
+ * energies, so speaking from the first frame does not poison the floor.
38
+ */
39
+ calibrateMs?: number;
40
+ }
41
+ export declare const VAD_DEFAULTS: {
42
+ readonly silenceMs: 800;
43
+ readonly minSpeechMs: 150;
44
+ readonly preRollMs: 300;
45
+ readonly maxUtteranceMs: 30000;
46
+ readonly sensitivity: 0.5;
47
+ readonly calibrateMs: 300;
48
+ };
49
+ /** Length of one analysed frame. */
50
+ export declare const VAD_FRAME_MS = 20;
51
+ export type VadEndReason = 'silence' | 'max' | 'flush';
52
+ export type VadEvent =
53
+ /** Speech began; `leadMs` is how much of the recent past belongs to it. */
54
+ {
55
+ type: 'start';
56
+ leadMs: number;
57
+ }
58
+ /** The utterance ended; `trailingSilenceMs` of it is silence. */
59
+ | {
60
+ type: 'end';
61
+ reason: VadEndReason;
62
+ trailingSilenceMs: number;
63
+ }
64
+ /** `maxUtteranceMs` reached while speaking: end this one, start the next. */
65
+ | {
66
+ type: 'split';
67
+ };
68
+ export interface VadStep {
69
+ events: VadEvent[];
70
+ /** 0 to 1, how loud this frame is against what counts as speech. */
71
+ level: number;
72
+ /** Currently inside an utterance. */
73
+ speaking: boolean;
74
+ }
75
+ export interface VadSegmenter {
76
+ /** Feed the RMS energy (0 to 1) of the next frame of `frameMs`. */
77
+ push(energy: number, frameMs?: number): VadStep;
78
+ /** Finish: ends an utterance in progress (`end` with reason `flush`). */
79
+ flush(): VadEvent[];
80
+ readonly speaking: boolean;
81
+ /** The learned noise floor (RMS). */
82
+ readonly noiseFloor: number;
83
+ /** The energy above which a frame counts as speech. */
84
+ readonly threshold: number;
85
+ }
86
+ /** Resolve options against the defaults, clamping silly values. */
87
+ export declare function resolveVadOptions(options?: VadOptions): Required<VadOptions>;
88
+ /** Root-mean-square loudness of a block of samples (-1 to 1). */
89
+ export declare function frameEnergy(samples: ArrayLike<number>): number;
90
+ export declare function createVadSegmenter(options?: VadOptions): VadSegmenter;
91
+ /** One finished utterance. */
92
+ export interface VadUtterance {
93
+ /** Mono samples (-1 to 1) at `sampleRate`. */
94
+ pcm: Float32Array;
95
+ sampleRate: number;
96
+ durationMs: number;
97
+ reason: VadEndReason;
98
+ }
99
+ export interface UtteranceCollectorOptions extends VadOptions {
100
+ /** Sample rate of the PCM fed in. */
101
+ sampleRate: number;
102
+ onUtterance: (utterance: VadUtterance) => void;
103
+ /** An utterance began (`true`) or ended (`false`). */
104
+ onSpeaking?: (speaking: boolean) => void;
105
+ /** Loudness 0 to 1 for each frame, for a meter. */
106
+ onLevel?: (level: number) => void;
107
+ }
108
+ export interface UtteranceCollector {
109
+ /** Feed any amount of mono PCM. */
110
+ push(samples: Float32Array): void;
111
+ /** Hand over an utterance in progress and reset. */
112
+ flush(): void;
113
+ /**
114
+ * Drop an utterance in progress and all buffered audio (pre-roll included)
115
+ * without emitting it. The learned noise floor is kept.
116
+ */
117
+ discard(): void;
118
+ readonly speaking: boolean;
119
+ readonly segmenter: VadSegmenter;
120
+ }
121
+ export declare function createUtteranceCollector(options: UtteranceCollectorOptions): UtteranceCollector;
122
+ //# sourceMappingURL=vad.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"vad.d.ts","sourceRoot":"","sources":["../../src/shared/vad.ts"],"names":[],"mappings":"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"}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@happyvertical/speech",
3
- "version": "0.102.4",
3
+ "version": "0.102.5",
4
4
  "description": "Speech provider abstraction for STT and TTS backends",
5
5
  "type": "module",
6
6
  "main": "./dist/index.js",
@@ -1 +0,0 @@
1
- {"version":3,"file":"wav-B0Djpj2r.js","names":[],"sources":["../../src/shared/resample.ts","../../src/shared/wav.ts"],"sourcesContent":["/**\n * Mono resampler: windowed-sinc (Hann, 16 zero crossings per side)\n * interpolation. When downsampling, the kernel's cutoff is scaled to 95% of\n * the target Nyquist frequency so content above it is attenuated before it can\n * alias; when upsampling it is a plain band-limited interpolator. Edges treat\n * missing samples as absent and renormalise the kernel, so DC gain stays 1.\n * Quality is suited to speech recognition (passband flat to roughly 90% of\n * Nyquist, stopband well below -40 dB), not mastering. Cost is\n * O(output length x taps), taps ~ 32 x max(1, fromRate / toRate).\n */\n\nimport { WavFormatError } from './errors.js';\n\n/** Largest output a single call may produce (samples). */\nexport const MAX_RESAMPLE_OUTPUT_SAMPLES = 2 ** 28;\n\nconst ZERO_CROSSINGS = 16;\nconst DOWNSAMPLE_CUTOFF = 0.95;\n\nfunction sinc(x: number): number {\n if (x === 0) return 1;\n const px = Math.PI * x;\n return Math.sin(px) / px;\n}\n\n/** Resamples mono float samples from `fromRate` to `toRate` (Hz). */\nexport function resampleMono(\n samples: Float32Array,\n fromRate: number,\n toRate: number,\n): Float32Array {\n for (const [name, rate] of [\n ['fromRate', fromRate],\n ['toRate', toRate],\n ] as const) {\n if (!Number.isFinite(rate) || rate <= 0) {\n throw new WavFormatError(\n 'invalid_argument',\n `${name} must be a positive finite number (got ${String(rate)})`,\n );\n }\n }\n const length = Math.floor((samples.length * toRate) / fromRate);\n if (length > MAX_RESAMPLE_OUTPUT_SAMPLES) {\n throw new WavFormatError(\n 'invalid_argument',\n `resampled output would be ${length} samples; the limit is ${MAX_RESAMPLE_OUTPUT_SAMPLES}`,\n );\n }\n if (fromRate === toRate) return samples.slice();\n const out = new Float32Array(length);\n const step = fromRate / toRate;\n // Cutoff relative to the input Nyquist; 1 means no low-pass.\n const cutoff = step > 1 ? DOWNSAMPLE_CUTOFF / step : 1;\n const halfWidth = ZERO_CROSSINGS / cutoff;\n const last = samples.length - 1;\n\n for (let index = 0; index < length; index += 1) {\n const center = index * step;\n const first = Math.max(0, Math.ceil(center - halfWidth));\n const end = Math.min(last, Math.floor(center + halfWidth));\n let sum = 0;\n let weights = 0;\n for (let tap = first; tap <= end; tap += 1) {\n const distance = tap - center;\n const window = 0.5 + 0.5 * Math.cos((Math.PI * distance) / halfWidth);\n const weight = cutoff * sinc(cutoff * distance) * window;\n sum += weight * samples[tap];\n weights += weight;\n }\n out[index] = weights !== 0 ? sum / weights : 0;\n }\n return out;\n}\n","/**\n * Pure, dependency-free 16-bit PCM WAV helpers. No Node APIs and no DOM: they\n * run in the browser, in Node and in workers. Exposed as\n * `@happyvertical/speech/pcm`.\n *\n * `parseWavPcm16` is strict because it reads untrusted bytes: every declared\n * size is checked against the buffer before it is trusted, nothing is\n * allocated from a declared (rather than present) size, and work is linear in\n * the input length.\n */\n\nimport { WavFormatError } from './errors.js';\n\nconst HEADER_BYTES = 44;\nconst MAX_UINT32 = 0xffff_ffff;\nconst FORMAT_PCM = 1;\nconst FORMAT_EXTENSIBLE = 0xfffe;\n/** `KSDATAFORMAT_SUBTYPE_PCM` minus its leading format tag: 00000000-0010-8000-00AA-00389B71. */\nconst PCM_GUID_TAIL = [\n 0x00, 0x00, 0x00, 0x00, 0x10, 0x00, 0x80, 0x00, 0x00, 0xaa, 0x00, 0x38, 0x9b,\n 0x71,\n];\n\n/**\n * Converts float samples (-1..1) to 16-bit PCM (scale 32768, the inverse of\n * `pcm16ToFloat32`, so Int16 round-trips exactly). Clamps; NaN becomes 0.\n */\nexport function float32ToPcm16(samples: Float32Array): Int16Array {\n const out = new Int16Array(samples.length);\n for (let index = 0; index < samples.length; index += 1) {\n const value = samples[index];\n if (Number.isNaN(value)) continue; // stays 0\n const clamped = value > 1 ? 1 : value < -1 ? -1 : value;\n out[index] = Math.min(0x7fff, Math.round(clamped * 0x8000));\n }\n return out;\n}\n\n/** Converts 16-bit PCM to float samples in -1..1 (divides by 32768). */\nexport function pcm16ToFloat32(samples: Int16Array): Float32Array {\n const out = new Float32Array(samples.length);\n for (let index = 0; index < samples.length; index += 1) {\n out[index] = samples[index] / 0x8000;\n }\n return out;\n}\n\nfunction assertSampleRate(sampleRate: number, what: string): void {\n // byteRate = sampleRate * 2 must fit an unsigned 32-bit field.\n if (\n !Number.isInteger(sampleRate) ||\n sampleRate < 1 ||\n sampleRate * 2 > MAX_UINT32\n ) {\n throw new WavFormatError(\n 'invalid_argument',\n `${what} must be a positive integer sample rate (got ${String(sampleRate)})`,\n );\n }\n}\n\nfunction writeTag(view: DataView, offset: number, tag: string): void {\n for (let index = 0; index < tag.length; index += 1) {\n view.setUint8(offset + index, tag.charCodeAt(index));\n }\n}\n\n/**\n * Encodes mono samples as a canonical 44-byte-header 16-bit PCM WAV. Float\n * input is clamped and converted; `Int16Array` is written as is.\n */\nexport function encodeWavPcm16(\n samples: Float32Array | Int16Array,\n sampleRate: number,\n): Uint8Array {\n assertSampleRate(sampleRate, 'sampleRate');\n const dataBytes = samples.length * 2;\n if (dataBytes + HEADER_BYTES - 8 > MAX_UINT32) {\n throw new WavFormatError(\n 'invalid_argument',\n 'too many samples for a single WAV (4 GiB limit)',\n );\n }\n const pcm = samples instanceof Int16Array ? samples : float32ToPcm16(samples);\n const bytes = new Uint8Array(HEADER_BYTES + dataBytes);\n const view = new DataView(bytes.buffer);\n writeTag(view, 0, 'RIFF');\n view.setUint32(4, 36 + dataBytes, true);\n writeTag(view, 8, 'WAVE');\n writeTag(view, 12, 'fmt ');\n view.setUint32(16, 16, true);\n view.setUint16(20, FORMAT_PCM, true);\n view.setUint16(22, 1, true);\n view.setUint32(24, sampleRate, true);\n view.setUint32(28, sampleRate * 2, true);\n view.setUint16(32, 2, true);\n view.setUint16(34, 16, true);\n writeTag(view, 36, 'data');\n view.setUint32(40, dataBytes, true);\n for (let index = 0; index < pcm.length; index += 1) {\n view.setInt16(HEADER_BYTES + index * 2, pcm[index], true);\n }\n return bytes;\n}\n\n/** Constraints `parseWavPcm16` enforces on the declared format. */\nexport interface WavExpectation {\n /** Required sample rate in Hz. */\n sampleRate?: number;\n /** Required channel count. */\n channels?: number;\n}\n\n/** A validated 16-bit PCM WAV. */\nexport interface ParsedWavPcm16 {\n sampleRate: number;\n channels: number;\n /** Frames (one sample per channel). */\n frames: number;\n durationMs: number;\n /** Interleaved samples, decoded to native endianness (a copy). */\n samples: Int16Array;\n /** The little-endian PCM bytes, a view into the input. */\n data: Uint8Array;\n}\n\nfunction fail(\n reason: ConstructorParameters<typeof WavFormatError>[0],\n message: string,\n): never {\n throw new WavFormatError(reason, message);\n}\n\nfunction tagAt(bytes: Uint8Array, offset: number): string {\n return String.fromCharCode(\n bytes[offset],\n bytes[offset + 1],\n bytes[offset + 2],\n bytes[offset + 3],\n );\n}\n\ninterface Fmt {\n channels: number;\n sampleRate: number;\n}\n\nfunction readFmt(\n view: DataView,\n bytes: Uint8Array,\n body: number,\n size: number,\n): Fmt {\n if (size < 16) {\n fail('malformed_chunk', `fmt chunk is ${size} bytes; at least 16 required`);\n }\n let format = view.getUint16(body, true);\n const channels = view.getUint16(body + 2, true);\n const sampleRate = view.getUint32(body + 4, true);\n const byteRate = view.getUint32(body + 8, true);\n const blockAlign = view.getUint16(body + 12, true);\n const bits = view.getUint16(body + 14, true);\n\n if (format === FORMAT_EXTENSIBLE) {\n if (size < 40) {\n fail(\n 'malformed_chunk',\n `WAVE_FORMAT_EXTENSIBLE fmt chunk is ${size} bytes; at least 40 required`,\n );\n }\n const cbSize = view.getUint16(body + 16, true);\n if (cbSize < 22 || 18 + cbSize > size) {\n fail(\n 'malformed_chunk',\n `WAVE_FORMAT_EXTENSIBLE extension (${cbSize} bytes) is under 22 or runs past its fmt chunk`,\n );\n }\n const validBits = view.getUint16(body + 18, true);\n if (validBits !== 0 && validBits !== 16) {\n fail(\n 'unsupported_bits',\n `WAVE_FORMAT_EXTENSIBLE valid bits is ${validBits}; only 16 supported`,\n );\n }\n const guid = body + 24;\n const subFormat = view.getUint16(guid, true);\n const tailMatches = PCM_GUID_TAIL.every(\n (byte, index) => bytes[guid + 2 + index] === byte,\n );\n if (subFormat !== FORMAT_PCM || !tailMatches) {\n fail('unsupported_format', 'WAVE_FORMAT_EXTENSIBLE subtype is not PCM');\n }\n format = FORMAT_PCM;\n }\n\n if (format !== FORMAT_PCM) {\n fail('unsupported_format', `WAV format tag ${format} is not PCM`);\n }\n if (bits !== 16) {\n fail(\n 'unsupported_bits',\n `WAV is ${bits}-bit; only 16-bit PCM is supported`,\n );\n }\n if (channels < 1) {\n fail('inconsistent_header', 'WAV declares zero channels');\n }\n if (sampleRate < 1) {\n fail('inconsistent_header', 'WAV declares a zero sample rate');\n }\n if (blockAlign !== channels * 2) {\n fail(\n 'inconsistent_header',\n `WAV block align ${blockAlign} is not channels x 2 (${channels * 2})`,\n );\n }\n if (byteRate !== sampleRate * blockAlign) {\n fail(\n 'inconsistent_header',\n 'WAV byte rate is not sample rate x block align',\n );\n }\n return { channels, sampleRate };\n}\n\n/**\n * Strictly parses a 16-bit PCM WAV. Rejects (with `WavFormatError`) anything\n * that is not a well-formed RIFF/WAVE with a `fmt ` chunk before its `data`\n * chunk, format tag 1 (or WAVE_FORMAT_EXTENSIBLE with the PCM subtype), 16\n * bits, a consistent header, and a declared data length that lies within the\n * buffer, is non-zero and is a whole number of frames. Nothing is clamped or\n * guessed. Every chunk inside the declared RIFF is bounds-checked, including\n * those after `data`; bytes beyond the RIFF are ignored.\n */\nexport function parseWavPcm16(\n bytes: Uint8Array,\n expect: WavExpectation = {},\n): ParsedWavPcm16 {\n if (\n bytes.length < 12 ||\n tagAt(bytes, 0) !== 'RIFF' ||\n tagAt(bytes, 8) !== 'WAVE'\n ) {\n fail('not_riff', 'not a RIFF/WAVE file');\n }\n const view = new DataView(bytes.buffer, bytes.byteOffset, bytes.byteLength);\n const riffEnd = 8 + view.getUint32(4, true);\n if (riffEnd < 12) {\n fail('malformed_chunk', 'RIFF size is too small');\n }\n if (riffEnd > bytes.length) {\n fail(\n 'truncated',\n `RIFF declares ${riffEnd} bytes but only ${bytes.length} are present`,\n );\n }\n\n let fmt: Fmt | undefined;\n let dataAt: number | undefined;\n let dataSize = 0;\n let offset = 12;\n while (offset < riffEnd) {\n if (offset + 8 > riffEnd) {\n fail('malformed_chunk', 'chunk header runs past the end of the RIFF');\n }\n const id = tagAt(bytes, offset);\n const size = view.getUint32(offset + 4, true);\n const body = offset + 8;\n if (body + size > riffEnd) {\n fail(\n 'truncated',\n `${JSON.stringify(id)} chunk declares ${size} bytes but only ${riffEnd - body} remain`,\n );\n }\n\n if (id === 'fmt ') {\n if (fmt) fail('duplicate_fmt', 'WAV has more than one fmt chunk');\n fmt = readFmt(view, bytes, body, size);\n if (\n expect.sampleRate !== undefined &&\n fmt.sampleRate !== expect.sampleRate\n ) {\n fail(\n 'rate_mismatch',\n `WAV is ${fmt.sampleRate} Hz; expected ${expect.sampleRate} Hz`,\n );\n }\n if (expect.channels !== undefined && fmt.channels !== expect.channels) {\n fail(\n 'channel_mismatch',\n `WAV has ${fmt.channels} channel(s); expected ${expect.channels}`,\n );\n }\n } else if (id === 'data') {\n if (dataAt !== undefined) {\n fail('malformed_chunk', 'WAV has more than one data chunk');\n }\n if (!fmt)\n fail('data_before_fmt', 'WAV data chunk precedes its fmt chunk');\n if (size === 0) fail('empty_data', 'WAV data chunk is empty');\n const frameBytes = fmt.channels * 2;\n if (size % frameBytes !== 0) {\n fail(\n 'misaligned_data',\n `WAV data (${size} bytes) is not a whole number of ${frameBytes}-byte frames`,\n );\n }\n dataAt = body;\n dataSize = size;\n }\n // RIFF chunks are word aligned: an odd size is followed by one pad byte,\n // which must also lie inside the RIFF.\n const next = body + size + (size % 2);\n if (next > riffEnd) {\n fail('truncated', `${JSON.stringify(id)} chunk is missing its pad byte`);\n }\n offset = next;\n }\n if (!fmt || dataAt === undefined) {\n return fail(\n fmt ? 'missing_data' : 'missing_fmt',\n fmt ? 'WAV has no data chunk' : 'WAV has no fmt chunk',\n );\n }\n // Every chunk inside the RIFF has now been bounds-checked; decode.\n const data = bytes.subarray(dataAt, dataAt + dataSize);\n const samples = new Int16Array(dataSize / 2);\n for (let index = 0; index < samples.length; index += 1) {\n samples[index] = view.getInt16(dataAt + index * 2, true);\n }\n const frames = dataSize / (fmt.channels * 2);\n return {\n sampleRate: fmt.sampleRate,\n channels: fmt.channels,\n frames,\n durationMs: Math.round((frames / fmt.sampleRate) * 1000),\n samples,\n data,\n };\n}\n"],"mappings":"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"}