// caprobe: output-only Core Audio stress probe. Plays SILENCE to a named device at a chosen
// buffer size, optionally burns a fraction of each cycle time budget to mimic plug-in load,
// and counts HAL overloads and sample-time discontinuities. No input, no mic permission.
// usage: caprobe "<device name substring>" <frames> <seconds> <load 0..0.95>
#include <CoreAudio/CoreAudio.h>
#include <mach/mach_time.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <math.h>

static volatile long g_overloads = 0, g_cycles = 0, g_gaps = 0, g_gapFrames = 0, g_late = 0;
static double g_lastSample = -1, g_maxWallMs = 0, g_maxLateMs = 0;
static uint64_t g_lastHost = 0;
static UInt32 g_frames = 64;
static double g_load = 0, g_tickToNs = 1, g_budgetNs = 0;
static FILE *g_ev;
static double g_t0 = 0;

static double nowSec(void) { return mach_absolute_time() * g_tickToNs / 1e9; }

static OSStatus overloadListener(AudioObjectID o, UInt32 n, const AudioObjectPropertyAddress *a, void *c) {
    g_overloads++;
    fprintf(g_ev, "%.3f OVERLOAD\n", nowSec() - g_t0); fflush(g_ev);
    return 0;
}

static OSStatus ioproc(AudioObjectID dev, const AudioTimeStamp *now, const AudioBufferList *in,
                       const AudioTimeStamp *inTime, AudioBufferList *out, const AudioTimeStamp *outTime, void *ctx) {
    uint64_t t = mach_absolute_time();
    for (UInt32 i = 0; i < out->mNumberBuffers; i++) memset(out->mBuffers[i].mData, 0, out->mBuffers[i].mDataByteSize);
    double s = outTime->mSampleTime;
    if (g_lastSample >= 0) {
        double d = s - g_lastSample;
        if (fabs(d - g_frames) > 0.5) {
            g_gaps++; g_gapFrames += (long)(d - g_frames);
            fprintf(g_ev, "%.3f GAP expected %u got %.0f\n", nowSec() - g_t0, g_frames, d); fflush(g_ev);
        }
        double wallMs = (t - g_lastHost) * g_tickToNs / 1e6;
        if (wallMs > g_maxWallMs) g_maxWallMs = wallMs;
        double nominal = g_frames / 48.0;
        if (wallMs > nominal * 2.0) { g_late++; if (wallMs > g_maxLateMs) g_maxLateMs = wallMs; }
    }
    g_lastSample = s; g_lastHost = t; g_cycles++;
    if (g_load > 0) {
        uint64_t until = t + (uint64_t)(g_budgetNs * g_load / g_tickToNs);
        volatile double x = 1.0;
        while (mach_absolute_time() < until) x = sin(x) + 1.0;
    }
    return 0;
}

int main(int argc, char **argv) {
    if (argc < 5) { fprintf(stderr, "usage: caprobe name frames seconds load\n"); return 2; }
    mach_timebase_info_data_t tb; mach_timebase_info(&tb); g_tickToNs = (double)tb.numer / tb.denom;
    g_frames = atoi(argv[2]); int secs = atoi(argv[3]); g_load = atof(argv[4]);
    g_budgetNs = g_frames / 48000.0 * 1e9;
    g_ev = stdout;
    AudioObjectPropertyAddress pa = {kAudioHardwarePropertyDevices, kAudioObjectPropertyScopeGlobal, kAudioObjectPropertyElementMain};
    UInt32 sz = 0; AudioObjectGetPropertyDataSize(kAudioObjectSystemObject, &pa, 0, NULL, &sz);
    int n = sz / sizeof(AudioObjectID); AudioObjectID *ids = malloc(sz);
    AudioObjectGetPropertyData(kAudioObjectSystemObject, &pa, 0, NULL, &sz, ids);
    AudioObjectID dev = 0;
    for (int i = 0; i < n; i++) {
        CFStringRef name = NULL; UInt32 s2 = sizeof(name);
        AudioObjectPropertyAddress na = {kAudioObjectPropertyName, kAudioObjectPropertyScopeGlobal, kAudioObjectPropertyElementMain};
        if (AudioObjectGetPropertyData(ids[i], &na, 0, NULL, &s2, &name) == 0 && name) {
            char buf[256]; CFStringGetCString(name, buf, sizeof buf, kCFStringEncodingUTF8); CFRelease(name);
            if (strstr(buf, argv[1])) { dev = ids[i]; printf("device: %s (id %u)\n", buf, dev); break; }
        }
    }
    if (!dev) { fprintf(stderr, "device not found\n"); return 1; }
    AudioObjectPropertyAddress bf = {kAudioDevicePropertyBufferFrameSize, kAudioObjectPropertyScopeGlobal, kAudioObjectPropertyElementMain};
    UInt32 fr = g_frames; OSStatus e = AudioObjectSetPropertyData(dev, &bf, 0, NULL, sizeof fr, &fr);
    UInt32 got = 0; sz = sizeof got; AudioObjectGetPropertyData(dev, &bf, 0, NULL, &sz, &got);
    Float64 sr = 0; sz = sizeof sr;
    AudioObjectPropertyAddress ra = {kAudioDevicePropertyNominalSampleRate, kAudioObjectPropertyScopeGlobal, kAudioObjectPropertyElementMain};
    AudioObjectGetPropertyData(dev, &ra, 0, NULL, &sz, &sr);
    UInt32 lat = 0, safety = 0; sz = sizeof lat;
    AudioObjectPropertyAddress la = {kAudioDevicePropertyLatency, kAudioObjectPropertyScopeOutput, kAudioObjectPropertyElementMain};
    AudioObjectGetPropertyData(dev, &la, 0, NULL, &sz, &lat);
    AudioObjectPropertyAddress sa = {kAudioDevicePropertySafetyOffset, kAudioObjectPropertyScopeOutput, kAudioObjectPropertyElementMain};
    sz = sizeof safety; AudioObjectGetPropertyData(dev, &sa, 0, NULL, &sz, &safety);
    g_frames = got;
    printf("set buffer %s -> %u frames, rate %.0f, device latency %u, safety %u, load %.0f%% of %.3f ms budget\n",
           e ? "FAILED" : "ok", got, sr, lat, safety, g_load * 100, g_budgetNs / 1e6);
    AudioObjectPropertyAddress oa = {kAudioDeviceProcessorOverload, kAudioObjectPropertyScopeGlobal, kAudioObjectPropertyElementMain};
    AudioObjectAddPropertyListener(dev, &oa, overloadListener, NULL);
    AudioDeviceIOProcID pid = NULL;
    if (AudioDeviceCreateIOProcID(dev, ioproc, NULL, &pid)) { fprintf(stderr, "ioproc failed\n"); return 1; }
    g_t0 = nowSec();
    AudioDeviceStart(dev, pid);
    for (int i = 0; i < secs; i++) sleep(1);
    AudioDeviceStop(dev, pid);
    AudioDeviceDestroyIOProcID(dev, pid);
    printf("RESULT frames=%u secs=%d load=%.2f cycles=%ld overloads=%ld gaps=%ld gapFrames=%ld late_cycles=%ld max_cycle_ms=%.2f nominal_ms=%.2f\n",
           g_frames, secs, g_load, g_cycles, g_overloads, g_gaps, g_gapFrames, g_late, g_maxWallMs, g_frames / 48.0);
    return 0;
}
