@siteed/expo-audio-studio 2.4.1 → 2.5.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.
Files changed (80) hide show
  1. package/CHANGELOG.md +10 -1
  2. package/README.md +25 -0
  3. package/android/src/main/java/net/siteed/audiostream/AudioAnalysisData.kt +22 -0
  4. package/android/src/main/java/net/siteed/audiostream/AudioDeviceManager.kt +1501 -0
  5. package/android/src/main/java/net/siteed/audiostream/AudioFileHandler.kt +10 -5
  6. package/android/src/main/java/net/siteed/audiostream/AudioNotificationsManager.kt +27 -25
  7. package/android/src/main/java/net/siteed/audiostream/AudioProcessor.kt +73 -71
  8. package/android/src/main/java/net/siteed/audiostream/AudioRecorderManager.kt +576 -252
  9. package/android/src/main/java/net/siteed/audiostream/Constants.kt +17 -1
  10. package/android/src/main/java/net/siteed/audiostream/ExpoAudioStreamModule.kt +419 -155
  11. package/android/src/main/java/net/siteed/audiostream/LogUtils.kt +65 -0
  12. package/android/src/main/java/net/siteed/audiostream/RecordingConfig.kt +9 -1
  13. package/build/AudioAnalysis/AudioAnalysis.types.js.map +1 -1
  14. package/build/AudioDeviceManager.d.ts +107 -0
  15. package/build/AudioDeviceManager.d.ts.map +1 -0
  16. package/build/AudioDeviceManager.js +493 -0
  17. package/build/AudioDeviceManager.js.map +1 -0
  18. package/build/AudioRecorder.provider.d.ts.map +1 -1
  19. package/build/AudioRecorder.provider.js +3 -0
  20. package/build/AudioRecorder.provider.js.map +1 -1
  21. package/build/ExpoAudioStream.types.d.ts +90 -1
  22. package/build/ExpoAudioStream.types.d.ts.map +1 -1
  23. package/build/ExpoAudioStream.types.js +7 -1
  24. package/build/ExpoAudioStream.types.js.map +1 -1
  25. package/build/ExpoAudioStream.web.d.ts +37 -0
  26. package/build/ExpoAudioStream.web.d.ts.map +1 -1
  27. package/build/ExpoAudioStream.web.js +399 -54
  28. package/build/ExpoAudioStream.web.js.map +1 -1
  29. package/build/ExpoAudioStreamModule.d.ts.map +1 -1
  30. package/build/ExpoAudioStreamModule.js +20 -0
  31. package/build/ExpoAudioStreamModule.js.map +1 -1
  32. package/build/WebRecorder.web.d.ts +63 -10
  33. package/build/WebRecorder.web.d.ts.map +1 -1
  34. package/build/WebRecorder.web.js +277 -68
  35. package/build/WebRecorder.web.js.map +1 -1
  36. package/build/hooks/useAudioDevices.d.ts +14 -0
  37. package/build/hooks/useAudioDevices.d.ts.map +1 -0
  38. package/build/hooks/useAudioDevices.js +151 -0
  39. package/build/hooks/useAudioDevices.js.map +1 -0
  40. package/build/index.d.ts +2 -0
  41. package/build/index.d.ts.map +1 -1
  42. package/build/index.js +4 -0
  43. package/build/index.js.map +1 -1
  44. package/build/useAudioRecorder.d.ts +1 -0
  45. package/build/useAudioRecorder.d.ts.map +1 -1
  46. package/build/useAudioRecorder.js +20 -1
  47. package/build/useAudioRecorder.js.map +1 -1
  48. package/build/utils/BlobFix.d.ts.map +1 -1
  49. package/build/utils/BlobFix.js +2 -2
  50. package/build/utils/BlobFix.js.map +1 -1
  51. package/build/workers/InlineFeaturesExtractor.web.d.ts +1 -1
  52. package/build/workers/InlineFeaturesExtractor.web.d.ts.map +1 -1
  53. package/build/workers/InlineFeaturesExtractor.web.js +27 -26
  54. package/build/workers/InlineFeaturesExtractor.web.js.map +1 -1
  55. package/build/workers/inlineAudioWebWorker.web.d.ts +1 -1
  56. package/build/workers/inlineAudioWebWorker.web.d.ts.map +1 -1
  57. package/build/workers/inlineAudioWebWorker.web.js +25 -1
  58. package/build/workers/inlineAudioWebWorker.web.js.map +1 -1
  59. package/ios/AudioDeviceManager.swift +654 -0
  60. package/ios/AudioStreamManager.swift +964 -760
  61. package/ios/ExpoAudioStreamModule.swift +174 -19
  62. package/ios/Features.swift +1 -1
  63. package/ios/ISSUE_IOS.md +45 -0
  64. package/ios/Logger.swift +13 -1
  65. package/ios/RecordingSettings.swift +12 -0
  66. package/package.json +2 -2
  67. package/src/AudioAnalysis/AudioAnalysis.types.ts +2 -2
  68. package/src/AudioDeviceManager.ts +571 -0
  69. package/src/AudioRecorder.provider.tsx +3 -0
  70. package/src/ExpoAudioStream.types.ts +97 -1
  71. package/src/ExpoAudioStream.web.ts +513 -63
  72. package/src/ExpoAudioStreamModule.ts +23 -0
  73. package/src/WebRecorder.web.ts +346 -81
  74. package/src/hooks/useAudioDevices.ts +180 -0
  75. package/src/index.ts +6 -0
  76. package/src/types/crc-32.d.ts +6 -6
  77. package/src/useAudioRecorder.tsx +27 -1
  78. package/src/utils/BlobFix.ts +6 -4
  79. package/src/workers/InlineFeaturesExtractor.web.tsx +27 -26
  80. package/src/workers/inlineAudioWebWorker.web.tsx +25 -1
@@ -0,0 +1,180 @@
1
+ import { useCallback, useEffect, useState } from 'react'
2
+
3
+ import { audioDeviceManager } from '../AudioDeviceManager'
4
+ import { AudioDevice } from '../ExpoAudioStream.types'
5
+
6
+ /**
7
+ * React hook for managing audio input devices
8
+ */
9
+ export function useAudioDevices() {
10
+ const [devices, setDevices] = useState<AudioDevice[]>([])
11
+ const [currentDevice, setCurrentDevice] = useState<AudioDevice | null>(null)
12
+ const [loading, setLoading] = useState(true)
13
+ const [error, setError] = useState<Error | null>(null)
14
+
15
+ // Load devices on mount
16
+ useEffect(() => {
17
+ let isMounted = true
18
+
19
+ const loadDevices = async () => {
20
+ try {
21
+ setLoading(true)
22
+ setError(null)
23
+
24
+ // Load available devices
25
+ const availableDevices =
26
+ await audioDeviceManager.getAvailableDevices()
27
+ if (isMounted) setDevices(availableDevices)
28
+
29
+ // Get current device
30
+ const device = await audioDeviceManager.getCurrentDevice()
31
+ if (isMounted) setCurrentDevice(device)
32
+ } catch (err) {
33
+ console.error('Failed to load audio devices:', err)
34
+ if (isMounted)
35
+ setError(
36
+ err instanceof Error
37
+ ? err
38
+ : new Error('Failed to load audio devices')
39
+ )
40
+ } finally {
41
+ if (isMounted) setLoading(false)
42
+ }
43
+ }
44
+
45
+ loadDevices()
46
+
47
+ // Set up device change listener
48
+ const removeListener = audioDeviceManager.addDeviceChangeListener(
49
+ (updatedDevices: AudioDevice[]) => {
50
+ if (isMounted) {
51
+ setDevices(updatedDevices)
52
+
53
+ // If our current device is no longer available, update it
54
+ if (
55
+ currentDevice &&
56
+ !updatedDevices.some(
57
+ (d: AudioDevice) => d.id === currentDevice.id
58
+ )
59
+ ) {
60
+ audioDeviceManager
61
+ .getCurrentDevice()
62
+ .then((newDevice: AudioDevice | null) => {
63
+ if (isMounted) setCurrentDevice(newDevice)
64
+ })
65
+ }
66
+ }
67
+ }
68
+ )
69
+
70
+ return () => {
71
+ isMounted = false
72
+ removeListener()
73
+ }
74
+ }, [])
75
+
76
+ /**
77
+ * Select a specific audio input device
78
+ * @param deviceId The ID of the device to select
79
+ * @returns Promise resolving to a boolean indicating success
80
+ */
81
+ const selectDevice = useCallback(
82
+ async (deviceId: string): Promise<boolean> => {
83
+ try {
84
+ setLoading(true)
85
+ setError(null)
86
+
87
+ const success = await audioDeviceManager.selectDevice(deviceId)
88
+
89
+ if (success) {
90
+ // Get the updated current device after selection
91
+ const device = await audioDeviceManager.getCurrentDevice()
92
+ setCurrentDevice(device)
93
+ }
94
+
95
+ return success
96
+ } catch (err) {
97
+ console.error('Failed to select audio device:', err)
98
+ setError(
99
+ err instanceof Error
100
+ ? err
101
+ : new Error('Failed to select audio device')
102
+ )
103
+ return false
104
+ } finally {
105
+ setLoading(false)
106
+ }
107
+ },
108
+ []
109
+ )
110
+
111
+ /**
112
+ * Reset to the default audio input device
113
+ * @returns Promise resolving to a boolean indicating success
114
+ */
115
+ const resetToDefaultDevice = useCallback(async (): Promise<boolean> => {
116
+ try {
117
+ setLoading(true)
118
+ setError(null)
119
+
120
+ const success = await audioDeviceManager.resetToDefaultDevice()
121
+
122
+ if (success) {
123
+ // Get the updated current device after reset
124
+ const device = await audioDeviceManager.getCurrentDevice()
125
+ setCurrentDevice(device)
126
+ }
127
+
128
+ return success
129
+ } catch (err) {
130
+ console.error('Failed to reset to default audio device:', err)
131
+ setError(
132
+ err instanceof Error
133
+ ? err
134
+ : new Error('Failed to reset to default audio device')
135
+ )
136
+ return false
137
+ } finally {
138
+ setLoading(false)
139
+ }
140
+ }, [])
141
+
142
+ /**
143
+ * Refresh the list of available devices
144
+ */
145
+ const refreshDevices = useCallback(async (): Promise<AudioDevice[]> => {
146
+ try {
147
+ setLoading(true)
148
+ setError(null)
149
+
150
+ const updatedDevices = await audioDeviceManager.refreshDevices()
151
+ setDevices(updatedDevices)
152
+
153
+ // Also refresh the current device
154
+ const device = await audioDeviceManager.getCurrentDevice()
155
+ setCurrentDevice(device)
156
+
157
+ return updatedDevices
158
+ } catch (err) {
159
+ console.error('Failed to refresh audio devices:', err)
160
+ setError(
161
+ err instanceof Error
162
+ ? err
163
+ : new Error('Failed to refresh audio devices')
164
+ )
165
+ return []
166
+ } finally {
167
+ setLoading(false)
168
+ }
169
+ }, [])
170
+
171
+ return {
172
+ devices,
173
+ currentDevice,
174
+ loading,
175
+ error,
176
+ selectDevice,
177
+ resetToDefaultDevice,
178
+ refreshDevices,
179
+ }
180
+ }
package/src/index.ts CHANGED
@@ -19,6 +19,12 @@ export * from './utils/convertPCMToFloat32'
19
19
  export * from './utils/getWavFileInfo'
20
20
  export * from './utils/writeWavHeader'
21
21
 
22
+ // Export AudioDeviceManager
23
+ export { AudioDeviceManager, audioDeviceManager } from './AudioDeviceManager'
24
+
25
+ // Export useAudioDevices hook
26
+ export { useAudioDevices } from './hooks/useAudioDevices'
27
+
22
28
  export {
23
29
  AudioRecorderProvider,
24
30
  ExpoAudioStreamModule,
@@ -1,9 +1,9 @@
1
1
  declare module 'crc-32' {
2
2
  interface CRC32 {
3
- (data: string | Uint8Array): number;
4
- buf(data: Uint8Array): number;
3
+ (data: string | Uint8Array): number
4
+ buf(data: Uint8Array): number
5
5
  }
6
-
7
- const crc32: CRC32;
8
- export default crc32;
9
- }
6
+
7
+ const crc32: CRC32
8
+ export default crc32
9
+ }
@@ -27,6 +27,7 @@ export interface UseAudioRecorderProps {
27
27
  }
28
28
 
29
29
  export interface UseAudioRecorderState {
30
+ prepareRecording: (_: RecordingConfig) => Promise<void>
30
31
  startRecording: (_: RecordingConfig) => Promise<StartRecordingResult>
31
32
  stopRecording: () => Promise<AudioRecording>
32
33
  pauseRecording: () => Promise<void>
@@ -280,7 +281,7 @@ export function useAudioRecorder({
280
281
 
281
282
  logger?.debug(
282
283
  `[handleAudioAnalysis] Updated analysis data: durationMs=${savedAnalysisData.durationMs}`,
283
- savedAnalysisData
284
+ { dataPoints: savedAnalysisData.dataPoints.length }
284
285
  )
285
286
 
286
287
  // Call the onAudioAnalysis callback if it exists in the recording config
@@ -511,6 +512,30 @@ export function useAudioRecorder({
511
512
  [handleAudioAnalysis, dispatch]
512
513
  )
513
514
 
515
+ const prepareRecording = useCallback(
516
+ async (recordingOptions: RecordingConfig) => {
517
+ recordingConfigRef.current = recordingOptions
518
+ logger?.debug(`preparing recording`, recordingOptions)
519
+
520
+ analysisRef.current = { ...defaultAnalysis } // Reset analysis data
521
+ fullAnalysisRef.current = { ...defaultAnalysis }
522
+ const { onAudioStream, ...options } = recordingOptions
523
+
524
+ // Store onAudioStream for later use when recording starts
525
+ if (typeof onAudioStream === 'function') {
526
+ onAudioStreamRef.current = onAudioStream
527
+ } else {
528
+ logger?.warn(`onAudioStream is not a function`, onAudioStream)
529
+ onAudioStreamRef.current = null
530
+ }
531
+
532
+ // Call the native prepareRecording method
533
+ await ExpoAudioStream.prepareRecording(options)
534
+ logger?.debug(`recording prepared successfully`)
535
+ },
536
+ []
537
+ )
538
+
514
539
  const stopRecording = useCallback(async () => {
515
540
  logger?.debug(`stoping recording`)
516
541
 
@@ -606,6 +631,7 @@ export function useAudioRecorder({
606
631
  }, []) // Empty dependency array since we want this to run once
607
632
 
608
633
  return {
634
+ prepareRecording,
609
635
  startRecording,
610
636
  stopRecording,
611
637
  pauseRecording,
@@ -249,11 +249,13 @@ const sections: Record<number, Section> = {
249
249
  class WebmBase<T> {
250
250
  source?: Uint8Array
251
251
  data?: T
252
+ name: string
253
+ type: string
252
254
 
253
- constructor(
254
- private name = 'Unknown',
255
- private type = 'Unknown'
256
- ) {}
255
+ constructor(name = 'Unknown', type = 'Unknown') {
256
+ this.name = name
257
+ this.type = type
258
+ }
257
259
 
258
260
  updateBySource() {}
259
261
 
@@ -440,16 +440,36 @@ function estimatePitch(segment, sampleRate) {
440
440
  }
441
441
  }
442
442
 
443
- // Unique ID counter
443
+ // Unique ID counter - the only state we need to maintain
444
444
  let uniqueIdCounter = 0
445
- let accumulatedDataPoints = []
446
445
  let lastEmitTime = Date.now()
447
446
 
448
447
  self.onmessage = function (event) {
448
+ // Extract enableLogging early so we can use it consistently
449
+ const enableLogging = event.data.enableLogging || false;
450
+
451
+ // Create consistent logger that only logs when enabled
452
+ const logger = enableLogging ? {
453
+ debug: (...args) => console.debug('[Worker]', ...args),
454
+ log: (...args) => console.log('[Worker]', ...args),
455
+ warn: (...args) => console.warn('[Worker]', ...args),
456
+ error: (...args) => console.error('[Worker]', ...args)
457
+ } : {
458
+ debug: () => {},
459
+ log: () => {},
460
+ warn: () => {},
461
+ error: () => {}
462
+ };
463
+
449
464
  // Check if this is a reset command
450
465
  if (event.data.command === 'resetCounter') {
451
- uniqueIdCounter = event.data.startCounterFrom || 0;
452
- console.log('[Worker] Reset counter to', uniqueIdCounter);
466
+ const newValue = event.data.value;
467
+ logger.log('Reset counter request received with value:', newValue);
468
+
469
+ // Always respect explicit resets through the resetCounter command
470
+ uniqueIdCounter = typeof newValue === 'number' ? newValue : 0;
471
+ logger.log('Counter explicitly set to:', uniqueIdCounter);
472
+
453
473
  return; // Exit early, don't process audio
454
474
  }
455
475
 
@@ -464,34 +484,13 @@ self.onmessage = function (event) {
464
484
  numberOfChannels,
465
485
  features: _features,
466
486
  intervalAnalysis = 500,
467
- enableLogging,
468
- resetCounter,
469
- startCounterFrom,
470
487
  } = event.data
471
488
 
472
- // Also handle reset as part of regular message
473
- if (resetCounter) {
474
- uniqueIdCounter = startCounterFrom || 0;
475
- }
476
-
477
489
  // Calculate subChunkStartTime safely, defaulting to 0 if fullAudioDurationMs is not a valid number
478
490
  const subChunkStartTime = (typeof fullAudioDurationMs === 'number' && !isNaN(fullAudioDurationMs) && fullAudioDurationMs >= 0)
479
491
  ? fullAudioDurationMs / 1000
480
492
  : 0;
481
493
 
482
-
483
- // Create a simple logger that only logs when enabled
484
- const logger = enableLogging ? {
485
- debug: (...args) => console.debug('[Worker]', ...args),
486
- log: (...args) => console.log('[Worker]', ...args),
487
- error: (...args) => console.error('[Worker]', ...args)
488
- } : {
489
- debug: () => {},
490
- log: () => {},
491
- error: () => {}
492
- }
493
- logger.log('[Worker] START Feature Extractor - hasData: ' + (event.data ? true : false) + ', channelData: ' + (event.data.channelData ? event.data.channelData.length : 0) + ', fullAudioDurationMs: ' + (event.data.fullAudioDurationMs || 0) + ', sampleRate: ' + (event.data.sampleRate || 0) + ', segmentDurationMs: ' + (event.data.segmentDurationMs || 0) + ', algorithm: ' + (event.data.algorithm || 'none') + ', bitDepth: ' + (event.data.bitDepth || 0) + ', numberOfChannels: ' + (event.data.numberOfChannels || 0) + ', features: ' + (event.data.features ? Object.keys(event.data.features).length : 0) + ', intervalAnalysis: ' + (event.data.intervalAnalysis || 0) + ', dataKeys: ' + (event.data ? Object.keys(event.data).join(',') : ''));
494
-
495
494
  const features = _features || {}
496
495
  const bytesPerSample = bitDepth / 8; // Calculate bytes per sample
497
496
 
@@ -692,6 +691,7 @@ self.onmessage = function (event) {
692
691
 
693
692
  var spectralFeatures = computeSpectralFeatures(channelData.slice(startIdx, endIdx), sampleRate, features);
694
693
 
694
+ // Simply use the counter, increment after assigning
695
695
  const dataPoint = {
696
696
  id: uniqueIdCounter++,
697
697
  amplitude: maxAmp,
@@ -756,6 +756,7 @@ self.onmessage = function (event) {
756
756
 
757
757
  var spectralFeatures = computeSpectralFeatures(channelData.slice(startIdx, endIdx), sampleRate, features);
758
758
 
759
+ // Simply use the counter, increment after assigning
759
760
  const dataPoint = {
760
761
  id: uniqueIdCounter++,
761
762
  amplitude: maxAmp,
@@ -769,7 +770,7 @@ self.onmessage = function (event) {
769
770
  samples: remainingSamples,
770
771
  }
771
772
 
772
- logger.log('[Worker] extractWaveform - dataPoint', dataPoint)
773
+ logger.debug('extractWaveform - dataPoint', dataPoint);
773
774
  // Extract features if any are requested
774
775
  const extractedFeatures = createFeaturesObject(
775
776
  features,
@@ -17,6 +17,7 @@ class RecorderProcessor extends AudioWorkletProcessor {
17
17
  this.port.onmessage = this.handleMessage.bind(this)
18
18
  this.enableLogging = false
19
19
  this.exportIntervalSamples = 0
20
+ this.currentPosition = 0 // Track current position in seconds
20
21
  }
21
22
 
22
23
  handleMessage(event) {
@@ -36,6 +37,14 @@ class RecorderProcessor extends AudioWorkletProcessor {
36
37
  }
37
38
  this.exportBitDepth =
38
39
  event.data.exportBitDepth || this.recordBitDepth
40
+
41
+ // Handle position parameter for device switching
42
+ if (typeof event.data.position === 'number' && event.data.position > 0) {
43
+ this.currentPosition = event.data.position
44
+ if (this.enableLogging) {
45
+ console.log('AudioWorklet initialized with position:', this.currentPosition)
46
+ }
47
+ }
39
48
  break
40
49
 
41
50
  case 'stop':
@@ -44,6 +53,14 @@ class RecorderProcessor extends AudioWorkletProcessor {
44
53
  this.processChunk()
45
54
  }
46
55
  break
56
+
57
+ case 'pause':
58
+ // Just a placeholder for pause handling
59
+ break
60
+
61
+ case 'resume':
62
+ // Just a placeholder for resume handling
63
+ break
47
64
  }
48
65
  }
49
66
 
@@ -138,14 +155,21 @@ class RecorderProcessor extends AudioWorkletProcessor {
138
155
  ? this.convertBitDepth(resampledChunk, this.exportBitDepth)
139
156
  : resampledChunk
140
157
 
141
- // Send processed chunk
158
+ // Calculate the duration in seconds
159
+ const chunkDuration = finalBuffer.length / this.exportSampleRate
160
+
161
+ // Send processed chunk with the current position
142
162
  this.port.postMessage({
143
163
  command: 'newData',
144
164
  recordedData: finalBuffer,
145
165
  sampleRate: this.exportSampleRate,
146
166
  bitDepth: this.exportBitDepth,
147
167
  numberOfChannels: this.numberOfChannels,
168
+ position: this.currentPosition,
148
169
  })
170
+
171
+ // Update the position
172
+ this.currentPosition += chunkDuration
149
173
 
150
174
  // Clear the current chunk
151
175
  this.currentChunk = []