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
- 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'].
- Clamp on creation: duration = Math.min(dur, 10), gain = Math.max(0, Math.min(1, g)), pan = Math.max(-1, Math.min(1, p)).
- Cap frequency at sampleRate/2 for the rate you will render at, not at an absolute constant.
- Fix NaN sources: guard optional lookups feeding v.start/v.frequency so undefined arithmetic never reaches the voice.
- 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
- Clamp gain and pan into [0,1] and [-1,1] at voice-creation time.
- Cap voice duration at 10 s and cap frequency at sampleRate/2 for the target render rate.
- Make kind a TypeScript union type ('tone' | 'noise') so typos fail at compile time.
- Never let optional lookups feed numeric fields — coerce/guard against undefined to avoid NaN.
- Filter voices through a validator before rendering rather than trusting producers.
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
- Codewhale terminal receipt contained a non-scalar field
- Duplicate .
- Duplicate policy identity.
- Facts must be scalar metadata, not content objects.
- Fleet task ' ' metadata.coordination_contracts must be an…
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)