@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.17.0...HEAD
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
- bufferSizeInBytes = recordingConfig.bufferDurationSeconds?.let { bufferDuration ->
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
- val requestedSize = (bufferDuration * recordingConfig.sampleRate *
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 (!recordingConfig.output.primary.enabled) {
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
- // Also accumulate data for analysis if enabled
1464
- if (shouldProcessAnalysis) {
1465
- accumulatedAnalysisData.write(audioData, 0, bytesRead)
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
- // Process and emit analysis data
1494
- val analysisData = audioProcessor.processAudioData(
1495
- accumulatedAnalysisData.toByteArray(),
1496
- recordingConfig
1497
- )
1498
-
1499
- LogUtils.d(CLASS_NAME, """
1500
- Analysis data details:
1501
- - Raw data size: ${accumulatedAnalysisData.size()} bytes
1502
- """.trimIndent())
1503
-
1504
- mainHandler.post {
1505
- try {
1506
- eventSender.sendExpoEvent(
1507
- Constants.AUDIO_ANALYSIS_EVENT_NAME,
1508
- analysisData.toBundle()
1509
- )
1510
- } catch (e: Exception) {
1511
- LogUtils.e(CLASS_NAME, "Failed to send audio analysis event", e)
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
- if (recordingConfig.enableProcessing) {
1624
- processAudioData(audioData)
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.17.0",
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",