// sfxMixHost.c - Host-cc differential harness for the shared SFX // overlay mixer (src/core/audioSfxMix.c). Built by // scripts/check-sfxmix.sh on the millisHost.c / check-millis.sh // pattern. The mixer TU has no jlp* platform hooks; its only external // input is the stream fill callback, mocked below (streamFill) the way // millisHost.c mocks jlpFrameCount/jlpFrameHz. // // Every scenario is fully deterministic: all sample data, background // (dst) data, and stream chunks come from a fixed-seed LCG -- no time, // no rand(), no host entropy. Each scenario prints: // // name= slots= len= hash=0x // first= // raw= // // check-sfxmix.sh captures the first run as // tests/goldens/sfxmix-baseline.txt and later diffs against it: // * exact mode (default): hashes must match -- the gate for // PERF-AUDIT findings #7 and #33 (bit-identical rewrites). // * JL_SFX_TOLERANCE=1 mode: per-sample compare of the raw lines; // finding #6 (interp 16x16 multiply) may deviate by at most 1 LSB // PER SLOT CONTRIBUTION, so the allowed per-sample delta is // tolerance * slots (deviations from simultaneous slots stack // before the clamp). That is why slots= is part of the output. // // The mixer has no per-slot volume field (volume is applied upstream // when the sample data is produced), so the "volume 0 / volume max" // scenarios use amplitude-scaled sample data: amp 0 = silent samples // (output must equal the untouched background), amp 127 = full-scale // samples on a full-range background (exercises both clamp branches). #include #include #include #include #include "audioSfxMixInternal.h" // Output samples per single mix call. #define MIX_LEN 1024u // Debug prefix bytes echoed on the first= line. #define FIRST_BYTES 16u // Mixer output rate used by most scenarios (DOS SB-ish rate). #define OUT_RATE 11025u // Largest fixed-buffer sample used by the single-mix scenarios. #define SAMPLE_MAX 4096u // fixedLongPos64k: enough sequential mix blocks at ~8.19 step to push // the read index past 65535 input samples (the uint64 posFp headroom // that the finding #7 rewrite must preserve): 8 * 1024 * 8.19 ~ 67k. #define LONG_BLOCKS 8u #define LONG_SAMPLE_LEN 68000u #define LONG_RATE_HZ 65535u #define LONG_OUT_RATE 8000u // Mock stream source: hands out LCG-generated signed samples in // chunks of at most chunkCap until remaining is exhausted, then // returns 0 (end-of-stream). typedef struct { uint32_t lcgState; uint32_t remaining; uint32_t chunkCap; int32_t amp; } StreamCtxT; // Prototypes (harness-local, alphabetical). static void dstFill(uint8_t *dst, uint32_t len, uint32_t seed); static uint32_t fnv1a(const uint8_t *data, uint32_t len); static void genSignedSamples(uint32_t *state, uint8_t *dst, uint32_t count, int32_t amp); static uint32_t lcgNext(uint32_t *state); static void reportScenario(const char *name, uint8_t slots, const uint8_t *dst, uint32_t len); static void runFixedScenario(const char *name, uint16_t rateHz, uint32_t outputRate, int32_t amp, uint32_t sampleCount, uint16_t mixLen, uint32_t dstSeed, uint32_t sampleSeed); static uint32_t streamFill(void *ctx, int8_t *dst, uint32_t count); static uint8_t gDst[MIX_LEN]; static uint8_t gLongOut[LONG_BLOCKS * MIX_LEN]; static uint8_t gLongSample[LONG_SAMPLE_LEN]; static uint8_t gSampleA[SAMPLE_MAX]; static uint8_t gSampleB[SAMPLE_MAX]; static uint8_t gSampleC[SAMPLE_MAX]; // Fill a mix destination with full-range unsigned LCG bytes -- a // deterministic stand-in for already-rendered MOD output. Full range // (not centered) so loud SFX hit both clamp branches. static void dstFill(uint8_t *dst, uint32_t len, uint32_t seed) { uint32_t state; uint32_t i; state = seed; for (i = 0; i < len; i++) { dst[i] = (uint8_t)(lcgNext(&state) >> 24); } } // FNV-1a 32-bit. static uint32_t fnv1a(const uint8_t *data, uint32_t len) { uint32_t hash; uint32_t i; hash = 2166136261u; for (i = 0; i < len; i++) { hash ^= (uint32_t)data[i]; hash *= 16777619u; } return hash; } // Fill dst with signed 8-bit samples (stored as their uint8_t bit // pattern, matching AudioSfxSlotT.sample) scaled to amplitude `amp` // (0 = silence, 127 = full scale). Truncation toward zero is // C99-defined, so the scaling is portable and deterministic. static void genSignedSamples(uint32_t *state, uint8_t *dst, uint32_t count, int32_t amp) { uint32_t i; int32_t raw; int32_t scaled; for (i = 0; i < count; i++) { raw = (int32_t)(lcgNext(state) >> 24) - 128; scaled = (raw * amp) / 127; dst[i] = (uint8_t)(int8_t)scaled; } } // Numerical Recipes LCG; only the high byte is consumed (the low bits // of an LCG are weak). static uint32_t lcgNext(uint32_t *state) { *state = (*state * 1664525u) + 1013904223u; return *state; } // Emit the three per-scenario output lines (see file header). static void reportScenario(const char *name, uint8_t slots, const uint8_t *dst, uint32_t len) { uint32_t i; uint32_t show; printf("name=%s slots=%u len=%lu hash=0x%08lX\n", name, (unsigned)slots, (unsigned long)len, (unsigned long)fnv1a(dst, len)); show = (len < FIRST_BYTES) ? len : FIRST_BYTES; printf("first="); for (i = 0; i < show; i++) { if (i > 0) { putchar(' '); } printf("%02X", dst[i]); } printf("\n"); printf("raw="); for (i = 0; i < len; i++) { printf("%02X", dst[i]); } printf("\n"); } // One fixed-buffer slot mixed once into a fresh background. mixLen // may exceed MIX_LEN up to sizeof(gLongOut). static void runFixedScenario(const char *name, uint16_t rateHz, uint32_t outputRate, int32_t amp, uint32_t sampleCount, uint16_t mixLen, uint32_t dstSeed, uint32_t sampleSeed) { AudioSfxSlotT slot; uint32_t state; state = sampleSeed; genSignedSamples(&state, gSampleA, sampleCount, amp); dstFill(gLongOut, mixLen, dstSeed); memset(&slot, 0, sizeof(slot)); audioSfxSlotArm(&slot, gSampleA, sampleCount, rateHz, outputRate); audioSfxOverlayMix(gLongOut, mixLen, &slot, 1u); reportScenario(name, 1u, gLongOut, mixLen); } // Mock jlAudioStreamFillT (see StreamCtxT). static uint32_t streamFill(void *ctx, int8_t *dst, uint32_t count) { StreamCtxT *sc; uint32_t give; sc = (StreamCtxT *)ctx; give = count; if (give > sc->chunkCap) { give = sc->chunkCap; } if (give > sc->remaining) { give = sc->remaining; } genSignedSamples(&sc->lcgState, (uint8_t *)dst, give, sc->amp); sc->remaining -= give; return give; } int main(void) { AudioSfxSlotT slot; AudioSfxSlotT slots[3]; StreamCtxT ctx; uint32_t state; uint32_t block; printf("sfxMixHost: audioSfxOverlayMix scenarios (LCG-seeded, deterministic)\n"); // --- Fixed-buffer slots, one slot per mix. --- // Rate conversion up: 8363 Hz source into an 11025 Hz mix // (step ~0.7585, every fractional interp path exercised). runFixedScenario("fixedUp8363to11025", 8363u, OUT_RATE, 100, 2048u, (uint16_t)MIX_LEN, 0xD57B0001u, 0x5EED0001u); // Rate conversion down: 22050 -> 11025 (step 2.0, input skipping). runFixedScenario("fixedDown22050to11025", 22050u, OUT_RATE, 100, 2200u, (uint16_t)MIX_LEN, 0xD57B0002u, 0x5EED0002u); // Unity rate: step exactly 1.0, frac stays 0 (interp identity). runFixedScenario("fixedUnity11025", 11025u, OUT_RATE, 80, 1100u, (uint16_t)MIX_LEN, 0xD57B0003u, 0x5EED0003u); // Odd stepFp ((5000 << 16) / 11025 = 29721): frac takes odd // values, the case where the finding #6 interp reformulation // deviates. Single slot, so tolerance mode checks the strict // 1-LSB-per-sample bound here (the even-step scenarios above are // insensitive to the frac low bit and must stay bit-identical). // The deviation only fires when the dropped d * 1 crosses a 65536 // boundary (~|delta| / 65536 odds per odd-frac sample), so this // one mixes 8x longer at full amplitude to make hits certain. runFixedScenario("fixedOddStep5000", 5000u, OUT_RATE, 127, 4096u, (uint16_t)(LONG_BLOCKS * MIX_LEN), 0xD57B000Bu, 0x5EED000Cu); // "Volume" 0: silent samples; output must equal the background. runFixedScenario("fixedAmpZero", 8363u, OUT_RATE, 0, 2048u, (uint16_t)MIX_LEN, 0xD57B0004u, 0x5EED0004u); // "Volume" max: full-scale samples on a full-range background -- // both clamp branches (+127 / -128) fire. runFixedScenario("fixedAmpMax", 8363u, OUT_RATE, 127, 2048u, (uint16_t)MIX_LEN, 0xD57B0005u, 0x5EED0005u); // Short sample: slot runs out and deactivates mid-mix (~output // 395 of 1024); also hits the s1 == s0 tail at idx + 1 == length. runFixedScenario("fixedShortEnd", 8363u, OUT_RATE, 127, 300u, (uint16_t)MIX_LEN, 0xD57B0006u, 0x5EED0006u); // --- Multiple simultaneous fixed slots. --- // Three rates (down / unity / up), three amplitudes; slot B is // short and deactivates mid-mix while A and C keep going. state = 0x5EED0007u; genSignedSamples(&state, gSampleA, 2048u, 60); state = 0x5EED0008u; genSignedSamples(&state, gSampleB, 900u, 90); state = 0x5EED0009u; genSignedSamples(&state, gSampleC, 2048u, 127); memset(slots, 0, sizeof(slots)); audioSfxSlotArm(&slots[0], gSampleA, 2048u, 5000u, OUT_RATE); audioSfxSlotArm(&slots[1], gSampleB, 900u, 11025u, OUT_RATE); audioSfxSlotArm(&slots[2], gSampleC, 2048u, 18000u, OUT_RATE); dstFill(gDst, MIX_LEN, 0xD57B0007u); audioSfxOverlayMix(gDst, (uint16_t)MIX_LEN, slots, 3u); reportScenario("multiSlot3", 3u, gDst, MIX_LEN); // --- Streaming slots. --- // Rate-up stream whose 200-sample chunks force several // streamRefill calls inside a single mix (prime + ~3 mid-mix). memset(&slot, 0, sizeof(slot)); ctx.lcgState = 0x5712EA41u; ctx.remaining = 4000u; ctx.chunkCap = 200u; ctx.amp = 100; audioSfxSlotArmStream(&slot, streamFill, &ctx, 8363u, OUT_RATE); dstFill(gDst, MIX_LEN, 0xD57B0008u); audioSfxOverlayMix(gDst, (uint16_t)MIX_LEN, &slot, 1u); reportScenario("streamRefillMid", 1u, gDst, MIX_LEN); // Rate-down stream (step 2.0) that ends mid-mix: 500 source // samples cover only ~250 outputs, then fill returns 0 and the // slot deactivates with the background left untouched after it. memset(&slot, 0, sizeof(slot)); ctx.lcgState = 0x5712EA42u; ctx.remaining = 500u; ctx.chunkCap = 255u; ctx.amp = 110; audioSfxSlotArmStream(&slot, streamFill, &ctx, 22050u, OUT_RATE); dstFill(gDst, MIX_LEN, 0xD57B0009u); audioSfxOverlayMix(gDst, (uint16_t)MIX_LEN, &slot, 1u); reportScenario("streamEndMid", 1u, gDst, MIX_LEN); // Fixed + streaming slot in the same mix call (the isStream // branch takes both arms across one dst). state = 0x5EED000Au; genSignedSamples(&state, gSampleA, 2048u, 70); memset(slots, 0, sizeof(slots)); audioSfxSlotArm(&slots[0], gSampleA, 2048u, 6000u, OUT_RATE); ctx.lcgState = 0x5712EA43u; ctx.remaining = 3000u; ctx.chunkCap = 180u; ctx.amp = 95; audioSfxSlotArmStream(&slots[1], streamFill, &ctx, 16000u, OUT_RATE); dstFill(gDst, MIX_LEN, 0xD57B000Au); audioSfxOverlayMix(gDst, (uint16_t)MIX_LEN, slots, 2u); reportScenario("mixedFixedStream", 2u, gDst, MIX_LEN); // --- Long-position fixed slot (finding #7 headroom). --- // Sequential mix blocks walk the read index past 65535 input // samples; a posFp rewrite that truncates the index (or drops the // frac carry) diverges here. Output is the concatenation of all // blocks so every block is part of the hash and raw compare. state = 0x5EED000Bu; genSignedSamples(&state, gLongSample, LONG_SAMPLE_LEN, 90); memset(&slot, 0, sizeof(slot)); audioSfxSlotArm(&slot, gLongSample, LONG_SAMPLE_LEN, LONG_RATE_HZ, LONG_OUT_RATE); for (block = 0; block < LONG_BLOCKS; block++) { dstFill(gLongOut + (block * MIX_LEN), MIX_LEN, 0xD57B0100u + block); audioSfxOverlayMix(gLongOut + (block * MIX_LEN), (uint16_t)MIX_LEN, &slot, 1u); } reportScenario("fixedLongPos64k", 1u, gLongOut, LONG_BLOCKS * MIX_LEN); return 0; }