Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
24 changes: 21 additions & 3 deletions Sources/quill/Audio/MicRecorder.swift
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import AVFoundation
import Foundation
import os.lock

/// Records the default input device to a file via AVAudioEngine, encoding AAC
/// mono. Buffers stream straight to disk — nothing is held in memory, so
Expand Down Expand Up @@ -28,15 +29,32 @@ final class MicRecorder: @unchecked Sendable {
}

private var engine = AVAudioEngine()
private var file: AVAudioFile?
private var url: URL?
private(set) var isRecording = false

// Thread-safe shared state: accessed from both the main thread and the
// audio-tap callback (background audio thread) without further sync.
private struct LockedState {
var file: AVAudioFile?
var firstBufferAt: Date?
}
private let state = OSAllocatedUnfairLock(initialState: LockedState())

private var file: AVAudioFile? {
get { state.withLock { $0.file } }
set { state.withLock { $0.file = newValue } }
}

/// Wall-clock time of the first captured buffer — the track's true start,
/// used to offset-align the two tracks' transcript timestamps.
private(set) var firstBufferAt: Date?
private(set) var firstBufferAt: Date? {
get { state.withLock { $0.firstBufferAt } }
set { state.withLock { $0.firstBufferAt = newValue } }
}

// Liveness check state (voice-processing path only). Written from the tap
// callback, read on main when deciding to fall back.
// callback, read on main when deciding to fall back. The dispatch to main
// in fallBackToRaw creates a happens-before, so these need no lock.
private var livenessFrames = 0
private var livenessPeak: Float = 0
private var livenessSettled = false
Expand Down
21 changes: 19 additions & 2 deletions Sources/quill/Audio/SystemAudioRecorder.swift
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import AVFoundation
import CoreAudio
import Foundation
import os.lock

/// Records all system audio output to a file via a Core Audio process tap
/// (macOS 14.2+). No virtual device, no kernel extension — the tap mixes every
Expand Down Expand Up @@ -32,12 +33,28 @@ final class SystemAudioRecorder {
private var tapID = AudioObjectID(kAudioObjectUnknown)
private var aggregateID = AudioObjectID(kAudioObjectUnknown)
private var procID: AudioDeviceIOProcID?
private var file: AVAudioFile?
private let queue = DispatchQueue(label: "com.digimata.quill.system-tap")
private(set) var isRecording = false

// Thread-safe shared state: accessed from both the main thread and the
// IOProc callback (background serial queue) without further sync.
private struct LockedState {
var file: AVAudioFile?
var firstBufferAt: Date?
}
private let state = OSAllocatedUnfairLock(initialState: LockedState())

private var file: AVAudioFile? {
get { state.withLock { $0.file } }
set { state.withLock { $0.file = newValue } }
}

/// Wall-clock time of the first captured buffer — the track's true start,
/// used to offset-align the two tracks' transcript timestamps.
private(set) var firstBufferAt: Date?
private(set) var firstBufferAt: Date? {
get { state.withLock { $0.firstBufferAt } }
set { state.withLock { $0.firstBufferAt = newValue } }
}

/// Start capturing system audio, encoding AAC into `url` (use a .caf
/// extension — CAF needs no finalization pass, so a crash mid-meeting
Expand Down
12 changes: 6 additions & 6 deletions Sources/quill/UI/MenuBarController.swift
Original file line number Diff line number Diff line change
Expand Up @@ -90,12 +90,12 @@ final class MenuBarController {
// has no separate resource bundle to install alongside it — true
// single-binary.
private static let featherSVG = """
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" \
viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5" \
stroke-linecap="round" stroke-linejoin="round">\
<path d="M12.67 19a2 2 0 0 0 1.416-.588l6.154-6.172a6 6 0 0 0-8.49-8.49L5.586 9.914A2 2 0 0 0 5 11.328V18a1 1 0 0 0 1 1z"/>\
<path d="M16 8 2 22"/>\
<path d="M17.5 15H9"/>\
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24"
viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5"
stroke-linecap="round" stroke-linejoin="round">
<path d="M12.67 19a2 2 0 0 0 1.416-.588l6.154-6.172a6 6 0 0 0-8.49-8.49L5.586 9.914A2 2 0 0 0 5 11.328V18a1 1 0 0 0 1 1z"/>
<path d="M16 8 2 22"/>
<path d="M17.5 15H9"/>
</svg>
"""

Expand Down