@siteed/audio-studio 3.1.0 → 3.2.0-beta.1

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 (71) hide show
  1. package/CHANGELOG.md +30 -1
  2. package/README.md +97 -50
  3. package/android/src/androidTest/java/net/siteed/audiostudio/AudioFinalMetadataContractInstrumentedTest.kt +190 -0
  4. package/android/src/androidTest/java/net/siteed/audiostudio/AudioRecorderInstrumentedTest.kt +29 -83
  5. package/android/src/androidTest/java/net/siteed/audiostudio/AudioRecorderPerformanceInstrumentedTest.kt +17 -1
  6. package/android/src/androidTest/java/net/siteed/audiostudio/OpusRangeDecodeRegressionInstrumentedTest.kt +186 -0
  7. package/android/src/main/java/net/siteed/audiostudio/AudioProcessor.kt +473 -380
  8. package/android/src/main/java/net/siteed/audiostudio/AudioStreamDecoder.kt +640 -0
  9. package/android/src/main/java/net/siteed/audiostudio/AudioStudioModule.kt +187 -13
  10. package/android/src/main/java/net/siteed/audiostudio/AudioTrimmer.kt +174 -212
  11. package/android/src/main/java/net/siteed/audiostudio/Constants.kt +4 -0
  12. package/build/cjs/AudioAnalysis/AudioAnalysis.types.js.map +1 -1
  13. package/build/cjs/AudioAnalysis/extractPreview.js +92 -15
  14. package/build/cjs/AudioAnalysis/extractPreview.js.map +1 -1
  15. package/build/cjs/AudioAnalysis/extractPreviewBars.js +134 -0
  16. package/build/cjs/AudioAnalysis/extractPreviewBars.js.map +1 -0
  17. package/build/cjs/errors/AudioExtractionError.js +127 -0
  18. package/build/cjs/errors/AudioExtractionError.js.map +1 -0
  19. package/build/cjs/errors/AudioStreamError.js +152 -0
  20. package/build/cjs/errors/AudioStreamError.js.map +1 -0
  21. package/build/cjs/errors/AudioStreamError.test.js +61 -0
  22. package/build/cjs/errors/AudioStreamError.test.js.map +1 -0
  23. package/build/cjs/index.js +12 -1
  24. package/build/cjs/index.js.map +1 -1
  25. package/build/cjs/streamAudioData.js +467 -0
  26. package/build/cjs/streamAudioData.js.map +1 -0
  27. package/build/esm/AudioAnalysis/AudioAnalysis.types.js.map +1 -1
  28. package/build/esm/AudioAnalysis/extractPreview.js +92 -15
  29. package/build/esm/AudioAnalysis/extractPreview.js.map +1 -1
  30. package/build/esm/AudioAnalysis/extractPreviewBars.js +128 -0
  31. package/build/esm/AudioAnalysis/extractPreviewBars.js.map +1 -0
  32. package/build/esm/errors/AudioExtractionError.js +122 -0
  33. package/build/esm/errors/AudioExtractionError.js.map +1 -0
  34. package/build/esm/errors/AudioStreamError.js +147 -0
  35. package/build/esm/errors/AudioStreamError.js.map +1 -0
  36. package/build/esm/errors/AudioStreamError.test.js +59 -0
  37. package/build/esm/errors/AudioStreamError.test.js.map +1 -0
  38. package/build/esm/index.js +5 -1
  39. package/build/esm/index.js.map +1 -1
  40. package/build/esm/streamAudioData.js +460 -0
  41. package/build/esm/streamAudioData.js.map +1 -0
  42. package/build/types/AudioAnalysis/AudioAnalysis.types.d.ts +79 -0
  43. package/build/types/AudioAnalysis/AudioAnalysis.types.d.ts.map +1 -1
  44. package/build/types/AudioAnalysis/extractPreview.d.ts +2 -2
  45. package/build/types/AudioAnalysis/extractPreview.d.ts.map +1 -1
  46. package/build/types/AudioAnalysis/extractPreviewBars.d.ts +12 -0
  47. package/build/types/AudioAnalysis/extractPreviewBars.d.ts.map +1 -0
  48. package/build/types/errors/AudioExtractionError.d.ts +24 -0
  49. package/build/types/errors/AudioExtractionError.d.ts.map +1 -0
  50. package/build/types/errors/AudioStreamError.d.ts +25 -0
  51. package/build/types/errors/AudioStreamError.d.ts.map +1 -0
  52. package/build/types/errors/AudioStreamError.test.d.ts +2 -0
  53. package/build/types/errors/AudioStreamError.test.d.ts.map +1 -0
  54. package/build/types/index.d.ts +8 -1
  55. package/build/types/index.d.ts.map +1 -1
  56. package/build/types/streamAudioData.d.ts +114 -0
  57. package/build/types/streamAudioData.d.ts.map +1 -0
  58. package/ios/AudioProcessingHelpers.swift +10 -5
  59. package/ios/AudioProcessor.swift +99 -0
  60. package/ios/AudioStreamDecoder.swift +523 -0
  61. package/ios/AudioStudioModule.swift +210 -3
  62. package/ios/AudioStudioTests/AudioStreamDecoderTests.swift +128 -0
  63. package/package.json +7 -7
  64. package/src/AudioAnalysis/AudioAnalysis.types.ts +82 -0
  65. package/src/AudioAnalysis/extractPreview.ts +118 -17
  66. package/src/AudioAnalysis/extractPreviewBars.ts +193 -0
  67. package/src/errors/AudioExtractionError.ts +167 -0
  68. package/src/errors/AudioStreamError.test.ts +65 -0
  69. package/src/errors/AudioStreamError.ts +185 -0
  70. package/src/index.ts +34 -0
  71. package/src/streamAudioData.ts +654 -0
@@ -0,0 +1,186 @@
1
+ package net.siteed.audiostudio
2
+
3
+ import android.Manifest
4
+ import android.os.Build
5
+ import androidx.test.ext.junit.runners.AndroidJUnit4
6
+ import androidx.test.platform.app.InstrumentationRegistry
7
+ import androidx.test.rule.GrantPermissionRule
8
+ import expo.modules.kotlin.Promise
9
+ import org.junit.After
10
+ import org.junit.Assert.assertEquals
11
+ import org.junit.Assert.assertTrue
12
+ import org.junit.Assume.assumeTrue
13
+ import org.junit.Before
14
+ import org.junit.Rule
15
+ import org.junit.Test
16
+ import org.junit.runner.RunWith
17
+ import java.io.File
18
+ import java.net.URI
19
+ import java.util.concurrent.CountDownLatch
20
+ import java.util.concurrent.TimeUnit
21
+ import kotlin.math.abs
22
+
23
+ /**
24
+ * Device-only regression skeleton for compressed Opus range decode. It records a
25
+ * short Opus-only file, then verifies the range loader reports metadata and byte
26
+ * length from final target-converted PCM rather than source decoder output.
27
+ */
28
+ @RunWith(AndroidJUnit4::class)
29
+ class OpusRangeDecodeRegressionInstrumentedTest {
30
+ @get:Rule
31
+ val grantPermissionRule: GrantPermissionRule = GrantPermissionRule.grant(Manifest.permission.RECORD_AUDIO)
32
+
33
+ private val context = InstrumentationRegistry.getInstrumentation().targetContext
34
+ private val filesDir = context.filesDir
35
+ private lateinit var audioRecorderManager: AudioRecorderManager
36
+
37
+ @Before
38
+ fun setUp() {
39
+ assumeTrue("Opus MediaCodec recording requires Android Q+", Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q)
40
+ audioRecorderManager = AudioRecorderManager.initialize(
41
+ context = context,
42
+ filesDir = filesDir,
43
+ permissionUtils = PermissionUtils(context),
44
+ audioDataEncoder = AudioDataEncoder(),
45
+ eventSender = object : EventSender {
46
+ override fun sendExpoEvent(eventName: String, params: android.os.Bundle) = Unit
47
+ },
48
+ enablePhoneStateHandling = false,
49
+ enableBackgroundAudio = false
50
+ )
51
+ cleanupGeneratedAudio()
52
+ }
53
+
54
+ @After
55
+ fun tearDown() {
56
+ if (::audioRecorderManager.isInitialized && audioRecorderManager.isRecording) {
57
+ runCatching { stopRecordingSync() }
58
+ }
59
+ AudioRecorderManager.destroy()
60
+ cleanupGeneratedAudio()
61
+ }
62
+
63
+ @Test
64
+ fun loadAudioRange_returnsTargetPcmMetadataForGeneratedOpusRecording() {
65
+ startRecordingSync(
66
+ mapOf(
67
+ "sampleRate" to 48_000,
68
+ "channels" to 1,
69
+ "encoding" to "pcm_16bit",
70
+ "interval" to 100,
71
+ "showNotification" to false,
72
+ "output" to mapOf(
73
+ "primary" to mapOf("enabled" to false),
74
+ "compressed" to mapOf(
75
+ "enabled" to true,
76
+ "format" to "opus"
77
+ )
78
+ )
79
+ )
80
+ )
81
+ Thread.sleep(RECORDING_DURATION_MS + 250L)
82
+ val opusFile = resolveCompressedFile(stopRecordingSync())
83
+
84
+ val audioData = AudioProcessor(filesDir).loadAudioRange(
85
+ fileUri = opusFile.absolutePath,
86
+ startTimeMs = 0,
87
+ endTimeMs = REQUESTED_RANGE_MS,
88
+ config = DecodingConfig(
89
+ targetSampleRate = TARGET_SAMPLE_RATE,
90
+ targetChannels = TARGET_CHANNELS,
91
+ targetBitDepth = TARGET_BIT_DEPTH,
92
+ normalizeAudio = false
93
+ )
94
+ )
95
+
96
+ val decoded = requireNotNull(audioData) { "Opus range should decode without BufferOverflowException" }
97
+ assertEquals("sampleRate should describe final converted PCM", TARGET_SAMPLE_RATE, decoded.sampleRate)
98
+ assertEquals("channels should describe final converted PCM", TARGET_CHANNELS, decoded.channels)
99
+ assertEquals("bitDepth should describe final converted PCM", TARGET_BIT_DEPTH, decoded.bitDepth)
100
+
101
+ val expectedBytes = TARGET_SAMPLE_RATE * TARGET_CHANNELS * (TARGET_BIT_DEPTH / 8) * REQUESTED_RANGE_MS / 1_000
102
+ val toleranceBytes = TARGET_SAMPLE_RATE * TARGET_CHANNELS * (TARGET_BIT_DEPTH / 8) / 2
103
+ assertTrue(
104
+ "final PCM byte length should be near requested target duration: expected=$expectedBytes actual=${decoded.data.size}",
105
+ abs(decoded.data.size - expectedBytes) <= toleranceBytes
106
+ )
107
+ }
108
+
109
+ private fun startRecordingSync(recordingOptions: Map<String, Any?>) {
110
+ val latch = CountDownLatch(1)
111
+ var rejected: String? = null
112
+ audioRecorderManager.startRecording(recordingOptions, object : Promise {
113
+ override fun resolve(value: Any?) {
114
+ latch.countDown()
115
+ }
116
+
117
+ override fun reject(code: String, message: String?, cause: Throwable?) {
118
+ rejected = "$code - $message"
119
+ latch.countDown()
120
+ }
121
+ })
122
+ assertTrue("Recording should start within 2 seconds", latch.await(2, TimeUnit.SECONDS))
123
+ rejected?.let { throw AssertionError("Recording start failed: $it") }
124
+ }
125
+
126
+ private fun stopRecordingSync(): Map<String, Any?> {
127
+ val latch = CountDownLatch(1)
128
+ var result: Map<String, Any?>? = null
129
+ var rejected: String? = null
130
+ audioRecorderManager.stopRecording(object : Promise {
131
+ override fun resolve(value: Any?) {
132
+ result = when (value) {
133
+ is android.os.Bundle -> bundleToMap(value)
134
+ is Map<*, *> -> value.entries.associate { it.key.toString() to it.value }
135
+ else -> throw AssertionError("Unexpected stop result type: ${value?.javaClass?.name}")
136
+ }
137
+ latch.countDown()
138
+ }
139
+
140
+ override fun reject(code: String, message: String?, cause: Throwable?) {
141
+ rejected = "$code - $message"
142
+ latch.countDown()
143
+ }
144
+ })
145
+ assertTrue("Recording should stop within 2 seconds", latch.await(2, TimeUnit.SECONDS))
146
+ rejected?.let { throw AssertionError("Recording stop failed: $it") }
147
+ return result ?: throw AssertionError("Stop result should not be null")
148
+ }
149
+
150
+ private fun resolveCompressedFile(result: Map<String, Any?>): File {
151
+ val compression = result["compression"]
152
+ val compressionMap = when (compression) {
153
+ is android.os.Bundle -> bundleToMap(compression)
154
+ is Map<*, *> -> compression.entries.associate { it.key.toString() to it.value }
155
+ else -> emptyMap()
156
+ }
157
+ val uri = compressionMap["compressedFileUri"] as? String
158
+ ?: throw AssertionError("Compressed Opus URI should be present in stop result: $result")
159
+ val file = when {
160
+ uri.startsWith("file://") || uri.startsWith("file:") -> File(URI(uri))
161
+ else -> File(uri)
162
+ }
163
+ assertTrue("Generated Opus file should exist: ${file.absolutePath}", file.exists())
164
+ assertTrue("Generated file should be Opus: ${file.name}", file.name.endsWith(".opus"))
165
+ return file
166
+ }
167
+
168
+ private fun bundleToMap(bundle: android.os.Bundle): Map<String, Any?> =
169
+ bundle.keySet().associateWith { key -> bundle.get(key) }
170
+
171
+ private fun cleanupGeneratedAudio() {
172
+ filesDir.listFiles()?.forEach { file ->
173
+ if (file.name.startsWith("recording_") && file.extension in setOf("opus", "wav", "m4a", "aac")) {
174
+ file.delete()
175
+ }
176
+ }
177
+ }
178
+
179
+ companion object {
180
+ private const val RECORDING_DURATION_MS = 2_000L
181
+ private const val REQUESTED_RANGE_MS = 2_000L
182
+ private const val TARGET_SAMPLE_RATE = 16_000
183
+ private const val TARGET_CHANNELS = 1
184
+ private const val TARGET_BIT_DEPTH = 16
185
+ }
186
+ }