readBytes static method

WavData readBytes(
  1. Uint8List bytes
)

Read a WAV from bytes (e.g., network, asset, or file you already loaded).

Implementation

static WavData readBytes(Uint8List bytes) {
  final bd = ByteData.sublistView(bytes);

  int o = 0;
  String read4CC() {
    if (o + 4 > bd.lengthInBytes) {
      throw FormatException('Unexpected EOF while reading FourCC at $o');
    }
    final s = String.fromCharCodes([
      bd.getUint8(o),
      bd.getUint8(o + 1),
      bd.getUint8(o + 2),
      bd.getUint8(o + 3),
    ]);
    o += 4;
    return s;
  }

  int readU32LE() {
    final v = bd.getUint32(o, Endian.little);
    o += 4;
    return v;
  }

  // int readU16LE() {
  //   final v = bd.getUint16(o, Endian.little);
  //   o += 2;
  //   return v;
  // }

  // ---- RIFF header ----
  final riff = read4CC();
  if (riff != 'RIFF' && riff != 'RIFX') {
    throw FormatException('Not a RIFF WAV file (got $riff)');
  }
  final isLittleEndian = riff == 'RIFF';
  if (!isLittleEndian) {
    // RIFX is big-endian WAV; rare.
    throw UnsupportedError(
      'RIFX (big-endian) WAV not supported in this reader.',
    );
  }

  // final riffSize = readU32LE(); // not strictly needed
  final wave = read4CC();
  if (wave != 'WAVE') {
    throw FormatException('RIFF is not WAVE (got $wave)');
  }

  // ---- Scan chunks ----
  int? fmtOffset, fmtSize;
  int? dataOffset, dataSize;

  // Chunks start at current offset; each: 4cc + u32 size + payload (+ pad to even)
  while (o + 8 <= bd.lengthInBytes) {
    final id = read4CC();
    final size = readU32LE();
    final payloadOffset = o;

    if (id == 'fmt ') {
      fmtOffset = payloadOffset;
      fmtSize = size;
    } else if (id == 'data') {
      dataOffset = payloadOffset;
      dataSize = size;
      // We could break here, but some files put junk after data; fmt must already be found.
      // We'll break only if we already have fmt.
      if (fmtOffset != null) break;
    }

    // Skip payload
    o = payloadOffset + size;
    // Pad byte if size is odd (RIFF chunks are word-aligned)
    if (size.isOdd) o += 1;
  }

  if (fmtOffset == null || fmtSize == null) {
    throw FormatException('Missing fmt chunk');
  }
  if (dataOffset == null || dataSize == null) {
    throw FormatException('Missing data chunk');
  }
  if (fmtOffset + fmtSize > bd.lengthInBytes) {
    throw FormatException('fmt chunk exceeds file length');
  }
  if (dataOffset + dataSize > bd.lengthInBytes) {
    throw FormatException('data chunk exceeds file length');
  }

  // ---- Parse fmt ----
  int p = fmtOffset;

  int getU16(int off) => bd.getUint16(off, Endian.little);
  int getU32(int off) => bd.getUint32(off, Endian.little);

  final wFormatTag = getU16(p);
  p += 2;
  final nChannels = getU16(p);
  p += 2;
  final nSamplesPerSec = getU32(p);
  p += 4;
  final nAvgBytesPerSec = getU32(p);
  p += 4;
  final nBlockAlign = getU16(p);
  p += 2;
  final wBitsPerSample = getU16(p);
  p += 2;

  int resolvedFormat = wFormatTag; // 1 PCM, 3 float, 0xFFFE extensible

  // If fmt is extensible, decode subformat
  if (wFormatTag == 0xFFFE) {
    if (fmtSize < 18) {
      throw FormatException('WAVE_FORMAT_EXTENSIBLE fmt chunk too small');
    }
    final cbSize = getU16(p);
    p += 2;
    if (cbSize < 22 || fmtSize < 18 + cbSize) {
      // Spec says cbSize usually 22 for extensible.
      // Some files can be slightly weird, but we require subformat GUID bytes.
      throw FormatException('Invalid extensible fmt extension size');
    }

    // Skip: validBitsPerSample (2), channelMask (4)
    p += 2;
    p += 4;

    // SubFormat GUID (16 bytes). We only need to identify PCM vs IEEE float.
    // GUID layout in WAV is little-endian for first 3 fields.
    final sub0 = bd.getUint32(p, Endian.little);
    p += 4; // Data1
    final sub1 = bd.getUint16(p, Endian.little);
    p += 2; // Data2
    final sub2 = bd.getUint16(p, Endian.little);
    p += 2; // Data3
    // Next 8 bytes are big-endian byte order (as stored)
    final b8 = <int>[];
    for (int i = 0; i < 8; i++) {
      b8.add(bd.getUint8(p + i));
    }
    p += 8;

    // Known subformats:
    // PCM  : {00000001-0000-0010-8000-00AA00389B71}
    // FLOAT: {00000003-0000-0010-8000-00AA00389B71}
    final isKsd =
        (sub1 == 0x0000) &&
        (sub2 == 0x0010) &&
        (b8[0] == 0x80) &&
        (b8[1] == 0x00) &&
        (b8[2] == 0x00) &&
        (b8[3] == 0xAA) &&
        (b8[4] == 0x00) &&
        (b8[5] == 0x38) &&
        (b8[6] == 0x9B) &&
        (b8[7] == 0x71);

    if (!isKsd) {
      throw UnsupportedError('Unsupported extensible subformat GUID');
    }
    if (sub0 == 0x00000001) {
      resolvedFormat = 1; // PCM
    } else if (sub0 == 0x00000003) {
      resolvedFormat = 3; // IEEE float
    } else {
      throw UnsupportedError(
        'Unsupported extensible subformat: 0x${sub0.toRadixString(16)}',
      );
    }

    // Some files use validBitsPerSample < bitsPerSample; keep the container
    // bits for decoding. The channel mask is ignored because channel count
    // already tells us interleaving.
  }

  if (nChannels <= 0) {
    throw FormatException('Invalid channels: $nChannels');
  }
  if (nBlockAlign <= 0) {
    throw FormatException('Invalid blockAlign: $nBlockAlign');
  }
  if (wBitsPerSample <= 0) {
    throw FormatException('Invalid bitsPerSample: $wBitsPerSample');
  }

  // ---- Decode data ----
  final dataBd = bd; // same buffer
  final dataStart = dataOffset;
  final dataBytes = dataSize;

  // Ensure whole frames
  final usableBytes = dataBytes - (dataBytes % nBlockAlign);
  final frames = usableBytes ~/ nBlockAlign;
  final totalSamples = frames * nChannels;
  final out = Float32List(totalSamples);

  int di = dataStart;
  int oi = 0;

  double clamp1(double x) => x < -1.0 ? -1.0 : (x > 1.0 ? 1.0 : x);

  if (resolvedFormat == 1) {
    // PCM integer
    switch (wBitsPerSample) {
      case 8:
        // Unsigned [0..255] with 128 as zero
        for (int i = 0; i < totalSamples; i++) {
          final u = dataBd.getUint8(di);
          di += 1;
          out[oi++] = ((u - 128) / 128.0).toDouble();
        }
        break;

      case 16:
        for (int i = 0; i < totalSamples; i++) {
          final s = dataBd.getInt16(di, Endian.little);
          di += 2;
          out[oi++] = (s / 32768.0).toDouble();
        }
        break;

      case 24:
        for (int i = 0; i < totalSamples; i++) {
          final b0 = dataBd.getUint8(di);
          final b1 = dataBd.getUint8(di + 1);
          final b2 = dataBd.getUint8(di + 2);
          di += 3;

          int v = (b0) | (b1 << 8) | (b2 << 16);
          // sign-extend 24-bit
          if (v & 0x800000 != 0) v |= 0xFF000000;
          out[oi++] = (v / 8388608.0).toDouble(); // 2^23
        }
        break;

      case 32:
        for (int i = 0; i < totalSamples; i++) {
          final s = dataBd.getInt32(di, Endian.little);
          di += 4;
          out[oi++] = (s / 2147483648.0).toDouble(); // 2^31
        }
        break;

      default:
        throw UnsupportedError(
          'Unsupported PCM bitsPerSample: $wBitsPerSample',
        );
    }
  } else if (resolvedFormat == 3) {
    // IEEE float
    if (wBitsPerSample != 32) {
      throw UnsupportedError(
        'Unsupported float bitsPerSample: $wBitsPerSample (expected 32)',
      );
    }
    for (int i = 0; i < totalSamples; i++) {
      final f = dataBd.getFloat32(di, Endian.little);
      di += 4;
      out[oi++] = clamp1(f).toDouble();
    }
  } else {
    throw UnsupportedError(
      'Unsupported audio format: 0x${resolvedFormat.toRadixString(16)}',
    );
  }

  return WavData(
    sampleRate: nSamplesPerSec,
    channels: nChannels,
    bitsPerSample: wBitsPerSample,
    audioFormat: resolvedFormat,
    byteRate: nAvgBytesPerSec,
    blockAlign: nBlockAlign,
    samples: out,
  );
}