@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,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
+ }