@siteed/expo-audio-stream 1.1.2 → 1.1.3

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 (105) hide show
  1. package/build/AudioAnalysis/AudioAnalysis.types.d.ts +74 -0
  2. package/build/AudioAnalysis/AudioAnalysis.types.d.ts.map +1 -0
  3. package/build/AudioAnalysis/AudioAnalysis.types.js +3 -0
  4. package/build/AudioAnalysis/AudioAnalysis.types.js.map +1 -0
  5. package/build/AudioAnalysis/extractAudioAnalysis.d.ts +20 -0
  6. package/build/AudioAnalysis/extractAudioAnalysis.d.ts.map +1 -0
  7. package/build/AudioAnalysis/extractAudioAnalysis.js +88 -0
  8. package/build/AudioAnalysis/extractAudioAnalysis.js.map +1 -0
  9. package/build/AudioAnalysis/extractWaveform.d.ts +8 -0
  10. package/build/AudioAnalysis/extractWaveform.d.ts.map +1 -0
  11. package/build/AudioAnalysis/extractWaveform.js +14 -0
  12. package/build/AudioAnalysis/extractWaveform.js.map +1 -0
  13. package/build/AudioRecorder.provider.d.ts +23 -0
  14. package/build/AudioRecorder.provider.d.ts.map +1 -0
  15. package/build/AudioRecorder.provider.js +36 -0
  16. package/build/AudioRecorder.provider.js.map +1 -0
  17. package/build/ExpoAudioStream.native.d.ts +3 -0
  18. package/build/ExpoAudioStream.native.d.ts.map +1 -0
  19. package/{src/ExpoAudioStream.native.ts → build/ExpoAudioStream.native.js} +3 -3
  20. package/build/ExpoAudioStream.native.js.map +1 -0
  21. package/build/ExpoAudioStream.types.d.ts +65 -0
  22. package/build/ExpoAudioStream.types.d.ts.map +1 -0
  23. package/build/ExpoAudioStream.types.js +2 -0
  24. package/build/ExpoAudioStream.types.js.map +1 -0
  25. package/build/ExpoAudioStream.web.d.ts +42 -0
  26. package/build/ExpoAudioStream.web.d.ts.map +1 -0
  27. package/build/ExpoAudioStream.web.js +203 -0
  28. package/build/ExpoAudioStream.web.js.map +1 -0
  29. package/build/ExpoAudioStreamModule.d.ts +3 -0
  30. package/build/ExpoAudioStreamModule.d.ts.map +1 -0
  31. package/build/ExpoAudioStreamModule.js +25 -0
  32. package/build/ExpoAudioStreamModule.js.map +1 -0
  33. package/build/WebRecorder.web.d.ts +51 -0
  34. package/build/WebRecorder.web.d.ts.map +1 -0
  35. package/build/WebRecorder.web.js +298 -0
  36. package/build/WebRecorder.web.js.map +1 -0
  37. package/build/constants.d.ts +11 -0
  38. package/build/constants.d.ts.map +1 -0
  39. package/build/constants.js +14 -0
  40. package/build/constants.js.map +1 -0
  41. package/build/events.d.ts +18 -0
  42. package/build/events.d.ts.map +1 -0
  43. package/build/events.js +15 -0
  44. package/build/events.js.map +1 -0
  45. package/build/index.d.ts +11 -0
  46. package/build/index.d.ts.map +1 -0
  47. package/build/index.js.map +1 -0
  48. package/build/logger.d.ts +9 -0
  49. package/build/logger.d.ts.map +1 -0
  50. package/build/logger.js +13 -0
  51. package/build/logger.js.map +1 -0
  52. package/build/useAudioRecorder.d.ts +20 -0
  53. package/build/useAudioRecorder.d.ts.map +1 -0
  54. package/build/useAudioRecorder.js +271 -0
  55. package/build/useAudioRecorder.js.map +1 -0
  56. package/build/utils/BlobFix.d.ts +9 -0
  57. package/build/utils/BlobFix.d.ts.map +1 -0
  58. package/{src/utils/BlobFix.ts → build/utils/BlobFix.js} +66 -122
  59. package/build/utils/BlobFix.js.map +1 -0
  60. package/build/utils/concatenateBuffers.d.ts +8 -0
  61. package/build/utils/concatenateBuffers.d.ts.map +1 -0
  62. package/{src/utils/concatenateBuffers.ts → build/utils/concatenateBuffers.js} +10 -13
  63. package/build/utils/concatenateBuffers.js.map +1 -0
  64. package/build/utils/convertPCMToFloat32.d.ts +11 -0
  65. package/build/utils/convertPCMToFloat32.d.ts.map +1 -0
  66. package/build/utils/convertPCMToFloat32.js +54 -0
  67. package/build/utils/convertPCMToFloat32.js.map +1 -0
  68. package/build/utils/encodingToBitDepth.d.ts +5 -0
  69. package/build/utils/encodingToBitDepth.d.ts.map +1 -0
  70. package/build/utils/encodingToBitDepth.js +13 -0
  71. package/build/utils/encodingToBitDepth.js.map +1 -0
  72. package/build/utils/getWavFileInfo.d.ts +26 -0
  73. package/build/utils/getWavFileInfo.d.ts.map +1 -0
  74. package/build/utils/getWavFileInfo.js +92 -0
  75. package/build/utils/getWavFileInfo.js.map +1 -0
  76. package/build/utils/writeWavHeader.d.ts +9 -0
  77. package/build/utils/writeWavHeader.d.ts.map +1 -0
  78. package/build/utils/writeWavHeader.js +45 -0
  79. package/build/utils/writeWavHeader.js.map +1 -0
  80. package/build/workers/InlineFeaturesExtractor.web.d.ts +2 -0
  81. package/build/workers/InlineFeaturesExtractor.web.d.ts.map +1 -0
  82. package/{src/workers/InlineFeaturesExtractor.web.tsx → build/workers/InlineFeaturesExtractor.web.js} +2 -1
  83. package/build/workers/InlineFeaturesExtractor.web.js.map +1 -0
  84. package/build/workers/inlineAudioWebWorker.web.d.ts +2 -0
  85. package/build/workers/inlineAudioWebWorker.web.d.ts.map +1 -0
  86. package/{src/workers/inlineAudioWebWorker.web.tsx → build/workers/inlineAudioWebWorker.web.js} +2 -1
  87. package/build/workers/inlineAudioWebWorker.web.js.map +1 -0
  88. package/package.json +2 -2
  89. package/src/AudioAnalysis/AudioAnalysis.types.ts +0 -84
  90. package/src/AudioAnalysis/extractAudioAnalysis.ts +0 -147
  91. package/src/AudioAnalysis/extractWaveform.ts +0 -25
  92. package/src/AudioRecorder.provider.tsx +0 -70
  93. package/src/ExpoAudioStream.types.ts +0 -80
  94. package/src/ExpoAudioStream.web.ts +0 -255
  95. package/src/ExpoAudioStreamModule.ts +0 -31
  96. package/src/WebRecorder.web.ts +0 -433
  97. package/src/constants.ts +0 -18
  98. package/src/events.ts +0 -39
  99. package/src/index.ts +0 -24
  100. package/src/logger.ts +0 -22
  101. package/src/useAudioRecorder.tsx +0 -420
  102. package/src/utils/convertPCMToFloat32.ts +0 -75
  103. package/src/utils/encodingToBitDepth.ts +0 -18
  104. package/src/utils/getWavFileInfo.ts +0 -132
  105. package/src/utils/writeWavHeader.ts +0 -61
@@ -0,0 +1,203 @@
1
+ // src/ExpoAudioStreamModule.web.ts
2
+ import { EventEmitter } from 'expo-modules-core';
3
+ import { WebRecorder } from './WebRecorder.web';
4
+ import { getLogger } from './logger';
5
+ import { encodingToBitDepth } from './utils/encodingToBitDepth';
6
+ import { writeWavHeader } from './utils/writeWavHeader';
7
+ const logger = getLogger('ExpoAudioStreamWeb');
8
+ export class ExpoAudioStreamWeb extends EventEmitter {
9
+ customRecorder;
10
+ audioChunks;
11
+ isRecording;
12
+ isPaused;
13
+ recordingStartTime;
14
+ pausedTime;
15
+ currentDurationMs;
16
+ currentSize;
17
+ currentInterval;
18
+ lastEmittedSize;
19
+ lastEmittedTime;
20
+ streamUuid;
21
+ extension = 'wav'; // Default extension is 'webm'
22
+ recordingConfig;
23
+ bitDepth; // Bit depth of the audio
24
+ audioWorkletUrl;
25
+ featuresExtratorUrl;
26
+ constructor({ audioWorkletUrl, featuresExtratorUrl, }) {
27
+ const mockNativeModule = {
28
+ addListener: () => {
29
+ // Not used on web
30
+ },
31
+ removeListeners: () => {
32
+ // Not used on web
33
+ },
34
+ };
35
+ super(mockNativeModule); // Pass the mock native module to the parent class
36
+ this.customRecorder = null;
37
+ this.audioChunks = [];
38
+ this.isRecording = false;
39
+ this.isPaused = false;
40
+ this.recordingStartTime = 0;
41
+ this.pausedTime = 0;
42
+ this.currentDurationMs = 0;
43
+ this.currentSize = 0;
44
+ this.bitDepth = 32; // Default
45
+ this.currentInterval = 1000; // Default interval in ms
46
+ this.lastEmittedSize = 0;
47
+ this.lastEmittedTime = 0;
48
+ this.streamUuid = null; // Initialize UUID on first recording start
49
+ this.audioWorkletUrl = audioWorkletUrl;
50
+ this.featuresExtratorUrl = featuresExtratorUrl;
51
+ }
52
+ // Utility to handle user media stream
53
+ async getMediaStream() {
54
+ try {
55
+ return await navigator.mediaDevices.getUserMedia({ audio: true });
56
+ }
57
+ catch (error) {
58
+ console.error('Failed to get media stream:', error);
59
+ throw error;
60
+ }
61
+ }
62
+ // Start recording with options
63
+ async startRecording(recordingConfig = {}) {
64
+ if (this.isRecording) {
65
+ throw new Error('Recording is already in progress');
66
+ }
67
+ this.bitDepth = encodingToBitDepth({
68
+ encoding: recordingConfig.encoding ?? 'pcm_32bit',
69
+ });
70
+ const audioContext = new (window.AudioContext ||
71
+ // eslint-disable-next-line @typescript-eslint/ban-ts-comment
72
+ // @ts-ignore - Allow webkitAudioContext for Safari
73
+ window.webkitAudioContext)();
74
+ const stream = await this.getMediaStream();
75
+ const source = audioContext.createMediaStreamSource(stream);
76
+ this.customRecorder = new WebRecorder({
77
+ audioContext,
78
+ source,
79
+ recordingConfig,
80
+ audioWorkletUrl: this.audioWorkletUrl,
81
+ emitAudioEventCallback: ({ data, position, }) => {
82
+ this.audioChunks.push(data);
83
+ this.currentSize += data.byteLength;
84
+ this.emitAudioEvent({ data, position });
85
+ this.lastEmittedTime = Date.now();
86
+ this.lastEmittedSize = this.currentSize;
87
+ },
88
+ emitAudioAnalysisCallback: (audioAnalysisData) => {
89
+ logger.log(`Emitted AudioAnalysis:`, audioAnalysisData);
90
+ this.emit('AudioAnalysis', audioAnalysisData);
91
+ },
92
+ });
93
+ await this.customRecorder.init();
94
+ this.customRecorder.start();
95
+ // // Set a timer to stop recording after 5 seconds
96
+ // setTimeout(() => {
97
+ // logger.log("AUTO Stopping recording");
98
+ // this.customRecorder?.stopAndPlay();
99
+ // this.isRecording = false;
100
+ // }, 3000);
101
+ this.isRecording = true;
102
+ this.recordingConfig = recordingConfig;
103
+ this.recordingStartTime = Date.now();
104
+ this.pausedTime = 0;
105
+ this.lastEmittedSize = 0;
106
+ this.lastEmittedTime = 0;
107
+ this.streamUuid = Date.now().toString();
108
+ const fileUri = `${this.streamUuid}.${this.extension}`;
109
+ const streamConfig = {
110
+ fileUri,
111
+ mimeType: `audio/${this.extension}`,
112
+ bitDepth: this.bitDepth,
113
+ channels: recordingConfig.channels ?? 1,
114
+ sampleRate: recordingConfig.sampleRate ?? 44100,
115
+ };
116
+ return streamConfig;
117
+ }
118
+ emitAudioEvent({ data, position }) {
119
+ const fileUri = `${this.streamUuid}.${this.extension}`;
120
+ const audioEventPayload = {
121
+ fileUri,
122
+ mimeType: `audio/${this.extension}`,
123
+ lastEmittedSize: this.lastEmittedSize, // Since this might be continuously streaming, adjust accordingly
124
+ deltaSize: data.byteLength,
125
+ position,
126
+ totalSize: this.currentSize,
127
+ buffer: data,
128
+ streamUuid: this.streamUuid ?? '', // Generate or manage UUID for stream identification
129
+ };
130
+ this.emit('AudioData', audioEventPayload);
131
+ }
132
+ // Stop recording
133
+ async stopRecording() {
134
+ if (!this.customRecorder) {
135
+ throw new Error('Recorder is not initialized');
136
+ }
137
+ const fullPcmBufferArray = await this.customRecorder.stop();
138
+ // concat all audio chunks
139
+ logger.debug(`Stopped recording`, fullPcmBufferArray);
140
+ this.isRecording = false;
141
+ this.currentDurationMs = Date.now() - this.recordingStartTime;
142
+ const wavConfig = {
143
+ buffer: fullPcmBufferArray.buffer,
144
+ sampleRate: this.recordingConfig?.sampleRate ?? 44100,
145
+ numChannels: this.recordingConfig?.channels ?? 1,
146
+ bitDepth: this.bitDepth,
147
+ };
148
+ logger.debug(`Writing wav header`, wavConfig);
149
+ const wavBuffer = writeWavHeader(wavConfig).slice(0);
150
+ // Create blob fileUri from audio chunks
151
+ const blob = new Blob([wavBuffer], {
152
+ type: `audio/${this.extension}`,
153
+ });
154
+ const fileUri = URL.createObjectURL(blob);
155
+ const result = {
156
+ fileUri,
157
+ filename: `${this.streamUuid}.${this.extension}`,
158
+ wavPCMData: fullPcmBufferArray,
159
+ bitDepth: this.bitDepth,
160
+ channels: this.recordingConfig?.channels ?? 1,
161
+ sampleRate: this.recordingConfig?.sampleRate ?? 44100,
162
+ durationMs: this.currentDurationMs,
163
+ size: this.currentSize,
164
+ mimeType: `audio/${this.extension}`,
165
+ };
166
+ return result;
167
+ }
168
+ // Pause recording
169
+ async pauseRecording() {
170
+ if (!this.isRecording || this.isPaused) {
171
+ throw new Error('Recording is not active or already paused');
172
+ }
173
+ if (this.customRecorder) {
174
+ this.customRecorder.pause();
175
+ }
176
+ this.isPaused = true;
177
+ this.pausedTime = Date.now();
178
+ }
179
+ // Resume recording
180
+ async resumeRecording() {
181
+ if (!this.isPaused) {
182
+ throw new Error('Recording is not paused');
183
+ }
184
+ if (this.customRecorder) {
185
+ this.customRecorder.resume();
186
+ }
187
+ this.isPaused = false;
188
+ this.recordingStartTime += Date.now() - this.pausedTime;
189
+ }
190
+ // Get current status
191
+ status() {
192
+ const status = {
193
+ isRecording: this.isRecording,
194
+ isPaused: this.isPaused,
195
+ durationMs: Date.now() - this.recordingStartTime,
196
+ size: this.currentSize,
197
+ interval: this.currentInterval,
198
+ mimeType: `audio/${this.extension}`,
199
+ };
200
+ return status;
201
+ }
202
+ }
203
+ //# sourceMappingURL=ExpoAudioStream.web.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"ExpoAudioStream.web.js","sourceRoot":"","sources":["../src/ExpoAudioStream.web.ts"],"names":[],"mappings":"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","sourcesContent":["// src/ExpoAudioStreamModule.web.ts\nimport { EventEmitter } from 'expo-modules-core'\n\nimport { AudioAnalysis } from './AudioAnalysis/AudioAnalysis.types'\nimport {\n AudioRecording,\n AudioStreamStatus,\n BitDepth,\n RecordingConfig,\n StartRecordingResult,\n} from './ExpoAudioStream.types'\nimport { WebRecorder } from './WebRecorder.web'\nimport { AudioEventPayload } from './events'\nimport { getLogger } from './logger'\nimport { encodingToBitDepth } from './utils/encodingToBitDepth'\nimport { writeWavHeader } from './utils/writeWavHeader'\n\nexport interface EmitAudioEventProps {\n data: Float32Array\n position: number\n}\nexport type EmitAudioEventFunction = (_: EmitAudioEventProps) => void\nexport type EmitAudioAnalysisFunction = (_: AudioAnalysis) => void\n\nexport interface ExpoAudioStreamWebProps {\n audioWorkletUrl: string\n featuresExtratorUrl: string\n}\n\nconst logger = getLogger('ExpoAudioStreamWeb')\n\nexport class ExpoAudioStreamWeb extends EventEmitter {\n customRecorder: WebRecorder | null\n audioChunks: ArrayBuffer[]\n isRecording: boolean\n isPaused: boolean\n recordingStartTime: number\n pausedTime: number\n currentDurationMs: number\n currentSize: number\n currentInterval: number\n lastEmittedSize: number\n lastEmittedTime: number\n streamUuid: string | null\n extension: 'webm' | 'wav' = 'wav' // Default extension is 'webm'\n recordingConfig?: RecordingConfig\n bitDepth: BitDepth // Bit depth of the audio\n audioWorkletUrl: string\n featuresExtratorUrl: string\n\n constructor({\n audioWorkletUrl,\n featuresExtratorUrl,\n }: ExpoAudioStreamWebProps) {\n const mockNativeModule = {\n addListener: () => {\n // Not used on web\n },\n removeListeners: () => {\n // Not used on web\n },\n }\n super(mockNativeModule) // Pass the mock native module to the parent class\n\n this.customRecorder = null\n this.audioChunks = []\n this.isRecording = false\n this.isPaused = false\n this.recordingStartTime = 0\n this.pausedTime = 0\n this.currentDurationMs = 0\n this.currentSize = 0\n this.bitDepth = 32 // Default\n this.currentInterval = 1000 // Default interval in ms\n this.lastEmittedSize = 0\n this.lastEmittedTime = 0\n this.streamUuid = null // Initialize UUID on first recording start\n this.audioWorkletUrl = audioWorkletUrl\n this.featuresExtratorUrl = featuresExtratorUrl\n }\n\n // Utility to handle user media stream\n async getMediaStream() {\n try {\n return await navigator.mediaDevices.getUserMedia({ audio: true })\n } catch (error) {\n console.error('Failed to get media stream:', error)\n throw error\n }\n }\n\n // Start recording with options\n async startRecording(recordingConfig: RecordingConfig = {}) {\n if (this.isRecording) {\n throw new Error('Recording is already in progress')\n }\n\n this.bitDepth = encodingToBitDepth({\n encoding: recordingConfig.encoding ?? 'pcm_32bit',\n })\n\n const audioContext = new (window.AudioContext ||\n // eslint-disable-next-line @typescript-eslint/ban-ts-comment\n // @ts-ignore - Allow webkitAudioContext for Safari\n window.webkitAudioContext)()\n const stream = await this.getMediaStream()\n\n const source = audioContext.createMediaStreamSource(stream)\n\n this.customRecorder = new WebRecorder({\n audioContext,\n source,\n recordingConfig,\n audioWorkletUrl: this.audioWorkletUrl,\n emitAudioEventCallback: ({\n data,\n position,\n }: EmitAudioEventProps) => {\n this.audioChunks.push(data)\n this.currentSize += data.byteLength\n this.emitAudioEvent({ data, position })\n this.lastEmittedTime = Date.now()\n this.lastEmittedSize = this.currentSize\n },\n emitAudioAnalysisCallback: (audioAnalysisData: AudioAnalysis) => {\n logger.log(`Emitted AudioAnalysis:`, audioAnalysisData)\n this.emit('AudioAnalysis', audioAnalysisData)\n },\n })\n await this.customRecorder.init()\n this.customRecorder.start()\n\n // // Set a timer to stop recording after 5 seconds\n // setTimeout(() => {\n // logger.log(\"AUTO Stopping recording\");\n // this.customRecorder?.stopAndPlay();\n // this.isRecording = false;\n // }, 3000);\n\n this.isRecording = true\n this.recordingConfig = recordingConfig\n this.recordingStartTime = Date.now()\n this.pausedTime = 0\n this.lastEmittedSize = 0\n this.lastEmittedTime = 0\n this.streamUuid = Date.now().toString()\n const fileUri = `${this.streamUuid}.${this.extension}`\n const streamConfig: StartRecordingResult = {\n fileUri,\n mimeType: `audio/${this.extension}`,\n bitDepth: this.bitDepth,\n channels: recordingConfig.channels ?? 1,\n sampleRate: recordingConfig.sampleRate ?? 44100,\n }\n return streamConfig\n }\n\n emitAudioEvent({ data, position }: EmitAudioEventProps) {\n const fileUri = `${this.streamUuid}.${this.extension}`\n const audioEventPayload: AudioEventPayload = {\n fileUri,\n mimeType: `audio/${this.extension}`,\n lastEmittedSize: this.lastEmittedSize, // Since this might be continuously streaming, adjust accordingly\n deltaSize: data.byteLength,\n position,\n totalSize: this.currentSize,\n buffer: data,\n streamUuid: this.streamUuid ?? '', // Generate or manage UUID for stream identification\n }\n\n this.emit('AudioData', audioEventPayload)\n }\n\n // Stop recording\n async stopRecording(): Promise<AudioRecording> {\n if (!this.customRecorder) {\n throw new Error('Recorder is not initialized')\n }\n\n const fullPcmBufferArray = await this.customRecorder.stop()\n\n // concat all audio chunks\n logger.debug(`Stopped recording`, fullPcmBufferArray)\n this.isRecording = false\n this.currentDurationMs = Date.now() - this.recordingStartTime\n\n const wavConfig = {\n buffer: fullPcmBufferArray.buffer,\n sampleRate: this.recordingConfig?.sampleRate ?? 44100,\n numChannels: this.recordingConfig?.channels ?? 1,\n bitDepth: this.bitDepth,\n }\n logger.debug(`Writing wav header`, wavConfig)\n const wavBuffer = writeWavHeader(wavConfig).slice(0)\n\n // Create blob fileUri from audio chunks\n const blob = new Blob([wavBuffer], {\n type: `audio/${this.extension}`,\n })\n const fileUri = URL.createObjectURL(blob)\n\n const result: AudioRecording = {\n fileUri,\n filename: `${this.streamUuid}.${this.extension}`,\n wavPCMData: fullPcmBufferArray,\n bitDepth: this.bitDepth,\n channels: this.recordingConfig?.channels ?? 1,\n sampleRate: this.recordingConfig?.sampleRate ?? 44100,\n durationMs: this.currentDurationMs,\n size: this.currentSize,\n mimeType: `audio/${this.extension}`,\n }\n\n return result\n }\n\n // Pause recording\n async pauseRecording() {\n if (!this.isRecording || this.isPaused) {\n throw new Error('Recording is not active or already paused')\n }\n\n if (this.customRecorder) {\n this.customRecorder.pause()\n }\n this.isPaused = true\n this.pausedTime = Date.now()\n }\n\n // Resume recording\n async resumeRecording() {\n if (!this.isPaused) {\n throw new Error('Recording is not paused')\n }\n\n if (this.customRecorder) {\n this.customRecorder.resume()\n }\n this.isPaused = false\n this.recordingStartTime += Date.now() - this.pausedTime\n }\n\n // Get current status\n status() {\n const status: AudioStreamStatus = {\n isRecording: this.isRecording,\n isPaused: this.isPaused,\n durationMs: Date.now() - this.recordingStartTime,\n size: this.currentSize,\n interval: this.currentInterval,\n mimeType: `audio/${this.extension}`,\n }\n return status\n }\n}\n"]}
@@ -0,0 +1,3 @@
1
+ declare let ExpoAudioStreamModule: any;
2
+ export default ExpoAudioStreamModule;
3
+ //# sourceMappingURL=ExpoAudioStreamModule.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"ExpoAudioStreamModule.d.ts","sourceRoot":"","sources":["../src/ExpoAudioStreamModule.ts"],"names":[],"mappings":"AASA,QAAA,IAAI,qBAAqB,EAAE,GAAG,CAAA;AAqB9B,eAAe,qBAAqB,CAAA"}
@@ -0,0 +1,25 @@
1
+ import { requireNativeModule } from 'expo-modules-core';
2
+ import { Platform } from 'react-native';
3
+ import { ExpoAudioStreamWeb, } from './ExpoAudioStream.web';
4
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
5
+ let ExpoAudioStreamModule;
6
+ if (Platform.OS === 'web') {
7
+ let instance = null;
8
+ ExpoAudioStreamModule = (webProps) => {
9
+ if (!instance) {
10
+ instance = new ExpoAudioStreamWeb(webProps);
11
+ }
12
+ return instance;
13
+ };
14
+ ExpoAudioStreamModule.requestPermissionsAsync = async () => {
15
+ return { status: 'granted' };
16
+ };
17
+ ExpoAudioStreamModule.getPermissionsAsync = async () => {
18
+ return { status: 'granted' };
19
+ };
20
+ }
21
+ else {
22
+ ExpoAudioStreamModule = requireNativeModule('ExpoAudioStream');
23
+ }
24
+ export default ExpoAudioStreamModule;
25
+ //# sourceMappingURL=ExpoAudioStreamModule.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"ExpoAudioStreamModule.js","sourceRoot":"","sources":["../src/ExpoAudioStreamModule.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,mBAAmB,EAAE,MAAM,mBAAmB,CAAA;AACvD,OAAO,EAAE,QAAQ,EAAE,MAAM,cAAc,CAAA;AAEvC,OAAO,EACH,kBAAkB,GAErB,MAAM,uBAAuB,CAAA;AAE9B,8DAA8D;AAC9D,IAAI,qBAA0B,CAAA;AAE9B,IAAI,QAAQ,CAAC,EAAE,KAAK,KAAK,EAAE,CAAC;IACxB,IAAI,QAAQ,GAA8B,IAAI,CAAA;IAE9C,qBAAqB,GAAG,CAAC,QAAiC,EAAE,EAAE;QAC1D,IAAI,CAAC,QAAQ,EAAE,CAAC;YACZ,QAAQ,GAAG,IAAI,kBAAkB,CAAC,QAAQ,CAAC,CAAA;QAC/C,CAAC;QACD,OAAO,QAAQ,CAAA;IACnB,CAAC,CAAA;IACD,qBAAqB,CAAC,uBAAuB,GAAG,KAAK,IAAI,EAAE;QACvD,OAAO,EAAE,MAAM,EAAE,SAAS,EAAE,CAAA;IAChC,CAAC,CAAA;IACD,qBAAqB,CAAC,mBAAmB,GAAG,KAAK,IAAI,EAAE;QACnD,OAAO,EAAE,MAAM,EAAE,SAAS,EAAE,CAAA;IAChC,CAAC,CAAA;AACL,CAAC;KAAM,CAAC;IACJ,qBAAqB,GAAG,mBAAmB,CAAC,iBAAiB,CAAC,CAAA;AAClE,CAAC;AAED,eAAe,qBAAqB,CAAA","sourcesContent":["import { requireNativeModule } from 'expo-modules-core'\nimport { Platform } from 'react-native'\n\nimport {\n ExpoAudioStreamWeb,\n ExpoAudioStreamWebProps,\n} from './ExpoAudioStream.web'\n\n// eslint-disable-next-line @typescript-eslint/no-explicit-any\nlet ExpoAudioStreamModule: any\n\nif (Platform.OS === 'web') {\n let instance: ExpoAudioStreamWeb | null = null\n\n ExpoAudioStreamModule = (webProps: ExpoAudioStreamWebProps) => {\n if (!instance) {\n instance = new ExpoAudioStreamWeb(webProps)\n }\n return instance\n }\n ExpoAudioStreamModule.requestPermissionsAsync = async () => {\n return { status: 'granted' }\n }\n ExpoAudioStreamModule.getPermissionsAsync = async () => {\n return { status: 'granted' }\n }\n} else {\n ExpoAudioStreamModule = requireNativeModule('ExpoAudioStream')\n}\n\nexport default ExpoAudioStreamModule\n"]}
@@ -0,0 +1,51 @@
1
+ import { AudioAnalysis } from './AudioAnalysis/AudioAnalysis.types';
2
+ import { RecordingConfig } from './ExpoAudioStream.types';
3
+ import { EmitAudioAnalysisFunction, EmitAudioEventFunction } from './ExpoAudioStream.web';
4
+ interface AudioFeaturesEvent {
5
+ data: {
6
+ command: string;
7
+ result: AudioAnalysis;
8
+ };
9
+ }
10
+ export declare class WebRecorder {
11
+ private audioContext;
12
+ private audioWorkletNode;
13
+ private featureExtractorWorker?;
14
+ private source;
15
+ private audioWorkletUrl;
16
+ private emitAudioEventCallback;
17
+ private emitAudioAnalysisCallback;
18
+ private config;
19
+ private position;
20
+ private numberOfChannels;
21
+ private bitDepth;
22
+ private exportBitDepth;
23
+ private buffer;
24
+ private bufferSize;
25
+ private audioAnalysisData;
26
+ constructor({ audioContext, source, recordingConfig, audioWorkletUrl, emitAudioEventCallback, emitAudioAnalysisCallback, }: {
27
+ audioContext: AudioContext;
28
+ source: MediaStreamAudioSourceNode;
29
+ recordingConfig: RecordingConfig;
30
+ audioWorkletUrl: string;
31
+ emitAudioEventCallback: EmitAudioEventFunction;
32
+ emitAudioAnalysisCallback: EmitAudioAnalysisFunction;
33
+ });
34
+ init(): Promise<void>;
35
+ initFeatureExtractorWorker(featuresExtratorUrl?: string): void;
36
+ initFallbackWorker(): void;
37
+ handleWorkerError(error: ErrorEvent): void;
38
+ handleFeatureExtractorMessage(event: AudioFeaturesEvent): void;
39
+ start(): void;
40
+ stop(): Promise<Float32Array>;
41
+ pause(): void;
42
+ stopMediaStreamTracks(): void;
43
+ playRecordedData({ recordedData, }: {
44
+ recordedData: ArrayBuffer;
45
+ mimeType?: string;
46
+ }): Promise<void>;
47
+ private checkAudioContextFormat;
48
+ resume(): void;
49
+ }
50
+ export {};
51
+ //# sourceMappingURL=WebRecorder.web.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"WebRecorder.web.d.ts","sourceRoot":"","sources":["../src/WebRecorder.web.ts"],"names":[],"mappings":"AACA,OAAO,EAAE,aAAa,EAAE,MAAM,qCAAqC,CAAA;AACnE,OAAO,EAAE,eAAe,EAAE,MAAM,yBAAyB,CAAA;AACzD,OAAO,EACH,yBAAyB,EACzB,sBAAsB,EACzB,MAAM,uBAAuB,CAAA;AAc9B,UAAU,kBAAkB;IACxB,IAAI,EAAE;QACF,OAAO,EAAE,MAAM,CAAA;QACf,MAAM,EAAE,aAAa,CAAA;KACxB,CAAA;CACJ;AAWD,qBAAa,WAAW;IACpB,OAAO,CAAC,YAAY,CAAc;IAClC,OAAO,CAAC,gBAAgB,CAAmB;IAC3C,OAAO,CAAC,sBAAsB,CAAC,CAAQ;IACvC,OAAO,CAAC,MAAM,CAA4B;IAC1C,OAAO,CAAC,eAAe,CAAQ;IAC/B,OAAO,CAAC,sBAAsB,CAAwB;IACtD,OAAO,CAAC,yBAAyB,CAA2B;IAC5D,OAAO,CAAC,MAAM,CAAiB;IAC/B,OAAO,CAAC,QAAQ,CAAQ;IACxB,OAAO,CAAC,gBAAgB,CAAQ;IAChC,OAAO,CAAC,QAAQ,CAAQ;IACxB,OAAO,CAAC,cAAc,CAAQ;IAC9B,OAAO,CAAC,MAAM,CAAc;IAC5B,OAAO,CAAC,UAAU,CAAQ;IAC1B,OAAO,CAAC,iBAAiB,CAAe;gBAE5B,EACR,YAAY,EACZ,MAAM,EACN,eAAe,EACf,eAAe,EACf,sBAAsB,EACtB,yBAAyB,GAC5B,EAAE;QACC,YAAY,EAAE,YAAY,CAAA;QAC1B,MAAM,EAAE,0BAA0B,CAAA;QAClC,eAAe,EAAE,eAAe,CAAA;QAChC,eAAe,EAAE,MAAM,CAAA;QACvB,sBAAsB,EAAE,sBAAsB,CAAA;QAC9C,yBAAyB,EAAE,yBAAyB,CAAA;KACvD;IAkDK,IAAI;IA6FV,0BAA0B,CAAC,mBAAmB,CAAC,EAAE,MAAM;IA0BvD,kBAAkB;IAqBlB,iBAAiB,CAAC,KAAK,EAAE,UAAU;IAInC,6BAA6B,CAAC,KAAK,EAAE,kBAAkB;IAgCvD,KAAK;IAKL,IAAI,IAAI,OAAO,CAAC,YAAY,CAAC;IAsE7B,KAAK;IAML,qBAAqB;IAMf,gBAAgB,CAAC,EACnB,YAAY,GACf,EAAE;QACC,YAAY,EAAE,WAAW,CAAA;QACzB,QAAQ,CAAC,EAAE,MAAM,CAAA;KACpB;IAsBD,OAAO,CAAC,uBAAuB;IAoB/B,MAAM;CAKT"}
@@ -0,0 +1,298 @@
1
+ import { getLogger } from './logger';
2
+ import { encodingToBitDepth } from './utils/encodingToBitDepth';
3
+ import { InlineFeaturesExtractor } from './workers/InlineFeaturesExtractor.web';
4
+ import { InlineAudioWebWorker } from './workers/inlineAudioWebWorker.web';
5
+ const DEFAULT_WEB_BITDEPTH = 32;
6
+ const DEFAULT_WEB_POINTS_PER_SECOND = 10;
7
+ const DEFAULT_WEB_INTERVAL = 500;
8
+ const DEFAULT_WEB_NUMBER_OF_CHANNELS = 1;
9
+ const DEFAULT_ALGORITHM = 'rms';
10
+ const TAG = 'WebRecorder';
11
+ const logger = getLogger(TAG);
12
+ export class WebRecorder {
13
+ audioContext;
14
+ audioWorkletNode;
15
+ featureExtractorWorker;
16
+ source;
17
+ audioWorkletUrl;
18
+ emitAudioEventCallback;
19
+ emitAudioAnalysisCallback;
20
+ config;
21
+ position; // Track the cumulative position
22
+ numberOfChannels; // Number of audio channels
23
+ bitDepth; // Bit depth of the audio
24
+ exportBitDepth; // Bit depth of the audio
25
+ buffer; // Single buffer to store the audio data
26
+ bufferSize; // Keep track of the buffer size
27
+ audioAnalysisData; // Keep updating the full audio analysis data with latest events
28
+ constructor({ audioContext, source, recordingConfig, audioWorkletUrl, emitAudioEventCallback, emitAudioAnalysisCallback, }) {
29
+ this.audioContext = audioContext;
30
+ this.source = source;
31
+ this.audioWorkletUrl = audioWorkletUrl;
32
+ this.emitAudioEventCallback = emitAudioEventCallback;
33
+ this.emitAudioAnalysisCallback = emitAudioAnalysisCallback;
34
+ this.config = recordingConfig;
35
+ this.position = 0;
36
+ this.bufferSize = 0;
37
+ this.buffer = new Float32Array(0); // Initialize the buffer
38
+ const audioContextFormat = this.checkAudioContextFormat({
39
+ sampleRate: this.audioContext.sampleRate,
40
+ });
41
+ logger.debug('Initialized WebRecorder with config:', {
42
+ sampleRate: audioContextFormat.sampleRate,
43
+ bitDepth: audioContextFormat.bitDepth,
44
+ numberOfChannels: audioContextFormat.numberOfChannels,
45
+ });
46
+ this.bitDepth = audioContextFormat.bitDepth;
47
+ this.numberOfChannels =
48
+ audioContextFormat.numberOfChannels ||
49
+ DEFAULT_WEB_NUMBER_OF_CHANNELS; // Default to 1 if not available
50
+ this.exportBitDepth =
51
+ encodingToBitDepth({
52
+ encoding: recordingConfig.encoding ?? 'pcm_32bit',
53
+ }) ||
54
+ audioContextFormat.bitDepth ||
55
+ DEFAULT_WEB_BITDEPTH;
56
+ this.audioAnalysisData = {
57
+ amplitudeRange: { min: 0, max: 0 },
58
+ dataPoints: [],
59
+ durationMs: 0,
60
+ samples: 0,
61
+ amplitudeAlgorithm: recordingConfig.algorithm || DEFAULT_ALGORITHM,
62
+ bitDepth: this.bitDepth,
63
+ numberOfChannels: this.numberOfChannels,
64
+ sampleRate: this.config.sampleRate || this.audioContext.sampleRate,
65
+ pointsPerSecond: this.config.pointsPerSecond || DEFAULT_WEB_POINTS_PER_SECOND,
66
+ speakerChanges: [],
67
+ };
68
+ if (recordingConfig.enableProcessing) {
69
+ this.initFeatureExtractorWorker();
70
+ }
71
+ }
72
+ async init() {
73
+ try {
74
+ if (!this.audioWorkletUrl) {
75
+ const blob = new Blob([InlineAudioWebWorker], {
76
+ type: 'application/javascript',
77
+ });
78
+ const url = URL.createObjectURL(blob);
79
+ await this.audioContext.audioWorklet.addModule(url);
80
+ }
81
+ else {
82
+ await this.audioContext.audioWorklet.addModule(this.audioWorkletUrl);
83
+ }
84
+ this.audioWorkletNode = new AudioWorkletNode(this.audioContext, 'recorder-processor');
85
+ this.audioWorkletNode.port.onmessage = async (event) => {
86
+ const command = event.data.command;
87
+ if (command !== 'newData') {
88
+ return;
89
+ }
90
+ // Handle the audio blob (e.g., send it to the server or process it further)
91
+ logger.debug('Received audio blob from processor', event);
92
+ const pcmBufferFloat = event.data.recordedData;
93
+ if (!pcmBufferFloat) {
94
+ return;
95
+ }
96
+ // Concatenate the incoming Float32Array to the existing buffer
97
+ const newBuffer = new Float32Array(this.bufferSize + pcmBufferFloat.length);
98
+ newBuffer.set(this.buffer, 0);
99
+ newBuffer.set(pcmBufferFloat, this.bufferSize);
100
+ this.buffer = newBuffer;
101
+ this.bufferSize += pcmBufferFloat.length;
102
+ const sampleRate = event.data.sampleRate ?? this.audioContext.sampleRate;
103
+ const duration = pcmBufferFloat.length / sampleRate; // Calculate duration of the current buffer
104
+ this.emitAudioEventCallback({
105
+ data: pcmBufferFloat,
106
+ position: this.position,
107
+ });
108
+ this.position += duration; // Update position
109
+ this.featureExtractorWorker?.postMessage({
110
+ command: 'process',
111
+ channelData: pcmBufferFloat,
112
+ sampleRate,
113
+ pointsPerSecond: this.config.pointsPerSecond ||
114
+ DEFAULT_WEB_POINTS_PER_SECOND,
115
+ algorithm: this.config.algorithm || 'rms',
116
+ bitDepth: this.bitDepth,
117
+ fullAudioDurationMs: this.position * 1000,
118
+ numberOfChannels: this.numberOfChannels,
119
+ features: this.config.features,
120
+ }, []);
121
+ };
122
+ logger.debug(`WebRecorder initialized -- recordSampleRate=${this.audioContext.sampleRate}`, this.config);
123
+ this.audioWorkletNode.port.postMessage({
124
+ command: 'init',
125
+ recordSampleRate: this.audioContext.sampleRate, // Pass the original sample rate
126
+ exportSampleRate: this.config.sampleRate ?? this.audioContext.sampleRate,
127
+ bitDepth: this.bitDepth,
128
+ exportBitDepth: this.exportBitDepth,
129
+ channels: this.numberOfChannels,
130
+ interval: this.config.interval ?? DEFAULT_WEB_INTERVAL,
131
+ });
132
+ // Connect the source to the AudioWorkletNode and start recording
133
+ this.source.connect(this.audioWorkletNode);
134
+ this.audioWorkletNode.connect(this.audioContext.destination);
135
+ }
136
+ catch (error) {
137
+ console.error(`[${TAG}] Failed to initialize WebRecorder`, error);
138
+ }
139
+ }
140
+ initFeatureExtractorWorker(featuresExtratorUrl) {
141
+ try {
142
+ if (featuresExtratorUrl) {
143
+ // Initialize the feature extractor worker
144
+ //TODO: create audio feature extractor from a Blob instead of url since we cannot include the url directly in the library
145
+ // We keep the url during dev and use the blob in production.
146
+ this.featureExtractorWorker = new Worker(new URL(featuresExtratorUrl, window.location.href));
147
+ this.featureExtractorWorker.onmessage =
148
+ this.handleFeatureExtractorMessage.bind(this);
149
+ this.featureExtractorWorker.onerror =
150
+ this.handleWorkerError.bind(this);
151
+ }
152
+ else {
153
+ // Fallback to the inline worker if the URL is not provided
154
+ this.initFallbackWorker();
155
+ }
156
+ }
157
+ catch (error) {
158
+ console.error(`[${TAG}] Failed to initialize feature extractor worker`, error);
159
+ this.initFallbackWorker();
160
+ }
161
+ }
162
+ initFallbackWorker() {
163
+ try {
164
+ const blob = new Blob([InlineFeaturesExtractor], {
165
+ type: 'application/javascript',
166
+ });
167
+ const url = URL.createObjectURL(blob);
168
+ this.featureExtractorWorker = new Worker(url);
169
+ this.featureExtractorWorker.onmessage =
170
+ this.handleFeatureExtractorMessage.bind(this);
171
+ this.featureExtractorWorker.onerror = (error) => {
172
+ console.error(`[${TAG}] Default Inline worker failed`, error);
173
+ };
174
+ logger.log('Inline worker initialized successfully');
175
+ }
176
+ catch (error) {
177
+ console.error(`[${TAG}] Failed to initialize Inline Feature Extractor worker`, error);
178
+ }
179
+ }
180
+ handleWorkerError(error) {
181
+ console.error(`[${TAG}] Feature extractor worker error:`, error);
182
+ }
183
+ handleFeatureExtractorMessage(event) {
184
+ if (event.data.command === 'features') {
185
+ const segmentResult = event.data.result;
186
+ // Merge the segment result with the full audio analysis data
187
+ this.audioAnalysisData.dataPoints.push(...segmentResult.dataPoints);
188
+ this.audioAnalysisData.speakerChanges?.push(...(segmentResult.speakerChanges ?? []));
189
+ this.audioAnalysisData.durationMs = segmentResult.durationMs;
190
+ if (segmentResult.amplitudeRange) {
191
+ this.audioAnalysisData.amplitudeRange = {
192
+ min: Math.min(this.audioAnalysisData.amplitudeRange.min, segmentResult.amplitudeRange.min),
193
+ max: Math.max(this.audioAnalysisData.amplitudeRange.max, segmentResult.amplitudeRange.max),
194
+ };
195
+ }
196
+ // Handle the extracted features (e.g., emit an event or log them)
197
+ logger.debug('features event segmentResult', segmentResult);
198
+ logger.debug('features event audioAnalysisData', this.audioAnalysisData);
199
+ this.emitAudioAnalysisCallback(segmentResult);
200
+ }
201
+ }
202
+ start() {
203
+ this.source.connect(this.audioWorkletNode);
204
+ this.audioWorkletNode.connect(this.audioContext.destination);
205
+ }
206
+ stop() {
207
+ return new Promise((resolve, reject) => {
208
+ try {
209
+ if (this.audioWorkletNode) {
210
+ // this.source.disconnect(this.audioWorkletNode);
211
+ // this.audioWorkletNode.disconnect(this.audioContext.destination);
212
+ this.audioWorkletNode.port.postMessage({ command: 'stop' });
213
+ // Set a timeout to reject the promise if no message is received within 5 seconds
214
+ const timeout = setTimeout(() => {
215
+ this.audioWorkletNode.port.removeEventListener('message', onMessage);
216
+ reject(new Error("Timeout error, audioWorkletNode didn't complete."));
217
+ }, 5000);
218
+ // Listen for the recordedData message to confirm stopping
219
+ const onMessage = async (event) => {
220
+ const command = event.data.command;
221
+ if (command === 'recordedData') {
222
+ clearTimeout(timeout); // Clear the timeout
223
+ const rawPCMDataFull = event.data.recordedData?.slice(0);
224
+ if (!rawPCMDataFull) {
225
+ reject(new Error('Failed to get recorded data'));
226
+ return;
227
+ }
228
+ // Compute duration of the recorded data
229
+ const duration = rawPCMDataFull.byteLength /
230
+ (this.audioContext.sampleRate *
231
+ (this.exportBitDepth /
232
+ this.numberOfChannels));
233
+ logger.debug(`Received recorded data -- Duration: ${duration} vs ${rawPCMDataFull.byteLength / this.audioContext.sampleRate} seconds`);
234
+ logger.debug(`recordedData.length=${rawPCMDataFull.byteLength} vs transmittedData.length=${this.bufferSize}`);
235
+ // Remove the event listener after receiving the final data
236
+ this.audioWorkletNode.port.removeEventListener('message', onMessage);
237
+ resolve(this.buffer); // Resolve the promise with the collected buffers
238
+ }
239
+ };
240
+ this.audioWorkletNode.port.addEventListener('message', onMessage);
241
+ }
242
+ // Stop all media stream tracks to stop the browser recording
243
+ this.stopMediaStreamTracks();
244
+ }
245
+ catch (error) {
246
+ reject(error);
247
+ }
248
+ });
249
+ }
250
+ pause() {
251
+ this.source.disconnect(this.audioWorkletNode); // Disconnect the source from the AudioWorkletNode
252
+ this.audioWorkletNode.disconnect(this.audioContext.destination); // Disconnect the AudioWorkletNode from the destination
253
+ this.audioWorkletNode.port.postMessage({ command: 'pause' });
254
+ }
255
+ stopMediaStreamTracks() {
256
+ // Stop all audio tracks to stop the recording icon
257
+ const tracks = this.source.mediaStream.getTracks();
258
+ tracks.forEach((track) => track.stop());
259
+ }
260
+ async playRecordedData({ recordedData, }) {
261
+ try {
262
+ const blob = new Blob([recordedData]);
263
+ const url = URL.createObjectURL(blob);
264
+ const response = await fetch(url);
265
+ const arrayBuffer = await response.arrayBuffer();
266
+ // Decode the audio data
267
+ const audioBuffer = await this.audioContext.decodeAudioData(arrayBuffer);
268
+ // Create a buffer source node and play the audio
269
+ const bufferSource = this.audioContext.createBufferSource();
270
+ bufferSource.buffer = audioBuffer;
271
+ bufferSource.connect(this.audioContext.destination);
272
+ bufferSource.start();
273
+ logger.debug('Playing recorded data', recordedData);
274
+ }
275
+ catch (error) {
276
+ console.error(`[${TAG}] Failed to play recorded data:`, error);
277
+ }
278
+ }
279
+ checkAudioContextFormat({ sampleRate }) {
280
+ // Create a silent AudioBuffer
281
+ const frameCount = sampleRate * 1.0; // 1 second buffer
282
+ const audioBuffer = this.audioContext.createBuffer(1, frameCount, sampleRate);
283
+ // Check the format
284
+ const channelData = audioBuffer.getChannelData(0);
285
+ const bitDepth = channelData.BYTES_PER_ELEMENT * 8; // 4 bytes per element means 32-bit
286
+ return {
287
+ sampleRate: audioBuffer.sampleRate,
288
+ bitDepth,
289
+ numberOfChannels: audioBuffer.numberOfChannels,
290
+ };
291
+ }
292
+ resume() {
293
+ this.source.connect(this.audioWorkletNode);
294
+ this.audioWorkletNode.connect(this.audioContext.destination);
295
+ this.audioWorkletNode.port.postMessage({ command: 'resume' });
296
+ }
297
+ }
298
+ //# sourceMappingURL=WebRecorder.web.js.map