@siteed/expo-audio-studio 2.13.2 → 2.14.1
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
CHANGED
|
@@ -8,6 +8,12 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
|
|
8
8
|
## [Unreleased]
|
|
9
9
|
|
|
10
10
|
|
|
11
|
+
## [2.14.1] - 2025-06-11
|
|
12
|
+
### Changed
|
|
13
|
+
- fix(android): Fix duration returning 0 when primary output is disabled (#244) ([38d6f50](https://github.com/deeeed/expo-audio-stream/commit/38d6f50c084a10329be33a0f1c123aa9f457c371))
|
|
14
|
+
## [2.14.0] - 2025-06-11
|
|
15
|
+
### Changed
|
|
16
|
+
- feat(expo-audio-studio): comprehensive cross-platform stop recording performance optimization ([b3ed474](https://github.com/deeeed/expo-audio-stream/commit/b3ed474d91994698fe082354621adc98e758557e))
|
|
11
17
|
## [2.13.2] - 2025-06-10
|
|
12
18
|
### Changed
|
|
13
19
|
- fix: invalid type exports ([18340ea](https://github.com/deeeed/expo-audio-stream/commit/18340eac9cfef39691637400fc6af811d5b004df))
|
|
@@ -316,7 +322,9 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
|
|
316
322
|
- Feature: Audio features extraction during recording.
|
|
317
323
|
- Feature: Consistent WAV PCM recording format across all platforms.
|
|
318
324
|
|
|
319
|
-
[unreleased]: https://github.com/deeeed/expo-audio-stream/compare/@siteed/expo-audio-studio@2.
|
|
325
|
+
[unreleased]: https://github.com/deeeed/expo-audio-stream/compare/@siteed/expo-audio-studio@2.14.1...HEAD
|
|
326
|
+
[2.14.1]: https://github.com/deeeed/expo-audio-stream/compare/@siteed/expo-audio-studio@2.14.0...@siteed/expo-audio-studio@2.14.1
|
|
327
|
+
[2.14.0]: https://github.com/deeeed/expo-audio-stream/compare/@siteed/expo-audio-studio@2.13.2...@siteed/expo-audio-studio@2.14.0
|
|
320
328
|
[2.13.2]: https://github.com/deeeed/expo-audio-stream/compare/@siteed/expo-audio-studio@2.13.1...@siteed/expo-audio-studio@2.13.2
|
|
321
329
|
[2.13.1]: https://github.com/deeeed/expo-audio-stream/compare/@siteed/expo-audio-studio@2.13.0...@siteed/expo-audio-studio@2.13.1
|
|
322
330
|
[2.13.0]: https://github.com/deeeed/expo-audio-stream/compare/@siteed/expo-audio-studio@2.12.3...@siteed/expo-audio-studio@2.13.0
|
|
@@ -0,0 +1,234 @@
|
|
|
1
|
+
package net.siteed.audiostream
|
|
2
|
+
|
|
3
|
+
import android.Manifest
|
|
4
|
+
import android.content.Context
|
|
5
|
+
import android.util.Log
|
|
6
|
+
import androidx.test.ext.junit.runners.AndroidJUnit4
|
|
7
|
+
import androidx.test.platform.app.InstrumentationRegistry
|
|
8
|
+
import androidx.test.rule.GrantPermissionRule
|
|
9
|
+
import expo.modules.kotlin.Promise
|
|
10
|
+
import org.junit.After
|
|
11
|
+
import org.junit.Assert.*
|
|
12
|
+
import org.junit.Before
|
|
13
|
+
import org.junit.Rule
|
|
14
|
+
import org.junit.Test
|
|
15
|
+
import org.junit.runner.RunWith
|
|
16
|
+
import java.io.File
|
|
17
|
+
import java.util.concurrent.CountDownLatch
|
|
18
|
+
import java.util.concurrent.TimeUnit
|
|
19
|
+
import kotlin.system.measureTimeMillis
|
|
20
|
+
|
|
21
|
+
/**
|
|
22
|
+
* Performance tests for measuring stop recording times.
|
|
23
|
+
*/
|
|
24
|
+
@RunWith(AndroidJUnit4::class)
|
|
25
|
+
class AudioRecorderPerformanceInstrumentedTest {
|
|
26
|
+
|
|
27
|
+
@get:Rule
|
|
28
|
+
val grantPermissionRule: GrantPermissionRule = GrantPermissionRule.grant(
|
|
29
|
+
Manifest.permission.RECORD_AUDIO
|
|
30
|
+
)
|
|
31
|
+
|
|
32
|
+
private lateinit var context: Context
|
|
33
|
+
private lateinit var filesDir: File
|
|
34
|
+
private lateinit var audioRecorderManager: AudioRecorderManager
|
|
35
|
+
private lateinit var testEventSender: TestEventSender
|
|
36
|
+
private lateinit var permissionUtils: PermissionUtils
|
|
37
|
+
private lateinit var audioDataEncoder: AudioDataEncoder
|
|
38
|
+
|
|
39
|
+
companion object {
|
|
40
|
+
private const val TAG = "PerformanceTest"
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
// Test event sender to capture events
|
|
44
|
+
private class TestEventSender : EventSender {
|
|
45
|
+
override fun sendExpoEvent(eventName: String, params: android.os.Bundle) {
|
|
46
|
+
// No-op for performance tests
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
@Before
|
|
51
|
+
fun setUp() {
|
|
52
|
+
context = InstrumentationRegistry.getInstrumentation().targetContext
|
|
53
|
+
filesDir = context.filesDir
|
|
54
|
+
testEventSender = TestEventSender()
|
|
55
|
+
permissionUtils = PermissionUtils(context)
|
|
56
|
+
audioDataEncoder = AudioDataEncoder()
|
|
57
|
+
|
|
58
|
+
// Initialize AudioRecorderManager
|
|
59
|
+
audioRecorderManager = AudioRecorderManager.initialize(
|
|
60
|
+
context = context,
|
|
61
|
+
filesDir = filesDir,
|
|
62
|
+
permissionUtils = permissionUtils,
|
|
63
|
+
audioDataEncoder = audioDataEncoder,
|
|
64
|
+
eventSender = testEventSender,
|
|
65
|
+
enablePhoneStateHandling = false,
|
|
66
|
+
enableBackgroundAudio = false
|
|
67
|
+
)
|
|
68
|
+
|
|
69
|
+
// Clean up any existing audio files
|
|
70
|
+
cleanupAudioFiles()
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
@After
|
|
74
|
+
fun tearDown() {
|
|
75
|
+
// Stop any ongoing recording
|
|
76
|
+
if (audioRecorderManager.isRecording) {
|
|
77
|
+
val promise = object : Promise {
|
|
78
|
+
override fun resolve(value: Any?) {}
|
|
79
|
+
override fun reject(code: String, message: String?, cause: Throwable?) {}
|
|
80
|
+
}
|
|
81
|
+
audioRecorderManager.stopRecording(promise)
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
// Clean up
|
|
85
|
+
AudioRecorderManager.destroy()
|
|
86
|
+
cleanupAudioFiles()
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
private fun cleanupAudioFiles() {
|
|
90
|
+
filesDir.listFiles()?.forEach { file ->
|
|
91
|
+
if (file.name.endsWith(".wav") || file.name.endsWith(".aac") || file.name.endsWith(".opus")) {
|
|
92
|
+
file.delete()
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
@Test
|
|
98
|
+
fun measureStopTime_5seconds() {
|
|
99
|
+
runPerformanceTest(5_000L, "5 second recording")
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
@Test
|
|
103
|
+
fun measureStopTime_30seconds() {
|
|
104
|
+
runPerformanceTest(30_000L, "30 second recording")
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
@Test
|
|
108
|
+
fun measureStopTime_1minute() {
|
|
109
|
+
runPerformanceTest(60_000L, "1 minute recording")
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
@Test
|
|
113
|
+
fun measureStopTime_2minutes() {
|
|
114
|
+
runPerformanceTest(120_000L, "2 minute recording")
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
@Test
|
|
118
|
+
fun measureStopTime_5minutes() {
|
|
119
|
+
runPerformanceTest(300_000L, "5 minute recording")
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
@Test
|
|
123
|
+
fun measureStopTime_10minutes() {
|
|
124
|
+
runPerformanceTest(600_000L, "10 minute recording")
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
@Test
|
|
128
|
+
fun measureStopTime_15minutes() {
|
|
129
|
+
runPerformanceTest(900_000L, "15 minute recording")
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
private fun runPerformanceTest(recordingDurationMs: Long, testName: String) {
|
|
133
|
+
val recordingOptions = mapOf(
|
|
134
|
+
"sampleRate" to 44100,
|
|
135
|
+
"channels" to 1,
|
|
136
|
+
"encoding" to "pcm_16bit",
|
|
137
|
+
"interval" to 1000,
|
|
138
|
+
"enableProcessing" to false,
|
|
139
|
+
"showNotification" to false,
|
|
140
|
+
"output" to mapOf(
|
|
141
|
+
"primary" to mapOf("enabled" to true),
|
|
142
|
+
"compressed" to mapOf("enabled" to false)
|
|
143
|
+
)
|
|
144
|
+
)
|
|
145
|
+
|
|
146
|
+
// Start recording
|
|
147
|
+
val startLatch = CountDownLatch(1)
|
|
148
|
+
audioRecorderManager.startRecording(recordingOptions, object : Promise {
|
|
149
|
+
override fun resolve(value: Any?) {
|
|
150
|
+
startLatch.countDown()
|
|
151
|
+
}
|
|
152
|
+
override fun reject(code: String, message: String?, cause: Throwable?) {
|
|
153
|
+
fail("Start recording failed: $message")
|
|
154
|
+
}
|
|
155
|
+
})
|
|
156
|
+
|
|
157
|
+
assertTrue("Recording should start", startLatch.await(5, TimeUnit.SECONDS))
|
|
158
|
+
assertTrue("Recording should be active", audioRecorderManager.isRecording)
|
|
159
|
+
|
|
160
|
+
// Record for specified duration
|
|
161
|
+
Thread.sleep(recordingDurationMs)
|
|
162
|
+
|
|
163
|
+
// Measure stop time
|
|
164
|
+
val stopLatch = CountDownLatch(1)
|
|
165
|
+
var fileSize = 0L
|
|
166
|
+
var stopResult: Map<String, Any>? = null
|
|
167
|
+
|
|
168
|
+
val stopDuration = measureTimeMillis {
|
|
169
|
+
audioRecorderManager.stopRecording(object : Promise {
|
|
170
|
+
override fun resolve(value: Any?) {
|
|
171
|
+
when (value) {
|
|
172
|
+
is android.os.Bundle -> {
|
|
173
|
+
fileSize = value.getLong("size", 0)
|
|
174
|
+
stopResult = bundleToMap(value)
|
|
175
|
+
}
|
|
176
|
+
is Map<*, *> -> {
|
|
177
|
+
@Suppress("UNCHECKED_CAST")
|
|
178
|
+
stopResult = value as? Map<String, Any>
|
|
179
|
+
fileSize = (stopResult?.get("size") as? Long) ?: 0
|
|
180
|
+
}
|
|
181
|
+
}
|
|
182
|
+
stopLatch.countDown()
|
|
183
|
+
}
|
|
184
|
+
override fun reject(code: String, message: String?, cause: Throwable?) {
|
|
185
|
+
fail("Stop recording failed: $message")
|
|
186
|
+
}
|
|
187
|
+
})
|
|
188
|
+
|
|
189
|
+
assertTrue("Stop should complete", stopLatch.await(10, TimeUnit.SECONDS))
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
// Log results
|
|
193
|
+
val fileSizeMB = fileSize / (1024.0 * 1024.0)
|
|
194
|
+
Log.i(TAG, """
|
|
195
|
+
Performance Test: $testName
|
|
196
|
+
- Recording Duration: ${recordingDurationMs}ms
|
|
197
|
+
- Stop Duration: ${stopDuration}ms
|
|
198
|
+
- File Size: ${"%.2f".format(fileSizeMB)}MB
|
|
199
|
+
- Performance: ${if (stopDuration < getTargetTime(recordingDurationMs)) "PASS" else "FAIL"}
|
|
200
|
+
""".trimIndent())
|
|
201
|
+
|
|
202
|
+
println("""
|
|
203
|
+
Performance Test: $testName
|
|
204
|
+
- Recording Duration: ${recordingDurationMs}ms
|
|
205
|
+
- Stop Duration: ${stopDuration}ms
|
|
206
|
+
- File Size: ${"%.2f".format(fileSizeMB)}MB
|
|
207
|
+
- Performance: ${if (stopDuration < getTargetTime(recordingDurationMs)) "PASS" else "FAIL"}
|
|
208
|
+
""".trimIndent())
|
|
209
|
+
|
|
210
|
+
assertFalse("Recording should not be active", audioRecorderManager.isRecording)
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
private fun getTargetTime(recordingDurationMs: Long): Long {
|
|
214
|
+
return when {
|
|
215
|
+
recordingDurationMs <= 5_000 -> 100
|
|
216
|
+
recordingDurationMs <= 30_000 -> 150
|
|
217
|
+
recordingDurationMs <= 60_000 -> 200
|
|
218
|
+
recordingDurationMs <= 300_000 -> 500
|
|
219
|
+
else -> 750
|
|
220
|
+
}
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
private fun bundleToMap(bundle: android.os.Bundle): Map<String, Any> {
|
|
224
|
+
val map = mutableMapOf<String, Any>()
|
|
225
|
+
for (key in bundle.keySet()) {
|
|
226
|
+
val value = bundle.get(key)
|
|
227
|
+
when (value) {
|
|
228
|
+
is android.os.Bundle -> map[key] = bundleToMap(value)
|
|
229
|
+
else -> value?.let { map[key] = it }
|
|
230
|
+
}
|
|
231
|
+
}
|
|
232
|
+
return map
|
|
233
|
+
}
|
|
234
|
+
}
|
|
@@ -137,6 +137,10 @@ class AudioRecorderManager(
|
|
|
137
137
|
var isPrepared = false
|
|
138
138
|
private var selectedDeviceId: String? = null
|
|
139
139
|
private var deviceDisconnectionBehavior: String? = null
|
|
140
|
+
|
|
141
|
+
// Cache file sizes to avoid file system calls during stop
|
|
142
|
+
private var cachedPrimaryFileSize: Long = 44L // Start with WAV header size
|
|
143
|
+
private var cachedCompressedFileSize: Long = 0L
|
|
140
144
|
|
|
141
145
|
// Add a method to handle device changes
|
|
142
146
|
fun handleDeviceChange() {
|
|
@@ -819,6 +823,10 @@ class AudioRecorderManager(
|
|
|
819
823
|
streamUuid = java.util.UUID.randomUUID().toString()
|
|
820
824
|
totalDataSize = 0
|
|
821
825
|
|
|
826
|
+
// Reset cached file sizes
|
|
827
|
+
cachedPrimaryFileSize = 44L // WAV header size
|
|
828
|
+
cachedCompressedFileSize = 0L
|
|
829
|
+
|
|
822
830
|
// Only create file if primary output is enabled
|
|
823
831
|
if (recordingConfig.output.primary.enabled) {
|
|
824
832
|
audioFile = createRecordingFile(recordingConfig)
|
|
@@ -920,6 +928,10 @@ class AudioRecorderManager(
|
|
|
920
928
|
return
|
|
921
929
|
}
|
|
922
930
|
|
|
931
|
+
// Declare variables at the synchronized block level to ensure they're accessible in both try blocks
|
|
932
|
+
var duration: Long = 0
|
|
933
|
+
var fileSize: Long = 0
|
|
934
|
+
|
|
923
935
|
try {
|
|
924
936
|
if (isPaused.get()) {
|
|
925
937
|
val remainingData = ByteArray(bufferSizeInBytes)
|
|
@@ -936,7 +948,19 @@ class AudioRecorderManager(
|
|
|
936
948
|
|
|
937
949
|
_isRecording.set(false)
|
|
938
950
|
isPrepared = false // Reset preparation state
|
|
939
|
-
|
|
951
|
+
|
|
952
|
+
// Calculate adaptive timeout based on estimated file size
|
|
953
|
+
// Assume ~5MB per minute at 44.1kHz, 16-bit, mono
|
|
954
|
+
val recordingDurationMs = if (recordingStartTime > 0) {
|
|
955
|
+
System.currentTimeMillis() - recordingStartTime - pausedDuration
|
|
956
|
+
} else {
|
|
957
|
+
0L
|
|
958
|
+
}
|
|
959
|
+
val estimatedFileSizeMB = (recordingDurationMs / 60000.0) * 5.0
|
|
960
|
+
val timeoutMs = maxOf(2000L, (estimatedFileSizeMB * 100).toLong()) // 100ms per MB, min 2 seconds
|
|
961
|
+
|
|
962
|
+
LogUtils.d(CLASS_NAME, "Waiting for recording thread to complete with timeout: ${timeoutMs}ms (estimated size: ${estimatedFileSizeMB}MB)")
|
|
963
|
+
recordingThread?.join(timeoutMs)
|
|
940
964
|
|
|
941
965
|
val audioData = ByteArray(bufferSizeInBytes)
|
|
942
966
|
val bytesRead = audioRecord?.read(audioData, 0, bufferSizeInBytes) ?: -1
|
|
@@ -951,23 +975,11 @@ class AudioRecorderManager(
|
|
|
951
975
|
audioRecord!!.stop()
|
|
952
976
|
}
|
|
953
977
|
|
|
954
|
-
cleanup()
|
|
955
|
-
|
|
956
|
-
LogUtils.
|
|
957
|
-
} finally {
|
|
958
|
-
releaseWakeLock()
|
|
959
|
-
audioRecord?.release()
|
|
960
|
-
}
|
|
961
|
-
|
|
962
|
-
try {
|
|
963
|
-
AudioProcessor.resetUniqueIdCounter()
|
|
964
|
-
audioProcessor.resetCumulativeAmplitudeRange()
|
|
965
|
-
|
|
966
|
-
val fileSize = audioFile?.length() ?: 0
|
|
967
|
-
LogUtils.d(CLASS_NAME, "WAV File validation - Size: $fileSize bytes, Path: ${audioFile?.absolutePath}")
|
|
978
|
+
// Calculate duration BEFORE cleanup (which resets recordingStartTime)
|
|
979
|
+
fileSize = if (recordingConfig.output.primary.enabled) cachedPrimaryFileSize else 0L
|
|
980
|
+
LogUtils.d(CLASS_NAME, "WAV File validation - Size: $fileSize bytes (cached), Path: ${audioFile?.absolutePath}")
|
|
968
981
|
|
|
969
|
-
|
|
970
|
-
val duration = if (!recordingConfig.output.primary.enabled) {
|
|
982
|
+
duration = if (!recordingConfig.output.primary.enabled) {
|
|
971
983
|
// For streaming-only mode, calculate duration from actual recording time
|
|
972
984
|
val actualRecordingTime = if (recordingStartTime > 0) {
|
|
973
985
|
System.currentTimeMillis() - recordingStartTime - pausedDuration
|
|
@@ -991,15 +1003,30 @@ class AudioRecorderManager(
|
|
|
991
1003
|
fileDuration
|
|
992
1004
|
}
|
|
993
1005
|
|
|
1006
|
+
cleanup()
|
|
1007
|
+
} catch (e: IllegalStateException) {
|
|
1008
|
+
LogUtils.e(CLASS_NAME, "Error reading from AudioRecord", e)
|
|
1009
|
+
} finally {
|
|
1010
|
+
releaseWakeLock()
|
|
1011
|
+
audioRecord?.release()
|
|
1012
|
+
}
|
|
1013
|
+
|
|
1014
|
+
try {
|
|
1015
|
+
AudioProcessor.resetUniqueIdCounter()
|
|
1016
|
+
audioProcessor.resetCumulativeAmplitudeRange()
|
|
1017
|
+
|
|
994
1018
|
compressedRecorder?.apply {
|
|
995
1019
|
stop()
|
|
996
1020
|
release()
|
|
997
1021
|
}
|
|
998
1022
|
compressedRecorder = null
|
|
999
1023
|
|
|
1000
|
-
// Log compressed file status if enabled
|
|
1024
|
+
// Log compressed file status if enabled - use actual file size for validation
|
|
1001
1025
|
if (recordingConfig.output.compressed.enabled) {
|
|
1026
|
+
// Note: For compressed files, we need to get actual size as MediaRecorder handles the writing
|
|
1027
|
+
// Use actual file size here for validation purposes only
|
|
1002
1028
|
val compressedSize = compressedFile?.length() ?: 0
|
|
1029
|
+
cachedCompressedFileSize = compressedSize // Update cache with final size
|
|
1003
1030
|
LogUtils.d(CLASS_NAME, "Compressed File validation - Size: $compressedSize bytes, Path: ${compressedFile?.absolutePath}")
|
|
1004
1031
|
}
|
|
1005
1032
|
|
|
@@ -1008,7 +1035,7 @@ class AudioRecorderManager(
|
|
|
1008
1035
|
val localCompressedFile = compressedFile // Create local copy to avoid smart cast issues
|
|
1009
1036
|
val compressionBundle = if (recordingConfig.output.compressed.enabled && localCompressedFile != null) {
|
|
1010
1037
|
bundleOf(
|
|
1011
|
-
"size" to
|
|
1038
|
+
"size" to cachedCompressedFileSize, // Use cached size
|
|
1012
1039
|
"mimeType" to if (recordingConfig.output.compressed.format == "aac") "audio/aac" else "audio/opus",
|
|
1013
1040
|
"bitrate" to recordingConfig.output.compressed.bitrate,
|
|
1014
1041
|
"format" to recordingConfig.output.compressed.format,
|
|
@@ -1040,7 +1067,7 @@ class AudioRecorderManager(
|
|
|
1040
1067
|
"mimeType" to mimeType,
|
|
1041
1068
|
"createdAt" to System.currentTimeMillis(),
|
|
1042
1069
|
"compression" to if (compressedFile != null) bundleOf(
|
|
1043
|
-
"size" to
|
|
1070
|
+
"size" to cachedCompressedFileSize, // Use cached size
|
|
1044
1071
|
"mimeType" to if (recordingConfig.output.compressed.format == "aac") "audio/aac" else "audio/opus",
|
|
1045
1072
|
"bitrate" to recordingConfig.output.compressed.bitrate,
|
|
1046
1073
|
"format" to recordingConfig.output.compressed.format,
|
|
@@ -1195,20 +1222,32 @@ class AudioRecorderManager(
|
|
|
1195
1222
|
)
|
|
1196
1223
|
}
|
|
1197
1224
|
|
|
1198
|
-
|
|
1199
|
-
val
|
|
1200
|
-
|
|
1201
|
-
|
|
1202
|
-
|
|
1203
|
-
|
|
1204
|
-
|
|
1225
|
+
// Use cached file size instead of file system call
|
|
1226
|
+
val fileSize = if (recordingConfig.output.primary.enabled) cachedPrimaryFileSize else 0L
|
|
1227
|
+
val duration = if (!recordingConfig.output.primary.enabled) {
|
|
1228
|
+
// For streaming-only mode, calculate duration from actual recording time
|
|
1229
|
+
val actualRecordingTime = if (recordingStartTime > 0) {
|
|
1230
|
+
System.currentTimeMillis() - recordingStartTime - pausedDuration
|
|
1231
|
+
} else {
|
|
1232
|
+
0L
|
|
1233
|
+
}
|
|
1234
|
+
actualRecordingTime
|
|
1235
|
+
} else {
|
|
1236
|
+
// For file-based recording, calculate duration from file size
|
|
1237
|
+
when (mimeType) {
|
|
1238
|
+
"audio/wav" -> {
|
|
1239
|
+
val dataFileSize = fileSize - Constants.WAV_HEADER_SIZE
|
|
1240
|
+
val byteRate = recordingConfig.sampleRate * recordingConfig.channels *
|
|
1241
|
+
(if (recordingConfig.encoding == "pcm_8bit") 8 else 16) / 8
|
|
1242
|
+
if (byteRate > 0) dataFileSize * 1000 / byteRate else 0
|
|
1243
|
+
}
|
|
1244
|
+
else -> totalRecordedTime
|
|
1205
1245
|
}
|
|
1206
|
-
else -> totalRecordedTime
|
|
1207
1246
|
}
|
|
1208
1247
|
|
|
1209
1248
|
val compressionBundle = if (recordingConfig.output.compressed.enabled) {
|
|
1210
1249
|
bundleOf(
|
|
1211
|
-
"size" to
|
|
1250
|
+
"size" to cachedCompressedFileSize, // Use cached size
|
|
1212
1251
|
"mimeType" to if (recordingConfig.output.compressed.format == "aac") "audio/aac" else "audio/opus",
|
|
1213
1252
|
"bitrate" to recordingConfig.output.compressed.bitrate,
|
|
1214
1253
|
"format" to recordingConfig.output.compressed.format
|
|
@@ -1380,6 +1419,7 @@ class AudioRecorderManager(
|
|
|
1380
1419
|
// Only write to file if primary output is enabled
|
|
1381
1420
|
if (fos != null) {
|
|
1382
1421
|
fos.write(audioData, 0, bytesRead)
|
|
1422
|
+
cachedPrimaryFileSize += bytesRead // Update cached file size
|
|
1383
1423
|
}
|
|
1384
1424
|
totalDataSize += bytesRead
|
|
1385
1425
|
|
|
@@ -1440,16 +1480,17 @@ class AudioRecorderManager(
|
|
|
1440
1480
|
}
|
|
1441
1481
|
}
|
|
1442
1482
|
} finally {
|
|
1443
|
-
//
|
|
1483
|
+
// Flush and close the file output stream if it was opened
|
|
1484
|
+
try {
|
|
1485
|
+
fos?.flush()
|
|
1486
|
+
LogUtils.d(CLASS_NAME, "FileOutputStream flushed successfully")
|
|
1487
|
+
} catch (e: Exception) {
|
|
1488
|
+
LogUtils.e(CLASS_NAME, "Error flushing FileOutputStream", e)
|
|
1489
|
+
}
|
|
1444
1490
|
fos?.close()
|
|
1445
1491
|
}
|
|
1446
1492
|
|
|
1447
|
-
//
|
|
1448
|
-
if (recordingConfig.output.primary.enabled) {
|
|
1449
|
-
audioFile?.let { file ->
|
|
1450
|
-
audioFileHandler.updateWavHeader(file)
|
|
1451
|
-
}
|
|
1452
|
-
}
|
|
1493
|
+
// WAV header update is already handled in cleanup(), no need to duplicate here
|
|
1453
1494
|
|
|
1454
1495
|
} catch (e: Exception) {
|
|
1455
1496
|
// Ensure wake lock is released if the thread is interrupted
|
|
@@ -1463,7 +1504,8 @@ class AudioRecorderManager(
|
|
|
1463
1504
|
private fun emitAudioData(audioData: ByteArray, length: Int) {
|
|
1464
1505
|
val encodedBuffer = audioDataEncoder.encodeToBase64(audioData)
|
|
1465
1506
|
|
|
1466
|
-
|
|
1507
|
+
// Use cached file size instead of file system call
|
|
1508
|
+
val fileSize = if (recordingConfig.output.primary.enabled) cachedPrimaryFileSize else 0L
|
|
1467
1509
|
val from = lastEmittedSize
|
|
1468
1510
|
lastEmittedSize = fileSize
|
|
1469
1511
|
|
|
@@ -1472,7 +1514,14 @@ class AudioRecorderManager(
|
|
|
1472
1514
|
(from * 1000) / (recordingConfig.sampleRate * recordingConfig.channels * (if (recordingConfig.encoding == "pcm_8bit") 8 else 16) / 8)
|
|
1473
1515
|
|
|
1474
1516
|
val compressionBundle = if (recordingConfig.output.compressed.enabled) {
|
|
1475
|
-
|
|
1517
|
+
// For compressed files, we need to get actual size as MediaRecorder handles the writing
|
|
1518
|
+
// Only update cache periodically to avoid frequent file system calls
|
|
1519
|
+
val currentTime = System.currentTimeMillis()
|
|
1520
|
+
if (cachedCompressedFileSize == 0L || (currentTime - lastEmittedCompressedSize) > 5000) {
|
|
1521
|
+
cachedCompressedFileSize = compressedFile?.length() ?: 0
|
|
1522
|
+
}
|
|
1523
|
+
|
|
1524
|
+
val compressedSize = cachedCompressedFileSize
|
|
1476
1525
|
val eventDataSize = compressedSize - lastEmittedCompressedSize
|
|
1477
1526
|
|
|
1478
1527
|
// Read the new compressed data
|
|
@@ -112,6 +112,10 @@ class AudioStreamManager: NSObject, AudioDeviceManagerDelegate {
|
|
|
112
112
|
|
|
113
113
|
// Add the stopping flag to the class properties
|
|
114
114
|
private var stopping: Bool = false
|
|
115
|
+
|
|
116
|
+
// Performance optimization: Cache file sizes during recording
|
|
117
|
+
private var cachedWavFileSize: Int64 = 0
|
|
118
|
+
private var cachedCompressedFileSize: Int64 = 0
|
|
115
119
|
|
|
116
120
|
/// Initializes the AudioStreamManager
|
|
117
121
|
override init() {
|
|
@@ -819,6 +823,7 @@ class AudioStreamManager: NSObject, AudioDeviceManagerDelegate {
|
|
|
819
823
|
let header = createWavHeader(dataSize: 0)
|
|
820
824
|
self.fileHandle?.write(header)
|
|
821
825
|
self.totalDataSize = Int64(WAV_HEADER_SIZE) // Initialize size with header size
|
|
826
|
+
self.cachedWavFileSize = Int64(WAV_HEADER_SIZE) // Initialize cached size
|
|
822
827
|
Logger.debug("AudioStreamManager", "File handle opened and initial header written for \(url.path). Initial size: \(self.totalDataSize)")
|
|
823
828
|
} catch {
|
|
824
829
|
Logger.debug("AudioStreamManager", "Error creating/opening file handle: \(error.localizedDescription)")
|
|
@@ -1462,6 +1467,8 @@ class AudioStreamManager: NSObject, AudioDeviceManagerDelegate {
|
|
|
1462
1467
|
try handle.write(contentsOf: dataToWrite)
|
|
1463
1468
|
// Update total size state
|
|
1464
1469
|
self.totalDataSize += Int64(dataToWrite.count)
|
|
1470
|
+
// Cache WAV file size for performance
|
|
1471
|
+
self.cachedWavFileSize = self.totalDataSize
|
|
1465
1472
|
} catch {
|
|
1466
1473
|
Logger.debug("BG Write Error: Failed to seek/write: \(error.localizedDescription)")
|
|
1467
1474
|
}
|
|
@@ -1698,35 +1705,52 @@ class AudioStreamManager: NSObject, AudioDeviceManagerDelegate {
|
|
|
1698
1705
|
return nil
|
|
1699
1706
|
}
|
|
1700
1707
|
|
|
1701
|
-
|
|
1702
|
-
|
|
1708
|
+
// PERFORMANCE OPTIMIZATION: Capture current state for immediate return
|
|
1709
|
+
let capturedFileURL = recordingFileURL
|
|
1710
|
+
let capturedSettings = recordingSettings
|
|
1711
|
+
let capturedWavFileSize = cachedWavFileSize
|
|
1712
|
+
let capturedCompressedFileSize = cachedCompressedFileSize
|
|
1713
|
+
let capturedTotalDataSize = totalDataSize
|
|
1714
|
+
let capturedCompressedURL = compressedFileURL
|
|
1715
|
+
|
|
1716
|
+
// PERFORMANCE OPTIMIZATION: Move all slow operations to background
|
|
1717
|
+
let capturedShowNotification = recordingSettings?.showNotification == true
|
|
1718
|
+
|
|
1719
|
+
// Queue notification and audio session cleanup for background
|
|
1720
|
+
DispatchQueue.global(qos: .utility).async { [weak self] in
|
|
1721
|
+
guard let self = self else { return }
|
|
1722
|
+
|
|
1723
|
+
if capturedShowNotification {
|
|
1724
|
+
// Clean up notifications on main queue but don't wait
|
|
1725
|
+
DispatchQueue.main.async {
|
|
1726
|
+
self.mediaInfoUpdateTimer?.invalidate()
|
|
1727
|
+
self.mediaInfoUpdateTimer = nil
|
|
1728
|
+
|
|
1729
|
+
// Clean up notification manager
|
|
1730
|
+
self.notificationManager?.stopUpdates()
|
|
1731
|
+
self.notificationManager = nil
|
|
1732
|
+
|
|
1733
|
+
// Clean up media controls
|
|
1734
|
+
UIApplication.shared.endReceivingRemoteControlEvents()
|
|
1735
|
+
self.remoteCommandCenter?.pauseCommand.isEnabled = false
|
|
1736
|
+
self.remoteCommandCenter?.playCommand.isEnabled = false
|
|
1737
|
+
self.notificationView?.nowPlayingInfo = nil
|
|
1738
|
+
}
|
|
1739
|
+
}
|
|
1740
|
+
|
|
1741
|
+
// Reset audio session in background
|
|
1742
|
+
do {
|
|
1743
|
+
try AVAudioSession.sharedInstance().setActive(false, options: .notifyOthersOnDeactivation)
|
|
1744
|
+
} catch {
|
|
1745
|
+
Logger.debug("Background: Error deactivating audio session: \(error)")
|
|
1746
|
+
}
|
|
1747
|
+
|
|
1748
|
+
// Reset audio engine in background
|
|
1703
1749
|
DispatchQueue.main.async {
|
|
1704
|
-
self.
|
|
1705
|
-
self.mediaInfoUpdateTimer = nil
|
|
1706
|
-
|
|
1707
|
-
// Clean up notification manager
|
|
1708
|
-
self.notificationManager?.stopUpdates()
|
|
1709
|
-
self.notificationManager = nil
|
|
1710
|
-
|
|
1711
|
-
// Clean up media controls
|
|
1712
|
-
UIApplication.shared.endReceivingRemoteControlEvents()
|
|
1713
|
-
self.remoteCommandCenter?.pauseCommand.isEnabled = false
|
|
1714
|
-
self.remoteCommandCenter?.playCommand.isEnabled = false
|
|
1715
|
-
self.notificationView?.nowPlayingInfo = nil
|
|
1750
|
+
self.audioEngine.reset()
|
|
1716
1751
|
}
|
|
1717
1752
|
}
|
|
1718
1753
|
|
|
1719
|
-
// Reset audio session safely
|
|
1720
|
-
do {
|
|
1721
|
-
try AVAudioSession.sharedInstance().setActive(false, options: .notifyOthersOnDeactivation)
|
|
1722
|
-
} catch {
|
|
1723
|
-
Logger.debug("Error deactivating audio session: \(error)")
|
|
1724
|
-
// Continue with cleanup despite session errors
|
|
1725
|
-
}
|
|
1726
|
-
|
|
1727
|
-
// Reset audio engine
|
|
1728
|
-
audioEngine.reset()
|
|
1729
|
-
|
|
1730
1754
|
guard let settings = recordingSettings else {
|
|
1731
1755
|
Logger.debug("Recording settings is nil.")
|
|
1732
1756
|
stopping = false // Reset stopping flag before returning nil
|
|
@@ -1737,40 +1761,25 @@ class AudioStreamManager: NSObject, AudioDeviceManagerDelegate {
|
|
|
1737
1761
|
if !settings.output.primary.enabled {
|
|
1738
1762
|
let durationMs = Int64(finalDuration * 1000)
|
|
1739
1763
|
|
|
1740
|
-
// Check for compressed output
|
|
1764
|
+
// Check for compressed output using cached size
|
|
1741
1765
|
var compression: CompressedRecordingInfo?
|
|
1742
|
-
if settings.output.compressed.enabled,
|
|
1743
|
-
|
|
1744
|
-
|
|
1745
|
-
|
|
1746
|
-
|
|
1747
|
-
|
|
1748
|
-
|
|
1749
|
-
|
|
1750
|
-
|
|
1751
|
-
|
|
1752
|
-
|
|
1753
|
-
|
|
1754
|
-
|
|
1755
|
-
|
|
1756
|
-
|
|
1757
|
-
|
|
1758
|
-
|
|
1759
|
-
compressedFileUri: compressedURL.absoluteString,
|
|
1760
|
-
mimeType: compressedFormat == "aac" ? "audio/aac" : "audio/opus",
|
|
1761
|
-
bitrate: compressedBitRate,
|
|
1762
|
-
format: compressedFormat,
|
|
1763
|
-
size: compressedSize
|
|
1764
|
-
)
|
|
1765
|
-
} else {
|
|
1766
|
-
Logger.debug("Warning: Compressed file exists but is empty")
|
|
1767
|
-
}
|
|
1768
|
-
} catch {
|
|
1769
|
-
Logger.debug("Failed to validate compressed file: \(error)")
|
|
1770
|
-
}
|
|
1771
|
-
} else {
|
|
1772
|
-
Logger.debug("Warning: Compressed file not found at path: \(compressedPath)")
|
|
1773
|
-
}
|
|
1766
|
+
if settings.output.compressed.enabled,
|
|
1767
|
+
let compressedURL = capturedCompressedURL,
|
|
1768
|
+
capturedCompressedFileSize > 0 {
|
|
1769
|
+
compression = CompressedRecordingInfo(
|
|
1770
|
+
compressedFileUri: compressedURL.absoluteString,
|
|
1771
|
+
mimeType: compressedFormat == "aac" ? "audio/aac" : "audio/opus",
|
|
1772
|
+
bitrate: compressedBitRate,
|
|
1773
|
+
format: compressedFormat,
|
|
1774
|
+
size: capturedCompressedFileSize
|
|
1775
|
+
)
|
|
1776
|
+
|
|
1777
|
+
Logger.debug("""
|
|
1778
|
+
Compressed File (cached - primary disabled):
|
|
1779
|
+
- Format: \(compressedFormat)
|
|
1780
|
+
- Size: \(capturedCompressedFileSize) bytes
|
|
1781
|
+
- Bitrate: \(compressedBitRate) bps
|
|
1782
|
+
""")
|
|
1774
1783
|
}
|
|
1775
1784
|
|
|
1776
1785
|
let result = RecordingResult(
|
|
@@ -1804,140 +1813,79 @@ class AudioStreamManager: NSObject, AudioDeviceManagerDelegate {
|
|
|
1804
1813
|
return result
|
|
1805
1814
|
}
|
|
1806
1815
|
|
|
1807
|
-
guard let fileURL =
|
|
1816
|
+
guard let fileURL = capturedFileURL else {
|
|
1808
1817
|
Logger.debug("Recording file URL is nil.")
|
|
1809
1818
|
stopping = false // Reset stopping flag before returning nil
|
|
1810
1819
|
return nil
|
|
1811
1820
|
}
|
|
1812
1821
|
|
|
1813
|
-
//
|
|
1814
|
-
let
|
|
1815
|
-
stopping = false
|
|
1822
|
+
// PERFORMANCE OPTIMIZATION: Create result immediately with cached values
|
|
1823
|
+
let durationMs = Int64(finalDuration * 1000)
|
|
1816
1824
|
|
|
1817
|
-
//
|
|
1818
|
-
|
|
1819
|
-
|
|
1820
|
-
|
|
1821
|
-
|
|
1822
|
-
|
|
1823
|
-
|
|
1824
|
-
|
|
1825
|
-
|
|
1826
|
-
|
|
1827
|
-
|
|
1828
|
-
// Validate WAV file
|
|
1829
|
-
let wavPath = fileURL.path
|
|
1830
|
-
do {
|
|
1831
|
-
// Check if WAV file exists
|
|
1832
|
-
let wavFileAttributes = try FileManager.default.attributesOfItem(atPath: wavPath)
|
|
1833
|
-
let wavFileSize = wavFileAttributes[FileAttributeKey.size] as? Int64 ?? 0
|
|
1834
|
-
|
|
1835
|
-
Logger.debug("""
|
|
1836
|
-
WAV File validation:
|
|
1837
|
-
- Path: \(wavPath)
|
|
1838
|
-
- Exists: true
|
|
1839
|
-
- Size: \(wavFileSize) bytes
|
|
1840
|
-
- Duration: \(finalDuration) seconds
|
|
1841
|
-
- Expected minimum size: \(WAV_HEADER_SIZE) bytes (WAV header)
|
|
1842
|
-
""")
|
|
1843
|
-
|
|
1844
|
-
// Use the final totalDataSize tracked by the background queue
|
|
1845
|
-
let finalDataChunkSize = self.totalDataSize - Int64(WAV_HEADER_SIZE)
|
|
1846
|
-
if finalDataChunkSize <= 0 {
|
|
1847
|
-
Logger.debug("Recording file data chunk size is zero or negative (\(finalDataChunkSize) bytes), likely no audio data was recorded successfully after header")
|
|
1848
|
-
// Optionally delete the empty file?
|
|
1849
|
-
// try? FileManager.default.removeItem(at: fileURL)
|
|
1850
|
-
return nil
|
|
1851
|
-
}
|
|
1852
|
-
|
|
1853
|
-
// Update the WAV header with the correct final file size
|
|
1854
|
-
updateWavHeader(fileURL: fileURL, totalDataSize: finalDataChunkSize)
|
|
1855
|
-
Logger.debug("Final WAV header updated. Data chunk size: \(finalDataChunkSize)")
|
|
1856
|
-
|
|
1857
|
-
// Validate compressed file if enabled
|
|
1858
|
-
var compression: CompressedRecordingInfo?
|
|
1859
|
-
if let compressedURL = compressedFileURL {
|
|
1860
|
-
let compressedPath = compressedURL.path
|
|
1861
|
-
if FileManager.default.fileExists(atPath: compressedPath) {
|
|
1862
|
-
let compressedAttributes = try FileManager.default.attributesOfItem(atPath: compressedPath)
|
|
1863
|
-
let compressedSize = compressedAttributes[FileAttributeKey.size] as? Int64 ?? 0
|
|
1864
|
-
|
|
1865
|
-
Logger.debug("""
|
|
1866
|
-
Compressed File validation:
|
|
1867
|
-
- Path: \(compressedPath)
|
|
1868
|
-
- Format: \(compressedFormat)
|
|
1869
|
-
- Size: \(compressedSize) bytes
|
|
1870
|
-
- Bitrate: \(compressedBitRate) bps
|
|
1871
|
-
""")
|
|
1872
|
-
|
|
1873
|
-
if compressedSize > 0 {
|
|
1874
|
-
compression = CompressedRecordingInfo(
|
|
1875
|
-
compressedFileUri: compressedURL.absoluteString,
|
|
1876
|
-
mimeType: compressedFormat == "aac" ? "audio/aac" : "audio/opus",
|
|
1877
|
-
bitrate: compressedBitRate,
|
|
1878
|
-
format: compressedFormat,
|
|
1879
|
-
size: compressedSize
|
|
1880
|
-
)
|
|
1881
|
-
} else {
|
|
1882
|
-
Logger.debug("Warning: Compressed file exists but is empty")
|
|
1883
|
-
}
|
|
1884
|
-
} else {
|
|
1885
|
-
Logger.debug("Warning: Compressed file not found at path: \(compressedPath)")
|
|
1886
|
-
}
|
|
1887
|
-
}
|
|
1888
|
-
|
|
1889
|
-
let durationMs = Int64(finalDuration * 1000)
|
|
1890
|
-
|
|
1891
|
-
let result = RecordingResult(
|
|
1892
|
-
fileUri: fileURL.absoluteString,
|
|
1893
|
-
filename: fileURL.lastPathComponent,
|
|
1894
|
-
mimeType: mimeType,
|
|
1895
|
-
duration: durationMs,
|
|
1896
|
-
size: wavFileSize,
|
|
1897
|
-
channels: settings.numberOfChannels,
|
|
1898
|
-
bitDepth: settings.bitDepth,
|
|
1899
|
-
sampleRate: settings.sampleRate,
|
|
1900
|
-
compression: compression
|
|
1825
|
+
// Check compressed output
|
|
1826
|
+
var compression: CompressedRecordingInfo?
|
|
1827
|
+
if capturedSettings?.output.compressed.enabled == true,
|
|
1828
|
+
let compressedURL = capturedCompressedURL,
|
|
1829
|
+
capturedCompressedFileSize > 0 {
|
|
1830
|
+
compression = CompressedRecordingInfo(
|
|
1831
|
+
compressedFileUri: compressedURL.absoluteString,
|
|
1832
|
+
mimeType: compressedFormat == "aac" ? "audio/aac" : "audio/opus",
|
|
1833
|
+
bitrate: compressedBitRate,
|
|
1834
|
+
format: compressedFormat,
|
|
1835
|
+
size: capturedCompressedFileSize
|
|
1901
1836
|
)
|
|
1837
|
+
}
|
|
1838
|
+
|
|
1839
|
+
// Create result with cached values - no file system access
|
|
1840
|
+
let result = RecordingResult(
|
|
1841
|
+
fileUri: fileURL.absoluteString,
|
|
1842
|
+
filename: fileURL.lastPathComponent,
|
|
1843
|
+
mimeType: mimeType,
|
|
1844
|
+
duration: durationMs,
|
|
1845
|
+
size: capturedWavFileSize,
|
|
1846
|
+
channels: capturedSettings?.numberOfChannels ?? 1,
|
|
1847
|
+
bitDepth: capturedSettings?.bitDepth ?? 16,
|
|
1848
|
+
sampleRate: capturedSettings?.sampleRate ?? 44100,
|
|
1849
|
+
compression: compression
|
|
1850
|
+
)
|
|
1851
|
+
|
|
1852
|
+
// Perform file operations asynchronously after returning result
|
|
1853
|
+
DispatchQueue.global(qos: .utility).async { [weak self] in
|
|
1854
|
+
guard let self = self else { return }
|
|
1902
1855
|
|
|
1903
|
-
|
|
1904
|
-
|
|
1905
|
-
|
|
1906
|
-
|
|
1907
|
-
|
|
1908
|
-
|
|
1909
|
-
- Bit depth: \(settings.bitDepth)-bit
|
|
1910
|
-
- Channels: \(settings.numberOfChannels)
|
|
1911
|
-
- Compressed: \(compression != nil ? "yes" : "no")
|
|
1912
|
-
""")
|
|
1913
|
-
|
|
1914
|
-
// Additional cleanup
|
|
1915
|
-
recordingFileURL = nil
|
|
1916
|
-
lastBufferTime = nil
|
|
1917
|
-
lastValidDuration = nil
|
|
1918
|
-
compressedRecorder = nil
|
|
1919
|
-
compressedFileURL = nil
|
|
1920
|
-
recordingSettings = nil
|
|
1921
|
-
startTime = nil
|
|
1922
|
-
totalPausedDuration = 0
|
|
1923
|
-
currentPauseStart = nil
|
|
1924
|
-
lastEmissionTime = nil
|
|
1925
|
-
lastEmissionTimeAnalysis = nil
|
|
1926
|
-
lastEmittedSize = 0
|
|
1927
|
-
lastEmittedSizeAnalysis = 0
|
|
1928
|
-
lastEmittedCompressedSize = 0
|
|
1929
|
-
accumulatedData.removeAll()
|
|
1930
|
-
accumulatedAnalysisData.removeAll()
|
|
1931
|
-
recordingUUID = nil
|
|
1932
|
-
|
|
1933
|
-
return result
|
|
1856
|
+
// Update WAV header in background
|
|
1857
|
+
let finalDataChunkSize = capturedTotalDataSize - Int64(WAV_HEADER_SIZE)
|
|
1858
|
+
if finalDataChunkSize > 0 {
|
|
1859
|
+
self.updateWavHeader(fileURL: fileURL, totalDataSize: finalDataChunkSize)
|
|
1860
|
+
Logger.debug("Background: WAV header updated. Data chunk size: \(finalDataChunkSize)")
|
|
1861
|
+
}
|
|
1934
1862
|
|
|
1935
|
-
|
|
1936
|
-
|
|
1937
|
-
|
|
1863
|
+
// Cleanup
|
|
1864
|
+
self.recordingSettings = nil
|
|
1865
|
+
self.startTime = nil
|
|
1866
|
+
self.totalPausedDuration = 0
|
|
1867
|
+
self.currentPauseStart = nil
|
|
1868
|
+
self.lastEmissionTime = nil
|
|
1869
|
+
self.lastEmissionTimeAnalysis = nil
|
|
1870
|
+
self.lastEmittedSize = 0
|
|
1871
|
+
self.lastEmittedSizeAnalysis = 0
|
|
1872
|
+
self.lastEmittedCompressedSize = 0
|
|
1873
|
+
self.accumulatedData.removeAll()
|
|
1874
|
+
self.accumulatedAnalysisData.removeAll()
|
|
1875
|
+
self.recordingUUID = nil
|
|
1876
|
+
self.totalDataSize = 0
|
|
1877
|
+
self.cachedWavFileSize = 0
|
|
1878
|
+
self.cachedCompressedFileSize = 0
|
|
1879
|
+
self.recordingFileURL = nil
|
|
1880
|
+
self.compressedFileURL = nil
|
|
1881
|
+
self.fileHandle = nil
|
|
1938
1882
|
}
|
|
1883
|
+
|
|
1884
|
+
stopping = false
|
|
1885
|
+
return result
|
|
1939
1886
|
}
|
|
1940
1887
|
|
|
1888
|
+
|
|
1941
1889
|
// MARK: - AudioDeviceManagerDelegate Implementation
|
|
1942
1890
|
|
|
1943
1891
|
func audioDeviceManager(_ manager: AudioDeviceManager, didDetectDisconnectionOfDevice disconnectedDeviceId: String) {
|
|
@@ -2297,6 +2245,19 @@ extension AudioStreamManager: AVAudioRecorderDelegate {
|
|
|
2297
2245
|
Logger.debug("Compressed recording finished - success: \(flag)")
|
|
2298
2246
|
if !flag {
|
|
2299
2247
|
delegate?.audioStreamManager(self, didFailWithError: "Compressed recording failed to complete")
|
|
2248
|
+
} else {
|
|
2249
|
+
// Update cached compressed file size when recording finishes
|
|
2250
|
+
if let compressedURL = compressedFileURL {
|
|
2251
|
+
do {
|
|
2252
|
+
let attributes = try FileManager.default.attributesOfItem(atPath: compressedURL.path)
|
|
2253
|
+
if let size = attributes[.size] as? Int64 {
|
|
2254
|
+
cachedCompressedFileSize = size
|
|
2255
|
+
Logger.debug("Cached compressed file size: \(size) bytes")
|
|
2256
|
+
}
|
|
2257
|
+
} catch {
|
|
2258
|
+
Logger.debug("Failed to cache compressed file size: \(error)")
|
|
2259
|
+
}
|
|
2260
|
+
}
|
|
2300
2261
|
}
|
|
2301
2262
|
}
|
|
2302
2263
|
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@siteed/expo-audio-studio",
|
|
3
|
-
"version": "2.
|
|
3
|
+
"version": "2.14.1",
|
|
4
4
|
"description": "Comprehensive audio processing library for React Native and Expo with recording, analysis, visualization, and streaming capabilities across iOS, Android, and web",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"type": "commonjs",
|