@idealyst/microphone 1.1.2

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/types.ts ADDED
@@ -0,0 +1,316 @@
1
+ // ============================================
2
+ // AUDIO CONFIGURATION
3
+ // ============================================
4
+
5
+ export type SampleRate = 8000 | 16000 | 22050 | 44100 | 48000;
6
+ export type BitDepth = 8 | 16 | 32;
7
+ export type ChannelCount = 1 | 2;
8
+
9
+ export interface AudioConfig {
10
+ /** Sample rate in Hz. Default: 16000 (speech-optimized) */
11
+ sampleRate: SampleRate;
12
+
13
+ /** Number of audio channels. Default: 1 (mono) */
14
+ channels: ChannelCount;
15
+
16
+ /** Bits per sample. Default: 16 */
17
+ bitDepth: BitDepth;
18
+
19
+ /**
20
+ * Buffer size for audio processing (samples per callback).
21
+ * Lower = more responsive, higher CPU. Higher = less CPU, more latency.
22
+ * Web: Must be power of 2 (256, 512, 1024, 2048, 4096).
23
+ * Native: Approximate target, actual may vary.
24
+ * Default: 4096
25
+ */
26
+ bufferSize: number;
27
+ }
28
+
29
+ // ============================================
30
+ // PCM DATA TYPES
31
+ // ============================================
32
+
33
+ export interface PCMData {
34
+ /** Raw PCM samples as ArrayBuffer */
35
+ buffer: ArrayBuffer;
36
+
37
+ /** TypedArray view for easy sample access based on bit depth */
38
+ samples: Int8Array | Int16Array | Float32Array;
39
+
40
+ /** Timestamp when this buffer was captured (ms since epoch) */
41
+ timestamp: number;
42
+
43
+ /** Audio configuration this data was captured with */
44
+ config: AudioConfig;
45
+ }
46
+
47
+ export interface AudioLevel {
48
+ /** Current audio level (0.0 - 1.0, normalized) */
49
+ current: number;
50
+
51
+ /** Peak audio level since last reset (0.0 - 1.0) */
52
+ peak: number;
53
+
54
+ /** RMS (root mean square) level for more accurate metering */
55
+ rms: number;
56
+
57
+ /** Decibel value (-Infinity to 0) */
58
+ db: number;
59
+ }
60
+
61
+ // ============================================
62
+ // PERMISSION TYPES
63
+ // ============================================
64
+
65
+ export type PermissionStatus =
66
+ | 'granted'
67
+ | 'denied'
68
+ | 'undetermined'
69
+ | 'blocked' // User denied and "don't ask again" on native
70
+ | 'unavailable'; // No microphone hardware/not supported
71
+
72
+ export interface PermissionResult {
73
+ status: PermissionStatus;
74
+ canAskAgain: boolean;
75
+ }
76
+
77
+ // ============================================
78
+ // MICROPHONE STATE
79
+ // ============================================
80
+
81
+ export type MicrophoneState =
82
+ | 'idle' // Not started
83
+ | 'starting' // Initializing
84
+ | 'recording' // Actively capturing
85
+ | 'paused' // Paused (native only - web must stop/start)
86
+ | 'stopping' // Cleaning up
87
+ | 'error'; // Error state
88
+
89
+ export interface MicrophoneStatus {
90
+ state: MicrophoneState;
91
+
92
+ /** Current permission status */
93
+ permission: PermissionStatus;
94
+
95
+ /** Whether recording is active */
96
+ isRecording: boolean;
97
+
98
+ /** Duration of current recording session in milliseconds */
99
+ duration: number;
100
+
101
+ /** Current audio level metrics */
102
+ level: AudioLevel;
103
+
104
+ /** Error if state is 'error' */
105
+ error?: MicrophoneError;
106
+
107
+ /** Current audio configuration */
108
+ config: AudioConfig;
109
+ }
110
+
111
+ // ============================================
112
+ // ERROR HANDLING
113
+ // ============================================
114
+
115
+ export type MicrophoneErrorCode =
116
+ | 'PERMISSION_DENIED'
117
+ | 'PERMISSION_BLOCKED'
118
+ | 'DEVICE_NOT_FOUND'
119
+ | 'DEVICE_IN_USE'
120
+ | 'NOT_SUPPORTED'
121
+ | 'INITIALIZATION_FAILED'
122
+ | 'RECORDING_FAILED'
123
+ | 'INVALID_CONFIG'
124
+ | 'UNKNOWN';
125
+
126
+ export interface MicrophoneError {
127
+ code: MicrophoneErrorCode;
128
+ message: string;
129
+ originalError?: Error;
130
+ }
131
+
132
+ // ============================================
133
+ // CALLBACK TYPES
134
+ // ============================================
135
+
136
+ export type AudioDataCallback = (data: PCMData) => void;
137
+ export type AudioLevelCallback = (level: AudioLevel) => void;
138
+ export type StateChangeCallback = (status: MicrophoneStatus) => void;
139
+ export type ErrorCallback = (error: MicrophoneError) => void;
140
+
141
+ // ============================================
142
+ // MICROPHONE INTERFACE
143
+ // ============================================
144
+
145
+ export interface IMicrophone {
146
+ /** Current status */
147
+ readonly status: MicrophoneStatus;
148
+
149
+ /** Check microphone permission status */
150
+ checkPermission(): Promise<PermissionResult>;
151
+
152
+ /** Request microphone permission */
153
+ requestPermission(): Promise<PermissionResult>;
154
+
155
+ /**
156
+ * Start recording/streaming audio.
157
+ * @param config Optional audio configuration (uses defaults if not provided)
158
+ */
159
+ start(config?: Partial<AudioConfig>): Promise<void>;
160
+
161
+ /** Stop recording/streaming */
162
+ stop(): Promise<void>;
163
+
164
+ /**
165
+ * Pause recording (native only).
166
+ * On web, this will stop - use start() to resume.
167
+ */
168
+ pause(): Promise<void>;
169
+
170
+ /** Resume recording after pause (native only) */
171
+ resume(): Promise<void>;
172
+
173
+ /**
174
+ * Subscribe to PCM audio data chunks.
175
+ * @returns Unsubscribe function
176
+ */
177
+ onAudioData(callback: AudioDataCallback): () => void;
178
+
179
+ /**
180
+ * Subscribe to audio level updates (for visualization).
181
+ * @param intervalMs Update interval in milliseconds. Default: 100
182
+ * @returns Unsubscribe function
183
+ */
184
+ onAudioLevel(callback: AudioLevelCallback, intervalMs?: number): () => void;
185
+
186
+ /**
187
+ * Subscribe to state changes.
188
+ * @returns Unsubscribe function
189
+ */
190
+ onStateChange(callback: StateChangeCallback): () => void;
191
+
192
+ /**
193
+ * Subscribe to errors.
194
+ * @returns Unsubscribe function
195
+ */
196
+ onError(callback: ErrorCallback): () => void;
197
+
198
+ /** Reset peak level meter */
199
+ resetPeakLevel(): void;
200
+
201
+ /** Clean up resources */
202
+ dispose(): void;
203
+ }
204
+
205
+ // ============================================
206
+ // RECORDER INTERFACE (File Recording)
207
+ // ============================================
208
+
209
+ export type RecordingFormat = 'wav' | 'raw';
210
+
211
+ export interface RecordingOptions {
212
+ /** Output format. Default: 'wav' */
213
+ format: RecordingFormat;
214
+
215
+ /** Audio configuration */
216
+ audioConfig?: Partial<AudioConfig>;
217
+
218
+ /** Maximum recording duration in seconds. 0 = unlimited */
219
+ maxDuration?: number;
220
+ }
221
+
222
+ export interface RecordingResult {
223
+ /** File path (native) or Blob URL (web) */
224
+ uri: string;
225
+
226
+ /** Duration in milliseconds */
227
+ duration: number;
228
+
229
+ /** File size in bytes */
230
+ size: number;
231
+
232
+ /** Audio configuration used */
233
+ config: AudioConfig;
234
+
235
+ /** Format of the recording */
236
+ format: RecordingFormat;
237
+
238
+ /** Get the recording as ArrayBuffer (for upload, processing, etc.) */
239
+ getArrayBuffer(): Promise<ArrayBuffer>;
240
+
241
+ /** Get the recording as Blob (web) or base64 string (native) */
242
+ getData(): Promise<Blob | string>;
243
+ }
244
+
245
+ export interface IRecorder {
246
+ /** Whether currently recording to file */
247
+ readonly isRecording: boolean;
248
+
249
+ /** Current recording duration in milliseconds */
250
+ readonly duration: number;
251
+
252
+ /** Start recording to file */
253
+ startRecording(options?: RecordingOptions): Promise<void>;
254
+
255
+ /** Stop recording and get result */
256
+ stopRecording(): Promise<RecordingResult>;
257
+
258
+ /** Cancel recording without saving */
259
+ cancelRecording(): Promise<void>;
260
+
261
+ /** Clean up resources */
262
+ dispose(): void;
263
+ }
264
+
265
+ // ============================================
266
+ // HOOK TYPES
267
+ // ============================================
268
+
269
+ export interface UseMicrophoneOptions {
270
+ /** Audio configuration */
271
+ config?: Partial<AudioConfig>;
272
+
273
+ /** Auto-request permission on mount. Default: false */
274
+ autoRequestPermission?: boolean;
275
+
276
+ /** Audio level update interval in ms. Default: 100 */
277
+ levelUpdateInterval?: number;
278
+ }
279
+
280
+ export interface UseMicrophoneResult {
281
+ // State
282
+ status: MicrophoneStatus;
283
+ isRecording: boolean;
284
+ isPaused: boolean;
285
+ level: AudioLevel;
286
+ error: MicrophoneError | null;
287
+ permission: PermissionStatus;
288
+
289
+ // Actions
290
+ start: (config?: Partial<AudioConfig>) => Promise<void>;
291
+ stop: () => Promise<void>;
292
+ pause: () => Promise<void>;
293
+ resume: () => Promise<void>;
294
+ requestPermission: () => Promise<PermissionResult>;
295
+ resetPeakLevel: () => void;
296
+
297
+ // Data subscription (for custom processing)
298
+ subscribeToAudioData: (callback: AudioDataCallback) => () => void;
299
+ }
300
+
301
+ export interface UseRecorderOptions {
302
+ /** Recording options */
303
+ options?: RecordingOptions;
304
+ }
305
+
306
+ export interface UseRecorderResult {
307
+ // State
308
+ isRecording: boolean;
309
+ duration: number;
310
+ error: MicrophoneError | null;
311
+
312
+ // Actions
313
+ startRecording: (options?: RecordingOptions) => Promise<void>;
314
+ stopRecording: () => Promise<RecordingResult>;
315
+ cancelRecording: () => Promise<void>;
316
+ }
package/src/utils.ts ADDED
@@ -0,0 +1,217 @@
1
+ import type { AudioConfig, AudioLevel, BitDepth } from './types';
2
+ import { BIT_DEPTH_MAX_VALUES } from './constants';
3
+
4
+ /**
5
+ * Decode Base64 string to ArrayBuffer.
6
+ * Works in both web and React Native environments.
7
+ */
8
+ export function base64ToArrayBuffer(base64: string): ArrayBuffer {
9
+ // Use atob for web, Buffer for React Native
10
+ const binaryString =
11
+ typeof atob !== 'undefined'
12
+ ? atob(base64)
13
+ : Buffer.from(base64, 'base64').toString('binary');
14
+
15
+ const bytes = new Uint8Array(binaryString.length);
16
+ for (let i = 0; i < binaryString.length; i++) {
17
+ bytes[i] = binaryString.charCodeAt(i);
18
+ }
19
+ return bytes.buffer;
20
+ }
21
+
22
+ /**
23
+ * Encode ArrayBuffer to Base64 string.
24
+ * Works in both web and React Native environments.
25
+ */
26
+ export function arrayBufferToBase64(buffer: ArrayBuffer): string {
27
+ const bytes = new Uint8Array(buffer);
28
+ let binary = '';
29
+ for (let i = 0; i < bytes.length; i++) {
30
+ binary += String.fromCharCode(bytes[i]);
31
+ }
32
+ return typeof btoa !== 'undefined'
33
+ ? btoa(binary)
34
+ : Buffer.from(binary, 'binary').toString('base64');
35
+ }
36
+
37
+ /**
38
+ * Create appropriate TypedArray for PCM data based on bit depth.
39
+ */
40
+ export function createPCMTypedArray(
41
+ buffer: ArrayBuffer,
42
+ bitDepth: BitDepth
43
+ ): Int8Array | Int16Array | Float32Array {
44
+ switch (bitDepth) {
45
+ case 8:
46
+ return new Int8Array(buffer);
47
+ case 16:
48
+ return new Int16Array(buffer);
49
+ case 32:
50
+ return new Float32Array(buffer);
51
+ default:
52
+ return new Int16Array(buffer);
53
+ }
54
+ }
55
+
56
+ /**
57
+ * Calculate audio levels from PCM samples.
58
+ * @param samples - PCM sample data
59
+ * @param bitDepth - Bit depth of the samples
60
+ * @param previousPeak - Previous peak value to maintain peak hold
61
+ * @returns Audio level metrics
62
+ */
63
+ export function calculateAudioLevels(
64
+ samples: Int8Array | Int16Array | Float32Array,
65
+ bitDepth: BitDepth,
66
+ previousPeak: number = 0
67
+ ): AudioLevel {
68
+ const maxValue = BIT_DEPTH_MAX_VALUES[bitDepth];
69
+
70
+ let sum = 0;
71
+ let peak = previousPeak;
72
+ let current = 0;
73
+
74
+ for (let i = 0; i < samples.length; i++) {
75
+ const normalized = Math.abs(samples[i]) / maxValue;
76
+ sum += normalized * normalized;
77
+ if (normalized > current) {
78
+ current = normalized;
79
+ }
80
+ if (normalized > peak) {
81
+ peak = normalized;
82
+ }
83
+ }
84
+
85
+ const rms = Math.sqrt(sum / samples.length);
86
+ const db = rms > 0 ? 20 * Math.log10(rms) : -Infinity;
87
+
88
+ return { current, peak, rms, db };
89
+ }
90
+
91
+ /**
92
+ * Convert Float32 samples (-1.0 to 1.0) to Int16 samples.
93
+ */
94
+ export function float32ToInt16(float32Array: Float32Array): Int16Array {
95
+ const int16Array = new Int16Array(float32Array.length);
96
+ for (let i = 0; i < float32Array.length; i++) {
97
+ // Clamp and convert
98
+ const s = Math.max(-1, Math.min(1, float32Array[i]));
99
+ int16Array[i] = s < 0 ? s * 0x8000 : s * 0x7fff;
100
+ }
101
+ return int16Array;
102
+ }
103
+
104
+ /**
105
+ * Convert Float32 samples (-1.0 to 1.0) to Int8 samples.
106
+ */
107
+ export function float32ToInt8(float32Array: Float32Array): Int8Array {
108
+ const int8Array = new Int8Array(float32Array.length);
109
+ for (let i = 0; i < float32Array.length; i++) {
110
+ const s = Math.max(-1, Math.min(1, float32Array[i]));
111
+ int8Array[i] = s < 0 ? s * 0x80 : s * 0x7f;
112
+ }
113
+ return int8Array;
114
+ }
115
+
116
+ /**
117
+ * Write a string to a DataView at a specific offset.
118
+ */
119
+ function writeString(view: DataView, offset: number, string: string): void {
120
+ for (let i = 0; i < string.length; i++) {
121
+ view.setUint8(offset + i, string.charCodeAt(i));
122
+ }
123
+ }
124
+
125
+ /**
126
+ * Generate WAV file header.
127
+ * @param dataLength - Length of audio data in bytes
128
+ * @param config - Audio configuration
129
+ * @returns ArrayBuffer containing the 44-byte WAV header
130
+ */
131
+ export function createWavHeader(
132
+ dataLength: number,
133
+ config: AudioConfig
134
+ ): ArrayBuffer {
135
+ const headerLength = 44;
136
+ const header = new ArrayBuffer(headerLength);
137
+ const view = new DataView(header);
138
+
139
+ const bytesPerSample = config.bitDepth / 8;
140
+ const blockAlign = config.channels * bytesPerSample;
141
+ const byteRate = config.sampleRate * blockAlign;
142
+
143
+ // RIFF header
144
+ writeString(view, 0, 'RIFF');
145
+ view.setUint32(4, 36 + dataLength, true); // File size - 8
146
+ writeString(view, 8, 'WAVE');
147
+
148
+ // fmt subchunk
149
+ writeString(view, 12, 'fmt ');
150
+ view.setUint32(16, 16, true); // Subchunk1Size (16 for PCM)
151
+ view.setUint16(20, config.bitDepth === 32 ? 3 : 1, true); // AudioFormat (1=PCM, 3=IEEE float)
152
+ view.setUint16(22, config.channels, true);
153
+ view.setUint32(24, config.sampleRate, true);
154
+ view.setUint32(28, byteRate, true);
155
+ view.setUint16(32, blockAlign, true);
156
+ view.setUint16(34, config.bitDepth, true);
157
+
158
+ // data subchunk
159
+ writeString(view, 36, 'data');
160
+ view.setUint32(40, dataLength, true);
161
+
162
+ return header;
163
+ }
164
+
165
+ /**
166
+ * Concatenate multiple ArrayBuffers into one.
167
+ */
168
+ export function concatArrayBuffers(buffers: ArrayBuffer[]): ArrayBuffer {
169
+ const totalLength = buffers.reduce((sum, buf) => sum + buf.byteLength, 0);
170
+ const result = new Uint8Array(totalLength);
171
+
172
+ let offset = 0;
173
+ for (const buffer of buffers) {
174
+ result.set(new Uint8Array(buffer), offset);
175
+ offset += buffer.byteLength;
176
+ }
177
+
178
+ return result.buffer;
179
+ }
180
+
181
+ /**
182
+ * Create a complete WAV file from PCM data.
183
+ * @param pcmData - Raw PCM audio data
184
+ * @param config - Audio configuration
185
+ * @returns ArrayBuffer containing complete WAV file
186
+ */
187
+ export function createWavFile(
188
+ pcmData: ArrayBuffer,
189
+ config: AudioConfig
190
+ ): ArrayBuffer {
191
+ const header = createWavHeader(pcmData.byteLength, config);
192
+ return concatArrayBuffers([header, pcmData]);
193
+ }
194
+
195
+ /**
196
+ * Create a MicrophoneError object.
197
+ */
198
+ export function createMicrophoneError(
199
+ code: import('./types').MicrophoneErrorCode,
200
+ message: string,
201
+ originalError?: Error
202
+ ): import('./types').MicrophoneError {
203
+ return { code, message, originalError };
204
+ }
205
+
206
+ /**
207
+ * Merge partial config with defaults.
208
+ */
209
+ export function mergeConfig(
210
+ partial: Partial<AudioConfig> | undefined,
211
+ defaults: AudioConfig
212
+ ): AudioConfig {
213
+ return {
214
+ ...defaults,
215
+ ...partial,
216
+ };
217
+ }