blitztext-app-linux/BlitztextMac/Services/AudioRecorder.swift
2026-05-23 07:28:46 +02:00

99 lines
3.0 KiB
Swift

import AVFoundation
import Observation
@Observable
final class AudioRecorder: NSObject, AVAudioRecorderDelegate {
var isRecording = false
var recordingURL: URL?
var errorMessage: String?
var audioLevel: Float = 0
var lastRecordingDuration: TimeInterval = 0
private var audioRecorder: AVAudioRecorder?
private var levelTimer: Timer?
private var currentFileURL: URL?
private func makeRecordingURL() -> URL {
FileManager.default.temporaryDirectory
.appendingPathComponent("blitztext-\(UUID().uuidString).m4a")
}
func startRecording() {
errorMessage = nil
lastRecordingDuration = 0
recordingURL = nil
if let currentFileURL {
try? FileManager.default.removeItem(at: currentFileURL)
}
let settings: [String: Any] = [
AVFormatIDKey: Int(kAudioFormatMPEG4AAC),
AVSampleRateKey: 16000,
AVNumberOfChannelsKey: 1,
AVEncoderAudioQualityKey: AVAudioQuality.high.rawValue,
]
do {
let fileURL = makeRecordingURL()
currentFileURL = fileURL
audioRecorder = try AVAudioRecorder(url: fileURL, settings: settings)
audioRecorder?.delegate = self
audioRecorder?.isMeteringEnabled = true
audioRecorder?.record()
isRecording = true
startMetering()
} catch {
currentFileURL = nil
errorMessage = "Aufnahme konnte nicht gestartet werden: \(error.localizedDescription)"
}
}
func stopRecording() {
stopMetering()
lastRecordingDuration = audioRecorder?.currentTime ?? 0
audioRecorder?.stop()
isRecording = false
recordingURL = currentFileURL
currentFileURL = nil
audioRecorder = nil
audioLevel = 0
}
func discardRecording() {
if let recordingURL {
try? FileManager.default.removeItem(at: recordingURL)
self.recordingURL = nil
}
if let currentFileURL {
try? FileManager.default.removeItem(at: currentFileURL)
self.currentFileURL = nil
}
}
private func startMetering() {
levelTimer = Timer.scheduledTimer(withTimeInterval: 0.05, repeats: true) { [weak self] _ in
guard let self else { return }
self.audioRecorder?.updateMeters()
let power = self.audioRecorder?.averagePower(forChannel: 0) ?? -160
let normalized = max(0, min(1, (power + 50) / 50))
self.audioLevel = normalized
}
}
private func stopMetering() {
levelTimer?.invalidate()
levelTimer = nil
}
// MARK: - AVAudioRecorderDelegate
nonisolated func audioRecorderDidFinishRecording(_ recorder: AVAudioRecorder, successfully flag: Bool) {
if !flag {
Task { @MainActor in
self.errorMessage = "Aufnahme fehlgeschlagen"
}
}
}
}