encodeBytes static method

Uint8List encodeBytes(
  1. Float32List samples, {
  2. required int sampleRate,
  3. required int channels,
  4. int audioFormat = 1,
  5. int bitsPerSample = 16,
})

Encode normalized -1,1 float samples (interleaved) into a WAV byte buffer.

samples are interleaved: ch0, ch1, ..., chN, ch0, ch1, ... channels must divide samples.length.

By default writes 16-bit PCM. You can choose:

  • PCM: format=1, bitsPerSample in {8,16,24,32}
  • Float: format=3, bitsPerSample must be 32

Implementation

static Uint8List encodeBytes(
  Float32List samples, {
  required int sampleRate,
  required int channels,
  int audioFormat = 1, // 1 PCM, 3 IEEE float
  int bitsPerSample = 16,
}) {
  if (channels <= 0) throw ArgumentError.value(channels, 'channels');
  if (sampleRate <= 0) throw ArgumentError.value(sampleRate, 'sampleRate');
  if (samples.length % channels != 0) {
    throw ArgumentError(
      'samples.length (${samples.length}) must be a multiple of channels ($channels)',
    );
  }

  if (audioFormat == 1) {
    if (bitsPerSample != 8 &&
        bitsPerSample != 16 &&
        bitsPerSample != 24 &&
        bitsPerSample != 32) {
      throw ArgumentError('PCM bitsPerSample must be 8/16/24/32');
    }
  } else if (audioFormat == 3) {
    if (bitsPerSample != 32) {
      throw ArgumentError('Float WAV requires bitsPerSample=32');
    }
  } else {
    throw ArgumentError('audioFormat must be 1 (PCM) or 3 (float)');
  }

  final frames = samples.length ~/ channels;
  final blockAlign = (channels * bitsPerSample) ~/ 8;
  final byteRate = sampleRate * blockAlign;

  // data chunk size in bytes
  final dataSize = frames * blockAlign;

  // Minimal fmt chunk is 16 bytes for PCM/float (no extensible)
  const fmtChunkSize = 16;

  // RIFF size = 4 ("WAVE") + (8 + fmt) + (8 + data)
  final riffSize = 4 + (8 + fmtChunkSize) + (8 + dataSize);

  // Total file bytes = 8 ("RIFF"+size) + riffSize
  final totalSize = 8 + riffSize;

  final out = BytesBuilder(copy: false);

  void writeAscii4(String s) {
    if (s.length != 4) throw ArgumentError('FourCC must be 4 chars');
    out.add(Uint8List.fromList(s.codeUnits));
  }

  void writeU16LE(int v) {
    final b = ByteData(2)..setUint16(0, v, Endian.little);
    out.add(b.buffer.asUint8List());
  }

  void writeU32LE(int v) {
    final b = ByteData(4)..setUint32(0, v, Endian.little);
    out.add(b.buffer.asUint8List());
  }

  // ---- Header ----
  writeAscii4('RIFF');
  writeU32LE(riffSize);
  writeAscii4('WAVE');

  // ---- fmt chunk ----
  writeAscii4('fmt ');
  writeU32LE(fmtChunkSize);
  writeU16LE(audioFormat);
  writeU16LE(channels);
  writeU32LE(sampleRate);
  writeU32LE(byteRate);
  writeU16LE(blockAlign);
  writeU16LE(bitsPerSample);

  // ---- data chunk ----
  writeAscii4('data');
  writeU32LE(dataSize);

  // ---- Samples ----
  // Clamp to [-1,1] before conversion
  double clamp(double x) => x < -1.0 ? -1.0 : (x > 1.0 ? 1.0 : x);

  if (audioFormat == 3) {
    // 32-bit float
    final b = ByteData(dataSize);
    int o = 0;
    for (int i = 0; i < samples.length; i++) {
      final v = clamp(samples[i]).toDouble();
      b.setFloat32(o, v, Endian.little);
      o += 4;
    }
    out.add(b.buffer.asUint8List());
  } else {
    // PCM integer
    switch (bitsPerSample) {
      case 8:
        // unsigned 8-bit: 128 is zero
        final b = Uint8List(dataSize);
        for (int i = 0; i < samples.length; i++) {
          final v = clamp(samples[i]).toDouble();
          final u = (v * 128.0 + 128.0).round();
          b[i] = u.clamp(0, 255);
        }
        out.add(b);
        break;

      case 16:
        final b = ByteData(dataSize);
        int o = 0;
        for (int i = 0; i < samples.length; i++) {
          final v = clamp(samples[i]).toDouble();
          // scale to [-32768, 32767]
          int s = (v * 32768.0).round();
          s = s.clamp(-32768, 32767);
          b.setInt16(o, s, Endian.little);
          o += 2;
        }
        out.add(b.buffer.asUint8List());
        break;

      case 24:
        // 24-bit signed little-endian
        final b = Uint8List(dataSize);
        int o = 0;
        for (int i = 0; i < samples.length; i++) {
          final v = clamp(samples[i]).toDouble();
          int s = (v * 8388608.0).round(); // 2^23
          s = s.clamp(-8388608, 8388607);
          // Two's complement in 24 bits
          final u = s & 0xFFFFFF;
          b[o++] = (u & 0xFF);
          b[o++] = ((u >> 8) & 0xFF);
          b[o++] = ((u >> 16) & 0xFF);
        }
        out.add(b);
        break;

      case 32:
        final b = ByteData(dataSize);
        int o = 0;
        for (int i = 0; i < samples.length; i++) {
          final v = clamp(samples[i]).toDouble();
          int s = (v * 2147483648.0).round(); // 2^31
          s = s.clamp(-2147483648, 2147483647);
          b.setInt32(o, s, Endian.little);
          o += 4;
        }
        out.add(b.buffer.asUint8List());
        break;
    }
  }

  // Chunks are already even-sized here (fmt=16, data depends on align),
  // but RIFF requires word alignment *per chunk*. Since we wrote exact sizes,
  // and blockAlign guarantees dataSize is multiple of (bits/8*channels),
  // it can still be odd in PCM 8-bit mono (blockAlign=1). In that case, pad.
  if (dataSize.isOdd) {
    out.add(Uint8List(1)); // pad byte, NOT counted in chunk size
  }

  final bytes = out.toBytes();
  // Sanity check: total size might be +1 if padded
  if (bytes.length != totalSize && bytes.length != totalSize + 1) {
    // Don’t throw; just keep it as debug help.
    // ignore: avoid_print
    print(
      'WavWriter: size mismatch expected $totalSize(+pad) got ${bytes.length}',
    );
  }
  return bytes;
}