@siteed/audio-studio 3.0.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/CHANGELOG.md +535 -0
- package/LICENSE +21 -0
- package/README.md +167 -0
- package/android/build.gradle +143 -0
- package/android/src/androidTest/assets/chorus.wav +0 -0
- package/android/src/androidTest/assets/jfk.wav +0 -0
- package/android/src/androidTest/assets/osr_us_000_0010_8k.wav +0 -0
- package/android/src/androidTest/assets/recorder_hello_world.wav +0 -0
- package/android/src/androidTest/java/net/siteed/audiostudio/AudioProcessorInstrumentedTest.kt +197 -0
- package/android/src/androidTest/java/net/siteed/audiostudio/AudioRecorderInstrumentedTest.kt +541 -0
- package/android/src/androidTest/java/net/siteed/audiostudio/AudioRecorderPerformanceInstrumentedTest.kt +234 -0
- package/android/src/androidTest/java/net/siteed/audiostudio/integration/AudioFocusStrategyIntegrationTest.kt +332 -0
- package/android/src/androidTest/java/net/siteed/audiostudio/integration/BufferDurationIntegrationTest.kt +324 -0
- package/android/src/androidTest/java/net/siteed/audiostudio/integration/CompressedOnlyOutputTest.kt +253 -0
- package/android/src/androidTest/java/net/siteed/audiostudio/integration/DeviceDisconnectionFallbackTest.kt +218 -0
- package/android/src/androidTest/java/net/siteed/audiostudio/integration/EventEmissionIntervalTest.kt +120 -0
- package/android/src/androidTest/java/net/siteed/audiostudio/integration/M4aFormatTest.kt +345 -0
- package/android/src/androidTest/java/net/siteed/audiostudio/integration/OutputControlIntegrationTest.kt +340 -0
- package/android/src/androidTest/java/net/siteed/audiostudio/integration/PcmStreamingDurationTest.kt +252 -0
- package/android/src/androidTest/java/net/siteed/audiostudio/integration/README.md +95 -0
- package/android/src/androidTest/java/net/siteed/audiostudio/integration/run_integration_tests.sh +43 -0
- package/android/src/main/AndroidManifest.xml +30 -0
- package/android/src/main/CMakeLists.txt +29 -0
- package/android/src/main/java/net/siteed/audiostudio/AudioAnalysisData.kt +188 -0
- package/android/src/main/java/net/siteed/audiostudio/AudioDataEncoder.kt +9 -0
- package/android/src/main/java/net/siteed/audiostudio/AudioDeviceManager.kt +1741 -0
- package/android/src/main/java/net/siteed/audiostudio/AudioFeaturesNative.kt +26 -0
- package/android/src/main/java/net/siteed/audiostudio/AudioFileHandler.kt +136 -0
- package/android/src/main/java/net/siteed/audiostudio/AudioFormatUtils.kt +354 -0
- package/android/src/main/java/net/siteed/audiostudio/AudioNotificationsManager.kt +439 -0
- package/android/src/main/java/net/siteed/audiostudio/AudioProcessor.kt +2237 -0
- package/android/src/main/java/net/siteed/audiostudio/AudioRecorderManager.kt +2163 -0
- package/android/src/main/java/net/siteed/audiostudio/AudioRecordingService.kt +167 -0
- package/android/src/main/java/net/siteed/audiostudio/AudioStudioModule.kt +1112 -0
- package/android/src/main/java/net/siteed/audiostudio/AudioTrimmer.kt +1099 -0
- package/android/src/main/java/net/siteed/audiostudio/Constants.kt +37 -0
- package/android/src/main/java/net/siteed/audiostudio/EventSender.kt +7 -0
- package/android/src/main/java/net/siteed/audiostudio/FFT.kt +100 -0
- package/android/src/main/java/net/siteed/audiostudio/Features.kt +98 -0
- package/android/src/main/java/net/siteed/audiostudio/LogUtils.kt +93 -0
- package/android/src/main/java/net/siteed/audiostudio/MelSpectrogramNative.kt +36 -0
- package/android/src/main/java/net/siteed/audiostudio/NotificationConfig.kt +72 -0
- package/android/src/main/java/net/siteed/audiostudio/PermissionUtils.kt +68 -0
- package/android/src/main/java/net/siteed/audiostudio/RecordingActionReceiver.kt +59 -0
- package/android/src/main/java/net/siteed/audiostudio/RecordingConfig.kt +259 -0
- package/android/src/main/java/net/siteed/audiostudio/WaveformConfig.kt +19 -0
- package/android/src/main/java/net/siteed/audiostudio/WaveformRenderer.kt +159 -0
- package/android/src/main/jni/AudioFeaturesJNI.cpp +152 -0
- package/android/src/main/jni/MelSpectrogramJNI.cpp +165 -0
- package/android/src/main/res/drawable/ic_default_action_icon.xml +16 -0
- package/android/src/main/res/drawable/ic_microphone.xml +13 -0
- package/android/src/main/res/drawable/ic_pause.xml +10 -0
- package/android/src/main/res/drawable/ic_play.xml +10 -0
- package/android/src/main/res/drawable/ic_stop.xml +10 -0
- package/android/src/main/res/layout/notification_recording.xml +37 -0
- package/android/src/test/java/net/siteed/audiostudio/AudioFileHandlerTest.kt +279 -0
- package/android/src/test/java/net/siteed/audiostudio/AudioFocusStrategyTest.kt +249 -0
- package/android/src/test/java/net/siteed/audiostudio/AudioFormatTest.kt +151 -0
- package/android/src/test/java/net/siteed/audiostudio/AudioFormatUtilsTest.kt +273 -0
- package/android/src/test/java/net/siteed/audiostudio/DeviceDisconnectionFallbackUnitTest.kt +140 -0
- package/android/src/test/resources/chorus.wav +0 -0
- package/android/src/test/resources/generate_test_audio.py +94 -0
- package/android/src/test/resources/jfk.wav +0 -0
- package/android/src/test/resources/osr_us_000_0010_8k.wav +0 -0
- package/android/src/test/resources/recorder_hello_world.wav +0 -0
- package/app.plugin.js +3 -0
- package/build/cjs/AudioAnalysis/AudioAnalysis.types.js +4 -0
- package/build/cjs/AudioAnalysis/AudioAnalysis.types.js.map +1 -0
- package/build/cjs/AudioAnalysis/audioFeaturesWasm.js +164 -0
- package/build/cjs/AudioAnalysis/audioFeaturesWasm.js.map +1 -0
- package/build/cjs/AudioAnalysis/extractAudioAnalysis.js +213 -0
- package/build/cjs/AudioAnalysis/extractAudioAnalysis.js.map +1 -0
- package/build/cjs/AudioAnalysis/extractAudioData.js +21 -0
- package/build/cjs/AudioAnalysis/extractAudioData.js.map +1 -0
- package/build/cjs/AudioAnalysis/extractMelSpectrogram.js +90 -0
- package/build/cjs/AudioAnalysis/extractMelSpectrogram.js.map +1 -0
- package/build/cjs/AudioAnalysis/extractPreview.js +28 -0
- package/build/cjs/AudioAnalysis/extractPreview.js.map +1 -0
- package/build/cjs/AudioAnalysis/extractWaveform.js +18 -0
- package/build/cjs/AudioAnalysis/extractWaveform.js.map +1 -0
- package/build/cjs/AudioAnalysis/melSpectrogramWasm.js +149 -0
- package/build/cjs/AudioAnalysis/melSpectrogramWasm.js.map +1 -0
- package/build/cjs/AudioDeviceManager.js +688 -0
- package/build/cjs/AudioDeviceManager.js.map +1 -0
- package/build/cjs/AudioRecorder.provider.js +78 -0
- package/build/cjs/AudioRecorder.provider.js.map +1 -0
- package/build/cjs/AudioStudio.native.js +8 -0
- package/build/cjs/AudioStudio.native.js.map +1 -0
- package/build/cjs/AudioStudio.types.js +11 -0
- package/build/cjs/AudioStudio.types.js.map +1 -0
- package/build/cjs/AudioStudio.web.js +708 -0
- package/build/cjs/AudioStudio.web.js.map +1 -0
- package/build/cjs/AudioStudioModule.js +718 -0
- package/build/cjs/AudioStudioModule.js.map +1 -0
- package/build/cjs/WebRecorder.web.js +865 -0
- package/build/cjs/WebRecorder.web.js.map +1 -0
- package/build/cjs/constants/platformLimitations.js +99 -0
- package/build/cjs/constants/platformLimitations.js.map +1 -0
- package/build/cjs/constants.js +20 -0
- package/build/cjs/constants.js.map +1 -0
- package/build/cjs/events.js +29 -0
- package/build/cjs/events.js.map +1 -0
- package/build/cjs/hooks/useAudioDevices.js +179 -0
- package/build/cjs/hooks/useAudioDevices.js.map +1 -0
- package/build/cjs/index.js +64 -0
- package/build/cjs/index.js.map +1 -0
- package/build/cjs/trimAudio.js +76 -0
- package/build/cjs/trimAudio.js.map +1 -0
- package/build/cjs/useAudioRecorder.js +535 -0
- package/build/cjs/useAudioRecorder.js.map +1 -0
- package/build/cjs/utils/BlobFix.js +502 -0
- package/build/cjs/utils/BlobFix.js.map +1 -0
- package/build/cjs/utils/audioProcessing.js +136 -0
- package/build/cjs/utils/audioProcessing.js.map +1 -0
- package/build/cjs/utils/cleanNativeOptions.js +22 -0
- package/build/cjs/utils/cleanNativeOptions.js.map +1 -0
- package/build/cjs/utils/concatenateBuffers.js +25 -0
- package/build/cjs/utils/concatenateBuffers.js.map +1 -0
- package/build/cjs/utils/convertPCMToFloat32.js +124 -0
- package/build/cjs/utils/convertPCMToFloat32.js.map +1 -0
- package/build/cjs/utils/crc32.js +52 -0
- package/build/cjs/utils/crc32.js.map +1 -0
- package/build/cjs/utils/encodingToBitDepth.js +17 -0
- package/build/cjs/utils/encodingToBitDepth.js.map +1 -0
- package/build/cjs/utils/getWavFileInfo.js +96 -0
- package/build/cjs/utils/getWavFileInfo.js.map +1 -0
- package/build/cjs/utils/writeWavHeader.js +88 -0
- package/build/cjs/utils/writeWavHeader.js.map +1 -0
- package/build/cjs/workers/InlineFeaturesExtractor.web.js +294 -0
- package/build/cjs/workers/InlineFeaturesExtractor.web.js.map +1 -0
- package/build/cjs/workers/inlineAudioWebWorker.web.js +190 -0
- package/build/cjs/workers/inlineAudioWebWorker.web.js.map +1 -0
- package/build/cjs/workers/wasmGlueString.web.js +27 -0
- package/build/cjs/workers/wasmGlueString.web.js.map +1 -0
- package/build/esm/AudioAnalysis/AudioAnalysis.types.js +3 -0
- package/build/esm/AudioAnalysis/AudioAnalysis.types.js.map +1 -0
- package/build/esm/AudioAnalysis/audioFeaturesWasm.js +126 -0
- package/build/esm/AudioAnalysis/audioFeaturesWasm.js.map +1 -0
- package/build/esm/AudioAnalysis/extractAudioAnalysis.js +205 -0
- package/build/esm/AudioAnalysis/extractAudioAnalysis.js.map +1 -0
- package/build/esm/AudioAnalysis/extractAudioData.js +14 -0
- package/build/esm/AudioAnalysis/extractAudioData.js.map +1 -0
- package/build/esm/AudioAnalysis/extractMelSpectrogram.js +86 -0
- package/build/esm/AudioAnalysis/extractMelSpectrogram.js.map +1 -0
- package/build/esm/AudioAnalysis/extractPreview.js +25 -0
- package/build/esm/AudioAnalysis/extractPreview.js.map +1 -0
- package/build/esm/AudioAnalysis/extractWaveform.js +11 -0
- package/build/esm/AudioAnalysis/extractWaveform.js.map +1 -0
- package/build/esm/AudioAnalysis/melSpectrogramWasm.js +111 -0
- package/build/esm/AudioAnalysis/melSpectrogramWasm.js.map +1 -0
- package/build/esm/AudioDeviceManager.js +681 -0
- package/build/esm/AudioDeviceManager.js.map +1 -0
- package/build/esm/AudioRecorder.provider.js +40 -0
- package/build/esm/AudioRecorder.provider.js.map +1 -0
- package/build/esm/AudioStudio.native.js +6 -0
- package/build/esm/AudioStudio.native.js.map +1 -0
- package/build/esm/AudioStudio.types.js +8 -0
- package/build/esm/AudioStudio.types.js.map +1 -0
- package/build/esm/AudioStudio.web.js +704 -0
- package/build/esm/AudioStudio.web.js.map +1 -0
- package/build/esm/AudioStudioModule.js +713 -0
- package/build/esm/AudioStudioModule.js.map +1 -0
- package/build/esm/WebRecorder.web.js +861 -0
- package/build/esm/WebRecorder.web.js.map +1 -0
- package/build/esm/constants/platformLimitations.js +90 -0
- package/build/esm/constants/platformLimitations.js.map +1 -0
- package/build/esm/constants.js +17 -0
- package/build/esm/constants.js.map +1 -0
- package/build/esm/events.js +21 -0
- package/build/esm/events.js.map +1 -0
- package/build/esm/hooks/useAudioDevices.js +176 -0
- package/build/esm/hooks/useAudioDevices.js.map +1 -0
- package/build/esm/index.js +23 -0
- package/build/esm/index.js.map +1 -0
- package/build/esm/trimAudio.js +69 -0
- package/build/esm/trimAudio.js.map +1 -0
- package/build/esm/useAudioRecorder.js +529 -0
- package/build/esm/useAudioRecorder.js.map +1 -0
- package/build/esm/utils/BlobFix.js +498 -0
- package/build/esm/utils/BlobFix.js.map +1 -0
- package/build/esm/utils/audioProcessing.js +133 -0
- package/build/esm/utils/audioProcessing.js.map +1 -0
- package/build/esm/utils/cleanNativeOptions.js +19 -0
- package/build/esm/utils/cleanNativeOptions.js.map +1 -0
- package/build/esm/utils/concatenateBuffers.js +21 -0
- package/build/esm/utils/concatenateBuffers.js.map +1 -0
- package/build/esm/utils/convertPCMToFloat32.js +120 -0
- package/build/esm/utils/convertPCMToFloat32.js.map +1 -0
- package/build/esm/utils/crc32.js +50 -0
- package/build/esm/utils/crc32.js.map +1 -0
- package/build/esm/utils/encodingToBitDepth.js +13 -0
- package/build/esm/utils/encodingToBitDepth.js.map +1 -0
- package/build/esm/utils/getWavFileInfo.js +92 -0
- package/build/esm/utils/getWavFileInfo.js.map +1 -0
- package/build/esm/utils/writeWavHeader.js +84 -0
- package/build/esm/utils/writeWavHeader.js.map +1 -0
- package/build/esm/workers/InlineFeaturesExtractor.web.js +291 -0
- package/build/esm/workers/InlineFeaturesExtractor.web.js.map +1 -0
- package/build/esm/workers/inlineAudioWebWorker.web.js +187 -0
- package/build/esm/workers/inlineAudioWebWorker.web.js.map +1 -0
- package/build/esm/workers/wasmGlueString.web.js +24 -0
- package/build/esm/workers/wasmGlueString.web.js.map +1 -0
- package/build/types/AudioAnalysis/AudioAnalysis.types.d.ts +198 -0
- package/build/types/AudioAnalysis/AudioAnalysis.types.d.ts.map +1 -0
- package/build/types/AudioAnalysis/audioFeaturesWasm.d.ts +24 -0
- package/build/types/AudioAnalysis/audioFeaturesWasm.d.ts.map +1 -0
- package/build/types/AudioAnalysis/extractAudioAnalysis.d.ts +74 -0
- package/build/types/AudioAnalysis/extractAudioAnalysis.d.ts.map +1 -0
- package/build/types/AudioAnalysis/extractAudioData.d.ts +3 -0
- package/build/types/AudioAnalysis/extractAudioData.d.ts.map +1 -0
- package/build/types/AudioAnalysis/extractMelSpectrogram.d.ts +20 -0
- package/build/types/AudioAnalysis/extractMelSpectrogram.d.ts.map +1 -0
- package/build/types/AudioAnalysis/extractPreview.d.ts +11 -0
- package/build/types/AudioAnalysis/extractPreview.d.ts.map +1 -0
- package/build/types/AudioAnalysis/extractWaveform.d.ts +8 -0
- package/build/types/AudioAnalysis/extractWaveform.d.ts.map +1 -0
- package/build/types/AudioAnalysis/melSpectrogramWasm.d.ts +16 -0
- package/build/types/AudioAnalysis/melSpectrogramWasm.d.ts.map +1 -0
- package/build/types/AudioDeviceManager.d.ts +187 -0
- package/build/types/AudioDeviceManager.d.ts.map +1 -0
- package/build/types/AudioRecorder.provider.d.ts +11 -0
- package/build/types/AudioRecorder.provider.d.ts.map +1 -0
- package/build/types/AudioStudio.native.d.ts +3 -0
- package/build/types/AudioStudio.native.d.ts.map +1 -0
- package/build/types/AudioStudio.types.d.ts +760 -0
- package/build/types/AudioStudio.types.d.ts.map +1 -0
- package/build/types/AudioStudio.web.d.ts +96 -0
- package/build/types/AudioStudio.web.d.ts.map +1 -0
- package/build/types/AudioStudioModule.d.ts +3 -0
- package/build/types/AudioStudioModule.d.ts.map +1 -0
- package/build/types/WebRecorder.web.d.ts +208 -0
- package/build/types/WebRecorder.web.d.ts.map +1 -0
- package/build/types/constants/platformLimitations.d.ts +40 -0
- package/build/types/constants/platformLimitations.d.ts.map +1 -0
- package/build/types/constants.d.ts +14 -0
- package/build/types/constants.d.ts.map +1 -0
- package/build/types/events.d.ts +29 -0
- package/build/types/events.d.ts.map +1 -0
- package/build/types/hooks/useAudioDevices.d.ts +15 -0
- package/build/types/hooks/useAudioDevices.d.ts.map +1 -0
- package/build/types/index.d.ts +21 -0
- package/build/types/index.d.ts.map +1 -0
- package/build/types/trimAudio.d.ts +25 -0
- package/build/types/trimAudio.d.ts.map +1 -0
- package/build/types/useAudioRecorder.d.ts +22 -0
- package/build/types/useAudioRecorder.d.ts.map +1 -0
- package/build/types/utils/BlobFix.d.ts +9 -0
- package/build/types/utils/BlobFix.d.ts.map +1 -0
- package/build/types/utils/audioProcessing.d.ts +24 -0
- package/build/types/utils/audioProcessing.d.ts.map +1 -0
- package/build/types/utils/cleanNativeOptions.d.ts +15 -0
- package/build/types/utils/cleanNativeOptions.d.ts.map +1 -0
- package/build/types/utils/concatenateBuffers.d.ts +8 -0
- package/build/types/utils/concatenateBuffers.d.ts.map +1 -0
- package/build/types/utils/convertPCMToFloat32.d.ts +13 -0
- package/build/types/utils/convertPCMToFloat32.d.ts.map +1 -0
- package/build/types/utils/crc32.d.ts +7 -0
- package/build/types/utils/crc32.d.ts.map +1 -0
- package/build/types/utils/encodingToBitDepth.d.ts +5 -0
- package/build/types/utils/encodingToBitDepth.d.ts.map +1 -0
- package/build/types/utils/getWavFileInfo.d.ts +26 -0
- package/build/types/utils/getWavFileInfo.d.ts.map +1 -0
- package/build/types/utils/writeWavHeader.d.ts +34 -0
- package/build/types/utils/writeWavHeader.d.ts.map +1 -0
- package/build/types/workers/InlineFeaturesExtractor.web.d.ts +2 -0
- package/build/types/workers/InlineFeaturesExtractor.web.d.ts.map +1 -0
- package/build/types/workers/inlineAudioWebWorker.web.d.ts +2 -0
- package/build/types/workers/inlineAudioWebWorker.web.d.ts.map +1 -0
- package/build/types/workers/wasmGlueString.web.d.ts +2 -0
- package/build/types/workers/wasmGlueString.web.d.ts.map +1 -0
- package/cpp/AudioFeatures.cpp +274 -0
- package/cpp/AudioFeatures.h +85 -0
- package/cpp/AudioFeaturesBridge.cpp +146 -0
- package/cpp/AudioFeaturesBridge.h +47 -0
- package/cpp/MelSpectrogram.cpp +227 -0
- package/cpp/MelSpectrogram.h +82 -0
- package/cpp/MelSpectrogramBridge.cpp +112 -0
- package/cpp/MelSpectrogramBridge.h +33 -0
- package/cpp/kiss_fft/COPYING +11 -0
- package/cpp/kiss_fft/_kiss_fft_guts.h +167 -0
- package/cpp/kiss_fft/kiss_fft.c +424 -0
- package/cpp/kiss_fft/kiss_fft.h +160 -0
- package/cpp/kiss_fft/kiss_fft_log.h +36 -0
- package/cpp/kiss_fft/kiss_fftr.c +155 -0
- package/cpp/kiss_fft/kiss_fftr.h +54 -0
- package/expo-module.config.json +10 -0
- package/ios/AudioAnalysisData.swift +74 -0
- package/ios/AudioDeviceManager.swift +670 -0
- package/ios/AudioFeaturesWrapper.h +21 -0
- package/ios/AudioFeaturesWrapper.mm +63 -0
- package/ios/AudioNotificationManager.swift +154 -0
- package/ios/AudioProcessingHelpers.swift +797 -0
- package/ios/AudioProcessor.swift +1191 -0
- package/ios/AudioStreamError.swift +7 -0
- package/ios/AudioStreamManager.swift +2369 -0
- package/ios/AudioStreamManagerDelegate.swift +16 -0
- package/ios/AudioStudio.podspec +39 -0
- package/ios/AudioStudioModule.swift +1111 -0
- package/ios/AudioStudioTests/AudioFileHandlerTests.swift +338 -0
- package/ios/AudioStudioTests/AudioFormatUtilsTests.swift +331 -0
- package/ios/AudioStudioTests/AudioTestHelpers.swift +130 -0
- package/ios/AudioStudioTests/CompressedOnlyOutputTests.swift +294 -0
- package/ios/AudioStudioTests/EventEmissionIntervalTests.swift +105 -0
- package/ios/AudioStudioTests/Info.plist +22 -0
- package/ios/AudioStudioTests/README.md +39 -0
- package/ios/AudioStudioTests/SimpleAudioTest.swift +98 -0
- package/ios/AudioStudioTests/TestAudioGenerator.swift +75 -0
- package/ios/DataPoint.swift +54 -0
- package/ios/DecodingConfig.swift +59 -0
- package/ios/FFT.swift +62 -0
- package/ios/Features.swift +95 -0
- package/ios/ISSUE_IOS.md +68 -0
- package/ios/Logger.swift +39 -0
- package/ios/MelSpectrogramWrapper.h +30 -0
- package/ios/MelSpectrogramWrapper.mm +97 -0
- package/ios/NotificationExtension.swift +15 -0
- package/ios/RecordingResult.swift +22 -0
- package/ios/RecordingSettings.swift +311 -0
- package/ios/WaveformExtractor.swift +105 -0
- package/ios/tests/README.md +41 -0
- package/ios/tests/integration/buffer_and_fallback_test.swift +178 -0
- package/ios/tests/integration/buffer_duration_test.swift +185 -0
- package/ios/tests/integration/compressed_only_output_test.swift +271 -0
- package/ios/tests/integration/output_control_test.swift +322 -0
- package/ios/tests/integration/run_integration_tests.sh +37 -0
- package/ios/tests/opus_support_test_macos.swift +154 -0
- package/ios/tests/standalone/audio_processing_test.swift +144 -0
- package/ios/tests/standalone/audio_recording_test.swift +277 -0
- package/ios/tests/standalone/audio_streaming_test.swift +249 -0
- package/ios/tests/standalone/standalone_test.swift +144 -0
- package/package.json +146 -0
- package/plugin/build/index.cjs +194 -0
- package/plugin/build/index.d.cts +22 -0
- package/plugin/build/index.js +194 -0
- package/plugin/src/index.ts +285 -0
- package/plugin/tsconfig.json +10 -0
- package/plugin/tsconfig.tsbuildinfo +1 -0
- package/prebuilt/wasm/mel-spectrogram.js +18 -0
- package/src/AudioAnalysis/AudioAnalysis.types.ts +226 -0
- package/src/AudioAnalysis/audio-features-wasm.d.ts +37 -0
- package/src/AudioAnalysis/audioFeaturesWasm.ts +200 -0
- package/src/AudioAnalysis/extractAudioAnalysis.ts +350 -0
- package/src/AudioAnalysis/extractAudioData.ts +17 -0
- package/src/AudioAnalysis/extractMelSpectrogram.ts +140 -0
- package/src/AudioAnalysis/extractPreview.ts +34 -0
- package/src/AudioAnalysis/extractWaveform.ts +22 -0
- package/src/AudioAnalysis/mel-spectrogram-wasm.d.ts +48 -0
- package/src/AudioAnalysis/melSpectrogramWasm.ts +179 -0
- package/src/AudioDeviceManager.ts +800 -0
- package/src/AudioRecorder.provider.tsx +57 -0
- package/src/AudioStudio.native.ts +6 -0
- package/src/AudioStudio.types.ts +899 -0
- package/src/AudioStudio.web.ts +911 -0
- package/src/AudioStudioModule.ts +984 -0
- package/src/WebRecorder.web.ts +1114 -0
- package/src/constants/platformLimitations.ts +118 -0
- package/src/constants.ts +21 -0
- package/src/events.ts +63 -0
- package/src/hooks/useAudioDevices.ts +213 -0
- package/src/index.ts +67 -0
- package/src/trimAudio.ts +94 -0
- package/src/types/crc-32.d.ts +9 -0
- package/src/useAudioRecorder.tsx +784 -0
- package/src/utils/BlobFix.ts +561 -0
- package/src/utils/audioProcessing.ts +205 -0
- package/src/utils/cleanNativeOptions.ts +18 -0
- package/src/utils/concatenateBuffers.ts +24 -0
- package/src/utils/convertPCMToFloat32.ts +170 -0
- package/src/utils/crc32.ts +59 -0
- package/src/utils/encodingToBitDepth.ts +18 -0
- package/src/utils/getWavFileInfo.ts +132 -0
- package/src/utils/writeWavHeader.ts +115 -0
- package/src/workers/InlineFeaturesExtractor.web.tsx +291 -0
- package/src/workers/inlineAudioWebWorker.web.tsx +186 -0
- package/src/workers/wasmGlueString.web.ts +23 -0
|
@@ -0,0 +1,670 @@
|
|
|
1
|
+
//
|
|
2
|
+
// AudioDeviceManager.swift
|
|
3
|
+
// Pods
|
|
4
|
+
//
|
|
5
|
+
// Created by Arthur on 4/29/25.
|
|
6
|
+
//
|
|
7
|
+
|
|
8
|
+
import Foundation
|
|
9
|
+
import AVFoundation
|
|
10
|
+
import ExpoModulesCore
|
|
11
|
+
|
|
12
|
+
// MARK: - Delegate Protocol
|
|
13
|
+
protocol AudioDeviceManagerDelegate: AnyObject {
|
|
14
|
+
func audioDeviceManager(_ manager: AudioDeviceManager, didDetectDisconnectionOfDevice deviceId: String)
|
|
15
|
+
// Future delegate methods can be added here
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
/// Manages audio device detection, selection and capabilities
|
|
19
|
+
class AudioDeviceManager {
|
|
20
|
+
// MARK: - Properties
|
|
21
|
+
weak var delegate: AudioDeviceManagerDelegate? // Add delegate property
|
|
22
|
+
|
|
23
|
+
// MARK: - Device Type Constants
|
|
24
|
+
|
|
25
|
+
// Constants for device types - standardized across platforms
|
|
26
|
+
private let deviceTypeBuiltinMic = "builtin_mic"
|
|
27
|
+
private let deviceTypeBluetooth = "bluetooth"
|
|
28
|
+
private let deviceTypeUSB = "usb"
|
|
29
|
+
private let deviceTypeWiredHeadset = "wired_headset"
|
|
30
|
+
private let deviceTypeWiredHeadphones = "wired_headphones"
|
|
31
|
+
private let deviceTypeSpeaker = "speaker"
|
|
32
|
+
private let deviceTypeUnknown = "unknown"
|
|
33
|
+
|
|
34
|
+
// Flag to prevent infinite loops
|
|
35
|
+
private static var isAudioSessionPrepared = false
|
|
36
|
+
private static var lastPreparationTime: TimeInterval = 0
|
|
37
|
+
|
|
38
|
+
// Observer handle
|
|
39
|
+
private var routeChangeObserver: Any?
|
|
40
|
+
|
|
41
|
+
// MARK: - Initialization and Deinitialization
|
|
42
|
+
init() {
|
|
43
|
+
// Start monitoring route changes on initialization
|
|
44
|
+
startMonitoringDeviceChanges()
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
deinit {
|
|
48
|
+
// Stop monitoring when the instance is deallocated
|
|
49
|
+
stopMonitoringDeviceChanges()
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
// MARK: - Public Methods
|
|
53
|
+
|
|
54
|
+
/// Maps AVAudioSession port types to standardized device types
|
|
55
|
+
func mapDeviceType(_ portType: AVAudioSession.Port) -> String {
|
|
56
|
+
Logger.debug("AudioDeviceManager", "Mapping device type for port: \(portType.rawValue)")
|
|
57
|
+
switch portType {
|
|
58
|
+
case .builtInMic:
|
|
59
|
+
return deviceTypeBuiltinMic
|
|
60
|
+
case .bluetoothHFP, .bluetoothA2DP, .bluetoothLE:
|
|
61
|
+
return deviceTypeBluetooth
|
|
62
|
+
case .headphones:
|
|
63
|
+
return deviceTypeWiredHeadphones
|
|
64
|
+
case .headsetMic:
|
|
65
|
+
return deviceTypeWiredHeadset
|
|
66
|
+
case .usbAudio:
|
|
67
|
+
return deviceTypeUSB
|
|
68
|
+
case .builtInSpeaker:
|
|
69
|
+
return deviceTypeSpeaker
|
|
70
|
+
default:
|
|
71
|
+
return deviceTypeUnknown
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
/// Prepares the audio session to detect all available devices, including Bluetooth
|
|
76
|
+
private func prepareAudioSession(force: Bool = false) -> Bool {
|
|
77
|
+
// Skip preparation if already prepared and not forcing
|
|
78
|
+
let now = Date().timeIntervalSince1970
|
|
79
|
+
let timeSinceLastPreparation = now - AudioDeviceManager.lastPreparationTime
|
|
80
|
+
|
|
81
|
+
if AudioDeviceManager.isAudioSessionPrepared && !force && timeSinceLastPreparation < 5.0 {
|
|
82
|
+
Logger.debug("AudioDeviceManager", "Audio session already prepared, skipping")
|
|
83
|
+
return true
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
Logger.debug("AudioDeviceManager", "Preparing audio session for device detection")
|
|
87
|
+
do {
|
|
88
|
+
let session = AVAudioSession.sharedInstance()
|
|
89
|
+
|
|
90
|
+
// Preserve existing session configuration if already set up for recording
|
|
91
|
+
if session.category == .playAndRecord {
|
|
92
|
+
Logger.debug("AudioDeviceManager", "Session already .playAndRecord, preserving categoryOptions")
|
|
93
|
+
try session.setActive(true, options: .notifyOthersOnDeactivation)
|
|
94
|
+
} else {
|
|
95
|
+
// Configure with options needed for Bluetooth detection
|
|
96
|
+
try session.setCategory(.playAndRecord, mode: .default, options: [.allowBluetooth, .allowBluetoothA2DP, .mixWithOthers])
|
|
97
|
+
try session.setActive(true, options: .notifyOthersOnDeactivation)
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
// Give the system a moment to detect Bluetooth devices if needed
|
|
101
|
+
// Minimal delay that still allows devices to be detected
|
|
102
|
+
Thread.sleep(forTimeInterval: 0.1)
|
|
103
|
+
|
|
104
|
+
// Mark as prepared
|
|
105
|
+
AudioDeviceManager.isAudioSessionPrepared = true
|
|
106
|
+
AudioDeviceManager.lastPreparationTime = now
|
|
107
|
+
|
|
108
|
+
Logger.debug("AudioDeviceManager", "Audio session prepared for device detection")
|
|
109
|
+
return true
|
|
110
|
+
} catch {
|
|
111
|
+
Logger.debug("AudioDeviceManager", "Failed to prepare audio session: \(error.localizedDescription)")
|
|
112
|
+
return false
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
/// Force a refresh of the audio session preparation
|
|
117
|
+
public func forceRefreshAudioSession() -> Bool {
|
|
118
|
+
// Only allow force refresh once every second to prevent excessive refreshes
|
|
119
|
+
let now = Date().timeIntervalSince1970
|
|
120
|
+
let timeSinceLastPreparation = now - AudioDeviceManager.lastPreparationTime
|
|
121
|
+
|
|
122
|
+
if timeSinceLastPreparation < 1.0 {
|
|
123
|
+
Logger.debug("AudioDeviceManager", "Skipping force refresh - too soon since last preparation (\(timeSinceLastPreparation) seconds)")
|
|
124
|
+
return false
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
return prepareAudioSession(force: true)
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
/// Gets capabilities for an audio input device
|
|
131
|
+
func getDeviceCapabilities(_ port: AVAudioSessionPortDescription) -> [String: Any] {
|
|
132
|
+
Logger.debug("AudioDeviceManager", "Getting capabilities for device: \(port.portName) (ID: \(port.uid))")
|
|
133
|
+
let session = AVAudioSession.sharedInstance()
|
|
134
|
+
|
|
135
|
+
// Test standard sample rates for support
|
|
136
|
+
let sampleRates = [8000, 16000, 22050, 44100, 48000, 96000].filter { rate in
|
|
137
|
+
let format = AVAudioFormat(commonFormat: .pcmFormatFloat32, sampleRate: Double(rate), channels: 1, interleaved: false)
|
|
138
|
+
return session.isInputAvailable && format != nil
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
return [
|
|
142
|
+
"sampleRates": sampleRates,
|
|
143
|
+
"channelCounts": [1, 2], // Most iOS devices support mono and stereo
|
|
144
|
+
"bitDepths": [16, 24], // Common bit depths on iOS
|
|
145
|
+
"hasEchoCancellation": true, // iOS doesn't expose this per-device, set to true as it's generally available
|
|
146
|
+
"hasNoiseSuppression": true, // iOS doesn't expose this per-device, set to true as it's generally available
|
|
147
|
+
"hasAutomaticGainControl": true // iOS doesn't expose this per-device, set to true as it's generally available
|
|
148
|
+
]
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
/// Gets a list of available audio input devices
|
|
152
|
+
func getAvailableInputDevices(promise: Promise) {
|
|
153
|
+
Logger.debug("AudioDeviceManager", "Getting available input devices")
|
|
154
|
+
|
|
155
|
+
// Prepare audio session if needed
|
|
156
|
+
let prepared = prepareAudioSession()
|
|
157
|
+
if !prepared {
|
|
158
|
+
Logger.debug("AudioDeviceManager", "Warning: Audio session preparation failed, device list may be incomplete")
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
do {
|
|
162
|
+
let session = AVAudioSession.sharedInstance()
|
|
163
|
+
|
|
164
|
+
// We should have already activated the session in prepareAudioSession
|
|
165
|
+
// But ensure it's active just in case
|
|
166
|
+
try session.setActive(true)
|
|
167
|
+
|
|
168
|
+
let currentPreferredInput = session.preferredInput
|
|
169
|
+
|
|
170
|
+
var devices = [[String: Any]]()
|
|
171
|
+
|
|
172
|
+
// First add current route devices as they're definitely available
|
|
173
|
+
for input in session.currentRoute.inputs {
|
|
174
|
+
let deviceType = mapDeviceType(input.portType)
|
|
175
|
+
let isDefault = currentPreferredInput == nil ?
|
|
176
|
+
(input.portType == .builtInMic) : // Default is usually built-in mic
|
|
177
|
+
(input.uid == currentPreferredInput?.uid)
|
|
178
|
+
|
|
179
|
+
let deviceId = normalizeBluetoothDeviceId(input.uid)
|
|
180
|
+
|
|
181
|
+
Logger.debug("AudioDeviceManager", "Current route device: \(input.portName) (type: \(deviceType), ID: \(deviceId))")
|
|
182
|
+
|
|
183
|
+
devices.append([
|
|
184
|
+
"id": deviceId,
|
|
185
|
+
"name": input.portName,
|
|
186
|
+
"type": deviceType,
|
|
187
|
+
"isDefault": isDefault,
|
|
188
|
+
"capabilities": getDeviceCapabilities(input),
|
|
189
|
+
"isAvailable": true,
|
|
190
|
+
"source": "currentRoute"
|
|
191
|
+
])
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
// Then add from availableInputs
|
|
195
|
+
if let availableInputs = session.availableInputs {
|
|
196
|
+
for port in availableInputs {
|
|
197
|
+
let deviceType = mapDeviceType(port.portType)
|
|
198
|
+
let isDefault = currentPreferredInput == nil ?
|
|
199
|
+
(port.portType == .builtInMic) : // Default is usually built-in mic
|
|
200
|
+
(port.uid == currentPreferredInput?.uid)
|
|
201
|
+
|
|
202
|
+
let deviceId = normalizeBluetoothDeviceId(port.uid)
|
|
203
|
+
|
|
204
|
+
// Skip if already in our list
|
|
205
|
+
if !devices.contains(where: { ($0["id"] as? String) == deviceId }) {
|
|
206
|
+
Logger.debug("AudioDeviceManager", "Available input: \(port.portName) (type: \(deviceType), ID: \(deviceId))")
|
|
207
|
+
|
|
208
|
+
devices.append([
|
|
209
|
+
"id": deviceId,
|
|
210
|
+
"name": port.portName,
|
|
211
|
+
"type": deviceType,
|
|
212
|
+
"isDefault": isDefault,
|
|
213
|
+
"capabilities": getDeviceCapabilities(port),
|
|
214
|
+
"isAvailable": true,
|
|
215
|
+
"source": "availableInputs"
|
|
216
|
+
])
|
|
217
|
+
}
|
|
218
|
+
}
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
Logger.debug("AudioDeviceManager", "Found \(devices.count) available input devices")
|
|
222
|
+
promise.resolve(devices)
|
|
223
|
+
} catch {
|
|
224
|
+
Logger.debug("AudioDeviceManager", "Error getting available input devices: \(error.localizedDescription)")
|
|
225
|
+
promise.reject("DEVICE_DETECTION_ERROR", "Failed to get available audio devices: \(error.localizedDescription)")
|
|
226
|
+
}
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
/// Gets the currently selected audio input device
|
|
230
|
+
func getCurrentInputDevice(promise: Promise) {
|
|
231
|
+
Logger.debug("AudioDeviceManager", "Getting current input device")
|
|
232
|
+
|
|
233
|
+
// Prepare audio session if needed
|
|
234
|
+
let prepared = prepareAudioSession()
|
|
235
|
+
if !prepared {
|
|
236
|
+
Logger.debug("AudioDeviceManager", "Warning: Audio session preparation failed, current device may not be correctly detected")
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
do {
|
|
240
|
+
let session = AVAudioSession.sharedInstance()
|
|
241
|
+
|
|
242
|
+
// We should have already activated the session in prepareAudioSession
|
|
243
|
+
// But ensure it's active just in case
|
|
244
|
+
try session.setActive(true)
|
|
245
|
+
|
|
246
|
+
// Check current route first
|
|
247
|
+
if let currentPort = session.currentRoute.inputs.first {
|
|
248
|
+
let deviceType = mapDeviceType(currentPort.portType)
|
|
249
|
+
let isDefault = session.preferredInput == nil || session.preferredInput?.portType == currentPort.portType
|
|
250
|
+
let deviceId = normalizeBluetoothDeviceId(currentPort.uid)
|
|
251
|
+
|
|
252
|
+
Logger.debug("AudioDeviceManager", "Current input device: \(currentPort.portName) (ID: \(deviceId), type: \(deviceType))")
|
|
253
|
+
|
|
254
|
+
let device: [String: Any] = [
|
|
255
|
+
"id": deviceId,
|
|
256
|
+
"name": currentPort.portName,
|
|
257
|
+
"type": deviceType,
|
|
258
|
+
"isDefault": isDefault,
|
|
259
|
+
"capabilities": getDeviceCapabilities(currentPort),
|
|
260
|
+
"isAvailable": true,
|
|
261
|
+
"source": "currentRoute"
|
|
262
|
+
]
|
|
263
|
+
|
|
264
|
+
promise.resolve(device)
|
|
265
|
+
return
|
|
266
|
+
}
|
|
267
|
+
|
|
268
|
+
// Fallback to preferred input
|
|
269
|
+
if let preferredInput = session.preferredInput {
|
|
270
|
+
let deviceType = mapDeviceType(preferredInput.portType)
|
|
271
|
+
let deviceId = normalizeBluetoothDeviceId(preferredInput.uid)
|
|
272
|
+
|
|
273
|
+
Logger.debug("AudioDeviceManager", "Current input from preferred: \(preferredInput.portName) (ID: \(deviceId), type: \(deviceType))")
|
|
274
|
+
|
|
275
|
+
let device: [String: Any] = [
|
|
276
|
+
"id": deviceId,
|
|
277
|
+
"name": preferredInput.portName,
|
|
278
|
+
"type": deviceType,
|
|
279
|
+
"isDefault": true,
|
|
280
|
+
"capabilities": getDeviceCapabilities(preferredInput),
|
|
281
|
+
"isAvailable": true,
|
|
282
|
+
"source": "preferredInput"
|
|
283
|
+
]
|
|
284
|
+
|
|
285
|
+
promise.resolve(device)
|
|
286
|
+
return
|
|
287
|
+
}
|
|
288
|
+
|
|
289
|
+
// No input device is currently selected
|
|
290
|
+
Logger.debug("AudioDeviceManager", "No current input device found")
|
|
291
|
+
promise.resolve(nil)
|
|
292
|
+
} catch {
|
|
293
|
+
Logger.debug("AudioDeviceManager", "Error getting current input device: \(error.localizedDescription)")
|
|
294
|
+
promise.reject("DEVICE_DETECTION_ERROR", "Failed to get current audio device: \(error.localizedDescription)")
|
|
295
|
+
}
|
|
296
|
+
}
|
|
297
|
+
|
|
298
|
+
/// Gets the default audio input device (usually built-in mic)
|
|
299
|
+
/// This is an async version useful for fallback logic.
|
|
300
|
+
func getDefaultInputDevice() async -> AudioDevice? {
|
|
301
|
+
Logger.debug("AudioDeviceManager", "Getting default input device")
|
|
302
|
+
|
|
303
|
+
let prepared = prepareAudioSession()
|
|
304
|
+
if !prepared {
|
|
305
|
+
Logger.debug("AudioDeviceManager", "Warning: Audio session preparation failed, default device detection may be inaccurate")
|
|
306
|
+
}
|
|
307
|
+
|
|
308
|
+
let session = AVAudioSession.sharedInstance()
|
|
309
|
+
do {
|
|
310
|
+
try session.setActive(true) // Ensure session is active
|
|
311
|
+
|
|
312
|
+
// Find the built-in microphone port, which is typically the default fallback
|
|
313
|
+
if let defaultPort = session.availableInputs?.first(where: { $0.portType == .builtInMic }) {
|
|
314
|
+
let deviceType = mapDeviceType(defaultPort.portType)
|
|
315
|
+
let deviceId = normalizeBluetoothDeviceId(defaultPort.uid)
|
|
316
|
+
let capabilities = getDeviceCapabilities(defaultPort)
|
|
317
|
+
|
|
318
|
+
Logger.debug("AudioDeviceManager", "Found default device: \(defaultPort.portName) (ID: \(deviceId), Type: \(deviceType))")
|
|
319
|
+
|
|
320
|
+
// Convert capabilities dictionary to Capabilities struct/object if needed
|
|
321
|
+
let audioCapabilities = AudioDeviceCapabilities(
|
|
322
|
+
sampleRates: capabilities["sampleRates"] as? [Int] ?? [],
|
|
323
|
+
channelCounts: capabilities["channelCounts"] as? [Int] ?? [],
|
|
324
|
+
bitDepths: capabilities["bitDepths"] as? [Int] ?? []
|
|
325
|
+
// Add boolean flags if available in your dictionary
|
|
326
|
+
)
|
|
327
|
+
|
|
328
|
+
return AudioDevice(
|
|
329
|
+
id: deviceId,
|
|
330
|
+
name: defaultPort.portName,
|
|
331
|
+
type: deviceType,
|
|
332
|
+
isDefault: true, // Assume it's the default we're looking for
|
|
333
|
+
capabilities: audioCapabilities,
|
|
334
|
+
isAvailable: true // It's available if found here
|
|
335
|
+
)
|
|
336
|
+
} else {
|
|
337
|
+
Logger.debug("AudioDeviceManager", "Could not find built-in mic as default device.")
|
|
338
|
+
return nil
|
|
339
|
+
}
|
|
340
|
+
} catch {
|
|
341
|
+
Logger.debug("AudioDeviceManager", "Error getting default input device: \(error)")
|
|
342
|
+
return nil
|
|
343
|
+
}
|
|
344
|
+
}
|
|
345
|
+
|
|
346
|
+
/// Selects a specific audio input device for recording
|
|
347
|
+
func selectInputDevice(_ deviceId: String, promise: Promise) {
|
|
348
|
+
Logger.debug("AudioDeviceManager", "Attempting to select input device with ID: \(deviceId)")
|
|
349
|
+
|
|
350
|
+
// Prepare audio session - use force: true for device selection to ensure we get the latest devices
|
|
351
|
+
let prepared = prepareAudioSession(force: true)
|
|
352
|
+
if !prepared {
|
|
353
|
+
Logger.debug("AudioDeviceManager", "Warning: Audio session preparation failed, device selection may not work correctly")
|
|
354
|
+
}
|
|
355
|
+
|
|
356
|
+
do {
|
|
357
|
+
let session = AVAudioSession.sharedInstance()
|
|
358
|
+
|
|
359
|
+
// Ensure the session is active
|
|
360
|
+
try session.setActive(true)
|
|
361
|
+
|
|
362
|
+
// For Bluetooth devices, normalize and match by prefix
|
|
363
|
+
let normalizedRequestedId = normalizeBluetoothDeviceId(deviceId)
|
|
364
|
+
let isBluetoothDevice = deviceId.contains(":")
|
|
365
|
+
|
|
366
|
+
Logger.debug("AudioDeviceManager", "Selecting \(isBluetoothDevice ? "Bluetooth" : "non-Bluetooth") device with normalized ID: \(normalizedRequestedId)")
|
|
367
|
+
|
|
368
|
+
// Find the device with the specified ID
|
|
369
|
+
let selectedPort: AVAudioSessionPortDescription?
|
|
370
|
+
|
|
371
|
+
if isBluetoothDevice {
|
|
372
|
+
// For Bluetooth devices, match by normalized ID
|
|
373
|
+
selectedPort = session.availableInputs?.first { port in
|
|
374
|
+
let portNormalizedId = normalizeBluetoothDeviceId(port.uid)
|
|
375
|
+
let matches = portNormalizedId == normalizedRequestedId
|
|
376
|
+
Logger.debug("AudioDeviceManager", "Checking device \(port.portName) (ID: \(port.uid), Normalized: \(portNormalizedId)) - Matches: \(matches)")
|
|
377
|
+
return matches
|
|
378
|
+
}
|
|
379
|
+
} else {
|
|
380
|
+
// For non-Bluetooth devices, direct match
|
|
381
|
+
selectedPort = session.availableInputs?.first { port in
|
|
382
|
+
let matches = port.uid == deviceId
|
|
383
|
+
Logger.debug("AudioDeviceManager", "Checking device \(port.portName) (ID: \(port.uid)) - Matches: \(matches)")
|
|
384
|
+
return matches
|
|
385
|
+
}
|
|
386
|
+
}
|
|
387
|
+
|
|
388
|
+
guard let selectedPort = selectedPort else {
|
|
389
|
+
Logger.debug("AudioDeviceManager", "Device not found with ID \(deviceId)")
|
|
390
|
+
|
|
391
|
+
// Log all available devices to help debugging
|
|
392
|
+
if let availableInputs = session.availableInputs {
|
|
393
|
+
Logger.debug("AudioDeviceManager", "Available devices:")
|
|
394
|
+
for (index, device) in availableInputs.enumerated() {
|
|
395
|
+
Logger.debug("AudioDeviceManager", "\(index+1). \(device.portName) (ID: \(device.uid), Normalized: \(normalizeBluetoothDeviceId(device.uid)))")
|
|
396
|
+
}
|
|
397
|
+
} else {
|
|
398
|
+
Logger.debug("AudioDeviceManager", "No available devices found")
|
|
399
|
+
}
|
|
400
|
+
|
|
401
|
+
promise.reject("DEVICE_NOT_FOUND", "The selected audio device is not available")
|
|
402
|
+
return
|
|
403
|
+
}
|
|
404
|
+
|
|
405
|
+
// Set the preferred input device
|
|
406
|
+
Logger.debug("AudioDeviceManager", "Setting preferred input to: \(selectedPort.portName) (ID: \(selectedPort.uid))")
|
|
407
|
+
try session.setPreferredInput(selectedPort)
|
|
408
|
+
|
|
409
|
+
// Verify selection
|
|
410
|
+
if let currentInput = session.currentRoute.inputs.first {
|
|
411
|
+
let succeeded = (currentInput.uid == selectedPort.uid ||
|
|
412
|
+
normalizeBluetoothDeviceId(currentInput.uid) == normalizeBluetoothDeviceId(selectedPort.uid))
|
|
413
|
+
Logger.debug("AudioDeviceManager", "Device selection \(succeeded ? "succeeded" : "failed") - Current device: \(currentInput.portName) (ID: \(currentInput.uid))")
|
|
414
|
+
}
|
|
415
|
+
|
|
416
|
+
Logger.debug("AudioDeviceManager", "Device selected successfully")
|
|
417
|
+
promise.resolve(true)
|
|
418
|
+
} catch {
|
|
419
|
+
Logger.debug("AudioDeviceManager", "Failed to select device: \(error.localizedDescription)")
|
|
420
|
+
promise.reject("DEVICE_SELECTION_FAILED", "Failed to select audio device: \(error.localizedDescription)")
|
|
421
|
+
}
|
|
422
|
+
}
|
|
423
|
+
|
|
424
|
+
/// Selects a specific audio input device asynchronously (useful for internal calls)
|
|
425
|
+
func selectDevice(_ deviceId: String) async -> Bool {
|
|
426
|
+
Logger.debug("AudioDeviceManager", "Attempting to select input device with ID: \(deviceId) (async)")
|
|
427
|
+
|
|
428
|
+
let prepared = prepareAudioSession(force: true)
|
|
429
|
+
if !prepared {
|
|
430
|
+
Logger.debug("AudioDeviceManager", "Warning: Audio session preparation failed, device selection may not work correctly")
|
|
431
|
+
return false
|
|
432
|
+
}
|
|
433
|
+
|
|
434
|
+
do {
|
|
435
|
+
let session = AVAudioSession.sharedInstance()
|
|
436
|
+
try session.setActive(true)
|
|
437
|
+
|
|
438
|
+
let normalizedRequestedId = normalizeBluetoothDeviceId(deviceId)
|
|
439
|
+
let isBluetoothDevice = deviceId.contains(":")
|
|
440
|
+
|
|
441
|
+
Logger.debug("AudioDeviceManager", "Selecting \(isBluetoothDevice ? "Bluetooth" : "non-Bluetooth") device with normalized ID: \(normalizedRequestedId)")
|
|
442
|
+
|
|
443
|
+
let selectedPort: AVAudioSessionPortDescription?
|
|
444
|
+
if isBluetoothDevice {
|
|
445
|
+
selectedPort = session.availableInputs?.first { port in
|
|
446
|
+
normalizeBluetoothDeviceId(port.uid) == normalizedRequestedId
|
|
447
|
+
}
|
|
448
|
+
} else {
|
|
449
|
+
selectedPort = session.availableInputs?.first { $0.uid == deviceId }
|
|
450
|
+
}
|
|
451
|
+
|
|
452
|
+
guard let portToSet = selectedPort else {
|
|
453
|
+
Logger.debug("AudioDeviceManager", "Device not found with ID \(deviceId) for async selection")
|
|
454
|
+
return false
|
|
455
|
+
}
|
|
456
|
+
|
|
457
|
+
Logger.debug("AudioDeviceManager", "Setting preferred input to: \(portToSet.portName) (ID: \(portToSet.uid)) (async)")
|
|
458
|
+
try session.setPreferredInput(portToSet)
|
|
459
|
+
// Add a small delay hoping the system applies the change before potential next operations
|
|
460
|
+
try await Task.sleep(nanoseconds: 100_000_000) // 0.1 seconds
|
|
461
|
+
|
|
462
|
+
// Optional: Verify selection succeeded (might be less reliable immediately after setting)
|
|
463
|
+
if let currentInput = session.currentRoute.inputs.first {
|
|
464
|
+
let succeeded = (currentInput.uid == portToSet.uid || normalizeBluetoothDeviceId(currentInput.uid) == normalizedRequestedId)
|
|
465
|
+
Logger.debug("AudioDeviceManager", "Async selection verification: \(succeeded ? "succeeded" : "failed")")
|
|
466
|
+
return succeeded
|
|
467
|
+
} else {
|
|
468
|
+
// If no current input after setting, assume failure
|
|
469
|
+
return false
|
|
470
|
+
}
|
|
471
|
+
|
|
472
|
+
} catch {
|
|
473
|
+
Logger.debug("AudioDeviceManager", "Failed to select device asynchronously: \(error.localizedDescription)")
|
|
474
|
+
return false
|
|
475
|
+
}
|
|
476
|
+
}
|
|
477
|
+
|
|
478
|
+
/// Determines if a device is still available
|
|
479
|
+
func isDeviceAvailable(_ deviceId: String) -> Bool {
|
|
480
|
+
Logger.debug("AudioDeviceManager", "Checking availability for device ID: \(deviceId)")
|
|
481
|
+
|
|
482
|
+
// Prepare audio session if needed
|
|
483
|
+
let prepared = prepareAudioSession()
|
|
484
|
+
if !prepared {
|
|
485
|
+
Logger.debug("AudioDeviceManager", "Warning: Audio session preparation failed, device availability check may not be accurate")
|
|
486
|
+
}
|
|
487
|
+
|
|
488
|
+
let session = AVAudioSession.sharedInstance()
|
|
489
|
+
|
|
490
|
+
// Handle Bluetooth devices with multiple profiles (SCO, A2DP, etc.)
|
|
491
|
+
let isBluetoothDevice = deviceId.contains(":") // Most Bluetooth devices have MAC addresses with colons
|
|
492
|
+
|
|
493
|
+
if isBluetoothDevice {
|
|
494
|
+
// For Bluetooth devices, check if any device with the same MAC address prefix is available
|
|
495
|
+
let baseDeviceId = deviceId.split(separator: "-").first ?? Substring(deviceId)
|
|
496
|
+
|
|
497
|
+
// Log all available inputs for debugging
|
|
498
|
+
Logger.debug("AudioDeviceManager", "Available devices to check against:")
|
|
499
|
+
if let availableInputs = session.availableInputs {
|
|
500
|
+
for (index, device) in availableInputs.enumerated() {
|
|
501
|
+
let normalizedId = normalizeBluetoothDeviceId(device.uid)
|
|
502
|
+
let matches = device.uid.starts(with: String(baseDeviceId))
|
|
503
|
+
Logger.debug("AudioDeviceManager", "\(index+1). \(device.portName) (ID: \(device.uid), Normalized: \(normalizedId)) - Matches: \(matches)")
|
|
504
|
+
}
|
|
505
|
+
} else {
|
|
506
|
+
Logger.debug("AudioDeviceManager", "No available devices found")
|
|
507
|
+
}
|
|
508
|
+
|
|
509
|
+
// Also check current route
|
|
510
|
+
for (index, input) in session.currentRoute.inputs.enumerated() {
|
|
511
|
+
let normalizedId = normalizeBluetoothDeviceId(input.uid)
|
|
512
|
+
let matches = input.uid.starts(with: String(baseDeviceId))
|
|
513
|
+
Logger.debug("AudioDeviceManager", "Current route input \(index+1): \(input.portName) (ID: \(input.uid), Normalized: \(normalizedId)) - Matches: \(matches)")
|
|
514
|
+
}
|
|
515
|
+
|
|
516
|
+
let result = session.availableInputs?.contains { $0.uid.starts(with: String(baseDeviceId)) } ?? false
|
|
517
|
+
Logger.debug("AudioDeviceManager", "Bluetooth device \(deviceId) with base ID \(baseDeviceId) available: \(result)")
|
|
518
|
+
return result
|
|
519
|
+
} else {
|
|
520
|
+
// Standard device ID check for non-Bluetooth devices
|
|
521
|
+
return session.availableInputs?.contains { $0.uid == deviceId } ?? false
|
|
522
|
+
}
|
|
523
|
+
}
|
|
524
|
+
|
|
525
|
+
/// Resets the selected device to system default (usually built-in mic)
|
|
526
|
+
/// - Parameter completion: Callback with success (Bool) and optional error
|
|
527
|
+
func resetToDefaultDevice(completion: @escaping (Bool, Error?) -> Void) {
|
|
528
|
+
Logger.debug("AudioDeviceManager", "Attempting to reset to default input device")
|
|
529
|
+
|
|
530
|
+
// Prepare audio session if needed
|
|
531
|
+
let prepared = prepareAudioSession()
|
|
532
|
+
if !prepared {
|
|
533
|
+
Logger.debug("AudioDeviceManager", "Warning: Audio session preparation failed, device reset may not work correctly")
|
|
534
|
+
}
|
|
535
|
+
|
|
536
|
+
do {
|
|
537
|
+
let session = AVAudioSession.sharedInstance()
|
|
538
|
+
|
|
539
|
+
// Log current device before reset
|
|
540
|
+
if let currentDevice = session.currentRoute.inputs.first {
|
|
541
|
+
Logger.debug("AudioDeviceManager", "Current device before reset: \(currentDevice.portName) (ID: \(currentDevice.uid))")
|
|
542
|
+
} else {
|
|
543
|
+
Logger.debug("AudioDeviceManager", "No current device before reset")
|
|
544
|
+
}
|
|
545
|
+
|
|
546
|
+
// Setting preferred input to nil lets the system choose the default
|
|
547
|
+
try session.setPreferredInput(nil)
|
|
548
|
+
|
|
549
|
+
// Log the device after reset
|
|
550
|
+
if let newDevice = session.currentRoute.inputs.first {
|
|
551
|
+
Logger.debug("AudioDeviceManager", "Reset to default device: \(newDevice.portName) (ID: \(newDevice.uid))")
|
|
552
|
+
|
|
553
|
+
// Check if it's actually the built-in mic (which is the typical default)
|
|
554
|
+
let isBuiltIn = newDevice.portType == .builtInMic
|
|
555
|
+
Logger.debug("AudioDeviceManager", "Reset device is built-in mic: \(isBuiltIn)")
|
|
556
|
+
} else {
|
|
557
|
+
Logger.debug("AudioDeviceManager", "No device found after reset")
|
|
558
|
+
}
|
|
559
|
+
|
|
560
|
+
completion(true, nil)
|
|
561
|
+
} catch {
|
|
562
|
+
Logger.debug("AudioDeviceManager", "Failed to reset to default device: \(error.localizedDescription)")
|
|
563
|
+
completion(false, error)
|
|
564
|
+
}
|
|
565
|
+
}
|
|
566
|
+
|
|
567
|
+
/// Starts monitoring device connection/disconnection events
|
|
568
|
+
private func startMonitoringDeviceChanges() {
|
|
569
|
+
// Ensure we don't add multiple observers
|
|
570
|
+
stopMonitoringDeviceChanges()
|
|
571
|
+
|
|
572
|
+
Logger.debug("AudioDeviceManager", "Starting device change monitoring")
|
|
573
|
+
routeChangeObserver = NotificationCenter.default.addObserver(
|
|
574
|
+
forName: AVAudioSession.routeChangeNotification,
|
|
575
|
+
object: nil,
|
|
576
|
+
queue: .main // Process on main queue to avoid threading issues with delegate calls
|
|
577
|
+
) { [weak self] notification in
|
|
578
|
+
self?.handleRouteChange(notification)
|
|
579
|
+
}
|
|
580
|
+
}
|
|
581
|
+
|
|
582
|
+
/// Stops monitoring device changes
|
|
583
|
+
private func stopMonitoringDeviceChanges() {
|
|
584
|
+
if let observer = routeChangeObserver {
|
|
585
|
+
Logger.debug("AudioDeviceManager", "Stopping device change monitoring")
|
|
586
|
+
NotificationCenter.default.removeObserver(observer)
|
|
587
|
+
routeChangeObserver = nil
|
|
588
|
+
}
|
|
589
|
+
}
|
|
590
|
+
|
|
591
|
+
/// Handles route change notifications to detect device connections and disconnections
|
|
592
|
+
@objc private func handleRouteChange(_ notification: Notification) {
|
|
593
|
+
guard let userInfo = notification.userInfo,
|
|
594
|
+
let reasonValue = userInfo[AVAudioSessionRouteChangeReasonKey] as? UInt,
|
|
595
|
+
let reason = AVAudioSession.RouteChangeReason(rawValue: reasonValue) else {
|
|
596
|
+
return
|
|
597
|
+
}
|
|
598
|
+
|
|
599
|
+
Logger.debug("AudioDeviceManager", "Route change detected, reason: \(reason.rawValue)")
|
|
600
|
+
|
|
601
|
+
// Only proceed if a device was potentially removed or added or the route changed significantly
|
|
602
|
+
guard reason == .oldDeviceUnavailable || reason == .newDeviceAvailable || reason == .override || reason == .routeConfigurationChange else {
|
|
603
|
+
Logger.debug("AudioDeviceManager", "Ignoring route change reason: \(reason.rawValue)")
|
|
604
|
+
return
|
|
605
|
+
}
|
|
606
|
+
|
|
607
|
+
// Get the *previous* route description
|
|
608
|
+
guard let previousRoute = userInfo[AVAudioSessionRouteChangePreviousRouteKey] as? AVAudioSessionRouteDescription else {
|
|
609
|
+
Logger.debug("AudioDeviceManager", "No previous route info found for device change check.")
|
|
610
|
+
return
|
|
611
|
+
}
|
|
612
|
+
|
|
613
|
+
// Get the *current* available input devices
|
|
614
|
+
let currentInputs = AVAudioSession.sharedInstance().availableInputs ?? []
|
|
615
|
+
let currentInputIds = Set(currentInputs.map { normalizeBluetoothDeviceId($0.uid) })
|
|
616
|
+
let previousInputIds = Set(previousRoute.inputs.map { normalizeBluetoothDeviceId($0.uid) })
|
|
617
|
+
|
|
618
|
+
// Check for DISCONNECTED devices (were in previous route but not in current available)
|
|
619
|
+
for previousInputPort in previousRoute.inputs {
|
|
620
|
+
let normalizedPreviousId = normalizeBluetoothDeviceId(previousInputPort.uid)
|
|
621
|
+
if !currentInputIds.contains(normalizedPreviousId) {
|
|
622
|
+
Logger.debug("AudioDeviceManager", "Detected disconnection of device: \(previousInputPort.portName) (Normalized ID: \(normalizedPreviousId))")
|
|
623
|
+
// Keep existing disconnection delegate method unchanged
|
|
624
|
+
delegate?.audioDeviceManager(self, didDetectDisconnectionOfDevice: normalizedPreviousId)
|
|
625
|
+
}
|
|
626
|
+
}
|
|
627
|
+
|
|
628
|
+
// Check for CONNECTED devices (are in current available but were not in previous route)
|
|
629
|
+
for currentInput in currentInputs {
|
|
630
|
+
let normalizedCurrentId = normalizeBluetoothDeviceId(currentInput.uid)
|
|
631
|
+
if !previousInputIds.contains(normalizedCurrentId) {
|
|
632
|
+
Logger.debug("AudioDeviceManager", "Detected connection of device: \(currentInput.portName) (Normalized ID: \(normalizedCurrentId))")
|
|
633
|
+
// Emit connection event via notification
|
|
634
|
+
NotificationCenter.default.post(
|
|
635
|
+
name: NSNotification.Name("DeviceConnected"),
|
|
636
|
+
object: nil,
|
|
637
|
+
userInfo: ["deviceId": normalizedCurrentId]
|
|
638
|
+
)
|
|
639
|
+
}
|
|
640
|
+
}
|
|
641
|
+
}
|
|
642
|
+
|
|
643
|
+
/// Normalizes Bluetooth device IDs by removing profile suffixes
|
|
644
|
+
public func normalizeBluetoothDeviceId(_ deviceId: String) -> String {
|
|
645
|
+
// For Bluetooth devices with MAC addresses and profile suffixes (like -tsco)
|
|
646
|
+
if deviceId.contains(":") && deviceId.contains("-") {
|
|
647
|
+
// Split by the hyphen and take the first part (the MAC address)
|
|
648
|
+
return deviceId.split(separator: "-").first.map(String.init) ?? deviceId
|
|
649
|
+
}
|
|
650
|
+
return deviceId
|
|
651
|
+
}
|
|
652
|
+
}
|
|
653
|
+
|
|
654
|
+
// Add structure for AudioDeviceCapabilities if not defined elsewhere
|
|
655
|
+
struct AudioDeviceCapabilities {
|
|
656
|
+
let sampleRates: [Int]
|
|
657
|
+
let channelCounts: [Int]
|
|
658
|
+
let bitDepths: [Int]
|
|
659
|
+
// Add boolean flags if needed
|
|
660
|
+
}
|
|
661
|
+
|
|
662
|
+
// Add structure for AudioDevice if not defined elsewhere
|
|
663
|
+
struct AudioDevice {
|
|
664
|
+
let id: String
|
|
665
|
+
let name: String
|
|
666
|
+
let type: String
|
|
667
|
+
let isDefault: Bool
|
|
668
|
+
let capabilities: AudioDeviceCapabilities
|
|
669
|
+
let isAvailable: Bool
|
|
670
|
+
}
|