Hmbown/CodeWhale · error · Error

Invalid pet PCM range.

Error message

Invalid pet PCM range.

What it means

renderPetPCM() validates its range arguments before allocating buffers: sampleRate must be an integer in [8000, 96000], startSample a safe non-negative integer, and length a non-negative integer no larger than sampleRate*120 (two minutes of audio). This guard prevents allocating absurd buffers or synthesizing outside supported sample rates.

Solutions

  1. Log the three arguments at the call site and check them: integer sampleRate 8000–96000, integer startSample >= 0, integer 0 <= length <= sampleRate*120.
  2. Clamp or validate duration before converting: length = Math.min(Math.floor(seconds * sampleRate), sampleRate * 120).
  3. Use Math.round/Math.floor when deriving startSample and length from float seconds to avoid fractional values.
  4. Pick a supported sample rate constant (e.g. 48_000) instead of reading an unrestricted device rate.
  5. Wrap the call in try/catch and clamp to the nearest valid range on failure.

Example fix

// before
const { left, right } = renderPetPCM(voices, t * sr, durationMs * sr, deviceRate);

// after
const sr = 48_000;
const start = Math.max(0, Math.round(t * sr));
const len = Math.min(Math.max(0, Math.round(durationMs / 1000 * sr)), sr * 120);
const { left, right } = renderPetPCM(voices, start, len, sr);
Defensive patterns

Strategy: validation

Validate before calling

function isPcmRange(startSample: number, length: number, sampleRate: number): boolean {
  return Number.isInteger(sampleRate) && sampleRate >= 8000 && sampleRate <= 96000
    && Number.isSafeInteger(startSample) && startSample >= 0
    && Number.isInteger(length) && length >= 0 && length <= sampleRate * 120;
}
if (!isPcmRange(start, len, sr)) throw new Error('clamping needed');

Type guard

function isRenderablePcm(args: unknown): args is [number, number, number] {
  return Array.isArray(args) && args.length === 3
    && Number.isInteger(args[2]) && args[2] >= 8000 && args[2] <= 96000
    && Number.isSafeInteger(args[0]) && args[0] >= 0
    && Number.isInteger(args[1]) && args[1] >= 0 && args[1] <= args[2] * 120;
}

Try / catch

try {
  const pcm = renderPetPCM(voices, start, length, sampleRate);
} catch (err) {
  if (err instanceof Error && err.message === 'Invalid pet PCM range.') {
    const sr = Math.min(96000, Math.max(8000, Math.round(sampleRate)));
    const pcm = renderPetPCM(voices, Math.max(0, Math.round(start)), Math.min(length, sr * 120), sr);
  } else throw err;
}

Prevention

When it happens

Trigger: Calling renderPetPCM(voices, startSample, length) with: a non-integer sampleRate (e.g. 44100.5) or one outside 8000–96000 (e.g. 192000), a negative or fractional startSample, a negative length, a non-integer length, or length exceeding sampleRate*120 (e.g. length=48_000*300 for a 5-minute render).

Common situations: Computing length from milliseconds with a unit error (ms instead of samples); using a device sample rate of 192 kHz; float drift from arithmetic like startSample = seconds * sampleRate producing 44099.999; exporting longer-than-2-minute clips.

Related errors


AI-assisted analysis of Hmbown/CodeWhale@433685b202 (2026-09-15). Data as JSON: /api/errors/107c41ae83ba0245. Report an issue: GitHub.

Appendix: source

Thrown at pet/src/core/pet-audio.ts:74

        }
        if (frame.needs === 'call' && window % 10 === 0) add(`call:${window}`, CHANNELS[11].freq * 1.5, .38, .03);
      }
    }
    return out;
  }
}

/** Sample-addressed noise: no global RNG, identical samples when chunked/seeking. */
function noise(seed: number, sample: number): number {
  let x = (seed + Math.imul(sample, 0x6D2B79F5)) >>> 0;
  x = Math.imul(x ^ x >>> 15, x | 1); x ^= x + Math.imul(x ^ x >>> 7, x | 61);
  return ((x ^ x >>> 14) >>> 0) / 2147483648 - 1;
}

export function renderPetPCM(voices: readonly PetVoice[], startSample: number, length: number, sampleRate = 48_000): { left: Float32Array; right: Float32Array } {
  if (!Number.isInteger(sampleRate) || sampleRate < 8000 || sampleRate > 96000
    || !Number.isSafeInteger(startSample) || startSample < 0 || !Number.isInteger(length) || length < 0 || length > sampleRate * 120)
    throw new Error('Invalid pet PCM range.');
  const left = new Float32Array(length), right = new Float32Array(length);
  for (const v of voices) {
    if (![v.start, v.duration, v.frequency, v.gain, v.pan].every(Number.isFinite)
      || v.start < 0 || v.duration <= 0 || v.duration > 10 || v.frequency <= 0 || v.frequency > sampleRate / 2
      || v.gain < 0 || v.gain > 1 || Math.abs(v.pan) > 1 || !['tone', 'noise'].includes(v.kind)) throw new Error('Invalid pet voice.');
    const first = Math.max(startSample, Math.ceil(v.start * sampleRate));
    const last = Math.min(startSample + length, Math.ceil((v.start + v.duration) * sampleRate));
    const pan = (v.pan + 1) * Math.PI / 4, seed = (0xC0FFEE ^ stableHash(v.id)) >>> 0;
    for (let absolute = first; absolute < last; absolute++) {
      const age = absolute / sampleRate - v.start;
      const envelope = Math.min(1, age / .015, (v.duration - age) / .045);
      const sample = v.kind === 'noise' ? noise(seed, absolute) * Math.exp(-age * 14)
        : Math.sin(2 * Math.PI * v.frequency * age) * .88 + Math.sin(4 * Math.PI * v.frequency * age) * .12;
      const value = sample * Math.max(0, envelope) * v.gain, at = absolute - startSample;
      left[at] += value * Math.cos(pan); right[at] += value * Math.sin(pan);
    }
  }
  // Limiting is a presentation operation and cannot perturb voice scheduling.

View on GitHub (pinned to 433685b202)