// This file is part of the 64k demo project. // It implements the production audio backend using miniaudio. // Moved from audio.cc to enable backend abstraction for testing. #include "miniaudio_backend.h" #include "audio.h" #include "ring_buffer.h" #include "util/debug.h" #include #include // for abort() // Static callback for miniaudio (C API requirement) void MiniaudioBackend::audio_callback(ma_device* pDevice, void* pOutput, const void* pInput, ma_uint32 frameCount) { (void)pInput; #if defined(DEBUG_LOG_AUDIO) // Validate callback parameters static ma_uint32 last_frameCount = 0; static int callback_reentry = 0; static double last_time = 0.0; static int timing_initialized = 0; static uint64_t total_frames_requested = 0; static uint64_t callback_number = 0; callback_number++; total_frames_requested += frameCount; // Track timing struct timespec ts; clock_gettime(CLOCK_MONOTONIC, &ts); double now = ts.tv_sec + ts.tv_nsec / 1000000000.0; if (timing_initialized) { double delta = (now - last_time) * 1000.0; // ms double expected = ((double)frameCount / pDevice->sampleRate) * 1000.0; double jitter = delta - expected; // Enhanced logging: Log first 20 callbacks in detail, then periodic summary if (callback_number <= 20 || callback_number % 50 == 0) { const double elapsed_by_frames = (double)total_frames_requested / pDevice->sampleRate * 1000.0; const double elapsed_by_time = now * 1000.0; // Convert to ms DEBUG_AUDIO("[CB#%llu] frameCount=%u, Delta=%.2fms, Expected=%.2fms, Jitter=%.2fms, " "TotalFrames=%llu (%.1fms), TotalTime=%.1fms, Drift=%.2fms\n", callback_number, frameCount, delta, expected, jitter, total_frames_requested, elapsed_by_frames, elapsed_by_time, elapsed_by_time - elapsed_by_frames); } // Detect large timing anomalies (>5ms off from expected) if (fabs(jitter) > 5.0) { DEBUG_AUDIO("[TIMING ANOMALY] CB#%llu Delta=%.2fms, Expected=%.2fms, Jitter=%.2fms\n", callback_number, delta, expected, jitter); } } last_time = now; timing_initialized = 1; // Check for re-entrant calls if (callback_reentry > 0) { DEBUG_AUDIO("FATAL: Callback re-entered! depth=%d\n", callback_reentry); abort(); } callback_reentry++; // Check if frameCount changed unexpectedly if (last_frameCount != 0 && frameCount != last_frameCount) { DEBUG_AUDIO("WARNING: frameCount changed! was=%u, now=%u\n", last_frameCount, frameCount); } last_frameCount = frameCount; // Validate device state if (!pDevice || pDevice->sampleRate == 0) { DEBUG_AUDIO("FATAL: Invalid device in callback!\n"); abort(); } // Check actual sample rate matches our expectation if (pDevice->sampleRate != 32000) { static int rate_warning = 0; if (rate_warning++ == 0) { DEBUG_AUDIO("WARNING: Device sample rate is %u, not 32000! Resampling may occur.\n", pDevice->sampleRate); } } #endif /* defined(DEBUG_LOG_AUDIO) */ float* fOutput = (float*)pOutput; // BOUNDS CHECK: Sanity check on frameCount if (frameCount > 8192 || frameCount == 0) { fprintf(stderr, "AUDIO CALLBACK ERROR: frameCount=%u (unreasonable!)\n", frameCount); abort(); } // Read from ring buffer instead of calling synth directly AudioRingBuffer* ring_buffer = audio_get_ring_buffer(); if (ring_buffer != nullptr) { const int samples_to_read = (int)frameCount * 2; // Stereo #if defined(DEBUG_LOG_RING_BUFFER) // Track buffer level and detect drops static int min_available = 99999; const int available = ring_buffer->available_read(); if (available < min_available) { min_available = available; DEBUG_RING_BUFFER("[BUFFER] CB#%llu NEW MIN: available=%d (%.1fms)\n", callback_number, available, (float)available / (32000.0f * 2.0f) * 1000.0f); } // Log buffer state for first 20 callbacks and periodically if (callback_number <= 20 || callback_number % 50 == 0) { DEBUG_RING_BUFFER("[BUFFER] CB#%llu requested=%d, available=%d (%.1fms), min=%d\n", callback_number, samples_to_read, available, (float)available / (32000.0f * 2.0f) * 1000.0f, min_available); } // CRITICAL: Verify we have enough samples if (available < samples_to_read) { DEBUG_RING_BUFFER("[BUFFER UNDERRUN] CB#%llu requested=%d, available=%d, SHORT=%d\n", callback_number, samples_to_read, available, samples_to_read - available); } #endif /* defined(DEBUG_LOG_RING_BUFFER) */ const int actually_read = ring_buffer->read(fOutput, samples_to_read); #if defined(DEBUG_LOG_RING_BUFFER) if (actually_read < samples_to_read) { DEBUG_RING_BUFFER("[PARTIAL READ] CB#%llu requested=%d, got=%d, padded=%d with silence\n", callback_number, samples_to_read, actually_read, samples_to_read - actually_read); } #endif /* defined(DEBUG_LOG_RING_BUFFER) */ } #if defined(DEBUG_LOG_AUDIO) // Clear reentry flag callback_reentry--; #endif /* defined(DEBUG_LOG_AUDIO) */ } MiniaudioBackend::MiniaudioBackend() : initialized_(false) { } MiniaudioBackend::~MiniaudioBackend() { if (initialized_) { shutdown(); } } void MiniaudioBackend::init() { if (initialized_) { return; } ma_device_config config = ma_device_config_init(ma_device_type_playback); config.playback.format = ma_format_f32; config.playback.channels = 2; config.sampleRate = 32000; // Core Audio Backend-Specific Configuration // Problem: Core Audio uses 10ms periods optimized for 44.1kHz, causing // uneven callback timing (10ms/10ms/20ms) when resampling to 32kHz // // Solution 1: Force OS-level sample rate to 32kHz to avoid resampling config.coreaudio.allowNominalSampleRateChange = MA_TRUE; // Solution 2: Use conservative performance profile for larger buffers config.performanceProfile = ma_performance_profile_conservative; // Let Core Audio choose the period size based on conservative profile config.periodSizeInFrames = 0; // 0 = let backend decide config.periods = 0; // 0 = let backend decide based on performance profile config.dataCallback = MiniaudioBackend::audio_callback; config.pUserData = this; if (ma_device_init(NULL, &config, &device_) != MA_SUCCESS) { printf("Failed to open playback device.\n"); return; } #if defined(DEBUG_LOG_AUDIO) // Log actual device configuration (to stderr for visibility) DEBUG_AUDIO("\n=== MINIAUDIO DEVICE CONFIGURATION ===\n"); DEBUG_AUDIO(" Sample rate: %u (requested: 32000)\n", device_.sampleRate); DEBUG_AUDIO(" Channels: %u (requested: 2)\n", device_.playback.channels); DEBUG_AUDIO(" Format: %d (requested: %d, f32=%d)\n", device_.playback.format, config.playback.format, ma_format_f32); DEBUG_AUDIO(" Period size: %u frames (%.1fms at %uHz)\n", device_.playback.internalPeriodSizeInFrames, (float)device_.playback.internalPeriodSizeInFrames / device_.sampleRate * 1000.0f, device_.sampleRate); DEBUG_AUDIO(" Periods: %u (buffer multiplier)\n", device_.playback.internalPeriods); DEBUG_AUDIO(" Backend: %s\n", ma_get_backend_name(device_.pContext->backend)); DEBUG_AUDIO(" Total buffer size: %u frames (%.2fms) [period * periods]\n", device_.playback.internalPeriodSizeInFrames * device_.playback.internalPeriods, (float)(device_.playback.internalPeriodSizeInFrames * device_.playback.internalPeriods) / device_.sampleRate * 1000.0f); // Calculate expected callback interval if (device_.playback.internalPeriodSizeInFrames > 0) { const float expected_callback_ms = (float)device_.playback.internalPeriodSizeInFrames / device_.sampleRate * 1000.0f; DEBUG_AUDIO(" Expected callback interval: %.2fms (based on period size)\n", expected_callback_ms); DEBUG_AUDIO(" WARNING: If actual callback interval differs, audio corruption may occur!\n"); } DEBUG_AUDIO("======================================\n\n"); fflush(stderr); #endif /* defined(DEBUG_LOG_AUDIO) */ initialized_ = true; } void MiniaudioBackend::start() { if (!initialized_) { printf("Cannot start: backend not initialized.\n"); return; } if (ma_device_start(&device_) != MA_SUCCESS) { printf("Failed to start playback device.\n"); ma_device_uninit(&device_); initialized_ = false; return; } } void MiniaudioBackend::shutdown() { if (!initialized_) { return; } ma_device_stop(&device_); ma_device_uninit(&device_); initialized_ = false; }