@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,1099 @@
|
|
|
1
|
+
package net.siteed.audiostudio
|
|
2
|
+
|
|
3
|
+
import android.content.Context
|
|
4
|
+
import android.media.MediaExtractor
|
|
5
|
+
import android.media.MediaFormat
|
|
6
|
+
import android.media.MediaMetadataRetriever
|
|
7
|
+
import android.media.MediaMuxer
|
|
8
|
+
import android.net.Uri
|
|
9
|
+
import android.util.Log
|
|
10
|
+
import java.io.File
|
|
11
|
+
import java.io.FileInputStream
|
|
12
|
+
import java.io.FileOutputStream
|
|
13
|
+
import java.io.IOException
|
|
14
|
+
import java.nio.ByteBuffer
|
|
15
|
+
import kotlin.math.min
|
|
16
|
+
|
|
17
|
+
class AudioTrimmer(
|
|
18
|
+
private val context: Context,
|
|
19
|
+
private val fileHandler: AudioFileHandler
|
|
20
|
+
) {
|
|
21
|
+
companion object {
|
|
22
|
+
private const val TAG = "AudioTrimmer"
|
|
23
|
+
private const val BUFFER_SIZE = 8 * 1024 * 1024 // Increased from 1MB to 8MB for better I/O performance
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
interface ProgressListener {
|
|
27
|
+
fun onProgress(progress: Float, bytesProcessed: Long, totalBytes: Long)
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
/**
|
|
31
|
+
* Trims audio file based on the provided options
|
|
32
|
+
*/
|
|
33
|
+
fun trimAudio(
|
|
34
|
+
fileUri: String,
|
|
35
|
+
mode: String = "single",
|
|
36
|
+
startTimeMs: Long? = null,
|
|
37
|
+
endTimeMs: Long? = null,
|
|
38
|
+
ranges: List<Map<String, Long>>? = null,
|
|
39
|
+
outputFileName: String? = null,
|
|
40
|
+
outputFormat: Map<String, Any>? = null,
|
|
41
|
+
progressListener: ProgressListener? = null
|
|
42
|
+
): Map<String, Any> {
|
|
43
|
+
val startTime = System.currentTimeMillis()
|
|
44
|
+
Log.d(TAG, "Starting audio trim operation: mode=$mode, fileUri=$fileUri")
|
|
45
|
+
Log.d(TAG, "Parameters: startTimeMs=$startTimeMs, endTimeMs=$endTimeMs, ranges=$ranges")
|
|
46
|
+
Log.d(TAG, "Output format options: $outputFormat")
|
|
47
|
+
|
|
48
|
+
try {
|
|
49
|
+
// Resolve the input file URI
|
|
50
|
+
val inputUri = Uri.parse(fileUri)
|
|
51
|
+
|
|
52
|
+
// Get audio file metadata
|
|
53
|
+
val retriever = MediaMetadataRetriever()
|
|
54
|
+
retriever.setDataSource(context, inputUri)
|
|
55
|
+
|
|
56
|
+
// Extract audio format information
|
|
57
|
+
val audioFormat = getAudioFormat(retriever)
|
|
58
|
+
Log.d(TAG, "Source audio format: $audioFormat")
|
|
59
|
+
|
|
60
|
+
// Validate and process output format options
|
|
61
|
+
val formatOptions = outputFormat ?: emptyMap()
|
|
62
|
+
val outputFormatType = (formatOptions["format"] as? String)?.lowercase() ?: "wav"
|
|
63
|
+
|
|
64
|
+
// Validate format and provide consistent fallback
|
|
65
|
+
val effectiveFormatType = if (outputFormatType !in listOf("wav", "aac", "opus")) {
|
|
66
|
+
Log.w(TAG, "Unsupported format '$outputFormatType'. Falling back to 'aac'")
|
|
67
|
+
"aac"
|
|
68
|
+
} else {
|
|
69
|
+
outputFormatType
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
// Validate and normalize format-specific parameters
|
|
73
|
+
val sampleRate = (formatOptions["sampleRate"] as? Int)?.coerceIn(8000, 48000)
|
|
74
|
+
?: audioFormat.sampleRate
|
|
75
|
+
val channels = (formatOptions["channels"] as? Int)?.coerceIn(1, 2)
|
|
76
|
+
?: audioFormat.channels
|
|
77
|
+
val bitDepth = (formatOptions["bitDepth"] as? Int)?.coerceIn(8, 32)
|
|
78
|
+
?: audioFormat.bitDepth
|
|
79
|
+
val bitrate = (formatOptions["bitrate"] as? Int)?.coerceIn(8000, 320000)
|
|
80
|
+
?: 128000
|
|
81
|
+
|
|
82
|
+
Log.d(TAG, "Output format parameters: format=$effectiveFormatType, sampleRate=$sampleRate, " +
|
|
83
|
+
"channels=$channels, bitDepth=$bitDepth, bitrate=$bitrate")
|
|
84
|
+
|
|
85
|
+
// Determine the appropriate extension and format
|
|
86
|
+
val extension = when (effectiveFormatType) {
|
|
87
|
+
"wav" -> "wav"
|
|
88
|
+
"opus" -> "opus"
|
|
89
|
+
else -> "m4a" // Use m4a extension for AAC to match iOS
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
Log.d(TAG, "Using output extension: $extension")
|
|
93
|
+
|
|
94
|
+
// Create output file
|
|
95
|
+
val outputFile = if (outputFileName != null) {
|
|
96
|
+
File(context.filesDir, "$outputFileName.$extension")
|
|
97
|
+
} else {
|
|
98
|
+
fileHandler.createAudioFile(extension)
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
Log.d(TAG, "Created output file: ${outputFile.absolutePath}")
|
|
102
|
+
|
|
103
|
+
// Determine the time ranges to process based on the mode
|
|
104
|
+
val timeRanges = when (mode) {
|
|
105
|
+
"single" -> {
|
|
106
|
+
val start = startTimeMs ?: 0
|
|
107
|
+
val end = endTimeMs ?: audioFormat.durationMs
|
|
108
|
+
listOf(mapOf("startTimeMs" to start, "endTimeMs" to end))
|
|
109
|
+
}
|
|
110
|
+
"keep" -> ranges ?: emptyList()
|
|
111
|
+
"remove" -> {
|
|
112
|
+
// For remove mode, we need to invert the ranges
|
|
113
|
+
val invertedRanges = mutableListOf<Map<String, Long>>()
|
|
114
|
+
var lastEndTime = 0L
|
|
115
|
+
|
|
116
|
+
ranges?.sortedBy { it["startTimeMs"] }?.forEach { range ->
|
|
117
|
+
val start = range["startTimeMs"] ?: 0L
|
|
118
|
+
val end = range["endTimeMs"] ?: audioFormat.durationMs
|
|
119
|
+
|
|
120
|
+
if (start > lastEndTime) {
|
|
121
|
+
invertedRanges.add(mapOf("startTimeMs" to lastEndTime, "endTimeMs" to start))
|
|
122
|
+
}
|
|
123
|
+
lastEndTime = end
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
if (lastEndTime < audioFormat.durationMs) {
|
|
127
|
+
invertedRanges.add(mapOf("startTimeMs" to lastEndTime, "endTimeMs" to audioFormat.durationMs))
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
invertedRanges
|
|
131
|
+
}
|
|
132
|
+
else -> throw IllegalArgumentException("Invalid mode: $mode")
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
// Check if we need format conversion
|
|
136
|
+
val needFormatChange = formatOptions["sampleRate"] != null ||
|
|
137
|
+
formatOptions["channels"] != null ||
|
|
138
|
+
formatOptions["bitDepth"] != null
|
|
139
|
+
|
|
140
|
+
// Check if input is WAV format
|
|
141
|
+
val isWavInput = audioFormat.mimeType == "audio/wav" || audioFormat.mimeType == "audio/x-wav"
|
|
142
|
+
|
|
143
|
+
// Optimized approach based on input/output formats
|
|
144
|
+
if (isWavInput && extension == "wav" && !needFormatChange) {
|
|
145
|
+
// Fast path for WAV-to-WAV with no format changes
|
|
146
|
+
Log.d(TAG, "Using fast path: Direct WAV processing without decoding")
|
|
147
|
+
processWavFile(inputUri, outputFile, timeRanges
|
|
148
|
+
) { progress, bytesProcessed, totalBytes ->
|
|
149
|
+
progressListener?.onProgress(progress, bytesProcessed, totalBytes)
|
|
150
|
+
}
|
|
151
|
+
} else {
|
|
152
|
+
// Need to decode and possibly re-encode
|
|
153
|
+
Log.d(TAG, "Using decode/encode path for non-WAV input or format conversion")
|
|
154
|
+
val config = DecodingConfig(
|
|
155
|
+
targetSampleRate = formatOptions["sampleRate"] as? Int,
|
|
156
|
+
targetChannels = formatOptions["channels"] as? Int,
|
|
157
|
+
targetBitDepth = formatOptions["bitDepth"] as? Int ?: 16,
|
|
158
|
+
normalizeAudio = false
|
|
159
|
+
)
|
|
160
|
+
|
|
161
|
+
if (extension == "wav") {
|
|
162
|
+
// For any format to WAV conversion
|
|
163
|
+
Log.d(TAG, "Processing to WAV with possible format conversion")
|
|
164
|
+
processToWav(
|
|
165
|
+
inputUri,
|
|
166
|
+
outputFile,
|
|
167
|
+
timeRanges,
|
|
168
|
+
config,
|
|
169
|
+
progressListener
|
|
170
|
+
)
|
|
171
|
+
} else {
|
|
172
|
+
// For compressed output formats (AAC, Opus)
|
|
173
|
+
Log.d(TAG, "Processing to compressed format: $extension")
|
|
174
|
+
val tempWavFile = File(context.filesDir, "temp_${System.currentTimeMillis()}.wav")
|
|
175
|
+
|
|
176
|
+
try {
|
|
177
|
+
// First decode to WAV
|
|
178
|
+
processToWav(
|
|
179
|
+
inputUri,
|
|
180
|
+
tempWavFile,
|
|
181
|
+
timeRanges,
|
|
182
|
+
config,
|
|
183
|
+
object : ProgressListener {
|
|
184
|
+
override fun onProgress(progress: Float, bytesProcessed: Long, totalBytes: Long) {
|
|
185
|
+
progressListener?.onProgress(progress / 2, bytesProcessed, totalBytes)
|
|
186
|
+
}
|
|
187
|
+
}
|
|
188
|
+
)
|
|
189
|
+
|
|
190
|
+
// Now encode to the target format
|
|
191
|
+
if (extension == "opus") {
|
|
192
|
+
val audioProcessor = AudioProcessor(context.filesDir)
|
|
193
|
+
val audioData = audioProcessor.loadAudioFromAnyFormat(
|
|
194
|
+
tempWavFile.absolutePath,
|
|
195
|
+
DecodingConfig(
|
|
196
|
+
targetSampleRate = formatOptions["sampleRate"] as? Int ?: 16000,
|
|
197
|
+
targetChannels = formatOptions["channels"] as? Int ?: 1,
|
|
198
|
+
targetBitDepth = 16,
|
|
199
|
+
normalizeAudio = false
|
|
200
|
+
)
|
|
201
|
+
) ?: throw IOException("Failed to load WAV file")
|
|
202
|
+
|
|
203
|
+
encodeToOpus(
|
|
204
|
+
audioData,
|
|
205
|
+
outputFile,
|
|
206
|
+
formatOptions,
|
|
207
|
+
object : ProgressListener {
|
|
208
|
+
override fun onProgress(progress: Float, bytesProcessed: Long, totalBytes: Long) {
|
|
209
|
+
progressListener?.onProgress(50 + progress / 2, bytesProcessed, totalBytes)
|
|
210
|
+
}
|
|
211
|
+
}
|
|
212
|
+
)
|
|
213
|
+
} else {
|
|
214
|
+
// AAC encoding
|
|
215
|
+
encodeWavToAac(
|
|
216
|
+
tempWavFile,
|
|
217
|
+
outputFile,
|
|
218
|
+
formatOptions,
|
|
219
|
+
object : ProgressListener {
|
|
220
|
+
override fun onProgress(progress: Float, bytesProcessed: Long, totalBytes: Long) {
|
|
221
|
+
progressListener?.onProgress(50 + progress / 2, bytesProcessed, totalBytes)
|
|
222
|
+
}
|
|
223
|
+
}
|
|
224
|
+
)
|
|
225
|
+
}
|
|
226
|
+
} finally {
|
|
227
|
+
// Clean up temp file
|
|
228
|
+
if (tempWavFile.exists()) {
|
|
229
|
+
tempWavFile.delete()
|
|
230
|
+
}
|
|
231
|
+
}
|
|
232
|
+
}
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
// Get output file metadata
|
|
236
|
+
val outputFileSize = outputFile.length()
|
|
237
|
+
val outputDurationMs = calculateOutputDuration(timeRanges)
|
|
238
|
+
|
|
239
|
+
// Extract audio format details
|
|
240
|
+
val extractor = MediaExtractor()
|
|
241
|
+
try {
|
|
242
|
+
extractor.setDataSource(outputFile.absolutePath)
|
|
243
|
+
|
|
244
|
+
// Initialize variables that will be populated from the file or user options
|
|
245
|
+
val outputBitrate: Int
|
|
246
|
+
|
|
247
|
+
// First try to get values from the output file
|
|
248
|
+
if (extractor.trackCount > 0) {
|
|
249
|
+
val format = extractor.getTrackFormat(0)
|
|
250
|
+
|
|
251
|
+
outputBitrate = if (format.containsKey(MediaFormat.KEY_BIT_RATE)) {
|
|
252
|
+
format.getInteger(MediaFormat.KEY_BIT_RATE)
|
|
253
|
+
} else {
|
|
254
|
+
// Use original bitrate or user-specified value
|
|
255
|
+
bitrate
|
|
256
|
+
}
|
|
257
|
+
} else {
|
|
258
|
+
// If we can't get from the file, use user options or defaults
|
|
259
|
+
outputBitrate = bitrate
|
|
260
|
+
}
|
|
261
|
+
|
|
262
|
+
// Determine the correct MIME type
|
|
263
|
+
val mimeType = when (extension) {
|
|
264
|
+
"m4a" -> "audio/mp4" // Use audio/mp4 for AAC to match iOS
|
|
265
|
+
"opus" -> "audio/ogg" // Use audio/ogg for Opus
|
|
266
|
+
else -> "audio/wav"
|
|
267
|
+
}
|
|
268
|
+
|
|
269
|
+
val result = mutableMapOf<String, Any>(
|
|
270
|
+
"uri" to outputFile.absolutePath,
|
|
271
|
+
"filename" to outputFile.name,
|
|
272
|
+
"durationMs" to outputDurationMs,
|
|
273
|
+
"size" to outputFileSize,
|
|
274
|
+
"sampleRate" to sampleRate,
|
|
275
|
+
"channels" to channels,
|
|
276
|
+
"bitDepth" to bitDepth,
|
|
277
|
+
"mimeType" to mimeType,
|
|
278
|
+
"requestedFormat" to (formatOptions["format"] as? String ?: "wav"), // Add the originally requested format
|
|
279
|
+
"actualFormat" to extension // Add the actual format used
|
|
280
|
+
)
|
|
281
|
+
|
|
282
|
+
// Add compression info if not WAV
|
|
283
|
+
if (extension != "wav") {
|
|
284
|
+
result["compression"] = mapOf(
|
|
285
|
+
"format" to effectiveFormatType,
|
|
286
|
+
"bitrate" to outputBitrate,
|
|
287
|
+
"size" to outputFileSize
|
|
288
|
+
)
|
|
289
|
+
}
|
|
290
|
+
|
|
291
|
+
Log.d(TAG, "Audio trim completed in ${System.currentTimeMillis() - startTime}ms")
|
|
292
|
+
return result
|
|
293
|
+
} catch (e: Exception) {
|
|
294
|
+
Log.e(TAG, "Error reading output file metadata: ${e.message}")
|
|
295
|
+
// Continue with basic metadata if extractor fails
|
|
296
|
+
|
|
297
|
+
// Determine the correct MIME type
|
|
298
|
+
val mimeType = when (extension) {
|
|
299
|
+
"m4a" -> "audio/mp4" // Use audio/mp4 for AAC to match iOS
|
|
300
|
+
"opus" -> "audio/ogg" // Use audio/ogg for Opus
|
|
301
|
+
else -> "audio/wav"
|
|
302
|
+
}
|
|
303
|
+
|
|
304
|
+
val result = mutableMapOf<String, Any>(
|
|
305
|
+
"uri" to outputFile.absolutePath,
|
|
306
|
+
"filename" to outputFile.name,
|
|
307
|
+
"durationMs" to outputDurationMs,
|
|
308
|
+
"size" to outputFileSize,
|
|
309
|
+
"sampleRate" to sampleRate,
|
|
310
|
+
"channels" to channels,
|
|
311
|
+
"bitDepth" to bitDepth,
|
|
312
|
+
"mimeType" to mimeType,
|
|
313
|
+
"requestedFormat" to (formatOptions["format"] as? String ?: "wav"),
|
|
314
|
+
"actualFormat" to extension
|
|
315
|
+
)
|
|
316
|
+
|
|
317
|
+
// Add compression info if not WAV
|
|
318
|
+
if (extension != "wav") {
|
|
319
|
+
result["compression"] = mapOf(
|
|
320
|
+
"format" to effectiveFormatType,
|
|
321
|
+
"bitrate" to bitrate,
|
|
322
|
+
"size" to outputFileSize
|
|
323
|
+
)
|
|
324
|
+
}
|
|
325
|
+
|
|
326
|
+
Log.d(TAG, "Audio trim completed in ${System.currentTimeMillis() - startTime}ms")
|
|
327
|
+
return result
|
|
328
|
+
} finally {
|
|
329
|
+
try {
|
|
330
|
+
extractor.release()
|
|
331
|
+
} catch (e: Exception) {
|
|
332
|
+
// Ignore
|
|
333
|
+
}
|
|
334
|
+
}
|
|
335
|
+
|
|
336
|
+
} catch (e: Exception) {
|
|
337
|
+
Log.e(TAG, "Error trimming audio", e)
|
|
338
|
+
throw e
|
|
339
|
+
}
|
|
340
|
+
}
|
|
341
|
+
|
|
342
|
+
private fun calculateOutputDuration(timeRanges: List<Map<String, Long>>): Long {
|
|
343
|
+
var totalDurationMs = 0L
|
|
344
|
+
for (range in timeRanges) {
|
|
345
|
+
val start = range["startTimeMs"] ?: 0L
|
|
346
|
+
val end = range["endTimeMs"] ?: 0L
|
|
347
|
+
totalDurationMs += (end - start)
|
|
348
|
+
}
|
|
349
|
+
return totalDurationMs
|
|
350
|
+
}
|
|
351
|
+
|
|
352
|
+
/**
|
|
353
|
+
* Optimized version of processWavFile that directly copies bytes from input to output
|
|
354
|
+
* without decoding the entire file
|
|
355
|
+
*/
|
|
356
|
+
private fun processWavFile(
|
|
357
|
+
inputUri: Uri,
|
|
358
|
+
outputFile: File,
|
|
359
|
+
timeRanges: List<Map<String, Long>>,
|
|
360
|
+
progressCallback: (Float, Long, Long) -> Unit
|
|
361
|
+
) {
|
|
362
|
+
// Get input file path from URI
|
|
363
|
+
val inputPath = inputUri.path ?: throw IOException("Invalid input URI")
|
|
364
|
+
val inputFile = File(inputPath)
|
|
365
|
+
|
|
366
|
+
if (!inputFile.exists()) {
|
|
367
|
+
throw IOException("Input file does not exist: $inputPath")
|
|
368
|
+
}
|
|
369
|
+
|
|
370
|
+
// Create output file if it doesn't exist
|
|
371
|
+
if (!outputFile.exists() && !outputFile.createNewFile()) {
|
|
372
|
+
throw IOException("Failed to create output file: ${outputFile.path}")
|
|
373
|
+
}
|
|
374
|
+
|
|
375
|
+
// Use AudioProcessor to determine actual WAV header length
|
|
376
|
+
val audioProcessor = AudioProcessor(context.filesDir)
|
|
377
|
+
val headerSize = audioProcessor.getWavHeaderSize(inputFile.absolutePath) ?: 44 // Default to 44 if we can't determine
|
|
378
|
+
|
|
379
|
+
// Read WAV header to get format information using 'use' pattern
|
|
380
|
+
val headerBuffer = FileInputStream(inputFile).use { inputStream ->
|
|
381
|
+
ByteArray(headerSize).also { buffer ->
|
|
382
|
+
inputStream.read(buffer)
|
|
383
|
+
}
|
|
384
|
+
}
|
|
385
|
+
|
|
386
|
+
// Parse header to get format info
|
|
387
|
+
val sampleRate = ByteBuffer.wrap(headerBuffer, 24, 4).order(java.nio.ByteOrder.LITTLE_ENDIAN).int
|
|
388
|
+
val channels = ByteBuffer.wrap(headerBuffer, 22, 2).order(java.nio.ByteOrder.LITTLE_ENDIAN).short.toInt()
|
|
389
|
+
val bitDepth = ByteBuffer.wrap(headerBuffer, 34, 2).order(java.nio.ByteOrder.LITTLE_ENDIAN).short.toInt()
|
|
390
|
+
|
|
391
|
+
// Get file duration using MediaMetadataRetriever for consistency
|
|
392
|
+
val retriever = MediaMetadataRetriever()
|
|
393
|
+
retriever.setDataSource(inputFile.absolutePath)
|
|
394
|
+
val durationMsStr = retriever.extractMetadata(MediaMetadataRetriever.METADATA_KEY_DURATION)
|
|
395
|
+
val fileDurationMs = durationMsStr?.toLong() ?: 0
|
|
396
|
+
retriever.release()
|
|
397
|
+
|
|
398
|
+
// Create output file with WAV header
|
|
399
|
+
FileOutputStream(outputFile).use { outputStream ->
|
|
400
|
+
fileHandler.writeWavHeader(outputStream, sampleRate, channels, bitDepth)
|
|
401
|
+
|
|
402
|
+
// Process each time range
|
|
403
|
+
val bytesPerSample = bitDepth / 8
|
|
404
|
+
val bytesPerFrame = bytesPerSample * channels
|
|
405
|
+
val buffer = ByteArray(BUFFER_SIZE - (BUFFER_SIZE % bytesPerFrame)) // Ensure buffer size is multiple of frame size
|
|
406
|
+
|
|
407
|
+
var totalBytesProcessed = 0L
|
|
408
|
+
val totalRangeDuration = calculateOutputDuration(timeRanges)
|
|
409
|
+
var currentRangeProcessed = 0L
|
|
410
|
+
|
|
411
|
+
var lastUpdateTime = 0L
|
|
412
|
+
val updateIntervalMs = 100L // Update progress every 100ms
|
|
413
|
+
|
|
414
|
+
for (range in timeRanges) {
|
|
415
|
+
val startTimeMs = range["startTimeMs"] ?: 0
|
|
416
|
+
val endTimeMs = range["endTimeMs"] ?: fileDurationMs // Use actual file duration instead of Long.MAX_VALUE
|
|
417
|
+
|
|
418
|
+
// Calculate byte positions
|
|
419
|
+
val startByte = headerSize + ((startTimeMs * sampleRate * bytesPerFrame) / 1000)
|
|
420
|
+
val endByte = headerSize + ((endTimeMs * sampleRate * bytesPerFrame) / 1000)
|
|
421
|
+
|
|
422
|
+
val rangeSize = endByte - startByte
|
|
423
|
+
val rangeDuration = endTimeMs - startTimeMs
|
|
424
|
+
|
|
425
|
+
// Read and write the range using 'use' pattern
|
|
426
|
+
FileInputStream(inputFile).use { rangeInputStream ->
|
|
427
|
+
if (rangeInputStream.skip(startByte) != startByte) {
|
|
428
|
+
throw IOException("Failed to skip to position $startByte in input file")
|
|
429
|
+
}
|
|
430
|
+
|
|
431
|
+
var bytesRead: Int
|
|
432
|
+
var rangeProcessed = 0L
|
|
433
|
+
|
|
434
|
+
while (rangeInputStream.read(buffer).also { bytesRead = it } > 0 && rangeProcessed < rangeSize) {
|
|
435
|
+
// Ensure we don't read past the range
|
|
436
|
+
val bytesToWrite = min(bytesRead.toLong(), rangeSize - rangeProcessed).toInt()
|
|
437
|
+
|
|
438
|
+
outputStream.write(buffer, 0, bytesToWrite)
|
|
439
|
+
|
|
440
|
+
rangeProcessed += bytesToWrite
|
|
441
|
+
totalBytesProcessed += bytesToWrite
|
|
442
|
+
|
|
443
|
+
// Calculate progress based on time for consistency with compressed audio
|
|
444
|
+
val currentTimeInRange = (rangeProcessed * 1000) / (sampleRate * bytesPerFrame)
|
|
445
|
+
|
|
446
|
+
// Calculate overall progress directly
|
|
447
|
+
val overallProgress = (currentRangeProcessed + currentTimeInRange).toFloat() / totalRangeDuration
|
|
448
|
+
|
|
449
|
+
val currentTime = System.currentTimeMillis()
|
|
450
|
+
if (currentTime - lastUpdateTime >= updateIntervalMs) {
|
|
451
|
+
progressCallback(overallProgress * 100, bytesToWrite.toLong(), totalRangeDuration)
|
|
452
|
+
lastUpdateTime = currentTime
|
|
453
|
+
}
|
|
454
|
+
|
|
455
|
+
// Break if we've read the entire range
|
|
456
|
+
if (rangeProcessed >= rangeSize) {
|
|
457
|
+
break
|
|
458
|
+
}
|
|
459
|
+
}
|
|
460
|
+
}
|
|
461
|
+
|
|
462
|
+
currentRangeProcessed += rangeDuration
|
|
463
|
+
}
|
|
464
|
+
}
|
|
465
|
+
|
|
466
|
+
// Update WAV header with correct file size
|
|
467
|
+
fileHandler.updateWavHeader(outputFile)
|
|
468
|
+
}
|
|
469
|
+
|
|
470
|
+
/**
|
|
471
|
+
* Optimized version of processToWav that processes audio ranges more efficiently
|
|
472
|
+
*/
|
|
473
|
+
private fun processToWav(
|
|
474
|
+
inputUri: Uri,
|
|
475
|
+
outputFile: File,
|
|
476
|
+
timeRanges: List<Map<String, Long>>,
|
|
477
|
+
config: DecodingConfig,
|
|
478
|
+
progressListener: ProgressListener?
|
|
479
|
+
) {
|
|
480
|
+
val audioProcessor = AudioProcessor(context.filesDir)
|
|
481
|
+
val isWavInput = try {
|
|
482
|
+
val mimeType = MediaMetadataRetriever().apply {
|
|
483
|
+
setDataSource(context, inputUri)
|
|
484
|
+
}.extractMetadata(MediaMetadataRetriever.METADATA_KEY_MIMETYPE)
|
|
485
|
+
mimeType == "audio/wav" || mimeType == "audio/x-wav"
|
|
486
|
+
} catch (e: Exception) {
|
|
487
|
+
false
|
|
488
|
+
}
|
|
489
|
+
|
|
490
|
+
// Create output file with WAV header
|
|
491
|
+
FileOutputStream(outputFile).use { outputStream ->
|
|
492
|
+
// We'll write the header at the end when we know the total size
|
|
493
|
+
var totalBytes = 0L
|
|
494
|
+
var totalProgress: Float
|
|
495
|
+
val totalRanges = timeRanges.size
|
|
496
|
+
|
|
497
|
+
// Process each time range
|
|
498
|
+
for ((index, range) in timeRanges.withIndex()) {
|
|
499
|
+
val startTimeMs = range["startTimeMs"] ?: 0
|
|
500
|
+
val endTimeMs = range["endTimeMs"] ?: 0
|
|
501
|
+
val rangeDuration = endTimeMs - startTimeMs
|
|
502
|
+
|
|
503
|
+
Log.d(TAG, "Processing range $index: $startTimeMs-$endTimeMs ms")
|
|
504
|
+
|
|
505
|
+
// Load just this range of audio - use the optimized method for compressed audio
|
|
506
|
+
val audioData = if (isWavInput) {
|
|
507
|
+
// For WAV files, use the existing method
|
|
508
|
+
audioProcessor.loadAudioRange(
|
|
509
|
+
fileUri = inputUri.toString(),
|
|
510
|
+
startTimeMs = startTimeMs,
|
|
511
|
+
endTimeMs = endTimeMs,
|
|
512
|
+
config = config
|
|
513
|
+
)
|
|
514
|
+
} else {
|
|
515
|
+
// For compressed audio, use the new optimized method
|
|
516
|
+
audioProcessor.decodeAudioRangeToPCM(
|
|
517
|
+
fileUri = inputUri.toString(),
|
|
518
|
+
startTimeMs = startTimeMs,
|
|
519
|
+
endTimeMs = endTimeMs
|
|
520
|
+
)?.let { decodedData ->
|
|
521
|
+
// Apply any format conversion if needed
|
|
522
|
+
if (config.targetSampleRate != null && config.targetSampleRate != decodedData.sampleRate ||
|
|
523
|
+
config.targetChannels != null && config.targetChannels != decodedData.channels) {
|
|
524
|
+
|
|
525
|
+
// Need to resample or convert channels
|
|
526
|
+
val resampledData = audioProcessor.processAudio(
|
|
527
|
+
decodedData.data,
|
|
528
|
+
decodedData.sampleRate,
|
|
529
|
+
decodedData.channels,
|
|
530
|
+
config.targetSampleRate ?: decodedData.sampleRate,
|
|
531
|
+
config.targetChannels ?: decodedData.channels,
|
|
532
|
+
config.normalizeAudio
|
|
533
|
+
)
|
|
534
|
+
|
|
535
|
+
AudioProcessor.AudioData(
|
|
536
|
+
data = resampledData,
|
|
537
|
+
sampleRate = config.targetSampleRate ?: decodedData.sampleRate,
|
|
538
|
+
channels = config.targetChannels ?: decodedData.channels,
|
|
539
|
+
bitDepth = decodedData.bitDepth,
|
|
540
|
+
durationMs = decodedData.durationMs
|
|
541
|
+
)
|
|
542
|
+
} else {
|
|
543
|
+
// No conversion needed
|
|
544
|
+
decodedData
|
|
545
|
+
}
|
|
546
|
+
}
|
|
547
|
+
} ?: throw IOException("Failed to load audio range $startTimeMs-$endTimeMs")
|
|
548
|
+
|
|
549
|
+
// For the first range, write the WAV header
|
|
550
|
+
if (index == 0) {
|
|
551
|
+
fileHandler.writeWavHeader(
|
|
552
|
+
outputStream,
|
|
553
|
+
audioData.sampleRate,
|
|
554
|
+
audioData.channels,
|
|
555
|
+
audioData.bitDepth
|
|
556
|
+
)
|
|
557
|
+
}
|
|
558
|
+
|
|
559
|
+
// Write the PCM data for this range
|
|
560
|
+
outputStream.write(audioData.data)
|
|
561
|
+
totalBytes += audioData.data.size
|
|
562
|
+
|
|
563
|
+
// Update progress
|
|
564
|
+
val rangeProgress = (index + 1).toFloat() / totalRanges
|
|
565
|
+
totalProgress = rangeProgress * 100
|
|
566
|
+
progressListener?.onProgress(totalProgress, audioData.data.size.toLong(), rangeDuration)
|
|
567
|
+
|
|
568
|
+
Log.d(TAG, "Range $index processed: ${audioData.data.size} bytes, ${audioData.durationMs} ms")
|
|
569
|
+
}
|
|
570
|
+
}
|
|
571
|
+
|
|
572
|
+
// Update WAV header with correct file size
|
|
573
|
+
fileHandler.updateWavHeader(outputFile)
|
|
574
|
+
|
|
575
|
+
Log.d(TAG, "WAV file created successfully: ${outputFile.absolutePath}")
|
|
576
|
+
}
|
|
577
|
+
|
|
578
|
+
/**
|
|
579
|
+
* Encodes a WAV file to AAC format using MediaCodec
|
|
580
|
+
*/
|
|
581
|
+
private fun encodeWavToAac(
|
|
582
|
+
inputWavFile: File,
|
|
583
|
+
outputAacFile: File,
|
|
584
|
+
formatOptions: Map<String, Any>,
|
|
585
|
+
progressListener: ProgressListener?
|
|
586
|
+
) {
|
|
587
|
+
// Increase MediaCodec buffer size
|
|
588
|
+
val largerInputBufferSize = 65536 // 64KB
|
|
589
|
+
|
|
590
|
+
Log.d(TAG, "Encoding WAV to AAC: ${inputWavFile.absolutePath} -> ${outputAacFile.absolutePath}")
|
|
591
|
+
|
|
592
|
+
// Get WAV file details
|
|
593
|
+
val audioProcessor = AudioProcessor(context.filesDir)
|
|
594
|
+
val audioFormat = audioProcessor.getAudioFormat(inputWavFile.absolutePath)
|
|
595
|
+
?: throw IOException("Failed to get audio format from WAV file")
|
|
596
|
+
|
|
597
|
+
val sampleRate = formatOptions["sampleRate"] as? Int ?: audioFormat.sampleRate
|
|
598
|
+
val channels = formatOptions["channels"] as? Int ?: audioFormat.channels
|
|
599
|
+
val bitrate = formatOptions["bitrate"] as? Int ?: 128000
|
|
600
|
+
|
|
601
|
+
// Load the entire WAV file as PCM data
|
|
602
|
+
val audioData = audioProcessor.loadAudioFromAnyFormat(
|
|
603
|
+
inputWavFile.absolutePath,
|
|
604
|
+
DecodingConfig(
|
|
605
|
+
targetSampleRate = sampleRate,
|
|
606
|
+
targetChannels = channels,
|
|
607
|
+
targetBitDepth = 16,
|
|
608
|
+
normalizeAudio = false
|
|
609
|
+
)
|
|
610
|
+
) ?: throw IOException("Failed to load WAV file")
|
|
611
|
+
|
|
612
|
+
// Set up MediaCodec for AAC encoding
|
|
613
|
+
val mediaFormat = MediaFormat.createAudioFormat(MediaFormat.MIMETYPE_AUDIO_AAC, sampleRate, channels)
|
|
614
|
+
mediaFormat.setInteger(MediaFormat.KEY_BIT_RATE, bitrate)
|
|
615
|
+
mediaFormat.setInteger(MediaFormat.KEY_AAC_PROFILE, android.media.MediaCodecInfo.CodecProfileLevel.AACObjectLC)
|
|
616
|
+
mediaFormat.setInteger(MediaFormat.KEY_MAX_INPUT_SIZE, largerInputBufferSize)
|
|
617
|
+
|
|
618
|
+
val encoder = android.media.MediaCodec.createEncoderByType(MediaFormat.MIMETYPE_AUDIO_AAC)
|
|
619
|
+
encoder.configure(mediaFormat, null, null, android.media.MediaCodec.CONFIGURE_FLAG_ENCODE)
|
|
620
|
+
encoder.start()
|
|
621
|
+
|
|
622
|
+
// Set up MediaMuxer for MP4 container
|
|
623
|
+
val muxer = MediaMuxer(outputAacFile.absolutePath, MediaMuxer.OutputFormat.MUXER_OUTPUT_MPEG_4)
|
|
624
|
+
var trackIndex = -1
|
|
625
|
+
var muxerStarted = false
|
|
626
|
+
|
|
627
|
+
try {
|
|
628
|
+
val bufferInfo = android.media.MediaCodec.BufferInfo()
|
|
629
|
+
val timeoutUs = 10000L
|
|
630
|
+
var presentationTimeUs = 0L
|
|
631
|
+
var totalBytesProcessed = 0L
|
|
632
|
+
val totalBytes = audioData.data.size.toLong()
|
|
633
|
+
var allInputSubmitted = false
|
|
634
|
+
var encoderDone = false
|
|
635
|
+
|
|
636
|
+
// Calculate bytes per frame
|
|
637
|
+
val bytesPerSample = audioData.bitDepth / 8
|
|
638
|
+
val bytesPerFrame = bytesPerSample * audioData.channels
|
|
639
|
+
val frameSizeInBytes = 65536 // 64KB buffer instead of smaller chunks
|
|
640
|
+
|
|
641
|
+
// Process the PCM data in larger chunks
|
|
642
|
+
var inputOffset = 0
|
|
643
|
+
|
|
644
|
+
var lastUpdateTime = 0L
|
|
645
|
+
val updateIntervalMs = 100L
|
|
646
|
+
|
|
647
|
+
while (!encoderDone) {
|
|
648
|
+
// Submit input data if we have any left
|
|
649
|
+
if (!allInputSubmitted) {
|
|
650
|
+
val inputBufferIndex = encoder.dequeueInputBuffer(timeoutUs)
|
|
651
|
+
if (inputBufferIndex >= 0) {
|
|
652
|
+
val inputBuffer = encoder.getInputBuffer(inputBufferIndex)
|
|
653
|
+
inputBuffer?.clear()
|
|
654
|
+
|
|
655
|
+
// Calculate how many bytes to read
|
|
656
|
+
val bytesToRead = if (inputOffset + frameSizeInBytes <= audioData.data.size) {
|
|
657
|
+
frameSizeInBytes
|
|
658
|
+
} else {
|
|
659
|
+
audioData.data.size - inputOffset
|
|
660
|
+
}
|
|
661
|
+
|
|
662
|
+
if (bytesToRead > 0) {
|
|
663
|
+
// Copy data to the input buffer
|
|
664
|
+
inputBuffer?.put(audioData.data, inputOffset, bytesToRead)
|
|
665
|
+
|
|
666
|
+
// Calculate presentation time in microseconds
|
|
667
|
+
val frameDurationUs = (bytesToRead * 1000000L) / (sampleRate * bytesPerFrame)
|
|
668
|
+
|
|
669
|
+
// Submit the input buffer
|
|
670
|
+
encoder.queueInputBuffer(
|
|
671
|
+
inputBufferIndex,
|
|
672
|
+
0,
|
|
673
|
+
bytesToRead,
|
|
674
|
+
presentationTimeUs,
|
|
675
|
+
0
|
|
676
|
+
)
|
|
677
|
+
|
|
678
|
+
// Update state
|
|
679
|
+
presentationTimeUs += frameDurationUs
|
|
680
|
+
inputOffset += bytesToRead
|
|
681
|
+
totalBytesProcessed += bytesToRead
|
|
682
|
+
|
|
683
|
+
// Report progress
|
|
684
|
+
val progress = (totalBytesProcessed * 100f) / totalBytes
|
|
685
|
+
val currentTime = System.currentTimeMillis()
|
|
686
|
+
if (progressListener != null && (currentTime - lastUpdateTime >= updateIntervalMs)) {
|
|
687
|
+
progressListener.onProgress(progress, bytesToRead.toLong(), totalBytes)
|
|
688
|
+
lastUpdateTime = currentTime
|
|
689
|
+
}
|
|
690
|
+
} else {
|
|
691
|
+
// End of input
|
|
692
|
+
encoder.queueInputBuffer(
|
|
693
|
+
inputBufferIndex,
|
|
694
|
+
0,
|
|
695
|
+
0,
|
|
696
|
+
presentationTimeUs,
|
|
697
|
+
android.media.MediaCodec.BUFFER_FLAG_END_OF_STREAM
|
|
698
|
+
)
|
|
699
|
+
allInputSubmitted = true
|
|
700
|
+
}
|
|
701
|
+
}
|
|
702
|
+
}
|
|
703
|
+
|
|
704
|
+
// Get encoded output
|
|
705
|
+
val outputBufferIndex = encoder.dequeueOutputBuffer(bufferInfo, timeoutUs)
|
|
706
|
+
if (outputBufferIndex == android.media.MediaCodec.INFO_OUTPUT_FORMAT_CHANGED) {
|
|
707
|
+
// Encoder output format changed, must be the first before any data
|
|
708
|
+
if (trackIndex >= 0) {
|
|
709
|
+
throw RuntimeException("Format changed twice")
|
|
710
|
+
}
|
|
711
|
+
val newFormat = encoder.outputFormat
|
|
712
|
+
Log.d(TAG, "Encoder output format changed: $newFormat")
|
|
713
|
+
trackIndex = muxer.addTrack(newFormat)
|
|
714
|
+
muxer.start()
|
|
715
|
+
muxerStarted = true
|
|
716
|
+
} else if (outputBufferIndex >= 0) {
|
|
717
|
+
// Got encoded data
|
|
718
|
+
val encodedData = encoder.getOutputBuffer(outputBufferIndex)
|
|
719
|
+
|
|
720
|
+
if (encodedData != null) {
|
|
721
|
+
if ((bufferInfo.flags and android.media.MediaCodec.BUFFER_FLAG_CODEC_CONFIG) != 0) {
|
|
722
|
+
// Codec config data, not actual media data
|
|
723
|
+
bufferInfo.size = 0
|
|
724
|
+
}
|
|
725
|
+
|
|
726
|
+
if (bufferInfo.size > 0 && muxerStarted) {
|
|
727
|
+
// Adjust buffer info offset and size for the buffer
|
|
728
|
+
encodedData.position(bufferInfo.offset)
|
|
729
|
+
encodedData.limit(bufferInfo.offset + bufferInfo.size)
|
|
730
|
+
|
|
731
|
+
// Write to muxer
|
|
732
|
+
muxer.writeSampleData(trackIndex, encodedData, bufferInfo)
|
|
733
|
+
}
|
|
734
|
+
|
|
735
|
+
// Release the output buffer
|
|
736
|
+
encoder.releaseOutputBuffer(outputBufferIndex, false)
|
|
737
|
+
|
|
738
|
+
// Check if we're done
|
|
739
|
+
if ((bufferInfo.flags and android.media.MediaCodec.BUFFER_FLAG_END_OF_STREAM) != 0) {
|
|
740
|
+
encoderDone = true
|
|
741
|
+
}
|
|
742
|
+
}
|
|
743
|
+
}
|
|
744
|
+
}
|
|
745
|
+
|
|
746
|
+
// Make sure we report 100% progress
|
|
747
|
+
progressListener?.onProgress(100f, totalBytes, totalBytes)
|
|
748
|
+
|
|
749
|
+
} finally {
|
|
750
|
+
// Clean up resources
|
|
751
|
+
try {
|
|
752
|
+
encoder.stop()
|
|
753
|
+
encoder.release()
|
|
754
|
+
} catch (e: Exception) {
|
|
755
|
+
Log.w(TAG, "Error releasing encoder: ${e.message}")
|
|
756
|
+
}
|
|
757
|
+
|
|
758
|
+
if (muxerStarted) {
|
|
759
|
+
try {
|
|
760
|
+
muxer.stop()
|
|
761
|
+
} catch (e: Exception) {
|
|
762
|
+
Log.w(TAG, "Error stopping muxer: ${e.message}")
|
|
763
|
+
}
|
|
764
|
+
}
|
|
765
|
+
|
|
766
|
+
try {
|
|
767
|
+
muxer.release()
|
|
768
|
+
} catch (e: Exception) {
|
|
769
|
+
Log.w(TAG, "Error releasing muxer: ${e.message}")
|
|
770
|
+
}
|
|
771
|
+
}
|
|
772
|
+
|
|
773
|
+
Log.d(TAG, "WAV to AAC encoding completed successfully")
|
|
774
|
+
}
|
|
775
|
+
|
|
776
|
+
/**
|
|
777
|
+
* Encodes audio data to Opus format using MediaCodec
|
|
778
|
+
*/
|
|
779
|
+
private fun encodeToOpus(
|
|
780
|
+
audioData: AudioProcessor.AudioData,
|
|
781
|
+
outputFile: File,
|
|
782
|
+
formatOptions: Map<String, Any>,
|
|
783
|
+
progressListener: ProgressListener?
|
|
784
|
+
) {
|
|
785
|
+
Log.d(TAG, "Encoding to Opus: ${outputFile.absolutePath}")
|
|
786
|
+
|
|
787
|
+
try {
|
|
788
|
+
// Check if Opus codec is available
|
|
789
|
+
val codecList = android.media.MediaCodecList(android.media.MediaCodecList.REGULAR_CODECS)
|
|
790
|
+
val opusCodecName = codecList.codecInfos
|
|
791
|
+
.filter { it.isEncoder && it.supportedTypes.contains(MediaFormat.MIMETYPE_AUDIO_OPUS) }
|
|
792
|
+
.map { it.name }
|
|
793
|
+
.firstOrNull()
|
|
794
|
+
|
|
795
|
+
if (opusCodecName == null) {
|
|
796
|
+
Log.w(TAG, "Opus encoder not available, falling back to AAC")
|
|
797
|
+
|
|
798
|
+
// Create a temporary WAV file
|
|
799
|
+
val tempWavFile = File(context.filesDir, "temp_${System.currentTimeMillis()}.wav")
|
|
800
|
+
try {
|
|
801
|
+
// Use AudioFileHandler to write WAV header and data
|
|
802
|
+
val audioFileHandler = AudioFileHandler(context.filesDir)
|
|
803
|
+
tempWavFile.outputStream().use { outputStream ->
|
|
804
|
+
// Write WAV header
|
|
805
|
+
audioFileHandler.writeWavHeader(
|
|
806
|
+
outputStream,
|
|
807
|
+
audioData.sampleRate,
|
|
808
|
+
audioData.channels,
|
|
809
|
+
audioData.bitDepth
|
|
810
|
+
)
|
|
811
|
+
|
|
812
|
+
// Write PCM data
|
|
813
|
+
outputStream.write(audioData.data)
|
|
814
|
+
}
|
|
815
|
+
|
|
816
|
+
// Update WAV header with correct file size
|
|
817
|
+
audioFileHandler.updateWavHeader(tempWavFile)
|
|
818
|
+
|
|
819
|
+
// Now we can call encodeWavToAac with the temp file
|
|
820
|
+
encodeWavToAac(
|
|
821
|
+
tempWavFile,
|
|
822
|
+
outputFile,
|
|
823
|
+
formatOptions,
|
|
824
|
+
progressListener
|
|
825
|
+
)
|
|
826
|
+
} finally {
|
|
827
|
+
// Clean up temp file
|
|
828
|
+
if (tempWavFile.exists()) {
|
|
829
|
+
tempWavFile.delete()
|
|
830
|
+
}
|
|
831
|
+
}
|
|
832
|
+
return
|
|
833
|
+
}
|
|
834
|
+
|
|
835
|
+
// Set up MediaCodec for Opus encoding
|
|
836
|
+
val sampleRate = formatOptions["sampleRate"] as? Int ?: audioData.sampleRate
|
|
837
|
+
val channels = formatOptions["channels"] as? Int ?: audioData.channels
|
|
838
|
+
|
|
839
|
+
// Determine appropriate bitrate based on content type and channels
|
|
840
|
+
// For voice: 8-24kbps for mono, 16-32kbps for stereo is typically sufficient
|
|
841
|
+
val defaultBitrate = if (channels > 1) 32000 else 16000 // Lower defaults for voice
|
|
842
|
+
val bitrate = formatOptions["bitrate"] as? Int ?: defaultBitrate
|
|
843
|
+
|
|
844
|
+
// Determine if this is voice content based on sample rate and/or explicit flag
|
|
845
|
+
val isVoiceContent = formatOptions["isVoice"] as? Boolean ?: (sampleRate <= 16000)
|
|
846
|
+
|
|
847
|
+
val mediaFormat = MediaFormat.createAudioFormat(MediaFormat.MIMETYPE_AUDIO_OPUS, sampleRate, channels)
|
|
848
|
+
mediaFormat.setInteger(MediaFormat.KEY_BIT_RATE, bitrate)
|
|
849
|
+
mediaFormat.setInteger(MediaFormat.KEY_MAX_INPUT_SIZE, 65536)
|
|
850
|
+
|
|
851
|
+
// Set complexity - lower for voice (faster encoding, still good quality)
|
|
852
|
+
// Complexity range is 0-10, with 10 being highest quality but slowest
|
|
853
|
+
val complexity = if (isVoiceContent) 5 else 7
|
|
854
|
+
try {
|
|
855
|
+
mediaFormat.setInteger("complexity", complexity)
|
|
856
|
+
Log.d(TAG, "Set Opus complexity to $complexity")
|
|
857
|
+
} catch (e: Exception) {
|
|
858
|
+
// Some devices might not support this parameter
|
|
859
|
+
Log.w(TAG, "Failed to set complexity parameter: ${e.message}")
|
|
860
|
+
}
|
|
861
|
+
|
|
862
|
+
// For API 28+ we can set some additional parameters
|
|
863
|
+
if (android.os.Build.VERSION.SDK_INT >= android.os.Build.VERSION_CODES.P) {
|
|
864
|
+
try {
|
|
865
|
+
// Use 1 for speech, 2 for music
|
|
866
|
+
val contentTypeValue = if (isVoiceContent) 1 else 2
|
|
867
|
+
mediaFormat.setInteger("audio-content-type", contentTypeValue)
|
|
868
|
+
Log.d(TAG, "Set Opus content type to: ${if (isVoiceContent) "SPEECH" else "MUSIC"}")
|
|
869
|
+
} catch (e: Exception) {
|
|
870
|
+
// Some devices might not support this parameter
|
|
871
|
+
Log.w(TAG, "Failed to set audio-content-type parameter: ${e.message}")
|
|
872
|
+
}
|
|
873
|
+
}
|
|
874
|
+
|
|
875
|
+
// Create encoder
|
|
876
|
+
val encoder = android.media.MediaCodec.createByCodecName(opusCodecName)
|
|
877
|
+
encoder.configure(mediaFormat, null, null, android.media.MediaCodec.CONFIGURE_FLAG_ENCODE)
|
|
878
|
+
encoder.start()
|
|
879
|
+
|
|
880
|
+
// Set up MediaMuxer for Opus container (using OGG container)
|
|
881
|
+
val muxer = MediaMuxer(outputFile.absolutePath, MediaMuxer.OutputFormat.MUXER_OUTPUT_OGG)
|
|
882
|
+
var trackIndex = -1
|
|
883
|
+
var muxerStarted = false
|
|
884
|
+
|
|
885
|
+
try {
|
|
886
|
+
val bufferInfo = android.media.MediaCodec.BufferInfo()
|
|
887
|
+
val timeoutUs = 10000L
|
|
888
|
+
var presentationTimeUs = 0L
|
|
889
|
+
var totalBytesProcessed = 0L
|
|
890
|
+
val totalBytes = audioData.data.size.toLong()
|
|
891
|
+
var allInputSubmitted = false
|
|
892
|
+
var encoderDone = false
|
|
893
|
+
|
|
894
|
+
// Calculate bytes per frame
|
|
895
|
+
val bytesPerSample = audioData.bitDepth / 8
|
|
896
|
+
val bytesPerFrame = bytesPerSample * audioData.channels
|
|
897
|
+
val frameSizeInBytes = 65536 // 64KB buffer instead of smaller chunks
|
|
898
|
+
|
|
899
|
+
// Process the PCM data in chunks
|
|
900
|
+
var inputOffset = 0
|
|
901
|
+
|
|
902
|
+
var lastUpdateTime = 0L
|
|
903
|
+
val updateIntervalMs = 100L
|
|
904
|
+
|
|
905
|
+
while (!encoderDone) {
|
|
906
|
+
// Submit input data if we have any left
|
|
907
|
+
if (!allInputSubmitted) {
|
|
908
|
+
val inputBufferIndex = encoder.dequeueInputBuffer(timeoutUs)
|
|
909
|
+
if (inputBufferIndex >= 0) {
|
|
910
|
+
val inputBuffer = encoder.getInputBuffer(inputBufferIndex)
|
|
911
|
+
inputBuffer?.clear()
|
|
912
|
+
|
|
913
|
+
// Calculate how many bytes to read
|
|
914
|
+
val bytesToRead = if (inputOffset + frameSizeInBytes <= audioData.data.size) {
|
|
915
|
+
frameSizeInBytes
|
|
916
|
+
} else {
|
|
917
|
+
audioData.data.size - inputOffset
|
|
918
|
+
}
|
|
919
|
+
|
|
920
|
+
if (bytesToRead > 0) {
|
|
921
|
+
// Copy data to the input buffer
|
|
922
|
+
inputBuffer?.put(audioData.data, inputOffset, bytesToRead)
|
|
923
|
+
|
|
924
|
+
// Calculate presentation time in microseconds
|
|
925
|
+
val frameDurationUs = (bytesToRead * 1000000L) / (sampleRate * bytesPerFrame)
|
|
926
|
+
|
|
927
|
+
// Submit the input buffer
|
|
928
|
+
encoder.queueInputBuffer(
|
|
929
|
+
inputBufferIndex,
|
|
930
|
+
0,
|
|
931
|
+
bytesToRead,
|
|
932
|
+
presentationTimeUs,
|
|
933
|
+
0
|
|
934
|
+
)
|
|
935
|
+
|
|
936
|
+
// Update state
|
|
937
|
+
presentationTimeUs += frameDurationUs
|
|
938
|
+
inputOffset += bytesToRead
|
|
939
|
+
totalBytesProcessed += bytesToRead
|
|
940
|
+
|
|
941
|
+
// Report progress
|
|
942
|
+
val progress = (totalBytesProcessed * 100f) / totalBytes
|
|
943
|
+
val currentTime = System.currentTimeMillis()
|
|
944
|
+
if (progressListener != null && (currentTime - lastUpdateTime >= updateIntervalMs)) {
|
|
945
|
+
progressListener.onProgress(progress, bytesToRead.toLong(), totalBytes)
|
|
946
|
+
lastUpdateTime = currentTime
|
|
947
|
+
}
|
|
948
|
+
} else {
|
|
949
|
+
// End of input
|
|
950
|
+
encoder.queueInputBuffer(
|
|
951
|
+
inputBufferIndex,
|
|
952
|
+
0,
|
|
953
|
+
0,
|
|
954
|
+
presentationTimeUs,
|
|
955
|
+
android.media.MediaCodec.BUFFER_FLAG_END_OF_STREAM
|
|
956
|
+
)
|
|
957
|
+
allInputSubmitted = true
|
|
958
|
+
}
|
|
959
|
+
}
|
|
960
|
+
}
|
|
961
|
+
|
|
962
|
+
// Get encoded output
|
|
963
|
+
val outputBufferIndex = encoder.dequeueOutputBuffer(bufferInfo, timeoutUs)
|
|
964
|
+
if (outputBufferIndex == android.media.MediaCodec.INFO_OUTPUT_FORMAT_CHANGED) {
|
|
965
|
+
// Encoder output format changed, must be the first before any data
|
|
966
|
+
if (trackIndex >= 0) {
|
|
967
|
+
throw RuntimeException("Format changed twice")
|
|
968
|
+
}
|
|
969
|
+
val newFormat = encoder.outputFormat
|
|
970
|
+
Log.d(TAG, "Encoder output format changed: $newFormat")
|
|
971
|
+
trackIndex = muxer.addTrack(newFormat)
|
|
972
|
+
muxer.start()
|
|
973
|
+
muxerStarted = true
|
|
974
|
+
} else if (outputBufferIndex >= 0) {
|
|
975
|
+
// Got encoded data
|
|
976
|
+
val encodedData = encoder.getOutputBuffer(outputBufferIndex)
|
|
977
|
+
|
|
978
|
+
if (encodedData != null) {
|
|
979
|
+
if ((bufferInfo.flags and android.media.MediaCodec.BUFFER_FLAG_CODEC_CONFIG) != 0) {
|
|
980
|
+
// Codec config data, not actual media data
|
|
981
|
+
bufferInfo.size = 0
|
|
982
|
+
}
|
|
983
|
+
|
|
984
|
+
if (bufferInfo.size > 0 && muxerStarted) {
|
|
985
|
+
// Adjust buffer info offset and size for the buffer
|
|
986
|
+
encodedData.position(bufferInfo.offset)
|
|
987
|
+
encodedData.limit(bufferInfo.offset + bufferInfo.size)
|
|
988
|
+
|
|
989
|
+
// Write to muxer
|
|
990
|
+
muxer.writeSampleData(trackIndex, encodedData, bufferInfo)
|
|
991
|
+
}
|
|
992
|
+
|
|
993
|
+
// Release the output buffer
|
|
994
|
+
encoder.releaseOutputBuffer(outputBufferIndex, false)
|
|
995
|
+
|
|
996
|
+
// Check if we're done
|
|
997
|
+
if ((bufferInfo.flags and android.media.MediaCodec.BUFFER_FLAG_END_OF_STREAM) != 0) {
|
|
998
|
+
encoderDone = true
|
|
999
|
+
}
|
|
1000
|
+
}
|
|
1001
|
+
}
|
|
1002
|
+
}
|
|
1003
|
+
|
|
1004
|
+
// Make sure we report 100% progress
|
|
1005
|
+
progressListener?.onProgress(100f, totalBytes, totalBytes)
|
|
1006
|
+
|
|
1007
|
+
} finally {
|
|
1008
|
+
// Clean up resources
|
|
1009
|
+
try {
|
|
1010
|
+
encoder.stop()
|
|
1011
|
+
encoder.release()
|
|
1012
|
+
} catch (e: Exception) {
|
|
1013
|
+
Log.w(TAG, "Error releasing encoder: ${e.message}")
|
|
1014
|
+
}
|
|
1015
|
+
|
|
1016
|
+
if (muxerStarted) {
|
|
1017
|
+
try {
|
|
1018
|
+
muxer.stop()
|
|
1019
|
+
} catch (e: Exception) {
|
|
1020
|
+
Log.w(TAG, "Error stopping muxer: ${e.message}")
|
|
1021
|
+
}
|
|
1022
|
+
}
|
|
1023
|
+
|
|
1024
|
+
try {
|
|
1025
|
+
muxer.release()
|
|
1026
|
+
} catch (e: Exception) {
|
|
1027
|
+
Log.w(TAG, "Error releasing muxer: ${e.message}")
|
|
1028
|
+
}
|
|
1029
|
+
}
|
|
1030
|
+
|
|
1031
|
+
Log.d(TAG, "Opus encoding completed successfully")
|
|
1032
|
+
} catch (e: Exception) {
|
|
1033
|
+
Log.e(TAG, "Error encoding to Opus: ${e.message}", e)
|
|
1034
|
+
|
|
1035
|
+
// Fall back to AAC if Opus encoding fails
|
|
1036
|
+
Log.w(TAG, "Opus encoding failed, falling back to AAC")
|
|
1037
|
+
|
|
1038
|
+
// Create a temporary WAV file
|
|
1039
|
+
val tempWavFile = File(context.filesDir, "temp_${System.currentTimeMillis()}.wav")
|
|
1040
|
+
try {
|
|
1041
|
+
// Write the audio data to a temporary WAV file
|
|
1042
|
+
val audioFileHandler = AudioFileHandler(context.filesDir)
|
|
1043
|
+
tempWavFile.outputStream().use { outputStream ->
|
|
1044
|
+
audioFileHandler.writeWavHeader(
|
|
1045
|
+
outputStream,
|
|
1046
|
+
audioData.sampleRate,
|
|
1047
|
+
audioData.channels,
|
|
1048
|
+
audioData.bitDepth
|
|
1049
|
+
)
|
|
1050
|
+
outputStream.write(audioData.data)
|
|
1051
|
+
}
|
|
1052
|
+
audioFileHandler.updateWavHeader(tempWavFile)
|
|
1053
|
+
|
|
1054
|
+
// Encode to AAC
|
|
1055
|
+
encodeWavToAac(
|
|
1056
|
+
tempWavFile,
|
|
1057
|
+
File(outputFile.absolutePath.replace(".opus", ".aac")),
|
|
1058
|
+
formatOptions,
|
|
1059
|
+
progressListener
|
|
1060
|
+
)
|
|
1061
|
+
} finally {
|
|
1062
|
+
if (tempWavFile.exists()) {
|
|
1063
|
+
tempWavFile.delete()
|
|
1064
|
+
}
|
|
1065
|
+
}
|
|
1066
|
+
|
|
1067
|
+
throw IOException("Failed to encode to Opus: ${e.message}", e)
|
|
1068
|
+
}
|
|
1069
|
+
}
|
|
1070
|
+
|
|
1071
|
+
// Helper function to extract audio format from MediaMetadataRetriever
|
|
1072
|
+
private fun getAudioFormat(retriever: MediaMetadataRetriever): AudioFormat {
|
|
1073
|
+
val sampleRate = retriever.extractMetadata(MediaMetadataRetriever.METADATA_KEY_SAMPLERATE)?.toIntOrNull() ?: 44100
|
|
1074
|
+
val channels = retriever.extractMetadata(MediaMetadataRetriever.METADATA_KEY_BITRATE)?.toIntOrNull()?.let {
|
|
1075
|
+
// Estimate channels from bitrate and sample rate if not directly available
|
|
1076
|
+
if (it > sampleRate * 16) 2 else 1
|
|
1077
|
+
} ?: 1
|
|
1078
|
+
|
|
1079
|
+
// Bit depth is often not directly available, assume 16-bit as default
|
|
1080
|
+
val bitDepth = 16
|
|
1081
|
+
|
|
1082
|
+
// Get duration in milliseconds
|
|
1083
|
+
val durationMs = retriever.extractMetadata(MediaMetadataRetriever.METADATA_KEY_DURATION)?.toLongOrNull() ?: 0L
|
|
1084
|
+
|
|
1085
|
+
// Get MIME type
|
|
1086
|
+
val mimeType = retriever.extractMetadata(MediaMetadataRetriever.METADATA_KEY_MIMETYPE) ?: "audio/mpeg"
|
|
1087
|
+
|
|
1088
|
+
return AudioFormat(sampleRate, channels, bitDepth, durationMs, mimeType)
|
|
1089
|
+
}
|
|
1090
|
+
|
|
1091
|
+
// Data class to hold audio format information
|
|
1092
|
+
data class AudioFormat(
|
|
1093
|
+
val sampleRate: Int,
|
|
1094
|
+
val channels: Int,
|
|
1095
|
+
val bitDepth: Int,
|
|
1096
|
+
val durationMs: Long = 0,
|
|
1097
|
+
val mimeType: String = "audio/mpeg"
|
|
1098
|
+
)
|
|
1099
|
+
}
|