@siteed/expo-audio-stream 2.0.1 → 2.2.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.
Files changed (166) hide show
  1. package/README.md +46 -27
  2. package/build/index.d.ts +11 -12
  3. package/build/index.js +44 -10
  4. package/package.json +49 -110
  5. package/src/index.ts +18 -33
  6. package/CHANGELOG.md +0 -195
  7. package/android/build.gradle +0 -105
  8. package/android/src/main/AndroidManifest.xml +0 -27
  9. package/android/src/main/java/net/siteed/audiostream/AudioAnalysisData.kt +0 -166
  10. package/android/src/main/java/net/siteed/audiostream/AudioDataEncoder.kt +0 -9
  11. package/android/src/main/java/net/siteed/audiostream/AudioFileHandler.kt +0 -131
  12. package/android/src/main/java/net/siteed/audiostream/AudioFormatUtils.kt +0 -103
  13. package/android/src/main/java/net/siteed/audiostream/AudioNotificationsManager.kt +0 -435
  14. package/android/src/main/java/net/siteed/audiostream/AudioProcessor.kt +0 -1936
  15. package/android/src/main/java/net/siteed/audiostream/AudioRecorderManager.kt +0 -1437
  16. package/android/src/main/java/net/siteed/audiostream/AudioRecordingService.kt +0 -138
  17. package/android/src/main/java/net/siteed/audiostream/Constants.kt +0 -20
  18. package/android/src/main/java/net/siteed/audiostream/EventSender.kt +0 -7
  19. package/android/src/main/java/net/siteed/audiostream/ExpoAudioStreamModule.kt +0 -509
  20. package/android/src/main/java/net/siteed/audiostream/FFT.kt +0 -99
  21. package/android/src/main/java/net/siteed/audiostream/Features.kt +0 -98
  22. package/android/src/main/java/net/siteed/audiostream/NotificationConfig.kt +0 -70
  23. package/android/src/main/java/net/siteed/audiostream/PermissionUtils.kt +0 -59
  24. package/android/src/main/java/net/siteed/audiostream/RecordingActionReceiver.kt +0 -59
  25. package/android/src/main/java/net/siteed/audiostream/RecordingConfig.kt +0 -205
  26. package/android/src/main/java/net/siteed/audiostream/WaveformConfig.kt +0 -19
  27. package/android/src/main/java/net/siteed/audiostream/WaveformRenderer.kt +0 -159
  28. package/android/src/main/res/drawable/ic_default_action_icon.xml +0 -16
  29. package/android/src/main/res/drawable/ic_microphone.xml +0 -13
  30. package/android/src/main/res/drawable/ic_pause.xml +0 -10
  31. package/android/src/main/res/drawable/ic_play.xml +0 -10
  32. package/android/src/main/res/drawable/ic_stop.xml +0 -10
  33. package/android/src/main/res/layout/notification_recording.xml +0 -37
  34. package/android/src/main/test/java/net/siteed/audiostream/AudioProcessorTest.kt +0 -56
  35. package/app.plugin.js +0 -1
  36. package/build/AudioAnalysis/AudioAnalysis.types.d.ts +0 -144
  37. package/build/AudioAnalysis/AudioAnalysis.types.d.ts.map +0 -1
  38. package/build/AudioAnalysis/AudioAnalysis.types.js +0 -3
  39. package/build/AudioAnalysis/AudioAnalysis.types.js.map +0 -1
  40. package/build/AudioAnalysis/extractAudioAnalysis.d.ts +0 -78
  41. package/build/AudioAnalysis/extractAudioAnalysis.d.ts.map +0 -1
  42. package/build/AudioAnalysis/extractAudioAnalysis.js +0 -229
  43. package/build/AudioAnalysis/extractAudioAnalysis.js.map +0 -1
  44. package/build/AudioAnalysis/extractWaveform.d.ts +0 -8
  45. package/build/AudioAnalysis/extractWaveform.d.ts.map +0 -1
  46. package/build/AudioAnalysis/extractWaveform.js +0 -11
  47. package/build/AudioAnalysis/extractWaveform.js.map +0 -1
  48. package/build/AudioRecorder.provider.d.ts +0 -11
  49. package/build/AudioRecorder.provider.d.ts.map +0 -1
  50. package/build/AudioRecorder.provider.js +0 -37
  51. package/build/AudioRecorder.provider.js.map +0 -1
  52. package/build/ExpoAudioStream.native.d.ts +0 -3
  53. package/build/ExpoAudioStream.native.d.ts.map +0 -1
  54. package/build/ExpoAudioStream.native.js +0 -6
  55. package/build/ExpoAudioStream.native.js.map +0 -1
  56. package/build/ExpoAudioStream.types.d.ts +0 -206
  57. package/build/ExpoAudioStream.types.d.ts.map +0 -1
  58. package/build/ExpoAudioStream.types.js +0 -2
  59. package/build/ExpoAudioStream.types.js.map +0 -1
  60. package/build/ExpoAudioStream.web.d.ts +0 -59
  61. package/build/ExpoAudioStream.web.d.ts.map +0 -1
  62. package/build/ExpoAudioStream.web.js +0 -285
  63. package/build/ExpoAudioStream.web.js.map +0 -1
  64. package/build/ExpoAudioStreamModule.d.ts +0 -3
  65. package/build/ExpoAudioStreamModule.d.ts.map +0 -1
  66. package/build/ExpoAudioStreamModule.js +0 -239
  67. package/build/ExpoAudioStreamModule.js.map +0 -1
  68. package/build/WebRecorder.web.d.ts +0 -119
  69. package/build/WebRecorder.web.d.ts.map +0 -1
  70. package/build/WebRecorder.web.js +0 -436
  71. package/build/WebRecorder.web.js.map +0 -1
  72. package/build/constants.d.ts +0 -11
  73. package/build/constants.d.ts.map +0 -1
  74. package/build/constants.js +0 -14
  75. package/build/constants.js.map +0 -1
  76. package/build/events.d.ts +0 -26
  77. package/build/events.d.ts.map +0 -1
  78. package/build/events.js +0 -21
  79. package/build/events.js.map +0 -1
  80. package/build/index.d.ts.map +0 -1
  81. package/build/index.js.map +0 -1
  82. package/build/useAudioRecorder.d.ts +0 -21
  83. package/build/useAudioRecorder.d.ts.map +0 -1
  84. package/build/useAudioRecorder.js +0 -427
  85. package/build/useAudioRecorder.js.map +0 -1
  86. package/build/utils/BlobFix.d.ts +0 -9
  87. package/build/utils/BlobFix.d.ts.map +0 -1
  88. package/build/utils/BlobFix.js +0 -498
  89. package/build/utils/BlobFix.js.map +0 -1
  90. package/build/utils/audioProcessing.d.ts +0 -24
  91. package/build/utils/audioProcessing.d.ts.map +0 -1
  92. package/build/utils/audioProcessing.js +0 -133
  93. package/build/utils/audioProcessing.js.map +0 -1
  94. package/build/utils/concatenateBuffers.d.ts +0 -8
  95. package/build/utils/concatenateBuffers.d.ts.map +0 -1
  96. package/build/utils/concatenateBuffers.js +0 -21
  97. package/build/utils/concatenateBuffers.js.map +0 -1
  98. package/build/utils/convertPCMToFloat32.d.ts +0 -13
  99. package/build/utils/convertPCMToFloat32.d.ts.map +0 -1
  100. package/build/utils/convertPCMToFloat32.js +0 -120
  101. package/build/utils/convertPCMToFloat32.js.map +0 -1
  102. package/build/utils/encodingToBitDepth.d.ts +0 -5
  103. package/build/utils/encodingToBitDepth.d.ts.map +0 -1
  104. package/build/utils/encodingToBitDepth.js +0 -13
  105. package/build/utils/encodingToBitDepth.js.map +0 -1
  106. package/build/utils/getWavFileInfo.d.ts +0 -26
  107. package/build/utils/getWavFileInfo.d.ts.map +0 -1
  108. package/build/utils/getWavFileInfo.js +0 -92
  109. package/build/utils/getWavFileInfo.js.map +0 -1
  110. package/build/utils/writeWavHeader.d.ts +0 -49
  111. package/build/utils/writeWavHeader.d.ts.map +0 -1
  112. package/build/utils/writeWavHeader.js +0 -91
  113. package/build/utils/writeWavHeader.js.map +0 -1
  114. package/build/workers/InlineFeaturesExtractor.web.d.ts +0 -2
  115. package/build/workers/InlineFeaturesExtractor.web.d.ts.map +0 -1
  116. package/build/workers/InlineFeaturesExtractor.web.js +0 -828
  117. package/build/workers/InlineFeaturesExtractor.web.js.map +0 -1
  118. package/build/workers/inlineAudioWebWorker.web.d.ts +0 -2
  119. package/build/workers/inlineAudioWebWorker.web.d.ts.map +0 -1
  120. package/build/workers/inlineAudioWebWorker.web.js +0 -157
  121. package/build/workers/inlineAudioWebWorker.web.js.map +0 -1
  122. package/expo-module.config.json +0 -9
  123. package/ios/AudioAnalysisData.swift +0 -74
  124. package/ios/AudioNotificationManager.swift +0 -135
  125. package/ios/AudioProcessingHelpers.swift +0 -743
  126. package/ios/AudioProcessor.swift +0 -858
  127. package/ios/AudioStreamError.swift +0 -7
  128. package/ios/AudioStreamManager.swift +0 -1708
  129. package/ios/AudioStreamManagerDelegate.swift +0 -16
  130. package/ios/DataPoint.swift +0 -54
  131. package/ios/DecodingConfig.swift +0 -47
  132. package/ios/ExpoAudioStream.podspec +0 -27
  133. package/ios/ExpoAudioStreamModule.swift +0 -698
  134. package/ios/FFT.swift +0 -62
  135. package/ios/Features.swift +0 -95
  136. package/ios/Logger.swift +0 -7
  137. package/ios/NotificationExtension.swift +0 -15
  138. package/ios/RecordingResult.swift +0 -22
  139. package/ios/RecordingSettings.swift +0 -265
  140. package/ios/WaveformExtractor.swift +0 -105
  141. package/plugin/build/index.d.ts +0 -21
  142. package/plugin/build/index.js +0 -191
  143. package/plugin/src/index.ts +0 -278
  144. package/plugin/tsconfig.json +0 -10
  145. package/plugin/tsconfig.tsbuildinfo +0 -1
  146. package/src/AudioAnalysis/AudioAnalysis.types.ts +0 -165
  147. package/src/AudioAnalysis/extractAudioAnalysis.ts +0 -370
  148. package/src/AudioAnalysis/extractWaveform.ts +0 -22
  149. package/src/AudioRecorder.provider.tsx +0 -54
  150. package/src/ExpoAudioStream.native.ts +0 -6
  151. package/src/ExpoAudioStream.types.ts +0 -329
  152. package/src/ExpoAudioStream.web.ts +0 -359
  153. package/src/ExpoAudioStreamModule.ts +0 -286
  154. package/src/WebRecorder.web.ts +0 -580
  155. package/src/constants.ts +0 -18
  156. package/src/events.ts +0 -60
  157. package/src/useAudioRecorder.tsx +0 -620
  158. package/src/utils/BlobFix.ts +0 -559
  159. package/src/utils/audioProcessing.ts +0 -205
  160. package/src/utils/concatenateBuffers.ts +0 -24
  161. package/src/utils/convertPCMToFloat32.ts +0 -170
  162. package/src/utils/encodingToBitDepth.ts +0 -18
  163. package/src/utils/getWavFileInfo.ts +0 -132
  164. package/src/utils/writeWavHeader.ts +0 -114
  165. package/src/workers/InlineFeaturesExtractor.web.tsx +0 -827
  166. package/src/workers/inlineAudioWebWorker.web.tsx +0 -156
@@ -1,698 +0,0 @@
1
- // packages/expo-audio-stream/ios/ExpoAudioStreamModule.swift
2
- import ExpoModulesCore
3
- import AVFoundation
4
-
5
- let audioDataEvent: String = "AudioData"
6
- let audioAnalysisEvent: String = "AudioAnalysis"
7
- let recordingInterruptedEvent: String = "onRecordingInterrupted"
8
-
9
- public class ExpoAudioStreamModule: Module, AudioStreamManagerDelegate {
10
- private var streamManager = AudioStreamManager()
11
- private let notificationCenter = UNUserNotificationCenter.current()
12
- private let notificationIdentifier = "audio_recording_notification"
13
-
14
- public func definition() -> ModuleDefinition {
15
- Name("ExpoAudioStream")
16
-
17
- // Defines event names that the module can send to JavaScript.
18
- Events([
19
- audioDataEvent,
20
- audioAnalysisEvent,
21
- recordingInterruptedEvent
22
- ])
23
-
24
- OnCreate {
25
- print("Setting streamManager delegate")
26
- streamManager.delegate = self
27
- }
28
-
29
- /// Extracts audio analysis data from an audio file.
30
- ///
31
- /// - Parameters:
32
- /// - options: A dictionary containing:
33
- /// - `fileUri`: The URI of the audio file.
34
- /// - `pointsPerSecond`: The number of data points to extract per second of audio.
35
- /// - `features`: A dictionary specifying which features to extract (e.g., `energy`, `mfcc`, `rms`, etc.).
36
- /// - promise: A promise to resolve with the extracted audio analysis data or reject with an error.
37
- /// - Returns: Promise to be resolved with audio analysis data.
38
- AsyncFunction("extractAudioAnalysis") { (options: [String: Any], promise: Promise) in
39
- guard let fileUri = options["fileUri"] as? String,
40
- let url = URL(string: fileUri) else {
41
- promise.reject("INVALID_ARGUMENTS", "Invalid file URI provided")
42
- return
43
- }
44
-
45
- // Get time or byte range options
46
- let startTimeMs = options["startTimeMs"] as? Double
47
- let endTimeMs = options["endTimeMs"] as? Double
48
- let position = options["position"] as? Int
49
- let byteLength = options["length"] as? Int
50
-
51
- // Validate ranges - can have time range OR byte range OR no range
52
- let hasTimeRange = startTimeMs != nil && endTimeMs != nil
53
- let hasByteRange = position != nil && byteLength != nil
54
-
55
- // Only throw if both ranges are provided
56
- guard !(hasTimeRange && hasByteRange) else {
57
- promise.reject("INVALID_ARGUMENTS", "Cannot specify both time range and byte range")
58
- return
59
- }
60
-
61
- let features = options["features"] as? [String: Bool] ?? [:]
62
- let featureOptions = self.extractFeatureOptions(from: features)
63
- let segmentDurationMs = options["segmentDurationMs"] as? Int ?? 100 // Default value of 100ms
64
-
65
- DispatchQueue.global().async(execute: {
66
- do {
67
- let audioFile = try AVAudioFile(forReading: url)
68
- let bitDepth = audioFile.fileFormat.settings[AVLinearPCMBitDepthKey] as? Int ?? 16
69
- let numberOfChannels = Int(audioFile.fileFormat.channelCount)
70
- let sampleRate = audioFile.fileFormat.sampleRate
71
-
72
- // Convert time range to byte range if needed
73
- let effectivePosition: Int?
74
- let effectiveLength: Int?
75
-
76
- if hasTimeRange {
77
- let bytesPerSecond = Int(sampleRate) * numberOfChannels * (bitDepth / 8)
78
- effectivePosition = Int(startTimeMs! * Double(bytesPerSecond) / 1000.0)
79
- effectiveLength = Int((endTimeMs! - startTimeMs!) * Double(bytesPerSecond) / 1000.0)
80
- } else {
81
- effectivePosition = position
82
- effectiveLength = byteLength
83
- }
84
-
85
- let audioProcessor = try AudioProcessor(url: url, resolve: { result in
86
- promise.resolve(result)
87
- }, reject: { code, message in
88
- promise.reject(code, message)
89
- })
90
-
91
- if let result = audioProcessor.processAudioData(
92
- numberOfSamples: nil,
93
- offset: 0,
94
- length: nil,
95
- segmentDurationMs: segmentDurationMs,
96
- featureOptions: featureOptions,
97
- bitDepth: bitDepth,
98
- numberOfChannels: numberOfChannels,
99
- position: effectivePosition,
100
- byteLength: effectiveLength
101
- ) {
102
- promise.resolve(result.toDictionary())
103
- } else {
104
- promise.reject("PROCESSING_ERROR", "Failed to process audio data")
105
- }
106
- } catch {
107
- promise.reject("PROCESSING_ERROR", "Failed to initialize audio processor: \(error.localizedDescription)")
108
- }
109
- })
110
- }
111
-
112
-
113
- /// Asynchronously starts audio recording with the given settings.
114
- ///
115
- /// - Parameters:
116
- /// - options: A dictionary containing:
117
- /// - `sampleRate`: The sample rate for recording (default is 16000.0).
118
- /// - `channelConfig`: The number of channels (default is 1 for mono).
119
- /// - `audioFormat`: The bit depth for recording (default is 16 bits).
120
- /// - `interval`: The interval in milliseconds at which to emit recording data (default is 1000 ms).
121
- /// - `intervalAnalysis`: The interval in milliseconds at which to emit analysis data (default is 500 ms).
122
- /// - `enableProcessing`: Boolean to enable/disable audio processing (default is false).
123
- /// - `pointsPerSecond`: The number of data points to extract per second of audio (default is 20).
124
- /// - `algorithm`: The algorithm to use for extraction (default is "rms").
125
- /// - `featureOptions`: A dictionary of feature options to extract (default is empty).
126
- /// - `maxRecentDataDuration`: The maximum duration of recent data to keep for processing (default is 10.0 seconds).
127
- /// - `compression`: A dictionary containing:
128
- /// - `enabled`: Boolean to enable/disable compression (default is false).
129
- /// - `format`: The compression format (default is "aac").
130
- /// - `bitrate`: The compression bitrate in bps (default is 128000).
131
- /// - promise: A promise to resolve with the recording settings or reject with an error.
132
- AsyncFunction("startRecording") { (options: [String: Any], promise: Promise) in
133
- self.checkMicrophonePermission { granted in
134
- guard granted else {
135
- promise.reject("PERMISSION_DENIED", "Recording permission has not been granted")
136
- return
137
- }
138
-
139
- // Create settings with validation
140
- let settingsResult = RecordingSettings.fromDictionary(options)
141
-
142
- switch settingsResult {
143
- case .success(let settings):
144
- // Initialize notification if enabled
145
- if settings.showNotification {
146
- Task {
147
- let notificationGranted = await self.requestNotificationPermissions()
148
- if !notificationGranted {
149
- Logger.debug("Notification permissions not granted")
150
- }
151
- }
152
- }
153
-
154
- if let result = self.streamManager.startRecording(settings: settings) {
155
- var resultDict: [String: Any] = [
156
- "fileUri": result.fileUri,
157
- "channels": result.channels,
158
- "bitDepth": result.bitDepth,
159
- "sampleRate": result.sampleRate,
160
- "mimeType": result.mimeType,
161
- ]
162
-
163
- // Add compression info if available
164
- if let compression = result.compression {
165
- resultDict["compression"] = [
166
- "compressedFileUri": compression.compressedFileUri,
167
- "mimeType": compression.mimeType,
168
- "bitrate": compression.bitrate,
169
- "format": compression.format
170
- ]
171
- }
172
-
173
- promise.resolve(resultDict)
174
- } else {
175
- promise.reject("ERROR", "Failed to start recording.")
176
- }
177
-
178
- case .failure(let error):
179
- promise.reject("INVALID_SETTINGS", error.localizedDescription)
180
- }
181
- }
182
- }
183
-
184
- /// Retrieves the current status of the audio stream.
185
- ///
186
- /// - Returns: The current status of the audio stream.Ï
187
- Function("status") {
188
- return self.streamManager.getStatus()
189
- }
190
-
191
- /// Pauses audio recording.
192
- Function("pauseRecording") {
193
- self.streamManager.pauseRecording()
194
- }
195
-
196
- /// Resumes audio recording.
197
- Function("resumeRecording") {
198
- self.streamManager.resumeRecording()
199
- }
200
-
201
- /// Asynchronously stops audio recording and retrieves the recording result.
202
- ///
203
- /// - Parameters:
204
- /// - promise: A promise to resolve with the recording result or reject with an error.
205
- AsyncFunction("stopRecording") { (promise: Promise) in
206
- if let recordingResult = self.streamManager.stopRecording() {
207
- var resultDict: [String: Any] = [
208
- "fileUri": recordingResult.fileUri,
209
- "filename": recordingResult.filename,
210
- "durationMs": recordingResult.duration,
211
- "size": recordingResult.size,
212
- "channels": recordingResult.channels,
213
- "bitDepth": recordingResult.bitDepth,
214
- "sampleRate": recordingResult.sampleRate,
215
- "mimeType": recordingResult.mimeType,
216
- "createdAt": Date().timeIntervalSince1970 * 1000,
217
- ]
218
-
219
- // Add compression info if available
220
- if let compression = recordingResult.compression {
221
- resultDict["compression"] = [
222
- "compressedFileUri": compression.compressedFileUri,
223
- "mimeType": compression.mimeType,
224
- "bitrate": compression.bitrate,
225
- "format": compression.format,
226
- "size": compression.size
227
- ]
228
- }
229
-
230
- promise.resolve(resultDict)
231
- } else {
232
- promise.reject("ERROR", "Failed to stop recording or no recording in progress.")
233
- }
234
- }
235
-
236
- /// Asynchronously lists all audio files stored in the document directory.
237
- ///
238
- /// - Parameters:
239
- /// - promise: A promise to resolve with the list of audio file URIs or reject with an error.
240
- /// - Returns: A promise that resolves with the list of audio file URIs or rejects with an error.
241
- AsyncFunction("listAudioFiles") { (promise: Promise) in
242
- let files = listAudioFiles()
243
- promise.resolve(files)
244
- }
245
-
246
- /// Clears all audio files stored in the document directory.
247
- Function("clearAudioFiles") {
248
- clearAudioFiles()
249
- }
250
-
251
-
252
- /// Requests audio recording permissions.
253
- ///
254
- /// - Parameters:
255
- /// - promise: A promise to resolve with the permission status or reject with an error.
256
- /// - Returns: Promise to be resolved with the permission status.
257
- AsyncFunction("requestPermissionsAsync") { (promise: Promise) in
258
- AVAudioSession.sharedInstance().requestRecordPermission { granted in
259
- promise.resolve([
260
- "status": granted ? "granted" : "denied",
261
- "granted": granted,
262
- "expires": "never",
263
- "canAskAgain": true
264
- ])
265
- }
266
- }
267
-
268
- AsyncFunction("requestNotificationPermissionsAsync") { (promise: Promise) in
269
- Task {
270
- let granted = await requestNotificationPermissions()
271
- promise.resolve([
272
- "granted": granted,
273
- "status": granted ? "granted" : "denied"
274
- ])
275
- }
276
- }
277
-
278
- /// Gets the current audio recording permissions.
279
- ///
280
- /// - Parameters:
281
- /// - promise: A promise to resolve with the permission status or reject with an error.
282
- /// - Returns: Promise to be resolved with the permission status.
283
- AsyncFunction("getPermissionsAsync") { (promise: Promise) in
284
- let permissionStatus = AVAudioSession.sharedInstance().recordPermission
285
- switch permissionStatus {
286
- case .granted:
287
- promise.resolve([
288
- "status": "granted",
289
- "granted": true,
290
- "expires": "never",
291
- "canAskAgain": true
292
- ])
293
- case .denied:
294
- promise.resolve([
295
- "status": "denied",
296
- "granted": false,
297
- "expires": "never",
298
- "canAskAgain": false
299
- ])
300
- case .undetermined:
301
- promise.resolve([
302
- "status": "undetermined",
303
- "granted": false,
304
- "expires": "never",
305
- "canAskAgain": true
306
- ])
307
- @unknown default:
308
- promise.reject("UNKNOWN_ERROR", "Unknown permission status")
309
- }
310
- }
311
-
312
- /// Trims an audio file to specified start and end times.
313
- /// - Parameters:
314
- /// - options: A dictionary containing:
315
- /// - `fileUri`: The URI of the audio file.
316
- /// - `startTimeMs`: Start time in milliseconds.
317
- /// - `endTimeMs`: End time in milliseconds.
318
- /// - `outputFormat`: Optional output format configuration.
319
- AsyncFunction("trimAudio") { (options: [String: Any], promise: Promise) in
320
- guard let fileUri = options["fileUri"] as? String,
321
- let startTimeMs = options["startTimeMs"] as? Double,
322
- let endTimeMs = options["endTimeMs"] as? Double,
323
- let url = URL(string: fileUri) else {
324
- promise.reject("INVALID_ARGUMENTS", "Invalid arguments provided")
325
- return
326
- }
327
-
328
- let outputFormat = options["outputFormat"] as? [String: Any]
329
-
330
- DispatchQueue.global().async {
331
- do {
332
- let audioProcessor = try AudioProcessor(
333
- url: url,
334
- resolve: { result in
335
- promise.resolve(result)
336
- },
337
- reject: { code, message in
338
- promise.reject(code, message)
339
- }
340
- )
341
-
342
- if let result = audioProcessor.trimAudio(
343
- startTimeMs: startTimeMs,
344
- endTimeMs: endTimeMs,
345
- outputFormat: outputFormat
346
- ) {
347
- promise.resolve([
348
- "uri": result.uri,
349
- "duration": result.duration,
350
- "size": result.size
351
- ])
352
- } else {
353
- promise.reject("TRIM_ERROR", "Failed to trim audio")
354
- }
355
- } catch {
356
- promise.reject("PROCESSING_ERROR", "Failed to initialize audio processor: \(error.localizedDescription)")
357
- }
358
- }
359
- }
360
-
361
- /// Extracts raw PCM audio data from a file with time or byte range support
362
- /// - Parameters:
363
- /// - options: A dictionary containing:
364
- /// - `fileUri`: The URI of the audio file
365
- /// - `startTimeMs`: Optional start time in milliseconds
366
- /// - `endTimeMs`: Optional end time in milliseconds
367
- /// - `position`: Optional byte position
368
- /// - `length`: Optional byte length
369
- /// - `includeNormalizedData`: Boolean to include normalized audio data in [-1, 1] range
370
- /// - `includeWavHeader`: Boolean to include WAV header in the PCM data
371
- /// - `decodingOptions`: Decoding configuration
372
- /// - `includeBase64Data`: Boolean to include base64 encoded string representation of the audio data
373
- /// - `computeChecksum`: Boolean to compute and include CRC32 checksum of the PCM data
374
- AsyncFunction("extractAudioData") { (options: [String: Any], promise: Promise) in
375
- guard let fileUri = options["fileUri"] as? String,
376
- let url = URL(string: fileUri) else {
377
- promise.reject("INVALID_ARGUMENTS", "Invalid file URI provided")
378
- return
379
- }
380
-
381
- // Get time or byte range options
382
- let startTimeMs = options["startTimeMs"] as? Double
383
- let endTimeMs = options["endTimeMs"] as? Double
384
- let position = options["position"] as? Int
385
- let length = options["length"] as? Int
386
- let includeWavHeader = options["includeWavHeader"] as? Bool ?? false
387
-
388
- // Validate that we have either time range or byte range, but not both and not neither
389
- let hasTimeRange = startTimeMs != nil && endTimeMs != nil
390
- let hasByteRange = position != nil && length != nil
391
-
392
- guard hasTimeRange || hasByteRange else {
393
- promise.reject("INVALID_ARGUMENTS", "Must specify either time range (startTimeMs, endTimeMs) or byte range (position, length)")
394
- return
395
- }
396
-
397
- guard !(hasTimeRange && hasByteRange) else {
398
- promise.reject("INVALID_ARGUMENTS", "Cannot specify both time range and byte range")
399
- return
400
- }
401
-
402
- do {
403
- let audioFile = try AVAudioFile(forReading: url)
404
- let format = audioFile.processingFormat
405
- let sampleRate = format.sampleRate
406
- let channels = Int(format.channelCount)
407
- let bitDepth = audioFile.fileFormat.settings[AVLinearPCMBitDepthKey] as? Int ?? 16
408
-
409
- // Calculate frame positions
410
- let startFrame: AVAudioFramePosition
411
- let endFrame: AVAudioFramePosition
412
-
413
- if hasTimeRange {
414
- startFrame = AVAudioFramePosition(startTimeMs! * sampleRate / 1000.0)
415
- endFrame = AVAudioFramePosition(endTimeMs! * sampleRate / 1000.0)
416
- } else {
417
- // Convert byte position to frame position
418
- let bytesPerFrame = Int64(channels * (bitDepth / 8))
419
- startFrame = AVAudioFramePosition(position!) / bytesPerFrame
420
- endFrame = startFrame + (AVAudioFramePosition(length!) / bytesPerFrame)
421
- }
422
-
423
- // Validate frame range
424
- guard startFrame >= 0 && endFrame <= audioFile.length && startFrame < endFrame else {
425
- promise.reject("INVALID_RANGE", "Invalid range specified")
426
- return
427
- }
428
-
429
- let frameCount = AVAudioFrameCount(endFrame - startFrame)
430
-
431
- // Create decoding config that includes normalization preference
432
- var decodingOptions = options["decodingOptions"] as? [String: Any] ?? [:]
433
- let includeNormalizedData = options["includeNormalizedData"] as? Bool ?? false
434
-
435
- // Pass both options separately - normalizeAudio from decodingOptions, and includeNormalizedData as is
436
- let decodingConfig = DecodingConfig.fromDictionary(decodingOptions)
437
-
438
- let (pcmData, normalizedData, base64Data) = try extractRawAudioData(
439
- from: url,
440
- startFrame: startFrame,
441
- frameCount: frameCount,
442
- format: format,
443
- decodingConfig: decodingConfig,
444
- includeNormalizedData: includeNormalizedData,
445
- includeBase64Data: options["includeBase64Data"] as? Bool ?? false
446
- )
447
-
448
- var resultDict: [String: Any] = [:]
449
-
450
- if includeWavHeader {
451
- // Create WAV header and prepend it to the PCM data
452
- let wavData = createWavHeader(
453
- pcmData: pcmData,
454
- sampleRate: Int(sampleRate),
455
- channels: channels,
456
- bitDepth: bitDepth
457
- )
458
- resultDict["pcmData"] = wavData
459
- resultDict["hasWavHeader"] = true
460
- } else {
461
- resultDict["pcmData"] = pcmData
462
- resultDict["hasWavHeader"] = false
463
- }
464
-
465
- // Add the rest of the data
466
- resultDict["sampleRate"] = Int(sampleRate)
467
- resultDict["channels"] = channels
468
- resultDict["bitDepth"] = bitDepth
469
- resultDict["durationMs"] = Int(Double(frameCount) * 1000.0 / sampleRate)
470
- resultDict["format"] = "pcm_\(bitDepth)bit"
471
- resultDict["samples"] = Int(frameCount) * channels
472
-
473
- // Add normalized data if requested, regardless of normalization setting
474
- if includeNormalizedData {
475
- resultDict["normalizedData"] = normalizedData
476
- }
477
-
478
- // Add checksum if requested
479
- if options["computeChecksum"] as? Bool == true {
480
- let checksum = calculateCRC32(data: pcmData)
481
- resultDict["checksum"] = Int(checksum)
482
-
483
- Logger.debug("Computed CRC32 checksum: \(checksum)")
484
- }
485
-
486
- if let includeBase64Data = options["includeBase64Data"] as? Bool, includeBase64Data {
487
- resultDict["base64Data"] = base64Data
488
- }
489
-
490
- promise.resolve(resultDict)
491
-
492
- } catch {
493
- promise.reject("PROCESSING_ERROR", "Failed to process audio file: \(error.localizedDescription)")
494
- }
495
- }
496
- }
497
-
498
- func audioStreamManager(_ manager: AudioStreamManager, didReceiveInterruption info: [String: Any]) {
499
- // Convert iOS interruption events to match the TypeScript types
500
- var reason: String
501
- var isPaused: Bool = true
502
-
503
- if let type = info["type"] as? String {
504
- switch type {
505
- case "began":
506
- // Phone call or other audio session interruption began
507
- reason = "audioFocusLoss"
508
- case "ended":
509
- reason = "audioFocusGain"
510
- isPaused = false
511
- // Check if this was from a phone call
512
- if let wasSuspended = info["wasSuspended"] as? Bool, wasSuspended {
513
- reason = "phoneCallEnded"
514
- }
515
- default:
516
- return
517
- }
518
- } else if let specificReason = info["reason"] as? String {
519
- // Handle specific reasons that are already properly formatted
520
- reason = specificReason
521
- isPaused = info["isPaused"] as? Bool ?? true
522
- } else {
523
- return
524
- }
525
-
526
- // Send event in the correct format
527
- sendEvent(recordingInterruptedEvent, [
528
- "reason": reason,
529
- "isPaused": isPaused,
530
- "timestamp": Date().timeIntervalSince1970 * 1000
531
- ])
532
- }
533
-
534
- func audioStreamManager(_ manager: AudioStreamManager, didPauseRecording pauseTime: Date) {
535
- sendEvent(recordingInterruptedEvent, [
536
- "reason": "userPaused",
537
- "isPaused": true,
538
- "timestamp": pauseTime.timeIntervalSince1970 * 1000
539
- ])
540
- }
541
-
542
- func audioStreamManager(_ manager: AudioStreamManager, didResumeRecording resumeTime: Date) {
543
- sendEvent(recordingInterruptedEvent, [
544
- "reason": "userResumed",
545
- "isPaused": false,
546
- "timestamp": resumeTime.timeIntervalSince1970 * 1000
547
- ])
548
- }
549
-
550
- func audioStreamManager(_ manager: AudioStreamManager, didUpdateNotificationState isPaused: Bool) {
551
- sendEvent(recordingInterruptedEvent, [
552
- "reason": "notification",
553
- "isPaused": isPaused,
554
- "timestamp": Date().timeIntervalSince1970 * 1000
555
- ])
556
- }
557
-
558
- /// Handles the reception of audio data from the AudioStreamManager.
559
- ///
560
- /// - Parameters:
561
- /// - manager: The AudioStreamManager instance.
562
- /// - data: The received audio data.
563
- /// - recordingTime: The current recording time.
564
- /// - totalDataSize: The total size of the received audio data.
565
- func audioStreamManager(
566
- _ manager: AudioStreamManager,
567
- didReceiveAudioData data: Data,
568
- recordingTime: TimeInterval,
569
- totalDataSize: Int64,
570
- compressionInfo: [String: Any]?
571
- ) {
572
- var resultDict: [String: Any] = [
573
- "fileUri": manager.recordingFileURL?.absoluteString ?? "",
574
- "lastEmittedSize": totalDataSize,
575
- "encoded": data.base64EncodedString(),
576
- "deltaSize": data.count,
577
- "position": Int64(recordingTime * 1000),
578
- "mimeType": manager.mimeType,
579
- "totalSize": totalDataSize,
580
- "streamUuid": manager.recordingUUID?.uuidString ?? UUID().uuidString
581
- ]
582
-
583
- if let compressionInfo = compressionInfo {
584
- resultDict["compression"] = compressionInfo
585
- }
586
-
587
- sendEvent(audioDataEvent, resultDict)
588
- }
589
-
590
- private func requestNotificationPermissions() async -> Bool {
591
- do {
592
- let options: UNAuthorizationOptions = [.alert, .sound]
593
- return try await notificationCenter.requestAuthorization(options: options)
594
- } catch {
595
- Logger.debug("Failed to request notification permissions: \(error)")
596
- return false
597
- }
598
- }
599
-
600
- func audioStreamManager(_ manager: AudioStreamManager, didReceiveProcessingResult result: AudioAnalysisData?) {
601
- // Handle the processed audio data
602
- // Emit the processing result event to JavaScript
603
- let resultDict = result?.toDictionary() ?? [:]
604
- Logger.debug("emitting \(audioAnalysisEvent) event with \(resultDict)")
605
- sendEvent(audioAnalysisEvent, resultDict)
606
- }
607
-
608
- /// Checks microphone permission and calls the completion handler with the result.
609
- ///
610
- /// - Parameters:
611
- /// - completion: A completion handler that receives a boolean indicating whether the microphone permission was granted.
612
- private func checkMicrophonePermission(completion: @escaping (Bool) -> Void) {
613
- switch AVAudioSession.sharedInstance().recordPermission {
614
- case .granted:
615
- completion(true)
616
- case .denied:
617
- completion(false)
618
- case .undetermined:
619
- AVAudioSession.sharedInstance().requestRecordPermission { granted in
620
- DispatchQueue.main.async {
621
- completion(granted)
622
- }
623
- }
624
- @unknown default:
625
- completion(false)
626
- }
627
- }
628
-
629
- /// Clears all audio files stored in the document directory.
630
- private func clearAudioFiles() {
631
- let fileURLs = listAudioFiles() // This now returns full URLs as strings
632
- fileURLs.forEach { fileURLString in
633
- if let fileURL = URL(string: fileURLString) {
634
- do {
635
- try FileManager.default.removeItem(at: fileURL)
636
- print("Removed file at:", fileURL.path)
637
- } catch {
638
- print("Error removing file at \(fileURL.path):", error.localizedDescription)
639
- }
640
- } else {
641
- print("Invalid URL string: \(fileURLString)")
642
- }
643
- }
644
- }
645
-
646
- /// Extracts feature options from the provided options dictionary.
647
- ///
648
- /// - Parameters:
649
- /// - options: The options dictionary containing feature flags.
650
- /// - Returns: A dictionary with feature flags and their boolean values.
651
- private func extractFeatureOptions(from options: [String: Any]) -> [String: Bool] {
652
- return [
653
- "energy": options["energy"] as? Bool ?? false,
654
- "mfcc": options["mfcc"] as? Bool ?? false,
655
- "rms": options["rms"] as? Bool ?? false,
656
- "zcr": options["zcr"] as? Bool ?? false,
657
- "dB": options["dB"] as? Bool ?? false,
658
- "spectralCentroid": options["spectralCentroid"] as? Bool ?? false,
659
- "spectralFlatness": options["spectralFlatness"] as? Bool ?? false,
660
- "spectralRollOff": options["spectralRollOff"] as? Bool ?? false,
661
- "spectralBandwidth": options["spectralBandwidth"] as? Bool ?? false,
662
- "chromagram": options["chromagram"] as? Bool ?? false,
663
- "tempo": options["tempo"] as? Bool ?? false,
664
- "hnr": options["hnr"] as? Bool ?? false,
665
- "melSpectrogram": options["melSpectrogram"] as? Bool ?? false,
666
- "spectralContrast": options["spectralContrast"] as? Bool ?? false,
667
- "tonnetz": options["tonnetz"] as? Bool ?? false,
668
- "pitch": options["pitch"] as? Bool ?? false,
669
- "crc32": options["crc32"] as? Bool ?? false
670
- ]
671
- }
672
-
673
- /// Lists all audio files stored in the document directory.
674
- ///
675
- /// - Returns: An array of file URIs as strings.
676
- func listAudioFiles() -> [String] {
677
- guard let documentDirectory = try? FileManager.default.url(for: .documentDirectory, in: .userDomainMask, appropriateFor: nil, create: false) else {
678
- print("Failed to access document directory.")
679
- return []
680
- }
681
-
682
- do {
683
- let files = try FileManager.default.contentsOfDirectory(at: documentDirectory, includingPropertiesForKeys: nil)
684
- let audioFiles = files.filter { $0.pathExtension == "wav" }.map { $0.absoluteString }
685
- return audioFiles
686
- } catch {
687
- print("Error listing audio files:", error.localizedDescription)
688
- return []
689
- }
690
- }
691
-
692
- func audioStreamManager(_ manager: AudioStreamManager, didFailWithError error: String) {
693
- // Send error event to JavaScript
694
- sendEvent("error", [
695
- "message": error
696
- ])
697
- }
698
- }