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