@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,136 @@
1
+ // packages/expo-audio-stream/src/AudioAnalysis/extractAudioAnalysis.ts
2
+ import { AudioAnalysisData } from "./AudioAnalysis.types";
3
+ import ExpoAudioStreamModule from "../ExpoAudioStreamModule";
4
+ import { isWeb } from "../constants";
5
+ import { getLogger } from "../logger";
6
+ import { ExtractMetadataProps } from "../useAudioRecorder";
7
+ import { convertPCMToFloat32 } from "../utils/convertPCMToFloat32";
8
+ import { getWavFileInfo } from "../utils/getWavFileInfo";
9
+ import { InlineFeaturesExtractor } from "../workers/InlineFeaturesExtractor.web";
10
+
11
+ const logger = getLogger("extractAudioAnalysis");
12
+
13
+ export const extractAudioAnalysis = async ({
14
+ fileUri,
15
+ pointsPerSecond = 20,
16
+ arrayBuffer,
17
+ bitDepth,
18
+ skipWavHeader,
19
+ durationMs,
20
+ sampleRate,
21
+ numberOfChannels,
22
+ algorithm = "rms",
23
+ features,
24
+ featuresExtratorUrl,
25
+ }: ExtractMetadataProps): Promise<AudioAnalysisData> => {
26
+ if (isWeb) {
27
+ if (!arrayBuffer && !fileUri) {
28
+ throw new Error("Either arrayBuffer or fileUri must be provided");
29
+ }
30
+
31
+ if (!arrayBuffer) {
32
+ logger.log(`fetching fileUri`, fileUri);
33
+ const response = await fetch(fileUri!);
34
+
35
+ if (!response.ok) {
36
+ throw new Error(`Failed to fetch fileUri: ${response.statusText}`);
37
+ }
38
+
39
+ arrayBuffer = (await response.arrayBuffer()).slice(0);
40
+ logger.log(`fetched fileUri`, arrayBuffer.byteLength, arrayBuffer);
41
+ }
42
+
43
+ // Create a new copy of the ArrayBuffer to avoid detachment issues
44
+ const bufferCopy = arrayBuffer.slice(0);
45
+ logger.log(
46
+ `extractAudioAnalysis skipWavHeader=${skipWavHeader} bitDepth=${bitDepth} len=${bufferCopy.byteLength}`,
47
+ bufferCopy.slice(0, 100),
48
+ );
49
+
50
+ let actualBitDepth = bitDepth;
51
+ if (!actualBitDepth) {
52
+ logger.log(
53
+ `extractAudioAnalysis bitDepth not provided -- getting wav file info`,
54
+ );
55
+ const fileInfo = await getWavFileInfo(bufferCopy);
56
+ actualBitDepth = fileInfo.bitDepth;
57
+ }
58
+ logger.log(`extractAudioAnalysis actualBitDepth=${actualBitDepth}`);
59
+ // let copyChannelData: Float32Array;
60
+ // try {
61
+ // const audioContext = new (window.AudioContext ||
62
+ // // @ts-ignore
63
+ // window.webkitAudioContext)();
64
+ // const audioBuffer = await audioContext.decodeAudioData(bufferCopy);
65
+ // const channelData = audioBuffer.getChannelData(0); // Use only the first channel
66
+ // copyChannelData = new Float32Array(channelData); // Create a new Float32Array
67
+ // } catch (error) {
68
+ // console.warn("Failed to decode audio data:", error);
69
+ // // Fall back to creating a new Float32Array from the ArrayBuffer if decoding fails
70
+ // copyChannelData = new Float32Array(arrayBuffer);
71
+ // }
72
+
73
+ const {
74
+ pcmValues: channelData,
75
+ min,
76
+ max,
77
+ } = convertPCMToFloat32({
78
+ buffer: arrayBuffer,
79
+ bitDepth: actualBitDepth,
80
+ skipWavHeader,
81
+ });
82
+ logger.log(
83
+ `extractAudioAnalysis skipWaveHeader=${skipWavHeader} convertPCMToFloat32 length=${channelData.length} range: [ ${min} :: ${max} ]`,
84
+ );
85
+
86
+ return new Promise((resolve, reject) => {
87
+ let worker: Worker;
88
+ if (featuresExtratorUrl) {
89
+ worker = new Worker(new URL(featuresExtratorUrl, window.location.href));
90
+ } else {
91
+ const blob = new Blob([InlineFeaturesExtractor], {
92
+ type: "application/javascript",
93
+ });
94
+ const url = URL.createObjectURL(blob);
95
+ worker = new Worker(url);
96
+ }
97
+
98
+ worker.onmessage = (event) => {
99
+ resolve(event.data.result);
100
+ };
101
+
102
+ worker.onerror = (error) => {
103
+ reject(error);
104
+ };
105
+
106
+ worker.postMessage({
107
+ command: "process",
108
+ channelData,
109
+ sampleRate,
110
+ pointsPerSecond,
111
+ algorithm,
112
+ bitDepth,
113
+ fullAudioDurationMs: durationMs,
114
+ numberOfChannels,
115
+ });
116
+ });
117
+ } else {
118
+ if (!fileUri) {
119
+ throw new Error("fileUri is required");
120
+ }
121
+ logger.log(`extractAudioAnalysis`, {
122
+ fileUri,
123
+ pointsPerSecond,
124
+ algorithm,
125
+ });
126
+ const res = await ExpoAudioStreamModule.extractAudioAnalysis({
127
+ fileUri,
128
+ pointsPerSecond,
129
+ skipWavHeader,
130
+ algorithm,
131
+ features,
132
+ });
133
+ logger.log(`extractAudioAnalysis`, res);
134
+ return res;
135
+ }
136
+ };
@@ -0,0 +1,25 @@
1
+ import ExpoAudioStreamModule from "../ExpoAudioStreamModule";
2
+ import { getLogger } from "../logger";
3
+
4
+ const logger = getLogger("extractWaveform");
5
+ export interface ExtractWaveformProps {
6
+ fileUri: string;
7
+ numberOfSamples: number;
8
+ offset?: number;
9
+ length?: number;
10
+ }
11
+ export const extractWaveform = async ({
12
+ fileUri,
13
+ numberOfSamples,
14
+ offset = 0,
15
+ length,
16
+ }: ExtractWaveformProps): Promise<unknown> => {
17
+ const res = await ExpoAudioStreamModule.extractAudioAnalysis({
18
+ fileUri,
19
+ numberOfSamples,
20
+ offset,
21
+ length,
22
+ });
23
+ logger.log(`extractWaveform`, res);
24
+ return res;
25
+ };
@@ -1,18 +1,46 @@
1
+ // packages/expo-audio-stream/src/AudioRecorder.provider.tsx
1
2
  import React, { createContext, useContext } from "react";
2
3
 
4
+ import { AudioAnalysisData } from "./AudioAnalysis/AudioAnalysis.types";
3
5
  import {
4
- UseAudioRecorderProps,
5
- UseAudioRecorderState,
6
- useAudioRecorder,
7
- } from "./useAudioRecording";
6
+ AudioRecordingResult,
7
+ RecordingConfig,
8
+ StartRecordingResult,
9
+ } from "./ExpoAudioStream.types";
10
+ import { UseAudioRecorderProps, useAudioRecorder } from "./useAudioRecorder";
8
11
 
9
- const AudioRecorderContext = createContext<UseAudioRecorderState>({
12
+ export interface UseAudioRecorderState {
13
+ startRecording: (_: RecordingConfig) => Promise<StartRecordingResult>;
14
+ stopRecording: () => Promise<AudioRecordingResult | null>;
15
+ pauseRecording: () => void;
16
+ resumeRecording: () => void;
17
+ isRecording: boolean;
18
+ isPaused: boolean;
19
+ durationMs: number; // Duration of the recording
20
+ size: number; // Size in bytes of the recorded audio
21
+ analysisData?: AudioAnalysisData;
22
+ }
23
+
24
+ const initContext: UseAudioRecorderState = {
10
25
  isRecording: false,
11
26
  isPaused: false,
12
- duration: 0,
27
+ durationMs: 0,
13
28
  size: 0,
14
- // other properties filled on useAudioRecorder
15
- } as UseAudioRecorderState);
29
+ startRecording: async () => {
30
+ throw new Error("AudioRecorderProvider not found");
31
+ },
32
+ stopRecording: async () => {
33
+ throw new Error("AudioRecorderProvider not found");
34
+ },
35
+ pauseRecording: () => {
36
+ throw new Error("AudioRecorderProvider not found");
37
+ },
38
+ resumeRecording: () => {
39
+ throw new Error("AudioRecorderProvider not found");
40
+ },
41
+ };
42
+
43
+ const AudioRecorderContext = createContext<UseAudioRecorderState>(initContext);
16
44
 
17
45
  interface AudioRecorderProviderProps {
18
46
  children: React.ReactNode;
@@ -0,0 +1,6 @@
1
+ // src/ExpoAudioStreamModule.ts
2
+ import { requireNativeModule } from "expo-modules-core";
3
+
4
+ // It loads the native module object from the JSI or falls back to
5
+ // the bridge module (from NativeModulesProxy) if the remote debugger is on.
6
+ export default requireNativeModule("ExpoAudioStream");
@@ -1,8 +1,29 @@
1
- import { AudioDataEvent } from "./useAudioRecording";
1
+ // packages/expo-audio-stream/src/ExpoAudioStream.types.ts
2
+ import {
3
+ AudioAnalysisData,
4
+ AudioFeaturesOptions,
5
+ } from "./AudioAnalysis/AudioAnalysis.types";
6
+
7
+ export interface AudioStreamStatus {
8
+ isRecording: boolean;
9
+ isPaused: boolean;
10
+ durationMs: number;
11
+ size: number;
12
+ interval: number;
13
+ mimeType: string;
14
+ }
15
+
16
+ export interface AudioDataEvent {
17
+ data: string | ArrayBuffer;
18
+ position: number;
19
+ fileUri: string;
20
+ eventDataSize: number;
21
+ totalSize: number;
22
+ }
2
23
 
3
24
  export interface AudioEventPayload {
4
25
  encoded?: string;
5
- buffer?: Blob;
26
+ buffer?: ArrayBuffer;
6
27
  fileUri: string;
7
28
  lastEmittedSize: number;
8
29
  position: number;
@@ -12,38 +33,42 @@ export interface AudioEventPayload {
12
33
  streamUuid: string;
13
34
  }
14
35
 
15
- export interface AudioStreamResult {
36
+ export type EncodingType = "pcm_32bit" | "pcm_16bit" | "pcm_8bit";
37
+ export type SampleRate = 16000 | 44100 | 48000;
38
+ export type BitDepth = 8 | 16 | 32;
39
+
40
+ export interface AudioRecordingResult {
16
41
  fileUri: string;
17
- duration: number;
42
+ webAudioUri?: string;
43
+ durationMs: number;
18
44
  size: number;
19
45
  mimeType: string;
20
- channels?: number;
21
- bitDepth?: number;
22
- sampleRate?: number;
46
+ channels: number;
47
+ bitDepth: BitDepth;
48
+ sampleRate: SampleRate;
23
49
  }
24
50
 
25
- export interface StartAudioStreamResult {
51
+ export interface StartRecordingResult {
26
52
  fileUri: string;
27
53
  mimeType: string;
28
54
  channels?: number;
29
- bitDepth?: number;
30
- sampleRate?: number;
31
- }
32
-
33
- export interface AudioStreamStatus {
34
- isRecording: boolean;
35
- isPaused: boolean;
36
- duration: number;
37
- size: number;
38
- interval: number;
39
- mimeType: string;
55
+ bitDepth?: BitDepth;
56
+ sampleRate?: SampleRate;
40
57
  }
41
58
 
42
- export type EncodingType = "pcm_16bit" | "pcm_8bit";
43
59
  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>;
60
+ sampleRate?: SampleRate; // Sample rate for recording
61
+ channels?: 1 | 2; // 1 or 2 (MONO or STEREO)
62
+ encoding?: EncodingType; // Encoding type for the recording
63
+ interval?: number; // Interval in milliseconds at which to emit recording data
64
+
65
+ // Optional parameters for audio processing
66
+ enableProcessing?: boolean; // Boolean to enable/disable audio processing (default is false)
67
+ pointsPerSecond?: number; // Number of data points to extract per second of audio (default is 1000)
68
+ algorithm?: string; // Algorithm to use for extraction (default is "rms")
69
+ features?: AudioFeaturesOptions; // Feature options to extract (default is empty)
70
+
71
+ // Optional paramters from web
72
+ onAudioStream?: (_: AudioDataEvent) => Promise<void>; // Callback function to handle audio stream
73
+ onProcessingResult?: (_: AudioAnalysisData) => Promise<void>; // Callback function to handle processing results
49
74
  }
@@ -0,0 +1,229 @@
1
+ // src/ExpoAudioStreamModule.web.ts
2
+ import { EventEmitter } from "expo-modules-core";
3
+
4
+ import { AudioAnalysisData } from "./AudioAnalysis/AudioAnalysis.types";
5
+ import {
6
+ AudioEventPayload,
7
+ AudioRecordingResult,
8
+ AudioStreamStatus,
9
+ BitDepth,
10
+ RecordingConfig,
11
+ StartRecordingResult,
12
+ } from "./ExpoAudioStream.types";
13
+ import { WebRecorder } from "./WebRecorder.web";
14
+ import { getLogger } from "./logger";
15
+ import { encodingToBitDepth } from "./utils/encodingToBitDepth";
16
+
17
+ export interface EmitAudioEventProps {
18
+ data: ArrayBuffer;
19
+ position: number;
20
+ }
21
+ export type EmitAudioEventFunction = (_: EmitAudioEventProps) => void;
22
+ export type EmitAudioAnalysisFunction = (_: AudioAnalysisData) => void;
23
+
24
+ export interface ExpoAudioStreamWebProps {
25
+ audioWorkletUrl: string;
26
+ featuresExtratorUrl: string;
27
+ }
28
+
29
+ const logger = getLogger("ExpoAudioStreamWeb");
30
+
31
+ export class ExpoAudioStreamWeb extends EventEmitter {
32
+ customRecorder: WebRecorder | null;
33
+ audioChunks: ArrayBuffer[];
34
+ isRecording: boolean;
35
+ isPaused: boolean;
36
+ recordingStartTime: number;
37
+ pausedTime: number;
38
+ currentDurationMs: number;
39
+ currentSize: number;
40
+ currentInterval: number;
41
+ lastEmittedSize: number;
42
+ lastEmittedTime: number;
43
+ streamUuid: string | null;
44
+ extension: "webm" | "wav" = "wav"; // Default extension is 'webm'
45
+ recordingConfig?: RecordingConfig;
46
+ bitDepth: BitDepth; // Bit depth of the audio
47
+ audioWorkletUrl: string;
48
+ featuresExtratorUrl: string;
49
+
50
+ constructor({
51
+ audioWorkletUrl,
52
+ featuresExtratorUrl,
53
+ }: ExpoAudioStreamWebProps) {
54
+ const mockNativeModule = {
55
+ addListener: (eventName: string) => {
56
+ // Not used on web
57
+ },
58
+ removeListeners: (count: number) => {
59
+ // Not used on web
60
+ },
61
+ };
62
+ super(mockNativeModule); // Pass the mock native module to the parent class
63
+
64
+ this.customRecorder = null;
65
+ this.audioChunks = [];
66
+ this.isRecording = false;
67
+ this.isPaused = false;
68
+ this.recordingStartTime = 0;
69
+ this.pausedTime = 0;
70
+ this.currentDurationMs = 0;
71
+ this.currentSize = 0;
72
+ this.bitDepth = 32; // Default
73
+ this.currentInterval = 1000; // Default interval in ms
74
+ this.lastEmittedSize = 0;
75
+ this.lastEmittedTime = 0;
76
+ this.streamUuid = null; // Initialize UUID on first recording start
77
+ this.audioWorkletUrl = audioWorkletUrl;
78
+ this.featuresExtratorUrl = featuresExtratorUrl;
79
+ }
80
+
81
+ // Utility to handle user media stream
82
+ async getMediaStream() {
83
+ try {
84
+ return await navigator.mediaDevices.getUserMedia({ audio: true });
85
+ } catch (error) {
86
+ console.error("Failed to get media stream:", error);
87
+ throw error;
88
+ }
89
+ }
90
+
91
+ // Start recording with options
92
+ async startRecording(recordingConfig: RecordingConfig = {}) {
93
+ if (this.isRecording) {
94
+ throw new Error("Recording is already in progress");
95
+ }
96
+
97
+ this.bitDepth = encodingToBitDepth({
98
+ encoding: recordingConfig.encoding ?? "pcm_32bit",
99
+ });
100
+
101
+ const audioContext = new (window.AudioContext ||
102
+ // @ts-ignore - Allow webkitAudioContext for Safari
103
+ window.webkitAudioContext)();
104
+ const stream = await this.getMediaStream();
105
+
106
+ const source = audioContext.createMediaStreamSource(stream);
107
+
108
+ this.customRecorder = new WebRecorder({
109
+ audioContext,
110
+ source,
111
+ recordingConfig,
112
+ audioWorkletUrl: this.audioWorkletUrl,
113
+ featuresExtratorUrl: this.featuresExtratorUrl,
114
+ emitAudioEventCallback: ({ data, position }: EmitAudioEventProps) => {
115
+ this.audioChunks.push(data);
116
+ this.currentSize += data.byteLength;
117
+ this.emitAudioEvent({ data, position });
118
+ this.lastEmittedTime = Date.now();
119
+ this.lastEmittedSize = this.currentSize;
120
+ },
121
+ emitAudioAnalysisCallback: (audioAnalysisData: AudioAnalysisData) => {
122
+ logger.log(`Emitted AudioAnalysis:`, audioAnalysisData);
123
+ this.emit("AudioAnalysis", audioAnalysisData);
124
+ },
125
+ });
126
+ await this.customRecorder.init();
127
+ this.customRecorder.start();
128
+
129
+ // // Set a timer to stop recording after 5 seconds
130
+ // setTimeout(() => {
131
+ // logger.log("AUTO Stopping recording");
132
+ // this.customRecorder?.stopAndPlay();
133
+ // this.isRecording = false;
134
+ // }, 3000);
135
+
136
+ this.isRecording = true;
137
+ this.recordingConfig = recordingConfig;
138
+ this.recordingStartTime = Date.now();
139
+ this.pausedTime = 0;
140
+ this.lastEmittedSize = 0;
141
+ this.lastEmittedTime = 0;
142
+ this.streamUuid = Date.now().toString();
143
+ const fileUri = `${this.streamUuid}.${this.extension}`;
144
+ const streamConfig: StartRecordingResult = {
145
+ fileUri,
146
+ mimeType: `audio/${this.extension}`,
147
+ bitDepth: this.bitDepth,
148
+ channels: recordingConfig.channels ?? 1,
149
+ sampleRate: recordingConfig.sampleRate ?? 44100,
150
+ };
151
+ return streamConfig;
152
+ }
153
+
154
+ emitAudioEvent({ data, position }: EmitAudioEventProps) {
155
+ const fileUri = `${this.streamUuid}.${this.extension}`;
156
+ const audioEventPayload: AudioEventPayload = {
157
+ fileUri,
158
+ mimeType: `audio/${this.extension}`,
159
+ lastEmittedSize: this.lastEmittedSize, // Since this might be continuously streaming, adjust accordingly
160
+ deltaSize: data.byteLength,
161
+ position,
162
+ totalSize: this.currentSize,
163
+ buffer: data,
164
+ streamUuid: this.streamUuid ?? "", // Generate or manage UUID for stream identification
165
+ };
166
+
167
+ this.emit("AudioData", audioEventPayload);
168
+ }
169
+
170
+ // Stop recording
171
+ async stopRecording(): Promise<AudioRecordingResult | null> {
172
+ if (this.customRecorder) {
173
+ const fullPcmBuffer = await this.customRecorder.stop();
174
+ logger.debug(`Stopped recording`, fullPcmBuffer);
175
+ }
176
+ this.isRecording = false;
177
+ this.currentDurationMs = Date.now() - this.recordingStartTime;
178
+ const result: AudioRecordingResult = {
179
+ fileUri: `${this.streamUuid}.${this.extension}`,
180
+ bitDepth: this.bitDepth,
181
+ channels: this.recordingConfig?.channels ?? 1,
182
+ sampleRate: this.recordingConfig?.sampleRate ?? 44100,
183
+ durationMs: this.currentDurationMs,
184
+ size: this.currentSize,
185
+ mimeType: `audio/${this.extension}`,
186
+ };
187
+
188
+ return result;
189
+ }
190
+
191
+ // Pause recording
192
+ async pauseRecording() {
193
+ if (!this.isRecording || this.isPaused) {
194
+ throw new Error("Recording is not active or already paused");
195
+ }
196
+
197
+ if (this.customRecorder) {
198
+ this.customRecorder.pause();
199
+ }
200
+ this.isPaused = true;
201
+ this.pausedTime = Date.now();
202
+ }
203
+
204
+ // Resume recording
205
+ async resumeRecording() {
206
+ if (!this.isPaused) {
207
+ throw new Error("Recording is not paused");
208
+ }
209
+
210
+ if (this.customRecorder) {
211
+ this.customRecorder.resume();
212
+ }
213
+ this.isPaused = false;
214
+ this.recordingStartTime += Date.now() - this.pausedTime;
215
+ }
216
+
217
+ // Get current status
218
+ status() {
219
+ const status: AudioStreamStatus = {
220
+ isRecording: this.isRecording,
221
+ isPaused: this.isPaused,
222
+ durationMs: Date.now() - this.recordingStartTime,
223
+ size: this.currentSize,
224
+ interval: this.currentInterval,
225
+ mimeType: `audio/${this.extension}`,
226
+ };
227
+ return status;
228
+ }
229
+ }
@@ -1,5 +1,24 @@
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
+ let instance: ExpoAudioStreamWeb | null = null;
13
+
14
+ ExpoAudioStreamModule = (webProps: ExpoAudioStreamWebProps) => {
15
+ if (!instance) {
16
+ instance = new ExpoAudioStreamWeb(webProps);
17
+ }
18
+ return instance;
19
+ };
20
+ } else {
21
+ ExpoAudioStreamModule = requireNativeModule("ExpoAudioStream");
22
+ }
23
+
24
+ export default ExpoAudioStreamModule;