@siteed/expo-audio-stream 0.2.4 → 0.3.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/README.md CHANGED
@@ -122,5 +122,8 @@ or set the DEBUG environment variable to `expo-audio-stream:*`
122
122
 
123
123
  ### TODO
124
124
  this package is still in development, and there are a few things that need to be done:
125
- - add multiple format for native audio stream (wav, mp3, opus)
125
+ - Add resume (vs currently use start) support and implement pause on iOS.
126
+ - Multi format support: Extend support to other audio formats beyond WAV, such as MP3 or AAC
127
+ - Integrate an audio processing library for optional audio analysis, such as equalization, noise reduction, or volume normalization.
128
+ - Implement a more robust error handling system to provide detailed error messages and recovery options.
126
129
 
@@ -69,18 +69,6 @@ class AudioStreamManager: NSObject {
69
69
 
70
70
  override init() {
71
71
  super.init()
72
- configureAudioSession()
73
- }
74
-
75
- private func configureAudioSession() {
76
- let session = AVAudioSession.sharedInstance()
77
- do {
78
- try session.setCategory(.playAndRecord, mode: .default)
79
- try session.setActive(true)
80
- print("Audio session configured successfully.")
81
- } catch {
82
- print("Failed to set up audio session: \(error.localizedDescription)")
83
- }
84
72
  }
85
73
 
86
74
  @objc func handleAudioSessionInterruption(notification: Notification) {
@@ -90,6 +78,7 @@ class AudioStreamManager: NSObject {
90
78
  return
91
79
  }
92
80
 
81
+ Logger.debug("audio session interruption \(type)")
93
82
  if type == .began {
94
83
  // Pause your audio recording
95
84
  } else if type == .ended {
@@ -97,6 +86,7 @@ class AudioStreamManager: NSObject {
97
86
  let options = AVAudioSession.InterruptionOptions(rawValue: optionsValue)
98
87
  if options.contains(.shouldResume) {
99
88
  // Resume your audio recording
89
+ Logger.debug("Resume audio recording \(recordingUUID!)")
100
90
  try? AVAudioSession.sharedInstance().setActive(true)
101
91
  }
102
92
  }
@@ -115,9 +105,8 @@ class AudioStreamManager: NSObject {
115
105
  let wavHeader = createWavHeader(dataSize: 0) // Initially set data size to 0
116
106
  fileHandle.write(wavHeader)
117
107
  fileHandle.closeFile()
118
- print("Recording file with header created at:", fileURL.path)
119
108
  } catch {
120
- print("Failed to write WAV header: \(error.localizedDescription)")
109
+ Logger.debug("Failed to write WAV header: \(error.localizedDescription)")
121
110
  return nil
122
111
  }
123
112
  }
@@ -131,8 +120,8 @@ class AudioStreamManager: NSObject {
131
120
  let channels = UInt32(recordingSettings!.numberOfChannels)
132
121
  let bitDepth = UInt32(recordingSettings!.bitDepth)
133
122
 
134
- // Calculate byteRate
135
123
  let byteRate = sampleRate * channels * (bitDepth / 8)
124
+ let blockAlign = channels * (bitDepth / 8)
136
125
 
137
126
  // "RIFF" chunk descriptor
138
127
  header.append(contentsOf: "RIFF".utf8)
@@ -172,62 +161,104 @@ class AudioStreamManager: NSObject {
172
161
 
173
162
  func startRecording(settings: RecordingSettings, intervalMilliseconds: Int) -> String? {
174
163
  guard !isRecording else {
175
- print("Debug: Recording is already in progress.")
164
+ Logger.debug("Debug: Recording is already in progress.")
176
165
  return nil
177
166
  }
178
-
167
+
168
+ guard !audioEngine.isRunning else {
169
+ Logger.debug("Debug: Audio engine already running.")
170
+ return nil
171
+ }
172
+
173
+ recordingSettings = settings
174
+
175
+ // Determine the commonFormat based on bitDepth
176
+ let commonFormat: AVAudioCommonFormat
177
+ switch settings.bitDepth {
178
+ case 16:
179
+ commonFormat = .pcmFormatInt16
180
+ case 32:
181
+ commonFormat = .pcmFormatInt32
182
+ default:
183
+ Logger.debug("Unsupported bit depth. Defaulting to 16-bit PCM")
184
+ commonFormat = .pcmFormatInt16
185
+ recordingSettings?.bitDepth = 16
186
+ }
187
+
179
188
  emissionInterval = max(100.0, Double(intervalMilliseconds)) / 1000.0
180
189
  lastEmissionTime = Date()
181
- recordingSettings = settings
182
190
 
183
191
  let session = AVAudioSession.sharedInstance()
184
192
  do {
185
- print("Debug: Configuring audio session with sample rate: \(settings.sampleRate) Hz")
193
+ Logger.debug("Debug: Configuring audio session with sample rate: \(settings.sampleRate) Hz")
186
194
  try session.setPreferredSampleRate(settings.sampleRate)
187
195
  try session.setPreferredIOBufferDuration(1024 / settings.sampleRate)
188
196
  try session.setCategory(.playAndRecord)
189
197
  try session.setActive(true)
190
- print("Debug: Audio session activated successfully.")
198
+ Logger.debug("Debug: Audio session activated successfully.")
191
199
  } catch {
192
- print("Error: Failed to set up audio session with preferred settings: \(error.localizedDescription)")
200
+ Logger.debug("Error: Failed to set up audio session with preferred settings: \(error.localizedDescription)")
193
201
  return nil
194
202
  }
195
-
203
+
196
204
  NotificationCenter.default.addObserver(self, selector: #selector(handleAudioSessionInterruption), name: AVAudioSession.interruptionNotification, object: nil)
197
-
198
- guard let channelLayout = AVAudioChannelLayout(layoutTag: settings.numberOfChannels == 1 ? kAudioChannelLayoutTag_Mono : kAudioChannelLayoutTag_Stereo) else {
199
- print("Error: Failed to create channel layout.")
205
+
206
+ // Correct the format to use 16-bit integer (PCM)
207
+ guard let audioFormat = AVAudioFormat(commonFormat: commonFormat, sampleRate: settings.sampleRate, channels: UInt32(settings.numberOfChannels), interleaved: true) else {
208
+ Logger.debug("Error: Failed to create audio format with the specified bit depth.")
200
209
  return nil
201
210
  }
202
- let errorFormat = AVAudioFormat(standardFormatWithSampleRate: settings.sampleRate, channelLayout: channelLayout)
203
-
204
- audioEngine.inputNode.installTap(onBus: 0, bufferSize: 1024, format: errorFormat) { [weak self] (buffer, time) in
211
+
212
+ audioEngine.inputNode.installTap(onBus: 0, bufferSize: 1024, format: audioFormat) { [weak self] (buffer, time) in
205
213
  guard let self = self, let fileURL = self.recordingFileURL else {
206
- print("Error: File URL or self is nil during buffer processing.")
214
+ Logger.debug("Error: File URL or self is nil during buffer processing.")
207
215
  return
208
216
  }
217
+ let formatDescription = describeAudioFormat(buffer.format)
218
+ Logger.debug("Debug: Buffer format - \(formatDescription)")
219
+
209
220
  self.processAudioBuffer(buffer, fileURL: fileURL)
210
221
  }
211
-
222
+
212
223
  recordingFileURL = createRecordingFile()
213
224
  if recordingFileURL == nil {
214
- print("Error: Failed to create recording file.")
225
+ Logger.debug("Error: Failed to create recording file.")
215
226
  return nil
216
227
  }
217
-
228
+
218
229
  do {
219
230
  startTime = Date()
220
231
  try audioEngine.start()
221
232
  isRecording = true
222
- print("Debug: Recording started successfully.")
223
- return recordingFileURL?.absoluteString
233
+ Logger.debug("Debug: Recording started successfully.")
234
+ return recordingFileURL?.path
224
235
  } catch {
225
- print("Error: Could not start the audio engine: \(error.localizedDescription)")
236
+ Logger.debug("Error: Could not start the audio engine: \(error.localizedDescription)")
226
237
  isRecording = false
227
238
  return nil
228
239
  }
229
240
  }
230
-
241
+
242
+ func describeAudioFormat(_ format: AVAudioFormat) -> String {
243
+ let sampleRate = format.sampleRate
244
+ let channelCount = format.channelCount
245
+ let bitDepth: String
246
+
247
+ switch format.commonFormat {
248
+ case .pcmFormatInt16:
249
+ bitDepth = "16-bit Int"
250
+ case .pcmFormatInt32:
251
+ bitDepth = "32-bit Int"
252
+ case .pcmFormatFloat32:
253
+ bitDepth = "32-bit Float"
254
+ case .pcmFormatFloat64:
255
+ bitDepth = "64-bit Float"
256
+ default:
257
+ bitDepth = "Unknown Format"
258
+ }
259
+
260
+ return "Sample Rate: \(sampleRate), Channels: \(channelCount), Format: \(bitDepth)"
261
+ }
231
262
 
232
263
  func stopRecording() -> RecordingResult? {
233
264
  audioEngine.stop()
@@ -297,19 +328,19 @@ class AudioStreamManager: NSObject {
297
328
  }
298
329
  let data = Data(bytes: bufferData, count: Int(audioData.mDataByteSize))
299
330
 
300
- print("Writing data size: \(data.count) bytes") // Debug: Check the size of data being written
331
+ // print("Writing data size: \(data.count) bytes") // Debug: Check the size of data being written
301
332
  fileHandle.seekToEndOfFile()
302
333
  fileHandle.write(data)
303
334
  fileHandle.closeFile()
304
335
 
305
336
  totalDataSize += Int64(data.count)
306
- print("Total data size written: \(totalDataSize) bytes") // Debug: Check total data written
307
-
337
+ // print("Total data size written: \(totalDataSize) bytes") // Debug: Check total data written
338
+
308
339
  let currentTime = Date()
309
340
  if let lastEmissionTime = lastEmissionTime, currentTime.timeIntervalSince(lastEmissionTime) >= emissionInterval {
310
341
  if let startTime = startTime {
311
342
  let recordingTime = currentTime.timeIntervalSince(startTime)
312
- print("Emitting data: Recording time \(recordingTime) seconds, Data size \(totalDataSize) bytes")
343
+ // print("Emitting data: Recording time \(recordingTime) seconds, Data size \(totalDataSize) bytes")
313
344
  self.delegate?.audioStreamManager(self, didReceiveAudioData: data, recordingTime: recordingTime, totalDataSize: totalDataSize)
314
345
  self.lastEmissionTime = currentTime // Update last emission time
315
346
  self.lastEmittedSize = totalDataSize
@@ -47,7 +47,8 @@ public class ExpoAudioStreamModule: Module, AudioStreamManagerDelegate {
47
47
  let resultDict: [String: Any] = [
48
48
  "fileUri": recordingResult.fileUri,
49
49
  "duration": recordingResult.duration,
50
- "size": recordingResult.size
50
+ "size": recordingResult.size,
51
+ "mimeType": recordingResult.mimeType,
51
52
  ]
52
53
  promise.resolve(resultDict)
53
54
  } else {
@@ -109,18 +110,20 @@ public class ExpoAudioStreamModule: Module, AudioStreamManagerDelegate {
109
110
  }
110
111
 
111
112
  private func clearAudioFiles() {
112
- let filenames = listAudioFiles()
113
- let documentDirectory = FileManager.default.urls(for: .documentDirectory, in: .userDomainMask).first!
114
-
115
- filenames.forEach { filename in
116
- let fileURL = documentDirectory.appendingPathComponent(filename)
117
- do {
118
- try FileManager.default.removeItem(at: fileURL)
119
- print("Removed file at:", fileURL.path)
120
- } catch {
121
- print("Error removing file at \(fileURL.path):", error.localizedDescription)
113
+ let fileURLs = listAudioFiles() // This now returns full URLs as strings
114
+ fileURLs.forEach { fileURLString in
115
+ if let fileURL = URL(string: fileURLString) {
116
+ do {
117
+ try FileManager.default.removeItem(at: fileURL)
118
+ print("Removed file at:", fileURL.path)
119
+ } catch {
120
+ print("Error removing file at \(fileURL.path):", error.localizedDescription)
121
+ }
122
+ } else {
123
+ print("Invalid URL string: \(fileURLString)")
122
124
  }
123
125
  }
126
+
124
127
  }
125
128
 
126
129
  func listAudioFiles() -> [String] {
@@ -131,7 +134,7 @@ public class ExpoAudioStreamModule: Module, AudioStreamManagerDelegate {
131
134
 
132
135
  do {
133
136
  let files = try FileManager.default.contentsOfDirectory(at: documentDirectory, includingPropertiesForKeys: nil)
134
- let audioFiles = files.filter { $0.pathExtension == "wav" }.map { $0.lastPathComponent }
137
+ let audioFiles = files.filter { $0.pathExtension == "wav" }.map { $0.absoluteString }
135
138
  return audioFiles
136
139
  } catch {
137
140
  print("Error listing audio files:", error.localizedDescription)
@@ -0,0 +1,7 @@
1
+ class Logger {
2
+ static func debug(_ message: @autoclosure () -> String) {
3
+ #if DEBUG
4
+ print(message())
5
+ #endif
6
+ }
7
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@siteed/expo-audio-stream",
3
- "version": "0.2.4",
3
+ "version": "0.3.0",
4
4
  "description": "stream audio crossplatform",
5
5
  "license": "MIT",
6
6
  "main": "build/index.js",
@@ -8,11 +8,19 @@ const withRecordingPermission = (config, existingPerms) => {
8
8
  }
9
9
  config = (0, config_plugins_1.withInfoPlist)(config, (config) => {
10
10
  config.modResults["NSMicrophoneUsageDescription"] = MICROPHONE_USAGE;
11
+ // Add audio to UIBackgroundModes to allow background audio recording
12
+ const existingBackgroundModes = config.modResults.UIBackgroundModes || [];
13
+ if (!existingBackgroundModes.includes("audio")) {
14
+ existingBackgroundModes.push("audio");
15
+ }
16
+ config.modResults.UIBackgroundModes = existingBackgroundModes;
11
17
  return config;
12
18
  });
13
19
  config = (0, config_plugins_1.withAndroidManifest)(config, (config) => {
14
20
  const mainApplication = config_plugins_1.AndroidConfig.Manifest.getMainApplicationOrThrow(config.modResults);
15
21
  config_plugins_1.AndroidConfig.Manifest.addMetaDataItemToMainApplication(mainApplication, "android.permission.RECORD_AUDIO", MICROPHONE_USAGE);
22
+ // Add FOREGROUND_SERVICE permission for handling background recording
23
+ config_plugins_1.AndroidConfig.Manifest.addMetaDataItemToMainApplication(mainApplication, "android.permission.FOREGROUND_SERVICE", "This apps needs access to the foreground service to record audio in the background");
16
24
  return config;
17
25
  });
18
26
  return config;
@@ -15,6 +15,14 @@ const withRecordingPermission: ConfigPlugin<{
15
15
  }
16
16
  config = withInfoPlist(config, (config) => {
17
17
  config.modResults["NSMicrophoneUsageDescription"] = MICROPHONE_USAGE;
18
+
19
+ // Add audio to UIBackgroundModes to allow background audio recording
20
+ const existingBackgroundModes = config.modResults.UIBackgroundModes || [];
21
+ if (!existingBackgroundModes.includes("audio")) {
22
+ existingBackgroundModes.push("audio");
23
+ }
24
+ config.modResults.UIBackgroundModes = existingBackgroundModes;
25
+
18
26
  return config;
19
27
  });
20
28
 
@@ -28,6 +36,14 @@ const withRecordingPermission: ConfigPlugin<{
28
36
  "android.permission.RECORD_AUDIO",
29
37
  MICROPHONE_USAGE,
30
38
  );
39
+
40
+ // Add FOREGROUND_SERVICE permission for handling background recording
41
+ AndroidConfig.Manifest.addMetaDataItemToMainApplication(
42
+ mainApplication,
43
+ "android.permission.FOREGROUND_SERVICE",
44
+ "This apps needs access to the foreground service to record audio in the background",
45
+ );
46
+
31
47
  return config;
32
48
  });
33
49