moeru-ai/airi · error · TypeError

PCM16 input must contain complete 16-bit samples.

Error message

PCM16 input must contain complete 16-bit samples.

What it means

Thrown by toFloat32FromPCM16 when the byte length of the PCM byte array is not an even multiple of 2 (Int16Array.BYTES_PER_ELEMENT). Every 16-bit PCM sample occupies exactly two little-endian bytes, so an odd-length buffer cannot represent complete samples and conversion is refused rather than silently dropping or misreading a byte.

Source

Thrown at packages/audio/src/encoding/wav.ts:65

  for (let i = 0; i < samples.length; i++) {
    const sample = Math.max(-1, Math.min(1, samples[i]))
    const value = sample < 0 ? sample * 0x8000 : sample * 0x7FFF
    dataView.setInt16(i * Int16Array.BYTES_PER_ELEMENT, value, true)
  }

  return output
}

/**
 * Converts little-endian signed PCM16 bytes to normalized Float32 PCM samples.
 *
 * @example
 * toFloat32FromPCM16(new Uint8Array([0, 128, 0, 0, 255, 127]))
 * // => Float32Array([-1, 0, 0.999969482421875])
 */
export function toFloat32FromPCM16(pcmBytes: Uint8Array): Float32Array<ArrayBuffer> {
  if (pcmBytes.byteLength % Int16Array.BYTES_PER_ELEMENT !== 0)
    throw new TypeError('PCM16 input must contain complete 16-bit samples.')

  const dataView = new DataView(pcmBytes.buffer, pcmBytes.byteOffset, pcmBytes.byteLength)
  const output = new Float32Array(pcmBytes.byteLength / Int16Array.BYTES_PER_ELEMENT)

  for (let i = 0; i < output.length; i++)
    output[i] = dataView.getInt16(i * Int16Array.BYTES_PER_ELEMENT, true) / 0x8000

  return output
}

/**
 * Encodes Float32 samples as a WAV file.
 *
 * @example
 * toWav(float32Samples.buffer, 24000)
 * // => WAV data with converted PCM16 samples
 */
export function toWav(buffer: ArrayBufferLike, sampleRate: number, channel = 1): ArrayBuffer {

View on GitHub (pinned to 0616eabd5b)

Solutions

  1. Verify pcmBytes.byteLength % 2 === 0 before calling; if odd, buffer the trailing byte and prepend it to the next chunk.
  2. If chunking, accumulate bytes and only convert on even boundaries (carry over the leftover byte).
  3. Check the upstream producer/transport (base64 decode, WebSocket frame, file slice) for truncation or off-by-one slicing.
  4. Confirm the source audio is actually 16-bit PCM, not 8-bit or 24-bit.

Example fix

// before
const floats = toFloat32FromPCM16(chunk) // throws on odd chunk

// after
if (chunk.byteLength % 2 !== 0) {
  pending = concat(pending ?? [], chunk)
  chunk = new Uint8Array(0)
}
const floats = toFloat32FromPCM16(chunk)
Defensive patterns

Strategy: validation

Validate before calling

if (pcmBytes.byteLength % Int16Array.BYTES_PER_ELEMENT !== 0) {
  // buffer the tail byte and wait for more data, or reject the chunk upstream
  throw new Error(`Incomplete PCM16 frame: ${pcmBytes.byteLength} bytes`)
}
const floats = toFloat32FromPCM16(pcmBytes)

Type guard

function hasCompletePCM16Frames(bytes: Uint8Array): boolean {
  return bytes.byteLength % Int16Array.BYTES_PER_ELEMENT === 0
}

Try / catch

try {
  features = base64ToFeatures(b64)
} catch (err) {
  if (err instanceof TypeError && err.message.includes('complete 16-bit samples')) {
    // drop/repair the truncated payload rather than crashing the stream
  } else throw err
}

Prevention

When it happens

Trigger: Calling toFloat32FromPCM16 (directly, or via pumpPcm16Input / base64ToFeatures) with a Uint8Array whose byteLength % 2 !== 0. Common with chunked stream reads that split a frame across chunks, or a base64 payload that was truncated by one byte.

Common situations: Audio capture callbacks delivering arbitrary byte counts; WebSocket/base64 transfer truncating payloads; slicing a buffer with a wrong offset/length; feeding 8-bit or 24-bit PCM into a PCM16 decoder; reinterpreting a subarray with a misaligned byteOffset.

Related errors


AI-assisted analysis of moeru-ai/airi@0616eabd5b (2026-08-28). Data as JSON: /api/errors/3b2854b8daf785a3. Report an issue: GitHub.