Hmbown/CodeWhale · error · Error

Invalid pet voice.

Error message

Invalid pet voice.

What it means

renderPetPCM() validates every PetVoice before synthesis. A voice must have finite start, duration, frequency, gain, and pan, with start >= 0, 0 < duration <= 10, 0 < frequency <= sampleRate/2 (Nyquist), 0 <= gain <= 1, |pan| <= 1, and kind of 'tone' or 'noise'. This rejects voices that would produce non-finite samples or alias above Nyquist.

Solutions

  1. Validate each voice before calling: finite numbers, start >= 0, 0 < duration <= 10, 0 < frequency <= sampleRate/2, 0 <= gain <= 1, |pan| <= 1, kind in ['tone','noise'].
  2. Clamp on creation: duration = Math.min(dur, 10), gain = Math.max(0, Math.min(1, g)), pan = Math.max(-1, Math.min(1, p)).
  3. Cap frequency at sampleRate/2 for the rate you will render at, not at an absolute constant.
  4. Fix NaN sources: guard optional lookups feeding v.start/v.frequency so undefined arithmetic never reaches the voice.
  5. Filter invalid voices out of the array before rendering instead of failing the whole batch.

Example fix

// before
const pcm = renderPetPCM([{ id, start: -0.5, duration: 12, frequency: 30000, gain: 2, pan: 0, kind: 'beep' }], 0, 48000);

// after
const voices = [{ id, start: Math.max(0, s), duration: Math.min(10, d),
  frequency: Math.min(f, 48000 / 2), gain: clamp(g, 0, 1), pan: clamp(p, -1, 1), kind: 'tone' as const }];
const pcm = renderPetPCM(voices, 0, 48000);
Defensive patterns

Strategy: validation

Validate before calling

function isValidVoice(v: PetVoice, sampleRate: number): boolean {
  return [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);
}
const safe = voices.filter(v => isValidVoice(v, 48000));

Type guard

function isPetVoice(v: unknown, sampleRate: number): v is PetVoice {
  const w = v as PetVoice;
  return !!w && typeof w.id === 'string'
    && Number.isFinite(w.start) && w.start >= 0
    && Number.isFinite(w.duration) && w.duration > 0 && w.duration <= 10
    && Number.isFinite(w.frequency) && w.frequency > 0 && w.frequency <= sampleRate / 2
    && Number.isFinite(w.gain) && w.gain >= 0 && w.gain <= 1
    && Number.isFinite(w.pan) && Math.abs(w.pan) <= 1
    && (w.kind === 'tone' || w.kind === 'noise');
}

Try / catch

try {
  const pcm = renderPetPCM(voices, start, length);
} catch (err) {
  if (err instanceof Error && err.message === 'Invalid pet voice.') {
    console.error('dropping voice', voices); // inspect and fix producer
    const pcm = renderPetPCM(voices.filter(v => isValidVoice(v, 48000)), start, length);
  } else throw err;
}

Prevention

When it happens

Trigger: Passing a voice array containing a voice with: a negative start; zero or negative duration; duration > 10 seconds; frequency <= 0 or above sampleRate/2 (e.g. 30 kHz tone at 48 kHz); gain outside 0–1; pan outside [-1, 1]; kind not 'tone'/'noise'; or any NaN/Infinity in the numeric fields (e.g. gain = v.gain * undefined).

Common situations: Generating voices with durations derived from user input that can exceed 10 s; synthesizing high-frequency alerts at low sample rates (Nyquist violation); gain left as a raw unmapped 0–255 value; kind typo like 'beep'; NaN propagating from a failed lookup used in frequency computation.

Understand the failure class

Background: "Must be a positive integer", "Invalid value", "Unsupported": the invalid-argument-value error family, when a library rejects the value you pass — this error's family across 35 libraries.

Related errors


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

Appendix: source

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

  }
}

/** 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.
  for (let i = 0; i < length; i++) { left[i] = clamp(left[i], -1, 1); right[i] = clamp(right[i], -1, 1); }
  return { left, right };
}

View on GitHub (pinned to 433685b202)