@siteed/expo-audio-stream 1.0.1 → 1.0.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.
Files changed (85) hide show
  1. package/README.md +6 -6
  2. package/android/build.gradle +5 -0
  3. package/android/src/main/java/net/siteed/audiostream/AudioAnalysisData.kt +120 -0
  4. package/android/src/main/java/net/siteed/audiostream/AudioFileHandler.kt +34 -4
  5. package/android/src/main/java/net/siteed/audiostream/AudioProcessor.kt +635 -0
  6. package/android/src/main/java/net/siteed/audiostream/AudioRecorderManager.kt +194 -79
  7. package/android/src/main/java/net/siteed/audiostream/Constants.kt +1 -0
  8. package/android/src/main/java/net/siteed/audiostream/ExpoAudioStreamModule.kt +48 -2
  9. package/android/src/main/java/net/siteed/audiostream/FFT.kt +44 -0
  10. package/android/src/main/java/net/siteed/audiostream/Features.kt +56 -0
  11. package/android/src/main/java/net/siteed/audiostream/RecordingConfig.kt +12 -0
  12. package/android/src/main/test/java/net/siteed/audiostream/AudioProcessorTest.kt +56 -0
  13. package/app.plugin.js +1 -1
  14. package/build/AudioRecorder.provider.js +1 -1
  15. package/build/AudioRecorder.provider.js.map +1 -1
  16. package/build/ExpoAudioStream.native.d.ts +3 -0
  17. package/build/ExpoAudioStream.native.d.ts.map +1 -0
  18. package/build/ExpoAudioStream.native.js +6 -0
  19. package/build/ExpoAudioStream.native.js.map +1 -0
  20. package/build/ExpoAudioStream.types.d.ts +79 -6
  21. package/build/ExpoAudioStream.types.d.ts.map +1 -1
  22. package/build/ExpoAudioStream.types.js.map +1 -1
  23. package/build/ExpoAudioStream.web.d.ts +41 -0
  24. package/build/ExpoAudioStream.web.d.ts.map +1 -0
  25. package/build/ExpoAudioStream.web.js +184 -0
  26. package/build/ExpoAudioStream.web.js.map +1 -0
  27. package/build/ExpoAudioStreamModule.d.ts +2 -2
  28. package/build/ExpoAudioStreamModule.d.ts.map +1 -1
  29. package/build/ExpoAudioStreamModule.js +12 -3
  30. package/build/ExpoAudioStreamModule.js.map +1 -1
  31. package/build/WebRecorder.d.ts +47 -0
  32. package/build/WebRecorder.d.ts.map +1 -0
  33. package/build/WebRecorder.js +243 -0
  34. package/build/WebRecorder.js.map +1 -0
  35. package/build/index.d.ts +14 -5
  36. package/build/index.d.ts.map +1 -1
  37. package/build/index.js +106 -7
  38. package/build/index.js.map +1 -1
  39. package/build/inlineAudioWebWorker.d.ts +3 -0
  40. package/build/inlineAudioWebWorker.d.ts.map +1 -0
  41. package/build/inlineAudioWebWorker.js +340 -0
  42. package/build/inlineAudioWebWorker.js.map +1 -0
  43. package/build/useAudioRecording.d.ts +24 -9
  44. package/build/useAudioRecording.d.ts.map +1 -1
  45. package/build/useAudioRecording.js +107 -29
  46. package/build/useAudioRecording.js.map +1 -1
  47. package/build/utils.d.ts +31 -0
  48. package/build/utils.d.ts.map +1 -0
  49. package/build/utils.js +143 -0
  50. package/build/utils.js.map +1 -0
  51. package/expo-module.config.json +13 -4
  52. package/ios/AudioAnalysisData.swift +39 -0
  53. package/ios/AudioProcessingHelpers.swift +59 -0
  54. package/ios/AudioProcessor.swift +317 -0
  55. package/ios/AudioStreamError.swift +7 -0
  56. package/ios/AudioStreamManager.swift +204 -52
  57. package/ios/AudioStreamManagerDelegate.swift +4 -0
  58. package/ios/DataPoint.swift +41 -0
  59. package/ios/ExpoAudioStreamModule.swift +188 -6
  60. package/ios/Features.swift +44 -0
  61. package/ios/RecordingResult.swift +19 -0
  62. package/ios/RecordingSettings.swift +13 -0
  63. package/ios/WaveformExtractor.swift +105 -0
  64. package/package.json +9 -9
  65. package/plugin/tsconfig.json +13 -8
  66. package/publish.sh +8 -0
  67. package/src/AudioRecorder.provider.tsx +1 -1
  68. package/src/ExpoAudioStream.native.ts +6 -0
  69. package/src/ExpoAudioStream.types.ts +97 -11
  70. package/src/ExpoAudioStream.web.ts +228 -0
  71. package/src/ExpoAudioStreamModule.ts +17 -3
  72. package/src/WebRecorder.ts +364 -0
  73. package/src/index.ts +166 -20
  74. package/src/inlineAudioWebWorker.tsx +340 -0
  75. package/src/useAudioRecording.tsx +410 -0
  76. package/src/utils.ts +189 -0
  77. package/build/ExpoAudioStreamModule.web.d.ts +0 -37
  78. package/build/ExpoAudioStreamModule.web.d.ts.map +0 -1
  79. package/build/ExpoAudioStreamModule.web.js +0 -156
  80. package/build/ExpoAudioStreamModule.web.js.map +0 -1
  81. package/docs/demo.gif +0 -0
  82. package/release-it.js +0 -18
  83. package/src/ExpoAudioStreamModule.web.ts +0 -181
  84. package/src/useAudioRecording.ts +0 -268
  85. package/yarn-error.log +0 -7793
@@ -1,8 +1,6 @@
1
- import { AudioDataEvent } from "./useAudioRecording";
2
-
3
1
  export interface AudioEventPayload {
4
2
  encoded?: string;
5
- buffer?: Blob;
3
+ buffer?: ArrayBuffer;
6
4
  fileUri: string;
7
5
  lastEmittedSize: number;
8
6
  position: number;
@@ -14,7 +12,8 @@ export interface AudioEventPayload {
14
12
 
15
13
  export interface AudioStreamResult {
16
14
  fileUri: string;
17
- duration: number;
15
+ webAudioUri?: string;
16
+ durationMs: number;
18
17
  size: number;
19
18
  mimeType: string;
20
19
  channels?: number;
@@ -33,17 +32,104 @@ export interface StartAudioStreamResult {
33
32
  export interface AudioStreamStatus {
34
33
  isRecording: boolean;
35
34
  isPaused: boolean;
36
- duration: number;
35
+ durationMs: number;
37
36
  size: number;
38
37
  interval: number;
39
38
  mimeType: string;
40
39
  }
41
40
 
42
- export type EncodingType = "pcm_16bit" | "pcm_8bit";
41
+ export interface AudioDataEvent {
42
+ data: string | ArrayBuffer;
43
+ position: number;
44
+ fileUri: string;
45
+ eventDataSize: number;
46
+ totalSize: number;
47
+ }
48
+
49
+ export interface AudioFeatures {
50
+ energy: number;
51
+ mfcc: number[];
52
+ rms: number;
53
+ minAmplitude: number;
54
+ maxAmplitude: number;
55
+ zcr: number;
56
+ spectralCentroid: number;
57
+ spectralFlatness: number;
58
+ spectralRolloff: number;
59
+ spectralBandwidth: number;
60
+ chromagram: number[];
61
+ tempo: number;
62
+ hnr: number;
63
+ }
64
+
65
+ export interface AudioFeaturesOptions {
66
+ energy?: boolean;
67
+ mfcc?: boolean;
68
+ rms?: boolean;
69
+ zcr?: boolean;
70
+ spectralCentroid?: boolean;
71
+ spectralFlatness?: boolean;
72
+ spectralRolloff?: boolean;
73
+ spectralBandwidth?: boolean;
74
+ chromagram?: boolean;
75
+ tempo?: boolean;
76
+ hnr?: boolean;
77
+ }
78
+
79
+ export interface DataPoint {
80
+ id: number;
81
+ amplitude: number;
82
+ activeSpeech?: boolean;
83
+ dB?: number;
84
+ silent?: boolean;
85
+ features?: AudioFeatures;
86
+ startTime?: number;
87
+ endTime?: number;
88
+ // start / end position in bytes
89
+ startPosition?: number;
90
+ endPosition?: number;
91
+ // number of audio samples for this point (samples size depends on bit depth)
92
+ samples?: number;
93
+ // Id of the speaker for this point
94
+ speaker?: number;
95
+ }
96
+
97
+ export interface AudioAnalysisData {
98
+ pointsPerSecond: number; // How many consolidated value per second
99
+ durationMs: number; // Duration of the audio in milliseconds
100
+ bitDepth: number; // Bit depth of the audio
101
+ samples: number; // Size of the audio in bytes
102
+ numberOfChannels: number; // Number of audio channels
103
+ sampleRate: number; // Sample rate of the audio
104
+ dataPoints: DataPoint[];
105
+ amplitudeRange: {
106
+ min: number;
107
+ max: number;
108
+ };
109
+ speakerChanges?: {
110
+ timestamp: number;
111
+ speaker: number;
112
+ }[];
113
+ }
114
+
115
+ export type EncodingType = "pcm_32bit" | "pcm_16bit" | "pcm_8bit";
116
+ export type SampleRate = 16000 | 44100 | 48000;
43
117
  export interface RecordingConfig {
44
- sampleRate?: 16000 | 44100 | 48000;
45
- channels?: 1 | 2; // 1 or 2 MONO or STEREO
46
- encoding?: EncodingType;
47
- interval?: number;
48
- onAudioStream?: (_: AudioDataEvent) => Promise<void>;
118
+ sampleRate?: SampleRate; // Sample rate for recording
119
+ channels?: 1 | 2; // 1 or 2 (MONO or STEREO)
120
+ encoding?: EncodingType; // Encoding type for the recording
121
+ interval?: number; // Interval in milliseconds at which to emit recording data
122
+
123
+ // Optional parameters for audio processing
124
+ //TODO remove maxRecentDataDuration - should be replaced by maxDataPoints to 100.
125
+ maxRecentDataDuration?: number; // Maximum duration of recent data to keep for processing (default is 10.0 seconds)
126
+ enableProcessing?: boolean; // Boolean to enable/disable audio processing (default is false)
127
+ pointsPerSecond?: number; // Number of data points to extract per second of audio (default is 1000)
128
+ algorithm?: string; // Algorithm to use for extraction (default is "rms")
129
+ features?: AudioFeaturesOptions; // Feature options to extract (default is empty)
130
+
131
+ // Optional paramters from web
132
+
133
+ onAudioStream?: (_: AudioDataEvent) => Promise<void>; // Callback function to handle audio stream
134
+ onProcessingResult?: (_: AudioAnalysisData) => Promise<void>; // Callback function to handle processing results
49
135
  }
@@ -0,0 +1,228 @@
1
+ // src/ExpoAudioStreamModule.web.ts
2
+ import debug from "debug";
3
+ import { EventEmitter } from "expo-modules-core";
4
+
5
+ import {
6
+ AudioAnalysisData,
7
+ AudioEventPayload,
8
+ AudioStreamResult,
9
+ AudioStreamStatus,
10
+ RecordingConfig,
11
+ StartAudioStreamResult,
12
+ } from "./ExpoAudioStream.types";
13
+ import { WebRecorder } from "./WebRecorder";
14
+ import { encodingToBitDepth } from "./utils";
15
+
16
+ export interface EmitAudioEventProps {
17
+ data: ArrayBuffer;
18
+ position: number;
19
+ }
20
+ export type EmitAudioEventFunction = (_: EmitAudioEventProps) => void;
21
+ export type EmitAudioAnalysisFunction = (_: AudioAnalysisData) => void;
22
+
23
+ export interface ExpoAudioStreamWebProps {
24
+ audioWorkletUrl: string;
25
+ featuresExtratorUrl: string;
26
+ }
27
+
28
+ // const log = debug("expo-audio-stream:useAudioRecording");
29
+ const log = console;
30
+ export class ExpoAudioStreamWeb extends EventEmitter {
31
+ customRecorder: WebRecorder | null;
32
+ audioChunks: ArrayBuffer[];
33
+ isRecording: boolean;
34
+ isPaused: boolean;
35
+ recordingStartTime: number;
36
+ pausedTime: number;
37
+ currentDurationMs: number;
38
+ currentSize: number;
39
+ currentInterval: number;
40
+ lastEmittedSize: number;
41
+ lastEmittedTime: number;
42
+ streamUuid: string | null;
43
+ extension: "webm" | "wav" = "wav"; // Default extension is 'webm'
44
+ recordingConfig?: RecordingConfig;
45
+ bitDepth: number; // Bit depth of the audio
46
+ audioWorkletUrl: string;
47
+ featuresExtratorUrl: string;
48
+
49
+ constructor({
50
+ audioWorkletUrl,
51
+ featuresExtratorUrl,
52
+ }: ExpoAudioStreamWebProps) {
53
+ const mockNativeModule = {
54
+ addListener: (eventName: string) => {
55
+ // Not used on web
56
+ },
57
+ removeListeners: (count: number) => {
58
+ // Not used on web
59
+ },
60
+ };
61
+ super(mockNativeModule); // Pass the mock native module to the parent class
62
+
63
+ this.customRecorder = null;
64
+ this.audioChunks = [];
65
+ this.isRecording = false;
66
+ this.isPaused = false;
67
+ this.recordingStartTime = 0;
68
+ this.pausedTime = 0;
69
+ this.currentDurationMs = 0;
70
+ this.currentSize = 0;
71
+ this.bitDepth = 32; // Default
72
+ this.currentInterval = 1000; // Default interval in ms
73
+ this.lastEmittedSize = 0;
74
+ this.lastEmittedTime = 0;
75
+ this.streamUuid = null; // Initialize UUID on first recording start
76
+ this.audioWorkletUrl = audioWorkletUrl;
77
+ this.featuresExtratorUrl = featuresExtratorUrl;
78
+ }
79
+
80
+ // Utility to handle user media stream
81
+ async getMediaStream() {
82
+ try {
83
+ return await navigator.mediaDevices.getUserMedia({ audio: true });
84
+ } catch (error) {
85
+ console.error("Failed to get media stream:", error);
86
+ throw error;
87
+ }
88
+ }
89
+
90
+ // Start recording with options
91
+ async startRecording(recordingConfig: RecordingConfig = {}) {
92
+ if (this.isRecording) {
93
+ throw new Error("Recording is already in progress");
94
+ }
95
+
96
+ this.bitDepth = encodingToBitDepth({
97
+ encoding: recordingConfig.encoding ?? "pcm_32bit",
98
+ });
99
+
100
+ const audioContext = new (window.AudioContext ||
101
+ // @ts-ignore - Allow webkitAudioContext for Safari
102
+ window.webkitAudioContext)();
103
+ const stream = await this.getMediaStream();
104
+
105
+ const source = audioContext.createMediaStreamSource(stream);
106
+
107
+ this.customRecorder = new WebRecorder({
108
+ audioContext,
109
+ source,
110
+ recordingConfig,
111
+ audioWorkletUrl: this.audioWorkletUrl,
112
+ featuresExtratorUrl: this.featuresExtratorUrl,
113
+ emitAudioEventCallback: ({ data, position }: EmitAudioEventProps) => {
114
+ this.audioChunks.push(data);
115
+ this.currentSize += data.byteLength;
116
+ this.emitAudioEvent({ data, position });
117
+ this.lastEmittedTime = Date.now();
118
+ this.lastEmittedSize = this.currentSize;
119
+ },
120
+ emitAudioAnalysisCallback: (audioAnalysisData: AudioAnalysisData) => {
121
+ console.log(`Emitted AudioAnalysis:`, audioAnalysisData);
122
+ this.emit("AudioAnalysis", audioAnalysisData);
123
+ },
124
+ });
125
+ await this.customRecorder.init();
126
+ this.customRecorder.start();
127
+
128
+ // // Set a timer to stop recording after 5 seconds
129
+ // setTimeout(() => {
130
+ // console.log("AUTO Stopping recording");
131
+ // this.customRecorder?.stopAndPlay();
132
+ // this.isRecording = false;
133
+ // }, 3000);
134
+
135
+ this.isRecording = true;
136
+ this.recordingConfig = recordingConfig;
137
+ this.recordingStartTime = Date.now();
138
+ this.pausedTime = 0;
139
+ this.lastEmittedSize = 0;
140
+ this.lastEmittedTime = 0;
141
+ this.streamUuid = Date.now().toString();
142
+ const fileUri = `${this.streamUuid}.${this.extension}`;
143
+ const streamConfig: StartAudioStreamResult = {
144
+ fileUri,
145
+ mimeType: `audio/${this.extension}`,
146
+ bitDepth: this.bitDepth,
147
+ channels: recordingConfig.channels ?? 1,
148
+ sampleRate: recordingConfig.sampleRate ?? 44100,
149
+ };
150
+ return streamConfig;
151
+ }
152
+
153
+ emitAudioEvent({ data, position }: EmitAudioEventProps) {
154
+ const fileUri = `${this.streamUuid}.${this.extension}`;
155
+ const audioEventPayload: AudioEventPayload = {
156
+ fileUri,
157
+ mimeType: `audio/${this.extension}`,
158
+ lastEmittedSize: this.lastEmittedSize, // Since this might be continuously streaming, adjust accordingly
159
+ deltaSize: data.byteLength,
160
+ position,
161
+ totalSize: this.currentSize,
162
+ buffer: data,
163
+ streamUuid: this.streamUuid ?? "", // Generate or manage UUID for stream identification
164
+ };
165
+
166
+ this.emit("AudioData", audioEventPayload);
167
+ }
168
+
169
+ // Stop recording
170
+ async stopRecording(): Promise<AudioStreamResult | null> {
171
+ if (this.customRecorder) {
172
+ const fullPcmBuffer = await this.customRecorder.stop();
173
+ log.debug(`Stopped recording`, fullPcmBuffer);
174
+ }
175
+ this.isRecording = false;
176
+ this.currentDurationMs = Date.now() - this.recordingStartTime;
177
+ const result: AudioStreamResult = {
178
+ fileUri: `${this.streamUuid}.${this.extension}`,
179
+ bitDepth: this.bitDepth,
180
+ channels: this.recordingConfig?.channels ?? 1,
181
+ sampleRate: this.recordingConfig?.sampleRate ?? 44100,
182
+ durationMs: this.currentDurationMs,
183
+ size: this.currentSize,
184
+ mimeType: `audio/${this.extension}`,
185
+ };
186
+
187
+ return result;
188
+ }
189
+
190
+ // Pause recording
191
+ async pauseRecording() {
192
+ if (!this.isRecording || this.isPaused) {
193
+ throw new Error("Recording is not active or already paused");
194
+ }
195
+
196
+ if (this.customRecorder) {
197
+ this.customRecorder.stop();
198
+ }
199
+ this.isPaused = true;
200
+ this.pausedTime = Date.now();
201
+ }
202
+
203
+ // Resume recording
204
+ async resumeRecording() {
205
+ if (!this.isPaused) {
206
+ throw new Error("Recording is not paused");
207
+ }
208
+
209
+ if (this.customRecorder) {
210
+ this.customRecorder.resume();
211
+ }
212
+ this.isPaused = false;
213
+ this.recordingStartTime += Date.now() - this.pausedTime;
214
+ }
215
+
216
+ // Get current status
217
+ status() {
218
+ const status: AudioStreamStatus = {
219
+ isRecording: this.isRecording,
220
+ isPaused: this.isPaused,
221
+ durationMs: Date.now() - this.recordingStartTime,
222
+ size: this.currentSize,
223
+ interval: this.currentInterval,
224
+ mimeType: `audio/${this.extension}`,
225
+ };
226
+ return status;
227
+ }
228
+ }
@@ -1,5 +1,19 @@
1
1
  import { requireNativeModule } from "expo-modules-core";
2
+ import { Platform } from "react-native";
2
3
 
3
- // It loads the native module object from the JSI or falls back to
4
- // the bridge module (from NativeModulesProxy) if the remote debugger is on.
5
- export default requireNativeModule("ExpoAudioStream");
4
+ import {
5
+ ExpoAudioStreamWeb,
6
+ ExpoAudioStreamWebProps,
7
+ } from "./ExpoAudioStream.web";
8
+
9
+ let ExpoAudioStreamModule: any;
10
+
11
+ if (Platform.OS === "web") {
12
+ ExpoAudioStreamModule = (webProps: ExpoAudioStreamWebProps) => {
13
+ return new ExpoAudioStreamWeb(webProps);
14
+ };
15
+ } else {
16
+ ExpoAudioStreamModule = requireNativeModule("ExpoAudioStream");
17
+ }
18
+
19
+ export default ExpoAudioStreamModule;