Phase 1: dual-track audio capture → mixed-mono 16 kHz WAV + mic VAD

AudioRecorder captures system audio (ScreenCaptureKit) + mic (AVAudioEngine) on a
single serial ioQueue, one shared monotonic t0, time-driven writers (pad gaps /
trim overlaps) so tracks stay aligned, and an energy mic-VAD for 'self' spans.
AudioMixer sums the aligned tracks into mixed_mono_16k.wav. SessionController
drives a serialized start/stop state machine, writes the session folder +
self_vad.json, exposes live level meters, and finalizes on quit.

Hardening from review: ioQueue single-domain (no races), stop() never hangs
(mic-first teardown + bounded stopCapture), layout-agnostic mic deep-copy,
discard-only video output to keep SCStream alive, VAD lockstep on committed
frames, stable signing team in project.yml, single-instance enforcement.
This commit is contained in:
Grant Gilliam
2026-06-05 21:30:11 -05:00
parent b2ae3a62b9
commit fd7e1a5907
12 changed files with 1018 additions and 10 deletions
+25
View File
@@ -6,5 +6,30 @@ final class AppDelegate: NSObject, NSApplicationDelegate {
// LSUIElement in Info.plist already enforces this; set it explicitly too
// so behavior is unambiguous regardless of how the app is launched.
NSApp.setActivationPolicy(.accessory)
terminateOtherInstances()
}
/// Single-instance: a fresh launch (e.g. each Xcode R) terminates any older
/// copies so you never end up with two menu-bar icons.
private func terminateOtherInstances() {
guard let bundleID = Bundle.main.bundleIdentifier else { return }
let me = NSRunningApplication.current.processIdentifier
for app in NSRunningApplication.runningApplications(withBundleIdentifier: bundleID)
where app.processIdentifier != me {
app.terminate()
}
}
/// If a recording is in progress when the user quits, finalize it (flush WAV
/// headers) before the process exits, so the session isn't corrupted.
func applicationShouldTerminate(_ sender: NSApplication) -> NSApplication.TerminateReply {
guard let controller = SessionController.shared, controller.isBusy else {
return .terminateNow
}
Task { @MainActor in
await controller.prepareForTermination()
NSApp.reply(toApplicationShouldTerminate: true)
}
return .terminateLater
}
}