Skip to content

Instantly share code, notes, and snippets.

@algal
Created June 26, 2025 00:57
Show Gist options
  • Save algal/163a7874f46e8cbb7347013b052f02cc to your computer and use it in GitHub Desktop.
Save algal/163a7874f46e8cbb7347013b052f02cc to your computer and use it in GitHub Desktop.
Recorder #swift
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