1 module waved.wav; 2 3 import std.range, 4 std.file, 5 std.array, 6 std.string; 7 8 import waved.utils; 9 10 /// Supports Microsoft WAV audio file format. 11 12 13 // wFormatTag 14 immutable int LinearPCM = 0x0001; 15 immutable int FloatingPointIEEE = 0x0003; 16 immutable int WAVE_FORMAT_EXTENSIBLE = 0xFFFE; 17 18 19 /// Decodes a WAV file. 20 /// Throws: WavedException on error. 21 Sound decodeWAV(string filepath) 22 { 23 auto bytes = cast(ubyte[]) std.file.read(filepath); 24 return decodeWAV(bytes); 25 } 26 27 /// Encodes a WAV file. 28 /// Throws: WavedException on error. 29 void encodeWAV(Sound sound, string filepath) 30 { 31 auto output = appender!(ubyte[])(); 32 output.encodeWAV(sound); 33 std.file.write(filepath, output.data); 34 } 35 36 /// Decodes a WAV. 37 /// Throws: WavedException on error. 38 Sound decodeWAV(R)(R input) if (isInputRange!R) 39 { 40 // check RIFF header 41 { 42 uint chunkId, chunkSize; 43 getRIFFChunkHeader(input, chunkId, chunkSize); 44 if (chunkId != RIFFChunkId!"RIFF") 45 throw new WavedException("Expected RIFF chunk."); 46 47 if (chunkSize < 4) 48 throw new WavedException("RIFF chunk is too small to contain a format."); 49 50 if (popBE!uint(input) != RIFFChunkId!"WAVE") 51 throw new WavedException("Expected WAVE format."); 52 } 53 54 bool foundFmt = false; 55 bool foundData = false; 56 57 58 int audioFormat; 59 int numChannels; 60 int sampleRate; 61 int byteRate; 62 int blockAlign; 63 int bitsPerSample; 64 65 Sound result; 66 // while chunk is not 67 while (!input.empty) 68 { 69 // Some corrupted WAV files in the wild finish with one 70 // extra 0 byte after an AFAn chunk, very odd 71 static if (hasLength!R) 72 { 73 if (input.length == 1 && input.front() == 0) 74 break; 75 } 76 77 uint chunkId, chunkSize; 78 getRIFFChunkHeader(input, chunkId, chunkSize); 79 if (chunkId == RIFFChunkId!"fmt ") 80 { 81 if (foundFmt) 82 throw new WavedException("Found several 'fmt ' chunks in RIFF file."); 83 84 foundFmt = true; 85 86 if (chunkSize < 16) 87 throw new WavedException("Expected at least 16 bytes in 'fmt ' chunk."); // found in real-world for the moment: 16 or 40 bytes 88 89 audioFormat = popLE!ushort(input); 90 if (audioFormat == WAVE_FORMAT_EXTENSIBLE) 91 throw new WavedException("No support for format WAVE_FORMAT_EXTENSIBLE yet."); // Reference: http://msdn.microsoft.com/en-us/windows/hardware/gg463006.aspx 92 93 if (audioFormat != LinearPCM && audioFormat != FloatingPointIEEE) 94 throw new WavedException(format("Unsupported audio format %s, only PCM and IEEE float are supported.", audioFormat)); 95 96 numChannels = popLE!ushort(input); 97 98 sampleRate = popLE!uint(input); 99 if (sampleRate <= 0) 100 throw new WavedException(format("Unsupported sample-rate %s.", cast(uint)sampleRate)); // we do not support sample-rate higher than 2^31hz 101 102 uint bytesPerSec = popLE!uint(input); 103 int bytesPerFrame = popLE!ushort(input); 104 bitsPerSample = popLE!ushort(input); 105 106 if (bitsPerSample != 8 && bitsPerSample != 16 && bitsPerSample != 24 && bitsPerSample != 32) 107 throw new WavedException(format("Unsupported bitdepth %s.", cast(uint)bitsPerSample)); 108 109 if (bytesPerFrame != (bitsPerSample / 8) * numChannels) 110 throw new WavedException("Invalid bytes-per-second, data might be corrupted."); 111 112 skipBytes(input, chunkSize - 16); 113 } 114 else if (chunkId == RIFFChunkId!"data") 115 { 116 if (foundData) 117 throw new WavedException("Found several 'data' chunks in RIFF file."); 118 119 if (!foundFmt) 120 throw new WavedException("'fmt ' chunk expected before the 'data' chunk."); 121 122 int bytePerSample = bitsPerSample / 8; 123 uint frameSize = numChannels * bytePerSample; 124 if (chunkSize % frameSize != 0) 125 throw new WavedException("Remaining bytes in 'data' chunk, inconsistent with audio data type."); 126 127 uint numFrames = chunkSize / frameSize; 128 uint numSamples = numFrames * numChannels; 129 130 result.samples.length = numSamples; 131 132 if (audioFormat == FloatingPointIEEE) 133 { 134 if (bytePerSample == 4) 135 { 136 for (uint i = 0; i < numSamples; ++i) 137 result.samples[i] = popFloatLE(input); 138 } 139 else if (bytePerSample == 8) 140 { 141 for (uint i = 0; i < numSamples; ++i) 142 result.samples[i] = popDoubleLE(input); 143 } 144 else 145 throw new WavedException("Unsupported bit-depth for floating point data, should be 32 or 64."); 146 } 147 else if (audioFormat == LinearPCM) 148 { 149 if (bytePerSample == 1) 150 { 151 for (uint i = 0; i < numSamples; ++i) 152 { 153 ubyte b = popUbyte(input); 154 result.samples[i] = (b - 128) / 127.0; 155 } 156 } 157 else if (bytePerSample == 2) 158 { 159 for (uint i = 0; i < numSamples; ++i) 160 { 161 int s = popLE!short(input); 162 result.samples[i] = s / 32767.0; 163 } 164 } 165 else if (bytePerSample == 3) 166 { 167 for (uint i = 0; i < numSamples; ++i) 168 { 169 int s = pop24bitsLE!R(input); 170 result.samples[i] = s / 8388607.0; 171 } 172 } 173 else if (bytePerSample == 4) 174 { 175 for (uint i = 0; i < numSamples; ++i) 176 { 177 int s = popLE!int(input); 178 result.samples[i] = s / 2147483648.0; 179 } 180 } 181 else 182 throw new WavedException("Unsupported bit-depth for integer PCM data, should be 8, 16, 24 or 32 bits."); 183 } 184 else 185 assert(false); // should have been handled earlier, crash 186 187 foundData = true; 188 } 189 else 190 { 191 // ignore unrecognized chunks 192 skipBytes(input, chunkSize); 193 } 194 } 195 196 if (!foundFmt) 197 throw new WavedException("'fmt ' chunk not found."); 198 199 if (!foundData) 200 throw new WavedException("'data' chunk not found."); 201 202 203 result.channels = numChannels; 204 result.sampleRate = sampleRate; 205 206 return result; 207 } 208 209 210 /// Encodes a WAV. 211 void encodeWAV(R)(ref R output, Sound sound) if (isOutputRange!(R, ubyte)) 212 { 213 // for now let's just pretend always saving to 32-bit float is OK 214 215 216 // Avoid a number of edge cases. 217 if (sound.channels < 0 || sound.channels > 1024) 218 throw new WavedException(format("Can't save a WAV with %s channels.", sound.channels)); 219 220 // RIFF header 221 output.writeRIFFChunkHeader(RIFFChunkId!"RIFF", 4 + (4 + 4 + 16) + (4 + 4 + float.sizeof * sound.samples.length) ); 222 output.writeBE!uint(RIFFChunkId!"WAVE"); 223 224 // 'fmt ' sub-chunk 225 output.writeRIFFChunkHeader(RIFFChunkId!"fmt ", 0x10); 226 output.writeLE!ushort(FloatingPointIEEE); 227 228 output.writeLE!ushort(cast(ushort)(sound.channels)); 229 230 if (sound.sampleRate == 0) 231 throw new WavedException("Can't save a WAV with sample-rate of 0hz. Assign the sampleRate field."); 232 233 output.writeLE!uint(cast(ushort)(sound.sampleRate)); 234 235 size_t bytesPerSec = sound.sampleRate * sound.channels * float.sizeof; 236 output.writeLE!uint( cast(uint)(bytesPerSec)); 237 238 int bytesPerFrame = cast(int)(sound.channels * float.sizeof); 239 output.writeLE!ushort(cast(ushort)bytesPerFrame); 240 241 output.writeLE!ushort(32); 242 243 // data sub-chunk 244 output.writeRIFFChunkHeader(RIFFChunkId!"data", float.sizeof * sound.samples.length); 245 foreach (float f; sound.samples) 246 output.writeFloatLE(f); 247 } 248