Files
transui/macos/Sources/Transui/AudioRecorder.swift
T
jourintownandintelligence 32e7f32e0a
Android / build-android (release) Successful in 2m18s
Build / build-amd64 (release) Successful in 34s
Build / build-arm64 (release) Canceled after 0s
Feat macos (#2)
---------

Co-authored-by: intelligence <intelligence@debian.local>
Co-authored-by: jourintown <jourintown@proton.me>
Reviewed-on: #2
2026-08-29 20:30:30 +02:00

87 lines
2.5 KiB
Swift

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
}
}
}