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