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 float duration; // in seconds, added nov 26 2022 223 224 private uint remainingDataLength; 225 226 private Range underlying; 227 228 private const(ubyte)[] frontBuffer; 229 230 static if(is(Range == CFileChunks)) { 231 this(FILE* fp) { 232 underlying = CFileChunks(fp); 233 this(0); 234 } 235 } else { 236 this(Range r) { 237 this.underlying = r; 238 this(0); 239 } 240 } 241 242 private this(int _initializationDummyVariable) { 243 this.frontBuffer = underlying.front; 244 245 WavFileHeader header; 246 ubyte[] headerBytes = (cast(ubyte*) &header)[0 .. header.sizeof - 8]; 247 248 if(this.frontBuffer.length >= headerBytes.length) { 249 headerBytes[] = this.frontBuffer[0 .. headerBytes.length]; 250 this.frontBuffer = this.frontBuffer[headerBytes.length .. $]; 251 } else { 252 throw new Exception("Probably not a wav file, or else pass bigger chunks please"); 253 } 254 255 if(header.header != ['R', 'I', 'F', 'F']) 256 throw new Exception("Not a wav file; no RIFF header"); 257 if(header.type != ['W', 'A', 'V', 'E']) 258 throw new Exception("Not a wav file"); 259 // so technically the spec does NOT require fmt to be the first chunk.. 260 // but im gonna just be lazy 261 if(header.fmtHeader != ['f', 'm', 't', ' ']) 262 throw new Exception("Malformed or unsupported wav file"); 263 264 if(header.fmtHeaderSize < 16) 265 throw new Exception("Unsupported wav format header"); 266 267 auto additionalSkip = header.fmtHeaderSize - 16; 268 269 if(header.audioFormat != 1) 270 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."); 271 272 this.numberOfChannels = header.numberOfChannels; 273 this.sampleRate = header.sampleRate; 274 this.bitsPerSample = header.bitsPerSample; 275 276 if(header.bytesPerSampleTimesChannels != header.bitsPerSample * header.numberOfChannels / 8) 277 throw new Exception("Malformed wav file: header.bytesPerSampleTimesChannels didn't match"); 278 if(header.bytesPerSeconds != header.bytesPerSampleTimesChannels * header.sampleRate) 279 throw new Exception("Malformed wav file: header.bytesPerSeconds didn't match"); 280 281 this.frontBuffer = this.frontBuffer[additionalSkip .. $]; 282 283 static struct ChunkHeader { 284 align(1): 285 ubyte[4] type; 286 uint size; 287 } 288 static assert(ChunkHeader.sizeof == 8); 289 290 ChunkHeader current; 291 ubyte[] chunkHeader = (cast(ubyte*) ¤t)[0 .. current.sizeof]; 292 293 another_chunk: 294 295 // now we're at the next chunk. want to skip until we hit data. 296 if(this.frontBuffer.length < chunkHeader.length) 297 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"); 298 299 chunkHeader[] = frontBuffer[0 .. chunkHeader.length]; 300 frontBuffer = frontBuffer[chunkHeader.length .. $]; 301 302 if(current.type != ['d', 'a', 't', 'a']) { 303 // skip unsupported chunk... 304 drop_more: 305 if(frontBuffer.length > current.size) { 306 frontBuffer = frontBuffer[current.size .. $]; 307 } else { 308 current.size -= frontBuffer.length; 309 underlying.popFront(); 310 if(underlying.empty) { 311 throw new Exception("Ran out of data while trying to read wav chunks"); 312 } else { 313 frontBuffer = underlying.front; 314 goto drop_more; 315 } 316 } 317 goto another_chunk; 318 } else { 319 this.remainingDataLength = current.size; 320 } 321 322 this.dataLength = this.remainingDataLength; 323 324 this.duration = cast(float) this.dataLength / header.bytesPerSeconds; 325 } 326 327 @property const(ubyte)[] front() { 328 return frontBuffer; 329 } 330 331 version(none) 332 void consumeBytes(size_t count) { 333 if(this.frontBuffer.length) 334 this.frontBuffer = this.frontBuffer[count .. $]; 335 } 336 337 void popFront() { 338 remainingDataLength -= front.length; 339 340 underlying.popFront(); 341 if(underlying.empty) 342 frontBuffer = null; 343 else 344 frontBuffer = underlying.front; 345 } 346 347 @property bool empty() { 348 return remainingDataLength == 0 || this.underlying.empty; 349 } 350 } 351 352 /++ 353 Convenience constructor for [WavReader] 354 355 To read from a file, pass a filename, a FILE*, or a range that 356 reads chunks from a file. 357 358 To read from a memory block, just pass it a `ubyte[]` slice. 359 +/ 360 WavReader!T wavReader(T)(T t) { 361 return WavReader!T(t); 362 } 363 364 /// ditto 365 WavReader!DataBlock wavReader(const(ubyte)[] data) { 366 return WavReader!DataBlock(DataBlock(data)); 367 } 368 369 struct DataBlock { 370 const(ubyte)[] front; 371 bool empty() { return front.length == 0; } 372 void popFront() { front = null; } 373 } 374 375 /// Construct a [WavReader] from a filename. 376 WavReader!CFileChunks wavReader(string filename) { 377 assert(filename.length < 290); 378 379 char[300] fn; 380 fn[0 .. filename.length] = filename[]; 381 fn[filename.length] = 0; 382 383 auto fp = fopen(fn.ptr, "rb"); 384 if(fp is null) 385 throw new Exception("wav file unopenable"); // FIXME details 386 387 return WavReader!CFileChunks(fp); 388 } 389 390 struct CFileChunks { 391 FILE* fp; 392 this(FILE* fp) { 393 this.fp = fp; 394 buffer = new ubyte[](4096); 395 refcount = new int; 396 *refcount = 1; 397 popFront(); 398 } 399 this(this) { 400 if(refcount !is null) 401 (*refcount) += 1; 402 } 403 ~this() { 404 if(refcount is null) return; 405 (*refcount) -= 1; 406 if(*refcount == 0) { 407 fclose(fp); 408 } 409 } 410 411 //ubyte[4096] buffer; 412 ubyte[] buffer; 413 int* refcount; 414 415 ubyte[] front; 416 417 void popFront() { 418 auto got = fread(buffer.ptr, 1, buffer.length, fp); 419 front = buffer[0 .. got]; 420 } 421 422 bool empty() { 423 return front.length == 0 && (feof(fp) ? true : false); 424 } 425 }