@siteed/expo-audio-studio 2.17.0 → 2.18.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,15 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
|
|
8
8
|
## [Unreleased]
|
|
9
9
|
|
|
10
10
|
|
|
11
|
+
## [2.18.1] - 2025-08-02
|
|
12
|
+
### Changed
|
|
13
|
+
- feat: improved memory monitoring ([55dfe16](https://github.com/deeeed/expo-audio-stream/commit/55dfe16d7e8c372392738d1441776a760e7ecdbe))
|
|
14
|
+
- chore(expo-audio-studio): release @siteed/expo-audio-studio@2.18.0 ([cc80ac5](https://github.com/deeeed/expo-audio-stream/commit/cc80ac5fa7ece05fc9fae031f101163acce2aff4))
|
|
15
|
+
## [2.18.0] - 2025-08-01
|
|
16
|
+
### Changed
|
|
17
|
+
- feat(expo-audio-studio): optimize buffer size on android to prevent oom ([32fcb9b](https://github.com/deeeed/expo-audio-stream/commit/32fcb9b0a965669b3a37c9860998ae46a1d26cd8))
|
|
18
|
+
- fix(expo-audio-studio): invalid paused duration on android ([c107258](https://github.com/deeeed/expo-audio-stream/commit/c107258054ebdbc733298c84b8d84b0f9f416e6e))
|
|
19
|
+
- chore(expo-audio-studio): release @siteed/expo-audio-studio@2.17.0 ([8a303b4](https://github.com/deeeed/expo-audio-stream/commit/8a303b4d96988b97604123d74daaa406d9ec517c))
|
|
11
20
|
## [2.17.0] - 2025-07-31
|
|
12
21
|
### Changed
|
|
13
22
|
- fix(expo-audio-studio): fix OutOfMemoryError by tracking stream position correctly ([b67e521](https://github.com/deeeed/expo-audio-stream/commit/b67e52142154d07873c5c1ec9c183d524d61e528))
|
|
@@ -368,7 +377,9 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
|
|
368
377
|
- Feature: Audio features extraction during recording.
|
|
369
378
|
- Feature: Consistent WAV PCM recording format across all platforms.
|
|
370
379
|
|
|
371
|
-
[unreleased]: https://github.com/deeeed/expo-audio-stream/compare/@siteed/expo-audio-studio@2.
|
|
380
|
+
[unreleased]: https://github.com/deeeed/expo-audio-stream/compare/@siteed/expo-audio-studio@2.18.1...HEAD
|
|
381
|
+
[2.18.1]: https://github.com/deeeed/expo-audio-stream/compare/@siteed/expo-audio-studio@2.18.0...@siteed/expo-audio-studio@2.18.1
|
|
382
|
+
[2.18.0]: https://github.com/deeeed/expo-audio-stream/compare/@siteed/expo-audio-studio@2.17.0...@siteed/expo-audio-studio@2.18.0
|
|
372
383
|
[2.17.0]: https://github.com/deeeed/expo-audio-stream/compare/@siteed/expo-audio-studio@2.16.2...@siteed/expo-audio-studio@2.17.0
|
|
373
384
|
[2.16.2]: https://github.com/deeeed/expo-audio-stream/compare/@siteed/expo-audio-studio@2.16.1...@siteed/expo-audio-studio@2.16.2
|
|
374
385
|
[2.16.1]: https://github.com/deeeed/expo-audio-stream/compare/@siteed/expo-audio-studio@2.16.0...@siteed/expo-audio-studio@2.16.1
|
|
@@ -75,6 +75,9 @@ class AudioRecorderManager(
|
|
|
75
75
|
}
|
|
76
76
|
}
|
|
77
77
|
|
|
78
|
+
// Maximum size for analysis buffer to prevent OOM on low-RAM devices with extreme configs
|
|
79
|
+
private val MAX_ANALYSIS_BUFFER_SIZE = 20 * 1024 * 1024 // 20MB
|
|
80
|
+
|
|
78
81
|
private var audioRecord: AudioRecord? = null
|
|
79
82
|
private var bufferSizeInBytes = 0
|
|
80
83
|
private val _isRecording = AtomicBoolean(false)
|
|
@@ -709,19 +712,29 @@ class AudioRecorderManager(
|
|
|
709
712
|
)
|
|
710
713
|
|
|
711
714
|
// Calculate buffer size based on bufferDurationSeconds if provided
|
|
712
|
-
|
|
715
|
+
var requestedBufferSize = recordingConfig.bufferDurationSeconds?.let { bufferDuration ->
|
|
713
716
|
val bytesPerSample = when (recordingConfig.encoding) {
|
|
714
717
|
"pcm_8bit" -> 1
|
|
715
718
|
"pcm_16bit" -> 2
|
|
716
719
|
"pcm_32bit" -> 4
|
|
717
720
|
else -> 2
|
|
718
721
|
}
|
|
719
|
-
|
|
720
|
-
bytesPerSample * recordingConfig.channels).toInt()
|
|
721
|
-
// Use the larger of requested size or minimum buffer size
|
|
722
|
-
maxOf(requestedSize, minBufferSize)
|
|
722
|
+
(bufferDuration * recordingConfig.sampleRate * bytesPerSample * recordingConfig.channels).toInt()
|
|
723
723
|
} ?: minBufferSize
|
|
724
724
|
|
|
725
|
+
LogUtils.d(CLASS_NAME, "Calculated minBufferSize: $minBufferSize bytes")
|
|
726
|
+
LogUtils.d(CLASS_NAME, "Requested buffer size: $requestedBufferSize bytes")
|
|
727
|
+
|
|
728
|
+
// Cap the buffer size to prevent OOM
|
|
729
|
+
val MAX_BUFFER_SIZE = 10485760 // 10MB
|
|
730
|
+
if (requestedBufferSize > MAX_BUFFER_SIZE) {
|
|
731
|
+
LogUtils.w(CLASS_NAME, "Requested buffer size $requestedBufferSize exceeds max limit of $MAX_BUFFER_SIZE, capping to max")
|
|
732
|
+
requestedBufferSize = MAX_BUFFER_SIZE
|
|
733
|
+
}
|
|
734
|
+
|
|
735
|
+
bufferSizeInBytes = maxOf(requestedBufferSize, minBufferSize)
|
|
736
|
+
LogUtils.d(CLASS_NAME, "Final bufferSizeInBytes: $bufferSizeInBytes (after capping and min check)")
|
|
737
|
+
|
|
725
738
|
when {
|
|
726
739
|
bufferSizeInBytes == AudioRecord.ERROR -> {
|
|
727
740
|
LogUtils.e(CLASS_NAME, "Error getting minimum buffer size: ERROR")
|
|
@@ -1253,7 +1266,14 @@ class AudioRecorderManager(
|
|
|
1253
1266
|
|
|
1254
1267
|
// Use cached file size instead of file system call
|
|
1255
1268
|
val fileSize = if (recordingConfig.output.primary.enabled) cachedPrimaryFileSize else 0L
|
|
1256
|
-
val duration = if (
|
|
1269
|
+
val duration = if (isPaused.get()) {
|
|
1270
|
+
// Return frozen duration when paused using lastPauseTime
|
|
1271
|
+
if (lastPauseTime > 0) {
|
|
1272
|
+
lastPauseTime - recordingStartTime - pausedDuration
|
|
1273
|
+
} else {
|
|
1274
|
+
0L
|
|
1275
|
+
}
|
|
1276
|
+
} else if (!recordingConfig.output.primary.enabled) {
|
|
1257
1277
|
// For streaming-only mode, calculate duration from actual recording time
|
|
1258
1278
|
val actualRecordingTime = if (recordingStartTime > 0) {
|
|
1259
1279
|
System.currentTimeMillis() - recordingStartTime - pausedDuration
|
|
@@ -1423,7 +1443,7 @@ class AudioRecorderManager(
|
|
|
1423
1443
|
while (_isRecording.get() && !Thread.currentThread().isInterrupted) {
|
|
1424
1444
|
loopCount++
|
|
1425
1445
|
if (loopCount % 100 == 0) {
|
|
1426
|
-
LogUtils.d(CLASS_NAME, "Recording loop iteration $loopCount, isRecording: ${_isRecording.get()}")
|
|
1446
|
+
LogUtils.d(CLASS_NAME, "Recording loop iteration $loopCount, isRecording: ${_isRecording.get()}, accumulatedAudioSize: ${accumulatedAudioData.size()}, accumulatedAnalysisSize: ${accumulatedAnalysisData.size()}")
|
|
1427
1447
|
}
|
|
1428
1448
|
if (isPaused.get()) {
|
|
1429
1449
|
Thread.sleep(100) // Add small delay when paused
|
|
@@ -1460,9 +1480,14 @@ class AudioRecorderManager(
|
|
|
1460
1480
|
|
|
1461
1481
|
accumulatedAudioData.write(audioData, 0, bytesRead)
|
|
1462
1482
|
|
|
1463
|
-
//
|
|
1464
|
-
if (
|
|
1465
|
-
|
|
1483
|
+
// Always accumulate data for analysis if enabled (moved outside shouldProcessAnalysis check)
|
|
1484
|
+
if (recordingConfig.enableProcessing) {
|
|
1485
|
+
// Check buffer size to prevent OOM on low-RAM devices with extreme configs
|
|
1486
|
+
if (accumulatedAnalysisData.size() + bytesRead <= MAX_ANALYSIS_BUFFER_SIZE) {
|
|
1487
|
+
accumulatedAnalysisData.write(audioData, 0, bytesRead)
|
|
1488
|
+
} else {
|
|
1489
|
+
LogUtils.w(CLASS_NAME, "Analysis buffer size limit reached (${accumulatedAnalysisData.size()} bytes). Skipping data to prevent OOM.")
|
|
1490
|
+
}
|
|
1466
1491
|
}
|
|
1467
1492
|
|
|
1468
1493
|
// Handle regular audio data emission
|
|
@@ -1490,31 +1515,37 @@ class AudioRecorderManager(
|
|
|
1490
1515
|
if (analysisDataSize > 0) {
|
|
1491
1516
|
// Add this check to enforce minimum interval
|
|
1492
1517
|
if (isFirstAnalysis || (currentTime - lastEmissionTimeAnalysis) >= recordingConfig.intervalAnalysis) {
|
|
1493
|
-
|
|
1494
|
-
|
|
1495
|
-
|
|
1496
|
-
|
|
1497
|
-
|
|
1498
|
-
|
|
1499
|
-
|
|
1500
|
-
|
|
1501
|
-
|
|
1502
|
-
|
|
1503
|
-
|
|
1504
|
-
|
|
1505
|
-
|
|
1506
|
-
|
|
1507
|
-
|
|
1508
|
-
|
|
1509
|
-
|
|
1510
|
-
|
|
1511
|
-
|
|
1518
|
+
try {
|
|
1519
|
+
// Process and emit analysis data
|
|
1520
|
+
val analysisData = audioProcessor.processAudioData(
|
|
1521
|
+
accumulatedAnalysisData.toByteArray(),
|
|
1522
|
+
recordingConfig
|
|
1523
|
+
)
|
|
1524
|
+
|
|
1525
|
+
LogUtils.d(CLASS_NAME, """
|
|
1526
|
+
Analysis data details:
|
|
1527
|
+
- Raw data size: ${accumulatedAnalysisData.size()} bytes
|
|
1528
|
+
""".trimIndent())
|
|
1529
|
+
|
|
1530
|
+
mainHandler.post {
|
|
1531
|
+
try {
|
|
1532
|
+
eventSender.sendExpoEvent(
|
|
1533
|
+
Constants.AUDIO_ANALYSIS_EVENT_NAME,
|
|
1534
|
+
analysisData.toBundle()
|
|
1535
|
+
)
|
|
1536
|
+
} catch (e: Exception) {
|
|
1537
|
+
LogUtils.e(CLASS_NAME, "Failed to send audio analysis event", e)
|
|
1538
|
+
}
|
|
1512
1539
|
}
|
|
1540
|
+
|
|
1541
|
+
lastEmissionTimeAnalysis = currentTime
|
|
1542
|
+
isFirstAnalysis = false
|
|
1543
|
+
} catch (e: Exception) {
|
|
1544
|
+
LogUtils.e(CLASS_NAME, "Failed to process audio analysis data", e)
|
|
1545
|
+
} finally {
|
|
1546
|
+
// Always reset the buffer to prevent unbounded growth
|
|
1547
|
+
accumulatedAnalysisData.reset()
|
|
1513
1548
|
}
|
|
1514
|
-
|
|
1515
|
-
lastEmissionTimeAnalysis = currentTime
|
|
1516
|
-
accumulatedAnalysisData.reset() // Clear the analysis accumulator
|
|
1517
|
-
isFirstAnalysis = false
|
|
1518
1549
|
}
|
|
1519
1550
|
}
|
|
1520
1551
|
}
|
|
@@ -1620,8 +1651,13 @@ class AudioRecorderManager(
|
|
|
1620
1651
|
}
|
|
1621
1652
|
}
|
|
1622
1653
|
|
|
1623
|
-
|
|
1624
|
-
|
|
1654
|
+
// Analysis is already handled in recordingProcess method to avoid duplicate processing
|
|
1655
|
+
// and prevent memory issues from accumulating data in multiple buffers
|
|
1656
|
+
|
|
1657
|
+
// Update notification waveform if needed (moved from processAudioData)
|
|
1658
|
+
if (recordingConfig.showNotification && recordingConfig.showWaveformInNotification) {
|
|
1659
|
+
val floatArray = convertByteArrayToFloatArray(audioData)
|
|
1660
|
+
notificationManager.updateNotification(floatArray)
|
|
1625
1661
|
}
|
|
1626
1662
|
}
|
|
1627
1663
|
|
|
@@ -1634,55 +1670,6 @@ class AudioRecorderManager(
|
|
|
1634
1670
|
return floatArray
|
|
1635
1671
|
}
|
|
1636
1672
|
|
|
1637
|
-
private fun processAudioData(audioData: ByteArray) {
|
|
1638
|
-
// Skip the WAV header only for the first chunk
|
|
1639
|
-
val dataToProcess = if (isFirstChunk && audioData.size > Constants.WAV_HEADER_SIZE) {
|
|
1640
|
-
audioData.copyOfRange(Constants.WAV_HEADER_SIZE, audioData.size)
|
|
1641
|
-
} else {
|
|
1642
|
-
audioData
|
|
1643
|
-
}
|
|
1644
|
-
|
|
1645
|
-
// Accumulate data for analysis
|
|
1646
|
-
if (recordingConfig.enableProcessing) {
|
|
1647
|
-
synchronized(analysisBuffer) {
|
|
1648
|
-
analysisBuffer.write(dataToProcess)
|
|
1649
|
-
}
|
|
1650
|
-
|
|
1651
|
-
val currentTime = SystemClock.elapsedRealtime()
|
|
1652
|
-
if (isFirstAnalysis || (currentTime - lastEmissionTimeAnalysis) >= recordingConfig.intervalAnalysis) {
|
|
1653
|
-
synchronized(analysisBuffer) {
|
|
1654
|
-
if (analysisBuffer.size() > 0) {
|
|
1655
|
-
val analysisData = audioProcessor.processAudioData(
|
|
1656
|
-
analysisBuffer.toByteArray(),
|
|
1657
|
-
recordingConfig
|
|
1658
|
-
)
|
|
1659
|
-
|
|
1660
|
-
mainHandler.post {
|
|
1661
|
-
eventSender.sendExpoEvent(
|
|
1662
|
-
Constants.AUDIO_ANALYSIS_EVENT_NAME,
|
|
1663
|
-
analysisData.toBundle()
|
|
1664
|
-
)
|
|
1665
|
-
}
|
|
1666
|
-
|
|
1667
|
-
// Reset buffer after processing
|
|
1668
|
-
analysisBuffer.reset()
|
|
1669
|
-
lastEmissionTimeAnalysis = currentTime
|
|
1670
|
-
isFirstAnalysis = false
|
|
1671
|
-
}
|
|
1672
|
-
}
|
|
1673
|
-
}
|
|
1674
|
-
}
|
|
1675
|
-
|
|
1676
|
-
// Only update notification if needed
|
|
1677
|
-
if (recordingConfig.showNotification && recordingConfig.showWaveformInNotification) {
|
|
1678
|
-
val floatArray = convertByteArrayToFloatArray(audioData)
|
|
1679
|
-
notificationManager.updateNotification(floatArray)
|
|
1680
|
-
}
|
|
1681
|
-
|
|
1682
|
-
// Reset isFirstChunk after processing
|
|
1683
|
-
isFirstChunk = false
|
|
1684
|
-
}
|
|
1685
|
-
|
|
1686
1673
|
fun cleanup() {
|
|
1687
1674
|
synchronized(audioRecordLock) {
|
|
1688
1675
|
try {
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@siteed/expo-audio-studio",
|
|
3
|
-
"version": "2.
|
|
3
|
+
"version": "2.18.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",
|