@siteed/expo-audio-studio 2.2.0 → 2.3.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,15 +8,19 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
8
8
  ## [Unreleased]
9
9
 
10
10
 
11
+ ## [2.3.1] - 2025-04-03
12
+ ### Changed
13
+ - feat: no external crc32 libs (#195) ([394b3b3](https://github.com/deeeed/expo-audio-stream/commit/394b3b3bb04e3f969db2a502af85d69c0f955b97))
14
+ ## [2.3.0] - 2025-03-29
15
+ ### Changed
16
+ - fix: always generate a new UUID unless filename is provided (#182) ([f98a9a5](https://github.com/deeeed/expo-audio-stream/commit/f98a9a52393829e6c4a79aee3575fbfcc9416c19))
17
+ - refactor(audio-studio): introduce constants for silence threshold and WAV header size (#188) ([e8aa329](https://github.com/deeeed/expo-audio-stream/commit/e8aa3298bd6ba029d38898360b7df26b3fd5485f))
18
+ - docs: enhance installation and API reference documentation for phone call handling (#187) ([fcaece1](https://github.com/deeeed/expo-audio-stream/commit/fcaece18cf046d970b9659f3f12a19deb096bceb))
11
19
  ## [2.2.0] - 2025-03-28
12
20
  ### Changed
13
21
  - refactor(audio-studio): implement platform-specific CRC32 handling ([b61a3d7](https://github.com/deeeed/expo-audio-stream/commit/b61a3d743914e66888ec6cc4cb8e010ff1992698))
14
22
  - chore: update Expo dependencies and remove invalid design-system version ([16e5007](https://github.com/deeeed/expo-audio-stream/commit/16e50077690b55977c22fbcb08be75834146ff47))
15
23
  - fix: linting issues ([741589d](https://github.com/deeeed/expo-audio-stream/commit/741589d60485a2d049e7adf529d3fd2b999fa098))
16
- - feat: Enhance Audio Processing and Transcription Capabilities in Playground App (#171) ([1ec6026](https://github.com/deeeed/expo-audio-stream/commit/1ec6026ff75fc3ff7122b5df72e8dcd15ce848bd))
17
- - feat: Enhance Essentia Integration with iOS and Android Build Improvements (#169) ([422fd50](https://github.com/deeeed/expo-audio-stream/commit/422fd501a5ec71f30df660d56559bc410084b797))
18
- - feat: Enhance Audio Analysis with Mel Spectrogram Comparison and Pipeline Support (#164) ([541e13c](https://github.com/deeeed/expo-audio-stream/commit/541e13c6e01b8ff9947bc69dc7c29ffed6d8ee07))
19
- - feat: Integrate Essentia Audio Analysis Library into React Native for Android (#163) ([4cac310](https://github.com/deeeed/expo-audio-stream/commit/4cac310e4af47ddda528dee0f2840e3a336c6823))
20
24
  - chore(expo-audio-studio): release @siteed/expo-audio-studio@2.1.1 ([1b17ac6](https://github.com/deeeed/expo-audio-stream/commit/1b17ac6e103f2ca50f29668b3ddaaf57a4b4b7d3))
21
25
  ## [2.1.1] - 2025-03-04
22
26
  ### Changed
@@ -181,7 +185,9 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
181
185
  - Feature: Audio features extraction during recording.
182
186
  - Feature: Consistent WAV PCM recording format across all platforms.
183
187
 
184
- [unreleased]: https://github.com/deeeed/expo-audio-stream/compare/@siteed/expo-audio-studio@2.2.0...HEAD
188
+ [unreleased]: https://github.com/deeeed/expo-audio-stream/compare/@siteed/expo-audio-studio@2.3.1...HEAD
189
+ [2.3.1]: https://github.com/deeeed/expo-audio-stream/compare/@siteed/expo-audio-studio@2.3.0...@siteed/expo-audio-studio@2.3.1
190
+ [2.3.0]: https://github.com/deeeed/expo-audio-stream/compare/@siteed/expo-audio-studio@2.2.0...@siteed/expo-audio-studio@2.3.0
185
191
  [2.2.0]: https://github.com/deeeed/expo-audio-stream/compare/@siteed/expo-audio-studio@2.1.1...@siteed/expo-audio-studio@2.2.0
186
192
  [2.1.1]: https://github.com/deeeed/expo-audio-stream/compare/@siteed/expo-audio-studio@2.1.0...@siteed/expo-audio-studio@2.1.1
187
193
  [2.1.0]: https://github.com/deeeed/expo-audio-stream/compare/@siteed/expo-audio-stream@2.0.1...@siteed/expo-audio-stream@2.1.0
@@ -1,6 +1,6 @@
1
+ <?xml version="1.0" encoding="utf-8"?>
1
2
  <manifest xmlns:android="http://schemas.android.com/apk/res/android">
2
3
  <!-- Permissions will be merged into the app's manifest -->
3
- <uses-permission android:name="android.permission.READ_PHONE_STATE" />
4
4
  <uses-permission android:name="android.permission.RECORD_AUDIO"/>
5
5
  <uses-permission android:name="android.permission.FOREGROUND_SERVICE"/>
6
6
  <uses-permission android:name="android.permission.FOREGROUND_SERVICE_MICROPHONE"/>
@@ -37,7 +37,8 @@ class AudioRecorderManager(
37
37
  private val filesDir: File,
38
38
  private val permissionUtils: PermissionUtils,
39
39
  private val audioDataEncoder: AudioDataEncoder,
40
- private val eventSender: EventSender
40
+ private val eventSender: EventSender,
41
+ private val enablePhoneStateHandling: Boolean = true
41
42
  ) {
42
43
  private var audioRecord: AudioRecord? = null
43
44
  private var bufferSizeInBytes = 0
@@ -348,8 +349,10 @@ class AudioRecorderManager(
348
349
  @RequiresApi(Build.VERSION_CODES.R)
349
350
  fun startRecording(options: Map<String, Any?>, promise: Promise) {
350
351
  try {
351
- // Initialize phone state listener
352
- initializePhoneStateListener()
352
+ // Initialize phone state listener only if enabled
353
+ if (enablePhoneStateHandling) {
354
+ initializePhoneStateListener()
355
+ }
353
356
 
354
357
  // Request audio focus
355
358
  if (!requestAudioFocus()) {
@@ -479,7 +482,8 @@ class AudioRecorderManager(
479
482
  return false
480
483
  }
481
484
 
482
- if (!permissionUtils.checkPhoneStatePermission()) {
485
+ // Only check phone state permission if enabled
486
+ if (enablePhoneStateHandling && !permissionUtils.checkPhoneStatePermission()) {
483
487
  Log.w(Constants.TAG, "READ_PHONE_STATE permission not granted, phone call interruption handling will be disabled")
484
488
  // Don't reject here, just log warning as this is optional
485
489
  }
@@ -16,6 +16,7 @@ import java.util.zip.CRC32
16
16
  class ExpoAudioStreamModule : Module(), EventSender {
17
17
  private lateinit var audioRecorderManager: AudioRecorderManager
18
18
  private lateinit var audioProcessor: AudioProcessor
19
+ private var enablePhoneStateHandling: Boolean = true // Default to true for backward compatibility
19
20
 
20
21
  private val audioFileHandler by lazy {
21
22
  AudioFileHandler(appContext.reactContext?.filesDir ?: throw IllegalStateException("React context not available"))
@@ -174,10 +175,14 @@ class ExpoAudioStreamModule : Module(), EventSender {
174
175
  AsyncFunction("requestPermissionsAsync") { promise: Promise ->
175
176
  try {
176
177
  val permissions = mutableListOf(
177
- Manifest.permission.RECORD_AUDIO,
178
- Manifest.permission.READ_PHONE_STATE
178
+ Manifest.permission.RECORD_AUDIO
179
179
  )
180
180
 
181
+ // Only add phone state permission if enabled
182
+ if (enablePhoneStateHandling) {
183
+ permissions.add(Manifest.permission.READ_PHONE_STATE)
184
+ }
185
+
181
186
  // Add foreground service permission for Android 14+
182
187
  if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.UPSIDE_DOWN_CAKE) {
183
188
  Log.d(Constants.TAG, "Adding FOREGROUND_SERVICE_MICROPHONE permission request")
@@ -198,10 +203,14 @@ class ExpoAudioStreamModule : Module(), EventSender {
198
203
 
199
204
  AsyncFunction("getPermissionsAsync") { promise: Promise ->
200
205
  val permissions = mutableListOf(
201
- Manifest.permission.RECORD_AUDIO,
202
- Manifest.permission.READ_PHONE_STATE
206
+ Manifest.permission.RECORD_AUDIO
203
207
  )
204
208
 
209
+ // Only add phone state permission if enabled
210
+ if (enablePhoneStateHandling) {
211
+ permissions.add(Manifest.permission.READ_PHONE_STATE)
212
+ }
213
+
205
214
  if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.UPSIDE_DOWN_CAKE) {
206
215
  permissions.add(Manifest.permission.FOREGROUND_SERVICE_MICROPHONE)
207
216
  }
@@ -714,20 +723,18 @@ class ExpoAudioStreamModule : Module(), EventSender {
714
723
  }
715
724
 
716
725
  private fun initializeManager() {
717
- val androidContext =
718
- appContext.reactContext ?: throw IllegalStateException("Android context not available")
719
- val permissionUtils = PermissionUtils(androidContext)
720
- val audioEncoder = AudioDataEncoder()
721
- audioRecorderManager =
722
- AudioRecorderManager(androidContext, androidContext.filesDir, permissionUtils, audioEncoder, this)
723
- audioRecorderManager = AudioRecorderManager.initialize(
724
- androidContext,
725
- androidContext.filesDir,
726
+ val filesDir = appContext.reactContext?.filesDir ?: throw IllegalStateException("React context not available")
727
+ val permissionUtils = PermissionUtils(appContext.reactContext!!)
728
+ val audioDataEncoder = AudioDataEncoder()
729
+ audioRecorderManager = AudioRecorderManager(
730
+ appContext.reactContext!!,
731
+ filesDir,
726
732
  permissionUtils,
727
- audioEncoder,
728
- this
733
+ audioDataEncoder,
734
+ this,
735
+ enablePhoneStateHandling
729
736
  )
730
- audioProcessor = AudioProcessor(androidContext.filesDir)
737
+ audioProcessor = AudioProcessor(filesDir)
731
738
  }
732
739
 
733
740
 
@@ -1,4 +1,4 @@
1
- interface CRC32 {
1
+ export interface CRC32 {
2
2
  (data: string | Uint8Array): number;
3
3
  buf(data: Uint8Array): number;
4
4
  }
@@ -1 +1 @@
1
- {"version":3,"file":"crc32.d.ts","sourceRoot":"","sources":["../../src/utils/crc32.ts"],"names":[],"mappings":"AAEA,UAAU,KAAK;IACX,CAAC,IAAI,EAAE,MAAM,GAAG,UAAU,GAAG,MAAM,CAAC;IACpC,GAAG,CAAC,IAAI,EAAE,UAAU,GAAG,MAAM,CAAC;CACjC;AAED,QAAA,IAAI,mBAAmB,EAAE,KAAK,CAAA;AAa9B,eAAe,mBAAmB,CAAA"}
1
+ {"version":3,"file":"crc32.d.ts","sourceRoot":"","sources":["../../src/utils/crc32.ts"],"names":[],"mappings":"AAKA,MAAM,WAAW,KAAK;IAClB,CAAC,IAAI,EAAE,MAAM,GAAG,UAAU,GAAG,MAAM,CAAA;IACnC,GAAG,CAAC,IAAI,EAAE,UAAU,GAAG,MAAM,CAAA;CAChC;AAsCD,QAAA,IAAI,mBAAmB,EAAE,KAAK,CAAA;AAY9B,eAAe,mBAAmB,CAAA"}
@@ -1,8 +1,46 @@
1
+ // Bundler (Metro/Webpack) will automatically resolve to crc32.web.ts or crc32.native.ts.
1
2
  import { Platform } from 'react-native';
3
+ // --- Web CRC32 Calculation Logic ---
4
+ let webCrcTable;
5
+ function computeWebCrc32(data) {
6
+ // Lazily compute the table only on web when first needed
7
+ if (!webCrcTable) {
8
+ webCrcTable = (() => {
9
+ const table = new Uint32Array(256);
10
+ for (let i = 0; i < 256; ++i) {
11
+ let crc = i;
12
+ for (let j = 0; j < 8; ++j) {
13
+ crc = crc & 1 ? (crc >>> 1) ^ 0xedb88320 : crc >>> 1;
14
+ }
15
+ table[i] = crc;
16
+ }
17
+ return table;
18
+ })();
19
+ }
20
+ let crc = -1; // Initialize with 0xFFFFFFFF
21
+ if (typeof data === 'string') {
22
+ const strBytes = new TextEncoder().encode(data);
23
+ for (let i = 0; i < strBytes.length; ++i) {
24
+ crc = (crc >>> 8) ^ webCrcTable[(crc ^ strBytes[i]) & 0xff];
25
+ }
26
+ }
27
+ else if (data instanceof Uint8Array) {
28
+ for (let i = 0; i < data.length; ++i) {
29
+ crc = (crc >>> 8) ^ webCrcTable[(crc ^ data[i]) & 0xff];
30
+ }
31
+ }
32
+ else {
33
+ throw new Error('Unsupported data type for CRC32 calculation.');
34
+ }
35
+ return (crc ^ -1) >>> 0; // Final XOR and ensure unsigned 32-bit
36
+ }
37
+ // --- End Web CRC32 Calculation Logic ---
2
38
  let crc32Implementation;
3
39
  if (Platform.OS === 'web') {
4
- // eslint-disable-next-line @typescript-eslint/no-var-requires
5
- crc32Implementation = require('crc-32');
40
+ // Assign the web implementation
41
+ crc32Implementation = Object.assign(computeWebCrc32, {
42
+ buf: (data) => computeWebCrc32(data),
43
+ });
6
44
  }
7
45
  else {
8
46
  // No-op implementation for native platforms
@@ -1 +1 @@
1
- {"version":3,"file":"crc32.js","sourceRoot":"","sources":["../../src/utils/crc32.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,QAAQ,EAAE,MAAM,cAAc,CAAA;AAOvC,IAAI,mBAA0B,CAAA;AAE9B,IAAI,QAAQ,CAAC,EAAE,KAAK,KAAK,EAAE,CAAC;IACxB,8DAA8D;IAC9D,mBAAmB,GAAG,OAAO,CAAC,QAAQ,CAAC,CAAA;AAC3C,CAAC;KAAM,CAAC;IACJ,4CAA4C;IAC5C,mBAAmB,GAAG,MAAM,CAAC,MAAM,CAC/B,GAAG,EAAE,CAAC,CAAC,EACP,EAAE,GAAG,EAAE,GAAG,EAAE,CAAC,CAAC,EAAE,CACnB,CAAA;AACL,CAAC;AAED,eAAe,mBAAmB,CAAA","sourcesContent":["import { Platform } from 'react-native'\n\ninterface CRC32 {\n (data: string | Uint8Array): number;\n buf(data: Uint8Array): number;\n}\n\nlet crc32Implementation: CRC32\n\nif (Platform.OS === 'web') {\n // eslint-disable-next-line @typescript-eslint/no-var-requires\n crc32Implementation = require('crc-32')\n} else {\n // No-op implementation for native platforms\n crc32Implementation = Object.assign(\n () => 0,\n { buf: () => 0 }\n )\n}\n\nexport default crc32Implementation\n"]}
1
+ {"version":3,"file":"crc32.js","sourceRoot":"","sources":["../../src/utils/crc32.ts"],"names":[],"mappings":"AAAA,yFAAyF;AAEzF,OAAO,EAAE,QAAQ,EAAE,MAAM,cAAc,CAAA;AAQvC,sCAAsC;AACtC,IAAI,WAAoC,CAAA;AACxC,SAAS,eAAe,CAAC,IAAyB;IAC9C,yDAAyD;IACzD,IAAI,CAAC,WAAW,EAAE,CAAC;QACf,WAAW,GAAG,CAAC,GAAG,EAAE;YAChB,MAAM,KAAK,GAAG,IAAI,WAAW,CAAC,GAAG,CAAC,CAAA;YAClC,KAAK,IAAI,CAAC,GAAG,CAAC,EAAE,CAAC,GAAG,GAAG,EAAE,EAAE,CAAC,EAAE,CAAC;gBAC3B,IAAI,GAAG,GAAG,CAAC,CAAA;gBACX,KAAK,IAAI,CAAC,GAAG,CAAC,EAAE,CAAC,GAAG,CAAC,EAAE,EAAE,CAAC,EAAE,CAAC;oBACzB,GAAG,GAAG,GAAG,GAAG,CAAC,CAAC,CAAC,CAAC,CAAC,GAAG,KAAK,CAAC,CAAC,GAAG,UAAU,CAAC,CAAC,CAAC,GAAG,KAAK,CAAC,CAAA;gBACxD,CAAC;gBACD,KAAK,CAAC,CAAC,CAAC,GAAG,GAAG,CAAA;YAClB,CAAC;YACD,OAAO,KAAK,CAAA;QAChB,CAAC,CAAC,EAAE,CAAA;IACR,CAAC;IAED,IAAI,GAAG,GAAG,CAAC,CAAC,CAAA,CAAC,6BAA6B;IAC1C,IAAI,OAAO,IAAI,KAAK,QAAQ,EAAE,CAAC;QAC3B,MAAM,QAAQ,GAAG,IAAI,WAAW,EAAE,CAAC,MAAM,CAAC,IAAI,CAAC,CAAA;QAC/C,KAAK,IAAI,CAAC,GAAG,CAAC,EAAE,CAAC,GAAG,QAAQ,CAAC,MAAM,EAAE,EAAE,CAAC,EAAE,CAAC;YACvC,GAAG,GAAG,CAAC,GAAG,KAAK,CAAC,CAAC,GAAG,WAAW,CAAC,CAAC,GAAG,GAAG,QAAQ,CAAC,CAAC,CAAC,CAAC,GAAG,IAAI,CAAC,CAAA;QAC/D,CAAC;IACL,CAAC;SAAM,IAAI,IAAI,YAAY,UAAU,EAAE,CAAC;QACpC,KAAK,IAAI,CAAC,GAAG,CAAC,EAAE,CAAC,GAAG,IAAI,CAAC,MAAM,EAAE,EAAE,CAAC,EAAE,CAAC;YACnC,GAAG,GAAG,CAAC,GAAG,KAAK,CAAC,CAAC,GAAG,WAAW,CAAC,CAAC,GAAG,GAAG,IAAI,CAAC,CAAC,CAAC,CAAC,GAAG,IAAI,CAAC,CAAA;QAC3D,CAAC;IACL,CAAC;SAAM,CAAC;QACJ,MAAM,IAAI,KAAK,CAAC,8CAA8C,CAAC,CAAA;IACnE,CAAC;IAED,OAAO,CAAC,GAAG,GAAG,CAAC,CAAC,CAAC,KAAK,CAAC,CAAA,CAAC,uCAAuC;AACnE,CAAC;AACD,0CAA0C;AAE1C,IAAI,mBAA0B,CAAA;AAE9B,IAAI,QAAQ,CAAC,EAAE,KAAK,KAAK,EAAE,CAAC;IACxB,gCAAgC;IAChC,mBAAmB,GAAG,MAAM,CAAC,MAAM,CAAC,eAAe,EAAE;QACjD,GAAG,EAAE,CAAC,IAAgB,EAAU,EAAE,CAAC,eAAe,CAAC,IAAI,CAAC;KAC3D,CAAC,CAAA;AACN,CAAC;KAAM,CAAC;IACJ,4CAA4C;IAC5C,mBAAmB,GAAG,MAAM,CAAC,MAAM,CAAC,GAAG,EAAE,CAAC,CAAC,EAAE,EAAE,GAAG,EAAE,GAAG,EAAE,CAAC,CAAC,EAAE,CAAC,CAAA;AAClE,CAAC;AAED,eAAe,mBAAmB,CAAA","sourcesContent":["// Bundler (Metro/Webpack) will automatically resolve to crc32.web.ts or crc32.native.ts.\n\nimport { Platform } from 'react-native'\n\n// Define the interface first\nexport interface CRC32 {\n (data: string | Uint8Array): number\n buf(data: Uint8Array): number\n}\n\n// --- Web CRC32 Calculation Logic ---\nlet webCrcTable: Uint32Array | undefined\nfunction computeWebCrc32(data: string | Uint8Array): number {\n // Lazily compute the table only on web when first needed\n if (!webCrcTable) {\n webCrcTable = (() => {\n const table = new Uint32Array(256)\n for (let i = 0; i < 256; ++i) {\n let crc = i\n for (let j = 0; j < 8; ++j) {\n crc = crc & 1 ? (crc >>> 1) ^ 0xedb88320 : crc >>> 1\n }\n table[i] = crc\n }\n return table\n })()\n }\n\n let crc = -1 // Initialize with 0xFFFFFFFF\n if (typeof data === 'string') {\n const strBytes = new TextEncoder().encode(data)\n for (let i = 0; i < strBytes.length; ++i) {\n crc = (crc >>> 8) ^ webCrcTable[(crc ^ strBytes[i]) & 0xff]\n }\n } else if (data instanceof Uint8Array) {\n for (let i = 0; i < data.length; ++i) {\n crc = (crc >>> 8) ^ webCrcTable[(crc ^ data[i]) & 0xff]\n }\n } else {\n throw new Error('Unsupported data type for CRC32 calculation.')\n }\n\n return (crc ^ -1) >>> 0 // Final XOR and ensure unsigned 32-bit\n}\n// --- End Web CRC32 Calculation Logic ---\n\nlet crc32Implementation: CRC32\n\nif (Platform.OS === 'web') {\n // Assign the web implementation\n crc32Implementation = Object.assign(computeWebCrc32, {\n buf: (data: Uint8Array): number => computeWebCrc32(data),\n })\n} else {\n // No-op implementation for native platforms\n crc32Implementation = Object.assign(() => 0, { buf: () => 0 })\n}\n\nexport default crc32Implementation\n"]}
@@ -5,6 +5,9 @@ import Accelerate
5
5
  import AVFoundation
6
6
  import QuartzCore
7
7
 
8
+ // Constants
9
+ private let SILENCE_THRESHOLD_RMS: Float = 0.01
10
+
8
11
  public struct TrimResult {
9
12
  let uri: String
10
13
  let filename: String
@@ -416,7 +419,7 @@ public class AudioProcessor {
416
419
  ) -> DataPoint {
417
420
  let sumSquares: Float = segment.reduce(0) { $0 + $1 * $1 }
418
421
  let rms = sqrt(sumSquares / Float(segment.count))
419
- let silent = rms < 0.01
422
+ let silent = rms < SILENCE_THRESHOLD_RMS
420
423
  let dB = Float(20 * log10(Double(rms)))
421
424
 
422
425
  let features = computeFeatures(
@@ -590,7 +593,7 @@ public class AudioProcessor {
590
593
  amplitude: localMax, // Always use peak amplitude
591
594
  rms: rms, // Use calculated RMS value
592
595
  dB: Float(20 * log10(Double(rms))), // Use RMS for dB calculation
593
- silent: rms < 0.01, // Use RMS for silence detection
596
+ silent: rms < SILENCE_THRESHOLD_RMS, // Use RMS for silence detection
594
597
  features: computeFeatures(
595
598
  segmentData: Array(UnsafeBufferPointer(start: summedData, count: Int(framesToRead))),
596
599
  sampleRate: sampleRate,
@@ -599,7 +602,7 @@ public class AudioProcessor {
599
602
  segmentLength: Int(framesToRead),
600
603
  featureOptions: featureOptions
601
604
  ),
602
- speech: SpeechFeatures(isActive: rms >= 0.01),
605
+ speech: SpeechFeatures(isActive: rms >= SILENCE_THRESHOLD_RMS),
603
606
  startTime: startTime,
604
607
  endTime: endTime,
605
608
  startPosition: Int(currentFrame),
@@ -1229,7 +1232,7 @@ public class AudioProcessor {
1229
1232
  featureOptions: featureOptions)
1230
1233
 
1231
1234
  let rms = features.rms
1232
- let silent = rms < 0.01
1235
+ let silent = rms < SILENCE_THRESHOLD_RMS
1233
1236
  let dB = Float(20 * log10(Double(rms)))
1234
1237
 
1235
1238
  let dataPoint = DataPoint(
@@ -12,6 +12,9 @@ import UIKit
12
12
  import MediaPlayer
13
13
  import UserNotifications
14
14
 
15
+ // Constants
16
+ internal let WAV_HEADER_SIZE: Int64 = 44 // Standard WAV header is 44 bytes
17
+
15
18
  // Helper to convert to little-endian byte array
16
19
  extension UInt32 {
17
20
  var littleEndianBytes: [UInt8] {
@@ -458,9 +461,8 @@ class AudioStreamManager: NSObject {
458
461
  let baseFilename: String
459
462
  if let existingFilename = recordingSettings?.filename {
460
463
  baseFilename = existingFilename
461
- } else if let existingUUID = recordingUUID {
462
- baseFilename = existingUUID.uuidString
463
464
  } else {
465
+ // Always create a new UUID for recording unless a filename is provided
464
466
  let newUUID = UUID()
465
467
  recordingUUID = newUUID
466
468
  baseFilename = newUUID.uuidString
@@ -1096,17 +1098,17 @@ class AudioStreamManager: NSObject {
1096
1098
  - Exists: true
1097
1099
  - Size: \(wavFileSize) bytes
1098
1100
  - Duration: \(finalDuration) seconds
1099
- - Expected minimum size: 44 bytes (WAV header)
1101
+ - Expected minimum size: \(WAV_HEADER_SIZE) bytes (WAV header)
1100
1102
  """)
1101
1103
 
1102
1104
  // Return nil if the file is too small
1103
- if wavFileSize <= 44 {
1104
- Logger.debug("Recording file is too small (≤ 44 bytes), likely no audio data was recorded")
1105
+ if wavFileSize <= WAV_HEADER_SIZE {
1106
+ Logger.debug("Recording file is too small (≤ \(WAV_HEADER_SIZE) bytes), likely no audio data was recorded")
1105
1107
  return nil
1106
1108
  }
1107
1109
 
1108
1110
  // Update the WAV header with the correct file size
1109
- updateWavHeader(fileURL: fileURL, totalDataSize: wavFileSize - 44)
1111
+ updateWavHeader(fileURL: fileURL, totalDataSize: wavFileSize - WAV_HEADER_SIZE)
1110
1112
 
1111
1113
  // Validate compressed file if enabled
1112
1114
  var compression: CompressedRecordingInfo?
@@ -1372,7 +1374,7 @@ class AudioStreamManager: NSObject {
1372
1374
  /// - fileURL: The URL of the WAV file.
1373
1375
  /// - totalDataSize: The total size of the audio data.
1374
1376
  private func updateWavHeader(fileURL: URL, totalDataSize: Int64) {
1375
- // Prevent negative values - minimum WAV file size should be at least the header size (44 bytes)
1377
+ // Prevent negative values - minimum WAV file size should be at least the header size (WAV_HEADER_SIZE bytes)
1376
1378
  guard totalDataSize >= 0 else {
1377
1379
  Logger.debug("Invalid file size: total data size is negative")
1378
1380
  return
@@ -1383,7 +1385,7 @@ class AudioStreamManager: NSObject {
1383
1385
  defer { fileHandle.closeFile() }
1384
1386
 
1385
1387
  // Calculate sizes
1386
- let fileSize = totalDataSize + 44 - 8 // Total file size minus 8 bytes for 'RIFF' and size field itself
1388
+ let fileSize = totalDataSize + WAV_HEADER_SIZE - 8 // Total file size minus 8 bytes for 'RIFF' and size field itself
1387
1389
  let dataSize = totalDataSize // Size of the 'data' sub-chunk
1388
1390
 
1389
1391
  // Update RIFF chunk size at offset 4
@@ -1585,8 +1587,19 @@ class AudioStreamManager: NSObject {
1585
1587
  guard let self = self else { return }
1586
1588
  if let processor = self.audioProcessor {
1587
1589
  Logger.debug("Processing audio buffer of size: \(dataToProcess.count)")
1590
+
1591
+ // Strip WAV header from the first buffer to avoid false amplitude detection
1592
+ let dataToAnalyze: Data
1593
+ if self.totalDataSizeAnalysis == 0 && dataToProcess.count > Int(WAV_HEADER_SIZE) {
1594
+ // This is the first buffer and may contain the WAV header
1595
+ dataToAnalyze = dataToProcess.subdata(in: Int(WAV_HEADER_SIZE)..<dataToProcess.count)
1596
+ Logger.debug("Removed WAV header (\(WAV_HEADER_SIZE) bytes) from first buffer for analysis")
1597
+ } else {
1598
+ dataToAnalyze = dataToProcess
1599
+ }
1600
+
1588
1601
  let processingResult = processor.processAudioBuffer(
1589
- data: dataToProcess,
1602
+ data: dataToAnalyze,
1590
1603
  sampleRate: Float(settings.sampleRate),
1591
1604
  segmentDurationMs: settings.segmentDurationMs,
1592
1605
  featureOptions: settings.featureOptions ?? [:],
@@ -1600,9 +1613,10 @@ class AudioStreamManager: NSObject {
1600
1613
  }
1601
1614
  }
1602
1615
 
1603
- // Update state after emission
1616
+ // Update state after emission
1604
1617
  self.lastEmissionTimeAnalysis = currentTime
1605
- self.lastEmittedSizeAnalysis = totalDataSizeAnalysis
1618
+ // Update the total analysis data size to mark that we've processed data
1619
+ self.totalDataSizeAnalysis = self.totalDataSize
1606
1620
  accumulatedAnalysisData.removeAll()
1607
1621
  }
1608
1622
  }
@@ -2,9 +2,11 @@
2
2
  import ExpoModulesCore
3
3
  import AVFoundation
4
4
 
5
- let audioDataEvent: String = "AudioData"
6
- let audioAnalysisEvent: String = "AudioAnalysis"
7
- let recordingInterruptedEvent: String = "onRecordingInterrupted"
5
+ // Constants
6
+ private let audioDataEvent: String = "AudioData"
7
+ private let audioAnalysisEvent: String = "AudioAnalysis"
8
+ private let recordingInterruptedEvent: String = "onRecordingInterrupted"
9
+ private let DEFAULT_SEGMENT_DURATION_MS = 100
8
10
 
9
11
  public class ExpoAudioStreamModule: Module, AudioStreamManagerDelegate {
10
12
  private var streamManager = AudioStreamManager()
@@ -60,7 +62,7 @@ public class ExpoAudioStreamModule: Module, AudioStreamManagerDelegate {
60
62
 
61
63
  let features = options["features"] as? [String: Bool] ?? [:]
62
64
  let featureOptions = self.extractFeatureOptions(from: features)
63
- let segmentDurationMs = options["segmentDurationMs"] as? Int ?? 100 // Default value of 100ms
65
+ let segmentDurationMs = options["segmentDurationMs"] as? Int ?? DEFAULT_SEGMENT_DURATION_MS // Default value of 100ms
64
66
 
65
67
  DispatchQueue.global().async(execute: {
66
68
  do {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@siteed/expo-audio-studio",
3
- "version": "2.2.0",
3
+ "version": "2.3.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
  "main": "build/index.js",
@@ -116,11 +116,6 @@
116
116
  "react": "*",
117
117
  "react-native": "*"
118
118
  },
119
- "peerDependenciesMeta": {
120
- "crc-32": {
121
- "optional": true
122
- }
123
- },
124
119
  "publishConfig": {
125
120
  "access": "public",
126
121
  "registry": "https://registry.npmjs.org"
@@ -113,10 +113,9 @@ const withRecordingPermission = (config, props) => {
113
113
  ];
114
114
  const optionalPermissions = [
115
115
  enableNotifications && 'android.permission.POST_NOTIFICATIONS',
116
- enablePhoneStateHandling && 'android.permission.READ_PHONE_STATE',
116
+ enablePhoneStateHandling && 'android.permission.READ_PHONE_STATE', // Only add if enabled
117
117
  enableBackgroundAudio && 'android.permission.FOREGROUND_SERVICE',
118
- enableBackgroundAudio &&
119
- 'android.permission.FOREGROUND_SERVICE_MICROPHONE',
118
+ enableBackgroundAudio && 'android.permission.FOREGROUND_SERVICE_MICROPHONE',
120
119
  ].filter(Boolean);
121
120
  const permissionsToAdd = [...basePermissions, ...optionalPermissions];
122
121
  debugLog('📋 Existing Android permissions:', config.modResults.manifest['uses-permission']?.map((p) => p.$?.['android:name']) || []);
@@ -169,10 +169,9 @@ const withRecordingPermission: ConfigPlugin<AudioStreamPluginOptions> = (
169
169
 
170
170
  const optionalPermissions = [
171
171
  enableNotifications && 'android.permission.POST_NOTIFICATIONS',
172
- enablePhoneStateHandling && 'android.permission.READ_PHONE_STATE',
172
+ enablePhoneStateHandling && 'android.permission.READ_PHONE_STATE', // Only add if enabled
173
173
  enableBackgroundAudio && 'android.permission.FOREGROUND_SERVICE',
174
- enableBackgroundAudio &&
175
- 'android.permission.FOREGROUND_SERVICE_MICROPHONE',
174
+ enableBackgroundAudio && 'android.permission.FOREGROUND_SERVICE_MICROPHONE',
176
175
  ].filter(Boolean) as string[]
177
176
 
178
177
  const permissionsToAdd = [...basePermissions, ...optionalPermissions]
@@ -1,21 +1,59 @@
1
+ // Bundler (Metro/Webpack) will automatically resolve to crc32.web.ts or crc32.native.ts.
2
+
1
3
  import { Platform } from 'react-native'
2
4
 
3
- interface CRC32 {
4
- (data: string | Uint8Array): number;
5
- buf(data: Uint8Array): number;
5
+ // Define the interface first
6
+ export interface CRC32 {
7
+ (data: string | Uint8Array): number
8
+ buf(data: Uint8Array): number
9
+ }
10
+
11
+ // --- Web CRC32 Calculation Logic ---
12
+ let webCrcTable: Uint32Array | undefined
13
+ function computeWebCrc32(data: string | Uint8Array): number {
14
+ // Lazily compute the table only on web when first needed
15
+ if (!webCrcTable) {
16
+ webCrcTable = (() => {
17
+ const table = new Uint32Array(256)
18
+ for (let i = 0; i < 256; ++i) {
19
+ let crc = i
20
+ for (let j = 0; j < 8; ++j) {
21
+ crc = crc & 1 ? (crc >>> 1) ^ 0xedb88320 : crc >>> 1
22
+ }
23
+ table[i] = crc
24
+ }
25
+ return table
26
+ })()
27
+ }
28
+
29
+ let crc = -1 // Initialize with 0xFFFFFFFF
30
+ if (typeof data === 'string') {
31
+ const strBytes = new TextEncoder().encode(data)
32
+ for (let i = 0; i < strBytes.length; ++i) {
33
+ crc = (crc >>> 8) ^ webCrcTable[(crc ^ strBytes[i]) & 0xff]
34
+ }
35
+ } else if (data instanceof Uint8Array) {
36
+ for (let i = 0; i < data.length; ++i) {
37
+ crc = (crc >>> 8) ^ webCrcTable[(crc ^ data[i]) & 0xff]
38
+ }
39
+ } else {
40
+ throw new Error('Unsupported data type for CRC32 calculation.')
41
+ }
42
+
43
+ return (crc ^ -1) >>> 0 // Final XOR and ensure unsigned 32-bit
6
44
  }
45
+ // --- End Web CRC32 Calculation Logic ---
7
46
 
8
47
  let crc32Implementation: CRC32
9
48
 
10
49
  if (Platform.OS === 'web') {
11
- // eslint-disable-next-line @typescript-eslint/no-var-requires
12
- crc32Implementation = require('crc-32')
50
+ // Assign the web implementation
51
+ crc32Implementation = Object.assign(computeWebCrc32, {
52
+ buf: (data: Uint8Array): number => computeWebCrc32(data),
53
+ })
13
54
  } else {
14
55
  // No-op implementation for native platforms
15
- crc32Implementation = Object.assign(
16
- () => 0,
17
- { buf: () => 0 }
18
- )
56
+ crc32Implementation = Object.assign(() => 0, { buf: () => 0 })
19
57
  }
20
58
 
21
59
  export default crc32Implementation
@@ -1,2 +0,0 @@
1
- export default function crc32(): number;
2
- //# sourceMappingURL=crc32.native.d.ts.map
@@ -1 +0,0 @@
1
- {"version":3,"file":"crc32.native.d.ts","sourceRoot":"","sources":["../../src/utils/crc32.native.ts"],"names":[],"mappings":"AACA,MAAM,CAAC,OAAO,UAAU,KAAK,IAAI,MAAM,CAEtC"}
@@ -1,5 +0,0 @@
1
- // No-op implementation for native platforms
2
- export default function crc32() {
3
- return 0;
4
- }
5
- //# sourceMappingURL=crc32.native.js.map
@@ -1 +0,0 @@
1
- {"version":3,"file":"crc32.native.js","sourceRoot":"","sources":["../../src/utils/crc32.native.ts"],"names":[],"mappings":"AAAA,4CAA4C;AAC5C,MAAM,CAAC,OAAO,UAAU,KAAK;IACzB,OAAO,CAAC,CAAC;AACb,CAAC","sourcesContent":["// No-op implementation for native platforms\nexport default function crc32(): number {\n return 0;\n}"]}
@@ -1,4 +0,0 @@
1
- /// <reference path="../../src/types/crc-32.d.ts" />
2
- import crc32 from 'crc-32';
3
- export default crc32;
4
- //# sourceMappingURL=crc32.web.d.ts.map
@@ -1 +0,0 @@
1
- {"version":3,"file":"crc32.web.d.ts","sourceRoot":"","sources":["../../src/utils/crc32.web.ts"],"names":[],"mappings":";AAAA,OAAO,KAAK,MAAM,QAAQ,CAAA;AAE1B,eAAe,KAAK,CAAA"}
@@ -1,3 +0,0 @@
1
- import crc32 from 'crc-32';
2
- export default crc32;
3
- //# sourceMappingURL=crc32.web.js.map
@@ -1 +0,0 @@
1
- {"version":3,"file":"crc32.web.js","sourceRoot":"","sources":["../../src/utils/crc32.web.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,MAAM,QAAQ,CAAA;AAE1B,eAAe,KAAK,CAAA","sourcesContent":["import crc32 from 'crc-32'\n\nexport default crc32\n"]}
@@ -1,4 +0,0 @@
1
- // No-op implementation for native platforms
2
- export default function crc32(): number {
3
- return 0;
4
- }
@@ -1,3 +0,0 @@
1
- import crc32 from 'crc-32'
2
-
3
- export default crc32