1 /++ 2 Basic .wav file reading and writing. 3 4 History: 5 Written May 15, 2020, but loosely based on code I wrote a 6 long time ago, at least August 2008 which is the oldest 7 file I have generated from the original code. 8 9 The old code could only write files, the reading support 10 was all added in 2020. 11 +/ 12 module arsd.wav; 13 14 import core.stdc.stdio; 15 16 /++ 17 18 +/ 19 struct WavWriter { 20 private FILE* fp; 21 22 /++ 23 Opens the file with the given header params. 24 25 Make sure you pass the correct params to header, except, 26 if you have a seekable stream, the data length can be zero 27 and it will be fixed when you close. If you have a non-seekable 28 stream though, you must give the size up front. 29 30 If you need to go to memory, the best way is to just 31 append your data to your own buffer, then create a [WavFileHeader] 32 separately and prepend it. Wav files are simple, aside from 33 the header and maybe a terminating byte (which isn't really important 34 anyway), there's nothing special going on. 35 36 Throws: Exception on error from [open]. 37 38 --- 39 auto writer = WavWriter("myfile.wav", WavFileHeader(44100, 2, 16)); 40 writer.write(shortSamples); 41 --- 42 +/ 43 this(string filename, WavFileHeader header) { 44 this.header = header; 45 46 if(!open(filename)) 47 throw new Exception("Couldn't open file for writing"); // FIXME: errno 48 } 49 50 /++ 51 `WavWriter(WavFileHeader(44100, 2, 16));` 52 +/ 53 this(WavFileHeader header) @nogc nothrow { 54 this.header = header; 55 } 56 57 /++ 58 Calls [close]. Errors are ignored. 59 +/ 60 ~this() @nogc { 61 close(); 62 } 63 64 @disable this(this); 65 66 private uint size; 67 private WavFileHeader header; 68 69 @nogc: 70 71 /++ 72 Returns: true on success, false on error. Check errno for details. 73 +/ 74 bool open(string filename) { 75 assert(fp is null); 76 assert(filename.length < 290); 77 78 char[300] fn; 79 fn[0 .. filename.length] = filename[]; 80 fn[filename.length] = 0; 81 82 fp = fopen(fn.ptr, "wb"); 83 if(fp is null) 84 return false; 85 if(fwrite(&header, header.sizeof, 1, fp) != 1) 86 return false; 87 88 return true; 89 } 90 91 /++ 92 Writes 8-bit samples to the file. You must have constructed the object with an 8 bit header. 93 94 Returns: true on success, false on error. Check errno for details. 95 +/ 96 bool write(ubyte[] data) { 97 assert(header.bitsPerSample == 8); 98 if(fp is null) 99 return false; 100 if(fwrite(data.ptr, 1, data.length, fp) != data.length) 101 return false; 102 size += data.length; 103 return true; 104 } 105 106 /++ 107 Writes 16-bit samples to the file. You must have constructed the object with 16 bit header. 108 109 Returns: true on success, false on error. Check errno for details. 110 +/ 111 bool write(short[] data) { 112 assert(header.bitsPerSample == 16); 113 if(fp is null) 114 return false; 115 if(fwrite(data.ptr, 2, data.length, fp) != data.length) 116 return false; 117 size += data.length * 2; 118 return true; 119 } 120 121 /++ 122 Returns: true on success, false on error. Check errno for details. 123 +/ 124 bool close() { 125 if(fp is null) 126 return true; 127 128 // pad odd sized file as required by spec... 129 if(size & 1) { 130 fputc(0, fp); 131 } 132 133 if(!header.dataLength) { 134 // put the length back at the beginning of the file 135 if(fseek(fp, 0, SEEK_SET) != 0) 136 return false; 137 auto n = header.withDataLengthInBytes(size); 138 if(fwrite(&n, 1, n.sizeof, fp) != 1) 139 return false; 140 } else { 141 assert(header.dataLength == size); 142 } 143 if(fclose(fp)) 144 return false; 145 fp = null; 146 return true; 147 } 148 } 149 150 version(LittleEndian) {} else static assert(0, "just needs endian conversion coded in but i was lazy"); 151 152 align(1) 153 /// 154 struct WavFileHeader { 155 align(1): 156 const ubyte[4] header = ['R', 'I', 'F', 'F']; 157 int topSize; // dataLength + 36 158 const ubyte[4] type = ['W', 'A', 'V', 'E']; 159 const ubyte[4] fmtHeader = ['f', 'm', 't', ' ']; 160 const int fmtHeaderSize = 16; 161 const ushort audioFormat = 1; // PCM 162 163 ushort numberOfChannels; 164 uint sampleRate; 165 166 uint bytesPerSeconds; // bytesPerSampleTimesChannels * sampleRate 167 ushort bytesPerSampleTimesChannels; // bitsPerSample * channels / 8 168 169 ushort bitsPerSample; // 16 170 171 const ubyte[4] dataHeader = ['d', 'a', 't', 'a']; 172 uint dataLength; 173 // data follows. put a 0 at the end if dataLength is odd. 174 175 /// 176 this(uint sampleRate, ushort numberOfChannels, ushort bitsPerSample, uint dataLengthInBytes = 0) @nogc pure @safe nothrow { 177 assert(bitsPerSample == 8 || bitsPerSample == 16); 178 179 this.numberOfChannels = numberOfChannels; 180 this.sampleRate = sampleRate; 181 this.bitsPerSample = bitsPerSample; 182 183 this.bytesPerSampleTimesChannels = cast(ushort) (numberOfChannels * bitsPerSample / 8); 184 this.bytesPerSeconds = this.bytesPerSampleTimesChannels * sampleRate; 185 186 this.topSize = dataLengthInBytes + 36; 187 this.dataLength = dataLengthInBytes; 188 } 189 190 /// 191 WavFileHeader withDataLengthInBytes(int dataLengthInBytes) const @nogc pure @safe nothrow { 192 return WavFileHeader(sampleRate, numberOfChannels, bitsPerSample, dataLengthInBytes); 193 } 194 } 195 static assert(WavFileHeader.sizeof == 44); 196 197 198 /++ 199 After construction, the parameters are set and you can set them. 200 After that, you process the samples range-style. 201 202 It ignores chunks in the file that aren't the basic standard. 203 It throws exceptions if it isn't a bare-basic PCM wav file. 204 205 See [wavReader] for the convenience constructors. 206 207 Note that if you are reading a 16 bit file (`bitsPerSample == 16`), 208 you'll actually need to `cast(short[]) front`. 209 210 --- 211 auto reader = wavReader(data[]); 212 foreach(chunk; reader) 213 play(chunk); 214 --- 215 +/ 216 struct WavReader(Range) { 217 const ushort numberOfChannels; 218 const int sampleRate; 219 const ushort bitsPerSample; 220 int dataLength; // don't modify plz 221 222 private uint remainingDataLength; 223 224 private Range underlying; 225 226 private const(ubyte)[] frontBuffer; 227 228 static if(is(Range == CFileChunks)) { 229 this(FILE* fp) { 230 underlying = CFileChunks(fp); 231 this(0); 232 } 233 } else { 234 this(Range r) { 235 this.underlying = r; 236 this(0); 237 } 238 } 239 240 private this(int _initializationDummyVariable) { 241 this.frontBuffer = underlying.front; 242 243 WavFileHeader header; 244 ubyte[] headerBytes = (cast(ubyte*) &header)[0 .. header.sizeof - 8]; 245 246 if(this.frontBuffer.length >= headerBytes.length) { 247 headerBytes[] = this.frontBuffer[0 .. headerBytes.length]; 248 this.frontBuffer = this.frontBuffer[headerBytes.length .. $]; 249 } else { 250 throw new Exception("Probably not a wav file, or else pass bigger chunks please"); 251 } 252 253 if(header.header != ['R', 'I', 'F', 'F']) 254 throw new Exception("Not a wav file; no RIFF header"); 255 if(header.type != ['W', 'A', 'V', 'E']) 256 throw new Exception("Not a wav file"); 257 // so technically the spec does NOT require fmt to be the first chunk.. 258 // but im gonna just be lazy 259 if(header.fmtHeader != ['f', 'm', 't', ' ']) 260 throw new Exception("Malformed or unsupported wav file"); 261 262 if(header.fmtHeaderSize < 16) 263 throw new Exception("Unsupported wav format header"); 264 265 auto additionalSkip = header.fmtHeaderSize - 16; 266 267 if(header.audioFormat != 1) 268 throw new Exception("arsd.wav only supports the most basic wav files and this one has advanced encoding. try converting to a .mp3 file and use arsd.mp3."); 269 270 this.numberOfChannels = header.numberOfChannels; 271 this.sampleRate = header.sampleRate; 272 this.bitsPerSample = header.bitsPerSample; 273 274 if(header.bytesPerSampleTimesChannels != header.bitsPerSample * header.numberOfChannels / 8) 275 throw new Exception("Malformed wav file: header.bytesPerSampleTimesChannels didn't match"); 276 if(header.bytesPerSeconds != header.bytesPerSampleTimesChannels * header.sampleRate) 277 throw new Exception("Malformed wav file: header.bytesPerSeconds didn't match"); 278 279 this.frontBuffer = this.frontBuffer[additionalSkip .. $]; 280 281 static struct ChunkHeader { 282 align(1): 283 ubyte[4] type; 284 uint size; 285 } 286 static assert(ChunkHeader.sizeof == 8); 287 288 ChunkHeader current; 289 ubyte[] chunkHeader = (cast(ubyte*) ¤t)[0 .. current.sizeof]; 290 291 another_chunk: 292 293 // now we're at the next chunk. want to skip until we hit data. 294 if(this.frontBuffer.length < chunkHeader.length) 295 throw new Exception("bug in arsd.wav the chunk isn't big enough to handle and im lazy. if you hit this send me your file plz"); 296 297 chunkHeader[] = frontBuffer[0 .. chunkHeader.length]; 298 frontBuffer = frontBuffer[chunkHeader.length .. $]; 299 300 if(current.type != ['d', 'a', 't', 'a']) { 301 // skip unsupported chunk... 302 drop_more: 303 if(frontBuffer.length > current.size) { 304 frontBuffer = frontBuffer[current.size .. $]; 305 } else { 306 current.size -= frontBuffer.length; 307 underlying.popFront(); 308 if(underlying.empty) { 309 throw new Exception("Ran out of data while trying to read wav chunks"); 310 } else { 311 frontBuffer = underlying.front; 312 goto drop_more; 313 } 314 } 315 goto another_chunk; 316 } else { 317 this.remainingDataLength = current.size; 318 } 319 320 this.dataLength = this.remainingDataLength; 321 } 322 323 @property const(ubyte)[] front() { 324 return frontBuffer; 325 } 326 327 version(none) 328 void consumeBytes(size_t count) { 329 if(this.frontBuffer.length) 330 this.frontBuffer = this.frontBuffer[count .. $]; 331 } 332 333 void popFront() { 334 remainingDataLength -= front.length; 335 336 underlying.popFront(); 337 if(underlying.empty) 338 frontBuffer = null; 339 else 340 frontBuffer = underlying.front; 341 } 342 343 @property bool empty() { 344 return remainingDataLength == 0 || this.underlying.empty; 345 } 346 } 347 348 /++ 349 Convenience constructor for [WavReader] 350 351 To read from a file, pass a filename, a FILE*, or a range that 352 reads chunks from a file. 353 354 To read from a memory block, just pass it a `ubyte[]` slice. 355 +/ 356 WavReader!T wavReader(T)(T t) { 357 return WavReader!T(t); 358 } 359 360 /// ditto 361 WavReader!DataBlock wavReader(const(ubyte)[] data) { 362 return WavReader!DataBlock(DataBlock(data)); 363 } 364 365 struct DataBlock { 366 const(ubyte)[] front; 367 bool empty() { return front.length == 0; } 368 void popFront() { front = null; } 369 } 370 371 /// Construct a [WavReader] from a filename. 372 WavReader!CFileChunks wavReader(string filename) { 373 assert(filename.length < 290); 374 375 char[300] fn; 376 fn[0 .. filename.length] = filename[]; 377 fn[filename.length] = 0; 378 379 auto fp = fopen(fn.ptr, "rb"); 380 if(fp is null) 381 throw new Exception("wav file unopenable"); // FIXME details 382 383 return WavReader!CFileChunks(fp); 384 } 385 386 struct CFileChunks { 387 FILE* fp; 388 this(FILE* fp) { 389 this.fp = fp; 390 buffer = new ubyte[](4096); 391 refcount = new int; 392 *refcount = 1; 393 popFront(); 394 } 395 this(this) { 396 if(refcount !is null) 397 (*refcount) += 1; 398 } 399 ~this() { 400 if(refcount is null) return; 401 (*refcount) -= 1; 402 if(*refcount == 0) { 403 fclose(fp); 404 } 405 } 406 407 //ubyte[4096] buffer; 408 ubyte[] buffer; 409 int* refcount; 410 411 ubyte[] front; 412 413 void popFront() { 414 auto got = fread(buffer.ptr, 1, buffer.length, fp); 415 front = buffer[0 .. got]; 416 } 417 418 bool empty() { 419 return front.length == 0 && (feof(fp) ? true : false); 420 } 421 }