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.
I am a complete beginner in audio programming so trying to construct the wav header, I am following this link.
From the byte stream:
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
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,
);
}