@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,1112 @@
|
|
|
1
|
+
// packages/audio-studio/android/src/main/java/net/siteed/audiostudio/AudioStudioModule.kt
|
|
2
|
+
package net.siteed.audiostudio
|
|
3
|
+
|
|
4
|
+
import android.Manifest
|
|
5
|
+
import android.os.Build
|
|
6
|
+
import android.os.Bundle
|
|
7
|
+
import android.util.Log
|
|
8
|
+
import android.content.pm.PackageManager
|
|
9
|
+
import androidx.annotation.RequiresApi
|
|
10
|
+
import androidx.core.content.ContextCompat
|
|
11
|
+
import androidx.core.os.bundleOf
|
|
12
|
+
import expo.modules.kotlin.Promise
|
|
13
|
+
import expo.modules.kotlin.modules.Module
|
|
14
|
+
import expo.modules.kotlin.modules.ModuleDefinition
|
|
15
|
+
import expo.modules.interfaces.permissions.Permissions
|
|
16
|
+
import java.util.zip.CRC32
|
|
17
|
+
import kotlinx.coroutines.CoroutineScope
|
|
18
|
+
import kotlinx.coroutines.Dispatchers
|
|
19
|
+
import kotlinx.coroutines.launch
|
|
20
|
+
import kotlinx.coroutines.withContext
|
|
21
|
+
|
|
22
|
+
class AudioStudioModule : Module(), EventSender {
|
|
23
|
+
companion object {
|
|
24
|
+
private const val CLASS_NAME = "AudioStudioModule"
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
private lateinit var audioRecorderManager: AudioRecorderManager
|
|
28
|
+
private lateinit var audioProcessor: AudioProcessor
|
|
29
|
+
private lateinit var audioDeviceManager: AudioDeviceManager
|
|
30
|
+
private var enablePhoneStateHandling: Boolean = false // Default to false until we check manifest
|
|
31
|
+
private var enableNotificationHandling: Boolean = false // Default to false until we check manifest
|
|
32
|
+
private var enableBackgroundAudio: Boolean = false // Default to false until we check manifest
|
|
33
|
+
private var enableDeviceDetection: Boolean = false // Default to false until we check manifest
|
|
34
|
+
private val coroutineScope = CoroutineScope(Dispatchers.Main)
|
|
35
|
+
|
|
36
|
+
private val audioFileHandler by lazy {
|
|
37
|
+
AudioFileHandler(appContext.reactContext?.filesDir ?: throw IllegalStateException("React context not available"))
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
private val audioTrimmer by lazy {
|
|
41
|
+
AudioTrimmer(
|
|
42
|
+
appContext.reactContext ?: throw IllegalStateException("React context not available"),
|
|
43
|
+
audioFileHandler
|
|
44
|
+
)
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
@RequiresApi(Build.VERSION_CODES.R)
|
|
48
|
+
override fun definition() = ModuleDefinition {
|
|
49
|
+
// The module will be accessible from `requireNativeModule('AudioStudio')` in JavaScript.
|
|
50
|
+
Name("AudioStudio")
|
|
51
|
+
|
|
52
|
+
// Check permissions declared in the manifest
|
|
53
|
+
try {
|
|
54
|
+
val context = appContext.reactContext ?: throw IllegalStateException("React context not available")
|
|
55
|
+
val packageInfo = context.packageManager.getPackageInfo(
|
|
56
|
+
context.packageName,
|
|
57
|
+
PackageManager.GET_PERMISSIONS
|
|
58
|
+
)
|
|
59
|
+
|
|
60
|
+
// Check if READ_PHONE_STATE is in the requested permissions
|
|
61
|
+
enablePhoneStateHandling = packageInfo.requestedPermissions?.contains(Manifest.permission.READ_PHONE_STATE) ?: false
|
|
62
|
+
|
|
63
|
+
// Check if POST_NOTIFICATIONS is in the requested permissions
|
|
64
|
+
enableNotificationHandling = packageInfo.requestedPermissions?.contains(Manifest.permission.POST_NOTIFICATIONS) ?: false
|
|
65
|
+
|
|
66
|
+
// Check if background audio is enabled by looking for FOREGROUND_SERVICE_MICROPHONE permission
|
|
67
|
+
enableBackgroundAudio = packageInfo.requestedPermissions?.contains(Manifest.permission.FOREGROUND_SERVICE_MICROPHONE) ?: false
|
|
68
|
+
|
|
69
|
+
// Check if device detection is enabled by looking for BLUETOOTH_CONNECT permission
|
|
70
|
+
enableDeviceDetection = packageInfo.requestedPermissions?.contains(Manifest.permission.BLUETOOTH_CONNECT) ?: false
|
|
71
|
+
|
|
72
|
+
LogUtils.d(CLASS_NAME, "Phone state handling ${if (enablePhoneStateHandling) "enabled" else "disabled"} based on manifest permissions")
|
|
73
|
+
LogUtils.d(CLASS_NAME, "Notification handling ${if (enableNotificationHandling) "enabled" else "disabled"} based on manifest permissions")
|
|
74
|
+
LogUtils.d(CLASS_NAME, "Background audio handling ${if (enableBackgroundAudio) "enabled" else "disabled"} based on manifest permissions")
|
|
75
|
+
LogUtils.d(CLASS_NAME, "Device detection ${if (enableDeviceDetection) "enabled" else "disabled"} based on manifest permissions")
|
|
76
|
+
} catch (e: Exception) {
|
|
77
|
+
LogUtils.e(CLASS_NAME, "Failed to check manifest permissions: ${e.message}", e)
|
|
78
|
+
enablePhoneStateHandling = false
|
|
79
|
+
enableNotificationHandling = false
|
|
80
|
+
enableBackgroundAudio = false
|
|
81
|
+
enableDeviceDetection = false
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
Events(
|
|
85
|
+
Constants.AUDIO_EVENT_NAME,
|
|
86
|
+
Constants.AUDIO_ANALYSIS_EVENT_NAME,
|
|
87
|
+
Constants.RECORDING_INTERRUPTED_EVENT_NAME,
|
|
88
|
+
Constants.TRIM_PROGRESS_EVENT,
|
|
89
|
+
Constants.DEVICE_CHANGED_EVENT // Add device changed event name
|
|
90
|
+
)
|
|
91
|
+
|
|
92
|
+
// Initialize Managers
|
|
93
|
+
initializeManager()
|
|
94
|
+
|
|
95
|
+
// Add a convenience function to check for foreground service permission separately
|
|
96
|
+
fun isForegroundServiceMicRequired(): Boolean {
|
|
97
|
+
return Build.VERSION.SDK_INT >= Build.VERSION_CODES.UPSIDE_DOWN_CAKE && enableBackgroundAudio
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
// Helper function to check if device detection is enabled
|
|
101
|
+
fun isDeviceDetectionEnabled(): Boolean {
|
|
102
|
+
return enableDeviceDetection
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
// Add device-related functions to the module
|
|
106
|
+
|
|
107
|
+
// Gets available audio input devices with an optional refresh parameter
|
|
108
|
+
AsyncFunction("getAvailableInputDevices") { options: Map<String, Any>?, promise: Promise ->
|
|
109
|
+
try {
|
|
110
|
+
LogUtils.d(CLASS_NAME, "getAvailableInputDevices called. Refresh: ${options?.get("refresh") ?: false}")
|
|
111
|
+
|
|
112
|
+
// Check if refresh is requested
|
|
113
|
+
if (options?.get("refresh") as? Boolean == true) {
|
|
114
|
+
audioDeviceManager.forceRefreshAudioDevices()
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
// Get the list of devices
|
|
118
|
+
audioDeviceManager.getAvailableInputDevices(promise)
|
|
119
|
+
} catch (e: Exception) {
|
|
120
|
+
LogUtils.e(CLASS_NAME, "Error getting available input devices: ${e.message}", e)
|
|
121
|
+
promise.reject("DEVICE_ERROR", "Failed to get available audio devices: ${e.message}", e)
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
// Gets the currently selected audio input device
|
|
126
|
+
AsyncFunction("getCurrentInputDevice") { promise: Promise ->
|
|
127
|
+
try {
|
|
128
|
+
LogUtils.d(CLASS_NAME, "getCurrentInputDevice called")
|
|
129
|
+
audioDeviceManager.getCurrentInputDevice(promise)
|
|
130
|
+
} catch (e: Exception) {
|
|
131
|
+
LogUtils.e(CLASS_NAME, "Error getting current input device: ${e.message}", e)
|
|
132
|
+
promise.reject("DEVICE_ERROR", "Failed to get current audio device: ${e.message}", e)
|
|
133
|
+
}
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
// Selects a specific audio input device for recording
|
|
137
|
+
AsyncFunction("selectInputDevice") { deviceId: String, promise: Promise ->
|
|
138
|
+
try {
|
|
139
|
+
LogUtils.d(CLASS_NAME, "selectInputDevice called with ID: $deviceId")
|
|
140
|
+
audioDeviceManager.selectInputDevice(deviceId, promise)
|
|
141
|
+
|
|
142
|
+
// Update recording if in progress
|
|
143
|
+
if (audioRecorderManager.isRecording || audioRecorderManager.isPrepared) {
|
|
144
|
+
LogUtils.d(CLASS_NAME, "selectInputDevice: Notifying recorder of device change")
|
|
145
|
+
audioRecorderManager.handleDeviceChange()
|
|
146
|
+
}
|
|
147
|
+
} catch (e: Exception) {
|
|
148
|
+
LogUtils.e(CLASS_NAME, "Error selecting input device: ${e.message}", e)
|
|
149
|
+
promise.reject("DEVICE_ERROR", "Failed to select audio device: ${e.message}", e)
|
|
150
|
+
}
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
// Resets to the default audio input device
|
|
154
|
+
AsyncFunction("resetToDefaultDevice") { promise: Promise ->
|
|
155
|
+
try {
|
|
156
|
+
LogUtils.d(CLASS_NAME, "resetToDefaultDevice called")
|
|
157
|
+
audioDeviceManager.resetToDefaultDevice { success, error ->
|
|
158
|
+
if (success) {
|
|
159
|
+
// Update recording if in progress
|
|
160
|
+
if (audioRecorderManager.isRecording || audioRecorderManager.isPrepared) {
|
|
161
|
+
LogUtils.d(CLASS_NAME, "resetToDefaultDevice: Notifying recorder of device change")
|
|
162
|
+
audioRecorderManager.handleDeviceChange()
|
|
163
|
+
}
|
|
164
|
+
promise.resolve(true)
|
|
165
|
+
} else {
|
|
166
|
+
LogUtils.e(CLASS_NAME, "Failed to reset to default device: ${error?.message}")
|
|
167
|
+
promise.reject("DEVICE_ERROR", "Failed to reset to default device: ${error?.message}", error)
|
|
168
|
+
}
|
|
169
|
+
}
|
|
170
|
+
} catch (e: Exception) {
|
|
171
|
+
LogUtils.e(CLASS_NAME, "Error resetting to default device: ${e.message}", e)
|
|
172
|
+
promise.reject("DEVICE_ERROR", "Failed to reset to default device: ${e.message}", e)
|
|
173
|
+
}
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
// Refreshes the audio devices list
|
|
177
|
+
Function("refreshAudioDevices") {
|
|
178
|
+
LogUtils.d(CLASS_NAME, "refreshAudioDevices called")
|
|
179
|
+
val success = audioDeviceManager.forceRefreshAudioDevices()
|
|
180
|
+
return@Function mapOf("success" to success)
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
|
|
184
|
+
|
|
185
|
+
AsyncFunction("prepareRecording") { options: Map<String, Any?>, promise: Promise ->
|
|
186
|
+
try {
|
|
187
|
+
// If notifications are requested but permission not in manifest, modify options
|
|
188
|
+
if (options["showNotification"] as? Boolean == true && !enableNotificationHandling) {
|
|
189
|
+
val modifiedOptions = options.toMutableMap()
|
|
190
|
+
modifiedOptions["showNotification"] = false
|
|
191
|
+
LogUtils.d(CLASS_NAME, "Notification permission not in manifest, disabling showNotification")
|
|
192
|
+
|
|
193
|
+
if (audioRecorderManager.prepareRecording(modifiedOptions)) {
|
|
194
|
+
promise.resolve(true)
|
|
195
|
+
} else {
|
|
196
|
+
promise.reject("PREPARE_ERROR", "Failed to prepare recording", null)
|
|
197
|
+
}
|
|
198
|
+
} else {
|
|
199
|
+
if (audioRecorderManager.prepareRecording(options)) {
|
|
200
|
+
promise.resolve(true)
|
|
201
|
+
} else {
|
|
202
|
+
promise.reject("PREPARE_ERROR", "Failed to prepare recording", null)
|
|
203
|
+
}
|
|
204
|
+
}
|
|
205
|
+
} catch (e: Exception) {
|
|
206
|
+
LogUtils.e(CLASS_NAME, "Error preparing recording", e)
|
|
207
|
+
promise.reject("PREPARE_ERROR", "Failed to prepare recording: ${e.message}", e)
|
|
208
|
+
}
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
AsyncFunction("startRecording") { options: Map<String, Any?>, promise: Promise ->
|
|
212
|
+
// If notifications are requested but permission not in manifest, modify options
|
|
213
|
+
if (options["showNotification"] as? Boolean == true && !enableNotificationHandling) {
|
|
214
|
+
val modifiedOptions = options.toMutableMap()
|
|
215
|
+
modifiedOptions["showNotification"] = false
|
|
216
|
+
LogUtils.d(CLASS_NAME, "Notification permission not in manifest, disabling showNotification")
|
|
217
|
+
audioRecorderManager.startRecording(modifiedOptions, promise)
|
|
218
|
+
} else {
|
|
219
|
+
audioRecorderManager.startRecording(options, promise)
|
|
220
|
+
}
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
Function("clearAudioFiles") {
|
|
224
|
+
audioRecorderManager.clearAudioStorage()
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
Function("status") {
|
|
228
|
+
return@Function audioRecorderManager.getStatus()
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
AsyncFunction("listAudioFiles") { promise: Promise ->
|
|
232
|
+
audioRecorderManager.listAudioFiles(promise)
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
AsyncFunction("pauseRecording") { promise: Promise ->
|
|
236
|
+
audioRecorderManager.pauseRecording(promise)
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
AsyncFunction("resumeRecording") { promise: Promise ->
|
|
240
|
+
LogUtils.d(CLASS_NAME, "⏺️ resumeRecording() called from JS layer")
|
|
241
|
+
try {
|
|
242
|
+
audioRecorderManager.resumeRecording(object : Promise {
|
|
243
|
+
override fun resolve(value: Any?) {
|
|
244
|
+
LogUtils.d(CLASS_NAME, "⏺️ resumeRecording completed successfully")
|
|
245
|
+
promise.resolve(value)
|
|
246
|
+
}
|
|
247
|
+
override fun reject(code: String, message: String?, cause: Throwable?) {
|
|
248
|
+
LogUtils.e(CLASS_NAME, "⏺️ resumeRecording failed: $code - $message", cause)
|
|
249
|
+
promise.reject(code, message, cause)
|
|
250
|
+
}
|
|
251
|
+
})
|
|
252
|
+
} catch (e: Exception) {
|
|
253
|
+
LogUtils.e(CLASS_NAME, "⏺️ Exception when calling resumeRecording: ${e.message}", e)
|
|
254
|
+
promise.reject("RESUME_ERROR", "Failed to resume recording: ${e.message}", e)
|
|
255
|
+
}
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
AsyncFunction("stopRecording") { promise: Promise ->
|
|
259
|
+
audioRecorderManager.stopRecording(promise)
|
|
260
|
+
}
|
|
261
|
+
|
|
262
|
+
AsyncFunction("requestPermissionsAsync") { promise: Promise ->
|
|
263
|
+
try {
|
|
264
|
+
val permissions = mutableListOf(
|
|
265
|
+
Manifest.permission.RECORD_AUDIO
|
|
266
|
+
)
|
|
267
|
+
|
|
268
|
+
// Only add phone state permission if enabled
|
|
269
|
+
if (enablePhoneStateHandling) {
|
|
270
|
+
permissions.add(Manifest.permission.READ_PHONE_STATE)
|
|
271
|
+
}
|
|
272
|
+
|
|
273
|
+
// Add foreground service permission for Android 14+ only if background audio is enabled
|
|
274
|
+
if (isForegroundServiceMicRequired()) {
|
|
275
|
+
LogUtils.d(CLASS_NAME, "Adding FOREGROUND_SERVICE_MICROPHONE permission request")
|
|
276
|
+
permissions.add(Manifest.permission.FOREGROUND_SERVICE_MICROPHONE)
|
|
277
|
+
}
|
|
278
|
+
|
|
279
|
+
// Add device detection permissions if device detection is enabled
|
|
280
|
+
if (isDeviceDetectionEnabled()) {
|
|
281
|
+
// BLUETOOTH_CONNECT is needed on Android 12+ to access device names/addresses
|
|
282
|
+
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) {
|
|
283
|
+
permissions.add(Manifest.permission.BLUETOOTH_CONNECT)
|
|
284
|
+
LogUtils.d(CLASS_NAME, "Adding BLUETOOTH_CONNECT permission request for device detection")
|
|
285
|
+
}
|
|
286
|
+
}
|
|
287
|
+
|
|
288
|
+
LogUtils.d(CLASS_NAME, "Requesting permissions: $permissions")
|
|
289
|
+
Permissions.askForPermissionsWithPermissionsManager(
|
|
290
|
+
appContext.permissions,
|
|
291
|
+
promise,
|
|
292
|
+
*permissions.toTypedArray()
|
|
293
|
+
)
|
|
294
|
+
} catch (e: Exception) {
|
|
295
|
+
LogUtils.e(CLASS_NAME, "Error requesting permissions", e)
|
|
296
|
+
promise.reject("PERMISSION_ERROR", "Failed to request permissions: ${e.message}", e)
|
|
297
|
+
}
|
|
298
|
+
}
|
|
299
|
+
|
|
300
|
+
AsyncFunction("getPermissionsAsync") { promise: Promise ->
|
|
301
|
+
val permissions = mutableListOf(
|
|
302
|
+
Manifest.permission.RECORD_AUDIO
|
|
303
|
+
)
|
|
304
|
+
|
|
305
|
+
// Only add phone state permission if enabled
|
|
306
|
+
if (enablePhoneStateHandling) {
|
|
307
|
+
permissions.add(Manifest.permission.READ_PHONE_STATE)
|
|
308
|
+
}
|
|
309
|
+
|
|
310
|
+
// Only check foreground service permission when background audio is enabled
|
|
311
|
+
if (isForegroundServiceMicRequired()) {
|
|
312
|
+
permissions.add(Manifest.permission.FOREGROUND_SERVICE_MICROPHONE)
|
|
313
|
+
}
|
|
314
|
+
|
|
315
|
+
// Add device detection permissions if enabled
|
|
316
|
+
if (isDeviceDetectionEnabled()) {
|
|
317
|
+
// BLUETOOTH_CONNECT is needed on Android 12+ to access device names/addresses
|
|
318
|
+
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) {
|
|
319
|
+
permissions.add(Manifest.permission.BLUETOOTH_CONNECT)
|
|
320
|
+
}
|
|
321
|
+
}
|
|
322
|
+
|
|
323
|
+
Permissions.getPermissionsWithPermissionsManager(
|
|
324
|
+
appContext.permissions,
|
|
325
|
+
promise,
|
|
326
|
+
*permissions.toTypedArray()
|
|
327
|
+
)
|
|
328
|
+
}
|
|
329
|
+
|
|
330
|
+
AsyncFunction("requestNotificationPermissionsAsync") { promise: Promise ->
|
|
331
|
+
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU && enableNotificationHandling) {
|
|
332
|
+
// Only request notification permissions if enabled in manifest
|
|
333
|
+
Permissions.askForPermissionsWithPermissionsManager(
|
|
334
|
+
appContext.permissions,
|
|
335
|
+
promise,
|
|
336
|
+
Manifest.permission.POST_NOTIFICATIONS
|
|
337
|
+
)
|
|
338
|
+
} else {
|
|
339
|
+
// Either notifications not required or running on Android < 13
|
|
340
|
+
promise.resolve(
|
|
341
|
+
bundleOf(
|
|
342
|
+
"status" to "granted",
|
|
343
|
+
"expires" to "never",
|
|
344
|
+
"granted" to true
|
|
345
|
+
)
|
|
346
|
+
)
|
|
347
|
+
}
|
|
348
|
+
}
|
|
349
|
+
|
|
350
|
+
AsyncFunction("getNotificationPermissionsAsync") { promise: Promise ->
|
|
351
|
+
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU && enableNotificationHandling) {
|
|
352
|
+
// Only check notification permissions if enabled in manifest
|
|
353
|
+
Permissions.getPermissionsWithPermissionsManager(
|
|
354
|
+
appContext.permissions,
|
|
355
|
+
promise,
|
|
356
|
+
Manifest.permission.POST_NOTIFICATIONS
|
|
357
|
+
)
|
|
358
|
+
} else {
|
|
359
|
+
// Either notifications not required or running on Android < 13
|
|
360
|
+
promise.resolve(
|
|
361
|
+
bundleOf(
|
|
362
|
+
"status" to "granted",
|
|
363
|
+
"expires" to "never",
|
|
364
|
+
"granted" to true
|
|
365
|
+
)
|
|
366
|
+
)
|
|
367
|
+
}
|
|
368
|
+
}
|
|
369
|
+
|
|
370
|
+
AsyncFunction("trimAudio") { options: Map<String, Any>, promise: Promise ->
|
|
371
|
+
try {
|
|
372
|
+
val fileUri = options["fileUri"] as? String ?: run {
|
|
373
|
+
promise.reject("INVALID_URI", "fileUri is required", null)
|
|
374
|
+
return@AsyncFunction
|
|
375
|
+
}
|
|
376
|
+
|
|
377
|
+
LogUtils.d(CLASS_NAME, "trimAudio called with fileUri: $fileUri")
|
|
378
|
+
LogUtils.d(CLASS_NAME, "Full options: $options")
|
|
379
|
+
|
|
380
|
+
val mode = options["mode"] as? String ?: "single"
|
|
381
|
+
val startTimeMs = (options["startTimeMs"] as? Number)?.toLong()
|
|
382
|
+
val endTimeMs = (options["endTimeMs"] as? Number)?.toLong()
|
|
383
|
+
|
|
384
|
+
@Suppress("UNCHECKED_CAST")
|
|
385
|
+
val ranges = options["ranges"] as? List<Map<String, Long>>
|
|
386
|
+
|
|
387
|
+
val outputFileName = options["outputFileName"] as? String
|
|
388
|
+
|
|
389
|
+
@Suppress("UNCHECKED_CAST")
|
|
390
|
+
var outputFormatMap = options["outputFormat"] as? Map<String, Any>
|
|
391
|
+
|
|
392
|
+
// Validate output format if provided
|
|
393
|
+
if (outputFormatMap != null) {
|
|
394
|
+
val format = outputFormatMap["format"] as? String
|
|
395
|
+
if (format != null && format != "wav" && format != "aac" && format != "opus") {
|
|
396
|
+
LogUtils.w(CLASS_NAME, "Requested format '$format' is not fully supported. Using 'aac' instead.")
|
|
397
|
+
// Create a new map with the corrected format
|
|
398
|
+
val newOutputFormat = HashMap<String, Any>(outputFormatMap)
|
|
399
|
+
newOutputFormat["format"] = "aac"
|
|
400
|
+
outputFormatMap = newOutputFormat
|
|
401
|
+
}
|
|
402
|
+
}
|
|
403
|
+
|
|
404
|
+
LogUtils.d(CLASS_NAME, "Output format options: $outputFormatMap")
|
|
405
|
+
|
|
406
|
+
// Create progress listener
|
|
407
|
+
val progressListener = object : AudioTrimmer.ProgressListener {
|
|
408
|
+
override fun onProgress(progress: Float, bytesProcessed: Long, totalBytes: Long) {
|
|
409
|
+
sendEvent(Constants.TRIM_PROGRESS_EVENT, mapOf(
|
|
410
|
+
"progress" to progress,
|
|
411
|
+
"bytesProcessed" to bytesProcessed,
|
|
412
|
+
"totalBytes" to totalBytes
|
|
413
|
+
))
|
|
414
|
+
}
|
|
415
|
+
}
|
|
416
|
+
|
|
417
|
+
// Record start time
|
|
418
|
+
val startTime = System.currentTimeMillis()
|
|
419
|
+
|
|
420
|
+
// Perform the trim operation
|
|
421
|
+
val result = audioTrimmer.trimAudio(
|
|
422
|
+
fileUri = fileUri,
|
|
423
|
+
mode = mode,
|
|
424
|
+
startTimeMs = startTimeMs,
|
|
425
|
+
endTimeMs = endTimeMs,
|
|
426
|
+
ranges = ranges,
|
|
427
|
+
outputFileName = outputFileName,
|
|
428
|
+
outputFormat = outputFormatMap,
|
|
429
|
+
progressListener = progressListener
|
|
430
|
+
)
|
|
431
|
+
|
|
432
|
+
// Calculate processing time
|
|
433
|
+
val processingTimeMs = System.currentTimeMillis() - startTime
|
|
434
|
+
|
|
435
|
+
// Add processing time to result
|
|
436
|
+
val resultWithProcessingTime = result.toMutableMap()
|
|
437
|
+
resultWithProcessingTime["processingInfo"] = mapOf(
|
|
438
|
+
"durationMs" to processingTimeMs
|
|
439
|
+
)
|
|
440
|
+
|
|
441
|
+
LogUtils.d(CLASS_NAME, "Trim operation completed successfully in ${processingTimeMs}ms: $result")
|
|
442
|
+
promise.resolve(resultWithProcessingTime)
|
|
443
|
+
} catch (e: Exception) {
|
|
444
|
+
LogUtils.e(CLASS_NAME, "Error trimming audio: ${e.message}", e)
|
|
445
|
+
promise.reject("TRIM_ERROR", "Error trimming audio: ${e.message}", e)
|
|
446
|
+
}
|
|
447
|
+
}
|
|
448
|
+
|
|
449
|
+
AsyncFunction("extractMelSpectrogram") { options: Map<String, Any>, promise: Promise ->
|
|
450
|
+
try {
|
|
451
|
+
// Log all incoming options for debugging
|
|
452
|
+
LogUtils.d(CLASS_NAME, "extractMelSpectrogram called with options: $options")
|
|
453
|
+
|
|
454
|
+
// Extract required parameters with detailed logging
|
|
455
|
+
val fileUri = options["fileUri"] as? String
|
|
456
|
+
LogUtils.d(CLASS_NAME, "fileUri: $fileUri")
|
|
457
|
+
if (fileUri == null) {
|
|
458
|
+
LogUtils.e(CLASS_NAME, "Missing required parameter: fileUri")
|
|
459
|
+
throw IllegalArgumentException("fileUri is required")
|
|
460
|
+
}
|
|
461
|
+
|
|
462
|
+
val windowSizeMs = options["windowSizeMs"] as? Double
|
|
463
|
+
LogUtils.d(CLASS_NAME, "windowSizeMs: $windowSizeMs")
|
|
464
|
+
if (windowSizeMs == null) {
|
|
465
|
+
LogUtils.e(CLASS_NAME, "Missing required parameter: windowSizeMs")
|
|
466
|
+
throw IllegalArgumentException("windowSizeMs is required")
|
|
467
|
+
}
|
|
468
|
+
|
|
469
|
+
val hopLengthMs = options["hopLengthMs"] as? Double
|
|
470
|
+
LogUtils.d(CLASS_NAME, "hopLengthMs: $hopLengthMs")
|
|
471
|
+
if (hopLengthMs == null) {
|
|
472
|
+
LogUtils.e(CLASS_NAME, "Missing required parameter: hopLengthMs")
|
|
473
|
+
throw IllegalArgumentException("hopLengthMs is required")
|
|
474
|
+
}
|
|
475
|
+
|
|
476
|
+
// Handle nMels which might come as Double from JavaScript
|
|
477
|
+
val nMelsValue = options["nMels"]
|
|
478
|
+
LogUtils.d(CLASS_NAME, "Raw nMels value: $nMelsValue (type: ${nMelsValue?.javaClass?.name})")
|
|
479
|
+
|
|
480
|
+
val nMels = when (nMelsValue) {
|
|
481
|
+
is Int -> nMelsValue
|
|
482
|
+
is Double -> nMelsValue.toInt()
|
|
483
|
+
is Number -> nMelsValue.toInt()
|
|
484
|
+
else -> {
|
|
485
|
+
LogUtils.e(CLASS_NAME, "Missing or invalid required parameter: nMels")
|
|
486
|
+
throw IllegalArgumentException("nMels is required and must be a number")
|
|
487
|
+
}
|
|
488
|
+
}
|
|
489
|
+
|
|
490
|
+
LogUtils.d(CLASS_NAME, "Converted nMels: $nMels (from ${nMelsValue?.javaClass?.name})")
|
|
491
|
+
|
|
492
|
+
// Extract optional parameters with defaults
|
|
493
|
+
val fMin = options["fMin"] as? Double ?: 0.0
|
|
494
|
+
val fMax = options["fMax"] as? Double
|
|
495
|
+
val windowType = options["windowType"] as? String ?: "hann"
|
|
496
|
+
val normalize = options["normalize"] as? Boolean ?: false
|
|
497
|
+
val logScale = options["logScale"] as? Boolean ?: true
|
|
498
|
+
|
|
499
|
+
// Fix the conversion from Number to Long to preserve decimal values
|
|
500
|
+
val startTimeMsNumber = options["startTimeMs"] as? Number
|
|
501
|
+
val endTimeMsNumber = options["endTimeMs"] as? Number
|
|
502
|
+
val startTimeMs = startTimeMsNumber?.toLong() ?: startTimeMsNumber?.toDouble()?.toLong()
|
|
503
|
+
val endTimeMs = endTimeMsNumber?.toLong() ?: endTimeMsNumber?.toDouble()?.toLong()
|
|
504
|
+
|
|
505
|
+
LogUtils.d(CLASS_NAME, """
|
|
506
|
+
Optional parameters:
|
|
507
|
+
- fMin: $fMin
|
|
508
|
+
- fMax: $fMax
|
|
509
|
+
- windowType: $windowType
|
|
510
|
+
- normalize: $normalize
|
|
511
|
+
- logScale: $logScale
|
|
512
|
+
- startTimeMs: $startTimeMs (original: $startTimeMsNumber)
|
|
513
|
+
- endTimeMs: $endTimeMs (original: $endTimeMsNumber)
|
|
514
|
+
""".trimIndent())
|
|
515
|
+
|
|
516
|
+
// Handle decoding options
|
|
517
|
+
val decodingOptions = options["decodingOptions"] as? Map<String, Any>
|
|
518
|
+
LogUtils.d(CLASS_NAME, "Decoding options: $decodingOptions")
|
|
519
|
+
|
|
520
|
+
val config = decodingOptions?.let {
|
|
521
|
+
val targetSampleRateValue = it["targetSampleRate"]
|
|
522
|
+
val targetSampleRate = when (targetSampleRateValue) {
|
|
523
|
+
is Int -> targetSampleRateValue
|
|
524
|
+
is Double -> targetSampleRateValue.toInt()
|
|
525
|
+
is Number -> targetSampleRateValue.toInt()
|
|
526
|
+
else -> null
|
|
527
|
+
}
|
|
528
|
+
|
|
529
|
+
val targetChannelsValue = it["targetChannels"]
|
|
530
|
+
val targetChannels = when (targetChannelsValue) {
|
|
531
|
+
is Int -> targetChannelsValue
|
|
532
|
+
is Double -> targetChannelsValue.toInt()
|
|
533
|
+
is Number -> targetChannelsValue.toInt()
|
|
534
|
+
else -> 1
|
|
535
|
+
}
|
|
536
|
+
|
|
537
|
+
val targetBitDepthValue = it["targetBitDepth"]
|
|
538
|
+
val targetBitDepth = when (targetBitDepthValue) {
|
|
539
|
+
is Int -> targetBitDepthValue
|
|
540
|
+
is Double -> targetBitDepthValue.toInt()
|
|
541
|
+
is Number -> targetBitDepthValue.toInt()
|
|
542
|
+
else -> 16
|
|
543
|
+
}
|
|
544
|
+
|
|
545
|
+
val normalizeAudio = it["normalizeAudio"] as? Boolean ?: false
|
|
546
|
+
|
|
547
|
+
DecodingConfig(
|
|
548
|
+
targetSampleRate = targetSampleRate,
|
|
549
|
+
targetChannels = targetChannels,
|
|
550
|
+
targetBitDepth = targetBitDepth,
|
|
551
|
+
normalizeAudio = normalizeAudio
|
|
552
|
+
).also { config ->
|
|
553
|
+
LogUtils.d(CLASS_NAME, """
|
|
554
|
+
Using decoding config:
|
|
555
|
+
- targetSampleRate: ${config.targetSampleRate ?: "original"}
|
|
556
|
+
- targetChannels: ${config.targetChannels ?: "original"}
|
|
557
|
+
- targetBitDepth: ${config.targetBitDepth}
|
|
558
|
+
- normalizeAudio: ${config.normalizeAudio}
|
|
559
|
+
""".trimIndent())
|
|
560
|
+
}
|
|
561
|
+
} ?: DecodingConfig(targetSampleRate = null, targetChannels = 1, targetBitDepth = 16).also {
|
|
562
|
+
LogUtils.d(CLASS_NAME, "Using default decoding config")
|
|
563
|
+
}
|
|
564
|
+
|
|
565
|
+
// Check if the audio data is too short
|
|
566
|
+
if (startTimeMs != null && endTimeMs != null) {
|
|
567
|
+
val durationMs = endTimeMs - startTimeMs
|
|
568
|
+
LogUtils.d(CLASS_NAME, "Audio duration for spectrogram: $durationMs ms")
|
|
569
|
+
if (durationMs < 25) { // 25ms is minimum for a single window
|
|
570
|
+
LogUtils.w(CLASS_NAME, "Audio duration is too short for spectrogram analysis: $durationMs ms")
|
|
571
|
+
throw IllegalArgumentException("Audio duration must be at least 25ms for spectrogram analysis")
|
|
572
|
+
}
|
|
573
|
+
}
|
|
574
|
+
|
|
575
|
+
// Load audio data with optional time range
|
|
576
|
+
LogUtils.d(CLASS_NAME, "Loading audio data...")
|
|
577
|
+
val audioData = when {
|
|
578
|
+
startTimeMs != null && endTimeMs != null -> {
|
|
579
|
+
LogUtils.d(CLASS_NAME, "Loading audio range: $startTimeMs to $endTimeMs ms")
|
|
580
|
+
audioProcessor.loadAudioRange(fileUri, startTimeMs, endTimeMs, config)
|
|
581
|
+
}
|
|
582
|
+
else -> {
|
|
583
|
+
LogUtils.d(CLASS_NAME, "Loading entire audio file")
|
|
584
|
+
audioProcessor.loadAudioFromAnyFormat(fileUri, config)
|
|
585
|
+
}
|
|
586
|
+
}
|
|
587
|
+
|
|
588
|
+
if (audioData == null) {
|
|
589
|
+
LogUtils.e(CLASS_NAME, "Failed to load audio data")
|
|
590
|
+
throw IllegalStateException("Failed to load audio data")
|
|
591
|
+
}
|
|
592
|
+
|
|
593
|
+
LogUtils.d(CLASS_NAME, """
|
|
594
|
+
Audio data loaded successfully:
|
|
595
|
+
- data size: ${audioData.data.size} bytes
|
|
596
|
+
- sampleRate: ${audioData.sampleRate}
|
|
597
|
+
- channels: ${audioData.channels}
|
|
598
|
+
- bitDepth: ${audioData.bitDepth}
|
|
599
|
+
- durationMs: ${audioData.durationMs}
|
|
600
|
+
""".trimIndent())
|
|
601
|
+
|
|
602
|
+
// Validate that we have enough audio data for processing
|
|
603
|
+
if (audioData.data.size == 0 || audioData.durationMs < windowSizeMs) {
|
|
604
|
+
LogUtils.e(CLASS_NAME, "Audio data is too short for spectrogram analysis: ${audioData.durationMs}ms, data size: ${audioData.data.size} bytes")
|
|
605
|
+
throw IllegalArgumentException(
|
|
606
|
+
"Audio data is too short for spectrogram analysis. " +
|
|
607
|
+
"Duration: ${audioData.durationMs}ms, minimum required: ${windowSizeMs}ms"
|
|
608
|
+
)
|
|
609
|
+
}
|
|
610
|
+
|
|
611
|
+
// Compute mel-spectrogram
|
|
612
|
+
LogUtils.d(CLASS_NAME, "Computing mel-spectrogram...")
|
|
613
|
+
val spectrogramData = audioProcessor.extractMelSpectrogram(
|
|
614
|
+
audioData = audioData,
|
|
615
|
+
windowSizeMs = windowSizeMs.toFloat(),
|
|
616
|
+
hopLengthMs = hopLengthMs.toFloat(),
|
|
617
|
+
nMels = nMels,
|
|
618
|
+
fMin = fMin.toFloat(),
|
|
619
|
+
fMax = fMax?.toFloat() ?: (audioData.sampleRate.toFloat() / 2),
|
|
620
|
+
normalize = normalize,
|
|
621
|
+
logScaling = logScale,
|
|
622
|
+
windowType = windowType
|
|
623
|
+
)
|
|
624
|
+
|
|
625
|
+
LogUtils.d(CLASS_NAME, "Mel-spectrogram computed successfully with ${spectrogramData.spectrogram.size} time steps")
|
|
626
|
+
|
|
627
|
+
// Convert to map for React Native
|
|
628
|
+
val result = mapOf(
|
|
629
|
+
"spectrogram" to spectrogramData.spectrogram.map { it.toList() },
|
|
630
|
+
"sampleRate" to audioData.sampleRate,
|
|
631
|
+
"nMels" to nMels,
|
|
632
|
+
"timeSteps" to spectrogramData.spectrogram.size,
|
|
633
|
+
"durationMs" to audioData.durationMs
|
|
634
|
+
)
|
|
635
|
+
|
|
636
|
+
LogUtils.d(CLASS_NAME, "Returning result with ${result["timeSteps"]} time steps and $nMels mel bands")
|
|
637
|
+
promise.resolve(result)
|
|
638
|
+
} catch (e: Exception) {
|
|
639
|
+
LogUtils.e(CLASS_NAME, "Failed to extract mel-spectrogram: ${e.message}")
|
|
640
|
+
LogUtils.e(CLASS_NAME, "Stack trace: ${e.stackTraceToString()}")
|
|
641
|
+
promise.reject("SPECTROGRAM_ERROR", e.message ?: "Unknown error", e)
|
|
642
|
+
}
|
|
643
|
+
}
|
|
644
|
+
|
|
645
|
+
OnDestroy {
|
|
646
|
+
AudioRecorderManager.destroy()
|
|
647
|
+
}
|
|
648
|
+
|
|
649
|
+
// Add a new function to check if recording is actually running
|
|
650
|
+
AsyncFunction("checkRecordingStatus") { promise: Promise ->
|
|
651
|
+
val isServiceRunning = AudioRecordingService.isServiceRunning()
|
|
652
|
+
|
|
653
|
+
val status = audioRecorderManager.getStatus()
|
|
654
|
+
|
|
655
|
+
// If service is running but isRecording is false, we need to cleanup
|
|
656
|
+
if (isServiceRunning && !status.getBoolean("isRecording")) {
|
|
657
|
+
audioRecorderManager.cleanup()
|
|
658
|
+
AudioRecordingService.stopService(appContext.reactContext!!)
|
|
659
|
+
}
|
|
660
|
+
|
|
661
|
+
promise.resolve(status)
|
|
662
|
+
}
|
|
663
|
+
|
|
664
|
+
|
|
665
|
+
AsyncFunction("extractAudioAnalysis") { options: Map<String, Any>, promise: Promise ->
|
|
666
|
+
try {
|
|
667
|
+
val fileUri = requireNotNull(options["fileUri"] as? String) { "fileUri is required" }
|
|
668
|
+
|
|
669
|
+
// Get time or byte range options
|
|
670
|
+
val startTimeMs = options["startTimeMs"] as? Number
|
|
671
|
+
val endTimeMs = options["endTimeMs"] as? Number
|
|
672
|
+
val position = options["position"] as? Number
|
|
673
|
+
val length = options["length"] as? Number
|
|
674
|
+
val segmentDurationMs = (options["segmentDurationMs"] as? Number)?.toInt() ?: 100
|
|
675
|
+
|
|
676
|
+
// Validate ranges - can have time range OR byte range OR no range
|
|
677
|
+
val hasTimeRange = startTimeMs != null && endTimeMs != null
|
|
678
|
+
val hasByteRange = position != null && length != null
|
|
679
|
+
|
|
680
|
+
// Only throw if both ranges are provided
|
|
681
|
+
if (hasTimeRange && hasByteRange) {
|
|
682
|
+
throw IllegalArgumentException("Cannot specify both time range and byte range")
|
|
683
|
+
}
|
|
684
|
+
|
|
685
|
+
// Get decoding options with default configuration
|
|
686
|
+
val defaultConfig = DecodingConfig(
|
|
687
|
+
targetSampleRate = null,
|
|
688
|
+
targetChannels = 1, // Default to mono
|
|
689
|
+
targetBitDepth = 16,
|
|
690
|
+
normalizeAudio = false
|
|
691
|
+
)
|
|
692
|
+
|
|
693
|
+
val config = (options["decodingOptions"] as? Map<String, Any>)?.let { decodingOptionsMap ->
|
|
694
|
+
DecodingConfig(
|
|
695
|
+
targetSampleRate = decodingOptionsMap["targetSampleRate"] as? Int,
|
|
696
|
+
targetChannels = decodingOptionsMap["targetChannels"] as? Int,
|
|
697
|
+
targetBitDepth = (decodingOptionsMap["targetBitDepth"] as? Int) ?: 16,
|
|
698
|
+
normalizeAudio = (decodingOptionsMap["normalizeAudio"] as? Boolean) ?: false
|
|
699
|
+
)
|
|
700
|
+
} ?: defaultConfig
|
|
701
|
+
|
|
702
|
+
// Load audio data based on range type (or full file if no range specified)
|
|
703
|
+
val audioData = when {
|
|
704
|
+
hasByteRange -> {
|
|
705
|
+
val format = audioProcessor.getAudioFormat(fileUri)
|
|
706
|
+
?: throw IllegalArgumentException("Could not determine audio format")
|
|
707
|
+
|
|
708
|
+
// Calculate time range from byte position
|
|
709
|
+
val bytesPerSecond = format.sampleRate * format.channels * (format.bitDepth / 8)
|
|
710
|
+
val effectiveStartTimeMs = (position!!.toLong() * 1000) / bytesPerSecond
|
|
711
|
+
val effectiveEndTimeMs = effectiveStartTimeMs + (length!!.toLong() * 1000) / bytesPerSecond
|
|
712
|
+
|
|
713
|
+
LogUtils.d(CLASS_NAME, "Loading audio with byte range: position=$position, length=$length")
|
|
714
|
+
|
|
715
|
+
audioProcessor.loadAudioRange(
|
|
716
|
+
fileUri = fileUri,
|
|
717
|
+
startTimeMs = effectiveStartTimeMs,
|
|
718
|
+
endTimeMs = effectiveEndTimeMs,
|
|
719
|
+
config = config
|
|
720
|
+
)
|
|
721
|
+
}
|
|
722
|
+
hasTimeRange -> {
|
|
723
|
+
LogUtils.d(CLASS_NAME, "Loading audio with time range: startTimeMs=$startTimeMs, endTimeMs=$endTimeMs")
|
|
724
|
+
|
|
725
|
+
audioProcessor.loadAudioRange(
|
|
726
|
+
fileUri = fileUri,
|
|
727
|
+
startTimeMs = startTimeMs!!.toLong(),
|
|
728
|
+
endTimeMs = endTimeMs!!.toLong(),
|
|
729
|
+
config = config
|
|
730
|
+
)
|
|
731
|
+
}
|
|
732
|
+
else -> {
|
|
733
|
+
LogUtils.d(CLASS_NAME, "Loading entire audio file")
|
|
734
|
+
audioProcessor.loadAudioFromAnyFormat(fileUri, config)
|
|
735
|
+
}
|
|
736
|
+
} ?: throw IllegalStateException("Failed to load audio data")
|
|
737
|
+
|
|
738
|
+
val featuresMap = options["features"] as? Map<*, *>
|
|
739
|
+
val features = Features.parseFeatureOptions(featuresMap)
|
|
740
|
+
|
|
741
|
+
val recordingConfig = RecordingConfig(
|
|
742
|
+
sampleRate = audioData.sampleRate,
|
|
743
|
+
channels = audioData.channels,
|
|
744
|
+
encoding = when (audioData.bitDepth) {
|
|
745
|
+
8 -> "pcm_8bit"
|
|
746
|
+
16 -> "pcm_16bit"
|
|
747
|
+
32 -> "pcm_32bit"
|
|
748
|
+
else -> throw IllegalArgumentException("Unsupported bit depth: ${audioData.bitDepth}")
|
|
749
|
+
},
|
|
750
|
+
segmentDurationMs = segmentDurationMs,
|
|
751
|
+
features = features
|
|
752
|
+
)
|
|
753
|
+
|
|
754
|
+
LogUtils.d(CLASS_NAME, "extractAudioAnalysis: $recordingConfig")
|
|
755
|
+
audioProcessor.resetCumulativeAmplitudeRange()
|
|
756
|
+
|
|
757
|
+
val analysisData = audioProcessor.processAudioData(audioData.data, recordingConfig)
|
|
758
|
+
promise.resolve(analysisData.toDictionary())
|
|
759
|
+
} catch (e: Exception) {
|
|
760
|
+
LogUtils.e(CLASS_NAME, "Failed to extract audio analysis: ${e.message}", e)
|
|
761
|
+
promise.reject("PROCESSING_ERROR", e.message ?: "Unknown error", e)
|
|
762
|
+
}
|
|
763
|
+
}
|
|
764
|
+
|
|
765
|
+
AsyncFunction("extractAudioData") { options: Map<String, Any>, promise: Promise ->
|
|
766
|
+
try {
|
|
767
|
+
val fileUri = requireNotNull(options["fileUri"] as? String) { "fileUri is required" }
|
|
768
|
+
val startTimeMs = options["startTimeMs"] as? Number
|
|
769
|
+
val endTimeMs = options["endTimeMs"] as? Number
|
|
770
|
+
val position = options["position"] as? Number
|
|
771
|
+
val length = options["length"] as? Number
|
|
772
|
+
|
|
773
|
+
// Validate that we have either time range or byte range, but not both and not neither
|
|
774
|
+
val hasTimeRange = startTimeMs != null && endTimeMs != null
|
|
775
|
+
val hasByteRange = position != null && length != null
|
|
776
|
+
|
|
777
|
+
if (!hasTimeRange && !hasByteRange) {
|
|
778
|
+
throw IllegalArgumentException("Must specify either time range (startTimeMs, endTimeMs) or byte range (position, length)")
|
|
779
|
+
}
|
|
780
|
+
if (hasTimeRange && hasByteRange) {
|
|
781
|
+
throw IllegalArgumentException("Cannot specify both time range and byte range")
|
|
782
|
+
}
|
|
783
|
+
|
|
784
|
+
// Get decoding options
|
|
785
|
+
val decodingOptionsMap = options["decodingOptions"] as? Map<String, Any>
|
|
786
|
+
val decodingConfig = if (decodingOptionsMap != null) {
|
|
787
|
+
DecodingConfig(
|
|
788
|
+
targetSampleRate = decodingOptionsMap["targetSampleRate"] as? Int,
|
|
789
|
+
targetChannels = decodingOptionsMap["targetChannels"] as? Int,
|
|
790
|
+
targetBitDepth = (decodingOptionsMap["targetBitDepth"] as? Int) ?: 16,
|
|
791
|
+
normalizeAudio = (decodingOptionsMap["normalizeAudio"] as? Boolean) ?: false
|
|
792
|
+
).also {
|
|
793
|
+
LogUtils.d(CLASS_NAME, """
|
|
794
|
+
Using decoding config:
|
|
795
|
+
- targetSampleRate: ${it.targetSampleRate ?: "original"}
|
|
796
|
+
- targetChannels: ${it.targetChannels ?: "original"}
|
|
797
|
+
- targetBitDepth: ${it.targetBitDepth}
|
|
798
|
+
- normalizeAudio: ${it.normalizeAudio}
|
|
799
|
+
""".trimIndent())
|
|
800
|
+
}
|
|
801
|
+
} else null
|
|
802
|
+
|
|
803
|
+
val audioData = if (hasByteRange) {
|
|
804
|
+
val format = audioProcessor.getAudioFormat(fileUri)
|
|
805
|
+
?: throw IllegalArgumentException("Could not determine audio format")
|
|
806
|
+
|
|
807
|
+
// Calculate time range from byte position
|
|
808
|
+
val bytesPerSecond = format.sampleRate * format.channels * (format.bitDepth / 8)
|
|
809
|
+
val effectiveStartTimeMs = (position!!.toLong() * 1000) / bytesPerSecond
|
|
810
|
+
val effectiveEndTimeMs = effectiveStartTimeMs + (length!!.toLong() * 1000) / bytesPerSecond
|
|
811
|
+
|
|
812
|
+
LogUtils.d(CLASS_NAME, """
|
|
813
|
+
Converting byte range to time range:
|
|
814
|
+
- position: $position bytes
|
|
815
|
+
- length: $length bytes
|
|
816
|
+
- bytesPerSecond: $bytesPerSecond
|
|
817
|
+
- effectiveStartTimeMs: $effectiveStartTimeMs
|
|
818
|
+
- effectiveEndTimeMs: $effectiveEndTimeMs
|
|
819
|
+
""".trimIndent())
|
|
820
|
+
|
|
821
|
+
audioProcessor.loadAudioRange(
|
|
822
|
+
fileUri = fileUri,
|
|
823
|
+
startTimeMs = effectiveStartTimeMs,
|
|
824
|
+
endTimeMs = effectiveEndTimeMs,
|
|
825
|
+
config = decodingConfig
|
|
826
|
+
)
|
|
827
|
+
} else {
|
|
828
|
+
// Must be time range due to earlier validation
|
|
829
|
+
LogUtils.d(CLASS_NAME, """
|
|
830
|
+
Using time range:
|
|
831
|
+
- startTimeMs: $startTimeMs
|
|
832
|
+
- endTimeMs: $endTimeMs
|
|
833
|
+
""".trimIndent())
|
|
834
|
+
|
|
835
|
+
audioProcessor.loadAudioRange(
|
|
836
|
+
fileUri = fileUri,
|
|
837
|
+
startTimeMs = startTimeMs!!.toLong(),
|
|
838
|
+
endTimeMs = endTimeMs!!.toLong(),
|
|
839
|
+
config = decodingConfig
|
|
840
|
+
)
|
|
841
|
+
} ?: throw IllegalStateException("Failed to load audio data")
|
|
842
|
+
|
|
843
|
+
LogUtils.d(CLASS_NAME, """
|
|
844
|
+
Audio data loaded successfully:
|
|
845
|
+
- data size: ${audioData.data.size} bytes
|
|
846
|
+
- sampleRate: ${audioData.sampleRate}
|
|
847
|
+
- channels: ${audioData.channels}
|
|
848
|
+
- bitDepth: ${audioData.bitDepth}
|
|
849
|
+
- durationMs: ${audioData.durationMs}
|
|
850
|
+
""".trimIndent())
|
|
851
|
+
|
|
852
|
+
val includeNormalizedData = options["includeNormalizedData"] as? Boolean ?: false
|
|
853
|
+
val includeBase64Data = options["includeBase64Data"] as? Boolean ?: false
|
|
854
|
+
val includeWavHeader = options["includeWavHeader"] as? Boolean ?: false
|
|
855
|
+
val bytesPerSample = audioData.bitDepth / 8
|
|
856
|
+
val samples = audioData.data.size / (bytesPerSample * audioData.channels)
|
|
857
|
+
|
|
858
|
+
// Create the result map
|
|
859
|
+
val resultMap = mutableMapOf<String, Any>()
|
|
860
|
+
|
|
861
|
+
// Add WAV header if requested
|
|
862
|
+
if (includeWavHeader) {
|
|
863
|
+
// Use ByteArrayOutputStream to write the WAV header and data
|
|
864
|
+
val outputStream = java.io.ByteArrayOutputStream()
|
|
865
|
+
val audioFileHandler = AudioFileHandler(appContext.reactContext!!.filesDir)
|
|
866
|
+
|
|
867
|
+
// Write the WAV header
|
|
868
|
+
audioFileHandler.writeWavHeader(
|
|
869
|
+
outputStream,
|
|
870
|
+
audioData.sampleRate,
|
|
871
|
+
audioData.channels,
|
|
872
|
+
audioData.bitDepth
|
|
873
|
+
)
|
|
874
|
+
|
|
875
|
+
// Write the PCM data
|
|
876
|
+
outputStream.write(audioData.data)
|
|
877
|
+
|
|
878
|
+
// Get the complete WAV data
|
|
879
|
+
val wavData = outputStream.toByteArray()
|
|
880
|
+
|
|
881
|
+
resultMap["pcmData"] = wavData
|
|
882
|
+
resultMap["hasWavHeader"] = true
|
|
883
|
+
|
|
884
|
+
LogUtils.d(CLASS_NAME, "Added WAV header to PCM data, total size: ${wavData.size} bytes")
|
|
885
|
+
} else {
|
|
886
|
+
resultMap["pcmData"] = audioData.data
|
|
887
|
+
resultMap["hasWavHeader"] = false
|
|
888
|
+
}
|
|
889
|
+
|
|
890
|
+
// Add the rest of the data
|
|
891
|
+
resultMap.putAll(mapOf(
|
|
892
|
+
"sampleRate" to audioData.sampleRate,
|
|
893
|
+
"channels" to audioData.channels,
|
|
894
|
+
"bitDepth" to audioData.bitDepth,
|
|
895
|
+
"durationMs" to audioData.durationMs,
|
|
896
|
+
"format" to "pcm_${audioData.bitDepth}bit",
|
|
897
|
+
"samples" to samples
|
|
898
|
+
))
|
|
899
|
+
|
|
900
|
+
// Add checksum if requested
|
|
901
|
+
if (options["computeChecksum"] == true) {
|
|
902
|
+
val crc32 = CRC32()
|
|
903
|
+
crc32.update(audioData.data)
|
|
904
|
+
resultMap["checksum"] = crc32.value.toInt()
|
|
905
|
+
|
|
906
|
+
LogUtils.d(CLASS_NAME, "Computed CRC32 checksum: ${crc32.value}")
|
|
907
|
+
}
|
|
908
|
+
|
|
909
|
+
if (includeNormalizedData) {
|
|
910
|
+
val float32Data = AudioFormatUtils.convertByteArrayToFloatArray(
|
|
911
|
+
audioData.data,
|
|
912
|
+
"pcm_${audioData.bitDepth}bit"
|
|
913
|
+
)
|
|
914
|
+
resultMap["normalizedData"] = float32Data
|
|
915
|
+
}
|
|
916
|
+
|
|
917
|
+
if (includeBase64Data) {
|
|
918
|
+
// Convert the PCM data to a base64 string
|
|
919
|
+
val base64Data = android.util.Base64.encodeToString(
|
|
920
|
+
audioData.data,
|
|
921
|
+
android.util.Base64.NO_WRAP
|
|
922
|
+
)
|
|
923
|
+
resultMap["base64Data"] = base64Data
|
|
924
|
+
}
|
|
925
|
+
|
|
926
|
+
promise.resolve(resultMap)
|
|
927
|
+
} catch (e: Exception) {
|
|
928
|
+
LogUtils.e(CLASS_NAME, "Failed to extract audio data: ${e.message}")
|
|
929
|
+
LogUtils.e(CLASS_NAME, "Stack trace: ${e.stackTraceToString()}")
|
|
930
|
+
promise.reject("PROCESSING_ERROR", e.message ?: "Unknown error", e)
|
|
931
|
+
}
|
|
932
|
+
}
|
|
933
|
+
}
|
|
934
|
+
|
|
935
|
+
private fun initializeManager() {
|
|
936
|
+
val context = appContext.reactContext ?: throw IllegalStateException("React context not available")
|
|
937
|
+
val filesDir = context.filesDir
|
|
938
|
+
val permissionUtils = PermissionUtils(context)
|
|
939
|
+
val audioDataEncoder = AudioDataEncoder()
|
|
940
|
+
|
|
941
|
+
// Initialize AudioDeviceManager
|
|
942
|
+
LogUtils.d(CLASS_NAME, "🔧 Initializing AudioDeviceManager...")
|
|
943
|
+
LogUtils.d(CLASS_NAME, "🔧 Device detection enabled: $enableDeviceDetection")
|
|
944
|
+
audioDeviceManager = AudioDeviceManager(context, enableDeviceDetection)
|
|
945
|
+
LogUtils.d(CLASS_NAME, "🔧 AudioDeviceManager initialized")
|
|
946
|
+
|
|
947
|
+
// Initialize AudioRecorderManager with AudioDeviceManager integration
|
|
948
|
+
audioRecorderManager = AudioRecorderManager.initialize(
|
|
949
|
+
context,
|
|
950
|
+
filesDir,
|
|
951
|
+
permissionUtils,
|
|
952
|
+
audioDataEncoder,
|
|
953
|
+
this,
|
|
954
|
+
enablePhoneStateHandling,
|
|
955
|
+
enableBackgroundAudio
|
|
956
|
+
)
|
|
957
|
+
|
|
958
|
+
// Set up the delegate for the AudioDeviceManager
|
|
959
|
+
audioDeviceManager.delegate = object : AudioDeviceManagerDelegate {
|
|
960
|
+
override fun onDeviceDisconnected(deviceId: String) {
|
|
961
|
+
LogUtils.d(CLASS_NAME, "📱 Device disconnected: $deviceId")
|
|
962
|
+
// Handle device disconnection
|
|
963
|
+
coroutineScope.launch {
|
|
964
|
+
try {
|
|
965
|
+
// If recording is active, handle the disconnection based on the recording config
|
|
966
|
+
if (audioRecorderManager.isRecording) {
|
|
967
|
+
handleDeviceDisconnection(deviceId)
|
|
968
|
+
}
|
|
969
|
+
|
|
970
|
+
// Notify JS about the disconnection
|
|
971
|
+
sendEvent(Constants.DEVICE_CHANGED_EVENT, bundleOf(
|
|
972
|
+
"type" to "deviceDisconnected",
|
|
973
|
+
"deviceId" to deviceId
|
|
974
|
+
))
|
|
975
|
+
} catch (e: Exception) {
|
|
976
|
+
LogUtils.e(CLASS_NAME, "📱 Error handling device disconnection: ${e.message}", e)
|
|
977
|
+
}
|
|
978
|
+
}
|
|
979
|
+
}
|
|
980
|
+
}
|
|
981
|
+
|
|
982
|
+
// Set up connection callback
|
|
983
|
+
audioDeviceManager.onDeviceConnected = { deviceId ->
|
|
984
|
+
LogUtils.d(CLASS_NAME, "📱 Device connected: $deviceId")
|
|
985
|
+
// Notify JS about the connection
|
|
986
|
+
sendEvent(Constants.DEVICE_CHANGED_EVENT, bundleOf(
|
|
987
|
+
"type" to "deviceConnected",
|
|
988
|
+
"deviceId" to deviceId
|
|
989
|
+
))
|
|
990
|
+
}
|
|
991
|
+
|
|
992
|
+
// Set up disconnection callback
|
|
993
|
+
audioDeviceManager.onDeviceDisconnected = { deviceId ->
|
|
994
|
+
LogUtils.d(CLASS_NAME, "📱 Device disconnected: $deviceId")
|
|
995
|
+
// Notify JS about the disconnection
|
|
996
|
+
sendEvent(Constants.DEVICE_CHANGED_EVENT, bundleOf(
|
|
997
|
+
"type" to "deviceDisconnected",
|
|
998
|
+
"deviceId" to deviceId
|
|
999
|
+
))
|
|
1000
|
+
}
|
|
1001
|
+
|
|
1002
|
+
audioProcessor = AudioProcessor(filesDir)
|
|
1003
|
+
}
|
|
1004
|
+
|
|
1005
|
+
/**
|
|
1006
|
+
* Handles audio device disconnection based on the recording configuration
|
|
1007
|
+
*/
|
|
1008
|
+
private suspend fun handleDeviceDisconnection(deviceId: String) {
|
|
1009
|
+
LogUtils.d(CLASS_NAME, "📱 handleDeviceDisconnection called for device: $deviceId")
|
|
1010
|
+
// Get disconnection behavior from recorder config
|
|
1011
|
+
val behavior = audioRecorderManager.getDeviceDisconnectionBehavior()
|
|
1012
|
+
LogUtils.d(CLASS_NAME, "📱 Device disconnection behavior configured as: $behavior")
|
|
1013
|
+
|
|
1014
|
+
when (behavior) {
|
|
1015
|
+
"fallback" -> {
|
|
1016
|
+
LogUtils.d(CLASS_NAME, "📱 Using fallback behavior, getting default device")
|
|
1017
|
+
// Get default device
|
|
1018
|
+
val defaultDevice = withContext(Dispatchers.IO) {
|
|
1019
|
+
audioDeviceManager.getDefaultInputDevice()
|
|
1020
|
+
}
|
|
1021
|
+
|
|
1022
|
+
if (defaultDevice != null) {
|
|
1023
|
+
LogUtils.d(CLASS_NAME, "📱 Falling back to default device: ${defaultDevice["name"]}")
|
|
1024
|
+
|
|
1025
|
+
// Select default device
|
|
1026
|
+
val deviceId = defaultDevice["id"] as String
|
|
1027
|
+
LogUtils.d(CLASS_NAME, "📱 Attempting to select default device: $deviceId")
|
|
1028
|
+
val success = audioDeviceManager.selectDevice(deviceId)
|
|
1029
|
+
|
|
1030
|
+
if (success) {
|
|
1031
|
+
LogUtils.d(CLASS_NAME, "📱 Successfully selected default device, notifying AudioRecorderManager")
|
|
1032
|
+
// Notify AudioRecorderManager to update its recording source
|
|
1033
|
+
audioRecorderManager.handleDeviceChange()
|
|
1034
|
+
|
|
1035
|
+
// Notify JS about fallback
|
|
1036
|
+
LogUtils.d(CLASS_NAME, "📱 Sending deviceFallback event to JS")
|
|
1037
|
+
sendEvent(Constants.RECORDING_INTERRUPTED_EVENT_NAME, bundleOf(
|
|
1038
|
+
"reason" to "deviceFallback",
|
|
1039
|
+
"isPaused" to false,
|
|
1040
|
+
"deviceId" to deviceId
|
|
1041
|
+
))
|
|
1042
|
+
} else {
|
|
1043
|
+
LogUtils.e(CLASS_NAME, "📱 Failed to select default device, pausing recording")
|
|
1044
|
+
|
|
1045
|
+
// Fall back to pause if we can't select the default device
|
|
1046
|
+
audioRecorderManager.pauseRecording(object : Promise {
|
|
1047
|
+
override fun resolve(value: Any?) {
|
|
1048
|
+
LogUtils.d(CLASS_NAME, "📱 Recording successfully paused, notifying AudioRecorderManager")
|
|
1049
|
+
// Notify AudioRecorderManager to handle device change while paused
|
|
1050
|
+
audioRecorderManager.handleDeviceChange()
|
|
1051
|
+
|
|
1052
|
+
sendEvent(Constants.RECORDING_INTERRUPTED_EVENT_NAME, bundleOf(
|
|
1053
|
+
"reason" to "deviceSwitchFailed",
|
|
1054
|
+
"isPaused" to true
|
|
1055
|
+
))
|
|
1056
|
+
}
|
|
1057
|
+
override fun reject(code: String, message: String?, cause: Throwable?) {
|
|
1058
|
+
LogUtils.e(CLASS_NAME, "📱 Failed to pause recording after device disconnection: $message")
|
|
1059
|
+
}
|
|
1060
|
+
})
|
|
1061
|
+
}
|
|
1062
|
+
} else {
|
|
1063
|
+
LogUtils.e(CLASS_NAME, "📱 No default device found, pausing recording")
|
|
1064
|
+
|
|
1065
|
+
// Fall back to pause if we can't find a default device
|
|
1066
|
+
audioRecorderManager.pauseRecording(object : Promise {
|
|
1067
|
+
override fun resolve(value: Any?) {
|
|
1068
|
+
LogUtils.d(CLASS_NAME, "📱 Recording successfully paused when no default device found")
|
|
1069
|
+
// Notify AudioRecorderManager to handle device change while paused
|
|
1070
|
+
audioRecorderManager.handleDeviceChange()
|
|
1071
|
+
|
|
1072
|
+
sendEvent(Constants.RECORDING_INTERRUPTED_EVENT_NAME, bundleOf(
|
|
1073
|
+
"reason" to "deviceDisconnected",
|
|
1074
|
+
"isPaused" to true
|
|
1075
|
+
))
|
|
1076
|
+
}
|
|
1077
|
+
override fun reject(code: String, message: String?, cause: Throwable?) {
|
|
1078
|
+
LogUtils.e(CLASS_NAME, "📱 Failed to pause recording after device disconnection: $message")
|
|
1079
|
+
}
|
|
1080
|
+
})
|
|
1081
|
+
}
|
|
1082
|
+
}
|
|
1083
|
+
|
|
1084
|
+
else -> { // Default to pause behavior
|
|
1085
|
+
LogUtils.d(CLASS_NAME, "📱 Using pause behavior for device disconnection")
|
|
1086
|
+
|
|
1087
|
+
// Pause recording
|
|
1088
|
+
audioRecorderManager.pauseRecording(object : Promise {
|
|
1089
|
+
override fun resolve(value: Any?) {
|
|
1090
|
+
LogUtils.d(CLASS_NAME, "📱 Recording successfully paused after device disconnection")
|
|
1091
|
+
// Notify AudioRecorderManager to handle device change while paused
|
|
1092
|
+
audioRecorderManager.handleDeviceChange()
|
|
1093
|
+
|
|
1094
|
+
sendEvent(Constants.RECORDING_INTERRUPTED_EVENT_NAME, bundleOf(
|
|
1095
|
+
"reason" to "deviceDisconnected",
|
|
1096
|
+
"isPaused" to true
|
|
1097
|
+
))
|
|
1098
|
+
}
|
|
1099
|
+
override fun reject(code: String, message: String?, cause: Throwable?) {
|
|
1100
|
+
LogUtils.e(CLASS_NAME, "📱 Failed to pause recording after device disconnection: $message")
|
|
1101
|
+
}
|
|
1102
|
+
})
|
|
1103
|
+
}
|
|
1104
|
+
}
|
|
1105
|
+
LogUtils.d(CLASS_NAME, "📱 handleDeviceDisconnection completed")
|
|
1106
|
+
}
|
|
1107
|
+
|
|
1108
|
+
override fun sendExpoEvent(eventName: String, params: Bundle) {
|
|
1109
|
+
LogUtils.d(CLASS_NAME, "Sending event: $eventName")
|
|
1110
|
+
this@AudioStudioModule.sendEvent(eventName, params)
|
|
1111
|
+
}
|
|
1112
|
+
}
|