@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.
@@ -0,0 +1,372 @@
1
+ import { NativeEventEmitter, NativeModules } from 'react-native';
2
+ import LiveAudioStream from 'react-native-live-audio-stream';
3
+ import type {
4
+ IMicrophone,
5
+ AudioConfig,
6
+ AudioLevel,
7
+ MicrophoneStatus,
8
+ MicrophoneState,
9
+ PermissionResult,
10
+ PCMData,
11
+ AudioDataCallback,
12
+ AudioLevelCallback,
13
+ StateChangeCallback,
14
+ ErrorCallback,
15
+ MicrophoneError,
16
+ } from './types';
17
+ import {
18
+ DEFAULT_AUDIO_CONFIG,
19
+ DEFAULT_AUDIO_LEVEL,
20
+ DEFAULT_LEVEL_UPDATE_INTERVAL,
21
+ } from './constants';
22
+ import {
23
+ base64ToArrayBuffer,
24
+ createPCMTypedArray,
25
+ calculateAudioLevels,
26
+ createMicrophoneError,
27
+ mergeConfig,
28
+ } from './utils';
29
+ import { checkPermission, requestPermission } from './permissions/permissions.native';
30
+
31
+ export class NativeMicrophone implements IMicrophone {
32
+ private eventEmitter: NativeEventEmitter;
33
+ private subscription: ReturnType<NativeEventEmitter['addListener']> | null = null;
34
+
35
+ private config: AudioConfig = DEFAULT_AUDIO_CONFIG;
36
+ private _status: MicrophoneStatus;
37
+ private peakLevel: number = 0;
38
+ private startTime: number = 0;
39
+ private levelIntervalId: ReturnType<typeof setInterval> | null = null;
40
+ private lastLevel: AudioLevel = DEFAULT_AUDIO_LEVEL;
41
+
42
+ // Callbacks
43
+ private audioDataCallbacks: Set<AudioDataCallback> = new Set();
44
+ private audioLevelCallbacks: Map<AudioLevelCallback, number> = new Map();
45
+ private stateChangeCallbacks: Set<StateChangeCallback> = new Set();
46
+ private errorCallbacks: Set<ErrorCallback> = new Set();
47
+
48
+ constructor() {
49
+ this._status = this.createInitialStatus();
50
+
51
+ // Create event emitter for LiveAudioStream
52
+ this.eventEmitter = new NativeEventEmitter(
53
+ NativeModules.LiveAudioStream || NativeModules.RNLiveAudioStream
54
+ );
55
+ }
56
+
57
+ get status(): MicrophoneStatus {
58
+ return { ...this._status };
59
+ }
60
+
61
+ async checkPermission(): Promise<PermissionResult> {
62
+ const result = await checkPermission();
63
+ this.updateStatus({ permission: result.status });
64
+ return result;
65
+ }
66
+
67
+ async requestPermission(): Promise<PermissionResult> {
68
+ const result = await requestPermission();
69
+ this.updateStatus({ permission: result.status });
70
+ return result;
71
+ }
72
+
73
+ async start(config?: Partial<AudioConfig>): Promise<void> {
74
+ if (this._status.state === 'recording') {
75
+ return;
76
+ }
77
+
78
+ this.updateState('starting');
79
+ this.config = mergeConfig(config, DEFAULT_AUDIO_CONFIG);
80
+
81
+ try {
82
+ // Check/request permission
83
+ const permResult = await this.requestPermission();
84
+ if (permResult.status === 'blocked') {
85
+ throw createMicrophoneError(
86
+ 'PERMISSION_BLOCKED',
87
+ 'Microphone permission blocked. Please enable in settings.'
88
+ );
89
+ }
90
+ if (permResult.status === 'denied') {
91
+ throw createMicrophoneError(
92
+ 'PERMISSION_DENIED',
93
+ 'Microphone permission denied'
94
+ );
95
+ }
96
+
97
+ // Initialize LiveAudioStream
98
+ LiveAudioStream.init({
99
+ sampleRate: this.config.sampleRate,
100
+ channels: this.config.channels,
101
+ bitsPerSample: this.config.bitDepth === 32 ? 16 : this.config.bitDepth, // Native doesn't support 32-bit
102
+ audioSource: 6, // VOICE_RECOGNITION on Android for better quality
103
+ bufferSize: this.config.bufferSize,
104
+ wavFile: '', // Empty string = streaming mode (no file output)
105
+ });
106
+
107
+ // Subscribe to audio data events
108
+ this.subscription = this.eventEmitter.addListener(
109
+ 'data',
110
+ (base64Data) => {
111
+ this.handleAudioData(base64Data as string);
112
+ }
113
+ );
114
+
115
+ // Start recording
116
+ LiveAudioStream.start();
117
+
118
+ // Start level metering/duration tracking
119
+ this.startLevelMetering();
120
+
121
+ // Update state
122
+ this.startTime = Date.now();
123
+ this.peakLevel = 0;
124
+ this.updateState('recording');
125
+ this.updateStatus({
126
+ config: this.config,
127
+ permission: 'granted', // If we got here, permission was granted
128
+ });
129
+ } catch (error) {
130
+ this.handleError(error);
131
+ throw error;
132
+ }
133
+ }
134
+
135
+ async stop(): Promise<void> {
136
+ if (this._status.state === 'idle' || this._status.state === 'stopping') {
137
+ return;
138
+ }
139
+
140
+ this.updateState('stopping');
141
+ this.cleanup();
142
+ this.updateState('idle');
143
+ }
144
+
145
+ async pause(): Promise<void> {
146
+ if (this._status.state !== 'recording') {
147
+ return;
148
+ }
149
+
150
+ try {
151
+ // Stop the stream but don't cleanup fully
152
+ LiveAudioStream.stop();
153
+ this.updateState('paused');
154
+ } catch (error) {
155
+ this.handleError(error);
156
+ }
157
+ }
158
+
159
+ async resume(): Promise<void> {
160
+ if (this._status.state !== 'paused') {
161
+ return;
162
+ }
163
+
164
+ try {
165
+ // Restart the stream with same config
166
+ LiveAudioStream.start();
167
+ this.updateState('recording');
168
+ } catch (error) {
169
+ this.handleError(error);
170
+ }
171
+ }
172
+
173
+ onAudioData(callback: AudioDataCallback): () => void {
174
+ this.audioDataCallbacks.add(callback);
175
+ return () => {
176
+ this.audioDataCallbacks.delete(callback);
177
+ };
178
+ }
179
+
180
+ onAudioLevel(
181
+ callback: AudioLevelCallback,
182
+ intervalMs: number = DEFAULT_LEVEL_UPDATE_INTERVAL
183
+ ): () => void {
184
+ this.audioLevelCallbacks.set(callback, intervalMs);
185
+ return () => {
186
+ this.audioLevelCallbacks.delete(callback);
187
+ };
188
+ }
189
+
190
+ onStateChange(callback: StateChangeCallback): () => void {
191
+ this.stateChangeCallbacks.add(callback);
192
+ return () => {
193
+ this.stateChangeCallbacks.delete(callback);
194
+ };
195
+ }
196
+
197
+ onError(callback: ErrorCallback): () => void {
198
+ this.errorCallbacks.add(callback);
199
+ return () => {
200
+ this.errorCallbacks.delete(callback);
201
+ };
202
+ }
203
+
204
+ resetPeakLevel(): void {
205
+ this.peakLevel = 0;
206
+ this.lastLevel = { ...this.lastLevel, peak: 0 };
207
+ }
208
+
209
+ dispose(): void {
210
+ this.cleanup();
211
+ this.audioDataCallbacks.clear();
212
+ this.audioLevelCallbacks.clear();
213
+ this.stateChangeCallbacks.clear();
214
+ this.errorCallbacks.clear();
215
+ }
216
+
217
+ // Private methods
218
+
219
+ private createInitialStatus(): MicrophoneStatus {
220
+ return {
221
+ state: 'idle',
222
+ permission: 'undetermined',
223
+ isRecording: false,
224
+ duration: 0,
225
+ level: DEFAULT_AUDIO_LEVEL,
226
+ config: DEFAULT_AUDIO_CONFIG,
227
+ };
228
+ }
229
+
230
+ private handleAudioData(base64Data: string): void {
231
+ try {
232
+ // Decode Base64 to ArrayBuffer
233
+ const buffer = base64ToArrayBuffer(base64Data);
234
+
235
+ // Create typed array based on bit depth
236
+ const effectiveBitDepth = this.config.bitDepth === 32 ? 16 : this.config.bitDepth;
237
+ const samples = createPCMTypedArray(buffer, effectiveBitDepth);
238
+
239
+ const pcmData: PCMData = {
240
+ buffer,
241
+ samples,
242
+ timestamp: Date.now(),
243
+ config: {
244
+ ...this.config,
245
+ bitDepth: effectiveBitDepth, // Actual bit depth used
246
+ },
247
+ };
248
+
249
+ // Calculate levels from samples
250
+ const level = calculateAudioLevels(samples, effectiveBitDepth, this.peakLevel);
251
+ this.peakLevel = level.peak;
252
+ this.lastLevel = level;
253
+
254
+ // Notify all audio data listeners
255
+ this.audioDataCallbacks.forEach((callback) => {
256
+ try {
257
+ callback(pcmData);
258
+ } catch (e) {
259
+ console.error('Error in audio data callback:', e);
260
+ }
261
+ });
262
+ } catch (error) {
263
+ console.error('Error processing audio data:', error);
264
+ }
265
+ }
266
+
267
+ private startLevelMetering(): void {
268
+ if (this.levelIntervalId) {
269
+ clearInterval(this.levelIntervalId);
270
+ }
271
+
272
+ // Use the smallest interval from all registered callbacks
273
+ let minInterval = DEFAULT_LEVEL_UPDATE_INTERVAL;
274
+ this.audioLevelCallbacks.forEach((interval) => {
275
+ if (interval < minInterval) {
276
+ minInterval = interval;
277
+ }
278
+ });
279
+
280
+ this.levelIntervalId = setInterval(() => {
281
+ // Notify level callbacks with last calculated level
282
+ this.audioLevelCallbacks.forEach((_, callback) => {
283
+ try {
284
+ callback(this.lastLevel);
285
+ } catch (e) {
286
+ console.error('Error in audio level callback:', e);
287
+ }
288
+ });
289
+
290
+ // Update duration
291
+ if (this._status.state === 'recording') {
292
+ this.updateStatus({
293
+ duration: Date.now() - this.startTime,
294
+ level: this.lastLevel,
295
+ });
296
+ }
297
+ }, minInterval);
298
+ }
299
+
300
+ private cleanup(): void {
301
+ if (this.levelIntervalId) {
302
+ clearInterval(this.levelIntervalId);
303
+ this.levelIntervalId = null;
304
+ }
305
+
306
+ if (this.subscription) {
307
+ this.subscription.remove();
308
+ this.subscription = null;
309
+ }
310
+
311
+ try {
312
+ LiveAudioStream.stop();
313
+ } catch {
314
+ // Ignore errors on stop
315
+ }
316
+ }
317
+
318
+ private updateState(state: MicrophoneState): void {
319
+ this.updateStatus({
320
+ state,
321
+ isRecording: state === 'recording',
322
+ });
323
+ }
324
+
325
+ private updateStatus(partial: Partial<MicrophoneStatus>): void {
326
+ this._status = { ...this._status, ...partial };
327
+
328
+ // Notify state change listeners
329
+ this.stateChangeCallbacks.forEach((callback) => {
330
+ try {
331
+ callback(this._status);
332
+ } catch (e) {
333
+ console.error('Error in state change callback:', e);
334
+ }
335
+ });
336
+ }
337
+
338
+ private handleError(error: unknown): void {
339
+ let micError: MicrophoneError;
340
+
341
+ if (error && typeof error === 'object' && 'code' in error) {
342
+ micError = error as MicrophoneError;
343
+ } else if (error instanceof Error) {
344
+ micError = createMicrophoneError(
345
+ 'INITIALIZATION_FAILED',
346
+ error.message,
347
+ error
348
+ );
349
+ } else {
350
+ micError = createMicrophoneError('UNKNOWN', String(error));
351
+ }
352
+
353
+ this.updateStatus({ state: 'error', error: micError });
354
+ this.cleanup();
355
+
356
+ // Notify error listeners
357
+ this.errorCallbacks.forEach((callback) => {
358
+ try {
359
+ callback(micError);
360
+ } catch (e) {
361
+ console.error('Error in error callback:', e);
362
+ }
363
+ });
364
+ }
365
+ }
366
+
367
+ /**
368
+ * Create a new NativeMicrophone instance.
369
+ */
370
+ export function createMicrophone(): IMicrophone {
371
+ return new NativeMicrophone();
372
+ }