@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,1111 @@
|
|
|
1
|
+
// packages/audio-studio/ios/AudioStudioModule.swift
|
|
2
|
+
import ExpoModulesCore
|
|
3
|
+
import AVFoundation
|
|
4
|
+
|
|
5
|
+
// Constants
|
|
6
|
+
private let audioDataEvent: String = "AudioData"
|
|
7
|
+
private let audioAnalysisEvent: String = "AudioAnalysis"
|
|
8
|
+
private let recordingInterruptedEvent: String = "onRecordingInterrupted"
|
|
9
|
+
private let deviceChangedEvent: String = "deviceChangedEvent"
|
|
10
|
+
private let trimProgressEvent: String = "TrimProgress"
|
|
11
|
+
private let errorEvent: String = "error"
|
|
12
|
+
private let DEFAULT_SEGMENT_DURATION_MS = 100
|
|
13
|
+
private let audioDeviceTypeBuiltinMic = "builtin_mic"
|
|
14
|
+
private let audioDeviceTypeBluetooth = "bluetooth"
|
|
15
|
+
private let audioDeviceTypeUSB = "usb"
|
|
16
|
+
private let audioDeviceTypeWiredHeadset = "wired_headset"
|
|
17
|
+
private let audioDeviceTypeWiredHeadphones = "wired_headphones"
|
|
18
|
+
private let audioDeviceTypeSpeaker = "speaker"
|
|
19
|
+
private let audioDeviceTypeUnknown = "unknown"
|
|
20
|
+
|
|
21
|
+
public class AudioStudioModule: Module, AudioStreamManagerDelegate, AudioDeviceManagerDelegate {
|
|
22
|
+
private var streamManager = AudioStreamManager()
|
|
23
|
+
private let notificationCenter = UNUserNotificationCenter.current()
|
|
24
|
+
private let notificationIdentifier = "audio_recording_notification"
|
|
25
|
+
private var deviceManager = AudioDeviceManager()
|
|
26
|
+
private var deviceChangeObserver: Any?
|
|
27
|
+
|
|
28
|
+
public func definition() -> ModuleDefinition {
|
|
29
|
+
Name("AudioStudio")
|
|
30
|
+
|
|
31
|
+
// Defines event names that the module can send to JavaScript.
|
|
32
|
+
Events([
|
|
33
|
+
audioDataEvent,
|
|
34
|
+
audioAnalysisEvent,
|
|
35
|
+
recordingInterruptedEvent,
|
|
36
|
+
deviceChangedEvent,
|
|
37
|
+
trimProgressEvent,
|
|
38
|
+
errorEvent
|
|
39
|
+
])
|
|
40
|
+
|
|
41
|
+
OnCreate {
|
|
42
|
+
Logger.debug("AudioStudioModule", "Module created, setting delegate and starting device monitoring.")
|
|
43
|
+
streamManager.delegate = self
|
|
44
|
+
// Set up device manager delegate to emit device change events
|
|
45
|
+
deviceManager.delegate = self
|
|
46
|
+
|
|
47
|
+
// Listen for device connection notifications (minimal addition)
|
|
48
|
+
NotificationCenter.default.addObserver(
|
|
49
|
+
forName: NSNotification.Name("DeviceConnected"),
|
|
50
|
+
object: nil,
|
|
51
|
+
queue: .main
|
|
52
|
+
) { [weak self] notification in
|
|
53
|
+
if let deviceId = notification.userInfo?["deviceId"] as? String {
|
|
54
|
+
Logger.debug("AudioStudioModule", "Device connected: \(deviceId)")
|
|
55
|
+
self?.sendEvent(deviceChangedEvent, [
|
|
56
|
+
"type": "deviceConnected",
|
|
57
|
+
"deviceId": deviceId
|
|
58
|
+
])
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
OnDestroy {
|
|
64
|
+
Logger.debug("AudioStudioModule", "Module destroyed, stopping device monitoring.")
|
|
65
|
+
_ = streamManager.stopRecording()
|
|
66
|
+
// Clear device manager delegate
|
|
67
|
+
deviceManager.delegate = nil
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
/// Extracts audio analysis data from an audio file.
|
|
71
|
+
///
|
|
72
|
+
/// - Parameters:
|
|
73
|
+
/// - options: A dictionary containing:
|
|
74
|
+
/// - `fileUri`: The URI of the audio file.
|
|
75
|
+
/// - `pointsPerSecond`: The number of data points to extract per second of audio.
|
|
76
|
+
/// - `features`: A dictionary specifying which features to extract (e.g., `energy`, `mfcc`, `rms`, etc.).
|
|
77
|
+
/// - promise: A promise to resolve with the extracted audio analysis data or reject with an error.
|
|
78
|
+
/// - Returns: Promise to be resolved with audio analysis data.
|
|
79
|
+
AsyncFunction("extractAudioAnalysis") { (options: [String: Any], promise: Promise) in
|
|
80
|
+
Logger.debug("AudioStudioModule", "extractAudioAnalysis called with options: \(options)")
|
|
81
|
+
guard let fileUri = options["fileUri"] as? String,
|
|
82
|
+
let url = URL(string: fileUri) else {
|
|
83
|
+
Logger.error("AudioStudioModule", "extractAudioAnalysis: Invalid file URI.")
|
|
84
|
+
promise.reject("INVALID_ARGUMENTS", "Invalid file URI provided")
|
|
85
|
+
return
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
// Get time or byte range options
|
|
89
|
+
let startTimeMs = options["startTimeMs"] as? Double
|
|
90
|
+
let endTimeMs = options["endTimeMs"] as? Double
|
|
91
|
+
let position = options["position"] as? Int
|
|
92
|
+
let byteLength = options["length"] as? Int
|
|
93
|
+
|
|
94
|
+
// Validate ranges - can have time range OR byte range OR no range
|
|
95
|
+
let hasTimeRange = startTimeMs != nil && endTimeMs != nil
|
|
96
|
+
let hasByteRange = position != nil && byteLength != nil
|
|
97
|
+
|
|
98
|
+
// Only throw if both ranges are provided
|
|
99
|
+
guard !(hasTimeRange && hasByteRange) else {
|
|
100
|
+
promise.reject("INVALID_ARGUMENTS", "Cannot specify both time range and byte range")
|
|
101
|
+
return
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
let features = options["features"] as? [String: Bool] ?? [:]
|
|
105
|
+
let featureOptions = self.extractFeatureOptions(from: features)
|
|
106
|
+
let segmentDurationMs = options["segmentDurationMs"] as? Int ?? DEFAULT_SEGMENT_DURATION_MS // Default value of 100ms
|
|
107
|
+
|
|
108
|
+
DispatchQueue.global().async(execute: {
|
|
109
|
+
do {
|
|
110
|
+
let audioFile = try AVAudioFile(forReading: url)
|
|
111
|
+
let bitDepth = audioFile.fileFormat.settings[AVLinearPCMBitDepthKey] as? Int ?? 16
|
|
112
|
+
let numberOfChannels = Int(audioFile.fileFormat.channelCount)
|
|
113
|
+
let sampleRate = audioFile.fileFormat.sampleRate
|
|
114
|
+
|
|
115
|
+
// Convert time range to byte range if needed
|
|
116
|
+
let effectivePosition: Int?
|
|
117
|
+
let effectiveLength: Int?
|
|
118
|
+
|
|
119
|
+
if hasTimeRange {
|
|
120
|
+
let bytesPerSecond = Int(sampleRate) * numberOfChannels * (bitDepth / 8)
|
|
121
|
+
effectivePosition = Int(startTimeMs! * Double(bytesPerSecond) / 1000.0)
|
|
122
|
+
effectiveLength = Int((endTimeMs! - startTimeMs!) * Double(bytesPerSecond) / 1000.0)
|
|
123
|
+
} else {
|
|
124
|
+
effectivePosition = position
|
|
125
|
+
effectiveLength = byteLength
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
Logger.debug("AudioStudioModule", "extractAudioAnalysis: Processing started for \(fileUri)")
|
|
129
|
+
let audioProcessor = try AudioProcessor(url: url, resolve: { result in
|
|
130
|
+
Logger.warn("AudioStudioModule", "extractAudioAnalysis: AudioProcessor resolve called unexpectedly.")
|
|
131
|
+
}, reject: { code, message in
|
|
132
|
+
Logger.warn("AudioStudioModule", "extractAudioAnalysis: AudioProcessor reject called unexpectedly: \(code) - \(message)")
|
|
133
|
+
})
|
|
134
|
+
|
|
135
|
+
if let result = audioProcessor.processAudioData(
|
|
136
|
+
numberOfSamples: nil,
|
|
137
|
+
offset: 0,
|
|
138
|
+
length: nil,
|
|
139
|
+
segmentDurationMs: segmentDurationMs,
|
|
140
|
+
featureOptions: featureOptions,
|
|
141
|
+
bitDepth: bitDepth,
|
|
142
|
+
numberOfChannels: numberOfChannels,
|
|
143
|
+
position: effectivePosition,
|
|
144
|
+
byteLength: effectiveLength
|
|
145
|
+
) {
|
|
146
|
+
Logger.debug("AudioStudioModule", "extractAudioAnalysis: Processing successful for \(fileUri)")
|
|
147
|
+
promise.resolve(result.toDictionary())
|
|
148
|
+
} else {
|
|
149
|
+
Logger.error("AudioStudioModule", "extractAudioAnalysis: audioProcessor.processAudioData returned nil for \(fileUri)")
|
|
150
|
+
promise.reject("PROCESSING_ERROR", "Failed to process audio data")
|
|
151
|
+
}
|
|
152
|
+
} catch {
|
|
153
|
+
Logger.error("AudioStudioModule", "extractAudioAnalysis: Error initializing AudioProcessor for \(fileUri): \(error.localizedDescription)")
|
|
154
|
+
promise.reject("PROCESSING_ERROR", "Failed to initialize audio processor: \(error.localizedDescription)")
|
|
155
|
+
}
|
|
156
|
+
})
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
|
|
160
|
+
/// Asynchronously starts audio recording with the given settings.
|
|
161
|
+
///
|
|
162
|
+
/// - Parameters:
|
|
163
|
+
/// - options: A dictionary containing:
|
|
164
|
+
/// - `sampleRate`: The sample rate for recording (default is 16000.0).
|
|
165
|
+
/// - `channelConfig`: The number of channels (default is 1 for mono).
|
|
166
|
+
/// - `audioFormat`: The bit depth for recording (default is 16 bits).
|
|
167
|
+
/// - `interval`: The interval in milliseconds at which to emit recording data (default is 1000 ms).
|
|
168
|
+
/// - `intervalAnalysis`: The interval in milliseconds at which to emit analysis data (default is 500 ms).
|
|
169
|
+
/// - `enableProcessing`: Boolean to enable/disable audio processing (default is false).
|
|
170
|
+
/// - `pointsPerSecond`: The number of data points to extract per second of audio (default is 20).
|
|
171
|
+
/// - `algorithm`: The algorithm to use for extraction (default is "rms").
|
|
172
|
+
/// - `featureOptions`: A dictionary of feature options to extract (default is empty).
|
|
173
|
+
/// - `maxRecentDataDuration`: The maximum duration of recent data to keep for processing (default is 10.0 seconds).
|
|
174
|
+
/// - `compression`: A dictionary containing:
|
|
175
|
+
/// - `enabled`: Boolean to enable/disable compression (default is false).
|
|
176
|
+
/// - `format`: The compression format (default is "aac").
|
|
177
|
+
/// - `bitrate`: The compression bitrate in bps (default is 128000).
|
|
178
|
+
/// - promise: A promise to resolve with the recording settings or reject with an error.
|
|
179
|
+
AsyncFunction("startRecording") { (options: [String: Any], promise: Promise) in
|
|
180
|
+
Logger.debug("AudioStudioModule", "startRecording called")
|
|
181
|
+
self.checkMicrophonePermission { granted in
|
|
182
|
+
guard granted else {
|
|
183
|
+
Logger.warn("AudioStudioModule", "startRecording: Permission denied.")
|
|
184
|
+
promise.reject("PERMISSION_DENIED", "Recording permission has not been granted")
|
|
185
|
+
return
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
// Check if output.compressed is enabled and format is Opus
|
|
189
|
+
var modifiedOptions = options
|
|
190
|
+
if let output = options["output"] as? [String: Any],
|
|
191
|
+
let compressed = output["compressed"] as? [String: Any],
|
|
192
|
+
let enabled = compressed["enabled"] as? Bool, enabled,
|
|
193
|
+
let format = compressed["format"] as? String,
|
|
194
|
+
format.lowercased() == "opus" {
|
|
195
|
+
|
|
196
|
+
// Create mutable copies
|
|
197
|
+
var modifiedOutput = output
|
|
198
|
+
var modifiedCompressed = compressed
|
|
199
|
+
|
|
200
|
+
// Change format to AAC and log warning
|
|
201
|
+
modifiedCompressed["format"] = "aac"
|
|
202
|
+
modifiedOutput["compressed"] = modifiedCompressed
|
|
203
|
+
modifiedOptions["output"] = modifiedOutput
|
|
204
|
+
|
|
205
|
+
Logger.warn("AudioStudioModule", "startRecording: Opus format is not supported on iOS. Falling back to AAC format.")
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
// Create settings with validation using the potentially modified options
|
|
209
|
+
let settingsResult = RecordingSettings.fromDictionary(modifiedOptions)
|
|
210
|
+
|
|
211
|
+
switch settingsResult {
|
|
212
|
+
case .success(let settings):
|
|
213
|
+
// Initialize notification if enabled
|
|
214
|
+
if settings.showNotification {
|
|
215
|
+
Task {
|
|
216
|
+
let notificationGranted = await self.requestNotificationPermissions()
|
|
217
|
+
if !notificationGranted {
|
|
218
|
+
Logger.debug("AudioStudioModule", "Notification permissions not granted")
|
|
219
|
+
}
|
|
220
|
+
}
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
if let result = self.streamManager.startRecording(settings: settings) {
|
|
224
|
+
var resultDict: [String: Any] = [
|
|
225
|
+
"fileUri": result.fileUri,
|
|
226
|
+
"channels": result.channels,
|
|
227
|
+
"bitDepth": result.bitDepth,
|
|
228
|
+
"sampleRate": result.sampleRate,
|
|
229
|
+
"mimeType": result.mimeType,
|
|
230
|
+
]
|
|
231
|
+
|
|
232
|
+
// Add compression info if available
|
|
233
|
+
if let compression = result.compression {
|
|
234
|
+
resultDict["compression"] = [
|
|
235
|
+
"compressedFileUri": compression.compressedFileUri,
|
|
236
|
+
"mimeType": compression.mimeType,
|
|
237
|
+
"bitrate": compression.bitrate,
|
|
238
|
+
"format": compression.format
|
|
239
|
+
]
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
Logger.info("AudioStudioModule", "Recording started successfully")
|
|
243
|
+
promise.resolve(resultDict)
|
|
244
|
+
} else {
|
|
245
|
+
Logger.error("AudioStudioModule", "Failed to start recording")
|
|
246
|
+
promise.reject("ERROR", "Failed to start recording.")
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
case .failure(let error):
|
|
250
|
+
Logger.error("AudioStudioModule", "Invalid settings - \(error.localizedDescription)")
|
|
251
|
+
promise.reject("INVALID_SETTINGS", error.localizedDescription)
|
|
252
|
+
}
|
|
253
|
+
}
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
/// Retrieves the current status of the audio stream.
|
|
257
|
+
///
|
|
258
|
+
/// - Returns: The current status of the audio stream.Ï
|
|
259
|
+
Function("status") {
|
|
260
|
+
let currentStatus = self.streamManager.getStatus()
|
|
261
|
+
Logger.debug("AudioStudioModule", "status requested: isRecording=\(currentStatus["isRecording"] ?? false), isPaused=\(currentStatus["isPaused"] ?? false)")
|
|
262
|
+
return currentStatus
|
|
263
|
+
}
|
|
264
|
+
|
|
265
|
+
/// Prepares audio recording with the specified settings without starting it.
|
|
266
|
+
///
|
|
267
|
+
/// - Parameters:
|
|
268
|
+
/// - options: The recording settings to use.
|
|
269
|
+
/// - promise: A promise to resolve with true if preparation was successful.
|
|
270
|
+
/// - Returns: A promise that resolves with a boolean indicating success.
|
|
271
|
+
AsyncFunction("prepareRecording") { (options: [String: Any], promise: Promise) in
|
|
272
|
+
Logger.debug("AudioStudioModule", "prepareRecording called with options: \(options)")
|
|
273
|
+
self.checkMicrophonePermission { granted in
|
|
274
|
+
guard granted else {
|
|
275
|
+
promise.reject("PERMISSION_DENIED", "Recording permission has not been granted")
|
|
276
|
+
return
|
|
277
|
+
}
|
|
278
|
+
|
|
279
|
+
// Create settings with validation
|
|
280
|
+
let settingsResult = RecordingSettings.fromDictionary(options)
|
|
281
|
+
|
|
282
|
+
switch settingsResult {
|
|
283
|
+
case .success(let settings):
|
|
284
|
+
Logger.debug("AudioStudioModule", "prepareRecording: Settings parsed successfully. Calling streamManager.prepareRecording")
|
|
285
|
+
if self.streamManager.prepareRecording(settings: settings) {
|
|
286
|
+
Logger.info("AudioStudioModule", "prepareRecording: Preparation successful.")
|
|
287
|
+
promise.resolve(true)
|
|
288
|
+
} else {
|
|
289
|
+
Logger.error("AudioStudioModule", "prepareRecording: streamManager.prepareRecording returned false.")
|
|
290
|
+
promise.reject("ERROR", "Failed to prepare recording.")
|
|
291
|
+
}
|
|
292
|
+
|
|
293
|
+
case .failure(let error):
|
|
294
|
+
promise.reject("INVALID_SETTINGS", error.localizedDescription)
|
|
295
|
+
}
|
|
296
|
+
}
|
|
297
|
+
}
|
|
298
|
+
|
|
299
|
+
/// Pauses audio recording.
|
|
300
|
+
Function("pauseRecording") {
|
|
301
|
+
Logger.debug("AudioStudioModule", "pauseRecording called.")
|
|
302
|
+
self.streamManager.pauseRecording()
|
|
303
|
+
}
|
|
304
|
+
|
|
305
|
+
/// Resumes audio recording.
|
|
306
|
+
Function("resumeRecording") {
|
|
307
|
+
Logger.debug("AudioStudioModule", "resumeRecording called.")
|
|
308
|
+
self.streamManager.resumeRecording()
|
|
309
|
+
}
|
|
310
|
+
|
|
311
|
+
/// Asynchronously stops audio recording and retrieves the recording result.
|
|
312
|
+
///
|
|
313
|
+
/// - Parameters:
|
|
314
|
+
/// - promise: A promise to resolve with the recording result or reject with an error.
|
|
315
|
+
AsyncFunction("stopRecording") { (promise: Promise) in
|
|
316
|
+
Logger.debug("AudioStudioModule", "stopRecording called.")
|
|
317
|
+
|
|
318
|
+
if let recordingResult = self.streamManager.stopRecording() {
|
|
319
|
+
var resultDict: [String: Any] = [
|
|
320
|
+
"fileUri": recordingResult.fileUri,
|
|
321
|
+
"filename": recordingResult.filename,
|
|
322
|
+
"durationMs": recordingResult.duration,
|
|
323
|
+
"size": recordingResult.size,
|
|
324
|
+
"channels": recordingResult.channels,
|
|
325
|
+
"bitDepth": recordingResult.bitDepth,
|
|
326
|
+
"sampleRate": recordingResult.sampleRate,
|
|
327
|
+
"mimeType": recordingResult.mimeType,
|
|
328
|
+
"createdAt": Date().timeIntervalSince1970 * 1000,
|
|
329
|
+
]
|
|
330
|
+
|
|
331
|
+
// Add compression info if available
|
|
332
|
+
if let compression = recordingResult.compression {
|
|
333
|
+
resultDict["compression"] = [
|
|
334
|
+
"compressedFileUri": compression.compressedFileUri,
|
|
335
|
+
"mimeType": compression.mimeType,
|
|
336
|
+
"bitrate": compression.bitrate,
|
|
337
|
+
"format": compression.format,
|
|
338
|
+
"size": compression.size
|
|
339
|
+
]
|
|
340
|
+
}
|
|
341
|
+
|
|
342
|
+
Logger.info("AudioStudioModule", "stopRecording: Recording stopped successfully. fileUri: \(recordingResult.fileUri), size: \(recordingResult.size)")
|
|
343
|
+
promise.resolve(resultDict)
|
|
344
|
+
} else {
|
|
345
|
+
Logger.error("AudioStudioModule", "stopRecording: streamManager.stopRecording returned nil.")
|
|
346
|
+
promise.reject("ERROR", "Failed to stop recording or no recording in progress.")
|
|
347
|
+
}
|
|
348
|
+
}
|
|
349
|
+
|
|
350
|
+
/// Asynchronously lists all audio files stored in the document directory.
|
|
351
|
+
///
|
|
352
|
+
/// - Parameters:
|
|
353
|
+
/// - promise: A promise to resolve with the list of audio file URIs or reject with an error.
|
|
354
|
+
/// - Returns: A promise that resolves with the list of audio file URIs or rejects with an error.
|
|
355
|
+
AsyncFunction("listAudioFiles") { (promise: Promise) in
|
|
356
|
+
Logger.debug("AudioStudioModule", "listAudioFiles called.")
|
|
357
|
+
let files = listAudioFiles()
|
|
358
|
+
Logger.debug("AudioStudioModule", "listAudioFiles returning \(files.count) files.")
|
|
359
|
+
promise.resolve(files)
|
|
360
|
+
}
|
|
361
|
+
|
|
362
|
+
/// Clears all audio files stored in the document directory.
|
|
363
|
+
Function("clearAudioFiles") {
|
|
364
|
+
Logger.debug("AudioStudioModule", "clearAudioFiles called.")
|
|
365
|
+
clearAudioFiles()
|
|
366
|
+
}
|
|
367
|
+
|
|
368
|
+
|
|
369
|
+
/// Requests audio recording permissions.
|
|
370
|
+
///
|
|
371
|
+
/// - Parameters:
|
|
372
|
+
/// - promise: A promise to resolve with the permission status or reject with an error.
|
|
373
|
+
/// - Returns: Promise to be resolved with the permission status.
|
|
374
|
+
AsyncFunction("requestPermissionsAsync") { (promise: Promise) in
|
|
375
|
+
AVAudioSession.sharedInstance().requestRecordPermission { granted in
|
|
376
|
+
promise.resolve([
|
|
377
|
+
"status": granted ? "granted" : "denied",
|
|
378
|
+
"granted": granted,
|
|
379
|
+
"expires": "never",
|
|
380
|
+
"canAskAgain": true
|
|
381
|
+
])
|
|
382
|
+
}
|
|
383
|
+
}
|
|
384
|
+
|
|
385
|
+
AsyncFunction("requestNotificationPermissionsAsync") { (promise: Promise) in
|
|
386
|
+
Task {
|
|
387
|
+
let granted = await requestNotificationPermissions()
|
|
388
|
+
promise.resolve([
|
|
389
|
+
"granted": granted,
|
|
390
|
+
"status": granted ? "granted" : "denied"
|
|
391
|
+
])
|
|
392
|
+
}
|
|
393
|
+
}
|
|
394
|
+
|
|
395
|
+
/// Gets the current audio recording permissions.
|
|
396
|
+
///
|
|
397
|
+
/// - Parameters:
|
|
398
|
+
/// - promise: A promise to resolve with the permission status or reject with an error.
|
|
399
|
+
/// - Returns: Promise to be resolved with the permission status.
|
|
400
|
+
AsyncFunction("getPermissionsAsync") { (promise: Promise) in
|
|
401
|
+
let permissionStatus = AVAudioSession.sharedInstance().recordPermission
|
|
402
|
+
switch permissionStatus {
|
|
403
|
+
case .granted:
|
|
404
|
+
promise.resolve([
|
|
405
|
+
"status": "granted",
|
|
406
|
+
"granted": true,
|
|
407
|
+
"expires": "never",
|
|
408
|
+
"canAskAgain": true
|
|
409
|
+
])
|
|
410
|
+
case .denied:
|
|
411
|
+
promise.resolve([
|
|
412
|
+
"status": "denied",
|
|
413
|
+
"granted": false,
|
|
414
|
+
"expires": "never",
|
|
415
|
+
"canAskAgain": false
|
|
416
|
+
])
|
|
417
|
+
case .undetermined:
|
|
418
|
+
promise.resolve([
|
|
419
|
+
"status": "undetermined",
|
|
420
|
+
"granted": false,
|
|
421
|
+
"expires": "never",
|
|
422
|
+
"canAskAgain": true
|
|
423
|
+
])
|
|
424
|
+
@unknown default:
|
|
425
|
+
promise.reject("UNKNOWN_ERROR", "Unknown permission status")
|
|
426
|
+
}
|
|
427
|
+
}
|
|
428
|
+
|
|
429
|
+
/// Trims an audio file to specified start and end times.
|
|
430
|
+
/// - Parameters:
|
|
431
|
+
/// - options: A dictionary containing:
|
|
432
|
+
/// - `fileUri`: The URI of the audio file.
|
|
433
|
+
/// - `mode`: Trim mode ('single', 'keep', or 'remove').
|
|
434
|
+
/// - `startTimeMs`: Start time in milliseconds (for 'single' mode).
|
|
435
|
+
/// - `endTimeMs`: End time in milliseconds (for 'single' mode).
|
|
436
|
+
/// - `ranges`: Array of time ranges (for 'keep' and 'remove' modes).
|
|
437
|
+
/// - `outputFileName`: Optional name for the output file.
|
|
438
|
+
/// - `outputFormat`: Optional output format configuration.
|
|
439
|
+
/// - `decodingOptions`: Optional decoding configuration.
|
|
440
|
+
AsyncFunction("trimAudio") { (options: [String: Any], promise: Promise) in
|
|
441
|
+
guard let fileUri = options["fileUri"] as? String,
|
|
442
|
+
let url = URL(string: fileUri) else {
|
|
443
|
+
promise.reject("INVALID_ARGUMENTS", "Invalid file URI provided")
|
|
444
|
+
return
|
|
445
|
+
}
|
|
446
|
+
|
|
447
|
+
let mode = options["mode"] as? String ?? "single"
|
|
448
|
+
let startTimeMs = options["startTimeMs"] as? Double
|
|
449
|
+
let endTimeMs = options["endTimeMs"] as? Double
|
|
450
|
+
let ranges = options["ranges"] as? [[String: Double]]
|
|
451
|
+
let outputFileName = options["outputFileName"] as? String
|
|
452
|
+
let outputFormat = options["outputFormat"] as? [String: Any]
|
|
453
|
+
let decodingOptions = options["decodingOptions"] as? [String: Any] ?? [:]
|
|
454
|
+
|
|
455
|
+
// Add detailed logging for filename and format options
|
|
456
|
+
Logger.debug("AudioStudioModule", "Trim audio request:")
|
|
457
|
+
Logger.debug("AudioStudioModule", "- Input file: \(fileUri)")
|
|
458
|
+
Logger.debug("AudioStudioModule", "- Mode: \(mode)")
|
|
459
|
+
Logger.debug("AudioStudioModule", "- Output filename: \(outputFileName ?? "not specified (will generate UUID)")")
|
|
460
|
+
if let format = outputFormat?["format"] as? String {
|
|
461
|
+
Logger.debug("AudioStudioModule", "- Output format: \(format)")
|
|
462
|
+
} else {
|
|
463
|
+
Logger.debug("AudioStudioModule", "- Output format: not specified (will use default)")
|
|
464
|
+
}
|
|
465
|
+
|
|
466
|
+
// Input validation based on mode
|
|
467
|
+
switch mode {
|
|
468
|
+
case "single":
|
|
469
|
+
guard let start = startTimeMs, let end = endTimeMs else {
|
|
470
|
+
promise.reject("INVALID_ARGUMENTS", "startTimeMs and endTimeMs required for 'single' mode")
|
|
471
|
+
return
|
|
472
|
+
}
|
|
473
|
+
guard start >= 0, end > start else {
|
|
474
|
+
promise.reject("INVALID_ARGUMENTS", "Invalid time range")
|
|
475
|
+
return
|
|
476
|
+
}
|
|
477
|
+
case "keep", "remove":
|
|
478
|
+
guard let rangesArray = ranges, !rangesArray.isEmpty else {
|
|
479
|
+
promise.reject("INVALID_ARGUMENTS", "'ranges' array required for 'keep' or 'remove' mode")
|
|
480
|
+
return
|
|
481
|
+
}
|
|
482
|
+
default:
|
|
483
|
+
promise.reject("INVALID_MODE", "Mode must be 'single', 'keep', or 'remove'")
|
|
484
|
+
return
|
|
485
|
+
}
|
|
486
|
+
|
|
487
|
+
DispatchQueue.global().async {
|
|
488
|
+
do {
|
|
489
|
+
let audioProcessor = try AudioProcessor(
|
|
490
|
+
url: url,
|
|
491
|
+
resolve: { result in promise.resolve(result) },
|
|
492
|
+
reject: { code, message in promise.reject(code, message) }
|
|
493
|
+
)
|
|
494
|
+
|
|
495
|
+
let progressCallback: (Float, Int64, Int64) -> Void = { progress, bytesProcessed, totalBytes in
|
|
496
|
+
self.sendEvent(trimProgressEvent, [
|
|
497
|
+
"progress": progress,
|
|
498
|
+
"bytesProcessed": bytesProcessed,
|
|
499
|
+
"totalBytes": totalBytes
|
|
500
|
+
])
|
|
501
|
+
}
|
|
502
|
+
|
|
503
|
+
let startTime = CACurrentMediaTime()
|
|
504
|
+
if let result = audioProcessor.trimAudio(
|
|
505
|
+
mode: mode,
|
|
506
|
+
startTimeMs: startTimeMs,
|
|
507
|
+
endTimeMs: endTimeMs,
|
|
508
|
+
ranges: ranges,
|
|
509
|
+
outputFileName: outputFileName,
|
|
510
|
+
outputFormat: outputFormat,
|
|
511
|
+
decodingOptions: decodingOptions,
|
|
512
|
+
progressCallback: progressCallback
|
|
513
|
+
) {
|
|
514
|
+
let processingTimeMs = Int((CACurrentMediaTime() - startTime) * 1000)
|
|
515
|
+
var resultDict = result.toDictionary()
|
|
516
|
+
resultDict["processingInfo"] = ["durationMs": processingTimeMs]
|
|
517
|
+
|
|
518
|
+
let uri = result.uri
|
|
519
|
+
Logger.debug("AudioStudioModule", "Trim completed successfully in \(processingTimeMs)ms")
|
|
520
|
+
Logger.debug("AudioStudioModule", "Output file URI: \(uri)")
|
|
521
|
+
|
|
522
|
+
// Verify file exists
|
|
523
|
+
let fileManager = FileManager.default
|
|
524
|
+
if let url = URL(string: uri) {
|
|
525
|
+
let exists = fileManager.fileExists(atPath: url.path)
|
|
526
|
+
Logger.debug("AudioStudioModule", "File exists at path \(url.path): \(exists)")
|
|
527
|
+
|
|
528
|
+
// Log filename details
|
|
529
|
+
Logger.debug("AudioStudioModule", "Filename: \(url.lastPathComponent)")
|
|
530
|
+
Logger.debug("AudioStudioModule", "File extension: \(url.pathExtension.lowercased())")
|
|
531
|
+
|
|
532
|
+
// If format is AAC, ensure we're using the correct extension and MIME type
|
|
533
|
+
if let format = outputFormat?["format"] as? String,
|
|
534
|
+
format.lowercased() == "aac" {
|
|
535
|
+
|
|
536
|
+
Logger.debug("AudioStudioModule", "AAC format detected - ensuring correct metadata")
|
|
537
|
+
|
|
538
|
+
// For AAC format, ensure we're using the correct extension and MIME type
|
|
539
|
+
if url.pathExtension.lowercased() == "m4a" {
|
|
540
|
+
Logger.debug("AudioStudioModule", "File has correct m4a extension for AAC audio")
|
|
541
|
+
|
|
542
|
+
// Just update the MIME type in the result to ensure correct playback
|
|
543
|
+
if var compression = resultDict["compression"] as? [String: Any] {
|
|
544
|
+
compression["mimeType"] = "audio/mp4"
|
|
545
|
+
resultDict["compression"] = compression
|
|
546
|
+
}
|
|
547
|
+
|
|
548
|
+
resultDict["mimeType"] = "audio/mp4"
|
|
549
|
+
resultDict["actualFormat"] = "m4a"
|
|
550
|
+
} else {
|
|
551
|
+
Logger.debug("AudioStudioModule", "Warning: AAC format should use .m4a extension, but found .\(url.pathExtension.lowercased())")
|
|
552
|
+
}
|
|
553
|
+
}
|
|
554
|
+
}
|
|
555
|
+
|
|
556
|
+
promise.resolve(resultDict)
|
|
557
|
+
} else {
|
|
558
|
+
Logger.debug("AudioStudioModule", "Failed to trim audio")
|
|
559
|
+
promise.reject("TRIM_ERROR", "Failed to trim audio")
|
|
560
|
+
}
|
|
561
|
+
} catch {
|
|
562
|
+
Logger.debug("AudioStudioModule", "Failed to initialize audio processor: \(error.localizedDescription)")
|
|
563
|
+
promise.reject("PROCESSING_ERROR", "Failed to initialize audio processor: \(error.localizedDescription)")
|
|
564
|
+
}
|
|
565
|
+
}
|
|
566
|
+
}
|
|
567
|
+
|
|
568
|
+
/// Extracts raw PCM audio data from a file with time or byte range support
|
|
569
|
+
/// - Parameters:
|
|
570
|
+
/// - options: A dictionary containing:
|
|
571
|
+
/// - `fileUri`: The URI of the audio file
|
|
572
|
+
/// - `startTimeMs`: Optional start time in milliseconds
|
|
573
|
+
/// - `endTimeMs`: Optional end time in milliseconds
|
|
574
|
+
/// - `position`: Optional byte position
|
|
575
|
+
/// - `length`: Optional byte length
|
|
576
|
+
/// - `includeNormalizedData`: Boolean to include normalized audio data in [-1, 1] range
|
|
577
|
+
/// - `includeWavHeader`: Boolean to include WAV header in the PCM data
|
|
578
|
+
/// - `decodingOptions`: Decoding configuration
|
|
579
|
+
/// - `includeBase64Data`: Boolean to include base64 encoded string representation of the audio data
|
|
580
|
+
/// - `computeChecksum`: Boolean to compute and include CRC32 checksum of the PCM data
|
|
581
|
+
AsyncFunction("extractAudioData") { (options: [String: Any], promise: Promise) in
|
|
582
|
+
guard let fileUri = options["fileUri"] as? String,
|
|
583
|
+
let url = URL(string: fileUri) else {
|
|
584
|
+
promise.reject("INVALID_ARGUMENTS", "Invalid file URI provided")
|
|
585
|
+
return
|
|
586
|
+
}
|
|
587
|
+
|
|
588
|
+
// Get time or byte range options
|
|
589
|
+
let startTimeMs = options["startTimeMs"] as? Double
|
|
590
|
+
let endTimeMs = options["endTimeMs"] as? Double
|
|
591
|
+
let position = options["position"] as? Int
|
|
592
|
+
let length = options["length"] as? Int
|
|
593
|
+
let includeWavHeader = options["includeWavHeader"] as? Bool ?? false
|
|
594
|
+
let includeNormalizedData = options["includeNormalizedData"] as? Bool ?? false
|
|
595
|
+
let decodingOptions = options["decodingOptions"] as? [String: Any] ?? [:]
|
|
596
|
+
let includeBase64Data = options["includeBase64Data"] as? Bool ?? false
|
|
597
|
+
|
|
598
|
+
// Validate that we have either time range or byte range, but not both and not neither
|
|
599
|
+
let hasTimeRange = startTimeMs != nil && endTimeMs != nil
|
|
600
|
+
let hasByteRange = position != nil && length != nil
|
|
601
|
+
|
|
602
|
+
guard hasTimeRange || hasByteRange else {
|
|
603
|
+
promise.reject("INVALID_ARGUMENTS", "Must specify either time range (startTimeMs, endTimeMs) or byte range (position, length)")
|
|
604
|
+
return
|
|
605
|
+
}
|
|
606
|
+
|
|
607
|
+
guard !(hasTimeRange && hasByteRange) else {
|
|
608
|
+
promise.reject("INVALID_ARGUMENTS", "Cannot specify both time range and byte range")
|
|
609
|
+
return
|
|
610
|
+
}
|
|
611
|
+
|
|
612
|
+
do {
|
|
613
|
+
let audioFile = try AVAudioFile(forReading: url)
|
|
614
|
+
let format = audioFile.processingFormat
|
|
615
|
+
let sampleRate = format.sampleRate
|
|
616
|
+
let channels = Int(format.channelCount)
|
|
617
|
+
let bitDepth = audioFile.fileFormat.settings[AVLinearPCMBitDepthKey] as? Int ?? 16
|
|
618
|
+
|
|
619
|
+
// Calculate frame positions
|
|
620
|
+
let startFrame: AVAudioFramePosition
|
|
621
|
+
let endFrame: AVAudioFramePosition
|
|
622
|
+
|
|
623
|
+
if hasTimeRange {
|
|
624
|
+
startFrame = AVAudioFramePosition(startTimeMs! * sampleRate / 1000.0)
|
|
625
|
+
endFrame = AVAudioFramePosition(endTimeMs! * sampleRate / 1000.0)
|
|
626
|
+
} else {
|
|
627
|
+
// Convert byte position to frame position
|
|
628
|
+
let bytesPerFrame = Int64(channels * (bitDepth / 8))
|
|
629
|
+
startFrame = AVAudioFramePosition(position!) / bytesPerFrame
|
|
630
|
+
endFrame = startFrame + (AVAudioFramePosition(length!) / bytesPerFrame)
|
|
631
|
+
}
|
|
632
|
+
|
|
633
|
+
// Validate frame range
|
|
634
|
+
guard startFrame >= 0 && endFrame <= audioFile.length && startFrame < endFrame else {
|
|
635
|
+
promise.reject("INVALID_RANGE", "Invalid range specified")
|
|
636
|
+
return
|
|
637
|
+
}
|
|
638
|
+
|
|
639
|
+
let frameCount = AVAudioFrameCount(endFrame - startFrame)
|
|
640
|
+
|
|
641
|
+
|
|
642
|
+
// Pass both options separately - normalizeAudio from decodingOptions, and includeNormalizedData as is
|
|
643
|
+
let decodingConfig = DecodingConfig.fromDictionary(decodingOptions)
|
|
644
|
+
|
|
645
|
+
let (pcmData, normalizedData, base64Data) = try extractRawAudioData(
|
|
646
|
+
from: url,
|
|
647
|
+
startFrame: startFrame,
|
|
648
|
+
frameCount: frameCount,
|
|
649
|
+
format: format,
|
|
650
|
+
decodingConfig: decodingConfig,
|
|
651
|
+
includeNormalizedData: includeNormalizedData,
|
|
652
|
+
includeBase64Data: includeBase64Data
|
|
653
|
+
)
|
|
654
|
+
|
|
655
|
+
var resultDict: [String: Any] = [:]
|
|
656
|
+
|
|
657
|
+
if includeWavHeader {
|
|
658
|
+
// Create WAV header and prepend it to the PCM data
|
|
659
|
+
let wavData = createWavHeader(
|
|
660
|
+
pcmData: pcmData,
|
|
661
|
+
sampleRate: Int(sampleRate),
|
|
662
|
+
channels: channels,
|
|
663
|
+
bitDepth: bitDepth
|
|
664
|
+
)
|
|
665
|
+
resultDict["pcmData"] = wavData
|
|
666
|
+
resultDict["hasWavHeader"] = true
|
|
667
|
+
} else {
|
|
668
|
+
resultDict["pcmData"] = pcmData
|
|
669
|
+
resultDict["hasWavHeader"] = false
|
|
670
|
+
}
|
|
671
|
+
|
|
672
|
+
// Add the rest of the data
|
|
673
|
+
resultDict["sampleRate"] = Int(sampleRate)
|
|
674
|
+
resultDict["channels"] = channels
|
|
675
|
+
resultDict["bitDepth"] = bitDepth
|
|
676
|
+
resultDict["durationMs"] = Int(Double(frameCount) * 1000.0 / sampleRate)
|
|
677
|
+
resultDict["format"] = "pcm_\(bitDepth)bit"
|
|
678
|
+
resultDict["samples"] = Int(frameCount) * channels
|
|
679
|
+
|
|
680
|
+
// Add normalized data if requested, regardless of normalization setting
|
|
681
|
+
if includeNormalizedData {
|
|
682
|
+
resultDict["normalizedData"] = normalizedData
|
|
683
|
+
}
|
|
684
|
+
|
|
685
|
+
// Add checksum if requested
|
|
686
|
+
if options["computeChecksum"] as? Bool == true {
|
|
687
|
+
let checksum = calculateCRC32(data: pcmData)
|
|
688
|
+
resultDict["checksum"] = Int(checksum)
|
|
689
|
+
|
|
690
|
+
Logger.debug("AudioStudioModule", "Computed CRC32 checksum: \(checksum)")
|
|
691
|
+
}
|
|
692
|
+
|
|
693
|
+
if let includeBase64Data = options["includeBase64Data"] as? Bool, includeBase64Data {
|
|
694
|
+
resultDict["base64Data"] = base64Data
|
|
695
|
+
}
|
|
696
|
+
|
|
697
|
+
promise.resolve(resultDict)
|
|
698
|
+
|
|
699
|
+
} catch {
|
|
700
|
+
promise.reject("PROCESSING_ERROR", "Failed to process audio file: \(error.localizedDescription)")
|
|
701
|
+
}
|
|
702
|
+
}
|
|
703
|
+
|
|
704
|
+
/// Extracts mel spectrogram data from a file.
|
|
705
|
+
///
|
|
706
|
+
/// - Parameters:
|
|
707
|
+
/// - options: A dictionary containing:
|
|
708
|
+
/// - `fileUri`: The URI of the audio file.
|
|
709
|
+
/// - `pointsPerSecond`: The number of data points to extract per second of audio.
|
|
710
|
+
/// - promise: A promise to resolve with the extracted mel spectrogram data or reject with an error.
|
|
711
|
+
/// - Returns: Promise to be resolved with mel spectrogram data.
|
|
712
|
+
AsyncFunction("extractMelSpectrogram") { (options: [String: Any], promise: Promise) in
|
|
713
|
+
do {
|
|
714
|
+
guard let fileUri = options["fileUri"] as? String else {
|
|
715
|
+
throw NSError(domain: "AudioStudio", code: -1, userInfo: [NSLocalizedDescriptionKey: "fileUri is required"])
|
|
716
|
+
}
|
|
717
|
+
guard let windowSizeMs = options["windowSizeMs"] as? Double else {
|
|
718
|
+
throw NSError(domain: "AudioStudio", code: -1, userInfo: [NSLocalizedDescriptionKey: "windowSizeMs is required"])
|
|
719
|
+
}
|
|
720
|
+
guard let hopLengthMs = options["hopLengthMs"] as? Double else {
|
|
721
|
+
throw NSError(domain: "AudioStudio", code: -1, userInfo: [NSLocalizedDescriptionKey: "hopLengthMs is required"])
|
|
722
|
+
}
|
|
723
|
+
guard let nMels = options["nMels"] as? Int ?? (options["nMels"] as? Double).map({ Int($0) }) else {
|
|
724
|
+
throw NSError(domain: "AudioStudio", code: -1, userInfo: [NSLocalizedDescriptionKey: "nMels is required"])
|
|
725
|
+
}
|
|
726
|
+
|
|
727
|
+
let fMin = Float(options["fMin"] as? Double ?? 0.0)
|
|
728
|
+
let fMaxParam = options["fMax"] as? Double
|
|
729
|
+
let windowType = options["windowType"] as? String ?? "hann"
|
|
730
|
+
let logScale = options["logScale"] as? Bool ?? true
|
|
731
|
+
let normalize = options["normalize"] as? Bool ?? false
|
|
732
|
+
let startTimeMs = options["startTimeMs"] as? Double
|
|
733
|
+
let endTimeMs = options["endTimeMs"] as? Double
|
|
734
|
+
|
|
735
|
+
// Load audio file to PCM float samples
|
|
736
|
+
let audioData = try loadAudioFile(fileUri)
|
|
737
|
+
let sampleRate = audioData.sampleRate
|
|
738
|
+
var samples = audioData.samples
|
|
739
|
+
|
|
740
|
+
// Apply time range trimming if specified
|
|
741
|
+
if let startMs = startTimeMs {
|
|
742
|
+
let startSample = Int(startMs * Double(sampleRate) / 1000.0)
|
|
743
|
+
let endSample: Int
|
|
744
|
+
if let endMs = endTimeMs {
|
|
745
|
+
endSample = min(Int(endMs * Double(sampleRate) / 1000.0), samples.count)
|
|
746
|
+
} else {
|
|
747
|
+
endSample = samples.count
|
|
748
|
+
}
|
|
749
|
+
if startSample < endSample && startSample < samples.count {
|
|
750
|
+
samples = Array(samples[startSample..<endSample])
|
|
751
|
+
}
|
|
752
|
+
}
|
|
753
|
+
|
|
754
|
+
let fMax = fMaxParam.map { Float($0) } ?? Float(sampleRate) / 2.0
|
|
755
|
+
|
|
756
|
+
// Convert ms to samples
|
|
757
|
+
let windowSizeSamples = Int(windowSizeMs * Double(sampleRate) / 1000.0)
|
|
758
|
+
let hopLengthSamples = Int(hopLengthMs * Double(sampleRate) / 1000.0)
|
|
759
|
+
|
|
760
|
+
let windowTypeInt: Int32 = windowType.lowercased() == "hamming" ? 1 : 0
|
|
761
|
+
|
|
762
|
+
// Call shared C++ implementation via ObjC++ wrapper
|
|
763
|
+
guard let result = samples.withUnsafeBufferPointer({ bufferPtr -> [AnyHashable: Any]? in
|
|
764
|
+
guard let baseAddress = bufferPtr.baseAddress else { return nil }
|
|
765
|
+
return MelSpectrogramWrapper.compute(
|
|
766
|
+
withSamples: baseAddress,
|
|
767
|
+
numSamples: Int32(samples.count),
|
|
768
|
+
sampleRate: Int32(sampleRate),
|
|
769
|
+
fftLength: 2048,
|
|
770
|
+
windowSizeSamples: Int32(windowSizeSamples),
|
|
771
|
+
hopLengthSamples: Int32(hopLengthSamples),
|
|
772
|
+
nMels: Int32(nMels),
|
|
773
|
+
fMin: fMin,
|
|
774
|
+
fMax: fMax,
|
|
775
|
+
windowType: windowTypeInt,
|
|
776
|
+
logScale: logScale,
|
|
777
|
+
normalize: normalize
|
|
778
|
+
)
|
|
779
|
+
}) else {
|
|
780
|
+
throw NSError(domain: "AudioStudio", code: -1, userInfo: [NSLocalizedDescriptionKey: "Audio data is too short for spectrogram analysis"])
|
|
781
|
+
}
|
|
782
|
+
|
|
783
|
+
let timeSteps = result["timeSteps"] as! Int
|
|
784
|
+
let durationMs = Double(samples.count) / Double(sampleRate) * 1000.0
|
|
785
|
+
|
|
786
|
+
let output: [String: Any] = [
|
|
787
|
+
"spectrogram": result["spectrogram"]!,
|
|
788
|
+
"sampleRate": sampleRate,
|
|
789
|
+
"nMels": nMels,
|
|
790
|
+
"timeSteps": timeSteps,
|
|
791
|
+
"durationMs": durationMs
|
|
792
|
+
]
|
|
793
|
+
|
|
794
|
+
promise.resolve(output)
|
|
795
|
+
} catch {
|
|
796
|
+
promise.reject("SPECTROGRAM_ERROR", "Failed to extract mel spectrogram: \(error.localizedDescription)")
|
|
797
|
+
}
|
|
798
|
+
}
|
|
799
|
+
|
|
800
|
+
/// Gets available audio input devices with an optional refresh parameter
|
|
801
|
+
/// - Parameters:
|
|
802
|
+
/// - options: Optional dictionary containing a refresh parameter
|
|
803
|
+
/// - promise: A promise to resolve with a list of available audio input devices
|
|
804
|
+
AsyncFunction("getAvailableInputDevices") { (options: [String: Any]?, promise: Promise) in
|
|
805
|
+
Logger.debug("AudioStudioModule", "getAvailableInputDevices called. Refresh: \(options?["refresh"] ?? false)")
|
|
806
|
+
if let options = options, let refresh = options["refresh"] as? Bool, refresh {
|
|
807
|
+
Logger.debug("AudioStudioModule", "Forcing refresh of audio devices")
|
|
808
|
+
_ = self.deviceManager.forceRefreshAudioSession()
|
|
809
|
+
}
|
|
810
|
+
|
|
811
|
+
// Call the device manager with the promise
|
|
812
|
+
self.deviceManager.getAvailableInputDevices(promise: promise)
|
|
813
|
+
}
|
|
814
|
+
|
|
815
|
+
/// Refreshes the audio session to detect newly connected devices
|
|
816
|
+
/// - Returns: Boolean indicating success
|
|
817
|
+
Function("refreshAudioDevices") {
|
|
818
|
+
Logger.debug("AudioStudioModule", "refreshAudioDevices called.")
|
|
819
|
+
let success = self.deviceManager.forceRefreshAudioSession()
|
|
820
|
+
Logger.debug("AudioStudioModule", "refreshAudioDevices result: \(success)")
|
|
821
|
+
return ["success": success]
|
|
822
|
+
}
|
|
823
|
+
|
|
824
|
+
/// Gets the currently selected audio input device
|
|
825
|
+
///
|
|
826
|
+
/// - Parameters:
|
|
827
|
+
/// - promise: A promise to resolve with the currently selected audio input device
|
|
828
|
+
AsyncFunction("getCurrentInputDevice") { (promise: Promise) in
|
|
829
|
+
Logger.debug("AudioStudioModule", "getCurrentInputDevice called.")
|
|
830
|
+
self.deviceManager.getCurrentInputDevice(promise: promise)
|
|
831
|
+
}
|
|
832
|
+
|
|
833
|
+
/// Selects a specific audio input device for recording
|
|
834
|
+
///
|
|
835
|
+
/// - Parameters:
|
|
836
|
+
/// - deviceId: The ID of the device to select
|
|
837
|
+
/// - promise: A promise to resolve with boolean indicating success
|
|
838
|
+
AsyncFunction("selectInputDevice") { (deviceId: String, promise: Promise) in
|
|
839
|
+
Logger.debug("AudioStudioModule", "selectInputDevice called with ID: \(deviceId)")
|
|
840
|
+
self.deviceManager.selectInputDevice(deviceId, promise: promise)
|
|
841
|
+
// Sync deviceId into recordingSettings so updateAudioSessionWithCurrentSettings can find the port
|
|
842
|
+
self.streamManager.recordingSettings?.deviceId = deviceId
|
|
843
|
+
// Update the audio recorder if recording is in progress or prepared
|
|
844
|
+
if self.streamManager.isRecording || self.streamManager.isPrepared {
|
|
845
|
+
Logger.debug("AudioStudioModule", "selectInputDevice: Calling updateAudioSessionWithCurrentSettings because recording/prepared.")
|
|
846
|
+
self.streamManager.updateAudioSessionWithCurrentSettings()
|
|
847
|
+
} else {
|
|
848
|
+
Logger.debug("AudioStudioModule", "selectInputDevice: Not calling updateAudioSessionWithCurrentSettings because not recording/prepared.")
|
|
849
|
+
}
|
|
850
|
+
}
|
|
851
|
+
|
|
852
|
+
/// Resets to the default audio input device
|
|
853
|
+
///
|
|
854
|
+
/// - Parameters:
|
|
855
|
+
/// - promise: A promise to resolve with boolean indicating success
|
|
856
|
+
AsyncFunction("resetToDefaultDevice") { (promise: Promise) in
|
|
857
|
+
Logger.debug("AudioStudioModule", "resetToDefaultDevice called.")
|
|
858
|
+
self.deviceManager.resetToDefaultDevice { success, error in
|
|
859
|
+
if success {
|
|
860
|
+
// Clear stored deviceId so updateAudioSessionWithCurrentSettings won't bail early
|
|
861
|
+
self.streamManager.recordingSettings?.deviceId = nil
|
|
862
|
+
if self.streamManager.isRecording || self.streamManager.isPrepared {
|
|
863
|
+
Logger.debug("AudioStudioModule", "resetToDefaultDevice: Performing device switch to system default.")
|
|
864
|
+
// Bug 1 fix: call performDeviceSwitch(nil) directly — updateAudioSessionWithCurrentSettings
|
|
865
|
+
// would bail immediately because deviceId is now nil.
|
|
866
|
+
self.streamManager.performDeviceSwitch(port: nil)
|
|
867
|
+
} else {
|
|
868
|
+
Logger.debug("AudioStudioModule", "resetToDefaultDevice: Not recording/prepared, no engine action needed.")
|
|
869
|
+
}
|
|
870
|
+
promise.resolve(true)
|
|
871
|
+
} else {
|
|
872
|
+
Logger.error("AudioStudioModule", "resetToDefaultDevice failed: \(error?.localizedDescription ?? "Unknown error")")
|
|
873
|
+
promise.reject("DEVICE_ERROR", "Failed to reset to default device: \(error?.localizedDescription ?? "Unknown error")")
|
|
874
|
+
}
|
|
875
|
+
}
|
|
876
|
+
}
|
|
877
|
+
}
|
|
878
|
+
|
|
879
|
+
func audioStreamManager(_ manager: AudioStreamManager, didReceiveInterruption info: [String: Any]) {
|
|
880
|
+
Logger.debug("AudioStudioModule", "Delegate: didReceiveInterruption: \(info)")
|
|
881
|
+
// Convert iOS interruption events to match the TypeScript types
|
|
882
|
+
var reason: String
|
|
883
|
+
var isPaused: Bool = true
|
|
884
|
+
|
|
885
|
+
if let type = info["type"] as? String {
|
|
886
|
+
switch type {
|
|
887
|
+
case "began":
|
|
888
|
+
// Phone call or other audio session interruption began
|
|
889
|
+
reason = "audioFocusLoss"
|
|
890
|
+
case "ended":
|
|
891
|
+
reason = "audioFocusGain"
|
|
892
|
+
isPaused = false
|
|
893
|
+
// Check if this was from a phone call
|
|
894
|
+
if let wasSuspended = info["wasSuspended"] as? Bool, wasSuspended {
|
|
895
|
+
reason = "phoneCallEnded"
|
|
896
|
+
}
|
|
897
|
+
default:
|
|
898
|
+
return
|
|
899
|
+
}
|
|
900
|
+
} else if let specificReason = info["reason"] as? String {
|
|
901
|
+
// Handle specific reasons that are already properly formatted
|
|
902
|
+
reason = specificReason
|
|
903
|
+
isPaused = info["isPaused"] as? Bool ?? true
|
|
904
|
+
} else {
|
|
905
|
+
return
|
|
906
|
+
}
|
|
907
|
+
|
|
908
|
+
// Send event in the correct format
|
|
909
|
+
sendEvent(recordingInterruptedEvent, [
|
|
910
|
+
"reason": reason,
|
|
911
|
+
"isPaused": isPaused,
|
|
912
|
+
"timestamp": Date().timeIntervalSince1970 * 1000
|
|
913
|
+
])
|
|
914
|
+
}
|
|
915
|
+
|
|
916
|
+
func audioStreamManager(_ manager: AudioStreamManager, didPauseRecording pauseTime: Date) {
|
|
917
|
+
Logger.debug("AudioStudioModule", "Delegate: didPauseRecording")
|
|
918
|
+
sendEvent(recordingInterruptedEvent, [
|
|
919
|
+
"reason": "userPaused",
|
|
920
|
+
"isPaused": true,
|
|
921
|
+
"timestamp": pauseTime.timeIntervalSince1970 * 1000
|
|
922
|
+
])
|
|
923
|
+
}
|
|
924
|
+
|
|
925
|
+
func audioStreamManager(_ manager: AudioStreamManager, didResumeRecording resumeTime: Date) {
|
|
926
|
+
Logger.debug("AudioStudioModule", "Delegate: didResumeRecording")
|
|
927
|
+
sendEvent(recordingInterruptedEvent, [
|
|
928
|
+
"reason": "userResumed",
|
|
929
|
+
"isPaused": false,
|
|
930
|
+
"timestamp": resumeTime.timeIntervalSince1970 * 1000
|
|
931
|
+
])
|
|
932
|
+
}
|
|
933
|
+
|
|
934
|
+
func audioStreamManager(_ manager: AudioStreamManager, didUpdateNotificationState isPaused: Bool) {
|
|
935
|
+
Logger.debug("AudioStudioModule", "Delegate: didUpdateNotificationState: isPaused=\(isPaused)")
|
|
936
|
+
sendEvent(recordingInterruptedEvent, [
|
|
937
|
+
"reason": "notification",
|
|
938
|
+
"isPaused": isPaused,
|
|
939
|
+
"timestamp": Date().timeIntervalSince1970 * 1000
|
|
940
|
+
])
|
|
941
|
+
}
|
|
942
|
+
|
|
943
|
+
/// Handles the reception of audio data from the AudioStreamManager.
|
|
944
|
+
///
|
|
945
|
+
/// - Parameters:
|
|
946
|
+
/// - manager: The AudioStreamManager instance.
|
|
947
|
+
/// - data: The received audio data.
|
|
948
|
+
/// - recordingTime: The current recording time.
|
|
949
|
+
/// - totalDataSize: The total size of the received audio data.
|
|
950
|
+
func audioStreamManager(
|
|
951
|
+
_ manager: AudioStreamManager,
|
|
952
|
+
didReceiveAudioData data: Data,
|
|
953
|
+
recordingTime: TimeInterval,
|
|
954
|
+
totalDataSize: Int64,
|
|
955
|
+
compressionInfo: [String: Any]?
|
|
956
|
+
) {
|
|
957
|
+
// Reduce log frequency or detail for this potentially high-frequency event
|
|
958
|
+
// Logger.debug("[AudioStudioModule] Delegate: didReceiveAudioData: size=\(data.count), totalSize=\(totalDataSize)")
|
|
959
|
+
var resultDict: [String: Any] = [
|
|
960
|
+
"fileUri": manager.recordingFileURL?.absoluteString ?? "",
|
|
961
|
+
"lastEmittedSize": totalDataSize,
|
|
962
|
+
"deltaSize": data.count,
|
|
963
|
+
"position": Int64(recordingTime * 1000),
|
|
964
|
+
"mimeType": manager.mimeType,
|
|
965
|
+
"totalSize": totalDataSize,
|
|
966
|
+
"streamUuid": manager.recordingUUID?.uuidString ?? UUID().uuidString
|
|
967
|
+
]
|
|
968
|
+
|
|
969
|
+
if manager.recordingSettings?.streamFormat == "float32" {
|
|
970
|
+
let sampleCount = data.count / 2
|
|
971
|
+
var floatArray = [Float](repeating: 0, count: sampleCount)
|
|
972
|
+
data.withUnsafeBytes { ptr in
|
|
973
|
+
let int16Ptr = ptr.bindMemory(to: Int16.self)
|
|
974
|
+
for i in 0..<sampleCount {
|
|
975
|
+
floatArray[i] = Float(int16Ptr[i]) / 32768.0
|
|
976
|
+
}
|
|
977
|
+
}
|
|
978
|
+
resultDict["pcmFloat32"] = floatArray
|
|
979
|
+
} else {
|
|
980
|
+
resultDict["encoded"] = data.base64EncodedString()
|
|
981
|
+
}
|
|
982
|
+
|
|
983
|
+
if let compressionInfo = compressionInfo {
|
|
984
|
+
resultDict["compression"] = compressionInfo
|
|
985
|
+
}
|
|
986
|
+
|
|
987
|
+
sendEvent(audioDataEvent, resultDict)
|
|
988
|
+
}
|
|
989
|
+
|
|
990
|
+
private func requestNotificationPermissions() async -> Bool {
|
|
991
|
+
do {
|
|
992
|
+
let options: UNAuthorizationOptions = [.alert, .sound]
|
|
993
|
+
return try await notificationCenter.requestAuthorization(options: options)
|
|
994
|
+
} catch {
|
|
995
|
+
Logger.debug("AudioStudioModule", "Failed to request notification permissions: \(error)")
|
|
996
|
+
return false
|
|
997
|
+
}
|
|
998
|
+
}
|
|
999
|
+
|
|
1000
|
+
func audioStreamManager(_ manager: AudioStreamManager, didReceiveProcessingResult result: AudioAnalysisData?) {
|
|
1001
|
+
if let data = result {
|
|
1002
|
+
Logger.debug("AudioStudioModule", "Delegate: didReceiveProcessingResult: Received \(data.dataPoints.count) data points.")
|
|
1003
|
+
} else {
|
|
1004
|
+
Logger.debug("AudioStudioModule", "Delegate: didReceiveProcessingResult: Received nil result.")
|
|
1005
|
+
}
|
|
1006
|
+
let resultDict = result?.toDictionary() ?? [:]
|
|
1007
|
+
sendEvent(audioAnalysisEvent, resultDict)
|
|
1008
|
+
}
|
|
1009
|
+
|
|
1010
|
+
/// Checks microphone permission and calls the completion handler with the result.
|
|
1011
|
+
///
|
|
1012
|
+
/// - Parameters:
|
|
1013
|
+
/// - completion: A completion handler that receives a boolean indicating whether the microphone permission was granted.
|
|
1014
|
+
private func checkMicrophonePermission(completion: @escaping (Bool) -> Void) {
|
|
1015
|
+
switch AVAudioSession.sharedInstance().recordPermission {
|
|
1016
|
+
case .granted:
|
|
1017
|
+
DispatchQueue.main.async { completion(true) }
|
|
1018
|
+
case .denied:
|
|
1019
|
+
DispatchQueue.main.async { completion(false) }
|
|
1020
|
+
case .undetermined:
|
|
1021
|
+
AVAudioSession.sharedInstance().requestRecordPermission { granted in
|
|
1022
|
+
DispatchQueue.main.async {
|
|
1023
|
+
completion(granted)
|
|
1024
|
+
}
|
|
1025
|
+
}
|
|
1026
|
+
@unknown default:
|
|
1027
|
+
DispatchQueue.main.async { completion(false) }
|
|
1028
|
+
}
|
|
1029
|
+
}
|
|
1030
|
+
|
|
1031
|
+
/// Clears all audio files stored in the document directory.
|
|
1032
|
+
private func clearAudioFiles() {
|
|
1033
|
+
let fileURLs = listAudioFiles() // This now returns full URLs as strings
|
|
1034
|
+
fileURLs.forEach { fileURLString in
|
|
1035
|
+
if let fileURL = URL(string: fileURLString) {
|
|
1036
|
+
do {
|
|
1037
|
+
try FileManager.default.removeItem(at: fileURL)
|
|
1038
|
+
print("AudioStudioModule", "Removed file at:", fileURL.path)
|
|
1039
|
+
} catch {
|
|
1040
|
+
print("AudioStudioModule", "Error removing file at \(fileURL.path):", error.localizedDescription)
|
|
1041
|
+
}
|
|
1042
|
+
} else {
|
|
1043
|
+
print("AudioStudioModule", "Invalid URL string: \(fileURLString)")
|
|
1044
|
+
}
|
|
1045
|
+
}
|
|
1046
|
+
}
|
|
1047
|
+
|
|
1048
|
+
/// Extracts feature options from the provided options dictionary.
|
|
1049
|
+
///
|
|
1050
|
+
/// - Parameters:
|
|
1051
|
+
/// - options: The options dictionary containing feature flags.
|
|
1052
|
+
/// - Returns: A dictionary with feature flags and their boolean values.
|
|
1053
|
+
private func extractFeatureOptions(from options: [String: Any]) -> [String: Bool] {
|
|
1054
|
+
return [
|
|
1055
|
+
"energy": options["energy"] as? Bool ?? false,
|
|
1056
|
+
"mfcc": options["mfcc"] as? Bool ?? false,
|
|
1057
|
+
"rms": options["rms"] as? Bool ?? false,
|
|
1058
|
+
"zcr": options["zcr"] as? Bool ?? false,
|
|
1059
|
+
"dB": options["dB"] as? Bool ?? false,
|
|
1060
|
+
"spectralCentroid": options["spectralCentroid"] as? Bool ?? false,
|
|
1061
|
+
"spectralFlatness": options["spectralFlatness"] as? Bool ?? false,
|
|
1062
|
+
"spectralRolloff": options["spectralRolloff"] as? Bool ?? false,
|
|
1063
|
+
"spectralBandwidth": options["spectralBandwidth"] as? Bool ?? false,
|
|
1064
|
+
"chromagram": options["chromagram"] as? Bool ?? false,
|
|
1065
|
+
"tempo": options["tempo"] as? Bool ?? false,
|
|
1066
|
+
"hnr": options["hnr"] as? Bool ?? false,
|
|
1067
|
+
"melSpectrogram": options["melSpectrogram"] as? Bool ?? false,
|
|
1068
|
+
"spectralContrast": options["spectralContrast"] as? Bool ?? false,
|
|
1069
|
+
"tonnetz": options["tonnetz"] as? Bool ?? false,
|
|
1070
|
+
"pitch": options["pitch"] as? Bool ?? false,
|
|
1071
|
+
"crc32": options["crc32"] as? Bool ?? false
|
|
1072
|
+
]
|
|
1073
|
+
}
|
|
1074
|
+
|
|
1075
|
+
/// Lists all audio files stored in the document directory.
|
|
1076
|
+
///
|
|
1077
|
+
/// - Returns: An array of file URIs as strings.
|
|
1078
|
+
func listAudioFiles() -> [String] {
|
|
1079
|
+
guard let documentDirectory = try? FileManager.default.url(for: .documentDirectory, in: .userDomainMask, appropriateFor: nil, create: false) else {
|
|
1080
|
+
print("AudioStudioModule", "Failed to access document directory.")
|
|
1081
|
+
return []
|
|
1082
|
+
}
|
|
1083
|
+
|
|
1084
|
+
do {
|
|
1085
|
+
let files = try FileManager.default.contentsOfDirectory(at: documentDirectory, includingPropertiesForKeys: nil)
|
|
1086
|
+
let audioFiles = files.filter { $0.pathExtension == "wav" }.map { $0.absoluteString }
|
|
1087
|
+
return audioFiles
|
|
1088
|
+
} catch {
|
|
1089
|
+
print("AudioStudioModule", "Error listing audio files:", error.localizedDescription)
|
|
1090
|
+
return []
|
|
1091
|
+
}
|
|
1092
|
+
}
|
|
1093
|
+
|
|
1094
|
+
func audioStreamManager(_ manager: AudioStreamManager, didFailWithError error: String) {
|
|
1095
|
+
Logger.error("AudioStudioModule", "Delegate: didFailWithError: \(error)")
|
|
1096
|
+
sendEvent(errorEvent, [ "message": error ])
|
|
1097
|
+
}
|
|
1098
|
+
|
|
1099
|
+
// MARK: - AudioDeviceManagerDelegate
|
|
1100
|
+
|
|
1101
|
+
/// Handles device disconnection events from the AudioDeviceManager
|
|
1102
|
+
func audioDeviceManager(_ manager: AudioDeviceManager, didDetectDisconnectionOfDevice deviceId: String) {
|
|
1103
|
+
Logger.debug("AudioStudioModule", "Device disconnected: \(deviceId)")
|
|
1104
|
+
|
|
1105
|
+
// Emit device change event to match Android implementation
|
|
1106
|
+
sendEvent(deviceChangedEvent, [
|
|
1107
|
+
"type": "deviceDisconnected",
|
|
1108
|
+
"deviceId": deviceId
|
|
1109
|
+
])
|
|
1110
|
+
}
|
|
1111
|
+
}
|