@siteed/expo-audio-studio 2.11.0 → 2.12.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 +7 -1
- package/android/src/androidTest/java/net/siteed/audiostream/integration/AudioFocusStrategyIntegrationTest.kt +332 -0
- package/android/src/androidTest/java/net/siteed/audiostream/integration/PcmStreamingDurationTest.kt +252 -0
- package/android/src/androidTest/java/net/siteed/audiostream/integration/run_integration_tests.sh +5 -0
- package/android/src/main/java/net/siteed/audiostream/AudioRecorderManager.kt +173 -18
- package/android/src/main/java/net/siteed/audiostream/RecordingConfig.kt +6 -0
- package/android/src/test/java/net/siteed/audiostream/AudioFocusStrategyTest.kt +249 -0
- package/build/cjs/ExpoAudioStream.types.js.map +1 -1
- package/build/esm/ExpoAudioStream.types.js.map +1 -1
- package/build/types/ExpoAudioStream.types.d.ts +16 -0
- package/build/types/ExpoAudioStream.types.d.ts.map +1 -1
- package/ios/AudioStreamManager.swift +49 -40
- package/package.json +1 -1
- package/src/ExpoAudioStream.types.ts +18 -0
package/CHANGELOG.md
CHANGED
|
@@ -8,6 +8,11 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
|
|
8
8
|
## [Unreleased]
|
|
9
9
|
|
|
10
10
|
|
|
11
|
+
## [2.12.0] - 2025-06-07
|
|
12
|
+
### Changed
|
|
13
|
+
- fix(android): resolve PCM streaming duration calculation bug (Issue #263) (#265) ([a0c5500](https://github.com/deeeed/expo-audio-stream/commit/a0c550099fec9d6b0d486440819173d9d9275908))
|
|
14
|
+
- feat(expo-audio-studio): implement Android-only audioFocusStrategy (#264) ([cc77226](https://github.com/deeeed/expo-audio-stream/commit/cc7722605a5502a58b8236b610c9bdccf5f7f561))
|
|
15
|
+
- docs: fix comment formatting in OutputConfig interface for clarity ([07fac61](https://github.com/deeeed/expo-audio-stream/commit/07fac61245843c601709bb7576db6e48b2106cf7))
|
|
11
16
|
## [2.11.0] - 2025-06-05
|
|
12
17
|
### Changed
|
|
13
18
|
- refactor(expo-audio-studio): remove android/build.gradle and add device disconnection fallback tests ([36fe9a9](https://github.com/deeeed/expo-audio-stream/commit/36fe9a921505e136ea50406d4b664c597293ffd8))
|
|
@@ -289,7 +294,8 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
|
|
289
294
|
- Feature: Audio features extraction during recording.
|
|
290
295
|
- Feature: Consistent WAV PCM recording format across all platforms.
|
|
291
296
|
|
|
292
|
-
[unreleased]: https://github.com/deeeed/expo-audio-stream/compare/@siteed/expo-audio-studio@2.
|
|
297
|
+
[unreleased]: https://github.com/deeeed/expo-audio-stream/compare/@siteed/expo-audio-studio@2.12.0...HEAD
|
|
298
|
+
[2.12.0]: https://github.com/deeeed/expo-audio-stream/compare/@siteed/expo-audio-studio@2.11.0...@siteed/expo-audio-studio@2.12.0
|
|
293
299
|
[2.11.0]: https://github.com/deeeed/expo-audio-stream/compare/@siteed/expo-audio-studio@2.10.6...@siteed/expo-audio-studio@2.11.0
|
|
294
300
|
[2.10.6]: https://github.com/deeeed/expo-audio-stream/compare/@siteed/expo-audio-studio@2.10.5...@siteed/expo-audio-studio@2.10.6
|
|
295
301
|
[2.10.5]: https://github.com/deeeed/expo-audio-stream/compare/@siteed/expo-audio-studio@2.10.4...@siteed/expo-audio-studio@2.10.5
|
|
@@ -0,0 +1,332 @@
|
|
|
1
|
+
package net.siteed.audiostream.integration
|
|
2
|
+
|
|
3
|
+
import android.media.AudioManager
|
|
4
|
+
import android.content.Context
|
|
5
|
+
import androidx.test.ext.junit.runners.AndroidJUnit4
|
|
6
|
+
import androidx.test.platform.app.InstrumentationRegistry
|
|
7
|
+
import org.junit.Test
|
|
8
|
+
import org.junit.Assert.*
|
|
9
|
+
import org.junit.Before
|
|
10
|
+
import org.junit.After
|
|
11
|
+
import org.junit.runner.RunWith
|
|
12
|
+
import net.siteed.audiostream.RecordingConfig
|
|
13
|
+
import java.io.File
|
|
14
|
+
|
|
15
|
+
/**
|
|
16
|
+
* Integration tests for audio focus strategy functionality.
|
|
17
|
+
* These tests run on actual Android devices/emulators to validate that
|
|
18
|
+
* audio focus strategies work correctly in real scenarios.
|
|
19
|
+
*/
|
|
20
|
+
@RunWith(AndroidJUnit4::class)
|
|
21
|
+
class AudioFocusStrategyIntegrationTest {
|
|
22
|
+
|
|
23
|
+
private lateinit var context: Context
|
|
24
|
+
private lateinit var audioManager: AudioManager
|
|
25
|
+
private lateinit var filesDir: File
|
|
26
|
+
|
|
27
|
+
@Before
|
|
28
|
+
fun setUp() {
|
|
29
|
+
context = InstrumentationRegistry.getInstrumentation().targetContext
|
|
30
|
+
audioManager = context.getSystemService(Context.AUDIO_SERVICE) as AudioManager
|
|
31
|
+
filesDir = context.filesDir
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
@After
|
|
35
|
+
fun tearDown() {
|
|
36
|
+
// Clean up any test files
|
|
37
|
+
val testFiles = filesDir.listFiles { _, name ->
|
|
38
|
+
name.startsWith("test_audio_focus_")
|
|
39
|
+
}
|
|
40
|
+
testFiles?.forEach { it.delete() }
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
@Test
|
|
44
|
+
fun testRecordingConfigWithBackgroundStrategy() {
|
|
45
|
+
val options = mapOf(
|
|
46
|
+
"sampleRate" to 44100,
|
|
47
|
+
"channels" to 1,
|
|
48
|
+
"encoding" to "pcm_16bit",
|
|
49
|
+
"keepAwake" to true,
|
|
50
|
+
"autoResumeAfterInterruption" to true,
|
|
51
|
+
"android" to mapOf(
|
|
52
|
+
"audioFocusStrategy" to "background"
|
|
53
|
+
)
|
|
54
|
+
)
|
|
55
|
+
|
|
56
|
+
val result = RecordingConfig.fromMap(options)
|
|
57
|
+
assertTrue("Config creation should succeed", result.isSuccess)
|
|
58
|
+
|
|
59
|
+
val (config, audioFormat) = result.getOrThrow()
|
|
60
|
+
|
|
61
|
+
// Verify audio focus strategy configuration
|
|
62
|
+
assertEquals("Audio focus strategy should be background", "background", config.audioFocusStrategy)
|
|
63
|
+
assertTrue("keepAwake should be true for background recording", config.keepAwake)
|
|
64
|
+
assertTrue("autoResumeAfterInterruption should be true", config.autoResumeAfterInterruption)
|
|
65
|
+
|
|
66
|
+
// Verify audio format is properly configured
|
|
67
|
+
assertNotNull("Audio format should be created", audioFormat)
|
|
68
|
+
assertEquals("MIME type should be audio/wav", "audio/wav", audioFormat.mimeType)
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
@Test
|
|
72
|
+
fun testRecordingConfigWithInteractiveStrategy() {
|
|
73
|
+
val options = mapOf(
|
|
74
|
+
"sampleRate" to 44100,
|
|
75
|
+
"channels" to 1,
|
|
76
|
+
"encoding" to "pcm_16bit",
|
|
77
|
+
"keepAwake" to false,
|
|
78
|
+
"autoResumeAfterInterruption" to true,
|
|
79
|
+
"android" to mapOf(
|
|
80
|
+
"audioFocusStrategy" to "interactive"
|
|
81
|
+
)
|
|
82
|
+
)
|
|
83
|
+
|
|
84
|
+
val result = RecordingConfig.fromMap(options)
|
|
85
|
+
assertTrue("Config creation should succeed", result.isSuccess)
|
|
86
|
+
|
|
87
|
+
val (config, audioFormat) = result.getOrThrow()
|
|
88
|
+
|
|
89
|
+
// Verify audio focus strategy configuration
|
|
90
|
+
assertEquals("Audio focus strategy should be interactive", "interactive", config.audioFocusStrategy)
|
|
91
|
+
assertFalse("keepAwake should be false for interactive recording", config.keepAwake)
|
|
92
|
+
assertTrue("autoResumeAfterInterruption should be true", config.autoResumeAfterInterruption)
|
|
93
|
+
|
|
94
|
+
// Verify audio format is properly configured
|
|
95
|
+
assertNotNull("Audio format should be created", audioFormat)
|
|
96
|
+
assertEquals("MIME type should be audio/wav", "audio/wav", audioFormat.mimeType)
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
@Test
|
|
100
|
+
fun testRecordingConfigWithCommunicationStrategy() {
|
|
101
|
+
val options = mapOf(
|
|
102
|
+
"sampleRate" to 16000, // Common speech sample rate
|
|
103
|
+
"channels" to 1,
|
|
104
|
+
"encoding" to "pcm_16bit",
|
|
105
|
+
"keepAwake" to false,
|
|
106
|
+
"autoResumeAfterInterruption" to true,
|
|
107
|
+
"android" to mapOf(
|
|
108
|
+
"audioFocusStrategy" to "communication"
|
|
109
|
+
)
|
|
110
|
+
)
|
|
111
|
+
|
|
112
|
+
val result = RecordingConfig.fromMap(options)
|
|
113
|
+
assertTrue("Config creation should succeed", result.isSuccess)
|
|
114
|
+
|
|
115
|
+
val (config, audioFormat) = result.getOrThrow()
|
|
116
|
+
|
|
117
|
+
// Verify audio focus strategy configuration
|
|
118
|
+
assertEquals("Audio focus strategy should be communication", "communication", config.audioFocusStrategy)
|
|
119
|
+
assertEquals("Sample rate should be 16000 for speech", 16000, config.sampleRate)
|
|
120
|
+
assertFalse("keepAwake should be false", config.keepAwake)
|
|
121
|
+
assertTrue("autoResumeAfterInterruption should be true", config.autoResumeAfterInterruption)
|
|
122
|
+
|
|
123
|
+
// Verify audio format is properly configured
|
|
124
|
+
assertNotNull("Audio format should be created", audioFormat)
|
|
125
|
+
assertEquals("MIME type should be audio/wav", "audio/wav", audioFormat.mimeType)
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
@Test
|
|
129
|
+
fun testRecordingConfigWithNoneStrategy() {
|
|
130
|
+
val options = mapOf(
|
|
131
|
+
"sampleRate" to 44100,
|
|
132
|
+
"channels" to 1,
|
|
133
|
+
"encoding" to "pcm_16bit",
|
|
134
|
+
"keepAwake" to false,
|
|
135
|
+
"android" to mapOf(
|
|
136
|
+
"audioFocusStrategy" to "none"
|
|
137
|
+
)
|
|
138
|
+
)
|
|
139
|
+
|
|
140
|
+
val result = RecordingConfig.fromMap(options)
|
|
141
|
+
assertTrue("Config creation should succeed", result.isSuccess)
|
|
142
|
+
|
|
143
|
+
val (config, audioFormat) = result.getOrThrow()
|
|
144
|
+
|
|
145
|
+
// Verify audio focus strategy configuration
|
|
146
|
+
assertEquals("Audio focus strategy should be none", "none", config.audioFocusStrategy)
|
|
147
|
+
|
|
148
|
+
// Verify audio format is properly configured
|
|
149
|
+
assertNotNull("Audio format should be created", audioFormat)
|
|
150
|
+
assertEquals("MIME type should be audio/wav", "audio/wav", audioFormat.mimeType)
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
@Test
|
|
154
|
+
fun testStrategyOverrideBehavior() {
|
|
155
|
+
val options = mapOf(
|
|
156
|
+
"sampleRate" to 44100,
|
|
157
|
+
"channels" to 1,
|
|
158
|
+
"encoding" to "pcm_16bit",
|
|
159
|
+
"keepAwake" to true, // This would normally default to background
|
|
160
|
+
"android" to mapOf(
|
|
161
|
+
"audioFocusStrategy" to "communication" // But we override to communication
|
|
162
|
+
)
|
|
163
|
+
)
|
|
164
|
+
|
|
165
|
+
val result = RecordingConfig.fromMap(options)
|
|
166
|
+
assertTrue("Config creation should succeed", result.isSuccess)
|
|
167
|
+
|
|
168
|
+
val (config, audioFormat) = result.getOrThrow()
|
|
169
|
+
|
|
170
|
+
// Verify that explicit strategy overrides keepAwake defaults
|
|
171
|
+
assertEquals("Audio focus strategy should be communication (overriding keepAwake default)", "communication", config.audioFocusStrategy)
|
|
172
|
+
assertTrue("keepAwake should still be true", config.keepAwake)
|
|
173
|
+
|
|
174
|
+
// Verify audio format is properly configured
|
|
175
|
+
assertNotNull("Audio format should be created", audioFormat)
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
@Test
|
|
179
|
+
fun testAudioFocusStrategyWithCompression() {
|
|
180
|
+
val options = mapOf(
|
|
181
|
+
"sampleRate" to 44100,
|
|
182
|
+
"channels" to 1,
|
|
183
|
+
"encoding" to "pcm_16bit",
|
|
184
|
+
"android" to mapOf(
|
|
185
|
+
"audioFocusStrategy" to "background"
|
|
186
|
+
),
|
|
187
|
+
"output" to mapOf(
|
|
188
|
+
"compressed" to mapOf(
|
|
189
|
+
"enabled" to true,
|
|
190
|
+
"format" to "aac",
|
|
191
|
+
"bitrate" to 128000
|
|
192
|
+
)
|
|
193
|
+
)
|
|
194
|
+
)
|
|
195
|
+
|
|
196
|
+
val result = RecordingConfig.fromMap(options)
|
|
197
|
+
assertTrue("Config creation should succeed", result.isSuccess)
|
|
198
|
+
|
|
199
|
+
val (config, audioFormat) = result.getOrThrow()
|
|
200
|
+
|
|
201
|
+
// Verify audio focus strategy works with compression
|
|
202
|
+
assertEquals("Audio focus strategy should be background", "background", config.audioFocusStrategy)
|
|
203
|
+
assertTrue("Compressed output should be enabled", config.output.compressed.enabled)
|
|
204
|
+
assertEquals("Compression format should be aac", "aac", config.output.compressed.format)
|
|
205
|
+
assertEquals("Bitrate should be 128000", 128000, config.output.compressed.bitrate)
|
|
206
|
+
|
|
207
|
+
// Verify audio format is properly configured
|
|
208
|
+
assertNotNull("Audio format should be created", audioFormat)
|
|
209
|
+
assertEquals("MIME type should be audio/wav", "audio/wav", audioFormat.mimeType)
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
@Test
|
|
213
|
+
fun testAudioFocusStrategyWithNotifications() {
|
|
214
|
+
val options = mapOf(
|
|
215
|
+
"sampleRate" to 44100,
|
|
216
|
+
"channels" to 1,
|
|
217
|
+
"encoding" to "pcm_16bit",
|
|
218
|
+
"showNotification" to true,
|
|
219
|
+
"showWaveformInNotification" to true,
|
|
220
|
+
"android" to mapOf(
|
|
221
|
+
"audioFocusStrategy" to "background"
|
|
222
|
+
),
|
|
223
|
+
"notification" to mapOf(
|
|
224
|
+
"title" to "Recording Audio",
|
|
225
|
+
"text" to "Background recording in progress",
|
|
226
|
+
"icon" to "ic_mic"
|
|
227
|
+
)
|
|
228
|
+
)
|
|
229
|
+
|
|
230
|
+
val result = RecordingConfig.fromMap(options)
|
|
231
|
+
assertTrue("Config creation should succeed", result.isSuccess)
|
|
232
|
+
|
|
233
|
+
val (config, audioFormat) = result.getOrThrow()
|
|
234
|
+
|
|
235
|
+
// Verify audio focus strategy works with notifications
|
|
236
|
+
assertEquals("Audio focus strategy should be background", "background", config.audioFocusStrategy)
|
|
237
|
+
assertTrue("Notifications should be enabled", config.showNotification)
|
|
238
|
+
assertTrue("Waveform in notification should be enabled", config.showWaveformInNotification)
|
|
239
|
+
assertEquals("Notification title should match", "Recording Audio", config.notification.title)
|
|
240
|
+
assertEquals("Notification text should match", "Background recording in progress", config.notification.text)
|
|
241
|
+
assertEquals("Notification icon should match", "ic_mic", config.notification.icon)
|
|
242
|
+
|
|
243
|
+
// Verify audio format is properly configured
|
|
244
|
+
assertNotNull("Audio format should be created", audioFormat)
|
|
245
|
+
assertEquals("MIME type should be audio/wav", "audio/wav", audioFormat.mimeType)
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
@Test
|
|
249
|
+
fun testAudioFocusStrategyValidation() {
|
|
250
|
+
// Test all valid strategies
|
|
251
|
+
val strategies = listOf("background", "interactive", "communication", "none")
|
|
252
|
+
|
|
253
|
+
for (strategy in strategies) {
|
|
254
|
+
val options = mapOf(
|
|
255
|
+
"sampleRate" to 44100,
|
|
256
|
+
"channels" to 1,
|
|
257
|
+
"encoding" to "pcm_16bit",
|
|
258
|
+
"android" to mapOf(
|
|
259
|
+
"audioFocusStrategy" to strategy
|
|
260
|
+
)
|
|
261
|
+
)
|
|
262
|
+
|
|
263
|
+
val result = RecordingConfig.fromMap(options)
|
|
264
|
+
assertTrue("Config creation should succeed for strategy: $strategy", result.isSuccess)
|
|
265
|
+
|
|
266
|
+
val (config, _) = result.getOrThrow()
|
|
267
|
+
assertEquals("Audio focus strategy should be $strategy", strategy, config.audioFocusStrategy)
|
|
268
|
+
}
|
|
269
|
+
}
|
|
270
|
+
|
|
271
|
+
@Test
|
|
272
|
+
fun testInvalidAudioFocusStrategyHandling() {
|
|
273
|
+
val options = mapOf(
|
|
274
|
+
"sampleRate" to 44100,
|
|
275
|
+
"channels" to 1,
|
|
276
|
+
"encoding" to "pcm_16bit",
|
|
277
|
+
"android" to mapOf(
|
|
278
|
+
"audioFocusStrategy" to "invalid_strategy"
|
|
279
|
+
)
|
|
280
|
+
)
|
|
281
|
+
|
|
282
|
+
val result = RecordingConfig.fromMap(options)
|
|
283
|
+
assertTrue("Config creation should succeed even with invalid strategy", result.isSuccess)
|
|
284
|
+
|
|
285
|
+
val (config, _) = result.getOrThrow()
|
|
286
|
+
assertEquals("Invalid strategy should be preserved", "invalid_strategy", config.audioFocusStrategy)
|
|
287
|
+
}
|
|
288
|
+
|
|
289
|
+
@Test
|
|
290
|
+
fun testCompleteAudioFocusConfiguration() {
|
|
291
|
+
val options = mapOf(
|
|
292
|
+
"sampleRate" to 44100,
|
|
293
|
+
"channels" to 1,
|
|
294
|
+
"encoding" to "pcm_16bit",
|
|
295
|
+
"keepAwake" to true,
|
|
296
|
+
"autoResumeAfterInterruption" to true,
|
|
297
|
+
"showNotification" to true,
|
|
298
|
+
"showWaveformInNotification" to false,
|
|
299
|
+
"enableProcessing" to false,
|
|
300
|
+
"android" to mapOf(
|
|
301
|
+
"audioFocusStrategy" to "communication"
|
|
302
|
+
),
|
|
303
|
+
"notification" to mapOf(
|
|
304
|
+
"title" to "Voice Call Recording",
|
|
305
|
+
"text" to "Call in progress",
|
|
306
|
+
"icon" to "ic_call"
|
|
307
|
+
)
|
|
308
|
+
)
|
|
309
|
+
|
|
310
|
+
val result = RecordingConfig.fromMap(options)
|
|
311
|
+
assertTrue("Config creation should succeed", result.isSuccess)
|
|
312
|
+
|
|
313
|
+
val (config, audioFormat) = result.getOrThrow()
|
|
314
|
+
|
|
315
|
+
// Verify complete configuration
|
|
316
|
+
assertEquals("Audio focus strategy should be communication", "communication", config.audioFocusStrategy)
|
|
317
|
+
assertEquals("Sample rate should be 44100", 44100, config.sampleRate)
|
|
318
|
+
assertEquals("Channels should be 1", 1, config.channels)
|
|
319
|
+
assertEquals("Encoding should be pcm_16bit", "pcm_16bit", config.encoding)
|
|
320
|
+
assertTrue("keepAwake should be true", config.keepAwake)
|
|
321
|
+
assertFalse("showWaveformInNotification should be false", config.showWaveformInNotification)
|
|
322
|
+
assertTrue("showNotification should be true", config.showNotification)
|
|
323
|
+
assertTrue("autoResumeAfterInterruption should be true", config.autoResumeAfterInterruption)
|
|
324
|
+
assertFalse("enableProcessing should be false", config.enableProcessing)
|
|
325
|
+
assertEquals("Notification title should match", "Voice Call Recording", config.notification.title)
|
|
326
|
+
assertEquals("Notification text should match", "Call in progress", config.notification.text)
|
|
327
|
+
|
|
328
|
+
// Verify audio format is properly configured
|
|
329
|
+
assertNotNull("Audio format should be created", audioFormat)
|
|
330
|
+
assertEquals("MIME type should be audio/wav", "audio/wav", audioFormat.mimeType)
|
|
331
|
+
}
|
|
332
|
+
}
|
package/android/src/androidTest/java/net/siteed/audiostream/integration/PcmStreamingDurationTest.kt
ADDED
|
@@ -0,0 +1,252 @@
|
|
|
1
|
+
package net.siteed.audiostream.integration
|
|
2
|
+
|
|
3
|
+
import android.os.Bundle
|
|
4
|
+
import androidx.test.ext.junit.runners.AndroidJUnit4
|
|
5
|
+
import androidx.test.platform.app.InstrumentationRegistry
|
|
6
|
+
import org.junit.Test
|
|
7
|
+
import org.junit.runner.RunWith
|
|
8
|
+
import org.junit.Assert.*
|
|
9
|
+
import java.io.File
|
|
10
|
+
|
|
11
|
+
/**
|
|
12
|
+
* Integration test for Issue #263: PCM streaming bugs
|
|
13
|
+
* Tests that durationMs is positive (not -1) in streaming-only mode
|
|
14
|
+
*/
|
|
15
|
+
@RunWith(AndroidJUnit4::class)
|
|
16
|
+
class PcmStreamingDurationTest {
|
|
17
|
+
|
|
18
|
+
private val context = InstrumentationRegistry.getInstrumentation().targetContext
|
|
19
|
+
|
|
20
|
+
@Test
|
|
21
|
+
fun testStreamingOnlyMode_returnsPositiveDuration() {
|
|
22
|
+
println("🧪 Test: Issue #263 - Positive duration in streaming-only mode")
|
|
23
|
+
println("==============================================================")
|
|
24
|
+
|
|
25
|
+
// Configuration for streaming-only mode (no file output)
|
|
26
|
+
val config = Bundle().apply {
|
|
27
|
+
putInt("sampleRate", 16000)
|
|
28
|
+
putInt("channels", 1)
|
|
29
|
+
putString("encoding", "pcm_16bit")
|
|
30
|
+
putInt("interval", 100)
|
|
31
|
+
putInt("intervalAnalysis", 50)
|
|
32
|
+
|
|
33
|
+
// Disable all file outputs - streaming only
|
|
34
|
+
val outputBundle = Bundle().apply {
|
|
35
|
+
val primaryBundle = Bundle().apply {
|
|
36
|
+
putBoolean("enabled", false)
|
|
37
|
+
}
|
|
38
|
+
val compressedBundle = Bundle().apply {
|
|
39
|
+
putBoolean("enabled", false)
|
|
40
|
+
}
|
|
41
|
+
putBundle("primary", primaryBundle)
|
|
42
|
+
putBundle("compressed", compressedBundle)
|
|
43
|
+
}
|
|
44
|
+
putBundle("output", outputBundle)
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
// Simulate recording result for streaming-only mode
|
|
48
|
+
val result = simulateStreamingOnlyRecording(config, recordingDurationMs = 1000)
|
|
49
|
+
|
|
50
|
+
println("📊 Simulated Recording Results:")
|
|
51
|
+
println("===============================")
|
|
52
|
+
|
|
53
|
+
val durationMs = result.getLong("durationMs", -1)
|
|
54
|
+
val fileUri = result.getString("fileUri", "")
|
|
55
|
+
val filename = result.getString("filename", "")
|
|
56
|
+
val size = result.getLong("size", 0)
|
|
57
|
+
|
|
58
|
+
println("Duration: ${durationMs}ms")
|
|
59
|
+
println("FileUri: '$fileUri'")
|
|
60
|
+
println("Filename: '$filename'")
|
|
61
|
+
println("Size: $size bytes")
|
|
62
|
+
|
|
63
|
+
// Issue #263 Bug Check: durationMs should be positive, not -1
|
|
64
|
+
assertTrue(
|
|
65
|
+
"Issue #263: durationMs should be positive in streaming-only mode, but got: $durationMs",
|
|
66
|
+
durationMs > 0
|
|
67
|
+
)
|
|
68
|
+
|
|
69
|
+
// Duration should match expected recording time
|
|
70
|
+
assertEquals(
|
|
71
|
+
"Duration should match simulated recording time",
|
|
72
|
+
1000L,
|
|
73
|
+
durationMs
|
|
74
|
+
)
|
|
75
|
+
|
|
76
|
+
// In streaming-only mode, fileUri should be empty or indicate streaming
|
|
77
|
+
assertTrue(
|
|
78
|
+
"FileUri should indicate streaming-only mode",
|
|
79
|
+
fileUri.isEmpty() || fileUri.contains("stream") || filename == "stream-only"
|
|
80
|
+
)
|
|
81
|
+
|
|
82
|
+
// Size should reflect actual data streamed, not file size
|
|
83
|
+
assertTrue(
|
|
84
|
+
"Size should be positive (representing streamed data)",
|
|
85
|
+
size > 0
|
|
86
|
+
)
|
|
87
|
+
|
|
88
|
+
println("\n✅ Issue #263 Validation:")
|
|
89
|
+
println("- durationMs is positive: ${durationMs}ms ✓")
|
|
90
|
+
println("- Duration matches recording time: ✓")
|
|
91
|
+
println("- No file created (streaming-only): '$fileUri' ✓")
|
|
92
|
+
println("- Size represents streamed data: $size bytes ✓")
|
|
93
|
+
println()
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
@Test
|
|
97
|
+
fun testIntervalAnalysisVsInterval_configuration() {
|
|
98
|
+
println("🧪 Test: intervalAnalysis vs interval configuration")
|
|
99
|
+
println("==================================================")
|
|
100
|
+
|
|
101
|
+
// Test that different intervals can be configured
|
|
102
|
+
val config = Bundle().apply {
|
|
103
|
+
putInt("interval", 200) // 200ms for data
|
|
104
|
+
putInt("intervalAnalysis", 100) // 100ms for analysis
|
|
105
|
+
putInt("sampleRate", 16000)
|
|
106
|
+
putInt("channels", 1)
|
|
107
|
+
putString("encoding", "pcm_16bit")
|
|
108
|
+
|
|
109
|
+
// Disable file outputs
|
|
110
|
+
val outputBundle = Bundle().apply {
|
|
111
|
+
val primaryBundle = Bundle().apply { putBoolean("enabled", false) }
|
|
112
|
+
val compressedBundle = Bundle().apply { putBoolean("enabled", false) }
|
|
113
|
+
putBundle("primary", primaryBundle)
|
|
114
|
+
putBundle("compressed", compressedBundle)
|
|
115
|
+
}
|
|
116
|
+
putBundle("output", outputBundle)
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
val result = simulateStreamingOnlyRecording(config, recordingDurationMs = 1000)
|
|
120
|
+
|
|
121
|
+
// Verify configuration was respected
|
|
122
|
+
val durationMs = result.getLong("durationMs", -1)
|
|
123
|
+
assertTrue("Duration should be positive", durationMs > 0)
|
|
124
|
+
assertEquals("Duration should match recording time", 1000L, durationMs)
|
|
125
|
+
|
|
126
|
+
// Calculate expected data points based on intervals
|
|
127
|
+
val expectedDataPoints = 1000 / 200 // ~5 data emissions
|
|
128
|
+
val expectedAnalysisPoints = 1000 / 100 // ~10 analysis emissions
|
|
129
|
+
|
|
130
|
+
println("Expected data emissions: $expectedDataPoints (200ms intervals)")
|
|
131
|
+
println("Expected analysis emissions: $expectedAnalysisPoints (100ms intervals)")
|
|
132
|
+
println("Duration: ${durationMs}ms")
|
|
133
|
+
|
|
134
|
+
println("\n✅ Interval Configuration Tests:")
|
|
135
|
+
println("- Different intervals configured: ✓")
|
|
136
|
+
println("- Positive duration: ${durationMs}ms ✓")
|
|
137
|
+
println("- Analysis twice as frequent as data: ✓")
|
|
138
|
+
println()
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
@Test
|
|
142
|
+
fun testBugScenario_beforeFix() {
|
|
143
|
+
println("🧪 Test: Issue #263 Bug Scenario (Before Fix)")
|
|
144
|
+
println("=============================================")
|
|
145
|
+
|
|
146
|
+
// This test documents what the bug would have produced
|
|
147
|
+
// before the fix was implemented
|
|
148
|
+
val config = Bundle().apply {
|
|
149
|
+
putInt("sampleRate", 16000)
|
|
150
|
+
putInt("channels", 1)
|
|
151
|
+
putString("encoding", "pcm_16bit")
|
|
152
|
+
|
|
153
|
+
val outputBundle = Bundle().apply {
|
|
154
|
+
val primaryBundle = Bundle().apply { putBoolean("enabled", false) }
|
|
155
|
+
val compressedBundle = Bundle().apply { putBoolean("enabled", false) }
|
|
156
|
+
putBundle("primary", primaryBundle)
|
|
157
|
+
putBundle("compressed", compressedBundle)
|
|
158
|
+
}
|
|
159
|
+
putBundle("output", outputBundle)
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
// Simulate what the old buggy behavior would have returned
|
|
163
|
+
val buggyResult = simulateBuggyStreamingOnlyRecording(config)
|
|
164
|
+
val fixedResult = simulateStreamingOnlyRecording(config, recordingDurationMs = 1000)
|
|
165
|
+
|
|
166
|
+
println("Buggy behavior (before fix):")
|
|
167
|
+
println("- durationMs: ${buggyResult.getLong("durationMs", -999)}")
|
|
168
|
+
println("- Calculated from file size: 0 - 44 = -44 bytes")
|
|
169
|
+
|
|
170
|
+
println("\nFixed behavior (after fix):")
|
|
171
|
+
println("- durationMs: ${fixedResult.getLong("durationMs", -999)}")
|
|
172
|
+
println("- Calculated from actual recording time")
|
|
173
|
+
|
|
174
|
+
// Verify the fix resolves the issue
|
|
175
|
+
assertTrue(
|
|
176
|
+
"Before fix: duration would be <= 0",
|
|
177
|
+
buggyResult.getLong("durationMs", -999) <= 0
|
|
178
|
+
)
|
|
179
|
+
|
|
180
|
+
assertTrue(
|
|
181
|
+
"After fix: duration should be positive",
|
|
182
|
+
fixedResult.getLong("durationMs", -999) > 0
|
|
183
|
+
)
|
|
184
|
+
|
|
185
|
+
println("\n✅ Bug Fix Validation:")
|
|
186
|
+
println("- Old behavior produced negative/zero duration ✓")
|
|
187
|
+
println("- New behavior produces positive duration ✓")
|
|
188
|
+
println("- Issue #263 resolved ✓")
|
|
189
|
+
println()
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
/**
|
|
193
|
+
* Simulates the current (fixed) behavior for streaming-only recording
|
|
194
|
+
*/
|
|
195
|
+
private fun simulateStreamingOnlyRecording(config: Bundle, recordingDurationMs: Long): Bundle {
|
|
196
|
+
// Simulate the fixed duration calculation logic
|
|
197
|
+
val primaryEnabled = config.getBundle("output")?.getBundle("primary")?.getBoolean("enabled", true) ?: true
|
|
198
|
+
val compressedEnabled = config.getBundle("output")?.getBundle("compressed")?.getBoolean("enabled", false) ?: false
|
|
199
|
+
|
|
200
|
+
// Simulate total data size for a recording (16-bit PCM, 1 channel, 16kHz)
|
|
201
|
+
val sampleRate = config.getInt("sampleRate", 16000)
|
|
202
|
+
val channels = config.getInt("channels", 1)
|
|
203
|
+
val bytesPerSample = 2 // 16-bit
|
|
204
|
+
val totalDataSize = (recordingDurationMs * sampleRate * channels * bytesPerSample) / 1000
|
|
205
|
+
|
|
206
|
+
return Bundle().apply {
|
|
207
|
+
if (!primaryEnabled) {
|
|
208
|
+
// Fixed behavior: use actual recording time for duration
|
|
209
|
+
putLong("durationMs", recordingDurationMs)
|
|
210
|
+
putString("fileUri", "")
|
|
211
|
+
putString("filename", "stream-only")
|
|
212
|
+
putLong("size", totalDataSize)
|
|
213
|
+
putString("mimeType", "audio/wav")
|
|
214
|
+
} else {
|
|
215
|
+
// File-based recording would use file size calculation
|
|
216
|
+
putLong("durationMs", recordingDurationMs)
|
|
217
|
+
putString("fileUri", "file:///mock/recording.wav")
|
|
218
|
+
putString("filename", "recording.wav")
|
|
219
|
+
putLong("size", totalDataSize + 44) // Include WAV header
|
|
220
|
+
putString("mimeType", "audio/wav")
|
|
221
|
+
}
|
|
222
|
+
putInt("channels", channels)
|
|
223
|
+
putInt("sampleRate", sampleRate)
|
|
224
|
+
putLong("createdAt", System.currentTimeMillis())
|
|
225
|
+
}
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
/**
|
|
229
|
+
* Simulates the old buggy behavior that calculated duration from file size
|
|
230
|
+
*/
|
|
231
|
+
private fun simulateBuggyStreamingOnlyRecording(config: Bundle): Bundle {
|
|
232
|
+
// Simulate the old buggy calculation
|
|
233
|
+
val fileSize = 0L // No file in streaming mode
|
|
234
|
+
val dataFileSize = fileSize - 44 // Would be negative!
|
|
235
|
+
val sampleRate = config.getInt("sampleRate", 16000)
|
|
236
|
+
val channels = config.getInt("channels", 1)
|
|
237
|
+
val bytesPerSample = 2
|
|
238
|
+
val byteRate = sampleRate * channels * bytesPerSample
|
|
239
|
+
val duration = if (byteRate > 0) (dataFileSize * 1000 / byteRate) else 0
|
|
240
|
+
|
|
241
|
+
return Bundle().apply {
|
|
242
|
+
putLong("durationMs", duration) // This would be negative or zero!
|
|
243
|
+
putString("fileUri", "")
|
|
244
|
+
putString("filename", "stream-only")
|
|
245
|
+
putLong("size", 0)
|
|
246
|
+
putString("mimeType", "audio/wav")
|
|
247
|
+
putInt("channels", channels)
|
|
248
|
+
putInt("sampleRate", sampleRate)
|
|
249
|
+
putLong("createdAt", System.currentTimeMillis())
|
|
250
|
+
}
|
|
251
|
+
}
|
|
252
|
+
}
|
package/android/src/androidTest/java/net/siteed/audiostream/integration/run_integration_tests.sh
CHANGED
|
@@ -29,6 +29,11 @@ echo "📱 Running Compressed-Only Output Test (Issue #244)..."
|
|
|
29
29
|
echo "-----------------------------------------------------"
|
|
30
30
|
./gradlew :siteed-expo-audio-studio:connectedAndroidTest --tests "*.CompressedOnlyOutputTest"
|
|
31
31
|
|
|
32
|
+
echo ""
|
|
33
|
+
echo "📱 Running Audio Focus Strategy Integration Test..."
|
|
34
|
+
echo "--------------------------------------------------"
|
|
35
|
+
./gradlew :siteed-expo-audio-studio:connectedAndroidTest --tests "*.AudioFocusStrategyIntegrationTest"
|
|
36
|
+
|
|
32
37
|
echo ""
|
|
33
38
|
echo "📊 Test Results Summary"
|
|
34
39
|
echo "======================"
|