@siteed/expo-audio-studio 2.12.3 → 2.13.1

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 (51) hide show
  1. package/CHANGELOG.md +11 -1
  2. package/android/build.gradle +11 -0
  3. package/android/src/main/AndroidManifest.xml +8 -0
  4. package/android/src/main/java/net/siteed/audiostream/AudioDeviceManager.kt +266 -42
  5. package/android/src/main/java/net/siteed/audiostream/ExpoAudioStreamModule.kt +55 -1
  6. package/app.plugin.js +3 -1
  7. package/build/cjs/AudioAnalysis/AudioAnalysis.types.js.map +1 -1
  8. package/build/cjs/AudioDeviceManager.js +229 -40
  9. package/build/cjs/AudioDeviceManager.js.map +1 -1
  10. package/build/cjs/WebRecorder.web.js +1 -0
  11. package/build/cjs/WebRecorder.web.js.map +1 -1
  12. package/build/cjs/hooks/useAudioDevices.js +30 -5
  13. package/build/cjs/hooks/useAudioDevices.js.map +1 -1
  14. package/build/cjs/useAudioRecorder.js +53 -8
  15. package/build/cjs/useAudioRecorder.js.map +1 -1
  16. package/build/cjs/workers/InlineFeaturesExtractor.web.js +8 -2
  17. package/build/cjs/workers/InlineFeaturesExtractor.web.js.map +1 -1
  18. package/build/esm/AudioAnalysis/AudioAnalysis.types.js.map +1 -1
  19. package/build/esm/AudioDeviceManager.js +229 -40
  20. package/build/esm/AudioDeviceManager.js.map +1 -1
  21. package/build/esm/WebRecorder.web.js +1 -0
  22. package/build/esm/WebRecorder.web.js.map +1 -1
  23. package/build/esm/hooks/useAudioDevices.js +31 -6
  24. package/build/esm/hooks/useAudioDevices.js.map +1 -1
  25. package/build/esm/useAudioRecorder.js +54 -9
  26. package/build/esm/useAudioRecorder.js.map +1 -1
  27. package/build/esm/workers/InlineFeaturesExtractor.web.js +8 -2
  28. package/build/esm/workers/InlineFeaturesExtractor.web.js.map +1 -1
  29. package/build/types/AudioAnalysis/AudioAnalysis.types.d.ts +1 -0
  30. package/build/types/AudioAnalysis/AudioAnalysis.types.d.ts.map +1 -1
  31. package/build/types/AudioDeviceManager.d.ts +82 -2
  32. package/build/types/AudioDeviceManager.d.ts.map +1 -1
  33. package/build/types/WebRecorder.web.d.ts.map +1 -1
  34. package/build/types/hooks/useAudioDevices.d.ts +1 -0
  35. package/build/types/hooks/useAudioDevices.d.ts.map +1 -1
  36. package/build/types/useAudioRecorder.d.ts.map +1 -1
  37. package/build/types/workers/InlineFeaturesExtractor.web.d.ts +1 -1
  38. package/build/types/workers/InlineFeaturesExtractor.web.d.ts.map +1 -1
  39. package/ios/AudioDeviceManager.swift +21 -9
  40. package/ios/ExpoAudioStreamModule.swift +33 -1
  41. package/package.json +7 -6
  42. package/plugin/build/index.cjs +194 -0
  43. package/plugin/build/index.d.cts +1 -0
  44. package/plugin/build/index.js +7 -6
  45. package/plugin/src/index.ts +8 -8
  46. package/src/AudioAnalysis/AudioAnalysis.types.ts +1 -0
  47. package/src/AudioDeviceManager.ts +290 -59
  48. package/src/WebRecorder.web.ts +1 -0
  49. package/src/hooks/useAudioDevices.ts +39 -6
  50. package/src/useAudioRecorder.tsx +103 -9
  51. package/src/workers/InlineFeaturesExtractor.web.tsx +8 -2
@@ -1,8 +1,9 @@
1
1
  // src/useAudioRecorder.ts
2
2
  import { EventSubscription, Platform } from 'expo-modules-core'
3
- import { useCallback, useEffect, useReducer, useRef } from 'react'
3
+ import { useCallback, useEffect, useReducer, useRef, useId } from 'react'
4
4
 
5
5
  import { AudioAnalysis } from './AudioAnalysis/AudioAnalysis.types'
6
+ import { audioDeviceManager } from './AudioDeviceManager'
6
7
  import {
7
8
  AudioDataEvent,
8
9
  AudioRecording,
@@ -84,6 +85,7 @@ const defaultAnalysis: AudioAnalysis = {
84
85
  min: Number.POSITIVE_INFINITY,
85
86
  max: Number.NEGATIVE_INFINITY,
86
87
  },
88
+ extractionTimeMs: 0,
87
89
  }
88
90
 
89
91
  function audioRecorderReducer(
@@ -157,6 +159,10 @@ export function useAudioRecorder({
157
159
  audioWorkletUrl,
158
160
  featuresExtratorUrl,
159
161
  }: UseAudioRecorderProps = {}): UseAudioRecorderState {
162
+ // Initialize AudioDeviceManager with logger (once)
163
+ if (logger) {
164
+ audioDeviceManager.setLogger(logger)
165
+ }
160
166
  const [state, dispatch] = useReducer(audioRecorderReducer, {
161
167
  isRecording: false,
162
168
  isPaused: false,
@@ -200,6 +206,9 @@ export function useAudioRecorder({
200
206
 
201
207
  const recordingConfigRef = useRef<RecordingConfig | null>(null)
202
208
 
209
+ // Generate unique instance ID for debugging
210
+ const instanceId = useId().replace(/:/g, '').slice(0, 5)
211
+
203
212
  const handleAudioAnalysis = useCallback(
204
213
  async ({
205
214
  analysis,
@@ -469,7 +478,12 @@ export function useAudioRecorder({
469
478
 
470
479
  analysisRef.current = { ...defaultAnalysis } // Reset analysis data
471
480
  fullAnalysisRef.current = { ...defaultAnalysis }
472
- const { onAudioStream, ...options } = recordingOptions
481
+ const {
482
+ onAudioStream,
483
+ onRecordingInterrupted,
484
+ onAudioAnalysis,
485
+ ...options
486
+ } = recordingOptions
473
487
  const { enableProcessing } = options
474
488
 
475
489
  const maxRecentDataDuration = 10000 // TODO compute maxRecentDataDuration based on screen dimensions
@@ -518,7 +532,12 @@ export function useAudioRecorder({
518
532
 
519
533
  analysisRef.current = { ...defaultAnalysis } // Reset analysis data
520
534
  fullAnalysisRef.current = { ...defaultAnalysis }
521
- const { onAudioStream, ...options } = recordingOptions
535
+ const {
536
+ onAudioStream,
537
+ onRecordingInterrupted,
538
+ onAudioAnalysis,
539
+ ...options
540
+ } = recordingOptions
522
541
 
523
542
  // Store onAudioStream for later use when recording starts
524
543
  if (typeof onAudioStream === 'function') {
@@ -546,6 +565,8 @@ export function useAudioRecorder({
546
565
  analysisListenerRef.current = null
547
566
  }
548
567
  onAudioStreamRef.current = null
568
+
569
+ // Note: We deliberately DON'T clear recordingConfigRef here to preserve interruption callback
549
570
  logger?.debug(`recording stopped`, stopResult)
550
571
  dispatch({ type: 'STOP' })
551
572
  return stopResult
@@ -603,31 +624,104 @@ export function useAudioRecorder({
603
624
 
604
625
  useEffect(() => {
605
626
  // Add event subscription for recording interruptions
606
- logger?.debug('Setting up recording interruption listener')
627
+ logger?.debug(
628
+ `Setting up recording interruption listener [${instanceId}]`
629
+ )
607
630
 
608
631
  const subscription = addRecordingInterruptionListener((event) => {
609
- logger?.debug('Received recording interruption event:', event)
632
+ logger?.debug(
633
+ `[${instanceId}] Received recording interruption event:`,
634
+ event
635
+ )
636
+
637
+ // Handle device disconnection for UI updates
638
+ if (event.reason === 'deviceDisconnected') {
639
+ logger?.debug(
640
+ `[${instanceId}] Device disconnected - temporarily hiding last device from UI`
641
+ )
642
+
643
+ // Get current device list before the native layer updates
644
+ const currentDevices = audioDeviceManager.getRawDevices()
645
+
646
+ // Wait a moment for native layer to update, then compare
647
+ setTimeout(async () => {
648
+ try {
649
+ // Get updated devices without notifying yet
650
+ const updatedDevices =
651
+ await audioDeviceManager.getAvailableDevices({
652
+ refresh: true,
653
+ })
654
+
655
+ // Find missing devices by comparing lists
656
+ const missingDevices = currentDevices.filter(
657
+ (oldDevice) =>
658
+ !updatedDevices.some(
659
+ (newDevice) => newDevice.id === oldDevice.id
660
+ )
661
+ )
662
+
663
+ if (missingDevices.length > 0) {
664
+ // Mark all missing devices as disconnected (silently)
665
+ missingDevices.forEach((missingDevice) => {
666
+ logger?.debug(
667
+ `[${instanceId}] Confirmed disconnected device: ${missingDevice.name} (${missingDevice.id})`
668
+ )
669
+ audioDeviceManager.markDeviceAsDisconnected(
670
+ missingDevice.id,
671
+ false
672
+ )
673
+ })
674
+ }
675
+
676
+ // Notify listeners once with the final filtered state
677
+ audioDeviceManager.notifyListeners()
678
+ } catch (error) {
679
+ logger?.warn(
680
+ `[${instanceId}] Error in delayed device disconnection handling:`,
681
+ error
682
+ )
683
+ }
684
+ }, 500) // 500ms delay to let native layer update
685
+ } else if (event.reason === 'deviceConnected') {
686
+ // Device reconnected - force refresh to show it immediately
687
+ logger?.debug(
688
+ `[${instanceId}] Device connected, forcing refresh`
689
+ )
690
+ audioDeviceManager.forceRefreshDevices()
691
+ }
610
692
 
611
693
  // Check if we have a callback configured
694
+ logger?.debug(
695
+ `[${instanceId}] recordingConfigRef.current exists:`,
696
+ !!recordingConfigRef.current
697
+ )
698
+
612
699
  if (recordingConfigRef.current?.onRecordingInterrupted) {
613
700
  try {
701
+ logger?.debug(
702
+ `[${instanceId}] Calling recording interruption callback`
703
+ )
614
704
  recordingConfigRef.current.onRecordingInterrupted(event)
615
705
  } catch (error) {
616
706
  logger?.error(
617
- 'Error in recording interruption callback:',
707
+ `[${instanceId}] Error in recording interruption callback:`,
618
708
  error
619
709
  )
620
710
  }
621
711
  } else {
622
- logger?.debug('No recording interruption callback configured')
712
+ logger?.debug(
713
+ `[${instanceId}] No recording interruption callback configured`
714
+ )
623
715
  }
624
716
  })
625
717
 
626
718
  return () => {
627
- logger?.debug('Removing recording interruption listener')
719
+ logger?.debug(
720
+ `[${instanceId}] Removing recording interruption listener`
721
+ )
628
722
  subscription.remove()
629
723
  }
630
- }, []) // Empty dependency array since we want this to run once
724
+ }, [instanceId, logger]) // Include instanceId and logger in dependencies
631
725
 
632
726
  return {
633
727
  prepareRecording,
@@ -802,12 +802,14 @@ self.onmessage = function (event) {
802
802
  rmsRange: {
803
803
  min: 0,
804
804
  max: Math.max(Math.abs(min), Math.abs(max))
805
- },
806
- extractionTimeMs: Date.now() - lastEmitTime
805
+ }
807
806
  }
808
807
  }
809
808
 
810
809
  try {
810
+ // Measure actual processing time using performance.now() for higher precision
811
+ const processingStartTime = performance.now()
812
+
811
813
  const result = extractWaveform(
812
814
  channelData,
813
815
  sampleRate,
@@ -815,6 +817,9 @@ self.onmessage = function (event) {
815
817
  numberOfChannels || 1, // Default to 1 channel if not provided
816
818
  bytesPerSample
817
819
  )
820
+
821
+ const processingEndTime = performance.now()
822
+ const actualExtractionTimeMs = processingEndTime - processingStartTime
818
823
 
819
824
  // Send complete result immediately
820
825
  self.postMessage({
@@ -829,6 +834,7 @@ self.onmessage = function (event) {
829
834
  dataPoints: result.dataPoints,
830
835
  amplitudeRange: result.amplitudeRange,
831
836
  rmsRange: result.rmsRange,
837
+ extractionTimeMs: actualExtractionTimeMs,
832
838
  }
833
839
  })
834
840
  } catch (error) {