├── .gitignore ├── COPYING ├── README.md ├── easywave.nim ├── easywave.nimble ├── examples ├── nim.cfg ├── readtest.nim └── writetest.nim └── tests └── tests.nim /.gitignore: -------------------------------------------------------------------------------- 1 | *~ 2 | nimcache 3 | *.exe 4 | *.sfk 5 | .DS_Store 6 | ._* 7 | *.wav 8 | -------------------------------------------------------------------------------- /COPYING: -------------------------------------------------------------------------------- 1 | DO WHAT THE FUCK YOU WANT TO PUBLIC LICENSE 2 | Version 2, December 2004 3 | 4 | Copyright (C) 2004 Sam Hocevar 5 | 6 | Everyone is permitted to copy and distribute verbatim or modified 7 | copies of this license document, and changing it is allowed as long 8 | as the name is changed. 9 | 10 | DO WHAT THE FUCK YOU WANT TO PUBLIC LICENSE 11 | TERMS AND CONDITIONS FOR COPYING, DISTRIBUTION AND MODIFICATION 12 | 13 | 0. You just DO WHAT THE FUCK YOU WANT TO. 14 | 15 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # easyWAVE 2 | 3 | **Work in progress, not ready for public use yet!** 4 | 5 | ## Overview 6 | 7 | **easyWAVE** is a native Nim library that supports the reading and writing of 8 | the most common subset of the WAVE audio file format. Only uncompressed PCM 9 | data is supported (which is used 99.99% of the time in the real world). The 10 | library does not abstract away the file format; you'll still need to have some 11 | understanding of how WAVE files are structured to use it. 12 | 13 | The WAVE format is not complicated, but there are lots of little details that 14 | are quite easy to get wrong. This library gives you a toolkit to read and 15 | write WAVE files in a safe and easy manner—most of the error prone and tedious 16 | stuff is handled by the library (e.g. chunk size calculation when writing 17 | nested chunks, automatic padding of odd-sized chunks, transparent byte-order 18 | swapping in I/O methods etc.) 19 | 20 | ### Features 21 | 22 | * Reading and writing of **8/16/24/32-bit integer PCM** and **32/64-bit IEEE float PCM** WAVE files 23 | * Reading and writing of **markers** and **regions** 24 | * An easy way to write **nested chunks** 25 | * Support for **little-endian (RIFF)** and **big-endian (RIFX)** files 26 | * Works on both little-endian and big-endian architectures (byte-swapping is 27 | handled transparently to the client code) 28 | * Native Nim implementation, no external dependencies 29 | * Released under [WTFPL](http://www.wtfpl.net/) 30 | 31 | ### Limitations 32 | 33 | * No support for compressed formats 34 | * No support for esoteric bit-lengths (e.g. 20-bit PCM) 35 | * Can only read/write the format and cue chunks, and partially the 36 | list chunk. Reading/writing of any other chunk types has to be implemented 37 | by the user. 38 | * No direct support for editing (updating) existing files 39 | * No "recovery mode" for handling malformed files 40 | * Only file I/O is supported (so no streams or memory buffers) 41 | 42 | ## Installation 43 | 44 | The best way to install the library is by using `nimble`: 45 | 46 | ``` 47 | nimble install easywave 48 | ``` 49 | 50 | ## Usage 51 | 52 | ### Reading WAVE files 53 | 54 | Reading WAVE files is accomplished through `WaveReader` objects. 55 | A `WaveReaderError` will be raised if an I/O error was encountered or if the 56 | WAVE file is invalid. 57 | 58 | #### Basic usage 59 | 60 | Just call `parseWaveFile()` with the filename of the WAVE file. You can set 61 | the `readRegions` option to `true` if you're interested in the markers/regions 62 | stored in the file as well. 63 | 64 | This method will: 65 | 66 | * Parse the WAVE file headers and the **format chunk** (`"fmt "`). Information 67 | about the sample format will be available via the `endianness`, `format`, 68 | `sampleRate` and `numChannels` properties. 69 | 70 | * Find all chunks in the file and store this info as a sequence of 71 | `ChunkInfo` objects in the `chunks` property. The size of the sample 72 | data in bytes will be available through the `dataSize` property. 73 | 74 | * If `readRegions` was set to `true`, try to read marker and region 75 | info from the **cue** (`"cue "`) and **list chunks** (`"LIST"`). 76 | 77 | * Set the file pointer to the start of the sample data in the **data chunk** 78 | (`"data"`). 79 | 80 | A simple example that illustrates all these points: 81 | 82 | ```nimrod 83 | import strformat, tables 84 | import easywave 85 | 86 | var wr = parseWaveFile("example.wav", readRegions = true) 87 | 88 | echo fmt"Endianness: {wr.endianness}" 89 | echo fmt"Format: {wr.format}" 90 | echo fmt"Samplerate: {wr.sampleRate}" 91 | echo fmt"Channels: {wr.numChannels}" 92 | 93 | for ci in wr.chunks: 94 | echo ci 95 | 96 | if wr.regions.len > 0: 97 | for id, r in wr.regions.pairs: 98 | echo fmt"id: {id}, {r}" 99 | 100 | var numBytes = wr.dataSize 101 | echo fmt"Sample data size: {numBytes} bytes" 102 | 103 | # File pointer is now at the start of the sample data 104 | ``` 105 | 106 | Reading single values or chunks of data from the file is accomplished through 107 | the various `read*` methods. See the API docs for the full list. It's your 108 | responsibility to ensure that you read the sample data with the appropriate 109 | read method; there's nothing stopping you from reading 16-bit integer data as 110 | 64-bit floats, for example, if that's what you really want 111 | :stuck_out_tongue_winking_eye: 112 | 113 | ```nimrod 114 | # Single value read 115 | let v3 = wr.readInt8() 116 | let v1 = wr.readUInt16() 117 | let v2 = wr.readFloat32() 118 | 119 | # Buffered read 120 | var buf16: array[4096, int16] 121 | wr.readData(buf16) # read until the buffer is full 122 | 123 | var buf32float = newSeq[float32](1024) 124 | wr.readData(buf32float, 50) # read only 50 elements 125 | ``` 126 | 127 | #### Advanced usage 128 | 129 | While the above basic usage pattern would be probably sufficient for most use 130 | cases, you can do the reading fully manually by calling the low-level read 131 | methods. 132 | 133 | The below code is an example for that; it approximates what `parseWaveFile()` 134 | is doing, minus the error checking. Consult the API docs for the list of 135 | available functions. 136 | 137 | ```nimrod 138 | var wr = openWaveFile("example.wav") 139 | 140 | var cueChunk, listChunk, dataChunk: ChunkInfo 141 | 142 | # Iterate through all chunks 143 | while wr.hasNextChunk(): 144 | var ci = wr.nextChunk() 145 | case ci.id 146 | of FOURCC_FORMAT: wr.readFormatChunk(ci) 147 | of FOURCC_CUE: cueChunk = ci 148 | of FOURCC_LIST: listChunk = ci 149 | of FOURCC_DATA: dataChunk = ci 150 | else: discard 151 | 152 | ww.readRegions(cueChunk, listChunk) 153 | 154 | # Seek to the start of the sample data 155 | setFilePos(wr.file, dataChunk.filePos + CHUNK_HEADER_SIZE) 156 | ``` 157 | 158 | ### Writing WAVE files 159 | 160 | Similarly to reading, writing WAVE files is accomplished through `WaveWriter` 161 | objects. A `WaveWriterError` will be raised if an I/O error was encountered 162 | or if you tried to perform an invalid operation (e.g. writing to a closed 163 | file, attempting to write data between chunks etc.) 164 | 165 | #### Creating a WAVE file 166 | 167 | To create a new WAVE file, a `WaveWriter` object needs to be instantiated 168 | first: 169 | 170 | ```nimrod 171 | import easywave 172 | 173 | var ww = writeWaveFile( 174 | filename = "example.wav", 175 | format = wf16BitInteger, 176 | sampleRate = 44100, 177 | numChannels = 2 178 | ) 179 | ``` 180 | 181 | Note that this will only create the file and write the master RIFF chunk 182 | (`"RIFF"`) header. 183 | 184 | #### Writing the format chunk 185 | 186 | You'll need to explicitly call `writeFormatChunk()` to write the actual format 187 | information to the file in the form of a **format chunk** (`"fmt "`). This 188 | gives you the flexibility to optionally insert some other chunks before the 189 | format chunk. 190 | 191 | #### Writing markers and regions 192 | 193 | To write markers and regions to the file, you'll need to descibe them as a 194 | table of values where the keys are the marker/region IDs (32-bit unsigned 195 | integers unique per marker/region) and the values `Region` objects. 196 | Markers are defined simply as regions with a length of zero. 197 | 198 | ```nimrod 199 | ww.regions = { 200 | 1'u32: Region(startFrame: 0, length: 0, label: "marker1"), 201 | 2'u32: Region(startFrame: 1000, length: 0, label: "marker2"), 202 | 3'u32: Region(startFrame: 30000, length: 10000, label: "region2") 203 | }.toOrderedTable 204 | 205 | ww.writeRegions() 206 | ``` 207 | 208 | Note that the start positions and lengths of the markers/regions need to be 209 | specified in sample frames—these are *not* byte offsets! (1 sample frame = *N* 210 | number of samples, where *N* is the number of channels) 211 | 212 | `writeRegions()` will technically create two new chunks right next to each 213 | other: 214 | 215 | * A **cue chunk** (`"cue "`) containing the IDs and the 216 | start offsets of the cue points (markers) 217 | * A **list chunk** (`"LIST"`) containing label (`"labl"`) and labeled text 218 | (`"ltxt"`) sub-chunks to store the labels and region lengths of the 219 | markers/regions, respectively 220 | 221 | The list chunks allows lots of other types of information to be stored in its 222 | various sub-chunks. If you need to store such extra data, you cannot use 223 | `writeRegions()`; you'll need to implement your own list chunk writing logic. 224 | 225 | 226 | #### Writing the data chunk and other chunks 227 | 228 | To write any other other chunks types, you'll need to do the following: 229 | 230 | 1. Call `startChunk("ABCD")`, where `"ABCD"` is the 4-char chunk ID 231 | ([FourCC](https://en.wikipedia.org/wiki/FourCC)). 232 | `startDataChunk()` is a shortcut for creating the data chunk (`"data"`). 233 | 234 | 2. Use the various `write*` methods to write the data (see the API docs for 235 | the full list). Byte-order swapping will be handled automatically depending 236 | on the CPU architecture and the endianness of the file. You need to ensure 237 | that you use the correct write method variant for the particular sample 238 | format you're using. 239 | 240 | 3. When you're done, call `endChunk()` to close the chunk. This will pad the 241 | data automatically with an extra byte at the end if an odd number of bytes 242 | have been written so far, and it will update the chunk size field in the chunk 243 | header. 244 | 245 | ```nimrod 246 | ww.startChunk("LIST") 247 | 248 | # Write single values 249 | ww.writeInt16(-442) 250 | ww.writeUInt32(3) 251 | ww.writeFloat64(1.12300934234) 252 | 253 | # Write buffered data 254 | var buf16 = array[4096, int16] 255 | ww.writeData(buf16) # writeData methods take an openArray argument 256 | 257 | var buf64float: seq[float64] 258 | ww.writeData(buf64float, 50) # write the first 50 elements only 259 | 260 | ww.endChunk() 261 | ``` 262 | 263 | Chunks can be nested; the library will make sure to calculate the correct 264 | chunk sizes for all parent chunks. 265 | 266 | Bear in my mind that it is invalid to write data "between chunks"—an error 267 | will be raised if you tried to write some data after ending a chunk but before 268 | starting a new one. 269 | 270 | #### Closing the file 271 | 272 | Finally, the `endFile()` method must be called to update the master 273 | RIFF chunk with the correct master chunk size. This will also close the file. 274 | 275 | ## Handling 24-bit data 276 | 277 | The library provides two ways to deal with 24-bit data: 278 | 279 | * **As packed data:** `readData24Packed()` and `writeData24Packed()` treat 280 | 24-bit data as a continuous stream of bytes (as they actually appear in 281 | the file). The first sample is bytes 1, 2 and 3, the second sample bytes 282 | 4, 5 and 6, and so on. Because of this, the size of the buffer used with 283 | these two methods must be divisable by three, otherwise an assertion error 284 | will be raised at runtime. The read and write methods only perform 285 | byte-order swapping, if necessary. 286 | 287 | * **As unpacked data:** `readData24Unpacked()` and `writeData24Unpacked()` 288 | treat 24-bit data as a stream of 32-bit integers. The read method unpacks 289 | the packed data from the file into a stream of 32-bit integers (with the 290 | most significant byte set to zero), while the write does the opposite. 291 | 292 | It is important to stress out that the data will be always written to the WAVE 293 | file in packed form—it's just sometimes more convenient to deal with 32-bit 294 | integers than with packed data, hence the two different methods. 295 | 296 | 297 | ## Some general notes about WAVE files 298 | 299 | * Little-endian WAVE files start with the `"RIFF"` master chunk ID, big-endian 300 | files start with `"RIFX"`. Apart from the byte-ordering, there are no 301 | differences between the two formats. The big-endian option is not really 302 | meant to be used when creating new WAVE files; I just included it because 303 | it made the testing of the byte-swapping code paths much easier on Intel 304 | hardware. Virtually nothing can read RIFX files nowadays, it's kind of a 305 | dead format. 306 | 307 | * The only restriction on the order of chunks is that the format chunk *must* 308 | appear before the data chunk (but not necessarily *immediately* before it). 309 | Apart from this restriction, other chunks can appear in *any* order. For 310 | example, there is no guarantee that the format chunk is always the first 311 | chunk (some old software mistakenly assumes this). 312 | 313 | * All chunks must start at even offsets. If a chunk contains an odd number of 314 | bytes, it must be padded with an extra byte at the end. However, the chunk 315 | header must contain the *original unpadded chunk length* in its size field 316 | (the writer takes care of this, but this might surprise some people when 317 | reading files). 318 | 319 | ## License 320 | 321 | Copyright © 2018-2014 John Novak <> 322 | 323 | This work is free. You can redistribute it and/or modify it under the terms of 324 | the **Do What The Fuck You Want To Public License, Version 2**, as published 325 | by Sam Hocevar. See the `COPYING` file for more details. 326 | -------------------------------------------------------------------------------- /easywave.nim: -------------------------------------------------------------------------------- 1 | ## :Author: John Novak 2 | ## 3 | 4 | import options 5 | import strformat 6 | import tables 7 | 8 | import riff 9 | 10 | export riff 11 | export tables 12 | 13 | # {{{ Common 14 | 15 | const 16 | FourCC_WAVE_fmt* = "fmt " ## Format chunk ID 17 | FourCC_WAVE_data* = "data" ## Data chunk ID 18 | FourCC_WAVE_cue* = "cue " ## Cue chunk ID 19 | FourCC_WAVE_adtl* = "adtl" ## Associated data list ID 20 | FourCC_WAVE_labl* = "labl" ## Label chunk ID 21 | FourCC_WAVE_ltxt* = "ltxt" ## Labeled text chunk ID 22 | FourCC_WAVE_rgn* = "rgn " ## Region purpose ID 23 | 24 | WaveFormatPCM = 1'u16 25 | WaveFormatIEEEFloat = 3'u16 26 | 27 | type 28 | SampleFormat* = enum 29 | sfPCM = (0, "PCM"), 30 | sfFloat = (1, "IEEE Float"), 31 | sfUnknown = (2, "Unknown") 32 | 33 | Region* = object 34 | ## represents a marker (if length is 0) or a region 35 | startFrame*: uint32 ## start sample frame of the marker/region 36 | length*: uint32 ## length of the region in frames (0 for markers) 37 | label*: string ## text label 38 | 39 | RegionTable* = OrderedTable[uint32, Region] 40 | 41 | WaveFormat* = object 42 | sampleFormat*: SampleFormat 43 | bitsPerSample*: Natural 44 | sampleRate*: Natural 45 | numChannels*: Natural 46 | 47 | 48 | proc initRegions*(): RegionTable = 49 | result = initOrderedTable[uint32, Region]() 50 | 51 | # }}} 52 | # {{{ Reader 53 | 54 | type 55 | WaveInfo* = object 56 | reader*: RiffReader 57 | format*: WaveFormat 58 | regions*: RegionTable 59 | dataCursor*: Cursor 60 | 61 | using rr: RiffReader 62 | 63 | #[ 64 | proc readData24Unpacked*(rr; dest: pointer, numItems: Natural) = 65 | const WIDTH = 3 66 | var 67 | bytesToRead = numItems * WIDTH 68 | readBufferSize = br.readBuffer.len - br.readBuffer.len mod WIDTH 69 | destArr = cast[ptr UncheckedArray[int32]](dest) 70 | destPos = 0 71 | 72 | while bytesToRead > 0: 73 | let count = min(readBufferSize, bytesToRead) 74 | br.readBuf(br.readBuffer[0].addr, count) 75 | var pos = 0 76 | while pos < count: 77 | var v: int32 78 | case br.endianness: 79 | of littleEndian: 80 | v = br.readBuffer[pos].int32 or 81 | (br.readBuffer[pos+1].int32 shl 8) or 82 | ashr(br.readBuffer[pos+2].int32 shl 24, 8) 83 | of bigEndian: 84 | v = br.readBuffer[pos+2].int32 or 85 | (br.readBuffer[pos+1].int32 shl 8) or 86 | ashr(br.readBuffer[pos].int32 shl 24, 8) 87 | destArr[destPos] = v 88 | inc(pos, WIDTH) 89 | inc(destPos) 90 | 91 | dec(bytesToRead, count) 92 | 93 | 94 | proc readData24Unpacked*(br: var BufferedReader, 95 | dest: var openArray[int32|uint32], numItems: Natural) = 96 | assert numItems <= dest.len 97 | br.readData24Unpacked(dest[0].addr, numItems) 98 | 99 | 100 | proc readData24Unpacked*(br: var BufferedReader, 101 | dest: var openArray[int32|uint32]) = 102 | br.readData24Unpacked(dest, dest.len) 103 | 104 | 105 | proc readData24Packed*(br: var BufferedReader, dest: pointer, 106 | numItems: Natural) = 107 | const WIDTH = 3 108 | var 109 | bytesToRead = numItems * WIDTH 110 | readBufferSize = br.readBuffer.len - br.readBuffer.len mod WIDTH 111 | destArr = cast[ptr UncheckedArray[uint8]](dest) 112 | destPos = 0 113 | 114 | while bytesToRead > 0: 115 | let count = min(readBufferSize, bytesToRead) 116 | br.readBuf(br.readBuffer[0].addr, count) 117 | var pos = 0 118 | while pos < count: 119 | if br.swapEndian: 120 | destArr[destPos] = br.readBuffer[pos+2] 121 | destArr[destPos+1] = br.readBuffer[pos+1] 122 | destArr[destPos+2] = br.readBuffer[pos] 123 | else: 124 | destArr[destPos] = br.readBuffer[pos] 125 | destArr[destPos+1] = br.readBuffer[pos+1] 126 | destArr[destPos+2] = br.readBuffer[pos+2] 127 | inc(pos, WIDTH) 128 | inc(destPos, WIDTH) 129 | 130 | dec(bytesToRead, count) 131 | 132 | 133 | proc readData24Packed*(br: var BufferedReader, dest: var openArray[int8|uint8], 134 | numItems: Natural) = 135 | assert numItems <= dest.len div 3 136 | br.readData24Packed(dest[0].addr, dest.len div 3) 137 | 138 | 139 | proc readData24Packed*(br: var BufferedReader, dest: var openArray[int8|uint8]) = 140 | br.readData24Packed(dest, dest.len div 3) 141 | ]# 142 | 143 | 144 | proc readFormatChunk*(rr): WaveFormat = 145 | {.hint[XDeclaredButNotUsed]: off.} 146 | let 147 | format = rr.read(uint16) 148 | channels = rr.read(uint16) 149 | samplesPerSec = rr.read(uint32) 150 | avgBytesPerSec = rr.read(uint32) # ignored 151 | blockAlign = rr.read(uint16) # ignored 152 | bitsPerSample = rr.read(uint16) 153 | 154 | var wf: WaveFormat 155 | wf.bitsPerSample = bitsPerSample 156 | wf.numChannels = channels 157 | wf.sampleRate = samplesPerSec 158 | 159 | wf.sampleFormat = case format 160 | of WaveFormatPCM: sfPCM 161 | of WaveFormatIEEEFloat: sfFloat 162 | else: sfUnknown 163 | 164 | result = wf 165 | 166 | 167 | proc readRegionIdsAndStartOffsetsFromCueChunk*(rr; regions: var RegionTable) = 168 | let numCuePoints = rr.read(uint32) 169 | 170 | if numCuePoints > 0'u32: 171 | for i in 0..= writeBufferSize: 320 | bw.writeBuf(bw.writeBuffer[0].addr, writeBufferSize) 321 | destPos = 0 322 | 323 | if destPos > 0: 324 | bw.writeBuf(bw.writeBuffer[0].addr, destPos) 325 | else: 326 | bw.writeBuf(src, numBytes) 327 | 328 | 329 | proc writeData24Packed*(bw: var BufferedWriter, 330 | src: var openArray[int8|uint8], numItems: Natural) = 331 | ## TODO 332 | assert numItems * 3 <= src.len 333 | bw.writeData24Packed(src[0].addr, numItems) 334 | 335 | 336 | proc writeData24Packed*(bw: var BufferedWriter, 337 | src: var openArray[int8|uint8]) = 338 | ## TODO 339 | bw.writeData24Packed(src, src.len div 3) 340 | 341 | 342 | proc writeData24Unpacked*(bw: var BufferedWriter, src: pointer, 343 | numItems: Natural) = 344 | ## TODO 345 | let numBytes = numItems * 4 346 | 347 | let writeBufferSize = bw.writeBuffer.len - bw.writeBuffer.len mod 3 348 | var 349 | src = cast[ptr UncheckedArray[uint8]](src) 350 | pos = 0 351 | destPos = 0 352 | 353 | while pos < numBytes: 354 | if bw.swapEndian: 355 | bw.writeBuffer[destPos] = src[pos+2] 356 | bw.writeBuffer[destPos+1] = src[pos+1] 357 | bw.writeBuffer[destPos+2] = src[pos] 358 | else: 359 | bw.writeBuffer[destPos] = src[pos] 360 | bw.writeBuffer[destPos+1] = src[pos+1] 361 | bw.writeBuffer[destPos+2] = src[pos+2] 362 | 363 | inc(destPos, 3) 364 | inc(pos, 4) 365 | if destPos >= writeBufferSize: 366 | bw.writeBuf(bw.writeBuffer[0].addr, writeBufferSize) 367 | destPos = 0 368 | 369 | if destPos > 0: 370 | bw.writeBuf(bw.writeBuffer[0].addr, destPos) 371 | 372 | 373 | proc writeData24Unpacked*(bw: var BufferedWriter, 374 | src: var openArray[int32|uint32], numItems: Natural) = 375 | assert numItems <= src.len 376 | bw.writeData24Unpacked(src[0].addr, numItems) 377 | 378 | 379 | proc writeData24Unpacked*(bw: var BufferedWriter, 380 | src: var openArray[int32|uint32]) = 381 | bw.writeData24Unpacked(src, src.len) 382 | 383 | 384 | ]# 385 | 386 | proc writeFormatChunk*(rw; wf: WaveFormat) = 387 | rw.beginChunk(FourCC_WAVE_fmt) 388 | 389 | var formatTag: uint16 = case wf.sampleFormat: 390 | of sfPCM: WaveFormatPCM 391 | of sfFloat: WaveFormatIEEEFloat 392 | of sfUnknown: 0 393 | 394 | var blockAlign = (wf.numChannels * wf.bitsPerSample div 8).uint16 395 | var avgBytesPerSec = wf.sampleRate.uint32 * blockAlign.uint32 396 | 397 | rw.write(formatTag) 398 | rw.write(wf.numChannels.uint16) 399 | rw.write(wf.sampleRate.uint32) 400 | rw.write(avgBytesPerSec) 401 | rw.write(blockAlign) 402 | rw.write(wf.bitsPerSample.uint16) 403 | # TODO write extended header for float formats (and for 24 bit) ? 404 | 405 | rw.endChunk() 406 | 407 | 408 | proc writeCueChunk*(rw; regions: RegionTable) = 409 | rw.beginChunk(FourCC_WAVE_cue) 410 | rw.write(regions.len.uint32) # number of markers/regions 411 | 412 | for id, region in regions.pairs: 413 | rw.write(id) # cuePointId 414 | rw.write(0'u32) # position (unused if dataChunkId is 'data') 415 | rw.writeFourCC(FourCC_WAVE_data) # dataChunkId 416 | rw.write(0'u32) # chunkStart (unused if dataChunkId is 'data') 417 | rw.write(0'u32) # blockStart (unused if dataChunkId is 'data') 418 | rw.write(region.startFrame) # sampleOffset 419 | 420 | rw.endChunk() 421 | 422 | 423 | proc writeAdtlListChunk*(rw; regions: RegionTable) = 424 | rw.beginListChunk(FourCC_WAVE_adtl) 425 | 426 | for id, region in regions.pairs: 427 | rw.beginChunk(FourCC_WAVE_labl) 428 | rw.write(id) # cuePointId 429 | rw.writeStr(region.label) # text 430 | rw.write(0'u8) # null terminator 431 | rw.endChunk() 432 | 433 | for id, region in regions.pairs: 434 | if region.length > 0: 435 | rw.beginChunk(FourCC_WAVE_ltxt) 436 | rw.write(id) # cuePointId 437 | rw.write(region.length.uint32) # sampleLength 438 | rw.writeFourCC(FourCC_WAVE_rgn) # purposeId 439 | rw.write(0'u16) # country (ignored) 440 | rw.write(0'u16) # language (ignored) 441 | rw.write(0'u16) # dialect (ignored) 442 | rw.write(0'u16) # codePage (ignored) 443 | rw.endChunk() 444 | 445 | rw.endChunk() 446 | 447 | 448 | # vim: et:ts=2:sw=2:fdm=marker 449 | -------------------------------------------------------------------------------- /easywave.nimble: -------------------------------------------------------------------------------- 1 | # Package 2 | 3 | version = "0.2.0" 4 | author = "John Novak " 5 | description = "Easy WAVE file handling in Nim" 6 | license = "WTFPL" 7 | 8 | skipDirs = @["doc", "examples"] 9 | 10 | # Dependencies 11 | 12 | requires "nim >= 1.6.0", "riff" 13 | 14 | # Tasks 15 | 16 | task examples, "Compiles the examples": 17 | exec "nim c -d:release examples/readtest.nim" 18 | exec "nim c -d:release examples/writetest.nim" 19 | 20 | task examplesDebug, "Compiles the examples (debug mode)": 21 | exec "nim c examples/readtest.nim" 22 | exec "nim c examples/writetest.nim" 23 | 24 | task docgen, "Generate HTML documentation": 25 | exec "nim doc -o:doc/easywave.html easywave" 26 | -------------------------------------------------------------------------------- /examples/nim.cfg: -------------------------------------------------------------------------------- 1 | path=".." 2 | -deepcopy:on 3 | -------------------------------------------------------------------------------- /examples/readtest.nim: -------------------------------------------------------------------------------- 1 | import os 2 | import strformat 3 | import strutils 4 | import times 5 | 6 | import easywave 7 | 8 | 9 | proc toTimeString(millis: Natural): string = 10 | let p = initDuration(milliseconds = millis).toParts 11 | fmt"{p[Hours]:02}:{p[Minutes]:02}:{p[Seconds]:02}.{p[Milliseconds]:03}" 12 | 13 | proc framesToMillis(frames, sampleRate: Natural): Natural = 14 | const MillisInSecond = 1000 15 | (frames / sampleRate * MillisInSecond).int 16 | 17 | proc printWaveInfo(wi: WaveInfo, dataChunk: ChunkInfo) = 18 | let 19 | sampleRate = wi.format.sampleRate 20 | bitsPerSample = wi.format.bitsPerSample 21 | numChans = wi.format.numChannels 22 | numBytes = dataChunk.size.int 23 | numBytesHuman = formatSize(numBytes, includeSpace=true) 24 | numSamples = numBytes div (bitsPerSample div 8) 25 | numSampleFrames = numSamples div numChans 26 | numMillis = framesToMillis(numSampleFrames, sampleRate) 27 | 28 | echo fmt"Endianness: {wi.reader.endian}" 29 | echo fmt"Sample format: {wi.format.sampleFormat}" 30 | echo fmt"Bits per sample: {bitsPerSample}" 31 | echo fmt"Sample rate: {sampleRate}" 32 | echo fmt"Channels: {numChans}" 33 | echo "" 34 | echo fmt"Sample data size: {numBytes} bytes ({numBytesHuman})" 35 | echo fmt"Num samples: {numSamples}" 36 | echo fmt"Num samples frames: {numSampleFrames}" 37 | echo fmt"Length: {toTimeString(numMillis)}" 38 | 39 | 40 | proc printRegionInfo(wi: WaveInfo) = 41 | let sampleRate = wi.format.sampleRate 42 | 43 | echo "\nRegions and labels:\n" 44 | 45 | for id, r in wi.regions.pairs: 46 | let startTime = framesToMillis(r.startFrame, sampleRate) 47 | let rtype = if r.length > 0: "region" else: "label" 48 | 49 | echo fmt" ID: {id}" 50 | echo fmt" Type: {rtype}" 51 | 52 | if r.length == 0: 53 | echo fmt" Position: {toTimeString(startTime)} (frame {r.startFrame})" 54 | else: 55 | let length = framesToMillis(r.length, sampleRate) 56 | let endFrame = r.startFrame + r.length 57 | let endTime = framesToMillis(endFrame, sampleRate) 58 | echo fmt" Start: {toTimeString(startTime)} (frame {r.startFrame})" 59 | echo fmt" End: {toTimeString(endTime)} (frame {endFrame})" 60 | echo fmt" Duration: {toTimeString(length)}" 61 | 62 | echo "" 63 | 64 | 65 | proc main() = 66 | if os.paramCount() == 0: 67 | quit "Usage: readtest WAVEFILE" 68 | 69 | var fname = os.paramStr(1) 70 | let wi: WaveInfo = openWaveFile(fname, readRegions=true) 71 | 72 | wi.reader.cursor = wi.dataCursor 73 | let dataChunk = wi.reader.currentChunk 74 | 75 | printWaveInfo(wi, dataChunk) 76 | printRegionInfo(wi) 77 | 78 | 79 | main() 80 | 81 | -------------------------------------------------------------------------------- /examples/writetest.nim: -------------------------------------------------------------------------------- 1 | import math 2 | import os 3 | import tables 4 | 5 | import easywave 6 | 7 | const 8 | SampleRate = 44100 9 | NumChannels = 2 10 | LengthSeconds = 1 11 | FreqHz = 440 12 | 13 | let regions = { 14 | 1'u32: Region(startFrame: 0, length: 0, label: "marker1"), 15 | 2'u32: Region(startFrame: 1000, length: 0, label: "marker2"), 16 | 3'u32: Region(startFrame: 3000, length: 0, label: "marker3"), 17 | 4'u32: Region(startFrame: 10000, length: 5000, label: "region1"), 18 | 5'u32: Region(startFrame: 30000, length: 10000, label: "region2") 19 | }.toOrderedTable 20 | 21 | 22 | proc writeTestFile[T: SomeNumber](filename: string, endian: Endianness, 23 | sampleFormat: SampleFormat, 24 | bitsPerSample: Natural) = 25 | var rw = createRiffFile(filename, FourCC_WAVE, endian) 26 | 27 | let wf = WaveFormat( 28 | sampleFormat: sampleFormat, 29 | bitsPerSample: sizeof(T) * 8, 30 | sampleRate: SampleRate, 31 | numChannels: NumChannels 32 | ) 33 | 34 | rw.writeFormatChunk(wf) 35 | rw.beginChunk(FourCC_WAVE_data) 36 | 37 | var amplitude = case bitsPerSample 38 | of 8: 2^7 / 4 39 | of 16: 2^15 / 4 40 | of 24: 2^23 / 4 41 | of 32: 42 | if sampleFormat == sfPCM: 2^31 / 4 else: 1.0 / 4 43 | of 64: 1.0 / 4 44 | else: 0 45 | 46 | var 47 | totalFrames = LengthSeconds * SampleRate # 1 frame = 2 samples (stereo) 48 | buf: array[1024, T] 49 | pos = 0 50 | phase = 0.0 51 | phaseInc = 2*PI / (SampleRate/FreqHz) 52 | 53 | while totalFrames > 0: 54 | var s = if sizeof(T) == 1: T(sin(phase) * amplitude + (2^7).float) 55 | else: T(sin(phase) * amplitude) 56 | 57 | buf[pos] = s 58 | buf[pos+1] = s 59 | 60 | inc(pos, 2) 61 | if pos >= buf.len: 62 | rw.write(buf, 0, buf.len) 63 | pos = 0 64 | dec(totalFrames) 65 | 66 | phase += phaseInc 67 | 68 | if pos > 0: 69 | rw.write(buf, 0, pos) 70 | 71 | rw.endChunk() 72 | 73 | rw.writeCueChunk(regions) 74 | rw.writeAdtlListChunk(regions) 75 | rw.close() 76 | 77 | #[ 78 | # {{{ write24BitPackedTestFile 79 | 80 | proc write24BitPackedTestFile(outfile: string, endian: endian) = 81 | var ww = writeWaveFile(outfile, sf24BitInteger, SampleRate, NumChannels, 82 | endian) 83 | ww.writeFormatChunk() 84 | ww.startDataChunk() 85 | 86 | let amplitude = 2^23 / 4 87 | var 88 | totalFrames = LengthSeconds * SampleRate # 1 frame = 2 samples (stereo) 89 | buf: array[256*6, uint8] # must be divisible by 6! 90 | pos = 0 91 | phase = 0.0 92 | phaseInc = 2*PI / (SampleRate/FreqHz) 93 | 94 | while totalFrames > 0: 95 | let s = (sin(phase) * amplitude).int32 96 | buf[pos] = ( s and 0xff).uint8 97 | buf[pos+1] = ((s shr 8) and 0xff).uint8 98 | buf[pos+2] = ((s shr 16) and 0xff).uint8 99 | 100 | buf[pos+3] = ( s and 0xff).uint8 101 | buf[pos+4] = ((s shr 8) and 0xff).uint8 102 | buf[pos+5] = ((s shr 16) and 0xff).uint8 103 | 104 | inc(pos, 6) 105 | if pos >= buf.len: 106 | ww.writeData24Packed(buf) 107 | pos = 0 108 | dec(totalFrames) 109 | 110 | phase += phaseInc 111 | 112 | if pos > 0: 113 | ww.writeData24Packed(buf, pos) 114 | 115 | ww.endChunk() 116 | 117 | ww.setRegions() 118 | ww.writeCueChunk() 119 | ww.writeListChunk() 120 | 121 | ww.close() 122 | 123 | # }}} 124 | ]# 125 | 126 | writeTestFile[uint8]("writetest-PCM8-LE.wav", littleEndian, sfPCM, 8) 127 | writeTestFile[uint16]("writetest-PCM16-LE.wav", littleEndian, sfPCM, 16) 128 | writeTestFile[uint32]("writetest-PCM24-unpacked-LE.wav", littleEndian, sfPCM, 24) 129 | # write24BitPackedTestFile("writetest-24bit-packed-LE.wav", littleEndian) 130 | writeTestFile[uint32]("writetest-PCM32-LE.wav", littleEndian, sfPCM, 32) 131 | writeTestFile[float32]("writetest-Float32-LE.wav", littleEndian, sfFloat, 32) 132 | writeTestFile[float64]("writetest-Float64-LE.wav", littleEndian, sfFloat, 64) 133 | 134 | 135 | writeTestFile[uint8]("writetest-PCM8-BE.wav", bigEndian, sfPCM, 8) 136 | writeTestFile[uint16]("writetest-PCM16-BE.wav", bigEndian, sfPCM, 16) 137 | writeTestFile[uint32]("writetest-PCM24-unpacked-BE.wav", bigEndian, sfPCM, 24) 138 | # write24BitPackedTestFile("writetest-24bit-packed-BE.wav", littleEndian) 139 | writeTestFile[uint32]("writetest-PCM32-BE.wav", bigEndian, sfPCM, 32) 140 | writeTestFile[float32]("writetest-Float32-BE.wav", bigEndian, sfFloat, 32) 141 | writeTestFile[float64]("writetest-Float64-BE.wav", bigEndian, sfFloat, 64) 142 | 143 | # vim: et:ts=2:sw=2:fdm=marker 144 | -------------------------------------------------------------------------------- /tests/tests.nim: -------------------------------------------------------------------------------- 1 | import unittest 2 | 3 | # {{{ Read tests / parseWaveFile 4 | suite "Read tests / parseWaveFile": 5 | 6 | test "parseWaveFile - file not found": 7 | parseWaveFile("testdata/emptyfile.wav") 8 | 9 | test "parseWaveFile - empty file": 10 | parseWaveFile("testdata/emptyfile.wav") 11 | 12 | test "parseWaveFile - no format chunk": 13 | parseWaveFile("testdata/emptyfile.wav") 14 | 15 | test "parseWaveFile - no data chunk": 16 | parseWaveFile("testdata/emptyfile.wav") 17 | 18 | test "parseWaveFile - valid file (don't read regions)": 19 | parseWaveFile("testdata/emptyfile.wav") 20 | 21 | test "parseWaveFile - valid file (read regions)": 22 | parseWaveFile("testdata/emptyfile.wav") 23 | 24 | # methods 25 | # filename 26 | # endianness 27 | # format 28 | # sampleRate 29 | # numChannels 30 | # chunks 31 | # regions 32 | # currChunk 33 | # 34 | # readFourCC 35 | # readInt8 36 | # readInt16 37 | # readInt32 38 | # readInt64 39 | # readUInt8 40 | # readUInt16 41 | # readUInt32 42 | # readUInt64 43 | # readFloat32 44 | # readFloat64 45 | # readData 46 | # 47 | # setCurrentChunk 48 | # hasNextChunk 49 | # nextChunk 50 | # setChunkPos 51 | # buildChunkList 52 | # findChunk 53 | # readFormatChunk 54 | # readRegions 55 | # 56 | 57 | # }}} 58 | # {{{ Read tests / openWaveFile 59 | suite "Read tests / openWaveFile": 60 | 61 | test "openWaveFile - file not found": 62 | parseWaveFile("testdata/emptyfile.wav") 63 | 64 | test "openWaveFile - empty file": 65 | parseWaveFile("testdata/emptyfile.wav") 66 | 67 | 68 | # vim: et:ts=2:sw=2:fdm=marker 69 | --------------------------------------------------------------------------------