@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/README.md +225 -0
- package/package.json +67 -0
- package/src/constants.ts +73 -0
- package/src/hooks/createUseMicrophoneHook.ts +144 -0
- package/src/hooks/createUseRecorderHook.ts +101 -0
- package/src/hooks/index.native.ts +2 -0
- package/src/hooks/index.ts +2 -0
- package/src/hooks/index.web.ts +2 -0
- package/src/hooks/useMicrophone.native.ts +45 -0
- package/src/hooks/useMicrophone.web.ts +43 -0
- package/src/hooks/useRecorder.native.ts +37 -0
- package/src/hooks/useRecorder.web.ts +37 -0
- package/src/index.native.ts +79 -0
- package/src/index.ts +79 -0
- package/src/index.web.ts +79 -0
- package/src/microphone.native.ts +372 -0
- package/src/microphone.web.ts +502 -0
- package/src/permissions/index.native.ts +1 -0
- package/src/permissions/index.ts +1 -0
- package/src/permissions/index.web.ts +1 -0
- package/src/permissions/permissions.native.ts +112 -0
- package/src/permissions/permissions.web.ts +78 -0
- package/src/react-native.d.ts +49 -0
- package/src/recorder/index.native.ts +1 -0
- package/src/recorder/index.ts +1 -0
- package/src/recorder/index.web.ts +1 -0
- package/src/recorder/recorder.native.ts +176 -0
- package/src/recorder/recorder.web.ts +174 -0
- package/src/types.ts +316 -0
- package/src/utils.ts +217 -0
|
@@ -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
|
+
}
|