@siteed/expo-audio-studio 2.15.0 → 2.16.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 CHANGED
@@ -8,6 +8,10 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
8
8
  ## [Unreleased]
9
9
 
10
10
 
11
+ ## [2.16.0] - 2025-07-27
12
+ ### Changed
13
+ - feat(expo-audio-studio): optimize stop recording performance for long recording on android ([4553dc9](https://github.com/deeeed/expo-audio-stream/commit/4553dc9d2bd101a461f3f2eadfed63114f7d1b22))
14
+ - chore(expo-audio-studio): release @siteed/expo-audio-studio@2.15.0 ([1af374a](https://github.com/deeeed/expo-audio-stream/commit/1af374ada18ec2cd4edeb151fc0e91e54f783b9e))
11
15
  ## [2.15.0] - 2025-07-15
12
16
  ### Changed
13
17
  - feat(android): add showPauseResumeActions option to notification config ([7456153](https://github.com/deeeed/expo-audio-stream/commit/7456153beb3f5041bd0199595b29d6b62c6b4c8f))
@@ -353,7 +357,8 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
353
357
  - Feature: Audio features extraction during recording.
354
358
  - Feature: Consistent WAV PCM recording format across all platforms.
355
359
 
356
- [unreleased]: https://github.com/deeeed/expo-audio-stream/compare/@siteed/expo-audio-studio@2.15.0...HEAD
360
+ [unreleased]: https://github.com/deeeed/expo-audio-stream/compare/@siteed/expo-audio-studio@2.16.0...HEAD
361
+ [2.16.0]: https://github.com/deeeed/expo-audio-stream/compare/@siteed/expo-audio-studio@2.15.0...@siteed/expo-audio-studio@2.16.0
357
362
  [2.15.0]: https://github.com/deeeed/expo-audio-stream/compare/@siteed/expo-audio-studio@2.14.9...@siteed/expo-audio-studio@2.15.0
358
363
  [2.14.9]: https://github.com/deeeed/expo-audio-stream/compare/@siteed/expo-audio-studio@2.14.8...@siteed/expo-audio-studio@2.14.9
359
364
  [2.14.8]: https://github.com/deeeed/expo-audio-stream/compare/@siteed/expo-audio-studio@2.14.7...@siteed/expo-audio-studio@2.14.8
@@ -921,6 +921,8 @@ class AudioRecorderManager(
921
921
  }
922
922
 
923
923
  fun stopRecording(promise: Promise) {
924
+ val stopStartTime = System.currentTimeMillis()
925
+
924
926
  synchronized(audioRecordLock) {
925
927
  if (!_isRecording.get()) {
926
928
  LogUtils.e(CLASS_NAME, "Recording is not active")
@@ -933,7 +935,9 @@ class AudioRecorderManager(
933
935
  var fileSize: Long = 0
934
936
 
935
937
  try {
938
+
936
939
  if (isPaused.get()) {
940
+ val readStartTime = System.currentTimeMillis()
937
941
  val remainingData = ByteArray(bufferSizeInBytes)
938
942
  val bytesRead = audioRecord?.read(remainingData, 0, bufferSizeInBytes) ?: -1
939
943
  if (bytesRead > 0) {
@@ -942,6 +946,7 @@ class AudioRecorderManager(
942
946
  }
943
947
 
944
948
  if (recordingConfig.showNotification) {
949
+ val notificationStartTime = System.currentTimeMillis()
945
950
  notificationManager.stopUpdates()
946
951
  AudioRecordingService.stopService(context)
947
952
  }
@@ -949,28 +954,23 @@ class AudioRecorderManager(
949
954
  _isRecording.set(false)
950
955
  isPrepared = false // Reset preparation state
951
956
 
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)")
957
+ // Use a reasonable fixed timeout for all cases
958
+ // The recording thread should exit quickly with non-blocking read
959
+ val timeoutMs = 2000L // 2 seconds should be more than enough
960
+ val threadJoinStartTime = System.currentTimeMillis()
963
961
  recordingThread?.join(timeoutMs)
964
962
 
963
+ val finalReadStartTime = System.currentTimeMillis()
965
964
  val audioData = ByteArray(bufferSizeInBytes)
966
965
  val bytesRead = audioRecord?.read(audioData, 0, bufferSizeInBytes) ?: -1
967
- LogUtils.d(CLASS_NAME, "Last Read $bytesRead bytes")
968
966
  if (bytesRead > 0) {
967
+ val emitStartTime = System.currentTimeMillis()
969
968
  emitAudioData(audioData.copyOfRange(0, bytesRead), bytesRead)
970
969
  }
971
970
 
972
971
  LogUtils.d(CLASS_NAME, "Stopping recording state = ${audioRecord?.state}")
973
972
  if (audioRecord != null && audioRecord!!.state == AudioRecord.STATE_INITIALIZED) {
973
+ val audioStopStartTime = System.currentTimeMillis()
974
974
  LogUtils.d(CLASS_NAME, "Stopping AudioRecord")
975
975
  audioRecord!!.stop()
976
976
  }
@@ -1003,6 +1003,7 @@ class AudioRecorderManager(
1003
1003
  fileDuration
1004
1004
  }
1005
1005
 
1006
+ val cleanupStartTime = System.currentTimeMillis()
1006
1007
  cleanup()
1007
1008
  } catch (e: IllegalStateException) {
1008
1009
  LogUtils.e(CLASS_NAME, "Error reading from AudioRecord", e)
@@ -1015,16 +1016,25 @@ class AudioRecorderManager(
1015
1016
  AudioProcessor.resetUniqueIdCounter()
1016
1017
  audioProcessor.resetCumulativeAmplitudeRange()
1017
1018
 
1018
- compressedRecorder?.apply {
1019
- stop()
1020
- release()
1019
+ if (compressedRecorder != null) {
1020
+ val compressedStopStartTime = System.currentTimeMillis()
1021
+ try {
1022
+ compressedRecorder?.stop()
1023
+
1024
+ val compressedReleaseStartTime = System.currentTimeMillis()
1025
+ compressedRecorder?.release()
1026
+ } catch (e: Exception) {
1027
+ LogUtils.e(CLASS_NAME, "Error stopping MediaRecorder: ${e.message}")
1028
+ }
1029
+ compressedRecorder = null
1021
1030
  }
1022
- compressedRecorder = null
1023
1031
 
1024
1032
  // Log compressed file status if enabled - use actual file size for validation
1025
1033
  if (recordingConfig.output.compressed.enabled) {
1034
+ val fileSizeStartTime = System.currentTimeMillis()
1026
1035
  // Note: For compressed files, we need to get actual size as MediaRecorder handles the writing
1027
1036
  // Use actual file size here for validation purposes only
1037
+ val compressedSizeStartTime = System.currentTimeMillis()
1028
1038
  val compressedSize = compressedFile?.length() ?: 0
1029
1039
  cachedCompressedFileSize = compressedSize // Update cache with final size
1030
1040
  LogUtils.d(CLASS_NAME, "Compressed File validation - Size: $compressedSize bytes, Path: ${compressedFile?.absolutePath}")
@@ -1084,6 +1094,13 @@ class AudioRecorderManager(
1084
1094
  ) else null
1085
1095
  )
1086
1096
  }
1097
+
1098
+ // Log total stop duration if it's slow
1099
+ val stopDuration = System.currentTimeMillis() - stopStartTime
1100
+ if (stopDuration > 200) {
1101
+ LogUtils.w(CLASS_NAME, "Stop recording took ${stopDuration}ms - consider investigating")
1102
+ }
1103
+
1087
1104
  promise.resolve(result)
1088
1105
 
1089
1106
  // Reset the timing variables
@@ -1399,7 +1416,12 @@ class AudioRecorderManager(
1399
1416
  """.trimIndent())
1400
1417
 
1401
1418
  // Recording loop
1419
+ var loopCount = 0
1402
1420
  while (_isRecording.get() && !Thread.currentThread().isInterrupted) {
1421
+ loopCount++
1422
+ if (loopCount % 100 == 0) {
1423
+ LogUtils.d(CLASS_NAME, "Recording loop iteration $loopCount, _isRecording: ${_isRecording.get()}")
1424
+ }
1403
1425
  if (isPaused.get()) {
1404
1426
  Thread.sleep(100) // Add small delay when paused
1405
1427
  continue
@@ -1416,7 +1438,8 @@ class AudioRecorderManager(
1416
1438
  LogUtils.e(CLASS_NAME, "AudioRecord not initialized")
1417
1439
  return@let -1
1418
1440
  }
1419
- it.read(audioData, 0, bufferSizeInBytes).also { bytes ->
1441
+ // Use non-blocking read mode to allow quick thread exit
1442
+ it.read(audioData, 0, bufferSizeInBytes, AudioRecord.READ_NON_BLOCKING).also { bytes ->
1420
1443
  if (bytes < 0) {
1421
1444
  LogUtils.e(CLASS_NAME, "AudioRecord read error: $bytes")
1422
1445
  }
@@ -1486,6 +1509,9 @@ class AudioRecorderManager(
1486
1509
  }
1487
1510
  }
1488
1511
  }
1512
+ } else if (bytesRead == 0) {
1513
+ // No data available yet, sleep briefly to avoid busy-waiting
1514
+ Thread.sleep(10)
1489
1515
  }
1490
1516
  }
1491
1517
  } finally {
@@ -1673,7 +1699,14 @@ class AudioRecorderManager(
1673
1699
 
1674
1700
  // Update the WAV header if needed
1675
1701
  audioFile?.let { file ->
1676
- audioFileHandler.updateWavHeader(file)
1702
+ // Skip WAV header update if we're only doing compressed output
1703
+ if (::recordingConfig.isInitialized &&
1704
+ !recordingConfig.output.primary.enabled &&
1705
+ recordingConfig.output.compressed.enabled) {
1706
+ // Skip WAV header update for compressed-only recording
1707
+ } else {
1708
+ audioFileHandler.updateWavHeader(file)
1709
+ }
1677
1710
  }
1678
1711
 
1679
1712
  // Send event to notify that recording was stopped
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@siteed/expo-audio-studio",
3
- "version": "2.15.0",
3
+ "version": "2.16.0",
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",