@idealyst/audio 1.2.48

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/src/utils.ts ADDED
@@ -0,0 +1,379 @@
1
+ import type { AudioError, AudioErrorCode, BitDepth, AudioConfig, AudioLevel } from './types';
2
+ import { BIT_DEPTH_MAX_VALUES } from './constants';
3
+
4
+ /**
5
+ * Create an AudioError object.
6
+ */
7
+ export function createAudioError(
8
+ code: AudioErrorCode,
9
+ message: string,
10
+ originalError?: Error
11
+ ): AudioError {
12
+ return { code, message, originalError };
13
+ }
14
+
15
+ /**
16
+ * Convert Int8 samples to Float32 samples (-1.0 to 1.0).
17
+ */
18
+ export function int8ToFloat32(int8Array: Int8Array): Float32Array {
19
+ const float32Array = new Float32Array(int8Array.length);
20
+ for (let i = 0; i < int8Array.length; i++) {
21
+ float32Array[i] = int8Array[i] / 128;
22
+ }
23
+ return float32Array;
24
+ }
25
+
26
+ /**
27
+ * Convert Int16 samples to Float32 samples (-1.0 to 1.0).
28
+ */
29
+ export function int16ToFloat32(int16Array: Int16Array): Float32Array {
30
+ const float32Array = new Float32Array(int16Array.length);
31
+ for (let i = 0; i < int16Array.length; i++) {
32
+ float32Array[i] = int16Array[i] / 32768;
33
+ }
34
+ return float32Array;
35
+ }
36
+
37
+ /**
38
+ * Convert Float32 samples (-1.0 to 1.0) to Int16 samples.
39
+ */
40
+ export function float32ToInt16(float32Array: Float32Array): Int16Array {
41
+ const int16Array = new Int16Array(float32Array.length);
42
+ for (let i = 0; i < float32Array.length; i++) {
43
+ const s = Math.max(-1, Math.min(1, float32Array[i]));
44
+ int16Array[i] = s < 0 ? s * 0x8000 : s * 0x7fff;
45
+ }
46
+ return int16Array;
47
+ }
48
+
49
+ /**
50
+ * Convert ArrayBuffer to appropriate TypedArray based on bit depth.
51
+ */
52
+ export function bufferToTypedArray(
53
+ buffer: ArrayBuffer,
54
+ bitDepth: BitDepth
55
+ ): Int8Array | Int16Array | Float32Array {
56
+ switch (bitDepth) {
57
+ case 8:
58
+ return new Int8Array(buffer);
59
+ case 16:
60
+ return new Int16Array(buffer);
61
+ case 32:
62
+ return new Float32Array(buffer);
63
+ default:
64
+ return new Int16Array(buffer);
65
+ }
66
+ }
67
+
68
+ /**
69
+ * Convert PCM samples to Float32 based on bit depth.
70
+ */
71
+ export function pcmToFloat32(
72
+ samples: ArrayBuffer | Int8Array | Int16Array | Float32Array,
73
+ bitDepth: BitDepth
74
+ ): Float32Array {
75
+ if (samples instanceof ArrayBuffer) {
76
+ samples = bufferToTypedArray(samples, bitDepth);
77
+ }
78
+
79
+ if (samples instanceof Float32Array) {
80
+ return samples;
81
+ }
82
+
83
+ if (samples instanceof Int8Array) {
84
+ return int8ToFloat32(samples);
85
+ }
86
+
87
+ if (samples instanceof Int16Array) {
88
+ return int16ToFloat32(samples);
89
+ }
90
+
91
+ return new Float32Array(0);
92
+ }
93
+
94
+ /**
95
+ * Simple linear interpolation resampling.
96
+ */
97
+ export function resampleLinear(
98
+ samples: Float32Array,
99
+ fromRate: number,
100
+ toRate: number
101
+ ): Float32Array {
102
+ if (fromRate === toRate) {
103
+ return samples;
104
+ }
105
+
106
+ const ratio = fromRate / toRate;
107
+ const newLength = Math.round(samples.length / ratio);
108
+ const result = new Float32Array(newLength);
109
+
110
+ for (let i = 0; i < newLength; i++) {
111
+ const srcIndex = i * ratio;
112
+ const srcIndexFloor = Math.floor(srcIndex);
113
+ const srcIndexCeil = Math.min(srcIndexFloor + 1, samples.length - 1);
114
+ const fraction = srcIndex - srcIndexFloor;
115
+
116
+ result[i] =
117
+ samples[srcIndexFloor] * (1 - fraction) +
118
+ samples[srcIndexCeil] * fraction;
119
+ }
120
+
121
+ return result;
122
+ }
123
+
124
+ /**
125
+ * Interleave mono samples to stereo.
126
+ */
127
+ export function monoToStereo(mono: Float32Array): Float32Array {
128
+ const stereo = new Float32Array(mono.length * 2);
129
+ for (let i = 0; i < mono.length; i++) {
130
+ stereo[i * 2] = mono[i];
131
+ stereo[i * 2 + 1] = mono[i];
132
+ }
133
+ return stereo;
134
+ }
135
+
136
+ /**
137
+ * Convert stereo samples to mono (average).
138
+ */
139
+ export function stereoToMono(stereo: Float32Array): Float32Array {
140
+ const mono = new Float32Array(stereo.length / 2);
141
+ for (let i = 0; i < mono.length; i++) {
142
+ mono[i] = (stereo[i * 2] + stereo[i * 2 + 1]) / 2;
143
+ }
144
+ return mono;
145
+ }
146
+
147
+ /**
148
+ * Deinterleave stereo samples into separate channels.
149
+ */
150
+ export function deinterleave(stereo: Float32Array): [Float32Array, Float32Array] {
151
+ const length = stereo.length / 2;
152
+ const left = new Float32Array(length);
153
+ const right = new Float32Array(length);
154
+
155
+ for (let i = 0; i < length; i++) {
156
+ left[i] = stereo[i * 2];
157
+ right[i] = stereo[i * 2 + 1];
158
+ }
159
+
160
+ return [left, right];
161
+ }
162
+
163
+ /**
164
+ * Interleave separate channels into stereo.
165
+ */
166
+ export function interleave(left: Float32Array, right: Float32Array): Float32Array {
167
+ const stereo = new Float32Array(left.length * 2);
168
+ for (let i = 0; i < left.length; i++) {
169
+ stereo[i * 2] = left[i];
170
+ stereo[i * 2 + 1] = right[i];
171
+ }
172
+ return stereo;
173
+ }
174
+
175
+ /**
176
+ * Concatenate multiple Float32Arrays into one.
177
+ */
178
+ export function concatFloat32Arrays(arrays: Float32Array[]): Float32Array {
179
+ const totalLength = arrays.reduce((sum, arr) => sum + arr.length, 0);
180
+ const result = new Float32Array(totalLength);
181
+
182
+ let offset = 0;
183
+ for (const arr of arrays) {
184
+ result.set(arr, offset);
185
+ offset += arr.length;
186
+ }
187
+
188
+ return result;
189
+ }
190
+
191
+ /**
192
+ * Concatenate multiple ArrayBuffers into one.
193
+ */
194
+ export function concatArrayBuffers(buffers: ArrayBuffer[]): ArrayBuffer {
195
+ const totalLength = buffers.reduce((sum, buf) => sum + buf.byteLength, 0);
196
+ const result = new Uint8Array(totalLength);
197
+
198
+ let offset = 0;
199
+ for (const buffer of buffers) {
200
+ result.set(new Uint8Array(buffer), offset);
201
+ offset += buffer.byteLength;
202
+ }
203
+
204
+ return result.buffer;
205
+ }
206
+
207
+ /**
208
+ * Calculate duration in milliseconds from sample count and sample rate.
209
+ */
210
+ export function samplesToDuration(sampleCount: number, sampleRate: number): number {
211
+ return (sampleCount / sampleRate) * 1000;
212
+ }
213
+
214
+ /**
215
+ * Calculate sample count from duration in milliseconds.
216
+ */
217
+ export function durationToSamples(durationMs: number, sampleRate: number): number {
218
+ return Math.round((durationMs / 1000) * sampleRate);
219
+ }
220
+
221
+ /**
222
+ * Merge partial config with defaults.
223
+ */
224
+ export function mergeConfig<T extends object>(
225
+ partial: Partial<T> | undefined,
226
+ defaults: T
227
+ ): T {
228
+ return {
229
+ ...defaults,
230
+ ...partial,
231
+ };
232
+ }
233
+
234
+ /**
235
+ * Clamp a value between min and max.
236
+ */
237
+ export function clamp(value: number, min: number, max: number): number {
238
+ return Math.max(min, Math.min(max, value));
239
+ }
240
+
241
+ /**
242
+ * Calculate RMS (Root Mean Square) level from samples.
243
+ */
244
+ export function calculateRMS(samples: Float32Array): number {
245
+ let sum = 0;
246
+ for (let i = 0; i < samples.length; i++) {
247
+ sum += samples[i] * samples[i];
248
+ }
249
+ return Math.sqrt(sum / samples.length);
250
+ }
251
+
252
+ /**
253
+ * Calculate audio levels from samples.
254
+ */
255
+ export function calculateAudioLevels(
256
+ samples: Int8Array | Int16Array | Float32Array,
257
+ bitDepth: BitDepth,
258
+ currentPeak: number = 0
259
+ ): AudioLevel {
260
+ const maxValue = BIT_DEPTH_MAX_VALUES[bitDepth];
261
+ let sum = 0;
262
+ let max = 0;
263
+
264
+ for (let i = 0; i < samples.length; i++) {
265
+ const normalized = Math.abs(samples[i]) / maxValue;
266
+ sum += normalized * normalized;
267
+ if (normalized > max) max = normalized;
268
+ }
269
+
270
+ const rms = Math.sqrt(sum / samples.length);
271
+ const peak = Math.max(currentPeak, max);
272
+ const db = rms > 0 ? 20 * Math.log10(rms) : -Infinity;
273
+
274
+ return {
275
+ current: max,
276
+ peak,
277
+ rms,
278
+ db,
279
+ };
280
+ }
281
+
282
+ /**
283
+ * Create a typed array for PCM samples based on bit depth.
284
+ */
285
+ export function createPCMTypedArray(
286
+ buffer: ArrayBuffer,
287
+ bitDepth: BitDepth
288
+ ): Int8Array | Int16Array | Float32Array {
289
+ switch (bitDepth) {
290
+ case 8:
291
+ return new Int8Array(buffer);
292
+ case 16:
293
+ return new Int16Array(buffer);
294
+ case 32:
295
+ return new Float32Array(buffer);
296
+ default:
297
+ return new Int16Array(buffer);
298
+ }
299
+ }
300
+
301
+ /**
302
+ * Convert ArrayBuffer to base64 string.
303
+ */
304
+ export function arrayBufferToBase64(buffer: ArrayBuffer): string {
305
+ const bytes = new Uint8Array(buffer);
306
+ let binary = '';
307
+ for (let i = 0; i < bytes.length; i++) {
308
+ binary += String.fromCharCode(bytes[i]);
309
+ }
310
+ return typeof btoa !== 'undefined'
311
+ ? btoa(binary)
312
+ : Buffer.from(binary, 'binary').toString('base64');
313
+ }
314
+
315
+ /**
316
+ * Convert base64 string to ArrayBuffer.
317
+ */
318
+ export function base64ToArrayBuffer(base64: string): ArrayBuffer {
319
+ const binaryString =
320
+ typeof atob !== 'undefined'
321
+ ? atob(base64)
322
+ : Buffer.from(base64, 'base64').toString('binary');
323
+
324
+ const bytes = new Uint8Array(binaryString.length);
325
+ for (let i = 0; i < binaryString.length; i++) {
326
+ bytes[i] = binaryString.charCodeAt(i);
327
+ }
328
+ return bytes.buffer;
329
+ }
330
+
331
+ /**
332
+ * Create a WAV file header.
333
+ */
334
+ export function createWavHeader(
335
+ dataLength: number,
336
+ config: AudioConfig
337
+ ): ArrayBuffer {
338
+ const header = new ArrayBuffer(44);
339
+ const view = new DataView(header);
340
+
341
+ const bytesPerSample = config.bitDepth / 8;
342
+ const blockAlign = config.channels * bytesPerSample;
343
+ const byteRate = config.sampleRate * blockAlign;
344
+
345
+ // RIFF header
346
+ writeString(view, 0, 'RIFF');
347
+ view.setUint32(4, 36 + dataLength, true);
348
+ writeString(view, 8, 'WAVE');
349
+
350
+ // fmt chunk
351
+ writeString(view, 12, 'fmt ');
352
+ view.setUint32(16, 16, true); // chunk size
353
+ view.setUint16(20, 1, true); // audio format (PCM)
354
+ view.setUint16(22, config.channels, true);
355
+ view.setUint32(24, config.sampleRate, true);
356
+ view.setUint32(28, byteRate, true);
357
+ view.setUint16(32, blockAlign, true);
358
+ view.setUint16(34, config.bitDepth, true);
359
+
360
+ // data chunk
361
+ writeString(view, 36, 'data');
362
+ view.setUint32(40, dataLength, true);
363
+
364
+ return header;
365
+ }
366
+
367
+ function writeString(view: DataView, offset: number, str: string): void {
368
+ for (let i = 0; i < str.length; i++) {
369
+ view.setUint8(offset + i, str.charCodeAt(i));
370
+ }
371
+ }
372
+
373
+ /**
374
+ * Create a WAV file from PCM data.
375
+ */
376
+ export function createWavFile(pcmData: ArrayBuffer, config: AudioConfig): Blob {
377
+ const header = createWavHeader(pcmData.byteLength, config);
378
+ return new Blob([header, pcmData], { type: 'audio/wav' });
379
+ }