@siteed/audio-studio 3.2.0-beta.1 → 3.2.1-beta.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 +356 -5
- package/android/src/main/java/net/siteed/audiostudio/AudioRecorderManager.kt +12 -12
- package/android/src/main/java/net/siteed/audiostudio/AudioRecordingService.kt +1 -1
- package/android/src/main/java/net/siteed/audiostudio/AudioStreamDecoder.kt +306 -94
- package/android/src/main/java/net/siteed/audiostudio/AudioStudioModule.kt +43 -10
- package/android/src/main/java/net/siteed/audiostudio/RecordingActionReceiver.kt +1 -1
- package/build/cjs/AudioRecorder.provider.js +3 -37
- package/build/cjs/AudioRecorder.provider.js.map +1 -1
- package/build/cjs/errors/AudioStreamError.js +9 -0
- package/build/cjs/errors/AudioStreamError.js.map +1 -1
- package/build/cjs/errors/AudioStreamError.test.js +22 -1
- package/build/cjs/errors/AudioStreamError.test.js.map +1 -1
- package/build/cjs/streamAudioData.js +99 -32
- package/build/cjs/streamAudioData.js.map +1 -1
- package/build/cjs/utils/audioProcessing.js +14 -10
- package/build/cjs/utils/audioProcessing.js.map +1 -1
- package/build/esm/AudioRecorder.provider.js +3 -4
- package/build/esm/AudioRecorder.provider.js.map +1 -1
- package/build/esm/errors/AudioStreamError.js +9 -0
- package/build/esm/errors/AudioStreamError.js.map +1 -1
- package/build/esm/errors/AudioStreamError.test.js +22 -1
- package/build/esm/errors/AudioStreamError.test.js.map +1 -1
- package/build/esm/streamAudioData.js +99 -32
- package/build/esm/streamAudioData.js.map +1 -1
- package/build/esm/utils/audioProcessing.js +14 -10
- package/build/esm/utils/audioProcessing.js.map +1 -1
- package/build/types/errors/AudioStreamError.d.ts.map +1 -1
- package/build/types/streamAudioData.d.ts +5 -0
- package/build/types/streamAudioData.d.ts.map +1 -1
- package/build/types/utils/audioProcessing.d.ts +2 -2
- package/build/types/utils/audioProcessing.d.ts.map +1 -1
- package/ios/AudioStreamDecoder.swift +191 -100
- package/ios/AudioStudio.podspec +1 -1
- package/ios/AudioStudioModule.swift +48 -9
- package/package.json +32 -15
- package/plugin/tsconfig.json +8 -2
- package/src/errors/AudioStreamError.test.ts +29 -2
- package/src/errors/AudioStreamError.ts +14 -0
- package/src/streamAudioData.ts +146 -42
- package/src/utils/audioProcessing.ts +25 -14
- 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/AudioFinalMetadataContractInstrumentedTest.kt +0 -190
- package/android/src/androidTest/java/net/siteed/audiostudio/AudioProcessorInstrumentedTest.kt +0 -197
- package/android/src/androidTest/java/net/siteed/audiostudio/AudioRecorderInstrumentedTest.kt +0 -487
- package/android/src/androidTest/java/net/siteed/audiostudio/AudioRecorderPerformanceInstrumentedTest.kt +0 -250
- package/android/src/androidTest/java/net/siteed/audiostudio/OpusRangeDecodeRegressionInstrumentedTest.kt +0 -186
- package/android/src/androidTest/java/net/siteed/audiostudio/integration/AudioFocusStrategyIntegrationTest.kt +0 -332
- package/android/src/androidTest/java/net/siteed/audiostudio/integration/BufferDurationIntegrationTest.kt +0 -324
- package/android/src/androidTest/java/net/siteed/audiostudio/integration/CompressedOnlyOutputTest.kt +0 -253
- package/android/src/androidTest/java/net/siteed/audiostudio/integration/DeviceDisconnectionFallbackTest.kt +0 -218
- package/android/src/androidTest/java/net/siteed/audiostudio/integration/EventEmissionIntervalTest.kt +0 -120
- package/android/src/androidTest/java/net/siteed/audiostudio/integration/M4aFormatTest.kt +0 -345
- package/android/src/androidTest/java/net/siteed/audiostudio/integration/OutputControlIntegrationTest.kt +0 -340
- package/android/src/androidTest/java/net/siteed/audiostudio/integration/PcmStreamingDurationTest.kt +0 -252
- package/android/src/androidTest/java/net/siteed/audiostudio/integration/README.md +0 -95
- package/android/src/androidTest/java/net/siteed/audiostudio/integration/run_integration_tests.sh +0 -43
- package/android/src/test/java/net/siteed/audiostudio/AndroidCallStateTest.kt +0 -37
- package/android/src/test/java/net/siteed/audiostudio/AndroidEventEmitterTest.kt +0 -28
- package/android/src/test/java/net/siteed/audiostudio/AudioFileHandlerTest.kt +0 -279
- package/android/src/test/java/net/siteed/audiostudio/AudioFocusStrategyTest.kt +0 -249
- package/android/src/test/java/net/siteed/audiostudio/AudioFormatTest.kt +0 -151
- package/android/src/test/java/net/siteed/audiostudio/AudioFormatUtilsTest.kt +0 -273
- package/android/src/test/java/net/siteed/audiostudio/DeviceDisconnectionFallbackUnitTest.kt +0 -140
- package/android/src/test/java/net/siteed/audiostudio/InterruptionAutoResumePolicyTest.kt +0 -49
- package/android/src/test/resources/chorus.wav +0 -0
- package/android/src/test/resources/generate_test_audio.py +0 -94
- 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/ios/AudioStudioTests/AudioFileHandlerTests.swift +0 -338
- package/ios/AudioStudioTests/AudioFormatUtilsTests.swift +0 -331
- package/ios/AudioStudioTests/AudioStreamDecoderTests.swift +0 -128
- package/ios/AudioStudioTests/AudioTestHelpers.swift +0 -130
- package/ios/AudioStudioTests/CompressedOnlyOutputTests.swift +0 -334
- package/ios/AudioStudioTests/EventEmissionIntervalTests.swift +0 -105
- package/ios/AudioStudioTests/Info.plist +0 -22
- package/ios/AudioStudioTests/README.md +0 -39
- package/ios/AudioStudioTests/SimpleAudioTest.swift +0 -98
- package/ios/AudioStudioTests/TestAudioGenerator.swift +0 -75
- package/ios/tests/README.md +0 -41
- package/ios/tests/integration/buffer_and_fallback_test.swift +0 -178
- package/ios/tests/integration/buffer_duration_test.swift +0 -185
- package/ios/tests/integration/compressed_only_output_test.swift +0 -271
- package/ios/tests/integration/output_control_test.swift +0 -322
- package/ios/tests/integration/run_integration_tests.sh +0 -37
- package/ios/tests/opus_support_test_macos.swift +0 -154
- package/ios/tests/standalone/audio_processing_test.swift +0 -144
- package/ios/tests/standalone/audio_recording_test.swift +0 -277
- package/ios/tests/standalone/audio_streaming_test.swift +0 -249
- package/ios/tests/standalone/standalone_test.swift +0 -144
|
@@ -1,190 +0,0 @@
|
|
|
1
|
-
package net.siteed.audiostudio
|
|
2
|
-
|
|
3
|
-
import android.content.Context
|
|
4
|
-
import android.net.Uri
|
|
5
|
-
import androidx.test.ext.junit.runners.AndroidJUnit4
|
|
6
|
-
import androidx.test.platform.app.InstrumentationRegistry
|
|
7
|
-
import org.junit.After
|
|
8
|
-
import org.junit.Assert.assertEquals
|
|
9
|
-
import org.junit.Assert.assertTrue
|
|
10
|
-
import org.junit.Before
|
|
11
|
-
import org.junit.Test
|
|
12
|
-
import org.junit.runner.RunWith
|
|
13
|
-
import java.io.File
|
|
14
|
-
import java.nio.ByteBuffer
|
|
15
|
-
import java.nio.ByteOrder
|
|
16
|
-
|
|
17
|
-
/**
|
|
18
|
-
* Regression coverage for Android range processing where the final PCM bytes,
|
|
19
|
-
* returned metadata, and WAV headers must all describe the post-conversion data.
|
|
20
|
-
*/
|
|
21
|
-
@RunWith(AndroidJUnit4::class)
|
|
22
|
-
class AudioFinalMetadataContractInstrumentedTest {
|
|
23
|
-
private lateinit var context: Context
|
|
24
|
-
private lateinit var filesDir: File
|
|
25
|
-
private lateinit var audioProcessor: AudioProcessor
|
|
26
|
-
|
|
27
|
-
@Before
|
|
28
|
-
fun setUp() {
|
|
29
|
-
context = InstrumentationRegistry.getInstrumentation().targetContext
|
|
30
|
-
filesDir = context.filesDir
|
|
31
|
-
audioProcessor = AudioProcessor(filesDir)
|
|
32
|
-
copyAssetToFilesDir("chorus.wav")
|
|
33
|
-
}
|
|
34
|
-
|
|
35
|
-
@After
|
|
36
|
-
fun tearDown() {
|
|
37
|
-
filesDir.listFiles()?.forEach { file ->
|
|
38
|
-
if (file.name.startsWith("final_metadata_contract_") || file.name == "chorus.wav") {
|
|
39
|
-
file.delete()
|
|
40
|
-
}
|
|
41
|
-
}
|
|
42
|
-
}
|
|
43
|
-
|
|
44
|
-
@Test
|
|
45
|
-
fun loadAudioRange_returnsMetadataFromFinalConvertedWavBytes() {
|
|
46
|
-
val audioData = audioProcessor.loadAudioRange(
|
|
47
|
-
fileUri = File(filesDir, "chorus.wav").absolutePath,
|
|
48
|
-
startTimeMs = 0,
|
|
49
|
-
endTimeMs = ONE_SECOND_MS,
|
|
50
|
-
config = DecodingConfig(
|
|
51
|
-
targetSampleRate = TARGET_SAMPLE_RATE,
|
|
52
|
-
targetChannels = TARGET_CHANNELS,
|
|
53
|
-
targetBitDepth = TARGET_BIT_DEPTH,
|
|
54
|
-
normalizeAudio = false
|
|
55
|
-
)
|
|
56
|
-
)
|
|
57
|
-
|
|
58
|
-
val converted = requireNotNull(audioData) { "Audio range should load" }
|
|
59
|
-
val bytesPerTargetFrame = TARGET_CHANNELS * BYTES_PER_TARGET_SAMPLE
|
|
60
|
-
val finalFrameCount = converted.data.size / bytesPerTargetFrame
|
|
61
|
-
val durationFromFinalBytes = finalFrameCount * 1_000L / TARGET_SAMPLE_RATE
|
|
62
|
-
|
|
63
|
-
assertEquals("sampleRate should describe final converted bytes", TARGET_SAMPLE_RATE, converted.sampleRate)
|
|
64
|
-
assertEquals("channels should describe final converted bytes", TARGET_CHANNELS, converted.channels)
|
|
65
|
-
assertEquals("bitDepth should describe final converted bytes", TARGET_BIT_DEPTH, converted.bitDepth)
|
|
66
|
-
assertEquals("final PCM data must end on a target frame boundary", 0, converted.data.size % bytesPerTargetFrame)
|
|
67
|
-
assertEquals(
|
|
68
|
-
"duration should be derived from actual final PCM bytes",
|
|
69
|
-
durationFromFinalBytes,
|
|
70
|
-
converted.durationMs
|
|
71
|
-
)
|
|
72
|
-
assertTrue(
|
|
73
|
-
"duration should remain close to requested range: ${converted.durationMs}ms",
|
|
74
|
-
kotlin.math.abs(converted.durationMs - ONE_SECOND_MS) <= 25
|
|
75
|
-
)
|
|
76
|
-
}
|
|
77
|
-
|
|
78
|
-
@Test
|
|
79
|
-
fun loadAudioRange_alignsConvertedWavBytesToTargetFrameSize() {
|
|
80
|
-
val audioData = audioProcessor.loadAudioRange(
|
|
81
|
-
fileUri = File(filesDir, "chorus.wav").absolutePath,
|
|
82
|
-
startTimeMs = 0,
|
|
83
|
-
endTimeMs = ONE_SECOND_MS,
|
|
84
|
-
config = DecodingConfig(
|
|
85
|
-
targetSampleRate = TARGET_SAMPLE_RATE,
|
|
86
|
-
targetChannels = TARGET_CHANNELS,
|
|
87
|
-
targetBitDepth = TARGET_BIT_DEPTH,
|
|
88
|
-
normalizeAudio = false
|
|
89
|
-
)
|
|
90
|
-
)
|
|
91
|
-
|
|
92
|
-
val converted = requireNotNull(audioData) { "Audio range should load" }
|
|
93
|
-
val bytesPerTargetFrame = TARGET_CHANNELS * BYTES_PER_TARGET_SAMPLE
|
|
94
|
-
|
|
95
|
-
assertEquals("final PCM data must end on a target frame boundary", 0, converted.data.size % bytesPerTargetFrame)
|
|
96
|
-
}
|
|
97
|
-
|
|
98
|
-
@Test
|
|
99
|
-
fun trimAudio_writesWavHeaderFromFinalConvertedBytes() {
|
|
100
|
-
val outputFileName = "final_metadata_contract_processor_trim.wav"
|
|
101
|
-
val trimmed = audioProcessor.trimAudio(
|
|
102
|
-
fileUri = File(filesDir, "chorus.wav").absolutePath,
|
|
103
|
-
startTimeMs = 0,
|
|
104
|
-
endTimeMs = ONE_SECOND_MS,
|
|
105
|
-
config = DecodingConfig(
|
|
106
|
-
targetSampleRate = TARGET_SAMPLE_RATE,
|
|
107
|
-
targetChannels = TARGET_CHANNELS,
|
|
108
|
-
targetBitDepth = TARGET_BIT_DEPTH,
|
|
109
|
-
normalizeAudio = false
|
|
110
|
-
),
|
|
111
|
-
outputFileName = outputFileName
|
|
112
|
-
)
|
|
113
|
-
|
|
114
|
-
requireNotNull(trimmed) { "Trimmed audio should be returned" }
|
|
115
|
-
val header = readWavHeader(File(filesDir, outputFileName))
|
|
116
|
-
|
|
117
|
-
assertEquals("WAV header sample rate should be target sample rate", TARGET_SAMPLE_RATE, header.sampleRate)
|
|
118
|
-
assertEquals("WAV header channels should be target channels", TARGET_CHANNELS, header.channels)
|
|
119
|
-
assertEquals("WAV header bit depth should be target bit depth", TARGET_BIT_DEPTH, header.bitDepth)
|
|
120
|
-
assertEquals("WAV data chunk should match returned final PCM bytes", trimmed.data.size, header.dataSize)
|
|
121
|
-
}
|
|
122
|
-
|
|
123
|
-
@Test
|
|
124
|
-
fun audioTrimmer_honorsJsNumberOutputFormatWhenWritingWavHeader() {
|
|
125
|
-
val trimmer = AudioTrimmer(context, AudioFileHandler(filesDir))
|
|
126
|
-
val result = trimmer.trimAudio(
|
|
127
|
-
fileUri = Uri.fromFile(File(filesDir, "chorus.wav")).toString(),
|
|
128
|
-
startTimeMs = 0,
|
|
129
|
-
endTimeMs = ONE_SECOND_MS,
|
|
130
|
-
outputFileName = "final_metadata_contract_audio_trimmer",
|
|
131
|
-
outputFormat = mapOf(
|
|
132
|
-
"format" to "wav",
|
|
133
|
-
"sampleRate" to TARGET_SAMPLE_RATE.toDouble(),
|
|
134
|
-
"channels" to TARGET_CHANNELS.toDouble(),
|
|
135
|
-
"bitDepth" to TARGET_BIT_DEPTH.toDouble()
|
|
136
|
-
)
|
|
137
|
-
)
|
|
138
|
-
|
|
139
|
-
val outputPath = result["uri"] as String
|
|
140
|
-
val header = readWavHeader(File(outputPath))
|
|
141
|
-
|
|
142
|
-
assertEquals("Double sampleRate option should drive WAV header", TARGET_SAMPLE_RATE, header.sampleRate)
|
|
143
|
-
assertEquals("Double channels option should drive WAV header", TARGET_CHANNELS, header.channels)
|
|
144
|
-
assertEquals("Double bitDepth option should drive WAV header", TARGET_BIT_DEPTH, header.bitDepth)
|
|
145
|
-
}
|
|
146
|
-
|
|
147
|
-
private fun copyAssetToFilesDir(fileName: String) {
|
|
148
|
-
context.assets.open(fileName).use { input ->
|
|
149
|
-
File(filesDir, fileName).outputStream().use { output ->
|
|
150
|
-
input.copyTo(output)
|
|
151
|
-
}
|
|
152
|
-
}
|
|
153
|
-
}
|
|
154
|
-
|
|
155
|
-
private fun readWavHeader(file: File): WavHeader {
|
|
156
|
-
assertTrue("WAV file should exist: ${file.absolutePath}", file.exists())
|
|
157
|
-
val bytes = file.inputStream().use { it.readNBytes(44) }
|
|
158
|
-
assertEquals("RIFF", String(bytes.sliceArray(0..3)))
|
|
159
|
-
assertEquals("WAVE", String(bytes.sliceArray(8..11)))
|
|
160
|
-
assertEquals("data", String(bytes.sliceArray(36..39)))
|
|
161
|
-
|
|
162
|
-
return WavHeader(
|
|
163
|
-
channels = bytes.shortAt(22),
|
|
164
|
-
sampleRate = bytes.intAt(24),
|
|
165
|
-
bitDepth = bytes.shortAt(34),
|
|
166
|
-
dataSize = bytes.intAt(40)
|
|
167
|
-
)
|
|
168
|
-
}
|
|
169
|
-
|
|
170
|
-
private fun ByteArray.shortAt(offset: Int): Int =
|
|
171
|
-
ByteBuffer.wrap(this, offset, 2).order(ByteOrder.LITTLE_ENDIAN).short.toInt()
|
|
172
|
-
|
|
173
|
-
private fun ByteArray.intAt(offset: Int): Int =
|
|
174
|
-
ByteBuffer.wrap(this, offset, 4).order(ByteOrder.LITTLE_ENDIAN).int
|
|
175
|
-
|
|
176
|
-
private data class WavHeader(
|
|
177
|
-
val channels: Int,
|
|
178
|
-
val sampleRate: Int,
|
|
179
|
-
val bitDepth: Int,
|
|
180
|
-
val dataSize: Int
|
|
181
|
-
)
|
|
182
|
-
|
|
183
|
-
companion object {
|
|
184
|
-
private const val ONE_SECOND_MS = 1_000L
|
|
185
|
-
private const val TARGET_SAMPLE_RATE = 16_000
|
|
186
|
-
private const val TARGET_CHANNELS = 2
|
|
187
|
-
private const val TARGET_BIT_DEPTH = 16
|
|
188
|
-
private const val BYTES_PER_TARGET_SAMPLE = TARGET_BIT_DEPTH / 8
|
|
189
|
-
}
|
|
190
|
-
}
|
package/android/src/androidTest/java/net/siteed/audiostudio/AudioProcessorInstrumentedTest.kt
DELETED
|
@@ -1,197 +0,0 @@
|
|
|
1
|
-
package net.siteed.audiostudio
|
|
2
|
-
|
|
3
|
-
import androidx.test.ext.junit.runners.AndroidJUnit4
|
|
4
|
-
import androidx.test.platform.app.InstrumentationRegistry
|
|
5
|
-
import org.junit.Test
|
|
6
|
-
import org.junit.Assert.*
|
|
7
|
-
import org.junit.Before
|
|
8
|
-
import org.junit.After
|
|
9
|
-
import org.junit.runner.RunWith
|
|
10
|
-
import java.io.File
|
|
11
|
-
import kotlin.math.abs
|
|
12
|
-
|
|
13
|
-
/**
|
|
14
|
-
* Instrumented tests for AudioProcessor that require Android framework components.
|
|
15
|
-
* These tests run on an Android device/emulator and have access to MediaExtractor/MediaCodec.
|
|
16
|
-
*/
|
|
17
|
-
@RunWith(AndroidJUnit4::class)
|
|
18
|
-
class AudioProcessorInstrumentedTest {
|
|
19
|
-
private lateinit var context: android.content.Context
|
|
20
|
-
private lateinit var audioProcessor: AudioProcessor
|
|
21
|
-
private lateinit var testFilesDir: File
|
|
22
|
-
|
|
23
|
-
@Before
|
|
24
|
-
fun setUp() {
|
|
25
|
-
context = InstrumentationRegistry.getInstrumentation().targetContext
|
|
26
|
-
testFilesDir = context.filesDir
|
|
27
|
-
audioProcessor = AudioProcessor(testFilesDir)
|
|
28
|
-
|
|
29
|
-
// Copy test assets to files directory
|
|
30
|
-
copyTestAssets()
|
|
31
|
-
}
|
|
32
|
-
|
|
33
|
-
@After
|
|
34
|
-
fun tearDown() {
|
|
35
|
-
// Clean up test files
|
|
36
|
-
testFilesDir.listFiles()?.forEach { file ->
|
|
37
|
-
if (file.name.endsWith(".wav")) {
|
|
38
|
-
file.delete()
|
|
39
|
-
}
|
|
40
|
-
}
|
|
41
|
-
}
|
|
42
|
-
|
|
43
|
-
private fun copyTestAssets() {
|
|
44
|
-
val assetManager = context.assets
|
|
45
|
-
val testFiles = listOf("jfk.wav", "chorus.wav", "recorder_hello_world.wav")
|
|
46
|
-
|
|
47
|
-
testFiles.forEach { fileName ->
|
|
48
|
-
try {
|
|
49
|
-
assetManager.open(fileName).use { input ->
|
|
50
|
-
File(testFilesDir, fileName).outputStream().use { output ->
|
|
51
|
-
input.copyTo(output)
|
|
52
|
-
}
|
|
53
|
-
}
|
|
54
|
-
} catch (e: Exception) {
|
|
55
|
-
// If asset doesn't exist, skip it
|
|
56
|
-
println("Warning: Test asset $fileName not found")
|
|
57
|
-
}
|
|
58
|
-
}
|
|
59
|
-
}
|
|
60
|
-
|
|
61
|
-
// ========== Audio Loading Tests ==========
|
|
62
|
-
|
|
63
|
-
@Test
|
|
64
|
-
fun testLoadAudioFromAnyFormat_loadsWavFile() {
|
|
65
|
-
// Given
|
|
66
|
-
val wavFile = File(testFilesDir, "jfk.wav")
|
|
67
|
-
assertTrue("Test file should exist", wavFile.exists())
|
|
68
|
-
|
|
69
|
-
// When
|
|
70
|
-
val audioData = audioProcessor.loadAudioFromAnyFormat(wavFile.absolutePath, null)
|
|
71
|
-
|
|
72
|
-
// Then
|
|
73
|
-
assertNotNull("Audio data should not be null", audioData)
|
|
74
|
-
assertEquals("Sample rate should be 16000", 16000, audioData!!.sampleRate)
|
|
75
|
-
assertEquals("Should be mono", 1, audioData.channels)
|
|
76
|
-
assertEquals("Should be 16-bit", 16, audioData.bitDepth)
|
|
77
|
-
assertTrue("Should have audio data", audioData.data.isNotEmpty())
|
|
78
|
-
assertTrue("Duration should be positive", audioData.durationMs > 0)
|
|
79
|
-
}
|
|
80
|
-
|
|
81
|
-
@Test
|
|
82
|
-
fun testLoadAudioFromAnyFormat_withDecodingConfig() {
|
|
83
|
-
// Given
|
|
84
|
-
val wavFile = File(testFilesDir, "jfk.wav")
|
|
85
|
-
val config = DecodingConfig(
|
|
86
|
-
targetSampleRate = 44100,
|
|
87
|
-
targetChannels = 2, // Test channel conversion - bug fixed
|
|
88
|
-
targetBitDepth = 16,
|
|
89
|
-
normalizeAudio = false
|
|
90
|
-
)
|
|
91
|
-
|
|
92
|
-
// When
|
|
93
|
-
val audioData = audioProcessor.loadAudioFromAnyFormat(wavFile.absolutePath, config)
|
|
94
|
-
|
|
95
|
-
// Then
|
|
96
|
-
assertNotNull("Audio data should not be null", audioData)
|
|
97
|
-
assertEquals("Sample rate should be converted to 44100", 44100, audioData!!.sampleRate)
|
|
98
|
-
assertEquals("Should be converted to stereo", 2, audioData.channels)
|
|
99
|
-
}
|
|
100
|
-
|
|
101
|
-
// ========== Audio Trimming Tests ==========
|
|
102
|
-
|
|
103
|
-
@Test
|
|
104
|
-
fun testTrimAudio_basicTrimming() {
|
|
105
|
-
// Given
|
|
106
|
-
val wavFile = File(testFilesDir, "jfk.wav")
|
|
107
|
-
val startTimeMs = 1000L
|
|
108
|
-
val endTimeMs = 3000L
|
|
109
|
-
|
|
110
|
-
// When
|
|
111
|
-
val trimmedAudio = audioProcessor.trimAudio(
|
|
112
|
-
fileUri = wavFile.absolutePath,
|
|
113
|
-
startTimeMs = startTimeMs,
|
|
114
|
-
endTimeMs = endTimeMs,
|
|
115
|
-
config = null,
|
|
116
|
-
outputFileName = "trimmed_jfk.wav"
|
|
117
|
-
)
|
|
118
|
-
|
|
119
|
-
// Then
|
|
120
|
-
assertNotNull("Trimmed audio should not be null", trimmedAudio)
|
|
121
|
-
|
|
122
|
-
// Verify the trimmed file was created
|
|
123
|
-
val trimmedFile = File(testFilesDir, "trimmed_jfk.wav")
|
|
124
|
-
assertTrue("Trimmed file should exist", trimmedFile.exists())
|
|
125
|
-
}
|
|
126
|
-
|
|
127
|
-
// ========== Mel Spectrogram Tests ==========
|
|
128
|
-
|
|
129
|
-
@Test
|
|
130
|
-
fun testExtractMelSpectrogram_basicGeneration() {
|
|
131
|
-
// Given
|
|
132
|
-
val wavFile = File(testFilesDir, "jfk.wav")
|
|
133
|
-
val audioData = audioProcessor.loadAudioFromAnyFormat(wavFile.absolutePath, null)
|
|
134
|
-
assertNotNull("Audio data should load", audioData)
|
|
135
|
-
|
|
136
|
-
// When
|
|
137
|
-
val melSpectrogram = audioProcessor.extractMelSpectrogram(
|
|
138
|
-
audioData = audioData!!,
|
|
139
|
-
windowSizeMs = 25f,
|
|
140
|
-
hopLengthMs = 10f,
|
|
141
|
-
nMels = 40,
|
|
142
|
-
fftLength = 512
|
|
143
|
-
)
|
|
144
|
-
|
|
145
|
-
// Then
|
|
146
|
-
assertNotNull("Mel spectrogram should not be null", melSpectrogram)
|
|
147
|
-
assertTrue("Should have time frames", melSpectrogram.spectrogram.isNotEmpty())
|
|
148
|
-
assertEquals("Should have 40 mel bins", 40, melSpectrogram.spectrogram[0].size)
|
|
149
|
-
|
|
150
|
-
// Verify timestamps
|
|
151
|
-
assertTrue("Should have timestamps", melSpectrogram.timeStamps.isNotEmpty())
|
|
152
|
-
assertEquals("Timestamps should match frames",
|
|
153
|
-
melSpectrogram.spectrogram.size, melSpectrogram.timeStamps.size)
|
|
154
|
-
|
|
155
|
-
// Verify frequencies
|
|
156
|
-
assertEquals("Should have 40 frequency bins", 40, melSpectrogram.frequencies.size)
|
|
157
|
-
assertTrue("Frequencies should be ascending",
|
|
158
|
-
melSpectrogram.frequencies.zip(melSpectrogram.frequencies.drop(1))
|
|
159
|
-
.all { (a, b) -> a < b })
|
|
160
|
-
}
|
|
161
|
-
|
|
162
|
-
// ========== Preview Generation Tests ==========
|
|
163
|
-
|
|
164
|
-
@Test
|
|
165
|
-
fun testGeneratePreview_basicPreview() {
|
|
166
|
-
// Given
|
|
167
|
-
val wavFile = File(testFilesDir, "chorus.wav")
|
|
168
|
-
val audioData = audioProcessor.loadAudioFromAnyFormat(wavFile.absolutePath, null)
|
|
169
|
-
assertNotNull("Audio data should load", audioData)
|
|
170
|
-
|
|
171
|
-
val config = RecordingConfig(
|
|
172
|
-
sampleRate = audioData!!.sampleRate,
|
|
173
|
-
channels = audioData.channels,
|
|
174
|
-
encoding = "pcm_16bit",
|
|
175
|
-
segmentDurationMs = 20 // 50 points per second
|
|
176
|
-
)
|
|
177
|
-
|
|
178
|
-
// When
|
|
179
|
-
val preview = audioProcessor.generatePreview(
|
|
180
|
-
audioData = audioData,
|
|
181
|
-
numberOfPoints = 100,
|
|
182
|
-
startTimeMs = null,
|
|
183
|
-
endTimeMs = null,
|
|
184
|
-
config = config
|
|
185
|
-
)
|
|
186
|
-
|
|
187
|
-
// Then
|
|
188
|
-
assertNotNull("Preview should not be null", preview)
|
|
189
|
-
assertEquals("Should have 100 data points", 100, preview.dataPoints.size)
|
|
190
|
-
|
|
191
|
-
// Verify amplitude range
|
|
192
|
-
assertTrue("Min amplitude should be reasonable", preview.amplitudeRange.min >= 0)
|
|
193
|
-
assertTrue("Max amplitude should be reasonable", preview.amplitudeRange.max <= 1)
|
|
194
|
-
assertTrue("Max should be greater than min",
|
|
195
|
-
preview.amplitudeRange.max > preview.amplitudeRange.min)
|
|
196
|
-
}
|
|
197
|
-
}
|