Created
June 26, 2025 00:57
-
-
Save algal/163a7874f46e8cbb7347013b052f02cc to your computer and use it in GitHub Desktop.
Recorder #swift
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
import AVFoundation | |
import Foundation | |
import os | |
// Microphone permission (supports macOS, fallback) | |
func requestMicrophonePermission() async -> Bool { | |
await withCheckedContinuation { continuation in | |
AVCaptureDevice.requestAccess(for: .audio) { granted in | |
continuation.resume(returning: granted) | |
} | |
} | |
} | |
actor Recorder { | |
private var audioRecorder: AVAudioRecorder? | |
private var fileURL: URL? | |
private var isRecording: Bool = false | |
private var lastRecordedDuration: Double = 0 | |
private let logger = Logger(subsystem: Bundle.main.bundleIdentifier ?? "Recorder", category: "Recorder") | |
enum RecorderError: Error { | |
case permissionDenied | |
case couldNotCreateRecorder(String) | |
} | |
init() async throws { | |
let granted = await requestMicrophonePermission() | |
if granted { | |
logger.info("Microphone permission granted") | |
} else { | |
logger.error("Microphone permission not granted") | |
throw RecorderError.permissionDenied | |
} | |
} | |
func startRecording() async throws { | |
let tempDirectory = FileManager.default.temporaryDirectory | |
let file = tempDirectory.appendingPathComponent("recording_\(UUID().uuidString).m4a") | |
self.fileURL = file | |
let settings: [String: Any] = [ | |
AVFormatIDKey: Int(kAudioFormatMPEG4AAC), | |
AVSampleRateKey: 44100, | |
AVNumberOfChannelsKey: 1, | |
AVEncoderAudioQualityKey: AVAudioQuality.high.rawValue | |
] | |
do { | |
let recorder = try AVAudioRecorder(url: file, settings: settings) | |
recorder.isMeteringEnabled = true | |
if recorder.record() { | |
self.audioRecorder = recorder | |
self.isRecording = true | |
logger.info("Started AVAudioRecorder at \(file.path)") | |
} else { | |
self.audioRecorder = nil | |
self.isRecording = false | |
throw RecorderError.couldNotCreateRecorder("Failed to start recording") | |
} | |
} catch { | |
logger.error("Failed to start AVAudioRecorder: \(error.localizedDescription)") | |
throw RecorderError.couldNotCreateRecorder(error.localizedDescription) | |
} | |
} | |
func stopRecording() async -> URL? { | |
guard let recorder = self.audioRecorder, self.isRecording else { | |
logger.error("stopRecording called but not recording") | |
return nil | |
} | |
// Capture duration BEFORE stopping | |
self.lastRecordedDuration = recorder.currentTime | |
recorder.stop() | |
let url = self.fileURL | |
self.audioRecorder = nil | |
self.isRecording = false | |
logger.info("Stopped recording, duration: \(self.lastRecordedDuration)s, file at \(url?.path ?? "nil")") | |
return url | |
} | |
func recordedDurationSeconds() async -> Double { | |
if let recorder = self.audioRecorder, self.isRecording { | |
return recorder.currentTime | |
} | |
return lastRecordedDuration | |
} | |
func getAudioLevel() async -> Float { | |
guard let recorder = self.audioRecorder, self.isRecording else { | |
return 0.0 | |
} | |
recorder.updateMeters() | |
let power = recorder.averagePower(forChannel: 0) | |
let level = pow(10, power / 20) | |
return max(0, min(level, 1.0)) | |
} | |
} |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment