// sysaudio — system-audio (loopback) capture for Claude Voice.
//
// Captures what the Mac is PLAYING — the remote voices of an online meeting
// (Zoom/Teams/Meet come out of the speakers) — straight from the audio engine
// via ScreenCaptureKit, cleanly and at full quality. This is the "them" side
// of a meeting: the AEC mic (helper/aecmic) deliberately ERASES speaker output,
// so the only way to hear the far end is to tap the digital audio here.
//
// `excludesCurrentProcessAudio` keeps Jarvis's own TTS out of the capture.
// SCK delivers float32 / 48 kHz / stereo (planar) CMSampleBuffers; we downmix
// to mono and resample 48000->16000, emit raw int16 mono PCM @ 16 kHz to
// stdout (SAME contract as aecmic). Status/errors go to stderr; runs until
// killed (RunLoop).
//
// Screen-Recording permission is required (SCK system-audio rides on it). A
// helper spawned by Jarvis.app inherits the app's TCC grant (same as aecmic
// inherits mic access); from a plain terminal it fails/prompts — on ANY SCK
// error we print a clear stderr line and exit non-zero so Python can detect it
// and tell Ahmed instead of recording his side only in silence.
//
// Build: swiftc -O -o sysaudio sysaudio.swift -framework ScreenCaptureKit -framework AVFoundation -framework CoreMedia

import AVFoundation
import CoreMedia
import Foundation
import ScreenCaptureKit

func fail(_ msg: String) -> Never {
    FileHandle.standardError.write("sysaudio: \(msg)\n".data(using: .utf8)!)
    exit(1)
}

// 16 kHz int16 mono, interleaved — the exact byte format aecmic emits.
let target = AVAudioFormat(
    commonFormat: .pcmFormatInt16, sampleRate: 16000, channels: 1,
    interleaved: true)!

final class Capturer: NSObject, SCStreamOutput, SCStreamDelegate {
    let out = FileHandle.standardOutput
    var converter: AVAudioConverter?   // built lazily from the real source format
    var stream: SCStream?

    func start() async {
        do {
            // Audio-only SCK still needs a content filter bound to a display.
            let content = try await SCShareableContent.excludingDesktopWindows(
                false, onScreenWindowsOnly: false)
            guard let display = content.displays.first else {
                fail("no display to attach the capture stream to")
            }
            let filter = SCContentFilter(
                display: display, excludingApplications: [], exceptingWindows: [])

            let config = SCStreamConfiguration()
            config.capturesAudio = true
            config.excludesCurrentProcessAudio = true  // don't record our own TTS
            config.sampleRate = 48000
            config.channelCount = 2
            // We only want audio; keep the mandatory video path as cheap as
            // possible (tiny frames, 1 fps) — its buffers are discarded.
            config.width = 2
            config.height = 2
            config.minimumFrameInterval = CMTime(value: 1, timescale: 1)
            config.queueDepth = 6

            let s = SCStream(filter: filter, configuration: config, delegate: self)
            try s.addStreamOutput(
                self, type: .audio,
                sampleHandlerQueue: DispatchQueue(label: "sysaudio.audio"))
            // A video output keeps the stream healthy on macOS builds that
            // won't deliver audio to an audio-only stream; frames are ignored.
            try s.addStreamOutput(
                self, type: .screen,
                sampleHandlerQueue: DispatchQueue(label: "sysaudio.video"))
            try await s.startCapture()
            self.stream = s
            FileHandle.standardError.write(
                "sysaudio: running (16kHz int16 mono on stdout, SCK loopback)\n"
                    .data(using: .utf8)!)
        } catch {
            fail("ScreenCaptureKit start failed: \(error.localizedDescription)")
        }
    }

    // SCStreamDelegate — the stream died (permission revoked, display gone…).
    func stream(_ stream: SCStream, didStopWithError error: Error) {
        fail("stream stopped: \(error.localizedDescription)")
    }

    func stream(_ stream: SCStream, didOutputSampleBuffer sampleBuffer: CMSampleBuffer,
                of type: SCStreamOutputType) {
        guard type == .audio, sampleBuffer.isValid else { return }
        // Read the ACTUAL source format off the buffer (don't assume 48k/stereo).
        guard let asbd = sampleBuffer.formatDescription?.audioStreamBasicDescription,
              let srcFormat = AVAudioFormat(
                standardFormatWithSampleRate: asbd.mSampleRate,
                channels: asbd.mChannelsPerFrame)
        else { return }

        // SCK hands us float32 deinterleaved (== "standard" format), so we can
        // wrap the buffer list without a copy and let AVAudioConverter do the
        // downmix (stereo->mono) + resample (->16k) + int16 conversion in one.
        do {
            try sampleBuffer.withAudioBufferList { abl, _ in
                guard let inBuf = AVAudioPCMBuffer(
                    pcmFormat: srcFormat, bufferListNoCopy: abl.unsafePointer)
                else { return }
                emit(inBuf, srcFormat: srcFormat)
            }
        } catch {
            // one bad buffer must not kill the stream
        }
    }

    private func emit(_ inBuf: AVAudioPCMBuffer, srcFormat: AVAudioFormat) {
        if converter == nil {
            converter = AVAudioConverter(from: srcFormat, to: target)
        }
        guard let conv = converter else { return }
        let ratio = target.sampleRate / srcFormat.sampleRate
        let cap = AVAudioFrameCount(Double(inBuf.frameLength) * ratio) + 16
        guard cap > 0,
              let outBuf = AVAudioPCMBuffer(pcmFormat: target, frameCapacity: cap)
        else { return }

        var fed = false
        var err: NSError?
        conv.convert(to: outBuf, error: &err) { _, status in
            if fed { status.pointee = .noDataNow; return nil }
            fed = true
            status.pointee = .haveData
            return inBuf
        }
        if err != nil { return }
        if let ch = outBuf.int16ChannelData, outBuf.frameLength > 0 {
            out.write(Data(bytes: ch[0], count: Int(outBuf.frameLength) * 2))
        }
    }
}

let cap = Capturer()
Task { await cap.start() }
RunLoop.main.run()
