@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 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
+ }
@@ -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,2 @@
1
+ export { useMicrophone } from './useMicrophone.native';
2
+ export { useRecorder } from './useRecorder.native';
@@ -0,0 +1,2 @@
1
+ export { useMicrophone } from './useMicrophone.web';
2
+ export { useRecorder } from './useRecorder.web';
@@ -0,0 +1,2 @@
1
+ export { useMicrophone } from './useMicrophone.web';
2
+ export { useRecorder } from './useRecorder.web';
@@ -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);