@siteed/expo-audio-stream 1.0.1 → 1.0.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 (142) hide show
  1. package/.size-limit.json +6 -0
  2. package/README.md +6 -6
  3. package/android/build.gradle +5 -0
  4. package/android/src/main/java/net/siteed/audiostream/AudioAnalysisData.kt +120 -0
  5. package/android/src/main/java/net/siteed/audiostream/AudioFileHandler.kt +34 -4
  6. package/android/src/main/java/net/siteed/audiostream/AudioProcessor.kt +635 -0
  7. package/android/src/main/java/net/siteed/audiostream/AudioRecorderManager.kt +194 -79
  8. package/android/src/main/java/net/siteed/audiostream/Constants.kt +1 -0
  9. package/android/src/main/java/net/siteed/audiostream/ExpoAudioStreamModule.kt +48 -2
  10. package/android/src/main/java/net/siteed/audiostream/FFT.kt +44 -0
  11. package/android/src/main/java/net/siteed/audiostream/Features.kt +56 -0
  12. package/android/src/main/java/net/siteed/audiostream/RecordingConfig.kt +12 -0
  13. package/android/src/main/test/java/net/siteed/audiostream/AudioProcessorTest.kt +56 -0
  14. package/app.plugin.js +1 -1
  15. package/build/AudioAnalysis/AudioAnalysis.types.d.ts +76 -0
  16. package/build/AudioAnalysis/AudioAnalysis.types.d.ts.map +1 -0
  17. package/build/AudioAnalysis/AudioAnalysis.types.js +3 -0
  18. package/build/AudioAnalysis/AudioAnalysis.types.js.map +1 -0
  19. package/build/AudioAnalysis/extractAudioAnalysis.d.ts +4 -0
  20. package/build/AudioAnalysis/extractAudioAnalysis.d.ts.map +1 -0
  21. package/build/AudioAnalysis/extractAudioAnalysis.js +101 -0
  22. package/build/AudioAnalysis/extractAudioAnalysis.js.map +1 -0
  23. package/build/AudioAnalysis/extractWaveform.d.ts +8 -0
  24. package/build/AudioAnalysis/extractWaveform.d.ts.map +1 -0
  25. package/build/AudioAnalysis/extractWaveform.js +14 -0
  26. package/build/AudioAnalysis/extractWaveform.js.map +1 -0
  27. package/build/AudioRecorder.provider.d.ts +14 -1
  28. package/build/AudioRecorder.provider.d.ts.map +1 -1
  29. package/build/AudioRecorder.provider.js +18 -5
  30. package/build/AudioRecorder.provider.js.map +1 -1
  31. package/build/ExpoAudioStream.native.d.ts +3 -0
  32. package/build/ExpoAudioStream.native.d.ts.map +1 -0
  33. package/build/ExpoAudioStream.native.js +6 -0
  34. package/build/ExpoAudioStream.native.js.map +1 -0
  35. package/build/ExpoAudioStream.types.d.ts +35 -20
  36. package/build/ExpoAudioStream.types.d.ts.map +1 -1
  37. package/build/ExpoAudioStream.types.js.map +1 -1
  38. package/build/ExpoAudioStream.web.d.ts +42 -0
  39. package/build/ExpoAudioStream.web.d.ts.map +1 -0
  40. package/build/ExpoAudioStream.web.js +185 -0
  41. package/build/ExpoAudioStream.web.js.map +1 -0
  42. package/build/ExpoAudioStreamModule.d.ts +2 -2
  43. package/build/ExpoAudioStreamModule.d.ts.map +1 -1
  44. package/build/ExpoAudioStreamModule.js +16 -3
  45. package/build/ExpoAudioStreamModule.js.map +1 -1
  46. package/build/WebRecorder.web.d.ts +51 -0
  47. package/build/WebRecorder.web.d.ts.map +1 -0
  48. package/build/WebRecorder.web.js +288 -0
  49. package/build/WebRecorder.web.js.map +1 -0
  50. package/build/constants.d.ts +11 -0
  51. package/build/constants.d.ts.map +1 -0
  52. package/build/constants.js +14 -0
  53. package/build/constants.js.map +1 -0
  54. package/build/events.d.ts +6 -0
  55. package/build/events.d.ts.map +1 -0
  56. package/build/events.js +15 -0
  57. package/build/events.js.map +1 -0
  58. package/build/index.d.ts +8 -7
  59. package/build/index.d.ts.map +1 -1
  60. package/build/index.js +7 -14
  61. package/build/index.js.map +1 -1
  62. package/build/logger.d.ts +9 -0
  63. package/build/logger.d.ts.map +1 -0
  64. package/build/logger.js +17 -0
  65. package/build/logger.js.map +1 -0
  66. package/build/useAudioRecorder.d.ts +37 -0
  67. package/build/useAudioRecorder.d.ts.map +1 -0
  68. package/build/useAudioRecorder.js +271 -0
  69. package/build/useAudioRecorder.js.map +1 -0
  70. package/build/utils/convertPCMToFloat32.d.ts +11 -0
  71. package/build/utils/convertPCMToFloat32.d.ts.map +1 -0
  72. package/build/utils/convertPCMToFloat32.js +41 -0
  73. package/build/utils/convertPCMToFloat32.js.map +1 -0
  74. package/build/utils/encodingToBitDepth.d.ts +5 -0
  75. package/build/utils/encodingToBitDepth.d.ts.map +1 -0
  76. package/build/utils/encodingToBitDepth.js +13 -0
  77. package/build/utils/encodingToBitDepth.js.map +1 -0
  78. package/build/utils/getWavFileInfo.d.ts +25 -0
  79. package/build/utils/getWavFileInfo.d.ts.map +1 -0
  80. package/build/utils/getWavFileInfo.js +89 -0
  81. package/build/utils/getWavFileInfo.js.map +1 -0
  82. package/build/utils/writeWavHeader.d.ts +9 -0
  83. package/build/utils/writeWavHeader.d.ts.map +1 -0
  84. package/build/utils/writeWavHeader.js +41 -0
  85. package/build/utils/writeWavHeader.js.map +1 -0
  86. package/build/workers/InlineFeaturesExtractor.web.d.ts +2 -0
  87. package/build/workers/InlineFeaturesExtractor.web.d.ts.map +1 -0
  88. package/build/workers/InlineFeaturesExtractor.web.js +303 -0
  89. package/build/workers/InlineFeaturesExtractor.web.js.map +1 -0
  90. package/build/workers/inlineAudioWebWorker.web.d.ts +2 -0
  91. package/build/workers/inlineAudioWebWorker.web.d.ts.map +1 -0
  92. package/build/workers/inlineAudioWebWorker.web.js +243 -0
  93. package/build/workers/inlineAudioWebWorker.web.js.map +1 -0
  94. package/expo-module.config.json +13 -4
  95. package/ios/AudioAnalysisData.swift +39 -0
  96. package/ios/AudioProcessingHelpers.swift +59 -0
  97. package/ios/AudioProcessor.swift +317 -0
  98. package/ios/AudioStreamError.swift +7 -0
  99. package/ios/AudioStreamManager.swift +243 -54
  100. package/ios/AudioStreamManagerDelegate.swift +4 -0
  101. package/ios/DataPoint.swift +41 -0
  102. package/ios/ExpoAudioStreamModule.swift +198 -6
  103. package/ios/Features.swift +44 -0
  104. package/ios/RecordingResult.swift +19 -0
  105. package/ios/RecordingSettings.swift +13 -0
  106. package/ios/WaveformExtractor.swift +105 -0
  107. package/package.json +13 -12
  108. package/plugin/tsconfig.json +13 -8
  109. package/publish.sh +8 -0
  110. package/src/AudioAnalysis/AudioAnalysis.types.ts +85 -0
  111. package/src/AudioAnalysis/extractAudioAnalysis.ts +136 -0
  112. package/src/AudioAnalysis/extractWaveform.ts +25 -0
  113. package/src/AudioRecorder.provider.tsx +36 -8
  114. package/src/ExpoAudioStream.native.ts +6 -0
  115. package/src/ExpoAudioStream.types.ts +50 -25
  116. package/src/ExpoAudioStream.web.ts +229 -0
  117. package/src/ExpoAudioStreamModule.ts +22 -3
  118. package/src/WebRecorder.web.ts +416 -0
  119. package/src/constants.ts +18 -0
  120. package/src/events.ts +25 -0
  121. package/src/index.ts +14 -29
  122. package/src/logger.ts +26 -0
  123. package/src/useAudioRecorder.tsx +415 -0
  124. package/src/utils/convertPCMToFloat32.ts +48 -0
  125. package/src/utils/encodingToBitDepth.ts +18 -0
  126. package/src/utils/getWavFileInfo.ts +125 -0
  127. package/src/utils/writeWavHeader.ts +56 -0
  128. package/src/workers/InlineFeaturesExtractor.web.tsx +302 -0
  129. package/src/workers/inlineAudioWebWorker.web.tsx +242 -0
  130. package/build/ExpoAudioStreamModule.web.d.ts +0 -37
  131. package/build/ExpoAudioStreamModule.web.d.ts.map +0 -1
  132. package/build/ExpoAudioStreamModule.web.js +0 -156
  133. package/build/ExpoAudioStreamModule.web.js.map +0 -1
  134. package/build/useAudioRecording.d.ts +0 -23
  135. package/build/useAudioRecording.d.ts.map +0 -1
  136. package/build/useAudioRecording.js +0 -189
  137. package/build/useAudioRecording.js.map +0 -1
  138. package/docs/demo.gif +0 -0
  139. package/release-it.js +0 -18
  140. package/src/ExpoAudioStreamModule.web.ts +0 -181
  141. package/src/useAudioRecording.ts +0 -268
  142. package/yarn-error.log +0 -7793
@@ -0,0 +1,416 @@
1
+ // src/WebRecorder.ts
2
+ import { AudioAnalysisData } from "./AudioAnalysis/AudioAnalysis.types";
3
+ import { RecordingConfig } from "./ExpoAudioStream.types";
4
+ import {
5
+ EmitAudioAnalysisFunction,
6
+ EmitAudioEventFunction,
7
+ } from "./ExpoAudioStream.web";
8
+ import { getLogger } from "./logger";
9
+ import { encodingToBitDepth } from "./utils/encodingToBitDepth";
10
+ import { InlineFeaturesExtractor } from "./workers/InlineFeaturesExtractor.web";
11
+ import { InlineAudioWebWorker } from "./workers/inlineAudioWebWorker.web";
12
+
13
+ interface AudioWorkletEvent {
14
+ data: {
15
+ command: string;
16
+ recordedData?: ArrayBuffer;
17
+ sampleRate?: number;
18
+ };
19
+ }
20
+
21
+ interface AudioFeaturesEvent {
22
+ data: {
23
+ command: string;
24
+ result: AudioAnalysisData;
25
+ };
26
+ }
27
+
28
+ const DEFAULT_WEB_BITDEPTH = 32;
29
+ const DEFAULT_WEB_POINTS_PER_SECOND = 10;
30
+ const DEFAULT_WEB_INTERVAL = 500;
31
+ const DEFAULT_WEB_NUMBER_OF_CHANNELS = 1;
32
+
33
+ const TAG = "WebRecorder";
34
+ const logger = getLogger(TAG);
35
+
36
+ export class WebRecorder {
37
+ private audioContext: AudioContext;
38
+ private audioWorkletNode!: AudioWorkletNode;
39
+ private featureExtractorWorker?: Worker;
40
+ private source: MediaStreamAudioSourceNode;
41
+ private audioWorkletUrl: string;
42
+ private emitAudioEventCallback: EmitAudioEventFunction;
43
+ private emitAudioAnalysisCallback: EmitAudioAnalysisFunction;
44
+ private config: RecordingConfig;
45
+ private position: number; // Track the cumulative position
46
+ private numberOfChannels: number; // Number of audio channels
47
+ private bitDepth: number; // Bit depth of the audio
48
+ private exportBitDepth: number; // Bit depth of the audio
49
+ private buffers: ArrayBuffer[]; // Array to store the buffers
50
+ private audioAnalysisData: AudioAnalysisData; // Keep updating the full audio analysis data with latest events
51
+
52
+ constructor({
53
+ audioContext,
54
+ source,
55
+ recordingConfig,
56
+ featuresExtratorUrl,
57
+ audioWorkletUrl,
58
+ emitAudioEventCallback,
59
+ emitAudioAnalysisCallback,
60
+ }: {
61
+ audioContext: AudioContext;
62
+ source: MediaStreamAudioSourceNode;
63
+ recordingConfig: RecordingConfig;
64
+ featuresExtratorUrl: string;
65
+ audioWorkletUrl: string;
66
+ emitAudioEventCallback: EmitAudioEventFunction;
67
+ emitAudioAnalysisCallback: EmitAudioAnalysisFunction;
68
+ }) {
69
+ this.audioContext = audioContext;
70
+ this.source = source;
71
+ this.audioWorkletUrl = audioWorkletUrl;
72
+ this.emitAudioEventCallback = emitAudioEventCallback;
73
+ this.emitAudioAnalysisCallback = emitAudioAnalysisCallback;
74
+ this.config = recordingConfig;
75
+ this.position = 0;
76
+ this.buffers = []; // Initialize the buffers array
77
+
78
+ const audioContextFormat = this.checkAudioContextFormat({
79
+ sampleRate: this.audioContext.sampleRate,
80
+ });
81
+ logger.debug("Initialized WebRecorder with config:", {
82
+ sampleRate: audioContextFormat.sampleRate,
83
+ bitDepth: audioContextFormat.bitDepth,
84
+ numberOfChannels: audioContextFormat.numberOfChannels,
85
+ });
86
+
87
+ this.bitDepth = audioContextFormat.bitDepth;
88
+ this.numberOfChannels =
89
+ audioContextFormat.numberOfChannels || DEFAULT_WEB_NUMBER_OF_CHANNELS; // Default to 1 if not available
90
+ this.exportBitDepth =
91
+ encodingToBitDepth({
92
+ encoding: recordingConfig.encoding ?? "pcm_32bit",
93
+ }) ||
94
+ audioContextFormat.bitDepth ||
95
+ DEFAULT_WEB_BITDEPTH;
96
+
97
+ this.audioAnalysisData = {
98
+ amplitudeRange: { min: 0, max: 0 },
99
+ dataPoints: [],
100
+ durationMs: 0,
101
+ samples: 0,
102
+ bitDepth: this.bitDepth,
103
+ numberOfChannels: this.numberOfChannels,
104
+ sampleRate: this.config.sampleRate || this.audioContext.sampleRate,
105
+ pointsPerSecond:
106
+ this.config.pointsPerSecond || DEFAULT_WEB_POINTS_PER_SECOND,
107
+ speakerChanges: [],
108
+ };
109
+
110
+ if (recordingConfig.enableProcessing) {
111
+ this.initFeatureExtractorWorker();
112
+ }
113
+ }
114
+
115
+ async init() {
116
+ try {
117
+ if (!this.audioWorkletUrl) {
118
+ const blob = new Blob([InlineAudioWebWorker], {
119
+ type: "application/javascript",
120
+ });
121
+ const url = URL.createObjectURL(blob);
122
+ await this.audioContext.audioWorklet.addModule(url);
123
+ } else {
124
+ await this.audioContext.audioWorklet.addModule(this.audioWorkletUrl);
125
+ }
126
+ this.audioWorkletNode = new AudioWorkletNode(
127
+ this.audioContext,
128
+ "recorder-processor",
129
+ );
130
+
131
+ this.audioWorkletNode.port.onmessage = async (
132
+ event: AudioWorkletEvent,
133
+ ) => {
134
+ const command = event.data.command;
135
+ if (command !== "newData") {
136
+ return;
137
+ }
138
+ // Handle the audio blob (e.g., send it to the server or process it further)
139
+ logger.debug("Received audio blob from processor", event);
140
+ const pcmBuffer = event.data.recordedData;
141
+
142
+ if (!pcmBuffer) {
143
+ return;
144
+ }
145
+
146
+ this.buffers.push(pcmBuffer); // Store the buffer
147
+ const sampleRate =
148
+ event.data.sampleRate ?? this.audioContext.sampleRate;
149
+ const otherSampleRate = this.audioContext.sampleRate;
150
+
151
+ // Pass the intermediary buffer to the feature extractor worker
152
+ const pcmBufferCopy = pcmBuffer.slice(0);
153
+ const channelData = new Float32Array(pcmBufferCopy);
154
+
155
+ const duration = channelData.length / sampleRate; // Calculate duration of the current buffer
156
+ const otherDuration =
157
+ pcmBuffer.byteLength /
158
+ (otherSampleRate * (this.exportBitDepth / this.numberOfChannels)); // Calculate duration of the current buffer
159
+ logger.debug(
160
+ `sampleRate=${sampleRate} Duration: ${duration} -- otherSampleRate=${otherSampleRate} Other duration: ${otherDuration}`,
161
+ );
162
+
163
+ this.emitAudioEventCallback({
164
+ data: pcmBuffer,
165
+ position: this.position,
166
+ });
167
+ this.position += duration; // Update position
168
+
169
+ this.featureExtractorWorker?.postMessage(
170
+ {
171
+ command: "process",
172
+ channelData,
173
+ sampleRate: this.audioContext.sampleRate,
174
+ pointsPerSecond:
175
+ this.config.pointsPerSecond || DEFAULT_WEB_POINTS_PER_SECOND,
176
+ algorithm: this.config.algorithm || "rms",
177
+ bitDepth: this.bitDepth,
178
+ fullAudioDurationMs: this.position * 1000,
179
+ numberOfChannels: this.numberOfChannels,
180
+ features: this.config.features,
181
+ },
182
+ [],
183
+ );
184
+ };
185
+
186
+ logger.debug(
187
+ `WebRecorder initialized -- recordSampleRate=${this.audioContext.sampleRate}`,
188
+ this.config,
189
+ );
190
+ this.audioWorkletNode.port.postMessage({
191
+ command: "init",
192
+ recordSampleRate: this.audioContext.sampleRate, // Pass the original sample rate
193
+ exportSampleRate:
194
+ this.config.sampleRate ?? this.audioContext.sampleRate,
195
+ bitDepth: this.bitDepth,
196
+ exportBitDepth: this.exportBitDepth,
197
+ channels: this.numberOfChannels,
198
+ interval: this.config.interval ?? DEFAULT_WEB_INTERVAL,
199
+ });
200
+
201
+ // Connect the source to the AudioWorkletNode and start recording
202
+ this.source.connect(this.audioWorkletNode);
203
+ this.audioWorkletNode.connect(this.audioContext.destination);
204
+ } catch (error) {
205
+ console.error(`[${TAG}] Failed to initialize WebRecorder`, error);
206
+ }
207
+ }
208
+
209
+ initFeatureExtractorWorker(featuresExtratorUrl?: string) {
210
+ try {
211
+ if (featuresExtratorUrl) {
212
+ // Initialize the feature extractor worker
213
+ //TODO: create audio feature extractor from a Blob instead of url since we cannot include the url directly in the library
214
+ // We keep the url during dev and use the blob in production.
215
+ this.featureExtractorWorker = new Worker(
216
+ new URL(featuresExtratorUrl, window.location.href),
217
+ );
218
+ this.featureExtractorWorker.onmessage =
219
+ this.handleFeatureExtractorMessage.bind(this);
220
+ this.featureExtractorWorker.onerror = this.handleWorkerError.bind(this);
221
+ } else {
222
+ // Fallback to the inline worker if the URL is not provided
223
+ this.initFallbackWorker();
224
+ }
225
+ } catch (error) {
226
+ console.error(
227
+ `[${TAG}] Failed to initialize feature extractor worker`,
228
+ error,
229
+ );
230
+ this.initFallbackWorker();
231
+ }
232
+ }
233
+
234
+ initFallbackWorker() {
235
+ try {
236
+ const blob = new Blob([InlineFeaturesExtractor], {
237
+ type: "application/javascript",
238
+ });
239
+ const url = URL.createObjectURL(blob);
240
+ this.featureExtractorWorker = new Worker(url);
241
+ this.featureExtractorWorker.onmessage =
242
+ this.handleFeatureExtractorMessage.bind(this);
243
+ this.featureExtractorWorker.onerror = (error) => {
244
+ console.error(`[${TAG}] Default Inline worker failed`, error);
245
+ };
246
+ logger.log("Inline worker initialized successfully");
247
+ } catch (error) {
248
+ console.error(
249
+ `[${TAG}] Failed to initialize Inline Feature Extractor worker`,
250
+ error,
251
+ );
252
+ }
253
+ }
254
+
255
+ handleWorkerError(error: ErrorEvent) {
256
+ console.error(`[${TAG}] Feature extractor worker error:`, error);
257
+ }
258
+
259
+ handleFeatureExtractorMessage(event: AudioFeaturesEvent) {
260
+ if (event.data.command === "features") {
261
+ const segmentResult = event.data.result;
262
+
263
+ // Merge the segment result with the full audio analysis data
264
+ this.audioAnalysisData.dataPoints.push(...segmentResult.dataPoints);
265
+ this.audioAnalysisData.speakerChanges?.push(
266
+ ...(segmentResult.speakerChanges ?? []),
267
+ );
268
+ this.audioAnalysisData.durationMs = segmentResult.durationMs;
269
+ if (segmentResult.amplitudeRange) {
270
+ this.audioAnalysisData.amplitudeRange = {
271
+ min: Math.min(
272
+ this.audioAnalysisData.amplitudeRange.min,
273
+ segmentResult.amplitudeRange.min,
274
+ ),
275
+ max: Math.max(
276
+ this.audioAnalysisData.amplitudeRange.max,
277
+ segmentResult.amplitudeRange.max,
278
+ ),
279
+ };
280
+ }
281
+ // Handle the extracted features (e.g., emit an event or log them)
282
+ logger.debug("features event segmentResult", segmentResult);
283
+ logger.debug("features event audioAnalysisData", this.audioAnalysisData);
284
+ this.emitAudioAnalysisCallback(segmentResult);
285
+ }
286
+ }
287
+
288
+ start() {
289
+ this.source.connect(this.audioWorkletNode);
290
+ this.audioWorkletNode.connect(this.audioContext.destination);
291
+ }
292
+
293
+ stop() {
294
+ return new Promise((resolve, reject) => {
295
+ try {
296
+ if (this.audioWorkletNode) {
297
+ // this.source.disconnect(this.audioWorkletNode);
298
+ // this.audioWorkletNode.disconnect(this.audioContext.destination);
299
+ this.audioWorkletNode.port.postMessage({ command: "stop" });
300
+
301
+ // Set a timeout to reject the promise if no message is received within 5 seconds
302
+ const timeout = setTimeout(() => {
303
+ this.audioWorkletNode.port.removeEventListener(
304
+ "message",
305
+ onMessage,
306
+ );
307
+ reject(
308
+ new Error("Timeout error, audioWorkletNode didn't complete."),
309
+ );
310
+ }, 5000);
311
+
312
+ // Listen for the recordedData message to confirm stopping
313
+ const onMessage = async (event: AudioWorkletEvent) => {
314
+ const command = event.data.command;
315
+ if (command === "recordedData") {
316
+ clearTimeout(timeout); // Clear the timeout
317
+
318
+ const rawPCMDataFull = event.data.recordedData?.slice(
319
+ 0,
320
+ ) as ArrayBuffer;
321
+
322
+ // Compute duration of the recorded data
323
+ const duration =
324
+ rawPCMDataFull.byteLength /
325
+ (this.audioContext.sampleRate *
326
+ (this.exportBitDepth / this.numberOfChannels));
327
+ logger.debug(
328
+ `Received recorded data -- Duration: ${duration} vs ${rawPCMDataFull.byteLength / this.audioContext.sampleRate} seconds`,
329
+ );
330
+ logger.debug(
331
+ `recordedData.length=${rawPCMDataFull.byteLength} vs transmittedData.length=${this.buffers[0].byteLength}`,
332
+ );
333
+
334
+ // Remove the event listener after receiving the final data
335
+ this.audioWorkletNode.port.removeEventListener(
336
+ "message",
337
+ onMessage,
338
+ );
339
+ resolve(this.buffers); // Resolve the promise with the collected buffers
340
+ }
341
+ };
342
+ this.audioWorkletNode.port.addEventListener("message", onMessage);
343
+ }
344
+
345
+ // Stop all media stream tracks to stop the browser recording
346
+ this.stopMediaStreamTracks();
347
+ } catch (error) {
348
+ reject(error);
349
+ }
350
+ });
351
+ }
352
+
353
+ pause() {
354
+ this.source.disconnect(this.audioWorkletNode); // Disconnect the source from the AudioWorkletNode
355
+ this.audioWorkletNode.disconnect(this.audioContext.destination); // Disconnect the AudioWorkletNode from the destination
356
+ this.audioWorkletNode.port.postMessage({ command: "pause" });
357
+ }
358
+
359
+ stopMediaStreamTracks() {
360
+ // Stop all audio tracks to stop the recording icon
361
+ const tracks = this.source.mediaStream.getTracks();
362
+ tracks.forEach((track) => track.stop());
363
+ }
364
+
365
+ async playRecordedData({
366
+ recordedData,
367
+ }: {
368
+ recordedData: ArrayBuffer;
369
+ mimeType?: string;
370
+ }) {
371
+ try {
372
+ const blob = new Blob([recordedData]);
373
+ const url = URL.createObjectURL(blob);
374
+ const response = await fetch(url);
375
+ const arrayBuffer = await response.arrayBuffer();
376
+
377
+ // Decode the audio data
378
+ const audioBuffer = await this.audioContext.decodeAudioData(arrayBuffer);
379
+
380
+ // Create a buffer source node and play the audio
381
+ const bufferSource = this.audioContext.createBufferSource();
382
+ bufferSource.buffer = audioBuffer;
383
+ bufferSource.connect(this.audioContext.destination);
384
+ bufferSource.start();
385
+ logger.debug("Playing recorded data", recordedData);
386
+ } catch (error) {
387
+ console.error(`[${TAG}] Failed to play recorded data:`, error);
388
+ }
389
+ }
390
+
391
+ private checkAudioContextFormat({ sampleRate }: { sampleRate: number }) {
392
+ // Create a silent AudioBuffer
393
+ const frameCount = sampleRate * 1.0; // 1 second buffer
394
+ const audioBuffer = this.audioContext.createBuffer(
395
+ 1,
396
+ frameCount,
397
+ sampleRate,
398
+ );
399
+
400
+ // Check the format
401
+ const channelData = audioBuffer.getChannelData(0);
402
+ const bitDepth = channelData.BYTES_PER_ELEMENT * 8; // 4 bytes per element means 32-bit
403
+
404
+ return {
405
+ sampleRate: audioBuffer.sampleRate,
406
+ bitDepth,
407
+ numberOfChannels: audioBuffer.numberOfChannels,
408
+ };
409
+ }
410
+
411
+ resume() {
412
+ this.source.connect(this.audioWorkletNode);
413
+ this.audioWorkletNode.connect(this.audioContext.destination);
414
+ this.audioWorkletNode.port.postMessage({ command: "resume" });
415
+ }
416
+ }
@@ -0,0 +1,18 @@
1
+ // packages/expo-audio-stream/src/constants.ts
2
+ import { Platform } from "react-native";
3
+
4
+ import { BitDepth, SampleRate } from "./ExpoAudioStream.types";
5
+
6
+ export const isWeb = Platform.OS === "web";
7
+ export const DEBUG_NAMESPACE = "expo-audio-stream";
8
+
9
+ // Constants for identifying chunks in a WAV file
10
+ export const RIFF_HEADER = 0x52494646; // "RIFF"
11
+ export const WAVE_HEADER = 0x57415645; // "WAVE"
12
+ export const FMT_CHUNK_ID = 0x666d7420; // "fmt "
13
+ export const DATA_CHUNK_ID = 0x64617461; // "data"
14
+ export const INFO_CHUNK_ID = 0x494e464f; // "INFO"
15
+
16
+ // Default values
17
+ export const DEFAULT_SAMPLE_RATE: SampleRate = 16000;
18
+ export const DEFAULT_BIT_DEPTH: BitDepth = 32;
package/src/events.ts ADDED
@@ -0,0 +1,25 @@
1
+ // packages/expo-audio-stream/src/events.ts
2
+
3
+ import { EventEmitter, type Subscription } from "expo-modules-core";
4
+
5
+ import { AudioAnalysisData } from "./AudioAnalysis/AudioAnalysis.types";
6
+ import { AudioEventPayload } from "./ExpoAudioStream.types";
7
+ import ExpoAudioStreamModule from "./ExpoAudioStreamModule";
8
+ import { getLogger } from "./logger";
9
+
10
+ const emitter = new EventEmitter(ExpoAudioStreamModule);
11
+ const logger = getLogger("events");
12
+
13
+ export function addAudioEventListener(
14
+ listener: (event: AudioEventPayload) => Promise<void>,
15
+ ): Subscription {
16
+ logger.log("Adding listener for AudioData event");
17
+ return emitter.addListener<AudioEventPayload>("AudioData", listener);
18
+ }
19
+
20
+ export function addAudioAnalysisListener(
21
+ listener: (event: AudioAnalysisData) => Promise<void>,
22
+ ): Subscription {
23
+ logger.log("Adding listener for AudioAnalysis event");
24
+ return emitter.addListener<AudioAnalysisData>("AudioAnalysis", listener);
25
+ }
package/src/index.ts CHANGED
@@ -1,37 +1,22 @@
1
- import {
2
- EventEmitter,
3
- NativeModulesProxy,
4
- type Subscription,
5
- } from "expo-modules-core";
1
+ // src/index.ts
6
2
 
7
- // Import the native module. On web, it will be resolved to ExpoAudioStream.web.ts
8
- // and on native platforms to ExpoAudioStream.ts
3
+ import { extractAudioAnalysis } from "./AudioAnalysis/extractAudioAnalysis";
9
4
  import {
10
5
  AudioRecorderProvider,
11
6
  useSharedAudioRecorder,
12
7
  } from "./AudioRecorder.provider";
13
- import { AudioEventPayload } from "./ExpoAudioStream.types";
14
- import ExpoAudioStreamModule from "./ExpoAudioStreamModule";
15
- import {
16
- AudioDataEvent,
17
- UseAudioRecorderState,
18
- useAudioRecorder,
19
- } from "./useAudioRecording";
20
-
21
- const emitter = new EventEmitter(
22
- ExpoAudioStreamModule ?? NativeModulesProxy.ExpoAudioStream,
23
- );
8
+ import { useAudioRecorder } from "./useAudioRecorder";
24
9
 
25
- export function test(): void {
26
- return ExpoAudioStreamModule.test();
27
- }
10
+ export * from "./utils/getWavFileInfo";
11
+ export * from "./utils/convertPCMToFloat32";
12
+ export * from "./utils/writeWavHeader";
28
13
 
29
- export function addAudioEventListener(
30
- listener: (event: AudioEventPayload) => Promise<void>,
31
- ): Subscription {
32
- console.log(`addAudioEventListener`, listener);
33
- return emitter.addListener<AudioEventPayload>("AudioData", listener);
34
- }
14
+ export {
15
+ AudioRecorderProvider,
16
+ extractAudioAnalysis,
17
+ useAudioRecorder,
18
+ useSharedAudioRecorder,
19
+ };
35
20
 
36
- export { AudioRecorderProvider, useAudioRecorder, useSharedAudioRecorder };
37
- export type { AudioDataEvent, AudioEventPayload, UseAudioRecorderState };
21
+ export type * from "./AudioAnalysis/AudioAnalysis.types";
22
+ export type * from "./ExpoAudioStream.types";
package/src/logger.ts ADDED
@@ -0,0 +1,26 @@
1
+ // packages/expo-audio-stream/src/logger.ts
2
+ import createDebug from "debug";
3
+
4
+ import { DEBUG_NAMESPACE } from "./constants";
5
+
6
+ type ConsoleLike = {
7
+ log: (message: string, ...args: unknown[]) => void;
8
+ debug: (message: string, ...args: unknown[]) => void;
9
+ };
10
+
11
+ export const getLogger = (tag: string): ConsoleLike => {
12
+ const baseLogger = createDebug(`${DEBUG_NAMESPACE}:${tag}`);
13
+
14
+ return {
15
+ log: (...args: unknown[]) => baseLogger(...(args as [unknown])),
16
+ debug: (...args: unknown[]) => baseLogger(...(args as [unknown])),
17
+ };
18
+ };
19
+
20
+ export const enableAllLoggers = () => {
21
+ createDebug.enable(`${DEBUG_NAMESPACE}:*`);
22
+ };
23
+
24
+ export const disableAllLoggers = () => {
25
+ createDebug.disable();
26
+ };