@siteed/expo-audio-stream 2.1.0 → 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.
- package/README.md +40 -222
- package/build/index.d.ts +11 -15
- package/build/index.js +44 -14
- package/package.json +49 -110
- package/src/index.ts +18 -32
- package/CHANGELOG.md +0 -206
- package/android/build.gradle +0 -105
- package/android/src/main/AndroidManifest.xml +0 -27
- package/android/src/main/java/net/siteed/audiostream/AudioAnalysisData.kt +0 -166
- package/android/src/main/java/net/siteed/audiostream/AudioDataEncoder.kt +0 -9
- package/android/src/main/java/net/siteed/audiostream/AudioFileHandler.kt +0 -131
- package/android/src/main/java/net/siteed/audiostream/AudioFormatUtils.kt +0 -103
- package/android/src/main/java/net/siteed/audiostream/AudioNotificationsManager.kt +0 -435
- package/android/src/main/java/net/siteed/audiostream/AudioProcessor.kt +0 -2235
- package/android/src/main/java/net/siteed/audiostream/AudioRecorderManager.kt +0 -1437
- package/android/src/main/java/net/siteed/audiostream/AudioRecordingService.kt +0 -152
- package/android/src/main/java/net/siteed/audiostream/AudioTrimmer.kt +0 -1099
- package/android/src/main/java/net/siteed/audiostream/Constants.kt +0 -21
- package/android/src/main/java/net/siteed/audiostream/EventSender.kt +0 -7
- package/android/src/main/java/net/siteed/audiostream/ExpoAudioStreamModule.kt +0 -739
- package/android/src/main/java/net/siteed/audiostream/FFT.kt +0 -99
- package/android/src/main/java/net/siteed/audiostream/Features.kt +0 -98
- package/android/src/main/java/net/siteed/audiostream/NotificationConfig.kt +0 -70
- package/android/src/main/java/net/siteed/audiostream/PermissionUtils.kt +0 -59
- package/android/src/main/java/net/siteed/audiostream/RecordingActionReceiver.kt +0 -59
- package/android/src/main/java/net/siteed/audiostream/RecordingConfig.kt +0 -205
- package/android/src/main/java/net/siteed/audiostream/WaveformConfig.kt +0 -19
- package/android/src/main/java/net/siteed/audiostream/WaveformRenderer.kt +0 -159
- package/android/src/main/res/drawable/ic_default_action_icon.xml +0 -16
- package/android/src/main/res/drawable/ic_microphone.xml +0 -13
- package/android/src/main/res/drawable/ic_pause.xml +0 -10
- package/android/src/main/res/drawable/ic_play.xml +0 -10
- package/android/src/main/res/drawable/ic_stop.xml +0 -10
- package/android/src/main/res/layout/notification_recording.xml +0 -37
- package/android/src/main/test/java/net/siteed/audiostream/AudioProcessorTest.kt +0 -56
- package/app.plugin.js +0 -1
- package/build/AudioAnalysis/AudioAnalysis.types.d.ts +0 -179
- package/build/AudioAnalysis/AudioAnalysis.types.d.ts.map +0 -1
- package/build/AudioAnalysis/AudioAnalysis.types.js +0 -3
- package/build/AudioAnalysis/AudioAnalysis.types.js.map +0 -1
- package/build/AudioAnalysis/extractAudioAnalysis.d.ts +0 -68
- package/build/AudioAnalysis/extractAudioAnalysis.d.ts.map +0 -1
- package/build/AudioAnalysis/extractAudioAnalysis.js +0 -203
- package/build/AudioAnalysis/extractAudioAnalysis.js.map +0 -1
- package/build/AudioAnalysis/extractAudioData.d.ts +0 -3
- package/build/AudioAnalysis/extractAudioData.d.ts.map +0 -1
- package/build/AudioAnalysis/extractAudioData.js +0 -5
- package/build/AudioAnalysis/extractAudioData.js.map +0 -1
- package/build/AudioAnalysis/extractMelSpectrogram.d.ts +0 -14
- package/build/AudioAnalysis/extractMelSpectrogram.d.ts.map +0 -1
- package/build/AudioAnalysis/extractMelSpectrogram.js +0 -85
- package/build/AudioAnalysis/extractMelSpectrogram.js.map +0 -1
- package/build/AudioAnalysis/extractPreview.d.ts +0 -11
- package/build/AudioAnalysis/extractPreview.d.ts.map +0 -1
- package/build/AudioAnalysis/extractPreview.js +0 -25
- package/build/AudioAnalysis/extractPreview.js.map +0 -1
- package/build/AudioAnalysis/extractWaveform.d.ts +0 -8
- package/build/AudioAnalysis/extractWaveform.d.ts.map +0 -1
- package/build/AudioAnalysis/extractWaveform.js +0 -11
- package/build/AudioAnalysis/extractWaveform.js.map +0 -1
- package/build/AudioRecorder.provider.d.ts +0 -11
- package/build/AudioRecorder.provider.d.ts.map +0 -1
- package/build/AudioRecorder.provider.js +0 -37
- package/build/AudioRecorder.provider.js.map +0 -1
- package/build/ExpoAudioStream.native.d.ts +0 -3
- package/build/ExpoAudioStream.native.d.ts.map +0 -1
- package/build/ExpoAudioStream.native.js +0 -6
- package/build/ExpoAudioStream.native.js.map +0 -1
- package/build/ExpoAudioStream.types.d.ts +0 -532
- package/build/ExpoAudioStream.types.d.ts.map +0 -1
- package/build/ExpoAudioStream.types.js +0 -2
- package/build/ExpoAudioStream.types.js.map +0 -1
- package/build/ExpoAudioStream.web.d.ts +0 -59
- package/build/ExpoAudioStream.web.d.ts.map +0 -1
- package/build/ExpoAudioStream.web.js +0 -285
- package/build/ExpoAudioStream.web.js.map +0 -1
- package/build/ExpoAudioStreamModule.d.ts +0 -3
- package/build/ExpoAudioStreamModule.d.ts.map +0 -1
- package/build/ExpoAudioStreamModule.js +0 -693
- package/build/ExpoAudioStreamModule.js.map +0 -1
- package/build/WebRecorder.web.d.ts +0 -119
- package/build/WebRecorder.web.d.ts.map +0 -1
- package/build/WebRecorder.web.js +0 -436
- package/build/WebRecorder.web.js.map +0 -1
- package/build/constants.d.ts +0 -11
- package/build/constants.d.ts.map +0 -1
- package/build/constants.js +0 -14
- package/build/constants.js.map +0 -1
- package/build/events.d.ts +0 -26
- package/build/events.d.ts.map +0 -1
- package/build/events.js +0 -21
- package/build/events.js.map +0 -1
- package/build/index.d.ts.map +0 -1
- package/build/index.js.map +0 -1
- package/build/trimAudio.d.ts +0 -25
- package/build/trimAudio.d.ts.map +0 -1
- package/build/trimAudio.js +0 -67
- package/build/trimAudio.js.map +0 -1
- package/build/useAudioRecorder.d.ts +0 -21
- package/build/useAudioRecorder.d.ts.map +0 -1
- package/build/useAudioRecorder.js +0 -427
- package/build/useAudioRecorder.js.map +0 -1
- package/build/utils/BlobFix.d.ts +0 -9
- package/build/utils/BlobFix.d.ts.map +0 -1
- package/build/utils/BlobFix.js +0 -498
- package/build/utils/BlobFix.js.map +0 -1
- package/build/utils/audioProcessing.d.ts +0 -24
- package/build/utils/audioProcessing.d.ts.map +0 -1
- package/build/utils/audioProcessing.js +0 -133
- package/build/utils/audioProcessing.js.map +0 -1
- package/build/utils/concatenateBuffers.d.ts +0 -8
- package/build/utils/concatenateBuffers.d.ts.map +0 -1
- package/build/utils/concatenateBuffers.js +0 -21
- package/build/utils/concatenateBuffers.js.map +0 -1
- package/build/utils/convertPCMToFloat32.d.ts +0 -13
- package/build/utils/convertPCMToFloat32.d.ts.map +0 -1
- package/build/utils/convertPCMToFloat32.js +0 -120
- package/build/utils/convertPCMToFloat32.js.map +0 -1
- package/build/utils/encodingToBitDepth.d.ts +0 -5
- package/build/utils/encodingToBitDepth.d.ts.map +0 -1
- package/build/utils/encodingToBitDepth.js +0 -13
- package/build/utils/encodingToBitDepth.js.map +0 -1
- package/build/utils/getWavFileInfo.d.ts +0 -26
- package/build/utils/getWavFileInfo.d.ts.map +0 -1
- package/build/utils/getWavFileInfo.js +0 -92
- package/build/utils/getWavFileInfo.js.map +0 -1
- package/build/utils/writeWavHeader.d.ts +0 -49
- package/build/utils/writeWavHeader.d.ts.map +0 -1
- package/build/utils/writeWavHeader.js +0 -91
- package/build/utils/writeWavHeader.js.map +0 -1
- package/build/workers/InlineFeaturesExtractor.web.d.ts +0 -2
- package/build/workers/InlineFeaturesExtractor.web.d.ts.map +0 -1
- package/build/workers/InlineFeaturesExtractor.web.js +0 -828
- package/build/workers/InlineFeaturesExtractor.web.js.map +0 -1
- package/build/workers/inlineAudioWebWorker.web.d.ts +0 -2
- package/build/workers/inlineAudioWebWorker.web.d.ts.map +0 -1
- package/build/workers/inlineAudioWebWorker.web.js +0 -157
- package/build/workers/inlineAudioWebWorker.web.js.map +0 -1
- package/expo-module.config.json +0 -9
- package/ios/AudioAnalysisData.swift +0 -74
- package/ios/AudioNotificationManager.swift +0 -135
- package/ios/AudioProcessingHelpers.swift +0 -743
- package/ios/AudioProcessor.swift +0 -1313
- package/ios/AudioStreamError.swift +0 -7
- package/ios/AudioStreamManager.swift +0 -1708
- package/ios/AudioStreamManagerDelegate.swift +0 -16
- package/ios/DataPoint.swift +0 -54
- package/ios/DecodingConfig.swift +0 -47
- package/ios/ExpoAudioStream.podspec +0 -27
- package/ios/ExpoAudioStreamModule.swift +0 -805
- package/ios/FFT.swift +0 -62
- package/ios/Features.swift +0 -95
- package/ios/Logger.swift +0 -7
- package/ios/NotificationExtension.swift +0 -15
- package/ios/RecordingResult.swift +0 -22
- package/ios/RecordingSettings.swift +0 -265
- package/ios/WaveformExtractor.swift +0 -105
- package/plugin/build/index.d.ts +0 -21
- package/plugin/build/index.js +0 -191
- package/plugin/src/index.ts +0 -278
- package/plugin/tsconfig.json +0 -10
- package/plugin/tsconfig.tsbuildinfo +0 -1
- package/src/AudioAnalysis/AudioAnalysis.types.ts +0 -202
- package/src/AudioAnalysis/extractAudioAnalysis.ts +0 -333
- package/src/AudioAnalysis/extractAudioData.ts +0 -6
- package/src/AudioAnalysis/extractMelSpectrogram.ts +0 -144
- package/src/AudioAnalysis/extractPreview.ts +0 -34
- package/src/AudioAnalysis/extractWaveform.ts +0 -22
- package/src/AudioRecorder.provider.tsx +0 -54
- package/src/ExpoAudioStream.native.ts +0 -6
- package/src/ExpoAudioStream.types.ts +0 -641
- package/src/ExpoAudioStream.web.ts +0 -359
- package/src/ExpoAudioStreamModule.ts +0 -967
- package/src/WebRecorder.web.ts +0 -580
- package/src/constants.ts +0 -18
- package/src/events.ts +0 -60
- package/src/trimAudio.ts +0 -90
- package/src/useAudioRecorder.tsx +0 -620
- package/src/utils/BlobFix.ts +0 -559
- package/src/utils/audioProcessing.ts +0 -205
- package/src/utils/concatenateBuffers.ts +0 -24
- package/src/utils/convertPCMToFloat32.ts +0 -170
- package/src/utils/encodingToBitDepth.ts +0 -18
- package/src/utils/getWavFileInfo.ts +0 -132
- package/src/utils/writeWavHeader.ts +0 -114
- package/src/workers/InlineFeaturesExtractor.web.tsx +0 -827
- 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
|
-
}
|