@siteed/expo-audio-studio 2.9.0 → 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.
- package/CHANGELOG.md +9 -1
- package/android/build.gradle +9 -0
- 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/audiostream/AudioProcessorInstrumentedTest.kt +197 -0
- package/android/src/androidTest/java/net/siteed/audiostream/AudioRecorderInstrumentedTest.kt +541 -0
- package/android/src/androidTest/java/net/siteed/audiostream/integration/BufferDurationIntegrationTest.kt +324 -0
- package/android/src/androidTest/java/net/siteed/audiostream/integration/OutputControlIntegrationTest.kt +340 -0
- package/android/src/androidTest/java/net/siteed/audiostream/integration/README.md +95 -0
- package/android/src/androidTest/java/net/siteed/audiostream/integration/run_integration_tests.sh +28 -0
- package/android/src/main/java/net/siteed/audiostream/AudioFormatUtils.kt +264 -13
- package/android/src/main/java/net/siteed/audiostream/AudioProcessor.kt +3 -13
- package/android/src/main/java/net/siteed/audiostream/AudioRecorderManager.kt +118 -55
- package/android/src/main/java/net/siteed/audiostream/LogUtils.kt +32 -4
- package/android/src/main/java/net/siteed/audiostream/RecordingConfig.kt +50 -15
- package/android/src/test/java/net/siteed/audiostream/AudioFileHandlerTest.kt +279 -0
- package/android/src/test/java/net/siteed/audiostream/AudioFormatUtilsTest.kt +273 -0
- package/android/src/test/resources/chorus.wav +0 -0
- package/android/src/test/resources/generate_test_audio.py +94 -0
- 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/build/cjs/ExpoAudioStream.types.js.map +1 -1
- package/build/cjs/ExpoAudioStream.web.js +37 -34
- package/build/cjs/ExpoAudioStream.web.js.map +1 -1
- package/build/cjs/WebRecorder.web.js +12 -10
- package/build/cjs/WebRecorder.web.js.map +1 -1
- package/build/esm/ExpoAudioStream.types.js.map +1 -1
- package/build/esm/ExpoAudioStream.web.js +37 -34
- package/build/esm/ExpoAudioStream.web.js.map +1 -1
- package/build/esm/WebRecorder.web.js +12 -10
- package/build/esm/WebRecorder.web.js.map +1 -1
- package/build/types/ExpoAudioStream.types.d.ts +54 -22
- package/build/types/ExpoAudioStream.types.d.ts.map +1 -1
- package/build/types/ExpoAudioStream.web.d.ts.map +1 -1
- package/build/types/WebRecorder.web.d.ts.map +1 -1
- package/ios/AudioNotificationManager.swift +2 -6
- package/ios/AudioStreamManager.swift +116 -50
- package/ios/ExpoAudioStream.podspec +6 -0
- package/ios/ExpoAudioStreamModule.swift +11 -8
- package/ios/ExpoAudioStudioTests/AudioFileHandlerTests.swift +338 -0
- package/ios/ExpoAudioStudioTests/AudioFormatUtilsTests.swift +331 -0
- package/ios/ExpoAudioStudioTests/AudioTestHelpers.swift +130 -0
- package/ios/ExpoAudioStudioTests/Info.plist +22 -0
- package/ios/ExpoAudioStudioTests/SimpleAudioTest.swift +98 -0
- package/ios/ExpoAudioStudioTests/TestAudioGenerator.swift +75 -0
- package/ios/RecordingSettings.swift +53 -22
- package/ios/tests/integration/buffer_duration_test.swift +185 -0
- package/ios/tests/integration/output_control_test.swift +322 -0
- package/ios/tests/integration/run_integration_tests.sh +27 -0
- package/ios/tests/standalone/audio_processing_test.swift +144 -0
- package/ios/tests/standalone/audio_recording_test.swift +277 -0
- package/ios/tests/standalone/audio_streaming_test.swift +249 -0
- package/ios/tests/standalone/standalone_test.swift +144 -0
- package/package.json +140 -133
- package/src/ExpoAudioStream.types.ts +66 -22
- package/src/ExpoAudioStream.web.ts +43 -38
- package/src/WebRecorder.web.ts +13 -10
- package/android/src/main/test/java/net/siteed/audiostream/AudioProcessorTest.kt +0 -56
- package/ios/siteedexpoaudiostudio.xcodeproj/project.xcworkspace/contents.xcworkspacedata +0 -7
- package/ios/siteedexpoaudiostudio.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist +0 -8
- /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
|
package/android/src/androidTest/java/net/siteed/audiostream/integration/run_integration_tests.sh
ADDED
|
@@ -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
|
-
|
|
89
|
-
|
|
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
|
}
|
|
@@ -1059,19 +1059,9 @@ class AudioProcessor(private val filesDir: File) {
|
|
|
1059
1059
|
}
|
|
1060
1060
|
|
|
1061
1061
|
private fun convertChannels(pcmData: ByteArray, originalChannels: Int, targetChannels: Int): ByteArray {
|
|
1062
|
-
|
|
1063
|
-
|
|
1064
|
-
|
|
1065
|
-
|
|
1066
|
-
for (i in result.indices) {
|
|
1067
|
-
val channelData = ShortArray(targetChannels)
|
|
1068
|
-
for (j in 0 until targetChannels) {
|
|
1069
|
-
channelData[j] = inputBuffer.get()
|
|
1070
|
-
}
|
|
1071
|
-
outputBuffer.put(channelData)
|
|
1072
|
-
}
|
|
1073
|
-
|
|
1074
|
-
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)
|
|
1075
1065
|
}
|
|
1076
1066
|
|
|
1077
1067
|
private fun debugWavHeader(file: File) {
|