// aecmic — echo-cancelled microphone capture for Claude Voice.
//
// Captures the default mic through Apple's VoiceProcessingIO audio unit
// (the AEC used by FaceTime): audio played by this Mac — including our
// own TTS from the Python process — is subtracted from the mic signal.
// Streams raw int16 mono PCM @ 16 kHz to stdout.
//
// Uses the raw AudioUnit API (bus 1 = mic in, bus 0 = render, disabled):
// the AVAudioEngine setVoiceProcessingEnabled wrapper silently produced
// bit-exact zero capture on macOS 26, so don't go back to it.
//
//   --bypass   disable voice processing (raw mic through the same path;
//              used by test_echo.py to prove the capture path is alive)
//
// Build: swiftc -O -o aecmic aecmic.swift -framework AudioToolbox

import AudioToolbox
import Foundation

func fail(_ msg: String, _ status: OSStatus = 0) -> Never {
    FileHandle.standardError.write("aecmic: \(msg) (\(status))\n".data(using: .utf8)!)
    exit(1)
}

var gUnit: AudioUnit?
let gOut = FileHandle.standardOutput

let inputCallback: AURenderCallback = {
    _, ioActionFlags, inTimeStamp, inBusNumber, inNumberFrames, _ in
    guard let unit = gUnit else { return noErr }
    var bufferList = AudioBufferList(
        mNumberBuffers: 1,
        mBuffers: AudioBuffer(
            mNumberChannels: 1,
            mDataByteSize: inNumberFrames * 2,
            mData: nil))  // nil -> the unit supplies its own buffer
    let status = AudioUnitRender(
        unit, ioActionFlags, inTimeStamp, inBusNumber, inNumberFrames,
        &bufferList)
    if status == noErr, let data = bufferList.mBuffers.mData {
        gOut.write(Data(bytes: data, count: Int(bufferList.mBuffers.mDataByteSize)))
    }
    return status
}

let bypass = CommandLine.arguments.contains("--bypass")

var desc = AudioComponentDescription(
    componentType: kAudioUnitType_Output,
    componentSubType: kAudioUnitSubType_VoiceProcessingIO,
    componentManufacturer: kAudioUnitManufacturer_Apple,
    componentFlags: 0, componentFlagsMask: 0)

guard let comp = AudioComponentFindNext(nil, &desc) else {
    fail("VoiceProcessingIO component not found")
}
var unitOpt: AudioUnit?
var status = AudioComponentInstanceNew(comp, &unitOpt)
guard status == noErr, let unit = unitOpt else { fail("instance", status) }
gUnit = unit

let busOutput: AudioUnitElement = 0
let busInput: AudioUnitElement = 1
var one: UInt32 = 1
var zero: UInt32 = 0
let u32 = UInt32(MemoryLayout<UInt32>.size)

status = AudioUnitSetProperty(
    unit, kAudioOutputUnitProperty_EnableIO, kAudioUnitScope_Input,
    busInput, &one, u32)
guard status == noErr else { fail("enable input", status) }
status = AudioUnitSetProperty(
    unit, kAudioOutputUnitProperty_EnableIO, kAudioUnitScope_Output,
    busOutput, &zero, u32)
guard status == noErr else { fail("disable output", status) }

var fmt = AudioStreamBasicDescription(
    mSampleRate: 16000,
    mFormatID: kAudioFormatLinearPCM,
    mFormatFlags: kAudioFormatFlagIsSignedInteger | kAudioFormatFlagIsPacked,
    mBytesPerPacket: 2, mFramesPerPacket: 1, mBytesPerFrame: 2,
    mChannelsPerFrame: 1, mBitsPerChannel: 16, mReserved: 0)
let fmtSize = UInt32(MemoryLayout<AudioStreamBasicDescription>.size)

status = AudioUnitSetProperty(
    unit, kAudioUnitProperty_StreamFormat, kAudioUnitScope_Output,
    busInput, &fmt, fmtSize)
guard status == noErr else { fail("format bus1", status) }
status = AudioUnitSetProperty(
    unit, kAudioUnitProperty_StreamFormat, kAudioUnitScope_Input,
    busOutput, &fmt, fmtSize)
guard status == noErr else { fail("format bus0", status) }

var cb = AURenderCallbackStruct(inputProc: inputCallback, inputProcRefCon: nil)
status = AudioUnitSetProperty(
    unit, kAudioOutputUnitProperty_SetInputCallback, kAudioUnitScope_Input,
    busInput, &cb, UInt32(MemoryLayout<AURenderCallbackStruct>.size))
guard status == noErr else { fail("input callback", status) }

var bypassFlag: UInt32 = bypass ? 1 : 0
status = AudioUnitSetProperty(
    unit, kAUVoiceIOProperty_BypassVoiceProcessing, kAudioUnitScope_Global,
    0, &bypassFlag, u32)
guard status == noErr else { fail("bypass toggle", status) }

// macOS DUCKS all other audio while a voice-processing mic is open —
// that includes our own TTS playback. Turn ducking down to minimum so
// Claude's voice stays at full volume.
if #available(macOS 14.0, *) {
    var ducking = AUVoiceIOOtherAudioDuckingConfiguration(
        mEnableAdvancedDucking: false,
        mDuckingLevel: .min)
    status = AudioUnitSetProperty(
        unit, kAUVoiceIOProperty_OtherAudioDuckingConfiguration,
        kAudioUnitScope_Global, 0, &ducking,
        UInt32(MemoryLayout<AUVoiceIOOtherAudioDuckingConfiguration>.size))
    if status != noErr {
        FileHandle.standardError.write(
            "aecmic: warning: ducking config failed (\(status))\n".data(using: .utf8)!)
    }
}

status = AudioUnitInitialize(unit)
guard status == noErr else { fail("initialize", status) }
status = AudioOutputUnitStart(unit)
guard status == noErr else { fail("start", status) }

FileHandle.standardError.write(
    "aecmic: running (16kHz int16 mono on stdout, AEC \(bypass ? "BYPASSED" : "on"))\n"
        .data(using: .utf8)!)
RunLoop.main.run()
