readBytes static method
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)
final validBitsPerSample = getU16(p);
p += 2;
final channelMask = getU32(p);
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, but you can inspect validBitsPerSample if you want.
// (We ignore channelMask here; channels count already tells us interleaving.)
// ignore: unused_local_variable
final _ = channelMask;
// ignore: unused_local_variable
final __ = validBitsPerSample;
}
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,
);
}