@siteed/expo-audio-studio 2.6.2 → 2.7.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,13 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
8
8
  ## [Unreleased]
9
9
 
10
10
 
11
+ ## [2.7.0] - 2025-05-04
12
+ ### Changed
13
+ - fix: Enhance iOS Background Audio Recording and Audio Format Conversion (#228) ([c17169b](https://github.com/deeeed/expo-audio-stream/commit/c17169bf9275706abf287712acc30df2f1814ed7))
14
+ - chore(expo-audio-studio): improve build script for cjs esm conversion ([767dfbe](https://github.com/deeeed/expo-audio-stream/commit/767dfbe5da0f1550b689f6859e2e5fccf7f8141c))
15
+ ## [2.6.3] - 2025-05-03
16
+ ### Changed
17
+ - chore: update readme with store download information (#224) ([c404d86](https://github.com/deeeed/expo-audio-stream/commit/c404d860cdb1c4c4bbc3767214f56bf547acec33))
11
18
  ## [2.6.2] - 2025-05-01
12
19
  ### Changed
13
20
  - fix(audio-studio): ensure foreground-only audio recording works with FOREGROUND_SERVICE #202 (#221) ([abc450c](https://github.com/deeeed/expo-audio-stream/commit/abc450cb73968cc260e430758df9b72e00f75ef7))
@@ -210,7 +217,9 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
210
217
  - Feature: Audio features extraction during recording.
211
218
  - Feature: Consistent WAV PCM recording format across all platforms.
212
219
 
213
- [unreleased]: https://github.com/deeeed/expo-audio-stream/compare/@siteed/expo-audio-studio@2.6.2...HEAD
220
+ [unreleased]: https://github.com/deeeed/expo-audio-stream/compare/@siteed/expo-audio-studio@2.7.0...HEAD
221
+ [2.7.0]: https://github.com/deeeed/expo-audio-stream/compare/@siteed/expo-audio-studio@2.6.3...@siteed/expo-audio-studio@2.7.0
222
+ [2.6.3]: https://github.com/deeeed/expo-audio-stream/compare/@siteed/expo-audio-studio@2.6.2...@siteed/expo-audio-studio@2.6.3
214
223
  [2.6.2]: https://github.com/deeeed/expo-audio-stream/compare/@siteed/expo-audio-studio@2.6.1...@siteed/expo-audio-studio@2.6.2
215
224
  [2.6.1]: https://github.com/deeeed/expo-audio-stream/compare/@siteed/expo-audio-studio@2.6.0...@siteed/expo-audio-studio@2.6.1
216
225
  [2.6.0]: https://github.com/deeeed/expo-audio-stream/compare/@siteed/expo-audio-studio@2.5.0...@siteed/expo-audio-studio@2.6.0
package/README.md CHANGED
@@ -21,6 +21,18 @@
21
21
  </div>
22
22
  </div>
23
23
 
24
+ <div style="display: flex; flex-direction: column; align-items: center; margin-bottom: 20px;">
25
+ <p><strong>Try AudioPlayground: Complete audio processing app built with this library</strong></p>
26
+ <div style="display: flex; justify-content: center; gap: 20px; margin: 10px 0;">
27
+ <a href="https://apps.apple.com/app/audio-playground/id6739774966">
28
+ <img src="https://developer.apple.com/app-store/marketing/guidelines/images/badge-download-on-the-app-store.svg" alt="Download on the App Store" height="40" />
29
+ </a>
30
+ <a href="https://play.google.com/store/apps/details?id=net.siteed.audioplayground">
31
+ <img src="https://play.google.com/intl/en_us/badges/static/images/badges/en_badge_web_generic.png" alt="Get it on Google Play" height="40" />
32
+ </a>
33
+ </div>
34
+ </div>
35
+
24
36
  <a href="https://deeeed.github.io/expo-audio-stream/playground" style="text-decoration:none;">
25
37
  <div style="display:inline-block; padding:10px 20px; background-color:#007bff; color:white; border-radius:5px; font-size:16px;">
26
38
  Try it in the Playground
package/app.plugin.cjs ADDED
@@ -0,0 +1,2 @@
1
+ // Simply export the plugin - this is a CommonJS file (.cjs)
2
+ module.exports = require('./plugin/build/index.cjs')
@@ -801,7 +801,16 @@ public class AudioProcessor {
801
801
  try audioFile.read(into: buffer, frameCount: frameCount)
802
802
  let converter = AVAudioConverter(from: inputFormat, to: targetFormat)!
803
803
  let convertedBuffer = AVAudioPCMBuffer(pcmFormat: targetFormat, frameCapacity: frameCount)!
804
- try converter.convert(to: convertedBuffer, from: buffer)
804
+ var error: NSError?
805
+ _ = converter.convert(to: convertedBuffer, error: &error) { inNumPackets, outStatus in
806
+ outStatus.pointee = .haveData
807
+ return buffer
808
+ }
809
+ if let error = error {
810
+ Logger.debug("AudioProcessor", "Format conversion failed: \(error.localizedDescription)")
811
+ reject("CONVERSION_ERROR", "Failed to convert audio format: \(error.localizedDescription)")
812
+ return nil
813
+ }
805
814
  try outputFile.write(from: convertedBuffer)
806
815
  cumulativeFrames += Int64(frameCount)
807
816
  let progress = Float(cumulativeFrames) / Float(totalFrames) * 100
@@ -933,7 +942,7 @@ public class AudioProcessor {
933
942
  let convertedBuffer = AVAudioPCMBuffer(pcmFormat: targetFormat, frameCapacity: framesToRead)!
934
943
 
935
944
  var error: NSError?
936
- let conversionStatus = converter.convert(to: convertedBuffer, error: &error) { inNumPackets, outStatus in
945
+ _ = converter.convert(to: convertedBuffer, error: &error) { inNumPackets, outStatus in
937
946
  outStatus.pointee = .haveData
938
947
  return buffer
939
948
  }
@@ -363,9 +363,15 @@ class AudioStreamManager: NSObject, AudioDeviceManagerDelegate {
363
363
 
364
364
  @objc private func handleAppDidEnterBackground(_ notification: Notification) {
365
365
  if isRecording {
366
- // If keepAwake is false, we should track this as a pause
366
+ // If keepAwake is false, we should track this as a pause and actually pause the engine
367
367
  if let settings = recordingSettings, !settings.keepAwake {
368
+ Logger.debug("AudioStreamManager", "App entering background with keepAwake=false, pausing recording")
368
369
  currentPauseStart = Date()
370
+ // Explicitly pause the engine but don't change isPaused state
371
+ // so we can automatically resume when returning to foreground
372
+ audioEngine.pause()
373
+ } else {
374
+ Logger.debug("AudioStreamManager", "App entering background with keepAwake=true, continuing recording")
369
375
  }
370
376
 
371
377
  DispatchQueue.main.asyncAfter(deadline: .now() + 0.5) { [weak self] in
@@ -382,6 +388,23 @@ class AudioStreamManager: NSObject, AudioDeviceManagerDelegate {
382
388
  totalPausedDuration += pauseDuration
383
389
  currentPauseStart = nil
384
390
  Logger.debug("AudioStreamManager", "Added background pause duration: \(pauseDuration), total paused: \(totalPausedDuration)")
391
+
392
+ // Now restart the engine if it was paused due to background
393
+ do {
394
+ // Reinstall tap with hardware format to ensure we have good input
395
+ _ = installTapWithHardwareFormat()
396
+ // Restart the engine
397
+ try audioEngine.start()
398
+ Logger.debug("AudioStreamManager", "Successfully restarted audio engine after returning from background")
399
+ } catch {
400
+ Logger.debug("AudioStreamManager", "Failed to restart audio engine after returning from background: \(error)")
401
+ // If we can't restart, officially pause the recording
402
+ if !isPaused {
403
+ isPaused = true
404
+ // Notify delegate
405
+ delegate?.audioStreamManager(self, didPauseRecording: Date())
406
+ }
407
+ }
385
408
  }
386
409
 
387
410
  notificationManager?.stopUpdates()
@@ -768,9 +791,21 @@ class AudioStreamManager: NSObject, AudioDeviceManagerDelegate {
768
791
  // Append necessary options for background recording if keepAwake is enabled
769
792
  if settings.keepAwake {
770
793
  Logger.debug("AudioStreamManager", "keepAwake enabled - configuring for background recording")
771
- // Add background audio option
794
+ // Set the category to PlayAndRecord with proper background options
772
795
  options.insert(.mixWithOthers)
773
- try session.setActive(true, options: .notifyOthersOnDeactivation)
796
+ // Add duckOthers to reduce volume of other apps instead of stopping them
797
+ options.insert(.duckOthers)
798
+
799
+ // Configure audio session for background audio
800
+ do {
801
+ try session.setCategory(.playAndRecord, mode: .default, options: options)
802
+ try session.setActive(true, options: .notifyOthersOnDeactivation)
803
+ // Ensure the app has appropriate Info.plist settings for background audio
804
+ Logger.debug("AudioStreamManager", "Audio session configured for background recording with options: \(options)")
805
+ } catch {
806
+ Logger.debug("AudioStreamManager", "Failed to configure audio session for background: \(error)")
807
+ try session.setActive(true, options: .notifyOthersOnDeactivation)
808
+ }
774
809
  } else {
775
810
  Logger.debug("AudioStreamManager", "keepAwake disabled - using standard session configuration")
776
811
  // If keepAwake is false, don't add background audio options
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@siteed/expo-audio-studio",
3
- "version": "2.6.2",
3
+ "version": "2.7.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
  "main": "build/index.js",
@@ -44,7 +44,7 @@
44
44
  "ios",
45
45
  "cpp",
46
46
  "plugin",
47
- "app.plugin.js",
47
+ "app.plugin.cjs",
48
48
  "LICENSE",
49
49
  "CHANGELOG.md",
50
50
  "generated",
@@ -66,7 +66,7 @@
66
66
  ],
67
67
  "scripts": {
68
68
  "build": "tsc",
69
- "build:plugin": "tsc --build plugin/tsconfig.json",
69
+ "build:plugin": "tsc --build plugin/tsconfig.json && node scripts/convert-to-cjs.js",
70
70
  "build:plugin:dev": "expo-module build plugin",
71
71
  "build:dev": "expo-module build",
72
72
  "clean": "expo-module clean && rimraf plugin/build",
@@ -74,7 +74,7 @@
74
74
  "test": "expo-module test",
75
75
  "typecheck": "tsc --noEmit",
76
76
  "docgen": "typedoc src/index.ts --plugin typedoc-plugin-markdown --readme none --out ../../documentation_site/docs/api-reference/API",
77
- "prepare": "yarn build && yarn build:plugin",
77
+ "prepare": "yarn build && yarn build:plugin && node -e \"require('fs').renameSync('./plugin/build/index.d.ts', './plugin/build/index.d.cts')\"",
78
78
  "prepublishOnly": "expo-module prepublishOnly",
79
79
  "expo-module": "expo-module",
80
80
  "open:ios": "open -a \"Xcode\" ../../apps/playground/ios",
@@ -99,7 +99,7 @@
99
99
  "eslint-plugin-prettier": "^5.1.3",
100
100
  "eslint-plugin-promise": "^6.1.1",
101
101
  "eslint-plugin-react": "^7.34.1",
102
- "expo": "^52.0.27",
102
+ "expo": "^53.0.6",
103
103
  "expo-module-scripts": "^4.0.2",
104
104
  "jest": "^29.7.0",
105
105
  "prettier": "^3.2.5",
@@ -121,6 +121,6 @@
121
121
  "registry": "https://registry.npmjs.org"
122
122
  },
123
123
  "dependencies": {
124
- "expo-modules-core": "~2.1.4"
124
+ "expo-modules-core": "~2.3.12"
125
125
  }
126
126
  }
@@ -0,0 +1,192 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ const config_plugins_1 = require("@expo/config-plugins");
4
+ const MICROPHONE_USAGE = 'Allow $(PRODUCT_NAME) to access your microphone';
5
+ const NOTIFICATION_USAGE = 'Show recording notifications and controls';
6
+ const LOG_PREFIX = '[@siteed/expo-audio-studio]';
7
+ function debugLog(message, ...args) {
8
+ if (process.env.EXPO_DEBUG) {
9
+ console.log(`${LOG_PREFIX} ${message}`, ...args);
10
+ }
11
+ }
12
+ const withRecordingPermission = (config, props) => {
13
+ const options = {
14
+ enablePhoneStateHandling: true, // Default to true for backward compatibility
15
+ enableNotifications: true,
16
+ enableBackgroundAudio: true,
17
+ iosBackgroundModes: {
18
+ useVoIP: false,
19
+ useAudio: false,
20
+ useProcessing: false,
21
+ useLocation: false,
22
+ useExternalAccessory: false,
23
+ },
24
+ iosConfig: {
25
+ microphoneUsageDescription: MICROPHONE_USAGE,
26
+ notificationUsageDescription: NOTIFICATION_USAGE,
27
+ },
28
+ ...(props || {}),
29
+ };
30
+ const { enablePhoneStateHandling, enableNotifications, enableBackgroundAudio, } = options;
31
+ debugLog('📱 Configuring Recording Permissions Plugin...', options);
32
+ // iOS Configuration
33
+ config = (0, config_plugins_1.withInfoPlist)(config, (config) => {
34
+ // Always set the microphone usage description from options first
35
+ config.modResults['NSMicrophoneUsageDescription'] =
36
+ options.iosConfig?.microphoneUsageDescription ||
37
+ config.modResults['NSMicrophoneUsageDescription'] ||
38
+ MICROPHONE_USAGE;
39
+ if (enableNotifications) {
40
+ config.modResults['NSUserNotificationsUsageDescription'] =
41
+ options.iosConfig?.notificationUsageDescription ||
42
+ config.modResults['NSUserNotificationsUsageDescription'] ||
43
+ NOTIFICATION_USAGE;
44
+ config.modResults['NSUserNotificationAlertStyle'] = 'alert';
45
+ }
46
+ const existingBackgroundModes = config.modResults.UIBackgroundModes || [];
47
+ // If background audio is enabled with useAudio, add the audio background mode
48
+ if (options.iosBackgroundModes?.useAudio === true &&
49
+ enableBackgroundAudio === true &&
50
+ !existingBackgroundModes.includes('audio')) {
51
+ // Add 'audio' background mode - REQUIRED for background recording
52
+ existingBackgroundModes.push('audio');
53
+ debugLog('✅ Added audio background mode for iOS background recording');
54
+ // Also ensure processing mode is recommended
55
+ if (options.iosBackgroundModes?.useProcessing !== true) {
56
+ console.warn(`${LOG_PREFIX} Warning: Background audio recording works best with both 'audio' and 'processing' background modes. Consider enabling 'useProcessing' in iosBackgroundModes.`);
57
+ }
58
+ }
59
+ if (options.iosBackgroundModes?.useVoIP === true &&
60
+ enablePhoneStateHandling === true) {
61
+ if (!existingBackgroundModes.includes('voip')) {
62
+ existingBackgroundModes.push('voip');
63
+ }
64
+ const existingCapabilities = (config.modResults
65
+ .UIRequiredDeviceCapabilities || []);
66
+ if (!existingCapabilities.includes('telephony')) {
67
+ existingCapabilities.push('telephony');
68
+ }
69
+ config.modResults.UIRequiredDeviceCapabilities =
70
+ existingCapabilities;
71
+ }
72
+ // Add additional background modes only if explicitly set to true
73
+ if (options.iosBackgroundModes?.useProcessing === true) {
74
+ if (!existingBackgroundModes.includes('processing')) {
75
+ existingBackgroundModes.push('processing');
76
+ }
77
+ // Add processing info if enabled
78
+ // Note: We keep the 'audiostream' namespace for native modules to maintain compatibility
79
+ config.modResults.BGTaskSchedulerPermittedIdentifiers = [
80
+ 'com.siteed.audiostream.processing',
81
+ ];
82
+ }
83
+ if (options.iosBackgroundModes?.useLocation === true) {
84
+ if (!existingBackgroundModes.includes('location')) {
85
+ existingBackgroundModes.push('location');
86
+ }
87
+ }
88
+ if (options.iosBackgroundModes?.useExternalAccessory === true) {
89
+ if (!existingBackgroundModes.includes('external-accessory')) {
90
+ existingBackgroundModes.push('external-accessory');
91
+ }
92
+ }
93
+ // Configure background processing info if enabled
94
+ if (options.iosConfig?.backgroundProcessingTitle) {
95
+ config.modResults.BGProcessingTaskTitle =
96
+ options.iosConfig.backgroundProcessingTitle;
97
+ }
98
+ // Configure audio session behavior
99
+ if (options.iosConfig?.allowBackgroundAudioControls) {
100
+ config.modResults.UIBackgroundModes = [
101
+ ...existingBackgroundModes,
102
+ 'remote-notification',
103
+ ];
104
+ config.modResults.MPNowPlayingInfoPropertyPlaybackRate = true;
105
+ }
106
+ config.modResults.UIBackgroundModes = existingBackgroundModes;
107
+ return config;
108
+ });
109
+ // Android Configuration
110
+ config = (0, config_plugins_1.withAndroidManifest)(config, (config) => {
111
+ const basePermissions = [
112
+ 'android.permission.RECORD_AUDIO',
113
+ 'android.permission.WAKE_LOCK',
114
+ ];
115
+ const optionalPermissions = [
116
+ enableNotifications && 'android.permission.POST_NOTIFICATIONS',
117
+ enablePhoneStateHandling && 'android.permission.READ_PHONE_STATE', // Only add if enabled
118
+ enableBackgroundAudio && 'android.permission.FOREGROUND_SERVICE',
119
+ enableBackgroundAudio && 'android.permission.FOREGROUND_SERVICE_MICROPHONE',
120
+ ].filter(Boolean);
121
+ const permissionsToAdd = [...basePermissions, ...optionalPermissions];
122
+ debugLog('📋 Existing Android permissions:', config.modResults.manifest['uses-permission']?.map((p) => p.$?.['android:name']) || []);
123
+ debugLog('➕ Adding Android permissions:', permissionsToAdd);
124
+ const { addPermission } = config_plugins_1.AndroidConfig.Permissions;
125
+ // Add each permission only if it doesn't exist
126
+ permissionsToAdd.forEach((permission) => {
127
+ const existingPermission = config.modResults.manifest['uses-permission']?.find((p) => p.$?.['android:name'] === permission);
128
+ if (!existingPermission) {
129
+ addPermission(config.modResults, permission);
130
+ }
131
+ });
132
+ // Get the main application node
133
+ const mainApplication = config.modResults.manifest.application?.[0];
134
+ if (mainApplication) {
135
+ debugLog('📱 Configuring Android application components...');
136
+ // Add RecordingActionReceiver
137
+ if (!mainApplication.receiver) {
138
+ mainApplication.receiver = [];
139
+ }
140
+ const receiverConfig = {
141
+ $: {
142
+ 'android:name': '.RecordingActionReceiver',
143
+ 'android:exported': 'false',
144
+ },
145
+ 'intent-filter': [
146
+ {
147
+ action: [
148
+ { $: { 'android:name': 'PAUSE_RECORDING' } },
149
+ { $: { 'android:name': 'RESUME_RECORDING' } },
150
+ { $: { 'android:name': 'STOP_RECORDING' } },
151
+ ],
152
+ },
153
+ ],
154
+ };
155
+ const receiverIndex = mainApplication.receiver.findIndex((receiver) => receiver.$?.['android:name'] === '.RecordingActionReceiver');
156
+ if (receiverIndex >= 0) {
157
+ mainApplication.receiver[receiverIndex] = receiverConfig;
158
+ }
159
+ else {
160
+ mainApplication.receiver.push(receiverConfig);
161
+ }
162
+ debugLog('✅ RecordingActionReceiver configured');
163
+ // Add AudioRecordingService
164
+ if (!mainApplication.service) {
165
+ mainApplication.service = [];
166
+ }
167
+ const serviceConfig = {
168
+ $: {
169
+ 'android:name': '.AudioRecordingService',
170
+ 'android:enabled': 'true',
171
+ 'android:exported': 'false',
172
+ 'android:foregroundServiceType': 'microphone',
173
+ },
174
+ };
175
+ const serviceIndex = mainApplication.service.findIndex((service) => service.$?.['android:name'] === '.AudioRecordingService');
176
+ if (serviceIndex >= 0) {
177
+ mainApplication.service[serviceIndex] = serviceConfig;
178
+ }
179
+ else {
180
+ mainApplication.service.push(serviceConfig);
181
+ }
182
+ debugLog('✅ AudioRecordingService configured');
183
+ }
184
+ else {
185
+ console.error(`${LOG_PREFIX} ❌ Main application node not found in Android Manifest`);
186
+ }
187
+ return config;
188
+ });
189
+ debugLog('✨ Recording Permissions Plugin configuration completed');
190
+ return config;
191
+ };
192
+ exports.default = withRecordingPermission;
@@ -44,15 +44,16 @@ const withRecordingPermission = (config, props) => {
44
44
  config.modResults['NSUserNotificationAlertStyle'] = 'alert';
45
45
  }
46
46
  const existingBackgroundModes = config.modResults.UIBackgroundModes || [];
47
- // Only add background modes if explicitly enabled and set to true
47
+ // If background audio is enabled with useAudio, add the audio background mode
48
48
  if (options.iosBackgroundModes?.useAudio === true &&
49
49
  enableBackgroundAudio === true &&
50
50
  !existingBackgroundModes.includes('audio')) {
51
- // Don't automatically add 'audio' background mode as it's only for playback
52
- // existingBackgroundModes.push('audio')
53
- // Instead, ensure processing mode is used for background recording
51
+ // Add 'audio' background mode - REQUIRED for background recording
52
+ existingBackgroundModes.push('audio');
53
+ debugLog('✅ Added audio background mode for iOS background recording');
54
+ // Also ensure processing mode is recommended
54
55
  if (options.iosBackgroundModes?.useProcessing !== true) {
55
- console.warn(`${LOG_PREFIX} Warning: Background audio recording requires 'processing' background mode. Please enable 'useProcessing' in iosBackgroundModes.`);
56
+ console.warn(`${LOG_PREFIX} Warning: Background audio recording works best with both 'audio' and 'processing' background modes. Consider enabling 'useProcessing' in iosBackgroundModes.`);
56
57
  }
57
58
  }
58
59
  if (options.iosBackgroundModes?.useVoIP === true &&
@@ -84,19 +84,22 @@ const withRecordingPermission: ConfigPlugin<AudioStreamPluginOptions> = (
84
84
  const existingBackgroundModes =
85
85
  config.modResults.UIBackgroundModes || []
86
86
 
87
- // Only add background modes if explicitly enabled and set to true
87
+ // If background audio is enabled with useAudio, add the audio background mode
88
88
  if (
89
89
  options.iosBackgroundModes?.useAudio === true &&
90
90
  enableBackgroundAudio === true &&
91
91
  !existingBackgroundModes.includes('audio')
92
92
  ) {
93
- // Don't automatically add 'audio' background mode as it's only for playback
94
- // existingBackgroundModes.push('audio')
93
+ // Add 'audio' background mode - REQUIRED for background recording
94
+ existingBackgroundModes.push('audio')
95
+ debugLog(
96
+ '✅ Added audio background mode for iOS background recording'
97
+ )
95
98
 
96
- // Instead, ensure processing mode is used for background recording
99
+ // Also ensure processing mode is recommended
97
100
  if (options.iosBackgroundModes?.useProcessing !== true) {
98
101
  console.warn(
99
- `${LOG_PREFIX} Warning: Background audio recording requires 'processing' background mode. Please enable 'useProcessing' in iosBackgroundModes.`
102
+ `${LOG_PREFIX} Warning: Background audio recording works best with both 'audio' and 'processing' background modes. Consider enabling 'useProcessing' in iosBackgroundModes.`
100
103
  )
101
104
  }
102
105
  }
package/app.plugin.js DELETED
@@ -1 +0,0 @@
1
- module.exports = require('./plugin/build')