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 }