@siteed/audio-studio 3.1.1 → 3.2.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 (96) hide show
  1. package/CHANGELOG.md +375 -4
  2. package/android/src/main/java/net/siteed/audiostudio/AudioStreamDecoder.kt +852 -0
  3. package/android/src/main/java/net/siteed/audiostudio/AudioStudioModule.kt +167 -3
  4. package/android/src/main/java/net/siteed/audiostudio/Constants.kt +4 -0
  5. package/build/cjs/errors/AudioStreamError.js +161 -0
  6. package/build/cjs/errors/AudioStreamError.js.map +1 -0
  7. package/build/cjs/errors/AudioStreamError.test.js +82 -0
  8. package/build/cjs/errors/AudioStreamError.test.js.map +1 -0
  9. package/build/cjs/index.js +7 -1
  10. package/build/cjs/index.js.map +1 -1
  11. package/build/cjs/streamAudioData.js +534 -0
  12. package/build/cjs/streamAudioData.js.map +1 -0
  13. package/build/cjs/utils/audioProcessing.js +14 -10
  14. package/build/cjs/utils/audioProcessing.js.map +1 -1
  15. package/build/esm/errors/AudioStreamError.js +156 -0
  16. package/build/esm/errors/AudioStreamError.js.map +1 -0
  17. package/build/esm/errors/AudioStreamError.test.js +80 -0
  18. package/build/esm/errors/AudioStreamError.test.js.map +1 -0
  19. package/build/esm/index.js +3 -1
  20. package/build/esm/index.js.map +1 -1
  21. package/build/esm/streamAudioData.js +527 -0
  22. package/build/esm/streamAudioData.js.map +1 -0
  23. package/build/esm/utils/audioProcessing.js +14 -10
  24. package/build/esm/utils/audioProcessing.js.map +1 -1
  25. package/build/types/errors/AudioStreamError.d.ts +25 -0
  26. package/build/types/errors/AudioStreamError.d.ts.map +1 -0
  27. package/build/types/errors/AudioStreamError.test.d.ts +2 -0
  28. package/build/types/errors/AudioStreamError.test.d.ts.map +1 -0
  29. package/build/types/index.d.ts +5 -1
  30. package/build/types/index.d.ts.map +1 -1
  31. package/build/types/streamAudioData.d.ts +119 -0
  32. package/build/types/streamAudioData.d.ts.map +1 -0
  33. package/build/types/utils/audioProcessing.d.ts +2 -2
  34. package/build/types/utils/audioProcessing.d.ts.map +1 -1
  35. package/ios/AudioProcessingHelpers.swift +10 -5
  36. package/ios/AudioStreamDecoder.swift +614 -0
  37. package/ios/AudioStudioModule.swift +186 -3
  38. package/package.json +163 -146
  39. package/scripts/README.md +58 -0
  40. package/src/errors/AudioStreamError.test.ts +92 -0
  41. package/src/errors/AudioStreamError.ts +199 -0
  42. package/src/index.ts +24 -0
  43. package/src/streamAudioData.ts +758 -0
  44. package/src/utils/audioProcessing.ts +25 -14
  45. package/android/src/androidTest/assets/chorus.wav +0 -0
  46. package/android/src/androidTest/assets/jfk.wav +0 -0
  47. package/android/src/androidTest/assets/osr_us_000_0010_8k.wav +0 -0
  48. package/android/src/androidTest/assets/recorder_hello_world.wav +0 -0
  49. package/android/src/androidTest/java/net/siteed/audiostudio/AudioFinalMetadataContractInstrumentedTest.kt +0 -190
  50. package/android/src/androidTest/java/net/siteed/audiostudio/AudioProcessorInstrumentedTest.kt +0 -197
  51. package/android/src/androidTest/java/net/siteed/audiostudio/AudioRecorderInstrumentedTest.kt +0 -487
  52. package/android/src/androidTest/java/net/siteed/audiostudio/AudioRecorderPerformanceInstrumentedTest.kt +0 -250
  53. package/android/src/androidTest/java/net/siteed/audiostudio/OpusRangeDecodeRegressionInstrumentedTest.kt +0 -186
  54. package/android/src/androidTest/java/net/siteed/audiostudio/integration/AudioFocusStrategyIntegrationTest.kt +0 -332
  55. package/android/src/androidTest/java/net/siteed/audiostudio/integration/BufferDurationIntegrationTest.kt +0 -324
  56. package/android/src/androidTest/java/net/siteed/audiostudio/integration/CompressedOnlyOutputTest.kt +0 -253
  57. package/android/src/androidTest/java/net/siteed/audiostudio/integration/DeviceDisconnectionFallbackTest.kt +0 -218
  58. package/android/src/androidTest/java/net/siteed/audiostudio/integration/EventEmissionIntervalTest.kt +0 -120
  59. package/android/src/androidTest/java/net/siteed/audiostudio/integration/M4aFormatTest.kt +0 -345
  60. package/android/src/androidTest/java/net/siteed/audiostudio/integration/OutputControlIntegrationTest.kt +0 -340
  61. package/android/src/androidTest/java/net/siteed/audiostudio/integration/PcmStreamingDurationTest.kt +0 -252
  62. package/android/src/androidTest/java/net/siteed/audiostudio/integration/README.md +0 -95
  63. package/android/src/androidTest/java/net/siteed/audiostudio/integration/run_integration_tests.sh +0 -43
  64. package/android/src/test/java/net/siteed/audiostudio/AndroidCallStateTest.kt +0 -37
  65. package/android/src/test/java/net/siteed/audiostudio/AndroidEventEmitterTest.kt +0 -28
  66. package/android/src/test/java/net/siteed/audiostudio/AudioFileHandlerTest.kt +0 -279
  67. package/android/src/test/java/net/siteed/audiostudio/AudioFocusStrategyTest.kt +0 -249
  68. package/android/src/test/java/net/siteed/audiostudio/AudioFormatTest.kt +0 -151
  69. package/android/src/test/java/net/siteed/audiostudio/AudioFormatUtilsTest.kt +0 -273
  70. package/android/src/test/java/net/siteed/audiostudio/DeviceDisconnectionFallbackUnitTest.kt +0 -140
  71. package/android/src/test/java/net/siteed/audiostudio/InterruptionAutoResumePolicyTest.kt +0 -49
  72. package/android/src/test/resources/chorus.wav +0 -0
  73. package/android/src/test/resources/generate_test_audio.py +0 -94
  74. package/android/src/test/resources/jfk.wav +0 -0
  75. package/android/src/test/resources/osr_us_000_0010_8k.wav +0 -0
  76. package/android/src/test/resources/recorder_hello_world.wav +0 -0
  77. package/ios/AudioStudioTests/AudioFileHandlerTests.swift +0 -338
  78. package/ios/AudioStudioTests/AudioFormatUtilsTests.swift +0 -331
  79. package/ios/AudioStudioTests/AudioTestHelpers.swift +0 -130
  80. package/ios/AudioStudioTests/CompressedOnlyOutputTests.swift +0 -334
  81. package/ios/AudioStudioTests/EventEmissionIntervalTests.swift +0 -105
  82. package/ios/AudioStudioTests/Info.plist +0 -22
  83. package/ios/AudioStudioTests/README.md +0 -39
  84. package/ios/AudioStudioTests/SimpleAudioTest.swift +0 -98
  85. package/ios/AudioStudioTests/TestAudioGenerator.swift +0 -75
  86. package/ios/tests/README.md +0 -41
  87. package/ios/tests/integration/buffer_and_fallback_test.swift +0 -178
  88. package/ios/tests/integration/buffer_duration_test.swift +0 -185
  89. package/ios/tests/integration/compressed_only_output_test.swift +0 -271
  90. package/ios/tests/integration/output_control_test.swift +0 -322
  91. package/ios/tests/integration/run_integration_tests.sh +0 -37
  92. package/ios/tests/opus_support_test_macos.swift +0 -154
  93. package/ios/tests/standalone/audio_processing_test.swift +0 -144
  94. package/ios/tests/standalone/audio_recording_test.swift +0 -277
  95. package/ios/tests/standalone/audio_streaming_test.swift +0 -249
  96. package/ios/tests/standalone/standalone_test.swift +0 -144
@@ -6,8 +6,8 @@ import { ConsoleLike } from '../AudioStudio.types'
6
6
  export interface ProcessAudioBufferOptions {
7
7
  arrayBuffer?: ArrayBuffer
8
8
  fileUri?: string
9
- targetSampleRate: number
10
- targetChannels: number
9
+ targetSampleRate?: number
10
+ targetChannels?: number
11
11
  normalizeAudio: boolean
12
12
  startTimeMs?: number
13
13
  endTimeMs?: number
@@ -84,9 +84,17 @@ export async function processAudioBuffer({
84
84
  // Create context at original sample rate first
85
85
  ctx =
86
86
  audioContext ||
87
- new (window.AudioContext || (window as any).webkitAudioContext)()
87
+ new (window.AudioContext ||
88
+ (
89
+ window as unknown as {
90
+ webkitAudioContext?: typeof AudioContext
91
+ }
92
+ ).webkitAudioContext)()
88
93
  buffer = await ctx.decodeAudioData(audioData)
89
94
 
95
+ const effectiveTargetSampleRate = targetSampleRate ?? buffer.sampleRate
96
+ const effectiveTargetChannels = targetChannels ?? buffer.numberOfChannels
97
+
90
98
  logger?.debug('Decoded audio buffer:', {
91
99
  originalChannels: buffer.numberOfChannels,
92
100
  originalSampleRate: buffer.sampleRate,
@@ -109,7 +117,7 @@ export async function processAudioBuffer({
109
117
  position !== undefined
110
118
  ? Math.floor(
111
119
  (position / bytesPerSample) *
112
- (buffer.sampleRate / targetSampleRate)
120
+ (buffer.sampleRate / effectiveTargetSampleRate)
113
121
  )
114
122
  : startSample
115
123
 
@@ -117,11 +125,12 @@ export async function processAudioBuffer({
117
125
  length !== undefined
118
126
  ? Math.floor(
119
127
  (length / bytesPerSample) *
120
- (buffer.sampleRate / targetSampleRate)
128
+ (buffer.sampleRate / effectiveTargetSampleRate)
121
129
  )
122
- : endTimeMs !== undefined && startTimeMs !== undefined
130
+ : endTimeMs !== undefined
123
131
  ? Math.floor(
124
- ((endTimeMs - startTimeMs) / 1000) * buffer.sampleRate
132
+ ((endTimeMs - (startTimeMs ?? 0)) / 1000) *
133
+ buffer.sampleRate
125
134
  )
126
135
  : buffer.length - adjustedStartSample
127
136
 
@@ -130,8 +139,8 @@ export async function processAudioBuffer({
130
139
  adjustedStartSample,
131
140
  samplesNeeded,
132
141
  originalSampleRate: buffer.sampleRate,
133
- targetSampleRate,
134
- conversionRatio: buffer.sampleRate / targetSampleRate,
142
+ targetSampleRate: effectiveTargetSampleRate,
143
+ conversionRatio: buffer.sampleRate / effectiveTargetSampleRate,
135
144
  expectedDurationMs: (samplesNeeded / buffer.sampleRate) * 1000,
136
145
  })
137
146
 
@@ -153,9 +162,11 @@ export async function processAudioBuffer({
153
162
 
154
163
  // Create offline context for resampling
155
164
  const offlineCtx = new OfflineAudioContext(
156
- targetChannels,
157
- Math.ceil((samplesNeeded * targetSampleRate) / buffer.sampleRate),
158
- targetSampleRate
165
+ effectiveTargetChannels,
166
+ Math.ceil(
167
+ (samplesNeeded * effectiveTargetSampleRate) / buffer.sampleRate
168
+ ),
169
+ effectiveTargetSampleRate
159
170
  )
160
171
 
161
172
  // Create source and connect
@@ -175,7 +186,7 @@ export async function processAudioBuffer({
175
186
 
176
187
  logger?.debug('Final processed audio:', {
177
188
  outputSamples: channelData.length,
178
- outputSampleRate: targetSampleRate,
189
+ outputSampleRate: effectiveTargetSampleRate,
179
190
  durationMs,
180
191
  })
181
192
 
@@ -184,7 +195,7 @@ export async function processAudioBuffer({
184
195
  channelData,
185
196
  samples: channelData.length,
186
197
  durationMs,
187
- sampleRate: targetSampleRate,
198
+ sampleRate: effectiveTargetSampleRate,
188
199
  channels: processedBuffer.numberOfChannels,
189
200
  }
190
201
  } catch (error) {
@@ -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
- }
@@ -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
- }