@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.
Files changed (375) hide show
  1. package/CHANGELOG.md +535 -0
  2. package/LICENSE +21 -0
  3. package/README.md +167 -0
  4. package/android/build.gradle +143 -0
  5. package/android/src/androidTest/assets/chorus.wav +0 -0
  6. package/android/src/androidTest/assets/jfk.wav +0 -0
  7. package/android/src/androidTest/assets/osr_us_000_0010_8k.wav +0 -0
  8. package/android/src/androidTest/assets/recorder_hello_world.wav +0 -0
  9. package/android/src/androidTest/java/net/siteed/audiostudio/AudioProcessorInstrumentedTest.kt +197 -0
  10. package/android/src/androidTest/java/net/siteed/audiostudio/AudioRecorderInstrumentedTest.kt +541 -0
  11. package/android/src/androidTest/java/net/siteed/audiostudio/AudioRecorderPerformanceInstrumentedTest.kt +234 -0
  12. package/android/src/androidTest/java/net/siteed/audiostudio/integration/AudioFocusStrategyIntegrationTest.kt +332 -0
  13. package/android/src/androidTest/java/net/siteed/audiostudio/integration/BufferDurationIntegrationTest.kt +324 -0
  14. package/android/src/androidTest/java/net/siteed/audiostudio/integration/CompressedOnlyOutputTest.kt +253 -0
  15. package/android/src/androidTest/java/net/siteed/audiostudio/integration/DeviceDisconnectionFallbackTest.kt +218 -0
  16. package/android/src/androidTest/java/net/siteed/audiostudio/integration/EventEmissionIntervalTest.kt +120 -0
  17. package/android/src/androidTest/java/net/siteed/audiostudio/integration/M4aFormatTest.kt +345 -0
  18. package/android/src/androidTest/java/net/siteed/audiostudio/integration/OutputControlIntegrationTest.kt +340 -0
  19. package/android/src/androidTest/java/net/siteed/audiostudio/integration/PcmStreamingDurationTest.kt +252 -0
  20. package/android/src/androidTest/java/net/siteed/audiostudio/integration/README.md +95 -0
  21. package/android/src/androidTest/java/net/siteed/audiostudio/integration/run_integration_tests.sh +43 -0
  22. package/android/src/main/AndroidManifest.xml +30 -0
  23. package/android/src/main/CMakeLists.txt +29 -0
  24. package/android/src/main/java/net/siteed/audiostudio/AudioAnalysisData.kt +188 -0
  25. package/android/src/main/java/net/siteed/audiostudio/AudioDataEncoder.kt +9 -0
  26. package/android/src/main/java/net/siteed/audiostudio/AudioDeviceManager.kt +1741 -0
  27. package/android/src/main/java/net/siteed/audiostudio/AudioFeaturesNative.kt +26 -0
  28. package/android/src/main/java/net/siteed/audiostudio/AudioFileHandler.kt +136 -0
  29. package/android/src/main/java/net/siteed/audiostudio/AudioFormatUtils.kt +354 -0
  30. package/android/src/main/java/net/siteed/audiostudio/AudioNotificationsManager.kt +439 -0
  31. package/android/src/main/java/net/siteed/audiostudio/AudioProcessor.kt +2237 -0
  32. package/android/src/main/java/net/siteed/audiostudio/AudioRecorderManager.kt +2163 -0
  33. package/android/src/main/java/net/siteed/audiostudio/AudioRecordingService.kt +167 -0
  34. package/android/src/main/java/net/siteed/audiostudio/AudioStudioModule.kt +1112 -0
  35. package/android/src/main/java/net/siteed/audiostudio/AudioTrimmer.kt +1099 -0
  36. package/android/src/main/java/net/siteed/audiostudio/Constants.kt +37 -0
  37. package/android/src/main/java/net/siteed/audiostudio/EventSender.kt +7 -0
  38. package/android/src/main/java/net/siteed/audiostudio/FFT.kt +100 -0
  39. package/android/src/main/java/net/siteed/audiostudio/Features.kt +98 -0
  40. package/android/src/main/java/net/siteed/audiostudio/LogUtils.kt +93 -0
  41. package/android/src/main/java/net/siteed/audiostudio/MelSpectrogramNative.kt +36 -0
  42. package/android/src/main/java/net/siteed/audiostudio/NotificationConfig.kt +72 -0
  43. package/android/src/main/java/net/siteed/audiostudio/PermissionUtils.kt +68 -0
  44. package/android/src/main/java/net/siteed/audiostudio/RecordingActionReceiver.kt +59 -0
  45. package/android/src/main/java/net/siteed/audiostudio/RecordingConfig.kt +259 -0
  46. package/android/src/main/java/net/siteed/audiostudio/WaveformConfig.kt +19 -0
  47. package/android/src/main/java/net/siteed/audiostudio/WaveformRenderer.kt +159 -0
  48. package/android/src/main/jni/AudioFeaturesJNI.cpp +152 -0
  49. package/android/src/main/jni/MelSpectrogramJNI.cpp +165 -0
  50. package/android/src/main/res/drawable/ic_default_action_icon.xml +16 -0
  51. package/android/src/main/res/drawable/ic_microphone.xml +13 -0
  52. package/android/src/main/res/drawable/ic_pause.xml +10 -0
  53. package/android/src/main/res/drawable/ic_play.xml +10 -0
  54. package/android/src/main/res/drawable/ic_stop.xml +10 -0
  55. package/android/src/main/res/layout/notification_recording.xml +37 -0
  56. package/android/src/test/java/net/siteed/audiostudio/AudioFileHandlerTest.kt +279 -0
  57. package/android/src/test/java/net/siteed/audiostudio/AudioFocusStrategyTest.kt +249 -0
  58. package/android/src/test/java/net/siteed/audiostudio/AudioFormatTest.kt +151 -0
  59. package/android/src/test/java/net/siteed/audiostudio/AudioFormatUtilsTest.kt +273 -0
  60. package/android/src/test/java/net/siteed/audiostudio/DeviceDisconnectionFallbackUnitTest.kt +140 -0
  61. package/android/src/test/resources/chorus.wav +0 -0
  62. package/android/src/test/resources/generate_test_audio.py +94 -0
  63. package/android/src/test/resources/jfk.wav +0 -0
  64. package/android/src/test/resources/osr_us_000_0010_8k.wav +0 -0
  65. package/android/src/test/resources/recorder_hello_world.wav +0 -0
  66. package/app.plugin.js +3 -0
  67. package/build/cjs/AudioAnalysis/AudioAnalysis.types.js +4 -0
  68. package/build/cjs/AudioAnalysis/AudioAnalysis.types.js.map +1 -0
  69. package/build/cjs/AudioAnalysis/audioFeaturesWasm.js +164 -0
  70. package/build/cjs/AudioAnalysis/audioFeaturesWasm.js.map +1 -0
  71. package/build/cjs/AudioAnalysis/extractAudioAnalysis.js +213 -0
  72. package/build/cjs/AudioAnalysis/extractAudioAnalysis.js.map +1 -0
  73. package/build/cjs/AudioAnalysis/extractAudioData.js +21 -0
  74. package/build/cjs/AudioAnalysis/extractAudioData.js.map +1 -0
  75. package/build/cjs/AudioAnalysis/extractMelSpectrogram.js +90 -0
  76. package/build/cjs/AudioAnalysis/extractMelSpectrogram.js.map +1 -0
  77. package/build/cjs/AudioAnalysis/extractPreview.js +28 -0
  78. package/build/cjs/AudioAnalysis/extractPreview.js.map +1 -0
  79. package/build/cjs/AudioAnalysis/extractWaveform.js +18 -0
  80. package/build/cjs/AudioAnalysis/extractWaveform.js.map +1 -0
  81. package/build/cjs/AudioAnalysis/melSpectrogramWasm.js +149 -0
  82. package/build/cjs/AudioAnalysis/melSpectrogramWasm.js.map +1 -0
  83. package/build/cjs/AudioDeviceManager.js +688 -0
  84. package/build/cjs/AudioDeviceManager.js.map +1 -0
  85. package/build/cjs/AudioRecorder.provider.js +78 -0
  86. package/build/cjs/AudioRecorder.provider.js.map +1 -0
  87. package/build/cjs/AudioStudio.native.js +8 -0
  88. package/build/cjs/AudioStudio.native.js.map +1 -0
  89. package/build/cjs/AudioStudio.types.js +11 -0
  90. package/build/cjs/AudioStudio.types.js.map +1 -0
  91. package/build/cjs/AudioStudio.web.js +708 -0
  92. package/build/cjs/AudioStudio.web.js.map +1 -0
  93. package/build/cjs/AudioStudioModule.js +718 -0
  94. package/build/cjs/AudioStudioModule.js.map +1 -0
  95. package/build/cjs/WebRecorder.web.js +865 -0
  96. package/build/cjs/WebRecorder.web.js.map +1 -0
  97. package/build/cjs/constants/platformLimitations.js +99 -0
  98. package/build/cjs/constants/platformLimitations.js.map +1 -0
  99. package/build/cjs/constants.js +20 -0
  100. package/build/cjs/constants.js.map +1 -0
  101. package/build/cjs/events.js +29 -0
  102. package/build/cjs/events.js.map +1 -0
  103. package/build/cjs/hooks/useAudioDevices.js +179 -0
  104. package/build/cjs/hooks/useAudioDevices.js.map +1 -0
  105. package/build/cjs/index.js +64 -0
  106. package/build/cjs/index.js.map +1 -0
  107. package/build/cjs/trimAudio.js +76 -0
  108. package/build/cjs/trimAudio.js.map +1 -0
  109. package/build/cjs/useAudioRecorder.js +535 -0
  110. package/build/cjs/useAudioRecorder.js.map +1 -0
  111. package/build/cjs/utils/BlobFix.js +502 -0
  112. package/build/cjs/utils/BlobFix.js.map +1 -0
  113. package/build/cjs/utils/audioProcessing.js +136 -0
  114. package/build/cjs/utils/audioProcessing.js.map +1 -0
  115. package/build/cjs/utils/cleanNativeOptions.js +22 -0
  116. package/build/cjs/utils/cleanNativeOptions.js.map +1 -0
  117. package/build/cjs/utils/concatenateBuffers.js +25 -0
  118. package/build/cjs/utils/concatenateBuffers.js.map +1 -0
  119. package/build/cjs/utils/convertPCMToFloat32.js +124 -0
  120. package/build/cjs/utils/convertPCMToFloat32.js.map +1 -0
  121. package/build/cjs/utils/crc32.js +52 -0
  122. package/build/cjs/utils/crc32.js.map +1 -0
  123. package/build/cjs/utils/encodingToBitDepth.js +17 -0
  124. package/build/cjs/utils/encodingToBitDepth.js.map +1 -0
  125. package/build/cjs/utils/getWavFileInfo.js +96 -0
  126. package/build/cjs/utils/getWavFileInfo.js.map +1 -0
  127. package/build/cjs/utils/writeWavHeader.js +88 -0
  128. package/build/cjs/utils/writeWavHeader.js.map +1 -0
  129. package/build/cjs/workers/InlineFeaturesExtractor.web.js +294 -0
  130. package/build/cjs/workers/InlineFeaturesExtractor.web.js.map +1 -0
  131. package/build/cjs/workers/inlineAudioWebWorker.web.js +190 -0
  132. package/build/cjs/workers/inlineAudioWebWorker.web.js.map +1 -0
  133. package/build/cjs/workers/wasmGlueString.web.js +27 -0
  134. package/build/cjs/workers/wasmGlueString.web.js.map +1 -0
  135. package/build/esm/AudioAnalysis/AudioAnalysis.types.js +3 -0
  136. package/build/esm/AudioAnalysis/AudioAnalysis.types.js.map +1 -0
  137. package/build/esm/AudioAnalysis/audioFeaturesWasm.js +126 -0
  138. package/build/esm/AudioAnalysis/audioFeaturesWasm.js.map +1 -0
  139. package/build/esm/AudioAnalysis/extractAudioAnalysis.js +205 -0
  140. package/build/esm/AudioAnalysis/extractAudioAnalysis.js.map +1 -0
  141. package/build/esm/AudioAnalysis/extractAudioData.js +14 -0
  142. package/build/esm/AudioAnalysis/extractAudioData.js.map +1 -0
  143. package/build/esm/AudioAnalysis/extractMelSpectrogram.js +86 -0
  144. package/build/esm/AudioAnalysis/extractMelSpectrogram.js.map +1 -0
  145. package/build/esm/AudioAnalysis/extractPreview.js +25 -0
  146. package/build/esm/AudioAnalysis/extractPreview.js.map +1 -0
  147. package/build/esm/AudioAnalysis/extractWaveform.js +11 -0
  148. package/build/esm/AudioAnalysis/extractWaveform.js.map +1 -0
  149. package/build/esm/AudioAnalysis/melSpectrogramWasm.js +111 -0
  150. package/build/esm/AudioAnalysis/melSpectrogramWasm.js.map +1 -0
  151. package/build/esm/AudioDeviceManager.js +681 -0
  152. package/build/esm/AudioDeviceManager.js.map +1 -0
  153. package/build/esm/AudioRecorder.provider.js +40 -0
  154. package/build/esm/AudioRecorder.provider.js.map +1 -0
  155. package/build/esm/AudioStudio.native.js +6 -0
  156. package/build/esm/AudioStudio.native.js.map +1 -0
  157. package/build/esm/AudioStudio.types.js +8 -0
  158. package/build/esm/AudioStudio.types.js.map +1 -0
  159. package/build/esm/AudioStudio.web.js +704 -0
  160. package/build/esm/AudioStudio.web.js.map +1 -0
  161. package/build/esm/AudioStudioModule.js +713 -0
  162. package/build/esm/AudioStudioModule.js.map +1 -0
  163. package/build/esm/WebRecorder.web.js +861 -0
  164. package/build/esm/WebRecorder.web.js.map +1 -0
  165. package/build/esm/constants/platformLimitations.js +90 -0
  166. package/build/esm/constants/platformLimitations.js.map +1 -0
  167. package/build/esm/constants.js +17 -0
  168. package/build/esm/constants.js.map +1 -0
  169. package/build/esm/events.js +21 -0
  170. package/build/esm/events.js.map +1 -0
  171. package/build/esm/hooks/useAudioDevices.js +176 -0
  172. package/build/esm/hooks/useAudioDevices.js.map +1 -0
  173. package/build/esm/index.js +23 -0
  174. package/build/esm/index.js.map +1 -0
  175. package/build/esm/trimAudio.js +69 -0
  176. package/build/esm/trimAudio.js.map +1 -0
  177. package/build/esm/useAudioRecorder.js +529 -0
  178. package/build/esm/useAudioRecorder.js.map +1 -0
  179. package/build/esm/utils/BlobFix.js +498 -0
  180. package/build/esm/utils/BlobFix.js.map +1 -0
  181. package/build/esm/utils/audioProcessing.js +133 -0
  182. package/build/esm/utils/audioProcessing.js.map +1 -0
  183. package/build/esm/utils/cleanNativeOptions.js +19 -0
  184. package/build/esm/utils/cleanNativeOptions.js.map +1 -0
  185. package/build/esm/utils/concatenateBuffers.js +21 -0
  186. package/build/esm/utils/concatenateBuffers.js.map +1 -0
  187. package/build/esm/utils/convertPCMToFloat32.js +120 -0
  188. package/build/esm/utils/convertPCMToFloat32.js.map +1 -0
  189. package/build/esm/utils/crc32.js +50 -0
  190. package/build/esm/utils/crc32.js.map +1 -0
  191. package/build/esm/utils/encodingToBitDepth.js +13 -0
  192. package/build/esm/utils/encodingToBitDepth.js.map +1 -0
  193. package/build/esm/utils/getWavFileInfo.js +92 -0
  194. package/build/esm/utils/getWavFileInfo.js.map +1 -0
  195. package/build/esm/utils/writeWavHeader.js +84 -0
  196. package/build/esm/utils/writeWavHeader.js.map +1 -0
  197. package/build/esm/workers/InlineFeaturesExtractor.web.js +291 -0
  198. package/build/esm/workers/InlineFeaturesExtractor.web.js.map +1 -0
  199. package/build/esm/workers/inlineAudioWebWorker.web.js +187 -0
  200. package/build/esm/workers/inlineAudioWebWorker.web.js.map +1 -0
  201. package/build/esm/workers/wasmGlueString.web.js +24 -0
  202. package/build/esm/workers/wasmGlueString.web.js.map +1 -0
  203. package/build/types/AudioAnalysis/AudioAnalysis.types.d.ts +198 -0
  204. package/build/types/AudioAnalysis/AudioAnalysis.types.d.ts.map +1 -0
  205. package/build/types/AudioAnalysis/audioFeaturesWasm.d.ts +24 -0
  206. package/build/types/AudioAnalysis/audioFeaturesWasm.d.ts.map +1 -0
  207. package/build/types/AudioAnalysis/extractAudioAnalysis.d.ts +74 -0
  208. package/build/types/AudioAnalysis/extractAudioAnalysis.d.ts.map +1 -0
  209. package/build/types/AudioAnalysis/extractAudioData.d.ts +3 -0
  210. package/build/types/AudioAnalysis/extractAudioData.d.ts.map +1 -0
  211. package/build/types/AudioAnalysis/extractMelSpectrogram.d.ts +20 -0
  212. package/build/types/AudioAnalysis/extractMelSpectrogram.d.ts.map +1 -0
  213. package/build/types/AudioAnalysis/extractPreview.d.ts +11 -0
  214. package/build/types/AudioAnalysis/extractPreview.d.ts.map +1 -0
  215. package/build/types/AudioAnalysis/extractWaveform.d.ts +8 -0
  216. package/build/types/AudioAnalysis/extractWaveform.d.ts.map +1 -0
  217. package/build/types/AudioAnalysis/melSpectrogramWasm.d.ts +16 -0
  218. package/build/types/AudioAnalysis/melSpectrogramWasm.d.ts.map +1 -0
  219. package/build/types/AudioDeviceManager.d.ts +187 -0
  220. package/build/types/AudioDeviceManager.d.ts.map +1 -0
  221. package/build/types/AudioRecorder.provider.d.ts +11 -0
  222. package/build/types/AudioRecorder.provider.d.ts.map +1 -0
  223. package/build/types/AudioStudio.native.d.ts +3 -0
  224. package/build/types/AudioStudio.native.d.ts.map +1 -0
  225. package/build/types/AudioStudio.types.d.ts +760 -0
  226. package/build/types/AudioStudio.types.d.ts.map +1 -0
  227. package/build/types/AudioStudio.web.d.ts +96 -0
  228. package/build/types/AudioStudio.web.d.ts.map +1 -0
  229. package/build/types/AudioStudioModule.d.ts +3 -0
  230. package/build/types/AudioStudioModule.d.ts.map +1 -0
  231. package/build/types/WebRecorder.web.d.ts +208 -0
  232. package/build/types/WebRecorder.web.d.ts.map +1 -0
  233. package/build/types/constants/platformLimitations.d.ts +40 -0
  234. package/build/types/constants/platformLimitations.d.ts.map +1 -0
  235. package/build/types/constants.d.ts +14 -0
  236. package/build/types/constants.d.ts.map +1 -0
  237. package/build/types/events.d.ts +29 -0
  238. package/build/types/events.d.ts.map +1 -0
  239. package/build/types/hooks/useAudioDevices.d.ts +15 -0
  240. package/build/types/hooks/useAudioDevices.d.ts.map +1 -0
  241. package/build/types/index.d.ts +21 -0
  242. package/build/types/index.d.ts.map +1 -0
  243. package/build/types/trimAudio.d.ts +25 -0
  244. package/build/types/trimAudio.d.ts.map +1 -0
  245. package/build/types/useAudioRecorder.d.ts +22 -0
  246. package/build/types/useAudioRecorder.d.ts.map +1 -0
  247. package/build/types/utils/BlobFix.d.ts +9 -0
  248. package/build/types/utils/BlobFix.d.ts.map +1 -0
  249. package/build/types/utils/audioProcessing.d.ts +24 -0
  250. package/build/types/utils/audioProcessing.d.ts.map +1 -0
  251. package/build/types/utils/cleanNativeOptions.d.ts +15 -0
  252. package/build/types/utils/cleanNativeOptions.d.ts.map +1 -0
  253. package/build/types/utils/concatenateBuffers.d.ts +8 -0
  254. package/build/types/utils/concatenateBuffers.d.ts.map +1 -0
  255. package/build/types/utils/convertPCMToFloat32.d.ts +13 -0
  256. package/build/types/utils/convertPCMToFloat32.d.ts.map +1 -0
  257. package/build/types/utils/crc32.d.ts +7 -0
  258. package/build/types/utils/crc32.d.ts.map +1 -0
  259. package/build/types/utils/encodingToBitDepth.d.ts +5 -0
  260. package/build/types/utils/encodingToBitDepth.d.ts.map +1 -0
  261. package/build/types/utils/getWavFileInfo.d.ts +26 -0
  262. package/build/types/utils/getWavFileInfo.d.ts.map +1 -0
  263. package/build/types/utils/writeWavHeader.d.ts +34 -0
  264. package/build/types/utils/writeWavHeader.d.ts.map +1 -0
  265. package/build/types/workers/InlineFeaturesExtractor.web.d.ts +2 -0
  266. package/build/types/workers/InlineFeaturesExtractor.web.d.ts.map +1 -0
  267. package/build/types/workers/inlineAudioWebWorker.web.d.ts +2 -0
  268. package/build/types/workers/inlineAudioWebWorker.web.d.ts.map +1 -0
  269. package/build/types/workers/wasmGlueString.web.d.ts +2 -0
  270. package/build/types/workers/wasmGlueString.web.d.ts.map +1 -0
  271. package/cpp/AudioFeatures.cpp +274 -0
  272. package/cpp/AudioFeatures.h +85 -0
  273. package/cpp/AudioFeaturesBridge.cpp +146 -0
  274. package/cpp/AudioFeaturesBridge.h +47 -0
  275. package/cpp/MelSpectrogram.cpp +227 -0
  276. package/cpp/MelSpectrogram.h +82 -0
  277. package/cpp/MelSpectrogramBridge.cpp +112 -0
  278. package/cpp/MelSpectrogramBridge.h +33 -0
  279. package/cpp/kiss_fft/COPYING +11 -0
  280. package/cpp/kiss_fft/_kiss_fft_guts.h +167 -0
  281. package/cpp/kiss_fft/kiss_fft.c +424 -0
  282. package/cpp/kiss_fft/kiss_fft.h +160 -0
  283. package/cpp/kiss_fft/kiss_fft_log.h +36 -0
  284. package/cpp/kiss_fft/kiss_fftr.c +155 -0
  285. package/cpp/kiss_fft/kiss_fftr.h +54 -0
  286. package/expo-module.config.json +10 -0
  287. package/ios/AudioAnalysisData.swift +74 -0
  288. package/ios/AudioDeviceManager.swift +670 -0
  289. package/ios/AudioFeaturesWrapper.h +21 -0
  290. package/ios/AudioFeaturesWrapper.mm +63 -0
  291. package/ios/AudioNotificationManager.swift +154 -0
  292. package/ios/AudioProcessingHelpers.swift +797 -0
  293. package/ios/AudioProcessor.swift +1191 -0
  294. package/ios/AudioStreamError.swift +7 -0
  295. package/ios/AudioStreamManager.swift +2369 -0
  296. package/ios/AudioStreamManagerDelegate.swift +16 -0
  297. package/ios/AudioStudio.podspec +39 -0
  298. package/ios/AudioStudioModule.swift +1111 -0
  299. package/ios/AudioStudioTests/AudioFileHandlerTests.swift +338 -0
  300. package/ios/AudioStudioTests/AudioFormatUtilsTests.swift +331 -0
  301. package/ios/AudioStudioTests/AudioTestHelpers.swift +130 -0
  302. package/ios/AudioStudioTests/CompressedOnlyOutputTests.swift +294 -0
  303. package/ios/AudioStudioTests/EventEmissionIntervalTests.swift +105 -0
  304. package/ios/AudioStudioTests/Info.plist +22 -0
  305. package/ios/AudioStudioTests/README.md +39 -0
  306. package/ios/AudioStudioTests/SimpleAudioTest.swift +98 -0
  307. package/ios/AudioStudioTests/TestAudioGenerator.swift +75 -0
  308. package/ios/DataPoint.swift +54 -0
  309. package/ios/DecodingConfig.swift +59 -0
  310. package/ios/FFT.swift +62 -0
  311. package/ios/Features.swift +95 -0
  312. package/ios/ISSUE_IOS.md +68 -0
  313. package/ios/Logger.swift +39 -0
  314. package/ios/MelSpectrogramWrapper.h +30 -0
  315. package/ios/MelSpectrogramWrapper.mm +97 -0
  316. package/ios/NotificationExtension.swift +15 -0
  317. package/ios/RecordingResult.swift +22 -0
  318. package/ios/RecordingSettings.swift +311 -0
  319. package/ios/WaveformExtractor.swift +105 -0
  320. package/ios/tests/README.md +41 -0
  321. package/ios/tests/integration/buffer_and_fallback_test.swift +178 -0
  322. package/ios/tests/integration/buffer_duration_test.swift +185 -0
  323. package/ios/tests/integration/compressed_only_output_test.swift +271 -0
  324. package/ios/tests/integration/output_control_test.swift +322 -0
  325. package/ios/tests/integration/run_integration_tests.sh +37 -0
  326. package/ios/tests/opus_support_test_macos.swift +154 -0
  327. package/ios/tests/standalone/audio_processing_test.swift +144 -0
  328. package/ios/tests/standalone/audio_recording_test.swift +277 -0
  329. package/ios/tests/standalone/audio_streaming_test.swift +249 -0
  330. package/ios/tests/standalone/standalone_test.swift +144 -0
  331. package/package.json +146 -0
  332. package/plugin/build/index.cjs +194 -0
  333. package/plugin/build/index.d.cts +22 -0
  334. package/plugin/build/index.js +194 -0
  335. package/plugin/src/index.ts +285 -0
  336. package/plugin/tsconfig.json +10 -0
  337. package/plugin/tsconfig.tsbuildinfo +1 -0
  338. package/prebuilt/wasm/mel-spectrogram.js +18 -0
  339. package/src/AudioAnalysis/AudioAnalysis.types.ts +226 -0
  340. package/src/AudioAnalysis/audio-features-wasm.d.ts +37 -0
  341. package/src/AudioAnalysis/audioFeaturesWasm.ts +200 -0
  342. package/src/AudioAnalysis/extractAudioAnalysis.ts +350 -0
  343. package/src/AudioAnalysis/extractAudioData.ts +17 -0
  344. package/src/AudioAnalysis/extractMelSpectrogram.ts +140 -0
  345. package/src/AudioAnalysis/extractPreview.ts +34 -0
  346. package/src/AudioAnalysis/extractWaveform.ts +22 -0
  347. package/src/AudioAnalysis/mel-spectrogram-wasm.d.ts +48 -0
  348. package/src/AudioAnalysis/melSpectrogramWasm.ts +179 -0
  349. package/src/AudioDeviceManager.ts +800 -0
  350. package/src/AudioRecorder.provider.tsx +57 -0
  351. package/src/AudioStudio.native.ts +6 -0
  352. package/src/AudioStudio.types.ts +899 -0
  353. package/src/AudioStudio.web.ts +911 -0
  354. package/src/AudioStudioModule.ts +984 -0
  355. package/src/WebRecorder.web.ts +1114 -0
  356. package/src/constants/platformLimitations.ts +118 -0
  357. package/src/constants.ts +21 -0
  358. package/src/events.ts +63 -0
  359. package/src/hooks/useAudioDevices.ts +213 -0
  360. package/src/index.ts +67 -0
  361. package/src/trimAudio.ts +94 -0
  362. package/src/types/crc-32.d.ts +9 -0
  363. package/src/useAudioRecorder.tsx +784 -0
  364. package/src/utils/BlobFix.ts +561 -0
  365. package/src/utils/audioProcessing.ts +205 -0
  366. package/src/utils/cleanNativeOptions.ts +18 -0
  367. package/src/utils/concatenateBuffers.ts +24 -0
  368. package/src/utils/convertPCMToFloat32.ts +170 -0
  369. package/src/utils/crc32.ts +59 -0
  370. package/src/utils/encodingToBitDepth.ts +18 -0
  371. package/src/utils/getWavFileInfo.ts +132 -0
  372. package/src/utils/writeWavHeader.ts +115 -0
  373. package/src/workers/InlineFeaturesExtractor.web.tsx +291 -0
  374. package/src/workers/inlineAudioWebWorker.web.tsx +186 -0
  375. 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
+ }