feat: add AudioRecorder (16 kHz mono) and record probe
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,67 @@
|
|||||||
|
import AVFoundation
|
||||||
|
|
||||||
|
public final class AudioRecorder: AudioRecording {
|
||||||
|
public enum RecorderError: Error, LocalizedError {
|
||||||
|
case converterUnavailable
|
||||||
|
|
||||||
|
public var errorDescription: String? {
|
||||||
|
"Kon de microfoon niet omzetten naar 16 kHz mono."
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private let engine = AVAudioEngine()
|
||||||
|
private let lock = NSLock()
|
||||||
|
private var buffer: [Float] = []
|
||||||
|
private let targetFormat = AVAudioFormat(
|
||||||
|
commonFormat: .pcmFormatFloat32,
|
||||||
|
sampleRate: 16_000,
|
||||||
|
channels: 1,
|
||||||
|
interleaved: false
|
||||||
|
)!
|
||||||
|
|
||||||
|
public init() {}
|
||||||
|
|
||||||
|
public func start() throws {
|
||||||
|
lock.withLock { buffer.removeAll(keepingCapacity: true) }
|
||||||
|
|
||||||
|
let input = engine.inputNode
|
||||||
|
let inputFormat = input.outputFormat(forBus: 0)
|
||||||
|
guard let converter = AVAudioConverter(from: inputFormat, to: targetFormat) else {
|
||||||
|
throw RecorderError.converterUnavailable
|
||||||
|
}
|
||||||
|
let ratio = targetFormat.sampleRate / inputFormat.sampleRate
|
||||||
|
|
||||||
|
input.installTap(onBus: 0, bufferSize: 4096, format: inputFormat) { [weak self] pcm, _ in
|
||||||
|
guard let self else { return }
|
||||||
|
let capacity = AVAudioFrameCount(Double(pcm.frameLength) * ratio) + 1
|
||||||
|
guard let out = AVAudioPCMBuffer(pcmFormat: self.targetFormat, frameCapacity: capacity) else { return }
|
||||||
|
|
||||||
|
var consumed = false
|
||||||
|
var error: NSError?
|
||||||
|
converter.convert(to: out, error: &error) { _, status in
|
||||||
|
if consumed {
|
||||||
|
status.pointee = .noDataNow
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
consumed = true
|
||||||
|
status.pointee = .haveData
|
||||||
|
return pcm
|
||||||
|
}
|
||||||
|
guard error == nil, let channel = out.floatChannelData?[0] else { return }
|
||||||
|
let samples = Array(UnsafeBufferPointer(start: channel, count: Int(out.frameLength)))
|
||||||
|
self.lock.withLock { self.buffer.append(contentsOf: samples) }
|
||||||
|
}
|
||||||
|
engine.prepare()
|
||||||
|
try engine.start()
|
||||||
|
}
|
||||||
|
|
||||||
|
public func stop() -> [Float] {
|
||||||
|
engine.inputNode.removeTap(onBus: 0)
|
||||||
|
engine.stop()
|
||||||
|
return lock.withLock {
|
||||||
|
let samples = buffer
|
||||||
|
buffer = []
|
||||||
|
return samples
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,24 @@
|
|||||||
|
import AVFoundation
|
||||||
|
import SstCore
|
||||||
|
|
||||||
|
/// Records `seconds` of microphone audio; returns nil if access is denied or recording fails.
|
||||||
|
func recordSamples(seconds: Double) async -> [Float]? {
|
||||||
|
guard await AVCaptureDevice.requestAccess(for: .audio) else {
|
||||||
|
print("Microfoon-toegang geweigerd (Systeeminstellingen → Privacy → Microfoon).")
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
let recorder = AudioRecorder()
|
||||||
|
do { try recorder.start() } catch {
|
||||||
|
print("start mislukt:", error.localizedDescription)
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
print("Neem \(Int(seconds)) s op — praat nu")
|
||||||
|
try? await Task.sleep(for: .seconds(seconds))
|
||||||
|
return recorder.stop()
|
||||||
|
}
|
||||||
|
|
||||||
|
func runRecordProbe() async {
|
||||||
|
guard let samples = await recordSamples(seconds: 3) else { return }
|
||||||
|
let peak = samples.map { abs($0) }.max() ?? 0
|
||||||
|
print("samples: \(samples.count) (verwacht ≈ 48000), piek: \(peak)")
|
||||||
|
}
|
||||||
@@ -4,6 +4,8 @@ let command = CommandLine.arguments.dropFirst().first ?? "help"
|
|||||||
switch command {
|
switch command {
|
||||||
case "hotkey":
|
case "hotkey":
|
||||||
runHotkeyProbe()
|
runHotkeyProbe()
|
||||||
|
case "record":
|
||||||
|
await runRecordProbe()
|
||||||
default:
|
default:
|
||||||
print("usage: sst-probe hotkey")
|
print("usage: sst-probe hotkey|record")
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user