Skip to content

Mod-matrix sums reach detune and amp gain unclamped; a valid imported program can play 32 octaves off at 33x gain #76

Description

@BootBlock

Confirmed — the mod matrix documents a clamping contract the voice builder half-implements

src/core/audio/modMatrix.ts:12-13 states the contract explicitly:

The result for a target is Σ(sourceValue × amount) over its routes, left un-clamped so the voice builder can clamp against each target's own range.

The voice builder honours that for two of the four targets it consumes.

Clamped, correctlysrc/core/audio/voicePool.ts:252-257:

      filter.frequency.value = clamp(
        spec.filter!.cutoff * stat.cutoffFactor,
        FILTER_CUTOFF_RANGE[0],
        FILTER_CUTOFF_RANGE[1],
      );
      filter.Q.value = clamp(spec.filter!.resonance, FILTER_RESONANCE_RANGE[0], FILTER_RESONANCE_RANGE[1]);

Not clampedvoicePool.ts:242 then :276:

    const baseDetune = spec.tuneSemitones * 100 + spec.tuneCents + stat.detuneCents;
    ...
      source.detune.value = baseDetune;

Not clampedvoicePool.ts:280:

    const peak = velocityToGain(spec.velocity, spec.gainDb) * stat.ampFactor;
    scheduleAmpAttack(ampGain.gain, peak, spec.amp, now);

Nothing upstream constrains the sum. ranges.ts:61 sets MAX_MOD_ROUTES = 32, program.ts:143 and :178 validate z.array(modRouteSchema).max(MAX_MOD_ROUTES), each route's amount is −1..1 — and nothing forbids all 32 routes targeting the same destination. §6 likewise caps the matrix at 32 routes without constraining per-target totals.

Measured:

Program Result
32 × {source:'velocity', target:'pitch', amount:1} at velocity 127 detuneCents = 38400 → 32 octaves up → playbackRate ×2³² → the buffer is consumed instantly
32 × {source:'velocity', target:'amp', amount:1} ampFactor = 33 → peak gain 33× → hard clipping

Consequence. A pad that either makes no sound (a click as the buffer is exhausted in a fraction of a millisecond) or is grossly distorted, with no error anywhere. The relevant clamps already exist and are used elsewhere — TUNE_SEMITONES_RANGE is [-36, 36] and LEVEL_RANGE is [0, 1.2].

Reachability matters here. This is not only a hand-authored-program concern: a .mpcweb pack is external content (§9.6) that is Zod-validated on import, and such a program passes validation. So an imported pack — the mechanism §9.8 intends to use for shipping factory content — can produce silent or clipping pads.

Suggested fix: clamp both at the point of use in voicePool.ts, matching the two lines above, so the module contract modMatrix.ts documents is actually met for all four targets. Alternatively tighten the schema to bound per-target route sums, but the per-target clamp is simpler and is what the code already says it does.

Coverage context

modMatrix.test.ts and voiceModulation.test.ts only ever use one or two routes per target, so the summing overflow is never exercised. A test with routes summing past each target's range — for all four targets, asserting the clamp — closes this and documents the contract.

Metadata

Metadata

Assignees

No one assigned

    Labels

    audio-engineAudio graph, voices, routing, and the Web Audio layer (spec 5)bugSomething isn't workingdspWASM kernels, worklets, and effect algorithms (spec 5.6/5.7)testingUnit, offline-render, and real-browser verification (spec 11)

    Projects

    No projects

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions