@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,49 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Minimal type declarations for react-native.
|
|
3
|
+
* Full types are provided by the consuming application.
|
|
4
|
+
*/
|
|
5
|
+
declare module 'react-native' {
|
|
6
|
+
export const Platform: {
|
|
7
|
+
OS: 'ios' | 'android' | 'windows' | 'macos' | 'web';
|
|
8
|
+
Version: number | string;
|
|
9
|
+
select: <T>(specifics: { [platform: string]: T }) => T;
|
|
10
|
+
};
|
|
11
|
+
|
|
12
|
+
export const PermissionsAndroid: {
|
|
13
|
+
PERMISSIONS: {
|
|
14
|
+
RECORD_AUDIO: string;
|
|
15
|
+
[key: string]: string;
|
|
16
|
+
};
|
|
17
|
+
RESULTS: {
|
|
18
|
+
GRANTED: 'granted';
|
|
19
|
+
DENIED: 'denied';
|
|
20
|
+
NEVER_ASK_AGAIN: 'never_ask_again';
|
|
21
|
+
[key: string]: string;
|
|
22
|
+
};
|
|
23
|
+
check: (permission: string) => Promise<boolean>;
|
|
24
|
+
request: (
|
|
25
|
+
permission: string,
|
|
26
|
+
rationale?: {
|
|
27
|
+
title: string;
|
|
28
|
+
message: string;
|
|
29
|
+
buttonNeutral?: string;
|
|
30
|
+
buttonNegative?: string;
|
|
31
|
+
buttonPositive?: string;
|
|
32
|
+
}
|
|
33
|
+
) => Promise<'granted' | 'denied' | 'never_ask_again'>;
|
|
34
|
+
};
|
|
35
|
+
|
|
36
|
+
export const NativeModules: {
|
|
37
|
+
[key: string]: unknown;
|
|
38
|
+
};
|
|
39
|
+
|
|
40
|
+
export class NativeEventEmitter {
|
|
41
|
+
constructor(nativeModule?: unknown);
|
|
42
|
+
addListener(
|
|
43
|
+
eventType: string,
|
|
44
|
+
listener: (...args: unknown[]) => void
|
|
45
|
+
): { remove: () => void };
|
|
46
|
+
removeAllListeners(eventType: string): void;
|
|
47
|
+
removeSubscription(subscription: { remove: () => void }): void;
|
|
48
|
+
}
|
|
49
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export { NativeRecorder, createRecorder } from './recorder.native';
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export { WebRecorder, createRecorder } from './recorder.web';
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export { WebRecorder, createRecorder } from './recorder.web';
|
|
@@ -0,0 +1,176 @@
|
|
|
1
|
+
import type {
|
|
2
|
+
IRecorder,
|
|
3
|
+
RecordingOptions,
|
|
4
|
+
RecordingResult,
|
|
5
|
+
AudioConfig,
|
|
6
|
+
PCMData,
|
|
7
|
+
} from '../types';
|
|
8
|
+
import { DEFAULT_AUDIO_CONFIG } from '../constants';
|
|
9
|
+
import {
|
|
10
|
+
createWavFile,
|
|
11
|
+
concatArrayBuffers,
|
|
12
|
+
mergeConfig,
|
|
13
|
+
createMicrophoneError,
|
|
14
|
+
arrayBufferToBase64,
|
|
15
|
+
} from '../utils';
|
|
16
|
+
import { NativeMicrophone } from '../microphone.native';
|
|
17
|
+
|
|
18
|
+
export class NativeRecorder implements IRecorder {
|
|
19
|
+
private microphone: NativeMicrophone | null = null;
|
|
20
|
+
private chunks: ArrayBuffer[] = [];
|
|
21
|
+
private startTime: number = 0;
|
|
22
|
+
private _isRecording: boolean = false;
|
|
23
|
+
private _duration: number = 0;
|
|
24
|
+
private config: AudioConfig = DEFAULT_AUDIO_CONFIG;
|
|
25
|
+
private format: 'wav' | 'raw' = 'wav';
|
|
26
|
+
private maxDuration: number = 0;
|
|
27
|
+
private durationIntervalId: ReturnType<typeof setInterval> | null = null;
|
|
28
|
+
private unsubscribeData: (() => void) | null = null;
|
|
29
|
+
|
|
30
|
+
get isRecording(): boolean {
|
|
31
|
+
return this._isRecording;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
get duration(): number {
|
|
35
|
+
return this._duration;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
async startRecording(options?: RecordingOptions): Promise<void> {
|
|
39
|
+
if (this._isRecording) {
|
|
40
|
+
throw createMicrophoneError(
|
|
41
|
+
'RECORDING_FAILED',
|
|
42
|
+
'Already recording'
|
|
43
|
+
);
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
this.format = options?.format ?? 'wav';
|
|
47
|
+
this.maxDuration = options?.maxDuration ?? 0;
|
|
48
|
+
this.config = mergeConfig(options?.audioConfig, DEFAULT_AUDIO_CONFIG);
|
|
49
|
+
|
|
50
|
+
// Create new microphone instance for recording
|
|
51
|
+
this.microphone = new NativeMicrophone();
|
|
52
|
+
this.chunks = [];
|
|
53
|
+
this.startTime = Date.now();
|
|
54
|
+
this._duration = 0;
|
|
55
|
+
|
|
56
|
+
// Subscribe to audio data
|
|
57
|
+
this.unsubscribeData = this.microphone.onAudioData((pcmData: PCMData) => {
|
|
58
|
+
this.chunks.push(pcmData.buffer.slice(0)); // Clone buffer
|
|
59
|
+
});
|
|
60
|
+
|
|
61
|
+
// Start microphone
|
|
62
|
+
await this.microphone.start(this.config);
|
|
63
|
+
this._isRecording = true;
|
|
64
|
+
|
|
65
|
+
// Start duration tracking
|
|
66
|
+
this.durationIntervalId = setInterval(() => {
|
|
67
|
+
this._duration = Date.now() - this.startTime;
|
|
68
|
+
|
|
69
|
+
// Check max duration
|
|
70
|
+
if (this.maxDuration > 0 && this._duration >= this.maxDuration * 1000) {
|
|
71
|
+
this.stopRecording().catch(console.error);
|
|
72
|
+
}
|
|
73
|
+
}, 100);
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
async stopRecording(): Promise<RecordingResult> {
|
|
77
|
+
if (!this._isRecording || !this.microphone) {
|
|
78
|
+
throw createMicrophoneError(
|
|
79
|
+
'RECORDING_FAILED',
|
|
80
|
+
'Not currently recording'
|
|
81
|
+
);
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
// Stop duration tracking
|
|
85
|
+
if (this.durationIntervalId) {
|
|
86
|
+
clearInterval(this.durationIntervalId);
|
|
87
|
+
this.durationIntervalId = null;
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
// Unsubscribe from data
|
|
91
|
+
if (this.unsubscribeData) {
|
|
92
|
+
this.unsubscribeData();
|
|
93
|
+
this.unsubscribeData = null;
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
// Stop microphone
|
|
97
|
+
await this.microphone.stop();
|
|
98
|
+
this.microphone.dispose();
|
|
99
|
+
this.microphone = null;
|
|
100
|
+
|
|
101
|
+
this._isRecording = false;
|
|
102
|
+
this._duration = Date.now() - this.startTime;
|
|
103
|
+
|
|
104
|
+
// Combine all chunks
|
|
105
|
+
const rawPcmData = concatArrayBuffers(this.chunks);
|
|
106
|
+
|
|
107
|
+
// Create final audio data (with WAV header if needed)
|
|
108
|
+
let audioData: ArrayBuffer;
|
|
109
|
+
if (this.format === 'wav') {
|
|
110
|
+
audioData = createWavFile(rawPcmData, this.config);
|
|
111
|
+
} else {
|
|
112
|
+
audioData = rawPcmData;
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
// For native, we return a base64 data URI
|
|
116
|
+
// In a real implementation, you might want to write to file system
|
|
117
|
+
const base64Data = arrayBufferToBase64(audioData);
|
|
118
|
+
const mimeType = this.format === 'wav' ? 'audio/wav' : 'application/octet-stream';
|
|
119
|
+
const uri = `data:${mimeType};base64,${base64Data}`;
|
|
120
|
+
|
|
121
|
+
const result: RecordingResult = {
|
|
122
|
+
uri,
|
|
123
|
+
duration: this._duration,
|
|
124
|
+
size: audioData.byteLength,
|
|
125
|
+
config: this.config,
|
|
126
|
+
format: this.format,
|
|
127
|
+
getArrayBuffer: async () => audioData,
|
|
128
|
+
getData: async () => base64Data,
|
|
129
|
+
};
|
|
130
|
+
|
|
131
|
+
// Clear chunks
|
|
132
|
+
this.chunks = [];
|
|
133
|
+
|
|
134
|
+
return result;
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
async cancelRecording(): Promise<void> {
|
|
138
|
+
if (!this._isRecording) {
|
|
139
|
+
return;
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
// Stop duration tracking
|
|
143
|
+
if (this.durationIntervalId) {
|
|
144
|
+
clearInterval(this.durationIntervalId);
|
|
145
|
+
this.durationIntervalId = null;
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
// Unsubscribe from data
|
|
149
|
+
if (this.unsubscribeData) {
|
|
150
|
+
this.unsubscribeData();
|
|
151
|
+
this.unsubscribeData = null;
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
// Stop and dispose microphone
|
|
155
|
+
if (this.microphone) {
|
|
156
|
+
await this.microphone.stop();
|
|
157
|
+
this.microphone.dispose();
|
|
158
|
+
this.microphone = null;
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
this._isRecording = false;
|
|
162
|
+
this._duration = 0;
|
|
163
|
+
this.chunks = [];
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
dispose(): void {
|
|
167
|
+
this.cancelRecording().catch(() => {});
|
|
168
|
+
}
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
/**
|
|
172
|
+
* Create a new NativeRecorder instance.
|
|
173
|
+
*/
|
|
174
|
+
export function createRecorder(): IRecorder {
|
|
175
|
+
return new NativeRecorder();
|
|
176
|
+
}
|
|
@@ -0,0 +1,174 @@
|
|
|
1
|
+
import type {
|
|
2
|
+
IRecorder,
|
|
3
|
+
RecordingOptions,
|
|
4
|
+
RecordingResult,
|
|
5
|
+
AudioConfig,
|
|
6
|
+
PCMData,
|
|
7
|
+
} from '../types';
|
|
8
|
+
import { DEFAULT_AUDIO_CONFIG } from '../constants';
|
|
9
|
+
import {
|
|
10
|
+
createWavFile,
|
|
11
|
+
concatArrayBuffers,
|
|
12
|
+
mergeConfig,
|
|
13
|
+
createMicrophoneError,
|
|
14
|
+
} from '../utils';
|
|
15
|
+
import { WebMicrophone } from '../microphone.web';
|
|
16
|
+
|
|
17
|
+
export class WebRecorder implements IRecorder {
|
|
18
|
+
private microphone: WebMicrophone | null = null;
|
|
19
|
+
private chunks: ArrayBuffer[] = [];
|
|
20
|
+
private startTime: number = 0;
|
|
21
|
+
private _isRecording: boolean = false;
|
|
22
|
+
private _duration: number = 0;
|
|
23
|
+
private config: AudioConfig = DEFAULT_AUDIO_CONFIG;
|
|
24
|
+
private format: 'wav' | 'raw' = 'wav';
|
|
25
|
+
private maxDuration: number = 0;
|
|
26
|
+
private durationIntervalId: ReturnType<typeof setInterval> | null = null;
|
|
27
|
+
private unsubscribeData: (() => void) | null = null;
|
|
28
|
+
|
|
29
|
+
get isRecording(): boolean {
|
|
30
|
+
return this._isRecording;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
get duration(): number {
|
|
34
|
+
return this._duration;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
async startRecording(options?: RecordingOptions): Promise<void> {
|
|
38
|
+
if (this._isRecording) {
|
|
39
|
+
throw createMicrophoneError(
|
|
40
|
+
'RECORDING_FAILED',
|
|
41
|
+
'Already recording'
|
|
42
|
+
);
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
this.format = options?.format ?? 'wav';
|
|
46
|
+
this.maxDuration = options?.maxDuration ?? 0;
|
|
47
|
+
this.config = mergeConfig(options?.audioConfig, DEFAULT_AUDIO_CONFIG);
|
|
48
|
+
|
|
49
|
+
// Create new microphone instance for recording
|
|
50
|
+
this.microphone = new WebMicrophone();
|
|
51
|
+
this.chunks = [];
|
|
52
|
+
this.startTime = Date.now();
|
|
53
|
+
this._duration = 0;
|
|
54
|
+
|
|
55
|
+
// Subscribe to audio data
|
|
56
|
+
this.unsubscribeData = this.microphone.onAudioData((pcmData: PCMData) => {
|
|
57
|
+
this.chunks.push(pcmData.buffer.slice(0)); // Clone buffer
|
|
58
|
+
});
|
|
59
|
+
|
|
60
|
+
// Start microphone
|
|
61
|
+
await this.microphone.start(this.config);
|
|
62
|
+
this._isRecording = true;
|
|
63
|
+
|
|
64
|
+
// Start duration tracking
|
|
65
|
+
this.durationIntervalId = setInterval(() => {
|
|
66
|
+
this._duration = Date.now() - this.startTime;
|
|
67
|
+
|
|
68
|
+
// Check max duration
|
|
69
|
+
if (this.maxDuration > 0 && this._duration >= this.maxDuration * 1000) {
|
|
70
|
+
this.stopRecording().catch(console.error);
|
|
71
|
+
}
|
|
72
|
+
}, 100);
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
async stopRecording(): Promise<RecordingResult> {
|
|
76
|
+
if (!this._isRecording || !this.microphone) {
|
|
77
|
+
throw createMicrophoneError(
|
|
78
|
+
'RECORDING_FAILED',
|
|
79
|
+
'Not currently recording'
|
|
80
|
+
);
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
// Stop duration tracking
|
|
84
|
+
if (this.durationIntervalId) {
|
|
85
|
+
clearInterval(this.durationIntervalId);
|
|
86
|
+
this.durationIntervalId = null;
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
// Unsubscribe from data
|
|
90
|
+
if (this.unsubscribeData) {
|
|
91
|
+
this.unsubscribeData();
|
|
92
|
+
this.unsubscribeData = null;
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
// Stop microphone
|
|
96
|
+
await this.microphone.stop();
|
|
97
|
+
this.microphone.dispose();
|
|
98
|
+
this.microphone = null;
|
|
99
|
+
|
|
100
|
+
this._isRecording = false;
|
|
101
|
+
this._duration = Date.now() - this.startTime;
|
|
102
|
+
|
|
103
|
+
// Combine all chunks
|
|
104
|
+
const rawPcmData = concatArrayBuffers(this.chunks);
|
|
105
|
+
|
|
106
|
+
// Create final audio data (with WAV header if needed)
|
|
107
|
+
let audioData: ArrayBuffer;
|
|
108
|
+
if (this.format === 'wav') {
|
|
109
|
+
audioData = createWavFile(rawPcmData, this.config);
|
|
110
|
+
} else {
|
|
111
|
+
audioData = rawPcmData;
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
// Create blob and URL
|
|
115
|
+
const mimeType = this.format === 'wav' ? 'audio/wav' : 'application/octet-stream';
|
|
116
|
+
const blob = new Blob([audioData], { type: mimeType });
|
|
117
|
+
const uri = URL.createObjectURL(blob);
|
|
118
|
+
|
|
119
|
+
const result: RecordingResult = {
|
|
120
|
+
uri,
|
|
121
|
+
duration: this._duration,
|
|
122
|
+
size: audioData.byteLength,
|
|
123
|
+
config: this.config,
|
|
124
|
+
format: this.format,
|
|
125
|
+
getArrayBuffer: async () => audioData,
|
|
126
|
+
getData: async () => blob,
|
|
127
|
+
};
|
|
128
|
+
|
|
129
|
+
// Clear chunks
|
|
130
|
+
this.chunks = [];
|
|
131
|
+
|
|
132
|
+
return result;
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
async cancelRecording(): Promise<void> {
|
|
136
|
+
if (!this._isRecording) {
|
|
137
|
+
return;
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
// Stop duration tracking
|
|
141
|
+
if (this.durationIntervalId) {
|
|
142
|
+
clearInterval(this.durationIntervalId);
|
|
143
|
+
this.durationIntervalId = null;
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
// Unsubscribe from data
|
|
147
|
+
if (this.unsubscribeData) {
|
|
148
|
+
this.unsubscribeData();
|
|
149
|
+
this.unsubscribeData = null;
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
// Stop and dispose microphone
|
|
153
|
+
if (this.microphone) {
|
|
154
|
+
await this.microphone.stop();
|
|
155
|
+
this.microphone.dispose();
|
|
156
|
+
this.microphone = null;
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
this._isRecording = false;
|
|
160
|
+
this._duration = 0;
|
|
161
|
+
this.chunks = [];
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
dispose(): void {
|
|
165
|
+
this.cancelRecording().catch(() => {});
|
|
166
|
+
}
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
/**
|
|
170
|
+
* Create a new WebRecorder instance.
|
|
171
|
+
*/
|
|
172
|
+
export function createRecorder(): IRecorder {
|
|
173
|
+
return new WebRecorder();
|
|
174
|
+
}
|