@siteed/expo-audio-stream 2.1.0 → 2.2.1-beta.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 (189) hide show
  1. package/README.md +23 -260
  2. package/build/index.d.ts +11 -15
  3. package/build/index.js +54 -14
  4. package/build/src/index.d.ts +11 -0
  5. package/build/src/index.js +54 -0
  6. package/package.json +49 -110
  7. package/src/index.ts +18 -32
  8. package/CHANGELOG.md +0 -206
  9. package/android/build.gradle +0 -105
  10. package/android/src/main/AndroidManifest.xml +0 -27
  11. package/android/src/main/java/net/siteed/audiostream/AudioAnalysisData.kt +0 -166
  12. package/android/src/main/java/net/siteed/audiostream/AudioDataEncoder.kt +0 -9
  13. package/android/src/main/java/net/siteed/audiostream/AudioFileHandler.kt +0 -131
  14. package/android/src/main/java/net/siteed/audiostream/AudioFormatUtils.kt +0 -103
  15. package/android/src/main/java/net/siteed/audiostream/AudioNotificationsManager.kt +0 -435
  16. package/android/src/main/java/net/siteed/audiostream/AudioProcessor.kt +0 -2235
  17. package/android/src/main/java/net/siteed/audiostream/AudioRecorderManager.kt +0 -1437
  18. package/android/src/main/java/net/siteed/audiostream/AudioRecordingService.kt +0 -152
  19. package/android/src/main/java/net/siteed/audiostream/AudioTrimmer.kt +0 -1099
  20. package/android/src/main/java/net/siteed/audiostream/Constants.kt +0 -21
  21. package/android/src/main/java/net/siteed/audiostream/EventSender.kt +0 -7
  22. package/android/src/main/java/net/siteed/audiostream/ExpoAudioStreamModule.kt +0 -739
  23. package/android/src/main/java/net/siteed/audiostream/FFT.kt +0 -99
  24. package/android/src/main/java/net/siteed/audiostream/Features.kt +0 -98
  25. package/android/src/main/java/net/siteed/audiostream/NotificationConfig.kt +0 -70
  26. package/android/src/main/java/net/siteed/audiostream/PermissionUtils.kt +0 -59
  27. package/android/src/main/java/net/siteed/audiostream/RecordingActionReceiver.kt +0 -59
  28. package/android/src/main/java/net/siteed/audiostream/RecordingConfig.kt +0 -205
  29. package/android/src/main/java/net/siteed/audiostream/WaveformConfig.kt +0 -19
  30. package/android/src/main/java/net/siteed/audiostream/WaveformRenderer.kt +0 -159
  31. package/android/src/main/res/drawable/ic_default_action_icon.xml +0 -16
  32. package/android/src/main/res/drawable/ic_microphone.xml +0 -13
  33. package/android/src/main/res/drawable/ic_pause.xml +0 -10
  34. package/android/src/main/res/drawable/ic_play.xml +0 -10
  35. package/android/src/main/res/drawable/ic_stop.xml +0 -10
  36. package/android/src/main/res/layout/notification_recording.xml +0 -37
  37. package/android/src/main/test/java/net/siteed/audiostream/AudioProcessorTest.kt +0 -56
  38. package/app.plugin.js +0 -1
  39. package/build/AudioAnalysis/AudioAnalysis.types.d.ts +0 -179
  40. package/build/AudioAnalysis/AudioAnalysis.types.d.ts.map +0 -1
  41. package/build/AudioAnalysis/AudioAnalysis.types.js +0 -3
  42. package/build/AudioAnalysis/AudioAnalysis.types.js.map +0 -1
  43. package/build/AudioAnalysis/extractAudioAnalysis.d.ts +0 -68
  44. package/build/AudioAnalysis/extractAudioAnalysis.d.ts.map +0 -1
  45. package/build/AudioAnalysis/extractAudioAnalysis.js +0 -203
  46. package/build/AudioAnalysis/extractAudioAnalysis.js.map +0 -1
  47. package/build/AudioAnalysis/extractAudioData.d.ts +0 -3
  48. package/build/AudioAnalysis/extractAudioData.d.ts.map +0 -1
  49. package/build/AudioAnalysis/extractAudioData.js +0 -5
  50. package/build/AudioAnalysis/extractAudioData.js.map +0 -1
  51. package/build/AudioAnalysis/extractMelSpectrogram.d.ts +0 -14
  52. package/build/AudioAnalysis/extractMelSpectrogram.d.ts.map +0 -1
  53. package/build/AudioAnalysis/extractMelSpectrogram.js +0 -85
  54. package/build/AudioAnalysis/extractMelSpectrogram.js.map +0 -1
  55. package/build/AudioAnalysis/extractPreview.d.ts +0 -11
  56. package/build/AudioAnalysis/extractPreview.d.ts.map +0 -1
  57. package/build/AudioAnalysis/extractPreview.js +0 -25
  58. package/build/AudioAnalysis/extractPreview.js.map +0 -1
  59. package/build/AudioAnalysis/extractWaveform.d.ts +0 -8
  60. package/build/AudioAnalysis/extractWaveform.d.ts.map +0 -1
  61. package/build/AudioAnalysis/extractWaveform.js +0 -11
  62. package/build/AudioAnalysis/extractWaveform.js.map +0 -1
  63. package/build/AudioRecorder.provider.d.ts +0 -11
  64. package/build/AudioRecorder.provider.d.ts.map +0 -1
  65. package/build/AudioRecorder.provider.js +0 -37
  66. package/build/AudioRecorder.provider.js.map +0 -1
  67. package/build/ExpoAudioStream.native.d.ts +0 -3
  68. package/build/ExpoAudioStream.native.d.ts.map +0 -1
  69. package/build/ExpoAudioStream.native.js +0 -6
  70. package/build/ExpoAudioStream.native.js.map +0 -1
  71. package/build/ExpoAudioStream.types.d.ts +0 -532
  72. package/build/ExpoAudioStream.types.d.ts.map +0 -1
  73. package/build/ExpoAudioStream.types.js +0 -2
  74. package/build/ExpoAudioStream.types.js.map +0 -1
  75. package/build/ExpoAudioStream.web.d.ts +0 -59
  76. package/build/ExpoAudioStream.web.d.ts.map +0 -1
  77. package/build/ExpoAudioStream.web.js +0 -285
  78. package/build/ExpoAudioStream.web.js.map +0 -1
  79. package/build/ExpoAudioStreamModule.d.ts +0 -3
  80. package/build/ExpoAudioStreamModule.d.ts.map +0 -1
  81. package/build/ExpoAudioStreamModule.js +0 -693
  82. package/build/ExpoAudioStreamModule.js.map +0 -1
  83. package/build/WebRecorder.web.d.ts +0 -119
  84. package/build/WebRecorder.web.d.ts.map +0 -1
  85. package/build/WebRecorder.web.js +0 -436
  86. package/build/WebRecorder.web.js.map +0 -1
  87. package/build/constants.d.ts +0 -11
  88. package/build/constants.d.ts.map +0 -1
  89. package/build/constants.js +0 -14
  90. package/build/constants.js.map +0 -1
  91. package/build/events.d.ts +0 -26
  92. package/build/events.d.ts.map +0 -1
  93. package/build/events.js +0 -21
  94. package/build/events.js.map +0 -1
  95. package/build/index.d.ts.map +0 -1
  96. package/build/index.js.map +0 -1
  97. package/build/trimAudio.d.ts +0 -25
  98. package/build/trimAudio.d.ts.map +0 -1
  99. package/build/trimAudio.js +0 -67
  100. package/build/trimAudio.js.map +0 -1
  101. package/build/useAudioRecorder.d.ts +0 -21
  102. package/build/useAudioRecorder.d.ts.map +0 -1
  103. package/build/useAudioRecorder.js +0 -427
  104. package/build/useAudioRecorder.js.map +0 -1
  105. package/build/utils/BlobFix.d.ts +0 -9
  106. package/build/utils/BlobFix.d.ts.map +0 -1
  107. package/build/utils/BlobFix.js +0 -498
  108. package/build/utils/BlobFix.js.map +0 -1
  109. package/build/utils/audioProcessing.d.ts +0 -24
  110. package/build/utils/audioProcessing.d.ts.map +0 -1
  111. package/build/utils/audioProcessing.js +0 -133
  112. package/build/utils/audioProcessing.js.map +0 -1
  113. package/build/utils/concatenateBuffers.d.ts +0 -8
  114. package/build/utils/concatenateBuffers.d.ts.map +0 -1
  115. package/build/utils/concatenateBuffers.js +0 -21
  116. package/build/utils/concatenateBuffers.js.map +0 -1
  117. package/build/utils/convertPCMToFloat32.d.ts +0 -13
  118. package/build/utils/convertPCMToFloat32.d.ts.map +0 -1
  119. package/build/utils/convertPCMToFloat32.js +0 -120
  120. package/build/utils/convertPCMToFloat32.js.map +0 -1
  121. package/build/utils/encodingToBitDepth.d.ts +0 -5
  122. package/build/utils/encodingToBitDepth.d.ts.map +0 -1
  123. package/build/utils/encodingToBitDepth.js +0 -13
  124. package/build/utils/encodingToBitDepth.js.map +0 -1
  125. package/build/utils/getWavFileInfo.d.ts +0 -26
  126. package/build/utils/getWavFileInfo.d.ts.map +0 -1
  127. package/build/utils/getWavFileInfo.js +0 -92
  128. package/build/utils/getWavFileInfo.js.map +0 -1
  129. package/build/utils/writeWavHeader.d.ts +0 -49
  130. package/build/utils/writeWavHeader.d.ts.map +0 -1
  131. package/build/utils/writeWavHeader.js +0 -91
  132. package/build/utils/writeWavHeader.js.map +0 -1
  133. package/build/workers/InlineFeaturesExtractor.web.d.ts +0 -2
  134. package/build/workers/InlineFeaturesExtractor.web.d.ts.map +0 -1
  135. package/build/workers/InlineFeaturesExtractor.web.js +0 -828
  136. package/build/workers/InlineFeaturesExtractor.web.js.map +0 -1
  137. package/build/workers/inlineAudioWebWorker.web.d.ts +0 -2
  138. package/build/workers/inlineAudioWebWorker.web.d.ts.map +0 -1
  139. package/build/workers/inlineAudioWebWorker.web.js +0 -157
  140. package/build/workers/inlineAudioWebWorker.web.js.map +0 -1
  141. package/expo-module.config.json +0 -9
  142. package/ios/AudioAnalysisData.swift +0 -74
  143. package/ios/AudioNotificationManager.swift +0 -135
  144. package/ios/AudioProcessingHelpers.swift +0 -743
  145. package/ios/AudioProcessor.swift +0 -1313
  146. package/ios/AudioStreamError.swift +0 -7
  147. package/ios/AudioStreamManager.swift +0 -1708
  148. package/ios/AudioStreamManagerDelegate.swift +0 -16
  149. package/ios/DataPoint.swift +0 -54
  150. package/ios/DecodingConfig.swift +0 -47
  151. package/ios/ExpoAudioStream.podspec +0 -27
  152. package/ios/ExpoAudioStreamModule.swift +0 -805
  153. package/ios/FFT.swift +0 -62
  154. package/ios/Features.swift +0 -95
  155. package/ios/Logger.swift +0 -7
  156. package/ios/NotificationExtension.swift +0 -15
  157. package/ios/RecordingResult.swift +0 -22
  158. package/ios/RecordingSettings.swift +0 -265
  159. package/ios/WaveformExtractor.swift +0 -105
  160. package/plugin/build/index.d.ts +0 -21
  161. package/plugin/build/index.js +0 -191
  162. package/plugin/src/index.ts +0 -278
  163. package/plugin/tsconfig.json +0 -10
  164. package/plugin/tsconfig.tsbuildinfo +0 -1
  165. package/src/AudioAnalysis/AudioAnalysis.types.ts +0 -202
  166. package/src/AudioAnalysis/extractAudioAnalysis.ts +0 -333
  167. package/src/AudioAnalysis/extractAudioData.ts +0 -6
  168. package/src/AudioAnalysis/extractMelSpectrogram.ts +0 -144
  169. package/src/AudioAnalysis/extractPreview.ts +0 -34
  170. package/src/AudioAnalysis/extractWaveform.ts +0 -22
  171. package/src/AudioRecorder.provider.tsx +0 -54
  172. package/src/ExpoAudioStream.native.ts +0 -6
  173. package/src/ExpoAudioStream.types.ts +0 -641
  174. package/src/ExpoAudioStream.web.ts +0 -359
  175. package/src/ExpoAudioStreamModule.ts +0 -967
  176. package/src/WebRecorder.web.ts +0 -580
  177. package/src/constants.ts +0 -18
  178. package/src/events.ts +0 -60
  179. package/src/trimAudio.ts +0 -90
  180. package/src/useAudioRecorder.tsx +0 -620
  181. package/src/utils/BlobFix.ts +0 -559
  182. package/src/utils/audioProcessing.ts +0 -205
  183. package/src/utils/concatenateBuffers.ts +0 -24
  184. package/src/utils/convertPCMToFloat32.ts +0 -170
  185. package/src/utils/encodingToBitDepth.ts +0 -18
  186. package/src/utils/getWavFileInfo.ts +0 -132
  187. package/src/utils/writeWavHeader.ts +0 -114
  188. package/src/workers/InlineFeaturesExtractor.web.tsx +0 -827
  189. package/src/workers/inlineAudioWebWorker.web.tsx +0 -156
@@ -1,805 +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
- /// - `mode`: Trim mode ('single', 'keep', or 'remove').
317
- /// - `startTimeMs`: Start time in milliseconds (for 'single' mode).
318
- /// - `endTimeMs`: End time in milliseconds (for 'single' mode).
319
- /// - `ranges`: Array of time ranges (for 'keep' and 'remove' modes).
320
- /// - `outputFileName`: Optional name for the output file.
321
- /// - `outputFormat`: Optional output format configuration.
322
- /// - `decodingOptions`: Optional decoding configuration.
323
- AsyncFunction("trimAudio") { (options: [String: Any], promise: Promise) in
324
- guard let fileUri = options["fileUri"] as? String,
325
- let url = URL(string: fileUri) else {
326
- promise.reject("INVALID_ARGUMENTS", "Invalid file URI provided")
327
- return
328
- }
329
-
330
- let mode = options["mode"] as? String ?? "single"
331
- let startTimeMs = options["startTimeMs"] as? Double
332
- let endTimeMs = options["endTimeMs"] as? Double
333
- let ranges = options["ranges"] as? [[String: Double]]
334
- let outputFileName = options["outputFileName"] as? String
335
- let outputFormat = options["outputFormat"] as? [String: Any]
336
- let decodingOptions = options["decodingOptions"] as? [String: Any]
337
-
338
- // Add detailed logging for filename and format options
339
- Logger.debug("Trim audio request:")
340
- Logger.debug("- Input file: \(fileUri)")
341
- Logger.debug("- Mode: \(mode)")
342
- Logger.debug("- Output filename: \(outputFileName ?? "not specified (will generate UUID)")")
343
- if let format = outputFormat?["format"] as? String {
344
- Logger.debug("- Output format: \(format)")
345
- } else {
346
- Logger.debug("- Output format: not specified (will use default)")
347
- }
348
-
349
- // Input validation based on mode
350
- switch mode {
351
- case "single":
352
- guard let start = startTimeMs, let end = endTimeMs else {
353
- promise.reject("INVALID_ARGUMENTS", "startTimeMs and endTimeMs required for 'single' mode")
354
- return
355
- }
356
- guard start >= 0, end > start else {
357
- promise.reject("INVALID_ARGUMENTS", "Invalid time range")
358
- return
359
- }
360
- case "keep", "remove":
361
- guard let rangesArray = ranges, !rangesArray.isEmpty else {
362
- promise.reject("INVALID_ARGUMENTS", "'ranges' array required for 'keep' or 'remove' mode")
363
- return
364
- }
365
- default:
366
- promise.reject("INVALID_MODE", "Mode must be 'single', 'keep', or 'remove'")
367
- return
368
- }
369
-
370
- DispatchQueue.global().async {
371
- do {
372
- let audioProcessor = try AudioProcessor(
373
- url: url,
374
- resolve: { result in promise.resolve(result) },
375
- reject: { code, message in promise.reject(code, message) }
376
- )
377
-
378
- let progressCallback: (Float, Int64, Int64) -> Void = { progress, bytesProcessed, totalBytes in
379
- self.sendEvent("TrimProgress", [
380
- "progress": progress,
381
- "bytesProcessed": bytesProcessed,
382
- "totalBytes": totalBytes
383
- ])
384
- }
385
-
386
- let startTime = CACurrentMediaTime()
387
- if let result = audioProcessor.trimAudio(
388
- mode: mode,
389
- startTimeMs: startTimeMs,
390
- endTimeMs: endTimeMs,
391
- ranges: ranges,
392
- outputFileName: outputFileName,
393
- outputFormat: outputFormat,
394
- decodingOptions: decodingOptions,
395
- progressCallback: progressCallback
396
- ) {
397
- let processingTimeMs = Int((CACurrentMediaTime() - startTime) * 1000)
398
- var resultDict = result.toDictionary()
399
- resultDict["processingInfo"] = ["durationMs": processingTimeMs]
400
-
401
- let uri = result.uri
402
- Logger.debug("Trim completed successfully in \(processingTimeMs)ms")
403
- Logger.debug("Output file URI: \(uri)")
404
-
405
- // Verify file exists
406
- let fileManager = FileManager.default
407
- if let url = URL(string: uri) {
408
- let exists = fileManager.fileExists(atPath: url.path)
409
- Logger.debug("File exists at path \(url.path): \(exists)")
410
-
411
- // Log filename details
412
- Logger.debug("Filename: \(url.lastPathComponent)")
413
- Logger.debug("File extension: \(url.pathExtension.lowercased())")
414
-
415
- // If format is AAC, ensure we're using the correct extension and MIME type
416
- if let format = outputFormat?["format"] as? String,
417
- format.lowercased() == "aac" {
418
-
419
- Logger.debug("AAC format detected - ensuring correct metadata")
420
-
421
- // For AAC format, ensure we're using the correct extension and MIME type
422
- if url.pathExtension.lowercased() == "m4a" {
423
- Logger.debug("File has correct m4a extension for AAC audio")
424
-
425
- // Just update the MIME type in the result to ensure correct playback
426
- if var compression = resultDict["compression"] as? [String: Any] {
427
- compression["mimeType"] = "audio/mp4"
428
- resultDict["compression"] = compression
429
- }
430
-
431
- resultDict["mimeType"] = "audio/mp4"
432
- resultDict["actualFormat"] = "m4a"
433
- } else {
434
- Logger.debug("Warning: AAC format should use .m4a extension, but found .\(url.pathExtension.lowercased())")
435
- }
436
- }
437
- }
438
-
439
- promise.resolve(resultDict)
440
- } else {
441
- Logger.debug("Failed to trim audio")
442
- promise.reject("TRIM_ERROR", "Failed to trim audio")
443
- }
444
- } catch {
445
- Logger.debug("Failed to initialize audio processor: \(error.localizedDescription)")
446
- promise.reject("PROCESSING_ERROR", "Failed to initialize audio processor: \(error.localizedDescription)")
447
- }
448
- }
449
- }
450
-
451
- /// Extracts raw PCM audio data from a file with time or byte range support
452
- /// - Parameters:
453
- /// - options: A dictionary containing:
454
- /// - `fileUri`: The URI of the audio file
455
- /// - `startTimeMs`: Optional start time in milliseconds
456
- /// - `endTimeMs`: Optional end time in milliseconds
457
- /// - `position`: Optional byte position
458
- /// - `length`: Optional byte length
459
- /// - `includeNormalizedData`: Boolean to include normalized audio data in [-1, 1] range
460
- /// - `includeWavHeader`: Boolean to include WAV header in the PCM data
461
- /// - `decodingOptions`: Decoding configuration
462
- /// - `includeBase64Data`: Boolean to include base64 encoded string representation of the audio data
463
- /// - `computeChecksum`: Boolean to compute and include CRC32 checksum of the PCM data
464
- AsyncFunction("extractAudioData") { (options: [String: Any], promise: Promise) in
465
- guard let fileUri = options["fileUri"] as? String,
466
- let url = URL(string: fileUri) else {
467
- promise.reject("INVALID_ARGUMENTS", "Invalid file URI provided")
468
- return
469
- }
470
-
471
- // Get time or byte range options
472
- let startTimeMs = options["startTimeMs"] as? Double
473
- let endTimeMs = options["endTimeMs"] as? Double
474
- let position = options["position"] as? Int
475
- let length = options["length"] as? Int
476
- let includeWavHeader = options["includeWavHeader"] as? Bool ?? false
477
-
478
- // Validate that we have either time range or byte range, but not both and not neither
479
- let hasTimeRange = startTimeMs != nil && endTimeMs != nil
480
- let hasByteRange = position != nil && length != nil
481
-
482
- guard hasTimeRange || hasByteRange else {
483
- promise.reject("INVALID_ARGUMENTS", "Must specify either time range (startTimeMs, endTimeMs) or byte range (position, length)")
484
- return
485
- }
486
-
487
- guard !(hasTimeRange && hasByteRange) else {
488
- promise.reject("INVALID_ARGUMENTS", "Cannot specify both time range and byte range")
489
- return
490
- }
491
-
492
- do {
493
- let audioFile = try AVAudioFile(forReading: url)
494
- let format = audioFile.processingFormat
495
- let sampleRate = format.sampleRate
496
- let channels = Int(format.channelCount)
497
- let bitDepth = audioFile.fileFormat.settings[AVLinearPCMBitDepthKey] as? Int ?? 16
498
-
499
- // Calculate frame positions
500
- let startFrame: AVAudioFramePosition
501
- let endFrame: AVAudioFramePosition
502
-
503
- if hasTimeRange {
504
- startFrame = AVAudioFramePosition(startTimeMs! * sampleRate / 1000.0)
505
- endFrame = AVAudioFramePosition(endTimeMs! * sampleRate / 1000.0)
506
- } else {
507
- // Convert byte position to frame position
508
- let bytesPerFrame = Int64(channels * (bitDepth / 8))
509
- startFrame = AVAudioFramePosition(position!) / bytesPerFrame
510
- endFrame = startFrame + (AVAudioFramePosition(length!) / bytesPerFrame)
511
- }
512
-
513
- // Validate frame range
514
- guard startFrame >= 0 && endFrame <= audioFile.length && startFrame < endFrame else {
515
- promise.reject("INVALID_RANGE", "Invalid range specified")
516
- return
517
- }
518
-
519
- let frameCount = AVAudioFrameCount(endFrame - startFrame)
520
-
521
- // Create decoding config that includes normalization preference
522
- var decodingOptions = options["decodingOptions"] as? [String: Any] ?? [:]
523
- let includeNormalizedData = options["includeNormalizedData"] as? Bool ?? false
524
-
525
- // Pass both options separately - normalizeAudio from decodingOptions, and includeNormalizedData as is
526
- let decodingConfig = DecodingConfig.fromDictionary(decodingOptions)
527
-
528
- let (pcmData, normalizedData, base64Data) = try extractRawAudioData(
529
- from: url,
530
- startFrame: startFrame,
531
- frameCount: frameCount,
532
- format: format,
533
- decodingConfig: decodingConfig,
534
- includeNormalizedData: includeNormalizedData,
535
- includeBase64Data: options["includeBase64Data"] as? Bool ?? false
536
- )
537
-
538
- var resultDict: [String: Any] = [:]
539
-
540
- if includeWavHeader {
541
- // Create WAV header and prepend it to the PCM data
542
- let wavData = createWavHeader(
543
- pcmData: pcmData,
544
- sampleRate: Int(sampleRate),
545
- channels: channels,
546
- bitDepth: bitDepth
547
- )
548
- resultDict["pcmData"] = wavData
549
- resultDict["hasWavHeader"] = true
550
- } else {
551
- resultDict["pcmData"] = pcmData
552
- resultDict["hasWavHeader"] = false
553
- }
554
-
555
- // Add the rest of the data
556
- resultDict["sampleRate"] = Int(sampleRate)
557
- resultDict["channels"] = channels
558
- resultDict["bitDepth"] = bitDepth
559
- resultDict["durationMs"] = Int(Double(frameCount) * 1000.0 / sampleRate)
560
- resultDict["format"] = "pcm_\(bitDepth)bit"
561
- resultDict["samples"] = Int(frameCount) * channels
562
-
563
- // Add normalized data if requested, regardless of normalization setting
564
- if includeNormalizedData {
565
- resultDict["normalizedData"] = normalizedData
566
- }
567
-
568
- // Add checksum if requested
569
- if options["computeChecksum"] as? Bool == true {
570
- let checksum = calculateCRC32(data: pcmData)
571
- resultDict["checksum"] = Int(checksum)
572
-
573
- Logger.debug("Computed CRC32 checksum: \(checksum)")
574
- }
575
-
576
- if let includeBase64Data = options["includeBase64Data"] as? Bool, includeBase64Data {
577
- resultDict["base64Data"] = base64Data
578
- }
579
-
580
- promise.resolve(resultDict)
581
-
582
- } catch {
583
- promise.reject("PROCESSING_ERROR", "Failed to process audio file: \(error.localizedDescription)")
584
- }
585
- }
586
-
587
- /// Extracts mel spectrogram data from a file.
588
- ///
589
- /// - Parameters:
590
- /// - options: A dictionary containing:
591
- /// - `fileUri`: The URI of the audio file.
592
- /// - `pointsPerSecond`: The number of data points to extract per second of audio.
593
- /// - promise: A promise to resolve with the extracted mel spectrogram data or reject with an error.
594
- /// - Returns: Promise to be resolved with mel spectrogram data.
595
- AsyncFunction("extractMelSpectrogram") { (options: [String: Any], promise: Promise) in
596
- // This is a placeholder implementation that will be fully implemented later
597
- // Currently, mel spectrogram extraction is only available on Android
598
- promise.reject(
599
- "UNSUPPORTED_PLATFORM",
600
- "Mel spectrogram extraction is currently only available on Android and is experimental"
601
- )
602
- }
603
- }
604
-
605
- func audioStreamManager(_ manager: AudioStreamManager, didReceiveInterruption info: [String: Any]) {
606
- // Convert iOS interruption events to match the TypeScript types
607
- var reason: String
608
- var isPaused: Bool = true
609
-
610
- if let type = info["type"] as? String {
611
- switch type {
612
- case "began":
613
- // Phone call or other audio session interruption began
614
- reason = "audioFocusLoss"
615
- case "ended":
616
- reason = "audioFocusGain"
617
- isPaused = false
618
- // Check if this was from a phone call
619
- if let wasSuspended = info["wasSuspended"] as? Bool, wasSuspended {
620
- reason = "phoneCallEnded"
621
- }
622
- default:
623
- return
624
- }
625
- } else if let specificReason = info["reason"] as? String {
626
- // Handle specific reasons that are already properly formatted
627
- reason = specificReason
628
- isPaused = info["isPaused"] as? Bool ?? true
629
- } else {
630
- return
631
- }
632
-
633
- // Send event in the correct format
634
- sendEvent(recordingInterruptedEvent, [
635
- "reason": reason,
636
- "isPaused": isPaused,
637
- "timestamp": Date().timeIntervalSince1970 * 1000
638
- ])
639
- }
640
-
641
- func audioStreamManager(_ manager: AudioStreamManager, didPauseRecording pauseTime: Date) {
642
- sendEvent(recordingInterruptedEvent, [
643
- "reason": "userPaused",
644
- "isPaused": true,
645
- "timestamp": pauseTime.timeIntervalSince1970 * 1000
646
- ])
647
- }
648
-
649
- func audioStreamManager(_ manager: AudioStreamManager, didResumeRecording resumeTime: Date) {
650
- sendEvent(recordingInterruptedEvent, [
651
- "reason": "userResumed",
652
- "isPaused": false,
653
- "timestamp": resumeTime.timeIntervalSince1970 * 1000
654
- ])
655
- }
656
-
657
- func audioStreamManager(_ manager: AudioStreamManager, didUpdateNotificationState isPaused: Bool) {
658
- sendEvent(recordingInterruptedEvent, [
659
- "reason": "notification",
660
- "isPaused": isPaused,
661
- "timestamp": Date().timeIntervalSince1970 * 1000
662
- ])
663
- }
664
-
665
- /// Handles the reception of audio data from the AudioStreamManager.
666
- ///
667
- /// - Parameters:
668
- /// - manager: The AudioStreamManager instance.
669
- /// - data: The received audio data.
670
- /// - recordingTime: The current recording time.
671
- /// - totalDataSize: The total size of the received audio data.
672
- func audioStreamManager(
673
- _ manager: AudioStreamManager,
674
- didReceiveAudioData data: Data,
675
- recordingTime: TimeInterval,
676
- totalDataSize: Int64,
677
- compressionInfo: [String: Any]?
678
- ) {
679
- var resultDict: [String: Any] = [
680
- "fileUri": manager.recordingFileURL?.absoluteString ?? "",
681
- "lastEmittedSize": totalDataSize,
682
- "encoded": data.base64EncodedString(),
683
- "deltaSize": data.count,
684
- "position": Int64(recordingTime * 1000),
685
- "mimeType": manager.mimeType,
686
- "totalSize": totalDataSize,
687
- "streamUuid": manager.recordingUUID?.uuidString ?? UUID().uuidString
688
- ]
689
-
690
- if let compressionInfo = compressionInfo {
691
- resultDict["compression"] = compressionInfo
692
- }
693
-
694
- sendEvent(audioDataEvent, resultDict)
695
- }
696
-
697
- private func requestNotificationPermissions() async -> Bool {
698
- do {
699
- let options: UNAuthorizationOptions = [.alert, .sound]
700
- return try await notificationCenter.requestAuthorization(options: options)
701
- } catch {
702
- Logger.debug("Failed to request notification permissions: \(error)")
703
- return false
704
- }
705
- }
706
-
707
- func audioStreamManager(_ manager: AudioStreamManager, didReceiveProcessingResult result: AudioAnalysisData?) {
708
- // Handle the processed audio data
709
- // Emit the processing result event to JavaScript
710
- let resultDict = result?.toDictionary() ?? [:]
711
- Logger.debug("emitting \(audioAnalysisEvent) event with \(resultDict)")
712
- sendEvent(audioAnalysisEvent, resultDict)
713
- }
714
-
715
- /// Checks microphone permission and calls the completion handler with the result.
716
- ///
717
- /// - Parameters:
718
- /// - completion: A completion handler that receives a boolean indicating whether the microphone permission was granted.
719
- private func checkMicrophonePermission(completion: @escaping (Bool) -> Void) {
720
- switch AVAudioSession.sharedInstance().recordPermission {
721
- case .granted:
722
- completion(true)
723
- case .denied:
724
- completion(false)
725
- case .undetermined:
726
- AVAudioSession.sharedInstance().requestRecordPermission { granted in
727
- DispatchQueue.main.async {
728
- completion(granted)
729
- }
730
- }
731
- @unknown default:
732
- completion(false)
733
- }
734
- }
735
-
736
- /// Clears all audio files stored in the document directory.
737
- private func clearAudioFiles() {
738
- let fileURLs = listAudioFiles() // This now returns full URLs as strings
739
- fileURLs.forEach { fileURLString in
740
- if let fileURL = URL(string: fileURLString) {
741
- do {
742
- try FileManager.default.removeItem(at: fileURL)
743
- print("Removed file at:", fileURL.path)
744
- } catch {
745
- print("Error removing file at \(fileURL.path):", error.localizedDescription)
746
- }
747
- } else {
748
- print("Invalid URL string: \(fileURLString)")
749
- }
750
- }
751
- }
752
-
753
- /// Extracts feature options from the provided options dictionary.
754
- ///
755
- /// - Parameters:
756
- /// - options: The options dictionary containing feature flags.
757
- /// - Returns: A dictionary with feature flags and their boolean values.
758
- private func extractFeatureOptions(from options: [String: Any]) -> [String: Bool] {
759
- return [
760
- "energy": options["energy"] as? Bool ?? false,
761
- "mfcc": options["mfcc"] as? Bool ?? false,
762
- "rms": options["rms"] as? Bool ?? false,
763
- "zcr": options["zcr"] as? Bool ?? false,
764
- "dB": options["dB"] as? Bool ?? false,
765
- "spectralCentroid": options["spectralCentroid"] as? Bool ?? false,
766
- "spectralFlatness": options["spectralFlatness"] as? Bool ?? false,
767
- "spectralRollOff": options["spectralRollOff"] as? Bool ?? false,
768
- "spectralBandwidth": options["spectralBandwidth"] as? Bool ?? false,
769
- "chromagram": options["chromagram"] as? Bool ?? false,
770
- "tempo": options["tempo"] as? Bool ?? false,
771
- "hnr": options["hnr"] as? Bool ?? false,
772
- "melSpectrogram": options["melSpectrogram"] as? Bool ?? false,
773
- "spectralContrast": options["spectralContrast"] as? Bool ?? false,
774
- "tonnetz": options["tonnetz"] as? Bool ?? false,
775
- "pitch": options["pitch"] as? Bool ?? false,
776
- "crc32": options["crc32"] as? Bool ?? false
777
- ]
778
- }
779
-
780
- /// Lists all audio files stored in the document directory.
781
- ///
782
- /// - Returns: An array of file URIs as strings.
783
- func listAudioFiles() -> [String] {
784
- guard let documentDirectory = try? FileManager.default.url(for: .documentDirectory, in: .userDomainMask, appropriateFor: nil, create: false) else {
785
- print("Failed to access document directory.")
786
- return []
787
- }
788
-
789
- do {
790
- let files = try FileManager.default.contentsOfDirectory(at: documentDirectory, includingPropertiesForKeys: nil)
791
- let audioFiles = files.filter { $0.pathExtension == "wav" }.map { $0.absoluteString }
792
- return audioFiles
793
- } catch {
794
- print("Error listing audio files:", error.localizedDescription)
795
- return []
796
- }
797
- }
798
-
799
- func audioStreamManager(_ manager: AudioStreamManager, didFailWithError error: String) {
800
- // Send error event to JavaScript
801
- sendEvent("error", [
802
- "message": error
803
- ])
804
- }
805
- }