@siteed/expo-audio-studio 2.8.6 → 2.10.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 (70) hide show
  1. package/CHANGELOG.md +17 -1
  2. package/android/build.gradle +9 -0
  3. package/android/src/androidTest/assets/chorus.wav +0 -0
  4. package/android/src/androidTest/assets/jfk.wav +0 -0
  5. package/android/src/androidTest/assets/osr_us_000_0010_8k.wav +0 -0
  6. package/android/src/androidTest/assets/recorder_hello_world.wav +0 -0
  7. package/android/src/androidTest/java/net/siteed/audiostream/AudioProcessorInstrumentedTest.kt +197 -0
  8. package/android/src/androidTest/java/net/siteed/audiostream/AudioRecorderInstrumentedTest.kt +541 -0
  9. package/android/src/androidTest/java/net/siteed/audiostream/integration/BufferDurationIntegrationTest.kt +324 -0
  10. package/android/src/androidTest/java/net/siteed/audiostream/integration/OutputControlIntegrationTest.kt +340 -0
  11. package/android/src/androidTest/java/net/siteed/audiostream/integration/README.md +95 -0
  12. package/android/src/androidTest/java/net/siteed/audiostream/integration/run_integration_tests.sh +28 -0
  13. package/android/src/main/java/net/siteed/audiostream/AudioFormatUtils.kt +264 -13
  14. package/android/src/main/java/net/siteed/audiostream/AudioProcessor.kt +3 -15
  15. package/android/src/main/java/net/siteed/audiostream/AudioRecorderManager.kt +118 -55
  16. package/android/src/main/java/net/siteed/audiostream/LogUtils.kt +32 -4
  17. package/android/src/main/java/net/siteed/audiostream/RecordingConfig.kt +50 -15
  18. package/android/src/test/java/net/siteed/audiostream/AudioFileHandlerTest.kt +279 -0
  19. package/android/src/test/java/net/siteed/audiostream/AudioFormatUtilsTest.kt +273 -0
  20. package/android/src/test/resources/chorus.wav +0 -0
  21. package/android/src/test/resources/generate_test_audio.py +94 -0
  22. package/android/src/test/resources/jfk.wav +0 -0
  23. package/android/src/test/resources/osr_us_000_0010_8k.wav +0 -0
  24. package/android/src/test/resources/recorder_hello_world.wav +0 -0
  25. package/build/cjs/AudioAnalysis/AudioAnalysis.types.js.map +1 -1
  26. package/build/cjs/ExpoAudioStream.types.js.map +1 -1
  27. package/build/cjs/ExpoAudioStream.web.js +38 -35
  28. package/build/cjs/ExpoAudioStream.web.js.map +1 -1
  29. package/build/cjs/WebRecorder.web.js +122 -102
  30. package/build/cjs/WebRecorder.web.js.map +1 -1
  31. package/build/esm/AudioAnalysis/AudioAnalysis.types.js.map +1 -1
  32. package/build/esm/ExpoAudioStream.types.js.map +1 -1
  33. package/build/esm/ExpoAudioStream.web.js +38 -35
  34. package/build/esm/ExpoAudioStream.web.js.map +1 -1
  35. package/build/esm/WebRecorder.web.js +122 -102
  36. package/build/esm/WebRecorder.web.js.map +1 -1
  37. package/build/types/AudioAnalysis/AudioAnalysis.types.d.ts +3 -1
  38. package/build/types/AudioAnalysis/AudioAnalysis.types.d.ts.map +1 -1
  39. package/build/types/ExpoAudioStream.types.d.ts +54 -22
  40. package/build/types/ExpoAudioStream.types.d.ts.map +1 -1
  41. package/build/types/ExpoAudioStream.web.d.ts.map +1 -1
  42. package/build/types/WebRecorder.web.d.ts +19 -3
  43. package/build/types/WebRecorder.web.d.ts.map +1 -1
  44. package/ios/AudioNotificationManager.swift +2 -6
  45. package/ios/AudioStreamManager.swift +116 -50
  46. package/ios/ExpoAudioStream.podspec +6 -0
  47. package/ios/ExpoAudioStreamModule.swift +11 -8
  48. package/ios/ExpoAudioStudioTests/AudioFileHandlerTests.swift +338 -0
  49. package/ios/ExpoAudioStudioTests/AudioFormatUtilsTests.swift +331 -0
  50. package/ios/ExpoAudioStudioTests/AudioTestHelpers.swift +130 -0
  51. package/ios/ExpoAudioStudioTests/Info.plist +22 -0
  52. package/ios/ExpoAudioStudioTests/SimpleAudioTest.swift +98 -0
  53. package/ios/ExpoAudioStudioTests/TestAudioGenerator.swift +75 -0
  54. package/ios/RecordingSettings.swift +53 -22
  55. package/ios/tests/integration/buffer_duration_test.swift +185 -0
  56. package/ios/tests/integration/output_control_test.swift +322 -0
  57. package/ios/tests/integration/run_integration_tests.sh +27 -0
  58. package/ios/tests/standalone/audio_processing_test.swift +144 -0
  59. package/ios/tests/standalone/audio_recording_test.swift +277 -0
  60. package/ios/tests/standalone/audio_streaming_test.swift +249 -0
  61. package/ios/tests/standalone/standalone_test.swift +144 -0
  62. package/package.json +140 -133
  63. package/src/AudioAnalysis/AudioAnalysis.types.ts +8 -1
  64. package/src/ExpoAudioStream.types.ts +66 -22
  65. package/src/ExpoAudioStream.web.ts +45 -39
  66. package/src/WebRecorder.web.ts +164 -130
  67. package/android/src/main/test/java/net/siteed/audiostream/AudioProcessorTest.kt +0 -56
  68. package/ios/siteedexpoaudiostudio.xcodeproj/project.xcworkspace/contents.xcworkspacedata +0 -7
  69. package/ios/siteedexpoaudiostudio.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist +0 -8
  70. /package/plugin/build/{index.d.ts → index.d.cts} +0 -0
@@ -0,0 +1,95 @@
1
+ # Android Integration Tests
2
+
3
+ ## Overview
4
+
5
+ This directory contains integration tests for the Buffer Duration and Skip File Writing features in expo-audio-studio. These tests validate ACTUAL Android platform behavior, not mocked behavior.
6
+
7
+ ## Test Structure
8
+
9
+ ### BufferDurationIntegrationTest
10
+ Tests the actual behavior of Android AudioRecord with different buffer sizes:
11
+ - Default buffer size handling
12
+ - Custom buffer sizes (10ms to 500ms)
13
+ - Buffer size limits (very small and very large)
14
+ - Buffer accumulation for small durations
15
+ - Different sample rates
16
+
17
+ ### SkipFileWritingIntegrationTest
18
+ Tests the skip file writing feature:
19
+ - Normal recording baseline
20
+ - Skip file writing mode
21
+ - Data emission without file I/O
22
+ - Compression behavior with skip mode
23
+ - Pause/Resume functionality
24
+ - Memory-only operation
25
+
26
+ ## Running the Tests
27
+
28
+ ### Prerequisites
29
+ 1. Android device or emulator connected
30
+ 2. USB debugging enabled
31
+ 3. Playground app built at least once
32
+
33
+ ### Run All Integration Tests
34
+ ```bash
35
+ cd packages/expo-audio-studio
36
+ ./android/src/androidTest/java/net/siteed/audiostream/integration/run_integration_tests.sh
37
+ ```
38
+
39
+ ### Run Individual Tests
40
+ ```bash
41
+ cd apps/playground/android
42
+
43
+ # Buffer Duration Test
44
+ ./gradlew :siteed-expo-audio-studio:connectedAndroidTest --tests "*.BufferDurationIntegrationTest"
45
+
46
+ # Skip File Writing Test
47
+ ./gradlew :siteed-expo-audio-studio:connectedAndroidTest --tests "*.SkipFileWritingIntegrationTest"
48
+ ```
49
+
50
+ ## Key Findings
51
+
52
+ ### Android Buffer Behavior
53
+ - Android uses `AudioRecord.getMinBufferSize()` to determine minimum buffer
54
+ - Minimum buffer size varies by device and sample rate
55
+ - Unlike iOS, Android respects smaller buffer requests (with accumulation)
56
+ - No hard-coded minimum like iOS's 4800 frames
57
+
58
+ ### Platform Differences from iOS
59
+ | Feature | iOS | Android |
60
+ |---------|-----|---------|
61
+ | Minimum Buffer | ~4800 frames (0.1s @ 48kHz) | Device-dependent |
62
+ | Buffer Flexibility | Rigid enforcement | More flexible |
63
+ | Small Buffer Handling | Ignored | Requires accumulation |
64
+ | API | AVAudioEngine | AudioRecord |
65
+
66
+ ## Test Results Location
67
+ - HTML Report: `android/build/reports/androidTests/connected/index.html`
68
+ - XML Report: `android/build/test-results/androidTests/connected/`
69
+
70
+ ## Implementation Notes
71
+
72
+ ### Buffer Duration
73
+ When implementing buffer duration on Android:
74
+ 1. Calculate buffer size: `frames * bytesPerSample * channels`
75
+ 2. Check against `AudioRecord.getMinBufferSize()`
76
+ 3. Use the larger of requested vs minimum
77
+ 4. For small buffers, implement accumulation strategy
78
+
79
+ ### Skip File Writing
80
+ When implementing skip file writing:
81
+ 1. Conditionally create file based on flag
82
+ 2. Continue audio data emission without file I/O
83
+ 3. Skip compression processing when enabled
84
+ 4. Maintain pause/resume functionality
85
+ 5. Track statistics without file writing
86
+
87
+ ## Next Steps
88
+
89
+ After these tests pass:
90
+ 1. Implement `bufferDurationSeconds` in RecordingConfig
91
+ 2. Implement `skipFileWriting` in RecordingConfig
92
+ 3. Update AudioRecorderManager to handle dynamic buffer sizing
93
+ 4. Update file creation/writing logic for skip mode
94
+ 5. Run integration tests to validate implementation
95
+ 6. Update playground app with new controls
@@ -0,0 +1,28 @@
1
+ #!/bin/bash
2
+
3
+ # Android Integration Tests for Buffer Duration & Skip File Writing
4
+ # Run this script from the expo-audio-studio package root
5
+
6
+ echo "🚀 Running Android Integration Tests"
7
+ echo "===================================="
8
+ echo ""
9
+
10
+ # Navigate to playground app (which has the gradle wrapper)
11
+ cd ../../../apps/playground/android || exit 1
12
+
13
+ echo "📱 Running Buffer Duration Integration Test..."
14
+ echo "--------------------------------------------"
15
+ ./gradlew :siteed-expo-audio-studio:connectedAndroidTest --tests "*.BufferDurationIntegrationTest"
16
+
17
+ echo ""
18
+ echo "📱 Running Skip File Writing Integration Test..."
19
+ echo "-----------------------------------------------"
20
+ ./gradlew :siteed-expo-audio-studio:connectedAndroidTest --tests "*.SkipFileWritingIntegrationTest"
21
+
22
+ echo ""
23
+ echo "📊 Test Results Summary"
24
+ echo "======================"
25
+ echo "Check the test reports at:"
26
+ echo "packages/expo-audio-studio/android/build/reports/androidTests/connected/index.html"
27
+ echo ""
28
+ echo "✅ Integration tests completed!"
@@ -3,6 +3,7 @@ package net.siteed.audiostream
3
3
  import android.media.AudioFormat
4
4
  import java.nio.ByteBuffer
5
5
  import java.nio.ByteOrder
6
+ import kotlin.math.*
6
7
 
7
8
  object AudioFormatUtils {
8
9
  /**
@@ -85,19 +86,269 @@ object AudioFormatUtils {
85
86
  * @return The converted audio data
86
87
  */
87
88
  fun convertBitDepth(audioData: ByteArray, sourceBitDepth: Int, targetBitDepth: Int): ByteArray {
88
- // First convert to float array for normalization
89
- val floatArray = convertByteArrayToFloatArray(audioData, "pcm_${sourceBitDepth}bit")
90
-
91
- // Convert back to bytes with new bit depth
92
- return when (targetBitDepth) {
93
- 8 -> floatArray.map { ((it + 1.0f) * 127.5f).toInt().toByte() }.toByteArray()
94
- 16 -> ByteBuffer.allocate(floatArray.size * 2).order(ByteOrder.LITTLE_ENDIAN).apply {
95
- floatArray.forEach { asShortBuffer().put((it * 32767f).toInt().toShort()) }
96
- }.array()
97
- 32 -> ByteBuffer.allocate(floatArray.size * 4).order(ByteOrder.LITTLE_ENDIAN).apply {
98
- floatArray.forEach { putFloat(it) }
99
- }.array()
100
- else -> throw IllegalArgumentException("Unsupported target bit depth: $targetBitDepth")
89
+ if (sourceBitDepth == targetBitDepth || audioData.isEmpty()) {
90
+ return audioData
101
91
  }
92
+
93
+ return when {
94
+ sourceBitDepth == 8 && targetBitDepth == 16 -> convert8to16(audioData)
95
+ sourceBitDepth == 16 && targetBitDepth == 8 -> convert16to8(audioData)
96
+ sourceBitDepth == 16 && targetBitDepth == 32 -> convert16to32(audioData)
97
+ sourceBitDepth == 32 && targetBitDepth == 16 -> convert32to16(audioData)
98
+ sourceBitDepth == 8 && targetBitDepth == 32 -> {
99
+ // Convert 8 -> 16 -> 32
100
+ val temp16 = convert8to16(audioData)
101
+ convert16to32(temp16)
102
+ }
103
+ sourceBitDepth == 32 && targetBitDepth == 8 -> {
104
+ // Convert 32 -> 16 -> 8
105
+ val temp16 = convert32to16(audioData)
106
+ convert16to8(temp16)
107
+ }
108
+ else -> throw IllegalArgumentException("Unsupported bit depth conversion: $sourceBitDepth to $targetBitDepth")
109
+ }
110
+ }
111
+
112
+ private fun convert8to16(data: ByteArray): ByteArray {
113
+ val output = ByteBuffer.allocate(data.size * 2).order(ByteOrder.LITTLE_ENDIAN)
114
+ for (sample in data) {
115
+ // Convert unsigned 8-bit (0-255) to signed 16-bit (-32768 to 32767)
116
+ val unsigned = sample.toInt() and 0xFF
117
+ // Map [0, 255] to [-32768, 32767]
118
+ // Special case for 0 to map to -32768
119
+ val signed16 = when (unsigned) {
120
+ 0 -> -32768
121
+ 255 -> 32767
122
+ else -> {
123
+ val normalized = (unsigned - 128) / 128.0f
124
+ (normalized * 32768).toInt().coerceIn(-32768, 32767)
125
+ }
126
+ }.toShort()
127
+ output.putShort(signed16)
128
+ }
129
+ return output.array()
130
+ }
131
+
132
+ private fun convert16to8(data: ByteArray): ByteArray {
133
+ val input = ByteBuffer.wrap(data).order(ByteOrder.LITTLE_ENDIAN)
134
+ val output = ByteArray(data.size / 2)
135
+
136
+ for (i in output.indices) {
137
+ // Convert signed 16-bit to unsigned 8-bit
138
+ val sample16 = input.getShort()
139
+ // Map [-32768, 32767] to [0, 255]
140
+ val normalized = sample16 / 32768.0f
141
+ val sample8 = ((normalized * 128) + 128).toInt().coerceIn(0, 255).toByte()
142
+ output[i] = sample8
143
+ }
144
+ return output
145
+ }
146
+
147
+ private fun convert16to32(data: ByteArray): ByteArray {
148
+ val input = ByteBuffer.wrap(data).order(ByteOrder.LITTLE_ENDIAN)
149
+ val output = ByteBuffer.allocate(data.size * 2).order(ByteOrder.LITTLE_ENDIAN)
150
+
151
+ while (input.hasRemaining()) {
152
+ val sample16 = input.getShort()
153
+ // Scale 16-bit to 32-bit range
154
+ val sample32 = (sample16.toInt() shl 16)
155
+ output.putInt(sample32)
156
+ }
157
+ return output.array()
158
+ }
159
+
160
+ private fun convert32to16(data: ByteArray): ByteArray {
161
+ val input = ByteBuffer.wrap(data).order(ByteOrder.LITTLE_ENDIAN)
162
+ val output = ByteBuffer.allocate(data.size / 2).order(ByteOrder.LITTLE_ENDIAN)
163
+
164
+ while (input.hasRemaining()) {
165
+ val sample32 = input.getInt()
166
+ // Scale 32-bit to 16-bit range
167
+ val sample16 = (sample32 shr 16).toShort()
168
+ output.putShort(sample16)
169
+ }
170
+ return output.array()
171
+ }
172
+
173
+ /**
174
+ * Convert between different channel configurations
175
+ */
176
+ fun convertChannels(data: ByteArray, fromChannels: Int, toChannels: Int, bitDepth: Int): ByteArray {
177
+ if (fromChannels == toChannels || data.isEmpty()) {
178
+ return data
179
+ }
180
+
181
+ val bytesPerSample = bitDepth / 8
182
+ val samplesPerFrame = fromChannels
183
+ val totalFrames = data.size / (bytesPerSample * samplesPerFrame)
184
+
185
+ return when {
186
+ fromChannels == 1 && toChannels == 2 -> monoToStereo(data, bitDepth, totalFrames)
187
+ fromChannels == 2 && toChannels == 1 -> stereoToMono(data, bitDepth, totalFrames)
188
+ else -> throw IllegalArgumentException("Unsupported channel conversion: $fromChannels to $toChannels")
189
+ }
190
+ }
191
+
192
+ private fun monoToStereo(data: ByteArray, bitDepth: Int, totalFrames: Int): ByteArray {
193
+ val bytesPerSample = bitDepth / 8
194
+ val output = ByteBuffer.allocate(data.size * 2).order(ByteOrder.LITTLE_ENDIAN)
195
+ val input = ByteBuffer.wrap(data).order(ByteOrder.LITTLE_ENDIAN)
196
+
197
+ for (i in 0 until totalFrames) {
198
+ when (bitDepth) {
199
+ 16 -> {
200
+ val sample = input.getShort()
201
+ output.putShort(sample) // Left
202
+ output.putShort(sample) // Right
203
+ }
204
+ 32 -> {
205
+ val sample = input.getInt()
206
+ output.putInt(sample) // Left
207
+ output.putInt(sample) // Right
208
+ }
209
+ 8 -> {
210
+ val sample = input.get()
211
+ output.put(sample) // Left
212
+ output.put(sample) // Right
213
+ }
214
+ }
215
+ }
216
+ return output.array()
217
+ }
218
+
219
+ private fun stereoToMono(data: ByteArray, bitDepth: Int, totalFrames: Int): ByteArray {
220
+ val bytesPerSample = bitDepth / 8
221
+ val output = ByteBuffer.allocate(data.size / 2).order(ByteOrder.LITTLE_ENDIAN)
222
+ val input = ByteBuffer.wrap(data).order(ByteOrder.LITTLE_ENDIAN)
223
+
224
+ for (i in 0 until totalFrames) {
225
+ when (bitDepth) {
226
+ 16 -> {
227
+ val left = input.getShort()
228
+ val right = input.getShort()
229
+ val mono = ((left + right) / 2).toShort()
230
+ output.putShort(mono)
231
+ }
232
+ 32 -> {
233
+ val left = input.getInt()
234
+ val right = input.getInt()
235
+ val mono = ((left.toLong() + right.toLong()) / 2).toInt()
236
+ output.putInt(mono)
237
+ }
238
+ 8 -> {
239
+ val left = input.get().toInt() and 0xFF
240
+ val right = input.get().toInt() and 0xFF
241
+ val mono = ((left + right) / 2).toByte()
242
+ output.put(mono)
243
+ }
244
+ }
245
+ }
246
+ return output.array()
247
+ }
248
+
249
+ /**
250
+ * Normalize audio to maximum amplitude
251
+ */
252
+ fun normalizeAudio(data: ByteArray, bitDepth: Int): ByteArray {
253
+ if (data.isEmpty()) return data
254
+
255
+ val input = ByteBuffer.wrap(data).order(ByteOrder.LITTLE_ENDIAN)
256
+ var maxAmplitude = 0
257
+
258
+ // Find maximum amplitude
259
+ input.rewind()
260
+ when (bitDepth) {
261
+ 16 -> {
262
+ while (input.hasRemaining()) {
263
+ val sample = abs(input.getShort().toInt())
264
+ maxAmplitude = maxOf(maxAmplitude, sample)
265
+ }
266
+ }
267
+ 32 -> {
268
+ while (input.hasRemaining()) {
269
+ val sample = abs(input.getInt())
270
+ maxAmplitude = maxOf(maxAmplitude, sample)
271
+ }
272
+ }
273
+ 8 -> {
274
+ while (input.hasRemaining()) {
275
+ val sample = abs((input.get().toInt() and 0xFF) - 128)
276
+ maxAmplitude = maxOf(maxAmplitude, sample)
277
+ }
278
+ }
279
+ }
280
+
281
+ // If already at max or silent, return as is
282
+ if (maxAmplitude == 0) return data
283
+
284
+ val maxValue = when (bitDepth) {
285
+ 16 -> Short.MAX_VALUE.toInt()
286
+ 32 -> Int.MAX_VALUE
287
+ 8 -> 127
288
+ else -> throw IllegalArgumentException("Unsupported bit depth: $bitDepth")
289
+ }
290
+
291
+ if (maxAmplitude >= maxValue) return data
292
+
293
+ // Normalize
294
+ val scaleFactor = maxValue.toFloat() / maxAmplitude
295
+ val output = ByteBuffer.allocate(data.size).order(ByteOrder.LITTLE_ENDIAN)
296
+ input.rewind()
297
+
298
+ when (bitDepth) {
299
+ 16 -> {
300
+ while (input.hasRemaining()) {
301
+ val sample = input.getShort()
302
+ val normalized = (sample * scaleFactor).toInt().coerceIn(-32768, 32767).toShort()
303
+ output.putShort(normalized)
304
+ }
305
+ }
306
+ 32 -> {
307
+ while (input.hasRemaining()) {
308
+ val sample = input.getInt()
309
+ val normalized = (sample * scaleFactor).toLong().coerceIn(Int.MIN_VALUE.toLong(), Int.MAX_VALUE.toLong()).toInt()
310
+ output.putInt(normalized)
311
+ }
312
+ }
313
+ 8 -> {
314
+ while (input.hasRemaining()) {
315
+ val sample = (input.get().toInt() and 0xFF) - 128
316
+ val normalized = ((sample * scaleFactor).toInt() + 128).coerceIn(0, 255).toByte()
317
+ output.put(normalized)
318
+ }
319
+ }
320
+ }
321
+
322
+ return output.array()
323
+ }
324
+
325
+ /**
326
+ * Resample audio data to a different sample rate
327
+ */
328
+ fun resampleAudio(samples: FloatArray, fromSampleRate: Int, toSampleRate: Int): FloatArray {
329
+ if (fromSampleRate == toSampleRate || samples.isEmpty()) {
330
+ return samples
331
+ }
332
+
333
+ val resampleRatio = toSampleRate.toDouble() / fromSampleRate
334
+ val newLength = (samples.size * resampleRatio).toInt()
335
+ val resampled = FloatArray(newLength)
336
+
337
+ for (i in resampled.indices) {
338
+ val sourceIndex = i / resampleRatio
339
+ val sourceIndexInt = sourceIndex.toInt()
340
+ val fraction = sourceIndex - sourceIndexInt
341
+
342
+ if (sourceIndexInt >= samples.size - 1) {
343
+ resampled[i] = samples.last()
344
+ } else {
345
+ // Linear interpolation
346
+ val sample1 = samples[sourceIndexInt]
347
+ val sample2 = samples[sourceIndexInt + 1]
348
+ resampled[i] = (sample1 * (1 - fraction) + sample2 * fraction).toFloat()
349
+ }
350
+ }
351
+
352
+ return resampled
102
353
  }
103
354
  }
@@ -1,5 +1,3 @@
1
- // packages/expo-audio-stream/android/src/main/java/net/siteed/audiostream/AudioProcessor.kt
2
- // packages/expo-audio-stream/android/src/main/java/net/siteed/audiostream/AudioProcessor.kt
3
1
  package net.siteed.audiostream
4
2
 
5
3
  import java.nio.ByteBuffer
@@ -1061,19 +1059,9 @@ class AudioProcessor(private val filesDir: File) {
1061
1059
  }
1062
1060
 
1063
1061
  private fun convertChannels(pcmData: ByteArray, originalChannels: Int, targetChannels: Int): ByteArray {
1064
- val result = ByteArray(pcmData.size * targetChannels / originalChannels)
1065
- val inputBuffer = ByteBuffer.wrap(pcmData).order(ByteOrder.LITTLE_ENDIAN).asShortBuffer()
1066
- val outputBuffer = ByteBuffer.wrap(result).order(ByteOrder.LITTLE_ENDIAN).asShortBuffer()
1067
-
1068
- for (i in result.indices) {
1069
- val channelData = ShortArray(targetChannels)
1070
- for (j in 0 until targetChannels) {
1071
- channelData[j] = inputBuffer.get()
1072
- }
1073
- outputBuffer.put(channelData)
1074
- }
1075
-
1076
- return result
1062
+ // Use the correct implementation from AudioFormatUtils
1063
+ // Assuming 16-bit audio (which is the default for most audio processing)
1064
+ return AudioFormatUtils.convertChannels(pcmData, originalChannels, targetChannels, 16)
1077
1065
  }
1078
1066
 
1079
1067
  private fun debugWavHeader(file: File) {