1 module pixelperfectengine.system.wavfile;
2 
3 static import std.file;
4 import std.format;
5 
6 
7 /// parse little-endian ushort
8 private ushort toUshort(const ubyte[] array, uint offset) {
9    // friggin' order of operations had me bughunt for half an hour
10    return cast(ushort)(array[offset] + (array[offset+1]<<8));
11 }
12 
13 /// parse little-endian uint
14 private uint toUint(const ubyte[] array, uint offset) {
15    return array[offset] + (array[offset+1]<<8) + (array[offset+2]<<16) + (array[offset+3]<<24);
16 }
17 
18 /**
19 Basic WAV file loader
20 */
21 class WavFile {
22    /**
23    WAV file header
24    */
25    struct WavHeader {
26       immutable uint size; /// number of bytes after this (overall file size - 8)
27       immutable ushort format; /// type of format. 1 means PCM
28       immutable ushort channels; /// no. of channels
29       immutable uint samplerate; /// samples per second
30       immutable uint bytesPerSecond; /// samplerate * bitsPerSample * channels / 8
31       immutable ushort bytesPerSample; /// bitsPerSample * channels / 8
32       immutable ushort bitsPerSample; /// bits per sample - 8-16-etc
33       immutable uint dataSize; /// size of data section
34       /**
35       default constructor from raw WAV file data
36       */
37       this (const ubyte[] rawData) {
38          // this *might* be out of spec, as in theory fmt chunks could be bigger if indicated by offset 16-20 (uint)
39          // so any offset above 35 should be dynamicall calculated
40          // but I don't think there's any WAV files like that
41          assert(rawData[0..4] == "RIFF", "Header corrupted: 'RIFF' string missing");
42          assert(rawData[8..12] == "WAVE", "Header corrupted: 'WAVE' string missing");
43          assert(rawData[12..16] == "fmt ", "Header corrupted: 'fmt ' string missing");
44          assert(rawData[36..40] == "data", "Header corrupted: 'data' string missing");
45          this.size = toUint(rawData, 4);
46          this.format = toUshort(rawData, 20);
47          this.channels = toUshort(rawData, 22);
48          this.samplerate = toUint(rawData, 24);
49          this.bytesPerSecond = toUint(rawData, 28);
50          this.bytesPerSample = toUshort(rawData, 32);
51          this.bitsPerSample = toUshort(rawData, 34);
52          this.dataSize = toUint(rawData, 40);
53 
54          assert(
55             this.bytesPerSecond == this.bitsPerSample * this.samplerate * this.channels / 8,
56             .format(
57                "Header corrupted: stored bytes per second value %s does not match calculated value %s",
58                this.bytesPerSecond, 
59                this.bitsPerSample * this.samplerate * this.channels / 8
60             )
61          );
62 
63          assert(
64             this.bytesPerSample == this.bitsPerSample * this.channels / 8,
65             .format(
66                "Header corrupted: stored bytes per sample value %s does not match calculated value %s",
67                this.bytesPerSample,
68                this.bitsPerSample * this.channels / 8
69             )
70          );
71 
72          assert(
73             this.dataSize == this.size - 44 + 8,
74             .format(
75                "Header corrupted: data size and file size does not add up!"
76             )
77          );
78       }
79    }
80 
81    // samples always start at offset 44
82    // but their interpretation depends on the header
83    public immutable ubyte[] rawData; /// raw file contents
84    public immutable WavHeader header; /// header info
85 
86 
87    /// load a wav file from path
88    this(string filename) {
89       rawData = cast(immutable(ubyte[]))std.file.read(filename);
90       this.header = WavHeader(this.rawData[0..44]);
91       assert(this.header.size + 8 == this.rawData.length, format(
92          "File size is corrupted: size is %s but it should be %s", 
93          this.header.size, this.rawData.length - 8)
94       );
95    }
96 
97 
98 
99 }