feat:mac os
This commit is contained in:
1 parent
136bf7cfa3
commit
3400e2b465
18 files changed
+1129
No files matched your search
@@ -0,0 +1,86 @@
|
||||
import AVFoundation
|
||||
import Foundation
|
||||
|
||||
enum RecordingError: LocalizedError {
|
||||
case microphoneDenied
|
||||
case couldNotStart
|
||||
|
||||
var errorDescription: String? {
|
||||
switch self {
|
||||
case .microphoneDenied: "Microphone access is off. Enable it in System Settings."
|
||||
case .couldNotStart: "The microphone could not start recording."
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@MainActor
|
||||
final class AudioRecorder: NSObject, AVAudioRecorderDelegate {
|
||||
private var recorder: AVAudioRecorder?
|
||||
private var meterTimer: Timer?
|
||||
private(set) var fileURL: URL?
|
||||
var onLevel: ((Double) -> Void)?
|
||||
|
||||
func start() async throws {
|
||||
guard await Self.requestPermission() else { throw RecordingError.microphoneDenied }
|
||||
let fileURL = FileManager.default.temporaryDirectory
|
||||
.appendingPathComponent("transui-\(UUID().uuidString).wav")
|
||||
self.fileURL = fileURL
|
||||
let settings: [String: Any] = [
|
||||
AVFormatIDKey: Int(kAudioFormatLinearPCM),
|
||||
AVSampleRateKey: 16_000,
|
||||
AVNumberOfChannelsKey: 1,
|
||||
AVLinearPCMBitDepthKey: 16,
|
||||
AVLinearPCMIsFloatKey: false,
|
||||
AVLinearPCMIsBigEndianKey: false,
|
||||
]
|
||||
let recorder = try AVAudioRecorder(url: fileURL, settings: settings)
|
||||
recorder.delegate = self
|
||||
recorder.isMeteringEnabled = true
|
||||
guard recorder.prepareToRecord(), recorder.record() else {
|
||||
throw RecordingError.couldNotStart
|
||||
}
|
||||
self.recorder = recorder
|
||||
meterTimer = Timer.scheduledTimer(withTimeInterval: 0.08, repeats: true) { [weak self] _ in
|
||||
Task { @MainActor in self?.updateLevel() }
|
||||
}
|
||||
}
|
||||
|
||||
func stop() -> URL? {
|
||||
meterTimer?.invalidate()
|
||||
meterTimer = nil
|
||||
recorder?.stop()
|
||||
recorder = nil
|
||||
onLevel?(0)
|
||||
let url = fileURL
|
||||
fileURL = nil
|
||||
return url
|
||||
}
|
||||
|
||||
func cancel() {
|
||||
let url = stop()
|
||||
if let url { try? FileManager.default.removeItem(at: url) }
|
||||
fileURL = nil
|
||||
}
|
||||
|
||||
private func updateLevel() {
|
||||
guard let recorder else { return }
|
||||
recorder.updateMeters()
|
||||
let decibels = Double(recorder.averagePower(forChannel: 0))
|
||||
let normalized = max(0, min(1, pow(10, decibels / 32)))
|
||||
onLevel?(normalized)
|
||||
}
|
||||
|
||||
private static func requestPermission() async -> Bool {
|
||||
switch AVCaptureDevice.authorizationStatus(for: .audio) {
|
||||
case .authorized: true
|
||||
case .denied, .restricted: false
|
||||
case .notDetermined:
|
||||
await withCheckedContinuation { continuation in
|
||||
AVCaptureDevice.requestAccess(for: .audio) { allowed in
|
||||
continuation.resume(returning: allowed)
|
||||
}
|
||||
}
|
||||
@unknown default: false
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in new issue
Block a user