flutterdartaudiowavbytestream

How to properly construct a wav file from a stream of bytes in flutter?


I am receiving bytes from a BLE device. The device is recording audio for 5 secs and sends it to an Android phone in pcm format.

  • 8khz
  • 4 channels
  • 16-bit sample (2 bytes per sample).
  • number of samples = 160 000
  • 5 secs recording

I am a complete beginner in audio programming so trying to construct the wav header, I am following this link.

From the byte stream:

  • The 3rd - 10th bytes are the data size to be received (ignore the first 3 bytes)
  • the next bytes are the data in the form:

Channel-1 1st data point = 0th & 1st position, where 0th is LSB and 1st is MSB

Channel-2 1st data point = 2nd & 3rd position, where 0th is LSB and 1st is MSB

...

My questions are, how can I represent the above in dart and is my header below ok?

The header are these values in hex, little-endian except for the strings

52 49 46 46  “RIFF”
24 E2 04 00  FILE SIZE =  320044 - 8 = 320036
57 41 56 45  “WAVE”
66 6d 74 20  fmt
    
10 00 00 00  size of fmt subchunk = 16
    
01 00        pcm = 1
04 00        channels = 4
40 1F 00 00  sample rate = 8000 

00 FA 00 00  byteRate = 64 000

08 00        blockAlign = 8
10 00        bits per sample = 16
    
64 61 74 61  “data”
    
00 E2 04 00  size of data = 320000

And this is the above header into a Uint8List to create a File from it with the data appended as Int8List

final header = Uint8List.fromList([ 82, 73, 70, 70, 36, 226, 4, 0, 87, 65, 86, 69, 102, 109, 116, 32, 1, 0, 0, 0, 0, 1, 0, 4, 64, 31, 0, 0, 0, 250, 0, 0, 8, 0, 1, 0, 100, 97, 116, 97, 0, 226, 4, 0, ]);

I was expecting to be able to play the wav file with the right header even if I dont modify the data bytes but keep getting this error:

I/ExoPlayerImpl(19598): Init 5dd78f5 [ExoPlayerLib/2.17.1] [raven, Pixel 6 Pro, Google, 33] W/io.platform.dev(19598): Accessing hidden method Landroid/media/AudioTrack;->getLatency()I (unsupported, reflection, allowed) E/LoadTask(19598): Unexpected exception loading stream E/LoadTask(19598): java.lang.IllegalStateException E/LoadTask(19598): at com.google.android.exoplayer2.util.Assertions.checkState(Assertions.java:84) E/LoadTask(19598): at com.google.android.exoplayer2.extractor.wav.WavHeaderReader.readFormat(WavHeaderReader.java:100) E/LoadTask(19598): at com.google.android.exoplayer2.extractor.wav.WavExtractor.readFormat(WavExtractor.java:175) E/LoadTask(19598): at com.google.android.exoplayer2.extractor.wav.WavExtractor.read(WavExtractor.java:134) E/LoadTask(19598): at com.google.android.exoplayer2.source.BundledExtractorsAdapter.read(BundledExtractorsAdapter.java:127) E/LoadTask(19598): at com.google.android.exoplayer2.source.ProgressiveMediaPeriod$ExtractingLoadable.load(ProgressiveMediaPeriod.java:1042) E/LoadTask(19598): at com.google.android.exoplayer2.upstream.Loader$LoadTask.run(Loader.java:412) E/LoadTask(19598): at java.util.concurrent.ThreadPoolExecutor.runWorker(ThreadPoolExecutor.java:1137) E/LoadTask(19598): at java.util.concurrent.ThreadPoolExecutor$Worker.run(ThreadPoolExecutor.java:637) E/LoadTask(19598): at java.lang.Thread.run(Thread.java:1012) E/ExoPlayerImplInternal(19598): Playback error E/ExoPlayerImplInternal(19598): com.google.android.exoplayer2.ExoPlaybackException: Source error


Solution

  • See the comments on the question. There are several typos in your hand-crafted header. It's generally best to generate the header parts in code.

    Here's an abstract class that you can extend with a concrete class that does most of the work of setting the appropriate values of the base class. (For historical reasons, I've always used the number of 'samples' as the driver to calculate the other values (including the length) but feel free to modify to, say, be driven off the number of bytes.) I've included an example concrete class which seems to give correct values but isn't tested.

    abstract class WavHeader {
      WavHeader(
        this.tag,
        this.channels,
        this.sampleRate,
        this.bitsPerSample,
        this.blockAlign,
        this.samples,
        this.length,
      );
    
      int get overallLength =>
          headerTemplate.length -
          8 +
          fmtTemplate.length +
          factTemplate.length +
          dataTemplate.length +
          length;
    
      Uint8List get header {
        final bb = BytesBuilder(copy: false)
          ..add(riffHeader)
          ..add(fmtHeader)
          ..add(factHeader)
          ..add(dataHeader);
        return bb.toBytes();
      }
    
      List<int> get riffHeader {
        final list = Uint8List.fromList(headerTemplate);
        list.buffer.asByteData().setUint32(4, overallLength, Endian.little);
        return list;
      }
    
      List<int> get fmtHeader {
        final list = Uint8List.fromList(fmtTemplate);
        list.buffer.asByteData()
          ..setUint16(8, tag, Endian.little)
          ..setUint16(10, channels, Endian.little)
          ..setUint32(12, sampleRate, Endian.little)
          ..setUint32(16, channels * sampleRate * bitsPerSample ~/ 8, Endian.little)
          ..setUint16(20, blockAlign, Endian.little)
          ..setUint16(22, bitsPerSample, Endian.little);
        return list;
      }
    
      List<int> get factHeader {
        final list = Uint8List.fromList(factTemplate);
        list.buffer.asByteData().setUint32(8, samples, Endian.little);
        return list;
      }
    
      List<int> get dataHeader {
        final list = Uint8List.fromList(dataTemplate);
        list.buffer.asByteData().setUint32(4, length, Endian.little);
        return list;
      }
    
      final int tag;
      final int channels;
      final int sampleRate;
      final int bitsPerSample;
      final int blockAlign;
      final int samples;
      final int length;
    
      final headerTemplate = <int>[
        0x52, 0x49, 0x46, 0x46, // RIFF
        0, 0, 0, 0, // length placeholder
        0x57, 0x41, 0x56, 0x45 // WAVE
      ];
    
      final fmtTemplate = <int>[
        0x66, 0x6d, 0x74, 0x20, // fmt<space>
        0x10, 0, 0, 0, // length 16
        0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0
      ];
    
      final factTemplate = <int>[
        // fact, length = 4
        0x66, 0x61, 0x63, 0x74, // fact
        4, 0, 0, 0, // length 4
        0, 0, 0, 0
      ];
    
      final dataTemplate = <int>[
        0x64, 0x61, 0x74, 0x61, //data
        0, 0, 0, 0
      ];
    }
    
    class PcmWavHeader extends WavHeader {
      PcmWavHeader(int samples, int channels)
          : super(
              1 /* PCM */,
              channels,
              8000,
              16,
              2 * channels,
              samples,
              channels * samples * 2,
            );
    }