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 }