@siteed/expo-audio-stream 0.1.0 → 0.2.0

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.
package/README.md CHANGED
@@ -1,4 +1,4 @@
1
- # expo-audio-stream
1
+ # @siteed/expo-audio-stream
2
2
 
3
3
  `@siteed/expo-audio-stream` is a comprehensive library designed to facilitate real-time audio processing and streaming across iOS, Android, and web platforms. This library leverages Expo's robust ecosystem to simplify the implementation of audio recording and streaming functionalities within React Native applications. Key features include audio streaming with configurable buffer intervals and automatic handling of microphone permissions in managed Expo projects.
4
4
 
@@ -7,10 +7,10 @@
7
7
  - Real-time audio streaming across iOS, Android, and web.
8
8
  - Configurable intervals for audio buffer receipt.
9
9
  - Automated microphone permissions setup in managed Expo projects.
10
+ - IOS is automatically setup to handle background audio recording.
10
11
  - Listeners for audio data events with detailed event payloads.
11
12
  - Utility functions for recording control and file management.
12
13
 
13
-
14
14
  ## Installation
15
15
 
16
16
  To install `@siteed/expo-audio-stream`, add it to your project using npm or Yarn:
@@ -46,25 +46,36 @@ import {
46
46
  useAudioRecorder,
47
47
  } from 'expo-audio-stream';
48
48
 
49
+ export interface AudioStreamResult {
50
+ fileUrl: string;
51
+ duration: number;
52
+ size: number;
53
+ mimeType: string;
54
+ }
55
+
49
56
  export default function App() {
50
57
  const { startRecording, stopRecording, duration, size, isRecording } = useAudioRecorder({
51
- onAudioStream: (base64Data) => {
52
- console.log(`audio event ${typeof base64Data}`, base64Data);
58
+ onAudioStream: (audioData: Blob) => {
59
+ console.log(`audio event`,audioData);
53
60
  }
54
61
  });
55
62
 
56
63
  const handleStart = async () => {
57
64
  const { granted } = await Audio.requestPermissionsAsync();
58
65
  if (granted) {
59
- startRecording({interval: 500});
66
+ const fileUri = await startRecording({interval: 500});
60
67
  }
61
68
  };
62
69
 
70
+ const handleStop = async () => {
71
+ const result: AudioStreamResult = await stopRecording();
72
+ };
73
+
63
74
  const renderRecording = () => (
64
75
  <View>
65
76
  <Text>Duration: {duration} ms</Text>
66
77
  <Text>Size: {size} bytes</Text>
67
- <Button title="Stop Recording" onPress={stopRecording} />
78
+ <Button title="Stop Recording" onPress={handleStop} />
68
79
  </View>
69
80
  );
70
81
 
@@ -83,3 +94,66 @@ export default function App() {
83
94
  }
84
95
  ```
85
96
 
97
+ The library also exposes an `addAudioEventListener` function that provides an `AudioEventPayload` object that you can subscribe to:
98
+ ```tsx
99
+ export interface AudioEventPayload {
100
+ encoded?: string,
101
+ buffer?: Blob,
102
+ fileUri: string,
103
+ from: number,
104
+ deltaSize: number,
105
+ totalSize: number,
106
+ mimeType: string;
107
+ streamUuid: string,
108
+ };
109
+
110
+ useEffect(() => {
111
+ const subscribe = addAudioEventListener(async ({fileUri, deltaSize, totalSize, from, streamUuid, encoded, mimeType, buffer}) => {
112
+ log(`Received audio event:`, {fileUri, deltaSize, totalSize, mimeType, from, streamUuid, encodedLength: encoded?.length})
113
+ if(deltaSize > 0) {
114
+ // Coming from native ( ios / android ) otherwise buffer is set
115
+ if(Platform.OS !== 'web') {
116
+ // Read the audio file as a base64 string for comparison
117
+ try {
118
+ // convert encoded string to binary data
119
+ const binaryData = atob(encoded);
120
+ const content = new Uint8Array(binaryData.length);
121
+ for (let i = 0; i < binaryData.length; i++) {
122
+ content[i] = binaryData.charCodeAt(i);
123
+ }
124
+ const audioBlob = new Blob([content], { type: mimeType });
125
+ console.info(`Received audio blob:`, audioBlob);
126
+ } catch (error) {
127
+ console.error('Error reading audio file:', error);
128
+ }
129
+ } else if(buffer) {
130
+ // Coming from web
131
+ console.info(`Received audio buffer:`, buffer)
132
+ }
133
+ }
134
+ });
135
+ return () => subscribe.remove();
136
+ }, []);
137
+ ```
138
+
139
+ ### Recording configuration
140
+
141
+ - on Android and IOS, audio is recorded in wav format, 16khz sample rate, 16 bit depth, 1 channel.
142
+ - on web, it usually records in opus but it depends on the browser configuration.
143
+
144
+ If you want to process the audio livestream directly, I recommend having another encoding step to align the audio format across platforms.
145
+
146
+
147
+ ### Debug Configuration
148
+
149
+ This library uses the npm `debug` package, to enable logging you can:
150
+ ```
151
+ localStorage.debug = 'expo-audio-stream:*'
152
+ ```
153
+
154
+
155
+ ### TODO
156
+ this package is still in development, and there are a few things that need to be done:
157
+ - remove the dependency on expo-av
158
+ - add multiple format of audio stream (wav, mp3, opus)
159
+
@@ -22,13 +22,18 @@ import java.util.concurrent.atomic.AtomicBoolean
22
22
  import java.io.File
23
23
  import java.io.FileOutputStream
24
24
  import java.io.IOException
25
+ import java.io.RandomAccessFile
26
+
25
27
  const val AUDIO_EVENT_NAME = "AudioData"
28
+ const val DEFAULT_SAMPLE_RATE = 16000 // Default sample rate for audio recording
29
+ const val DEFAULT_CHANNEL_CONFIG = AudioFormat.CHANNEL_IN_MONO
30
+ const val DEFAULT_AUDIO_FORMAT = AudioFormat.ENCODING_PCM_16BIT
26
31
 
27
32
  class ExpoAudioStreamModule() : Module() {
28
33
  private var audioRecord: AudioRecord? = null
29
- private var sampleRateInHz = 44100 // Default sample rate
30
- private var channelConfig = AudioFormat.CHANNEL_IN_MONO
31
- private var audioFormat = AudioFormat.ENCODING_PCM_16BIT
34
+ private var sampleRateInHz = DEFAULT_SAMPLE_RATE
35
+ private var channelConfig = DEFAULT_CHANNEL_CONFIG
36
+ private var audioFormat = DEFAULT_AUDIO_FORMAT
32
37
  private var bufferSizeInBytes: Int = AudioRecord.getMinBufferSize(sampleRateInHz, channelConfig, audioFormat)
33
38
  private var isRecording = AtomicBoolean(false)
34
39
  private val isPaused = AtomicBoolean(false)
@@ -44,6 +49,7 @@ class ExpoAudioStreamModule() : Module() {
44
49
  private var lastPauseTime = 0L
45
50
  private var pausedDuration = 0L
46
51
  private var lastEmittedSize = 0L
52
+ private var mimeType = "audio/wav"
47
53
  private val mainHandler = Handler(Looper.getMainLooper())
48
54
 
49
55
  @SuppressLint("MissingPermission")
@@ -72,6 +78,7 @@ class ExpoAudioStreamModule() : Module() {
72
78
  "duration" to totalRecordedTime,
73
79
  "isRecording" to isRecording.get(),
74
80
  "isPaused" to isPaused.get(),
81
+ "mime" to mimeType,
75
82
  "size" to totalDataSize,
76
83
  "interval" to interval,
77
84
  )
@@ -87,26 +94,62 @@ class ExpoAudioStreamModule() : Module() {
87
94
  }
88
95
 
89
96
  AsyncFunction("pauseRecording") { promise: Promise ->
97
+ Log.d("AudioRecorderModule", "Pausing recording")
90
98
  pauseRecording(promise)
91
99
  }
92
100
 
93
101
  AsyncFunction("stopRecording") { promise: Promise ->
102
+ Log.d("AudioRecorderModule", "Stopping recording")
94
103
  stopRecording(promise)
95
104
  }
96
105
  }
97
106
 
107
+ // Method to write WAV file header
108
+ private fun writeWavHeader(out: FileOutputStream, channelConfig: Int, sampleRate: Int, audioFormat: Int) {
109
+ val channels = if (channelConfig == AudioFormat.CHANNEL_IN_MONO) 1 else 2
110
+ val bitDepth = if (audioFormat == AudioFormat.ENCODING_PCM_16BIT) 16 else 8
111
+
112
+ val header = ByteArray(44)
113
+ val byteRate = sampleRate * channels * bitDepth / 8
114
+ val blockAlign = channels * bitDepth / 8
115
+
116
+ // RIFF/WAVE header
117
+ "RIFF".toByteArray().copyInto(header, 0)
118
+ header[4] = 0 // Size will be updated later
119
+ "WAVE".toByteArray().copyInto(header, 8)
120
+ "fmt ".toByteArray().copyInto(header, 12)
121
+
122
+ // 16 for PCM
123
+ header[16] = 16
124
+ header[20] = 1 // Audio format 1 for PCM
125
+ header[22] = channels.toByte()
126
+ header[24] = (sampleRate and 0xff).toByte()
127
+ header[25] = (sampleRate shr 8 and 0xff).toByte()
128
+ header[26] = (sampleRate shr 16 and 0xff).toByte()
129
+ header[27] = (sampleRate shr 24 and 0xff).toByte()
130
+ header[28] = (byteRate and 0xff).toByte()
131
+ header[29] = (byteRate shr 8 and 0xff).toByte()
132
+ header[30] = (byteRate shr 16 and 0xff).toByte()
133
+ header[31] = (byteRate shr 24 and 0xff).toByte()
134
+ header[32] = blockAlign.toByte()
135
+ header[34] = bitDepth.toByte()
136
+ "data".toByteArray().copyInto(header, 36)
137
+
138
+ out.write(header, 0, 44)
139
+ }
140
+
98
141
  private fun configureRecording(params: Map<String, Any?>) {
99
- sampleRateInHz = (params["sampleRate"] as? Int) ?: 44100
100
- channelConfig = (params["channelConfig"] as? Int) ?: AudioFormat.CHANNEL_IN_MONO
101
- audioFormat = (params["audioFormat"] as? Int) ?: AudioFormat.ENCODING_PCM_16BIT
142
+ sampleRateInHz = (params["sampleRate"] as? Int) ?: DEFAULT_SAMPLE_RATE
143
+ channelConfig = (params["channelConfig"] as? Int) ?: DEFAULT_CHANNEL_CONFIG
144
+ audioFormat = (params["audioFormat"] as? Int) ?: DEFAULT_AUDIO_FORMAT
102
145
  bufferSizeInBytes = AudioRecord.getMinBufferSize(sampleRateInHz, channelConfig, audioFormat)
103
146
  }
104
147
 
105
148
  private fun listAudioFiles(): List<String> {
106
149
  val filesDir = appContext.reactContext?.filesDir
107
- // Filter to include only .pcm files
150
+ // Filter to include only .wav files
108
151
  val files = filesDir?.listFiles { file ->
109
- file.isFile && file.name.endsWith(".pcm")
152
+ file.isFile && file.name.endsWith(".wav")
110
153
  }?.map { it.absolutePath } ?: listOf() // Use `listOf()` to return an empty list if null
111
154
  return files
112
155
  }
@@ -143,7 +186,7 @@ class ExpoAudioStreamModule() : Module() {
143
186
  // Log for new recording or resuming
144
187
  if (!isPaused.get()) {
145
188
  streamUuid = java.util.UUID.randomUUID().toString()
146
- audioFile = File(appContext.reactContext?.filesDir, "audio_${streamUuid}.pcm")
189
+ audioFile = File(appContext.reactContext?.filesDir, "audio_${streamUuid}.wav")
147
190
  Log.i("AudioRecorderModule", "Starting new recording $streamUuid with sample rate: $sampleRateInHz, channel config: $channelConfig, audio format: $audioFormat, buffer size: $bufferSizeInBytes, interval: $interval")
148
191
 
149
192
  } else {
@@ -156,6 +199,15 @@ class ExpoAudioStreamModule() : Module() {
156
199
  return
157
200
  }
158
201
 
202
+ try {
203
+ FileOutputStream(audioFile, true).use { fos ->
204
+ writeWavHeader(fos, channelConfig, sampleRateInHz, audioFormat)
205
+ }
206
+ } catch (e: IOException) {
207
+ promise.reject("FILE_CREATION_FAILED", "Failed to create audio file with WAV header", null)
208
+ return
209
+ }
210
+
159
211
  audioRecord?.startRecording()
160
212
  isPaused.set(false)
161
213
  isRecording.set(true)
@@ -166,7 +218,7 @@ class ExpoAudioStreamModule() : Module() {
166
218
  }
167
219
 
168
220
  recordingThread = Thread { recordingProcess() }.apply { start() }
169
- promise.resolve(null)
221
+ promise.resolve(audioFile?.toURI().toString())
170
222
  }
171
223
 
172
224
  private fun stopRecording(promise: Promise) {
@@ -182,7 +234,19 @@ class ExpoAudioStreamModule() : Module() {
182
234
  totalRecordedTime += (endTime - recordingStartTime - pausedDuration) // Adjust the total recording time
183
235
  isRecording.set(false)
184
236
  isPaused.set(false)
185
- promise.resolve(totalRecordedTime)
237
+
238
+ // Calculate the file size
239
+ val fileSize = audioFile?.length() ?: 0
240
+
241
+ // Create result bundle
242
+ val result = bundleOf(
243
+ "fileUri" to audioFile?.toURI().toString(),
244
+ "duration" to totalRecordedTime,
245
+ "size" to fileSize,
246
+ "mimeType" to mimeType
247
+ )
248
+ promise.resolve(result)
249
+
186
250
  // Reset the timing variables
187
251
  totalRecordedTime = 0
188
252
  pausedDuration = 0
@@ -194,30 +258,49 @@ class ExpoAudioStreamModule() : Module() {
194
258
  }
195
259
 
196
260
  private fun recordingProcess() {
197
-
198
- val audioData = ByteArray(bufferSizeInBytes)
199
- while (isRecording.get()) {
200
- if (!isPaused.get()) {
201
- val bytesRead = audioRecord?.read(audioData, 0, bufferSizeInBytes) ?: -1
202
- if (bytesRead < 0) {
203
- Log.e("AudioRecorderModule", "Read error: $bytesRead")
204
- break
205
- }
206
- if (bytesRead > 0) {
207
- audioDataBuffer.write(audioData, 0, bytesRead)
208
- totalDataSize += bytesRead
209
- if (SystemClock.elapsedRealtime() - lastEmitTime >= interval) {
210
- emitAudioData()
211
- lastEmitTime = SystemClock.elapsedRealtime() // Reset the timer
261
+ FileOutputStream(audioFile, true).use { fos ->
262
+ // Write audio data directly to the file
263
+ val audioData = ByteArray(bufferSizeInBytes)
264
+ while (isRecording.get()) {
265
+ if (!isPaused.get()) {
266
+ val bytesRead = audioRecord?.read(audioData, 0, bufferSizeInBytes) ?: -1
267
+ if (bytesRead < 0) {
268
+ Log.e("AudioRecorderModule", "Read error: $bytesRead")
269
+ break
270
+ }
271
+ if (bytesRead > 0) {
272
+ fos.write(audioData, 0, bytesRead)
273
+ totalDataSize += bytesRead
274
+
275
+ // Emit audio data at defined intervals
276
+ if (SystemClock.elapsedRealtime() - lastEmitTime >= interval) {
277
+ emitAudioData(audioData, bytesRead)
278
+ lastEmitTime = SystemClock.elapsedRealtime() // Reset the timer
279
+ }
212
280
  }
213
281
  }
214
282
  }
215
283
  }
216
- if (audioDataBuffer.size() > 0) {
217
- emitAudioData() // Emit any remaining data
218
- }
284
+ updateWavHeader() // Update the header with the correct file size after recording stops
219
285
  }
220
286
 
287
+ private fun updateWavHeader() {
288
+ try {
289
+ RandomAccessFile(audioFile, "rw").use { raf ->
290
+ val fileSize = raf.length()
291
+ val dataSize = fileSize - 44 // Subtract the header size
292
+ raf.seek(4) // Skip 'RIFF' label
293
+
294
+ // Write correct file size, excluding the first 8 bytes of the RIFF header
295
+ raf.writeInt(Integer.reverseBytes((dataSize + 36).toInt()))
296
+
297
+ raf.seek(40) // Go to the data size position
298
+ raf.writeInt(Integer.reverseBytes(dataSize.toInt())) // Write the size of the data segment
299
+ }
300
+ } catch (e: IOException) {
301
+ Log.e("AudioRecorderModule", "Could not update WAV header", e)
302
+ }
303
+ }
221
304
 
222
305
  private fun clearAudioStorage() {
223
306
  // Clear all files in the app's internal storage
@@ -228,34 +311,31 @@ class ExpoAudioStreamModule() : Module() {
228
311
  }
229
312
  }
230
313
 
231
- private fun emitAudioData() {
232
- val rawData = audioDataBuffer.toByteArray()
233
- if (!saveAudioToFile(rawData)) {
234
- Log.e("AudioRecorderModule", "Failed to save audio data")
235
- return
236
- }
237
- val encodedBuffer = encodeAudioData(rawData)
238
- val fileSize = audioFile?.length() ?: 0
314
+ private fun emitAudioData(audioData: ByteArray, length: Int) {
315
+ // Since audioData now contains the actual bytes read, you do not need to read from audioDataBuffer.
316
+ // Encode the part of the buffer that contains new audio data.
317
+ val encodedBuffer = encodeAudioData(audioData)
318
+ val fileSize = audioFile?.length() ?: 0 // Update file size information
239
319
  val from = lastEmittedSize
240
320
  val deltaSize = fileSize - lastEmittedSize
241
- lastEmittedSize = fileSize
321
+ lastEmittedSize = fileSize // Update last emitted size
322
+
242
323
  mainHandler.post {
243
324
  try {
244
- this@ExpoAudioStreamModule.sendEvent(AUDIO_EVENT_NAME,
245
- bundleOf(
246
- "fileUri" to audioFile?.toURI().toString(),
247
- "from" to from,
248
- "encoded" to encodedBuffer,
249
- "deltaSize" to deltaSize,
250
- "totalSize" to fileSize,
251
- "streamUuid" to streamUuid
252
- )
253
- )
325
+ this@ExpoAudioStreamModule.sendEvent(AUDIO_EVENT_NAME, bundleOf(
326
+ "fileUri" to audioFile?.toURI().toString(),
327
+ "from" to from,
328
+ "encoded" to encodedBuffer,
329
+ "deltaSize" to deltaSize,
330
+ "mimeType" to mimeType,
331
+ "totalSize" to fileSize,
332
+ "streamUuid" to streamUuid
333
+ ))
254
334
  } catch (e: Exception) {
255
335
  Log.e("AudioRecorderModule", "Failed to send event", e)
256
336
  }
257
337
  }
258
- audioDataBuffer.reset() // Clear the buffer after emitting
338
+ // audioDataBuffer.reset() is no longer needed here as we do not use the buffer to store ongoing data
259
339
  }
260
340
 
261
341
  private fun encodeAudioData(rawData: ByteArray): String {
@@ -5,19 +5,24 @@ export interface AudioEventPayload {
5
5
  from: number;
6
6
  deltaSize: number;
7
7
  totalSize: number;
8
+ mimeType: string;
8
9
  streamUuid: string;
9
10
  }
11
+ export interface AudioStreamResult {
12
+ fileUri: string;
13
+ duration: number;
14
+ size: number;
15
+ mimeType: string;
16
+ }
10
17
  export interface AudioStreamStatus {
11
18
  isRecording: boolean;
12
19
  isPaused: boolean;
13
20
  duration: number;
14
21
  size: number;
15
22
  interval: number;
23
+ mimeType: string;
16
24
  }
17
25
  export interface RecordingOptions {
18
- sampleRate?: number;
19
- channelConfig?: number;
20
- audioFormat?: number;
21
26
  interval?: number;
22
27
  }
23
28
  //# sourceMappingURL=ExpoAudioStream.types.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"ExpoAudioStream.types.d.ts","sourceRoot":"","sources":["../src/ExpoAudioStream.types.ts"],"names":[],"mappings":"AAAA,MAAM,WAAW,iBAAiB;IAChC,OAAO,CAAC,EAAE,MAAM,CAAC;IACjB,MAAM,CAAC,EAAE,IAAI,CAAC;IACd,OAAO,EAAE,MAAM,CAAC;IAChB,IAAI,EAAE,MAAM,CAAC;IACb,SAAS,EAAE,MAAM,CAAC;IAClB,SAAS,EAAE,MAAM,CAAC;IAClB,UAAU,EAAE,MAAM,CAAC;CACpB;AAED,MAAM,WAAW,iBAAiB;IAChC,WAAW,EAAE,OAAO,CAAC;IACrB,QAAQ,EAAE,OAAO,CAAC;IAClB,QAAQ,EAAE,MAAM,CAAC;IACjB,IAAI,EAAE,MAAM,CAAC;IACb,QAAQ,EAAE,MAAM,CAAC;CAClB;AAED,MAAM,WAAW,gBAAgB;IAE/B,UAAU,CAAC,EAAE,MAAM,CAAC;IACpB,aAAa,CAAC,EAAE,MAAM,CAAC;IACvB,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB,QAAQ,CAAC,EAAE,MAAM,CAAC;CACnB"}
1
+ {"version":3,"file":"ExpoAudioStream.types.d.ts","sourceRoot":"","sources":["../src/ExpoAudioStream.types.ts"],"names":[],"mappings":"AAAA,MAAM,WAAW,iBAAiB;IAChC,OAAO,CAAC,EAAE,MAAM,CAAC;IACjB,MAAM,CAAC,EAAE,IAAI,CAAC;IACd,OAAO,EAAE,MAAM,CAAC;IAChB,IAAI,EAAE,MAAM,CAAC;IACb,SAAS,EAAE,MAAM,CAAC;IAClB,SAAS,EAAE,MAAM,CAAC;IAClB,QAAQ,EAAE,MAAM,CAAC;IACjB,UAAU,EAAE,MAAM,CAAC;CACpB;AAED,MAAM,WAAW,iBAAiB;IAChC,OAAO,EAAE,MAAM,CAAC;IAChB,QAAQ,EAAE,MAAM,CAAC;IACjB,IAAI,EAAE,MAAM,CAAC;IACb,QAAQ,EAAE,MAAM,CAAC;CAClB;AAED,MAAM,WAAW,iBAAiB;IAChC,WAAW,EAAE,OAAO,CAAC;IACrB,QAAQ,EAAE,OAAO,CAAC;IAClB,QAAQ,EAAE,MAAM,CAAC;IACjB,IAAI,EAAE,MAAM,CAAC;IACb,QAAQ,EAAE,MAAM,CAAC;IACjB,QAAQ,EAAE,MAAM,CAAC;CAClB;AAED,MAAM,WAAW,gBAAgB;IAK/B,QAAQ,CAAC,EAAE,MAAM,CAAC;CACnB"}
@@ -1 +1 @@
1
- {"version":3,"file":"ExpoAudioStream.types.js","sourceRoot":"","sources":["../src/ExpoAudioStream.types.ts"],"names":[],"mappings":"AAQC,CAAC","sourcesContent":["export interface AudioEventPayload {\n encoded?: string, \n buffer?: Blob,\n fileUri: string,\n from: number,\n deltaSize: number,\n totalSize: number,\n streamUuid: string,\n};\n\nexport interface AudioStreamStatus {\n isRecording: boolean;\n isPaused: boolean;\n duration: number;\n size: number;\n interval: number;\n}\n\nexport interface RecordingOptions {\n // TODO align Android and IOS options\n sampleRate?: number;\n channelConfig?: number; // numberOfChannel\n audioFormat?: number; // bitDepth (ENCODING_PCM_16BIT --> 2)\n interval?: number;\n}\n"]}
1
+ {"version":3,"file":"ExpoAudioStream.types.js","sourceRoot":"","sources":["../src/ExpoAudioStream.types.ts"],"names":[],"mappings":"AASC,CAAC","sourcesContent":["export interface AudioEventPayload {\n encoded?: string, \n buffer?: Blob,\n fileUri: string,\n from: number,\n deltaSize: number,\n totalSize: number,\n mimeType: string;\n streamUuid: string,\n};\n\nexport interface AudioStreamResult {\n fileUri: string;\n duration: number;\n size: number;\n mimeType: string;\n}\n\nexport interface AudioStreamStatus {\n isRecording: boolean;\n isPaused: boolean;\n duration: number;\n size: number;\n interval: number;\n mimeType: string;\n}\n\nexport interface RecordingOptions {\n // TODO align Android and IOS options\n // sampleRate?: number;\n // channelConfig?: number; // numberOfChannel\n // audioFormat?: number; // bitDepth (ENCODING_PCM_16BIT --> 2)\n interval?: number;\n}\n"]}
@@ -1,5 +1,5 @@
1
1
  import { EventEmitter } from "expo-modules-core";
2
- import { RecordingOptions } from "./ExpoAudioStream.types";
2
+ import { AudioStreamResult, RecordingOptions } from "./ExpoAudioStream.types";
3
3
  declare class ExpoAudioStreamWeb extends EventEmitter {
4
4
  mediaRecorder: MediaRecorder | null;
5
5
  audioChunks: Blob[];
@@ -14,11 +14,11 @@ declare class ExpoAudioStreamWeb extends EventEmitter {
14
14
  streamUuid: string | null;
15
15
  constructor();
16
16
  getMediaStream(): Promise<MediaStream>;
17
- startRecording(options?: RecordingOptions): Promise<void>;
17
+ startRecording(options?: RecordingOptions): Promise<string>;
18
18
  setupRecordingListeners(): void;
19
19
  emitAudioEvent(data: Blob): void;
20
20
  generateUUID(): string;
21
- stopRecording(): Promise<number>;
21
+ stopRecording(): Promise<AudioStreamResult | null>;
22
22
  pauseRecording(): Promise<void>;
23
23
  status(): {
24
24
  isRecording: boolean;
@@ -1 +1 @@
1
- {"version":3,"file":"ExpoAudioStreamModule.web.d.ts","sourceRoot":"","sources":["../src/ExpoAudioStreamModule.web.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,YAAY,EAAE,MAAM,mBAAmB,CAAC;AACjD,OAAO,EAAqB,gBAAgB,EAAE,MAAM,yBAAyB,CAAC;AAE9E,cAAM,kBAAmB,SAAQ,YAAY;IACzC,aAAa,EAAE,aAAa,GAAG,IAAI,CAAC;IACpC,WAAW,EAAE,IAAI,EAAE,CAAC;IACpB,WAAW,EAAE,OAAO,CAAC;IACrB,QAAQ,EAAE,OAAO,CAAC;IAClB,kBAAkB,EAAE,MAAM,CAAC;IAC3B,UAAU,EAAE,MAAM,CAAC;IACnB,eAAe,EAAE,MAAM,CAAC;IACxB,WAAW,EAAE,MAAM,CAAC;IACpB,eAAe,EAAE,MAAM,CAAC;IACxB,eAAe,EAAE,MAAM,CAAC;IACxB,UAAU,EAAE,MAAM,GAAG,IAAI,CAAC;;IA4BpB,cAAc;IAUd,cAAc,CAAC,OAAO,GAAE,gBAAqB;IAiBnD,uBAAuB;IA0BvB,cAAc,CAAC,IAAI,EAAE,IAAI;IAexB,YAAY;IASP,aAAa;IASb,cAAc;IAcpB,MAAM;;;;;;;IAUN,cAAc;IAId,eAAe;CAGlB;;AAED,wBAAwC"}
1
+ {"version":3,"file":"ExpoAudioStreamModule.web.d.ts","sourceRoot":"","sources":["../src/ExpoAudioStreamModule.web.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,YAAY,EAAE,MAAM,mBAAmB,CAAC;AACjD,OAAO,EAAqB,iBAAiB,EAAE,gBAAgB,EAAE,MAAM,yBAAyB,CAAC;AAIjG,cAAM,kBAAmB,SAAQ,YAAY;IACzC,aAAa,EAAE,aAAa,GAAG,IAAI,CAAC;IACpC,WAAW,EAAE,IAAI,EAAE,CAAC;IACpB,WAAW,EAAE,OAAO,CAAC;IACrB,QAAQ,EAAE,OAAO,CAAC;IAClB,kBAAkB,EAAE,MAAM,CAAC;IAC3B,UAAU,EAAE,MAAM,CAAC;IACnB,eAAe,EAAE,MAAM,CAAC;IACxB,WAAW,EAAE,MAAM,CAAC;IACpB,eAAe,EAAE,MAAM,CAAC;IACxB,eAAe,EAAE,MAAM,CAAC;IACxB,UAAU,EAAE,MAAM,GAAG,IAAI,CAAC;;IA4BpB,cAAc;IAUd,cAAc,CAAC,OAAO,GAAE,gBAAqB;IAmBnD,uBAAuB;IA0BvB,cAAc,CAAC,IAAI,EAAE,IAAI;IAgBxB,YAAY;IASP,aAAa,IAAI,OAAO,CAAC,iBAAiB,GAAG,IAAI,CAAC;IAelD,cAAc;IAcpB,MAAM;;;;;;;IAUN,cAAc;IAId,eAAe;CAGlB;;AAED,wBAAwC"}
@@ -1,4 +1,6 @@
1
1
  import { EventEmitter } from "expo-modules-core";
2
+ import debug from 'debug';
3
+ const log = debug("expo-audio-stream:useAudioRecording");
2
4
  class ExpoAudioStreamWeb extends EventEmitter {
3
5
  mediaRecorder;
4
6
  audioChunks;
@@ -14,10 +16,10 @@ class ExpoAudioStreamWeb extends EventEmitter {
14
16
  constructor() {
15
17
  const mockNativeModule = {
16
18
  addListener: (eventName) => {
17
- console.log(`Web addListener called for ${eventName}`);
19
+ // Not used on web
18
20
  },
19
21
  removeListeners: (count) => {
20
- console.log(`Web removeListeners called, count: ${count}`);
22
+ // Not used on web
21
23
  }
22
24
  };
23
25
  super(mockNativeModule); // Pass the mock native module to the parent class
@@ -57,6 +59,8 @@ class ExpoAudioStreamWeb extends EventEmitter {
57
59
  this.pausedTime = 0;
58
60
  this.lastEmittedSize = 0;
59
61
  this.streamUuid = this.generateUUID(); // Generate a UUID for the new recording session
62
+ const fileUri = `${this.streamUuid}.webm`;
63
+ return fileUri;
60
64
  }
61
65
  // Setup listeners for the MediaRecorder
62
66
  setupRecordingListeners() {
@@ -71,7 +75,7 @@ class ExpoAudioStreamWeb extends EventEmitter {
71
75
  };
72
76
  this.mediaRecorder.onstop = () => {
73
77
  this.isRecording = false;
74
- console.log('Recording stopped', this.audioChunks);
78
+ log('Recording stopped', this.audioChunks);
75
79
  };
76
80
  this.mediaRecorder.onpause = () => {
77
81
  this.isPaused = true;
@@ -82,9 +86,10 @@ class ExpoAudioStreamWeb extends EventEmitter {
82
86
  };
83
87
  }
84
88
  emitAudioEvent(data) {
85
- const fileUri = `${this.streamUuid}.pcm`;
89
+ const fileUri = `${this.streamUuid}.webm`;
86
90
  const audioEventPayload = {
87
91
  fileUri: fileUri,
92
+ mimeType: 'audio/webm',
88
93
  from: this.lastEmittedSize, // Since this might be continuously streaming, adjust accordingly
89
94
  deltaSize: data.size,
90
95
  totalSize: this.currentSize,
@@ -103,11 +108,16 @@ class ExpoAudioStreamWeb extends EventEmitter {
103
108
  }
104
109
  // Stop recording
105
110
  async stopRecording() {
106
- console.debug('Stopping recording', this);
107
111
  this.mediaRecorder?.stop();
108
112
  this.isRecording = false;
109
113
  this.currentDuration = (Date.now() - this.recordingStartTime) / 1000;
110
- return this.currentDuration;
114
+ const result = {
115
+ fileUri: `${this.streamUuid}.webm`,
116
+ duration: this.currentDuration,
117
+ size: this.currentSize,
118
+ mimeType: 'audio/webm',
119
+ };
120
+ return result;
111
121
  }
112
122
  // Pause recording
113
123
  async pauseRecording() {
@@ -1 +1 @@
1
- {"version":3,"file":"ExpoAudioStreamModule.web.js","sourceRoot":"","sources":["../src/ExpoAudioStreamModule.web.ts"],"names":[],"mappings":"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","sourcesContent":["import { EventEmitter } from \"expo-modules-core\";\nimport { AudioEventPayload, RecordingOptions } from \"./ExpoAudioStream.types\";\n\nclass ExpoAudioStreamWeb extends EventEmitter {\n mediaRecorder: MediaRecorder | null;\n audioChunks: Blob[];\n isRecording: boolean;\n isPaused: boolean;\n recordingStartTime: number;\n pausedTime: number;\n currentDuration: number;\n currentSize: number;\n currentInterval: number;\n lastEmittedSize: number;\n streamUuid: string | null;\n\n constructor() {\n const mockNativeModule = {\n addListener: (eventName: string) => {\n console.log(`Web addListener called for ${eventName}`);\n },\n removeListeners: (count: number) => {\n console.log(`Web removeListeners called, count: ${count}`);\n }\n };\n super(mockNativeModule); // Pass the mock native module to the parent class\n\n\n this.mediaRecorder = null;\n this.audioChunks = [];\n this.isRecording = false;\n this.isPaused = false;\n this.recordingStartTime = 0;\n this.pausedTime = 0;\n this.currentDuration = 0;\n this.currentSize = 0;\n this.currentInterval = 1000; // Default interval in ms\n this.lastEmittedSize = 0;\n this.streamUuid = null; // Initialize UUID on first recording start\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(options: RecordingOptions = {}) {\n if (this.isRecording) {\n throw new Error('Recording is already in progress');\n }\n\n const stream = await this.getMediaStream();\n this.mediaRecorder = new MediaRecorder(stream);\n this.setupRecordingListeners();\n this.mediaRecorder.start(options.interval || this.currentInterval);\n this.isRecording = true;\n this.recordingStartTime = Date.now();\n this.pausedTime = 0;\n this.lastEmittedSize = 0;\n this.streamUuid = this.generateUUID(); // Generate a UUID for the new recording session\n }\n\n // Setup listeners for the MediaRecorder\n setupRecordingListeners() {\n if (!this.mediaRecorder) {\n throw new Error('No active media recorder');\n }\n this.mediaRecorder.ondataavailable = (event) => {\n this.audioChunks.push(event.data);\n this.currentSize += event.data.size; // Update the size of the recording\n this.emitAudioEvent(event.data); // Emit the event with the correct payload\n this.lastEmittedSize = this.currentSize;\n };\n\n this.mediaRecorder.onstop = () => {\n this.isRecording = false;\n console.log('Recording stopped', this.audioChunks);\n };\n\n this.mediaRecorder.onpause = () => {\n this.isPaused = true;\n };\n\n this.mediaRecorder.onresume = () => {\n this.isPaused = false;\n this.recordingStartTime += (Date.now() - this.pausedTime); // Adjust start time after resuming\n };\n }\n\n emitAudioEvent(data: Blob) {\n const fileUri = `${this.streamUuid}.pcm`;\n const audioEventPayload: AudioEventPayload = {\n fileUri: fileUri,\n from: this.lastEmittedSize, // Since this might be continuously streaming, adjust accordingly\n deltaSize: data.size,\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 // Helper method to generate a UUID\n generateUUID() {\n // Implementation of UUID generation (use a library or custom method)\n return 'xxxx-xxxx-xxxx-xxxx'.replace(/[x]/g, (c) => {\n const r = Math.random() * 16 | 0, v = c == 'x' ? r : (r & 0x3 | 0x8);\n return v.toString(16);\n });\n }\n\n // Stop recording\n async stopRecording() {\n console.debug('Stopping recording', this);\n this.mediaRecorder?.stop();\n this.isRecording = false;\n this.currentDuration = (Date.now() - this.recordingStartTime) / 1000;\n return this.currentDuration;\n }\n\n // Pause recording\n async pauseRecording() {\n if (!this.mediaRecorder) {\n throw new Error('No active media recorder');\n }\n\n if (this.isRecording && !this.isPaused) {\n this.mediaRecorder.pause();\n this.pausedTime = Date.now();\n } else {\n throw new Error('Recording is not active or already paused');\n }\n }\n\n // Get current status\n status() {\n return {\n isRecording: this.isRecording,\n isPaused: this.isPaused,\n duration: Date.now() - this.recordingStartTime,\n size: this.currentSize,\n interval: this.currentInterval,\n };\n }\n\n listAudioFiles() {\n // Not applicable on web\n }\n\n clearAudioFiles() {\n // Not applicable on web\n }\n}\n\nexport default new ExpoAudioStreamWeb();"]}
1
+ {"version":3,"file":"ExpoAudioStreamModule.web.js","sourceRoot":"","sources":["../src/ExpoAudioStreamModule.web.ts"],"names":[],"mappings":"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","sourcesContent":["import { EventEmitter } from \"expo-modules-core\";\nimport { AudioEventPayload, AudioStreamResult, RecordingOptions } from \"./ExpoAudioStream.types\";\nimport debug from 'debug';\n\nconst log = debug(\"expo-audio-stream:useAudioRecording\");\nclass ExpoAudioStreamWeb extends EventEmitter {\n mediaRecorder: MediaRecorder | null;\n audioChunks: Blob[];\n isRecording: boolean;\n isPaused: boolean;\n recordingStartTime: number;\n pausedTime: number;\n currentDuration: number;\n currentSize: number;\n currentInterval: number;\n lastEmittedSize: number;\n streamUuid: string | null;\n\n constructor() {\n const mockNativeModule = {\n addListener: (eventName: string) => {\n // Not used on web\n },\n removeListeners: (count: number) => {\n // Not used on web\n }\n };\n super(mockNativeModule); // Pass the mock native module to the parent class\n\n\n this.mediaRecorder = null;\n this.audioChunks = [];\n this.isRecording = false;\n this.isPaused = false;\n this.recordingStartTime = 0;\n this.pausedTime = 0;\n this.currentDuration = 0;\n this.currentSize = 0;\n this.currentInterval = 1000; // Default interval in ms\n this.lastEmittedSize = 0;\n this.streamUuid = null; // Initialize UUID on first recording start\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(options: RecordingOptions = {}) {\n if (this.isRecording) {\n throw new Error('Recording is already in progress');\n }\n\n const stream = await this.getMediaStream();\n this.mediaRecorder = new MediaRecorder(stream);\n this.setupRecordingListeners();\n this.mediaRecorder.start(options.interval || this.currentInterval);\n this.isRecording = true;\n this.recordingStartTime = Date.now();\n this.pausedTime = 0;\n this.lastEmittedSize = 0;\n this.streamUuid = this.generateUUID(); // Generate a UUID for the new recording session\n const fileUri = `${this.streamUuid}.webm`;\n return fileUri;\n }\n\n // Setup listeners for the MediaRecorder\n setupRecordingListeners() {\n if (!this.mediaRecorder) {\n throw new Error('No active media recorder');\n }\n this.mediaRecorder.ondataavailable = (event) => {\n this.audioChunks.push(event.data);\n this.currentSize += event.data.size; // Update the size of the recording\n this.emitAudioEvent(event.data); // Emit the event with the correct payload\n this.lastEmittedSize = this.currentSize;\n };\n\n this.mediaRecorder.onstop = () => {\n this.isRecording = false;\n log('Recording stopped', this.audioChunks);\n };\n\n this.mediaRecorder.onpause = () => {\n this.isPaused = true;\n };\n\n this.mediaRecorder.onresume = () => {\n this.isPaused = false;\n this.recordingStartTime += (Date.now() - this.pausedTime); // Adjust start time after resuming\n };\n }\n\n emitAudioEvent(data: Blob) {\n const fileUri = `${this.streamUuid}.webm`;\n const audioEventPayload: AudioEventPayload = {\n fileUri: fileUri,\n mimeType: 'audio/webm',\n from: this.lastEmittedSize, // Since this might be continuously streaming, adjust accordingly\n deltaSize: data.size,\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 // Helper method to generate a UUID\n generateUUID() {\n // Implementation of UUID generation (use a library or custom method)\n return 'xxxx-xxxx-xxxx-xxxx'.replace(/[x]/g, (c) => {\n const r = Math.random() * 16 | 0, v = c == 'x' ? r : (r & 0x3 | 0x8);\n return v.toString(16);\n });\n }\n\n // Stop recording\n async stopRecording(): Promise<AudioStreamResult | null> {\n this.mediaRecorder?.stop();\n this.isRecording = false;\n this.currentDuration = (Date.now() - this.recordingStartTime) / 1000;\n const result: AudioStreamResult = {\n fileUri: `${this.streamUuid}.webm`,\n duration: this.currentDuration,\n size: this.currentSize,\n mimeType: 'audio/webm',\n }\n\n return result;\n }\n\n // Pause recording\n async pauseRecording() {\n if (!this.mediaRecorder) {\n throw new Error('No active media recorder');\n }\n\n if (this.isRecording && !this.isPaused) {\n this.mediaRecorder.pause();\n this.pausedTime = Date.now();\n } else {\n throw new Error('Recording is not active or already paused');\n }\n }\n\n // Get current status\n status() {\n return {\n isRecording: this.isRecording,\n isPaused: this.isPaused,\n duration: Date.now() - this.recordingStartTime,\n size: this.currentSize,\n interval: this.currentInterval,\n };\n }\n\n listAudioFiles() {\n // Not applicable on web\n }\n\n clearAudioFiles() {\n // Not applicable on web\n }\n}\n\nexport default new ExpoAudioStreamWeb();"]}
package/build/index.d.ts CHANGED
@@ -1,9 +1,8 @@
1
1
  import { type Subscription } from 'expo-modules-core';
2
2
  import { AudioEventPayload } from './ExpoAudioStream.types';
3
3
  import { useAudioRecorder } from './useAudioRecording';
4
- export declare function getRecordingDuration(): Promise<number>;
5
4
  export declare function listAudioFiles(): Promise<string[]>;
6
5
  export declare function clearAudioFiles(): Promise<void>;
7
- export declare function addChangeListener(listener: (event: AudioEventPayload) => void): Subscription;
6
+ export declare function addAudioEventListener(listener: (event: AudioEventPayload) => void): Subscription;
8
7
  export { AudioEventPayload, useAudioRecorder };
9
8
  //# sourceMappingURL=index.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAAoC,KAAK,YAAY,EAAE,MAAM,mBAAmB,CAAC;AAKxF,OAAO,EAAE,iBAAiB,EAAoB,MAAM,yBAAyB,CAAC;AAC9E,OAAO,EAAE,gBAAgB,EAAE,MAAM,qBAAqB,CAAC;AAKvD,wBAAgB,oBAAoB,IAAI,OAAO,CAAC,MAAM,CAAC,CAEtD;AAED,wBAAgB,cAAc,IAAI,OAAO,CAAC,MAAM,EAAE,CAAC,CAElD;AAED,wBAAgB,eAAe,IAAI,OAAO,CAAC,IAAI,CAAC,CAE/C;AAED,wBAAgB,iBAAiB,CAAC,QAAQ,EAAE,CAAC,KAAK,EAAE,iBAAiB,KAAK,IAAI,GAAG,YAAY,CAE5F;AAGD,OAAO,EAAE,iBAAiB,EAAE,gBAAgB,EAAE,CAAC"}
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAAoC,KAAK,YAAY,EAAE,MAAM,mBAAmB,CAAC;AAKxF,OAAO,EAAE,iBAAiB,EAAoB,MAAM,yBAAyB,CAAC;AAC9E,OAAO,EAAE,gBAAgB,EAAE,MAAM,qBAAqB,CAAC;AAIvD,wBAAgB,cAAc,IAAI,OAAO,CAAC,MAAM,EAAE,CAAC,CAElD;AAED,wBAAgB,eAAe,IAAI,OAAO,CAAC,IAAI,CAAC,CAE/C;AAED,wBAAgB,qBAAqB,CAAC,QAAQ,EAAE,CAAC,KAAK,EAAE,iBAAiB,KAAK,IAAI,GAAG,YAAY,CAEhG;AAED,OAAO,EAAE,iBAAiB,EAAE,gBAAgB,EAAE,CAAC"}
package/build/index.js CHANGED
@@ -4,17 +4,13 @@ import { NativeModulesProxy, EventEmitter } from 'expo-modules-core';
4
4
  import ExpoAudioStreamModule from './ExpoAudioStreamModule';
5
5
  import { useAudioRecorder } from './useAudioRecording';
6
6
  const emitter = new EventEmitter(ExpoAudioStreamModule ?? NativeModulesProxy.ExpoAudioStream);
7
- // Function to get the recording duration
8
- export function getRecordingDuration() {
9
- return ExpoAudioStreamModule.getRecordingDuration();
10
- }
11
7
  export function listAudioFiles() {
12
8
  return ExpoAudioStreamModule.listAudioFiles();
13
9
  }
14
10
  export function clearAudioFiles() {
15
11
  return ExpoAudioStreamModule.clearAudioFiles();
16
12
  }
17
- export function addChangeListener(listener) {
13
+ export function addAudioEventListener(listener) {
18
14
  return emitter.addListener('AudioData', listener);
19
15
  }
20
16
  export { useAudioRecorder };
@@ -1 +1 @@
1
- {"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,kBAAkB,EAAE,YAAY,EAAqB,MAAM,mBAAmB,CAAC;AAExF,kFAAkF;AAClF,gDAAgD;AAChD,OAAO,qBAAqB,MAAM,yBAAyB,CAAC;AAE5D,OAAO,EAAE,gBAAgB,EAAE,MAAM,qBAAqB,CAAC;AAEvD,MAAM,OAAO,GAAG,IAAI,YAAY,CAAC,qBAAqB,IAAI,kBAAkB,CAAC,eAAe,CAAC,CAAC;AAE9F,yCAAyC;AACzC,MAAM,UAAU,oBAAoB;IAClC,OAAO,qBAAqB,CAAC,oBAAoB,EAAE,CAAC;AACtD,CAAC;AAED,MAAM,UAAU,cAAc;IAC5B,OAAO,qBAAqB,CAAC,cAAc,EAAE,CAAC;AAChD,CAAC;AAED,MAAM,UAAU,eAAe;IAC7B,OAAO,qBAAqB,CAAC,eAAe,EAAE,CAAC;AACjD,CAAC;AAED,MAAM,UAAU,iBAAiB,CAAC,QAA4C;IAC5E,OAAO,OAAO,CAAC,WAAW,CAAoB,WAAW,EAAE,QAAQ,CAAC,CAAC;AACvE,CAAC;AAGD,OAAO,EAAqB,gBAAgB,EAAE,CAAC","sourcesContent":["import { NativeModulesProxy, EventEmitter, type Subscription } from 'expo-modules-core';\n\n// Import the native module. On web, it will be resolved to ExpoAudioStream.web.ts\n// and on native platforms to ExpoAudioStream.ts\nimport ExpoAudioStreamModule from './ExpoAudioStreamModule';\nimport { AudioEventPayload, RecordingOptions } from './ExpoAudioStream.types';\nimport { useAudioRecorder } from './useAudioRecording';\n\nconst emitter = new EventEmitter(ExpoAudioStreamModule ?? NativeModulesProxy.ExpoAudioStream);\n\n// Function to get the recording duration\nexport function getRecordingDuration(): Promise<number> {\n return ExpoAudioStreamModule.getRecordingDuration();\n}\n\nexport function listAudioFiles(): Promise<string[]> {\n return ExpoAudioStreamModule.listAudioFiles();\n}\n\nexport function clearAudioFiles(): Promise<void> {\n return ExpoAudioStreamModule.clearAudioFiles();\n}\n\nexport function addChangeListener(listener: (event: AudioEventPayload) => void): Subscription {\n return emitter.addListener<AudioEventPayload>('AudioData', listener);\n}\n\n\nexport { AudioEventPayload, useAudioRecorder };\n"]}
1
+ {"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,kBAAkB,EAAE,YAAY,EAAqB,MAAM,mBAAmB,CAAC;AAExF,kFAAkF;AAClF,gDAAgD;AAChD,OAAO,qBAAqB,MAAM,yBAAyB,CAAC;AAE5D,OAAO,EAAE,gBAAgB,EAAE,MAAM,qBAAqB,CAAC;AAEvD,MAAM,OAAO,GAAG,IAAI,YAAY,CAAC,qBAAqB,IAAI,kBAAkB,CAAC,eAAe,CAAC,CAAC;AAE9F,MAAM,UAAU,cAAc;IAC5B,OAAO,qBAAqB,CAAC,cAAc,EAAE,CAAC;AAChD,CAAC;AAED,MAAM,UAAU,eAAe;IAC7B,OAAO,qBAAqB,CAAC,eAAe,EAAE,CAAC;AACjD,CAAC;AAED,MAAM,UAAU,qBAAqB,CAAC,QAA4C;IAChF,OAAO,OAAO,CAAC,WAAW,CAAoB,WAAW,EAAE,QAAQ,CAAC,CAAC;AACvE,CAAC;AAED,OAAO,EAAqB,gBAAgB,EAAE,CAAC","sourcesContent":["import { NativeModulesProxy, EventEmitter, type Subscription } from 'expo-modules-core';\n\n// Import the native module. On web, it will be resolved to ExpoAudioStream.web.ts\n// and on native platforms to ExpoAudioStream.ts\nimport ExpoAudioStreamModule from './ExpoAudioStreamModule';\nimport { AudioEventPayload, RecordingOptions } from './ExpoAudioStream.types';\nimport { useAudioRecorder } from './useAudioRecording';\n\nconst emitter = new EventEmitter(ExpoAudioStreamModule ?? NativeModulesProxy.ExpoAudioStream);\n\nexport function listAudioFiles(): Promise<string[]> {\n return ExpoAudioStreamModule.listAudioFiles();\n}\n\nexport function clearAudioFiles(): Promise<void> {\n return ExpoAudioStreamModule.clearAudioFiles();\n}\n\nexport function addAudioEventListener(listener: (event: AudioEventPayload) => void): Subscription {\n return emitter.addListener<AudioEventPayload>('AudioData', listener);\n}\n\nexport { AudioEventPayload, useAudioRecorder };\n"]}