@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.
- package/CHANGELOG.md +11 -1
- package/android/build.gradle +11 -0
- package/android/src/main/AndroidManifest.xml +8 -0
- package/android/src/main/java/net/siteed/audiostream/AudioDeviceManager.kt +266 -42
- package/android/src/main/java/net/siteed/audiostream/ExpoAudioStreamModule.kt +55 -1
- package/app.plugin.js +3 -1
- package/build/cjs/AudioAnalysis/AudioAnalysis.types.js.map +1 -1
- package/build/cjs/AudioDeviceManager.js +229 -40
- package/build/cjs/AudioDeviceManager.js.map +1 -1
- package/build/cjs/WebRecorder.web.js +1 -0
- package/build/cjs/WebRecorder.web.js.map +1 -1
- package/build/cjs/hooks/useAudioDevices.js +30 -5
- package/build/cjs/hooks/useAudioDevices.js.map +1 -1
- package/build/cjs/useAudioRecorder.js +53 -8
- package/build/cjs/useAudioRecorder.js.map +1 -1
- package/build/cjs/workers/InlineFeaturesExtractor.web.js +8 -2
- package/build/cjs/workers/InlineFeaturesExtractor.web.js.map +1 -1
- package/build/esm/AudioAnalysis/AudioAnalysis.types.js.map +1 -1
- package/build/esm/AudioDeviceManager.js +229 -40
- package/build/esm/AudioDeviceManager.js.map +1 -1
- package/build/esm/WebRecorder.web.js +1 -0
- package/build/esm/WebRecorder.web.js.map +1 -1
- package/build/esm/hooks/useAudioDevices.js +31 -6
- package/build/esm/hooks/useAudioDevices.js.map +1 -1
- package/build/esm/useAudioRecorder.js +54 -9
- package/build/esm/useAudioRecorder.js.map +1 -1
- package/build/esm/workers/InlineFeaturesExtractor.web.js +8 -2
- package/build/esm/workers/InlineFeaturesExtractor.web.js.map +1 -1
- package/build/types/AudioAnalysis/AudioAnalysis.types.d.ts +1 -0
- package/build/types/AudioAnalysis/AudioAnalysis.types.d.ts.map +1 -1
- package/build/types/AudioDeviceManager.d.ts +82 -2
- package/build/types/AudioDeviceManager.d.ts.map +1 -1
- package/build/types/WebRecorder.web.d.ts.map +1 -1
- package/build/types/hooks/useAudioDevices.d.ts +1 -0
- package/build/types/hooks/useAudioDevices.d.ts.map +1 -1
- package/build/types/useAudioRecorder.d.ts.map +1 -1
- package/build/types/workers/InlineFeaturesExtractor.web.d.ts +1 -1
- package/build/types/workers/InlineFeaturesExtractor.web.d.ts.map +1 -1
- package/ios/AudioDeviceManager.swift +21 -9
- package/ios/ExpoAudioStreamModule.swift +33 -1
- package/package.json +7 -6
- package/plugin/build/index.cjs +194 -0
- package/plugin/build/index.d.cts +1 -0
- package/plugin/build/index.js +7 -6
- package/plugin/src/index.ts +8 -8
- package/src/AudioAnalysis/AudioAnalysis.types.ts +1 -0
- package/src/AudioDeviceManager.ts +290 -59
- package/src/WebRecorder.web.ts +1 -0
- package/src/hooks/useAudioDevices.ts +39 -6
- package/src/useAudioRecorder.tsx +103 -9
- package/src/workers/InlineFeaturesExtractor.web.tsx +8 -2
package/src/useAudioRecorder.tsx
CHANGED
|
@@ -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 {
|
|
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 {
|
|
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(
|
|
627
|
+
logger?.debug(
|
|
628
|
+
`Setting up recording interruption listener [${instanceId}]`
|
|
629
|
+
)
|
|
607
630
|
|
|
608
631
|
const subscription = addRecordingInterruptionListener((event) => {
|
|
609
|
-
logger?.debug(
|
|
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
|
-
|
|
707
|
+
`[${instanceId}] Error in recording interruption callback:`,
|
|
618
708
|
error
|
|
619
709
|
)
|
|
620
710
|
}
|
|
621
711
|
} else {
|
|
622
|
-
logger?.debug(
|
|
712
|
+
logger?.debug(
|
|
713
|
+
`[${instanceId}] No recording interruption callback configured`
|
|
714
|
+
)
|
|
623
715
|
}
|
|
624
716
|
})
|
|
625
717
|
|
|
626
718
|
return () => {
|
|
627
|
-
logger?.debug(
|
|
719
|
+
logger?.debug(
|
|
720
|
+
`[${instanceId}] Removing recording interruption listener`
|
|
721
|
+
)
|
|
628
722
|
subscription.remove()
|
|
629
723
|
}
|
|
630
|
-
}, []) //
|
|
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) {
|