@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
package/README.md
ADDED
|
@@ -0,0 +1,225 @@
|
|
|
1
|
+
# @idealyst/microphone
|
|
2
|
+
|
|
3
|
+
Cross-platform microphone streaming library for React and React Native. Provides low-level access to raw PCM audio samples for real-time processing, speech recognition, audio visualization, and file recording.
|
|
4
|
+
|
|
5
|
+
## Installation
|
|
6
|
+
|
|
7
|
+
```bash
|
|
8
|
+
# npm
|
|
9
|
+
npm install @idealyst/microphone
|
|
10
|
+
|
|
11
|
+
# yarn
|
|
12
|
+
yarn add @idealyst/microphone
|
|
13
|
+
```
|
|
14
|
+
|
|
15
|
+
### React Native Setup
|
|
16
|
+
|
|
17
|
+
For React Native, you also need to install the native audio streaming library:
|
|
18
|
+
|
|
19
|
+
```bash
|
|
20
|
+
yarn add react-native-live-audio-stream
|
|
21
|
+
```
|
|
22
|
+
|
|
23
|
+
#### iOS
|
|
24
|
+
|
|
25
|
+
```bash
|
|
26
|
+
cd ios && pod install
|
|
27
|
+
```
|
|
28
|
+
|
|
29
|
+
Add microphone permission to `Info.plist`:
|
|
30
|
+
|
|
31
|
+
```xml
|
|
32
|
+
<key>NSMicrophoneUsageDescription</key>
|
|
33
|
+
<string>This app needs access to your microphone to record audio.</string>
|
|
34
|
+
```
|
|
35
|
+
|
|
36
|
+
#### Android
|
|
37
|
+
|
|
38
|
+
Add permission to `AndroidManifest.xml`:
|
|
39
|
+
|
|
40
|
+
```xml
|
|
41
|
+
<uses-permission android:name="android.permission.RECORD_AUDIO" />
|
|
42
|
+
```
|
|
43
|
+
|
|
44
|
+
## Quick Start
|
|
45
|
+
|
|
46
|
+
### Streaming Audio Data
|
|
47
|
+
|
|
48
|
+
```tsx
|
|
49
|
+
import { useMicrophone, AUDIO_PROFILES } from '@idealyst/microphone';
|
|
50
|
+
|
|
51
|
+
function VoiceInput() {
|
|
52
|
+
const {
|
|
53
|
+
isRecording,
|
|
54
|
+
level,
|
|
55
|
+
start,
|
|
56
|
+
stop,
|
|
57
|
+
subscribeToAudioData
|
|
58
|
+
} = useMicrophone({
|
|
59
|
+
config: AUDIO_PROFILES.speech,
|
|
60
|
+
autoRequestPermission: true,
|
|
61
|
+
});
|
|
62
|
+
|
|
63
|
+
useEffect(() => {
|
|
64
|
+
if (!isRecording) return;
|
|
65
|
+
|
|
66
|
+
return subscribeToAudioData((pcmData) => {
|
|
67
|
+
// Process raw PCM samples
|
|
68
|
+
console.log('Got', pcmData.samples.length, 'samples');
|
|
69
|
+
|
|
70
|
+
// Send to speech recognition API
|
|
71
|
+
sendToWhisperAPI(pcmData.buffer);
|
|
72
|
+
});
|
|
73
|
+
}, [isRecording, subscribeToAudioData]);
|
|
74
|
+
|
|
75
|
+
return (
|
|
76
|
+
<View>
|
|
77
|
+
<Button onPress={isRecording ? stop : start}>
|
|
78
|
+
{isRecording ? 'Stop' : 'Start'}
|
|
79
|
+
</Button>
|
|
80
|
+
<Text>Level: {Math.round(level.current * 100)}%</Text>
|
|
81
|
+
</View>
|
|
82
|
+
);
|
|
83
|
+
}
|
|
84
|
+
```
|
|
85
|
+
|
|
86
|
+
### Recording to File
|
|
87
|
+
|
|
88
|
+
```tsx
|
|
89
|
+
import { useRecorder } from '@idealyst/microphone';
|
|
90
|
+
|
|
91
|
+
function AudioRecorder() {
|
|
92
|
+
const { isRecording, duration, startRecording, stopRecording } = useRecorder();
|
|
93
|
+
|
|
94
|
+
const handleStop = async () => {
|
|
95
|
+
const result = await stopRecording();
|
|
96
|
+
|
|
97
|
+
// Get ArrayBuffer for upload
|
|
98
|
+
const data = await result.getArrayBuffer();
|
|
99
|
+
await uploadAudio(data);
|
|
100
|
+
};
|
|
101
|
+
|
|
102
|
+
return (
|
|
103
|
+
<View>
|
|
104
|
+
<Text>Duration: {Math.floor(duration / 1000)}s</Text>
|
|
105
|
+
<Button onPress={isRecording ? handleStop : () => startRecording()}>
|
|
106
|
+
{isRecording ? 'Stop' : 'Record'}
|
|
107
|
+
</Button>
|
|
108
|
+
</View>
|
|
109
|
+
);
|
|
110
|
+
}
|
|
111
|
+
```
|
|
112
|
+
|
|
113
|
+
## Audio Profiles
|
|
114
|
+
|
|
115
|
+
Pre-configured profiles for common use cases:
|
|
116
|
+
|
|
117
|
+
```typescript
|
|
118
|
+
import { AUDIO_PROFILES } from '@idealyst/microphone';
|
|
119
|
+
|
|
120
|
+
// Speech recognition (Whisper, Google STT, etc.)
|
|
121
|
+
AUDIO_PROFILES.speech // 16kHz mono 16-bit
|
|
122
|
+
|
|
123
|
+
// Music and high-quality audio
|
|
124
|
+
AUDIO_PROFILES.highQuality // 44.1kHz stereo 16-bit
|
|
125
|
+
|
|
126
|
+
// Real-time feedback with minimal latency
|
|
127
|
+
AUDIO_PROFILES.lowLatency // 16kHz mono, 256 buffer
|
|
128
|
+
|
|
129
|
+
// Minimal bandwidth for voice calls
|
|
130
|
+
AUDIO_PROFILES.minimal // 8kHz mono 8-bit
|
|
131
|
+
```
|
|
132
|
+
|
|
133
|
+
## API Reference
|
|
134
|
+
|
|
135
|
+
### useMicrophone Hook
|
|
136
|
+
|
|
137
|
+
```typescript
|
|
138
|
+
const {
|
|
139
|
+
// State
|
|
140
|
+
status, // Full MicrophoneStatus object
|
|
141
|
+
isRecording, // boolean
|
|
142
|
+
isPaused, // boolean (native only)
|
|
143
|
+
level, // AudioLevel { current, peak, rms, db }
|
|
144
|
+
error, // MicrophoneError | null
|
|
145
|
+
permission, // PermissionStatus
|
|
146
|
+
|
|
147
|
+
// Actions
|
|
148
|
+
start, // (config?) => Promise<void>
|
|
149
|
+
stop, // () => Promise<void>
|
|
150
|
+
pause, // () => Promise<void> (native only)
|
|
151
|
+
resume, // () => Promise<void> (native only)
|
|
152
|
+
requestPermission, // () => Promise<PermissionResult>
|
|
153
|
+
resetPeakLevel, // () => void
|
|
154
|
+
|
|
155
|
+
// Data subscription
|
|
156
|
+
subscribeToAudioData, // (callback) => unsubscribe
|
|
157
|
+
} = useMicrophone(options);
|
|
158
|
+
```
|
|
159
|
+
|
|
160
|
+
### useRecorder Hook
|
|
161
|
+
|
|
162
|
+
```typescript
|
|
163
|
+
const {
|
|
164
|
+
isRecording, // boolean
|
|
165
|
+
duration, // number (ms)
|
|
166
|
+
error, // MicrophoneError | null
|
|
167
|
+
|
|
168
|
+
startRecording, // (options?) => Promise<void>
|
|
169
|
+
stopRecording, // () => Promise<RecordingResult>
|
|
170
|
+
cancelRecording, // () => Promise<void>
|
|
171
|
+
} = useRecorder(options);
|
|
172
|
+
```
|
|
173
|
+
|
|
174
|
+
### PCMData Type
|
|
175
|
+
|
|
176
|
+
```typescript
|
|
177
|
+
interface PCMData {
|
|
178
|
+
buffer: ArrayBuffer; // Raw PCM data
|
|
179
|
+
samples: TypedArray; // Int8Array | Int16Array | Float32Array
|
|
180
|
+
timestamp: number; // Capture time (ms since epoch)
|
|
181
|
+
config: AudioConfig; // Audio configuration
|
|
182
|
+
}
|
|
183
|
+
```
|
|
184
|
+
|
|
185
|
+
### AudioLevel Type
|
|
186
|
+
|
|
187
|
+
```typescript
|
|
188
|
+
interface AudioLevel {
|
|
189
|
+
current: number; // 0.0 - 1.0
|
|
190
|
+
peak: number; // 0.0 - 1.0 (since last reset)
|
|
191
|
+
rms: number; // Root mean square
|
|
192
|
+
db: number; // Decibels (-Infinity to 0)
|
|
193
|
+
}
|
|
194
|
+
```
|
|
195
|
+
|
|
196
|
+
### RecordingResult Type
|
|
197
|
+
|
|
198
|
+
```typescript
|
|
199
|
+
interface RecordingResult {
|
|
200
|
+
uri: string; // Blob URL (web) or data URI (native)
|
|
201
|
+
duration: number; // Duration in ms
|
|
202
|
+
size: number; // File size in bytes
|
|
203
|
+
config: AudioConfig; // Audio config used
|
|
204
|
+
format: 'wav' | 'raw'; // Recording format
|
|
205
|
+
|
|
206
|
+
getArrayBuffer(): Promise<ArrayBuffer>; // For upload/processing
|
|
207
|
+
getData(): Promise<Blob | string>; // Blob (web) or base64 (native)
|
|
208
|
+
}
|
|
209
|
+
```
|
|
210
|
+
|
|
211
|
+
## Platform Notes
|
|
212
|
+
|
|
213
|
+
### Web
|
|
214
|
+
- Uses Web Audio API with AudioWorklet for low-latency processing
|
|
215
|
+
- Float32 samples internally, converted to requested bit depth
|
|
216
|
+
- Pause is not supported (stops the stream)
|
|
217
|
+
|
|
218
|
+
### React Native
|
|
219
|
+
- Uses `react-native-live-audio-stream` for audio capture
|
|
220
|
+
- 32-bit float not supported (falls back to 16-bit)
|
|
221
|
+
- Pause/resume supported on native
|
|
222
|
+
|
|
223
|
+
## License
|
|
224
|
+
|
|
225
|
+
MIT
|
package/package.json
ADDED
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@idealyst/microphone",
|
|
3
|
+
"version": "1.1.2",
|
|
4
|
+
"description": "Cross-platform microphone streaming for React and React Native",
|
|
5
|
+
"documentation": "https://github.com/IdealystIO/idealyst-framework/tree/main/packages/microphone#readme",
|
|
6
|
+
"readme": "README.md",
|
|
7
|
+
"main": "src/index.ts",
|
|
8
|
+
"module": "src/index.ts",
|
|
9
|
+
"types": "src/index.ts",
|
|
10
|
+
"react-native": "src/index.native.ts",
|
|
11
|
+
"repository": {
|
|
12
|
+
"type": "git",
|
|
13
|
+
"url": "https://github.com/IdealystIO/idealyst-framework.git",
|
|
14
|
+
"directory": "packages/microphone"
|
|
15
|
+
},
|
|
16
|
+
"author": "Your Name <your.email@example.com>",
|
|
17
|
+
"license": "MIT",
|
|
18
|
+
"publishConfig": {
|
|
19
|
+
"access": "public"
|
|
20
|
+
},
|
|
21
|
+
"exports": {
|
|
22
|
+
".": {
|
|
23
|
+
"react-native": "./src/index.native.ts",
|
|
24
|
+
"browser": "./src/index.web.ts",
|
|
25
|
+
"import": "./src/index.ts",
|
|
26
|
+
"require": "./src/index.ts",
|
|
27
|
+
"types": "./src/index.ts"
|
|
28
|
+
}
|
|
29
|
+
},
|
|
30
|
+
"scripts": {
|
|
31
|
+
"prepublishOnly": "echo 'Publishing TypeScript source directly'",
|
|
32
|
+
"publish:npm": "npm publish"
|
|
33
|
+
},
|
|
34
|
+
"peerDependencies": {
|
|
35
|
+
"react": ">=16.8.0",
|
|
36
|
+
"react-native": ">=0.60.0",
|
|
37
|
+
"react-native-live-audio-stream": ">=1.1.0"
|
|
38
|
+
},
|
|
39
|
+
"peerDependenciesMeta": {
|
|
40
|
+
"react-native": {
|
|
41
|
+
"optional": true
|
|
42
|
+
},
|
|
43
|
+
"react-native-live-audio-stream": {
|
|
44
|
+
"optional": true
|
|
45
|
+
}
|
|
46
|
+
},
|
|
47
|
+
"devDependencies": {
|
|
48
|
+
"@types/react": "^19.1.0",
|
|
49
|
+
"@types/react-native": "^0.73.0",
|
|
50
|
+
"react-native-live-audio-stream": "^1.1.0",
|
|
51
|
+
"typescript": "^5.0.0"
|
|
52
|
+
},
|
|
53
|
+
"files": [
|
|
54
|
+
"src",
|
|
55
|
+
"README.md"
|
|
56
|
+
],
|
|
57
|
+
"keywords": [
|
|
58
|
+
"react",
|
|
59
|
+
"react-native",
|
|
60
|
+
"microphone",
|
|
61
|
+
"audio",
|
|
62
|
+
"pcm",
|
|
63
|
+
"streaming",
|
|
64
|
+
"recording",
|
|
65
|
+
"cross-platform"
|
|
66
|
+
]
|
|
67
|
+
}
|
package/src/constants.ts
ADDED
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
import type {
|
|
2
|
+
AudioConfig,
|
|
3
|
+
SampleRate,
|
|
4
|
+
BitDepth,
|
|
5
|
+
ChannelCount,
|
|
6
|
+
AudioLevel,
|
|
7
|
+
} from './types';
|
|
8
|
+
|
|
9
|
+
export const DEFAULT_AUDIO_CONFIG: AudioConfig = {
|
|
10
|
+
sampleRate: 16000, // Optimal for speech recognition
|
|
11
|
+
channels: 1, // Mono
|
|
12
|
+
bitDepth: 16, // Standard CD quality per sample
|
|
13
|
+
bufferSize: 4096, // Balance between latency and CPU
|
|
14
|
+
};
|
|
15
|
+
|
|
16
|
+
export const DEFAULT_AUDIO_LEVEL: AudioLevel = {
|
|
17
|
+
current: 0,
|
|
18
|
+
peak: 0,
|
|
19
|
+
rms: 0,
|
|
20
|
+
db: -Infinity,
|
|
21
|
+
};
|
|
22
|
+
|
|
23
|
+
/**
|
|
24
|
+
* Pre-configured audio profiles for common use cases.
|
|
25
|
+
*/
|
|
26
|
+
export const AUDIO_PROFILES = {
|
|
27
|
+
/** Optimized for speech recognition (Whisper, Google STT, etc.) */
|
|
28
|
+
speech: {
|
|
29
|
+
sampleRate: 16000 as SampleRate,
|
|
30
|
+
channels: 1 as ChannelCount,
|
|
31
|
+
bitDepth: 16 as BitDepth,
|
|
32
|
+
bufferSize: 4096,
|
|
33
|
+
} satisfies AudioConfig,
|
|
34
|
+
|
|
35
|
+
/** Higher quality for music/audio visualization */
|
|
36
|
+
highQuality: {
|
|
37
|
+
sampleRate: 44100 as SampleRate,
|
|
38
|
+
channels: 2 as ChannelCount,
|
|
39
|
+
bitDepth: 16 as BitDepth,
|
|
40
|
+
bufferSize: 2048,
|
|
41
|
+
} satisfies AudioConfig,
|
|
42
|
+
|
|
43
|
+
/** Low latency for real-time feedback */
|
|
44
|
+
lowLatency: {
|
|
45
|
+
sampleRate: 16000 as SampleRate,
|
|
46
|
+
channels: 1 as ChannelCount,
|
|
47
|
+
bitDepth: 16 as BitDepth,
|
|
48
|
+
bufferSize: 256,
|
|
49
|
+
} satisfies AudioConfig,
|
|
50
|
+
|
|
51
|
+
/** Minimal bandwidth (voice calls, basic recording) */
|
|
52
|
+
minimal: {
|
|
53
|
+
sampleRate: 8000 as SampleRate,
|
|
54
|
+
channels: 1 as ChannelCount,
|
|
55
|
+
bitDepth: 8 as BitDepth,
|
|
56
|
+
bufferSize: 4096,
|
|
57
|
+
} satisfies AudioConfig,
|
|
58
|
+
} as const;
|
|
59
|
+
|
|
60
|
+
/**
|
|
61
|
+
* Maximum values for different bit depths.
|
|
62
|
+
* Used for normalization calculations.
|
|
63
|
+
*/
|
|
64
|
+
export const BIT_DEPTH_MAX_VALUES = {
|
|
65
|
+
8: 128,
|
|
66
|
+
16: 32768,
|
|
67
|
+
32: 1.0,
|
|
68
|
+
} as const;
|
|
69
|
+
|
|
70
|
+
/**
|
|
71
|
+
* Default level update interval in milliseconds.
|
|
72
|
+
*/
|
|
73
|
+
export const DEFAULT_LEVEL_UPDATE_INTERVAL = 100;
|
|
@@ -0,0 +1,144 @@
|
|
|
1
|
+
import { useState, useCallback, useEffect, useRef } from 'react';
|
|
2
|
+
import type {
|
|
3
|
+
UseMicrophoneOptions,
|
|
4
|
+
UseMicrophoneResult,
|
|
5
|
+
AudioDataCallback,
|
|
6
|
+
MicrophoneStatus,
|
|
7
|
+
MicrophoneError,
|
|
8
|
+
AudioLevel,
|
|
9
|
+
PermissionResult,
|
|
10
|
+
AudioConfig,
|
|
11
|
+
IMicrophone,
|
|
12
|
+
} from '../types';
|
|
13
|
+
import { DEFAULT_AUDIO_CONFIG, DEFAULT_AUDIO_LEVEL } from '../constants';
|
|
14
|
+
|
|
15
|
+
/**
|
|
16
|
+
* Factory function type for creating platform-specific microphone instances.
|
|
17
|
+
*/
|
|
18
|
+
export type CreateMicrophoneFactory = () => IMicrophone;
|
|
19
|
+
|
|
20
|
+
/**
|
|
21
|
+
* Create the useMicrophone hook with a platform-specific factory.
|
|
22
|
+
*/
|
|
23
|
+
export function createUseMicrophoneHook(
|
|
24
|
+
createMicrophone: CreateMicrophoneFactory
|
|
25
|
+
) {
|
|
26
|
+
return function useMicrophone(
|
|
27
|
+
options: UseMicrophoneOptions = {}
|
|
28
|
+
): UseMicrophoneResult {
|
|
29
|
+
const {
|
|
30
|
+
config = {},
|
|
31
|
+
autoRequestPermission = false,
|
|
32
|
+
levelUpdateInterval = 100,
|
|
33
|
+
} = options;
|
|
34
|
+
|
|
35
|
+
const microphoneRef = useRef<IMicrophone | null>(null);
|
|
36
|
+
const configRef = useRef<Partial<AudioConfig>>(config);
|
|
37
|
+
|
|
38
|
+
// Update config ref when it changes
|
|
39
|
+
useEffect(() => {
|
|
40
|
+
configRef.current = config;
|
|
41
|
+
}, [config]);
|
|
42
|
+
|
|
43
|
+
const [status, setStatus] = useState<MicrophoneStatus>({
|
|
44
|
+
state: 'idle',
|
|
45
|
+
permission: 'undetermined',
|
|
46
|
+
isRecording: false,
|
|
47
|
+
duration: 0,
|
|
48
|
+
level: DEFAULT_AUDIO_LEVEL,
|
|
49
|
+
config: { ...DEFAULT_AUDIO_CONFIG, ...config },
|
|
50
|
+
});
|
|
51
|
+
|
|
52
|
+
const [level, setLevel] = useState<AudioLevel>(DEFAULT_AUDIO_LEVEL);
|
|
53
|
+
const [error, setError] = useState<MicrophoneError | null>(null);
|
|
54
|
+
|
|
55
|
+
// Initialize microphone instance
|
|
56
|
+
useEffect(() => {
|
|
57
|
+
const mic = createMicrophone();
|
|
58
|
+
microphoneRef.current = mic;
|
|
59
|
+
|
|
60
|
+
const unsubscribeState = mic.onStateChange((newStatus) => {
|
|
61
|
+
setStatus(newStatus);
|
|
62
|
+
if (newStatus.error) {
|
|
63
|
+
setError(newStatus.error);
|
|
64
|
+
}
|
|
65
|
+
});
|
|
66
|
+
|
|
67
|
+
const unsubscribeLevel = mic.onAudioLevel((newLevel) => {
|
|
68
|
+
setLevel(newLevel);
|
|
69
|
+
}, levelUpdateInterval);
|
|
70
|
+
|
|
71
|
+
const unsubscribeError = mic.onError((err) => {
|
|
72
|
+
setError(err);
|
|
73
|
+
});
|
|
74
|
+
|
|
75
|
+
// Check or request permission on mount
|
|
76
|
+
if (autoRequestPermission) {
|
|
77
|
+
mic.requestPermission().catch(() => {});
|
|
78
|
+
} else {
|
|
79
|
+
mic.checkPermission().catch(() => {});
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
return () => {
|
|
83
|
+
unsubscribeState();
|
|
84
|
+
unsubscribeLevel();
|
|
85
|
+
unsubscribeError();
|
|
86
|
+
mic.dispose();
|
|
87
|
+
microphoneRef.current = null;
|
|
88
|
+
};
|
|
89
|
+
}, [autoRequestPermission, levelUpdateInterval]);
|
|
90
|
+
|
|
91
|
+
const start = useCallback(
|
|
92
|
+
async (overrideConfig?: Partial<AudioConfig>) => {
|
|
93
|
+
setError(null);
|
|
94
|
+
const finalConfig = { ...configRef.current, ...overrideConfig };
|
|
95
|
+
await microphoneRef.current?.start(finalConfig);
|
|
96
|
+
},
|
|
97
|
+
[]
|
|
98
|
+
);
|
|
99
|
+
|
|
100
|
+
const stop = useCallback(async () => {
|
|
101
|
+
await microphoneRef.current?.stop();
|
|
102
|
+
}, []);
|
|
103
|
+
|
|
104
|
+
const pause = useCallback(async () => {
|
|
105
|
+
await microphoneRef.current?.pause();
|
|
106
|
+
}, []);
|
|
107
|
+
|
|
108
|
+
const resume = useCallback(async () => {
|
|
109
|
+
await microphoneRef.current?.resume();
|
|
110
|
+
}, []);
|
|
111
|
+
|
|
112
|
+
const requestPermission = useCallback(async (): Promise<PermissionResult> => {
|
|
113
|
+
const result = await microphoneRef.current?.requestPermission();
|
|
114
|
+
return result ?? { status: 'unavailable', canAskAgain: false };
|
|
115
|
+
}, []);
|
|
116
|
+
|
|
117
|
+
const resetPeakLevel = useCallback(() => {
|
|
118
|
+
microphoneRef.current?.resetPeakLevel();
|
|
119
|
+
}, []);
|
|
120
|
+
|
|
121
|
+
const subscribeToAudioData = useCallback(
|
|
122
|
+
(callback: AudioDataCallback): (() => void) => {
|
|
123
|
+
return microphoneRef.current?.onAudioData(callback) ?? (() => {});
|
|
124
|
+
},
|
|
125
|
+
[]
|
|
126
|
+
);
|
|
127
|
+
|
|
128
|
+
return {
|
|
129
|
+
status,
|
|
130
|
+
isRecording: status.isRecording,
|
|
131
|
+
isPaused: status.state === 'paused',
|
|
132
|
+
level,
|
|
133
|
+
error,
|
|
134
|
+
permission: status.permission,
|
|
135
|
+
start,
|
|
136
|
+
stop,
|
|
137
|
+
pause,
|
|
138
|
+
resume,
|
|
139
|
+
requestPermission,
|
|
140
|
+
resetPeakLevel,
|
|
141
|
+
subscribeToAudioData,
|
|
142
|
+
};
|
|
143
|
+
};
|
|
144
|
+
}
|
|
@@ -0,0 +1,101 @@
|
|
|
1
|
+
import { useState, useCallback, useRef, useEffect } from 'react';
|
|
2
|
+
import type {
|
|
3
|
+
UseRecorderOptions,
|
|
4
|
+
UseRecorderResult,
|
|
5
|
+
RecordingOptions,
|
|
6
|
+
RecordingResult,
|
|
7
|
+
MicrophoneError,
|
|
8
|
+
IRecorder,
|
|
9
|
+
} from '../types';
|
|
10
|
+
|
|
11
|
+
/**
|
|
12
|
+
* Factory function type for creating platform-specific recorder instances.
|
|
13
|
+
*/
|
|
14
|
+
export type CreateRecorderFactory = () => IRecorder;
|
|
15
|
+
|
|
16
|
+
/**
|
|
17
|
+
* Create the useRecorder hook with a platform-specific factory.
|
|
18
|
+
*/
|
|
19
|
+
export function createUseRecorderHook(createRecorder: CreateRecorderFactory) {
|
|
20
|
+
return function useRecorder(
|
|
21
|
+
options: UseRecorderOptions = {}
|
|
22
|
+
): UseRecorderResult {
|
|
23
|
+
const recorderRef = useRef<IRecorder | null>(null);
|
|
24
|
+
const optionsRef = useRef<RecordingOptions | undefined>(options.options);
|
|
25
|
+
|
|
26
|
+
// Update options ref when it changes
|
|
27
|
+
useEffect(() => {
|
|
28
|
+
optionsRef.current = options.options;
|
|
29
|
+
}, [options.options]);
|
|
30
|
+
|
|
31
|
+
const [isRecording, setIsRecording] = useState(false);
|
|
32
|
+
const [duration, setDuration] = useState(0);
|
|
33
|
+
const [error, setError] = useState<MicrophoneError | null>(null);
|
|
34
|
+
|
|
35
|
+
// Initialize recorder instance
|
|
36
|
+
useEffect(() => {
|
|
37
|
+
recorderRef.current = createRecorder();
|
|
38
|
+
|
|
39
|
+
return () => {
|
|
40
|
+
recorderRef.current?.dispose();
|
|
41
|
+
recorderRef.current = null;
|
|
42
|
+
};
|
|
43
|
+
}, []);
|
|
44
|
+
|
|
45
|
+
// Duration tracking
|
|
46
|
+
useEffect(() => {
|
|
47
|
+
if (!isRecording) {
|
|
48
|
+
return;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
const interval = setInterval(() => {
|
|
52
|
+
if (recorderRef.current) {
|
|
53
|
+
setDuration(recorderRef.current.duration);
|
|
54
|
+
}
|
|
55
|
+
}, 100);
|
|
56
|
+
|
|
57
|
+
return () => clearInterval(interval);
|
|
58
|
+
}, [isRecording]);
|
|
59
|
+
|
|
60
|
+
const startRecording = useCallback(
|
|
61
|
+
async (overrideOptions?: RecordingOptions) => {
|
|
62
|
+
try {
|
|
63
|
+
setError(null);
|
|
64
|
+
const finalOptions = overrideOptions ?? optionsRef.current;
|
|
65
|
+
await recorderRef.current?.startRecording(finalOptions);
|
|
66
|
+
setIsRecording(true);
|
|
67
|
+
setDuration(0);
|
|
68
|
+
} catch (e) {
|
|
69
|
+
const err = e as MicrophoneError;
|
|
70
|
+
setError(err);
|
|
71
|
+
throw err;
|
|
72
|
+
}
|
|
73
|
+
},
|
|
74
|
+
[]
|
|
75
|
+
);
|
|
76
|
+
|
|
77
|
+
const stopRecording = useCallback(async (): Promise<RecordingResult> => {
|
|
78
|
+
const result = await recorderRef.current?.stopRecording();
|
|
79
|
+
setIsRecording(false);
|
|
80
|
+
if (!result) {
|
|
81
|
+
throw new Error('No recording result');
|
|
82
|
+
}
|
|
83
|
+
return result;
|
|
84
|
+
}, []);
|
|
85
|
+
|
|
86
|
+
const cancelRecording = useCallback(async () => {
|
|
87
|
+
await recorderRef.current?.cancelRecording();
|
|
88
|
+
setIsRecording(false);
|
|
89
|
+
setDuration(0);
|
|
90
|
+
}, []);
|
|
91
|
+
|
|
92
|
+
return {
|
|
93
|
+
isRecording,
|
|
94
|
+
duration,
|
|
95
|
+
error,
|
|
96
|
+
startRecording,
|
|
97
|
+
stopRecording,
|
|
98
|
+
cancelRecording,
|
|
99
|
+
};
|
|
100
|
+
};
|
|
101
|
+
}
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
import { createUseMicrophoneHook } from './createUseMicrophoneHook';
|
|
2
|
+
import { createMicrophone } from '../microphone.native';
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* React hook for microphone access on React Native platforms.
|
|
6
|
+
*
|
|
7
|
+
* @example
|
|
8
|
+
* ```tsx
|
|
9
|
+
* import { useMicrophone, AUDIO_PROFILES } from '@idealyst/microphone';
|
|
10
|
+
* import { View, Button, Text } from 'react-native';
|
|
11
|
+
*
|
|
12
|
+
* function VoiceInput() {
|
|
13
|
+
* const {
|
|
14
|
+
* isRecording,
|
|
15
|
+
* level,
|
|
16
|
+
* start,
|
|
17
|
+
* stop,
|
|
18
|
+
* subscribeToAudioData
|
|
19
|
+
* } = useMicrophone({
|
|
20
|
+
* config: AUDIO_PROFILES.speech,
|
|
21
|
+
* autoRequestPermission: true,
|
|
22
|
+
* });
|
|
23
|
+
*
|
|
24
|
+
* useEffect(() => {
|
|
25
|
+
* if (!isRecording) return;
|
|
26
|
+
*
|
|
27
|
+
* return subscribeToAudioData((pcmData) => {
|
|
28
|
+
* // Process audio data
|
|
29
|
+
* console.log('Got', pcmData.samples.length, 'samples');
|
|
30
|
+
* });
|
|
31
|
+
* }, [isRecording, subscribeToAudioData]);
|
|
32
|
+
*
|
|
33
|
+
* return (
|
|
34
|
+
* <View>
|
|
35
|
+
* <Button
|
|
36
|
+
* title={isRecording ? 'Stop' : 'Start'}
|
|
37
|
+
* onPress={isRecording ? stop : start}
|
|
38
|
+
* />
|
|
39
|
+
* <Text>Level: {Math.round(level.current * 100)}%</Text>
|
|
40
|
+
* </View>
|
|
41
|
+
* );
|
|
42
|
+
* }
|
|
43
|
+
* ```
|
|
44
|
+
*/
|
|
45
|
+
export const useMicrophone = createUseMicrophoneHook(createMicrophone);
|