Repository: SpotlightKid/nim-sndfile Branch: master Commit: 94591b36e0f1 Files: 17 Total size: 35.2 KB Directory structure: gitextract_o4nvxbth/ ├── LICENSE.md ├── README.md ├── examples/ │ ├── config.nims │ ├── dump_loops.nim │ ├── libsamplerate.nim │ ├── list_formats.nim │ ├── playfile_jack.nim │ ├── playfile_sdl.nim │ └── signal.nim ├── sndfile.nimble ├── src/ │ ├── sndfile/ │ │ ├── api.nim │ │ └── chunks.nim │ └── sndfile.nim ├── tests/ │ ├── config.nims │ ├── test_chunks.nim │ └── test_testwav.nim └── tools/ └── build_docs.sh ================================================ FILE CONTENTS ================================================ ================================================ FILE: LICENSE.md ================================================ # MIT License Copyright (c) 2014 - 2017 Julien Aubert https://github.com/julienaubert Copyright (c) 2024 - 2025 Christopher Arndt Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. ================================================ FILE: README.md ================================================ nim-sndfile =========== A wrapper of [libsndfile] for the [Nim] programming language. # API The libsndfile [API] only has a relatively small number of functions, but it defines quite a few types and enum values. The `sndfile` Nim module provides definitions for all types and enums (except those for deprecated and not-yet implemented functions) and wraps all functions, *except* the following: * `sf_open_fd` * `sf_open_virtual` * `sf_wchar_open` These missing functions may be added in future versions, if it makes sense to use them in Nim. # Naming conventions * The `sf_` prefix has been removed from function names, i.e. instead of `sf_read_double`, use just `read_double` (or `readDouble`, Nim's identifier naming rules make no distinction). The wrapper code generally uses lower `camelCase`. * All values within each enum have been stripped of their unique prefix and all enums are marked `{.pure.}`. This means you should prefix each enum symbol with its enum type name. For example `SF_FORMAT_WAV` becomes `SFFormat.WAV` (or `SF_Format.Wav` etc.). If the value symbol is non-ambiguous, the enum name can be omitted, e.g. `GET_LOG_INFO` instead of `SFCommand.GET_LOG_INFO`. * typedefs and structs start with a capital letter and retain their `SF` prefix, but Nim's identifier naming rules allow to omit underscores and use whatever case after the first letter. For example, instead of `SF_INFO`, you can use `SFInfo`, `SF_info` etc. The wrapper code generally uses `PascalCase`. ## Special cases | C prefix / name | Nim enum / symbol | | ------------------------------ | ---------------------- | | `SFM_` | `SFMode` | | `SF_STR_` | `SFStrType` | | `SF_LOOP_` | `SFLoopMode` | | `SFC_` | `SFCommand` | | `SNDFILE` | `SndFile` | | `sf_count_t` | `SFCount` | | `SF_TRUE` | `SFBool.TRUE` | | `SF_FALSE` | `SFBool.FALSE` | | `SF_STR_FIRST` | `SFStrType.low` | | `SF_STR_LAST` | `SFStrType.high` | | `SF_AMBISONIC_NONE` | `SFAmbisonic.NONE` | | `SF_AMBISONIC_B_FORMAT` | `SFAmbisonic.B_FORMAT` | | `SF_INSTRUMENT.loops` | `SFLoop` | | `SF_INSTRUMENT.loops[n].end` | `SFLoops.endPos` | | `SF_INSTRUMENT.loops[n].start` | `SFLoops.startPos` | ## Structs with customizable-sized array fields * `SF_CUES`: Define / allocate `array[, SFCuePoint]` as needed * `SF_BROADCAST_INFO`: Set number of entries in the `codingHistory` array member of `SFBroadcastInfo` with `-d:sfCodingHistSize=` at compile time. The default size is 256. * `SF_CART_INFO`: Set number of entries in the `tagText` array member of `SFCartInfo` with `-d:sfMaxTagTextSize=` at compile time. The default size is 256. ## Authors *nim-sndfile* was written by [Julien Aubert](https://github.com/julienaubert) and this fork was updated, re-factored and extended by [Christopher Arndt](https://github.com/SpotlightKid), while also applying some patches by [James Bradbury](https://github.com/jamesb93). ## License This software is released under the *MIT License*. See the file [LICENSE.md](./LICENSE.md) for more information. [API]: https://libsndfile.github.io/libsndfile/api.html [libsndfile]: https://libsndfile.github.io/libsndfile/ [Nim]: https://nim-lang.org/ ================================================ FILE: examples/config.nims ================================================ switch("path", "$projectDir/../src") ================================================ FILE: examples/dump_loops.nim ================================================ ## Read an audio sample file and print information about defined sample loops import std/[os, strformat] import sndfile proc main() = if paramCount() != 1: echo "Usage: dump_loops " quit(QuitFailure) var filename = paramStr(1) # Open the file var info: SFInfo var sf = sndfile.open(filename.cstring, SFMode.READ, info.addr) if sf.isNil: echo $sf.strerror() quit(QuitFailure) echo &"File: {filename}" echo &"Sample frames: {info.frames}" var instr = SFInstrument() if SFBool(command(sf, GET_INSTRUMENT, instr.addr, sizeof(instr).cint)) == FALSE: echo "Loops: no instrument data found" quit(QuitSuccess) else: echo &"Root note: {instr.basenote}" echo &"Loops: {instr.loopCount}" for i in 0.. MaxChannels: raise newException(ValueError, "Only mono or stereo files are suported.") let samples = createSharedU(Sample, info.frames * info.channels) if samples.isNil: raise newException(ResourceExhaustedError, "Error allocating memory for sample buffer") var samplesRead = sf.readFFloat(samples, info.frames) if samplesRead != info.frames: warn "Mismatch between # samples read (as returned by sf_readf_float) and info.frames:" warn &"{samplesRead} != {info.frames}" result.frames = if samplesRead != 0: samplesRead else: info.frames result.samples = cast[SampleBuffer](samples) result.channels = info.channels result.samplerate = info.samplerate result.pos = 0 proc convertSampleRate(input: SampleBuffer, inputSamples, inputRate, outputRate, channels: int): tuple[samples: SampleBuffer, frames: int] = var data: SrcData # Calculate the number of output frames outputFrames = int(inputSamples * outputRate / inputRate) var output = createSharedU(Sample, outputFrames * channels) if output.isNil: raise newException(ResourceExhaustedError, "Error allocating memory for resample buffer") # Set up the SRC_DATA structure data.dataIn = cast[ptr UncheckedArray[cfloat]](input) data.inputFrames = inputSamples.clong data.dataOut = cast[ptr UncheckedArray[cfloat]](output) data.outputFrames = outputFrames.clong data.srcRatio = float64(outputRate / inputRate) data.endOfInput = 0 # Perform the sample rate conversion var error = srcSimple(addr data, SINC_FASTEST, channels.cint) if error != 0: freeShared(output) raise newException(IOError, &"Error during sample rate conversion: {srcStrError(error)}") else: # Set output values result.samples = cast[SampleBuffer](output) result.frames = data.outputFramesGen.int proc processCb(nFrames: NFrames, arg: pointer): cint {.cdecl.} = var outL = cast[SampleBuffer](portGetBuffer(outPortL, nFrames)) var outR = cast[SampleBuffer](portGetBuffer(outPortR, nFrames)) let audio = cast[ptr AudioFile](arg) for i in 0 ..< nFrames: outL[i] = audio.samples[audio.pos] * Gain if audio.channels == 1: outR[i] = audio.samples[audio.pos] * Gain else: outR[i] = audio.samples[audio.pos + 1] * Gain audio.pos += audio.channels if audio.pos >= audio.frames * audio.channels: audio.pos = 0 return 0 proc main() = addHandler(log) if paramCount() != 1: echo "Usage: playfile_jacket " quit(QuitFailure) # Print libsndfile version info &"libsdnfile version: {versionString()}" # Print JACK version info &"JACK version: {getVersionString()}" # Create JACK client setErrorFunction(errorCb) jclient = clientOpen("playfile_jack", NullOption, status.addr) debug &"JACK server status: {status}" if jclient == nil: error getJackStatusErrorString(status) quit QuitFailure var audio: AudioFile try: audio = readAudio(paramStr(1)) except CatchableError: error getCurrentExceptionMsg() cleanup() quit QuitFailure defer: cleanup() freeShared(audio.samples) info &"Channels: {audio.channels}" info &"Samplerate: {audio.samplerate}" info &"Frames: {audio.frames}" var jackSampleRate = int(jclient.getSamplerate()) if audio.samplerate != jackSampleRate: info &"Resampling audio to {jackSampleRate} Hz ..." try: var resampled = convertSampleRate(audio.samples, audio.frames, audio.samplerate, jackSampleRate, audio.channels) freeShared(audio.samples) audio.samples = resampled.samples audio.frames = resampled.frames except CatchableError: error getCurrentExceptionMsg() quit QuitFailure info "Ready." # Set up signal handlers to clean up on exit when defined(windows): setSignalProc(signalCb, SIGABRT, SIGINT, SIGTERM) else: setSignalProc(signalCb, SIGABRT, SIGHUP, SIGINT, SIGQUIT, SIGTERM) # Register JACK callbacks if jclient.setProcessCallback(processCb, audio.addr) != 0: error "Could not set JACK process callback function." quit QuitFailure jclient.onShutdown(shutdownCb) # Create output ports outPortL = jclient.portRegister("out_1", JackDefaultAudioType, PortIsOutput, 0) outPortR = jclient.portRegister("out_2", JackDefaultAudioType, PortIsOutput, 0) # Activate JACK client ... if jclient.activate() == 0: # Connect our output ports to system playback ports jclient.connect(outPortL.portName(), "system:playback_1") jclient.connect(outPortR.portName(), "system:playback_2") # ... and keep running until a signal is received while not exitSignalled: sleep(50) main() ================================================ FILE: examples/playfile_sdl.nim ================================================ ## Read an audio file (e.g. WAV or Ogg Vorbis) with libsndfile and play it with sdl2 import std/[math, os, strformat] import sndfile import sdl2, sdl2/audio if paramCount() != 1: echo "Usage: playfile " quit(QuitFailure) var filename = paramStr(1) # Print libsndfile version echo &"libsdnfile version: {versionString()}" # Print SDL version var version: SDL_Version sdl2.getVersion(version) echo &"SDL version: {version.major}.{version.minor}.{version.patch}" # Open the file var info: SFInfo var file = sndfile.open(filename.cstring, SFMode.READ, info.addr) if file.isNil: echo $file.strerror() quit(QuitFailure) echo &"Channels: {info.channels}" echo &"Frames: {info.frames}" echo &"Samplerate: {info.samplerate}" echo &"Format: {info.format}" # Callback procedure for audio playback const bufferSizeInSamples = 4096 proc audioCallback(userdata: pointer; stream: ptr uint8; len: cint) {.cdecl.} = var buffer: array[bufferSizeInSamples, cfloat] let count = file.readFloat(addr buffer[0], bufferSizeInSamples) if count == 0: echo "End of file reached" quit(0) for i in 0..".}: cint SIGHUP* {.importc: "SIGHUP", header: "".}: cint SIGQUIT* {.importc: "SIGQUIT", header: "".}: cint else: const SIGTERM* = cint(15) type CSighandlerT = proc (a: cint) {.noconv.} proc setSignalProc* (`proc`: CSighandlerT, signals: varargs[cint]) = for sig in signals: discard c_signal(sig, `proc`) ================================================ FILE: sndfile.nimble ================================================ version = "0.2.6" author = "Julien Aubert, Christopher Arndt" description = "Wrapper for libsndfile" license = "MIT" srcDir = "src" skipDirs = @["examples"] requires "nim >= 2.0" taskrequires "examples", "sdl2" taskrequires "examples", "jacket >= 0.2.0" taskrequires "examples_debug", "sdl2" taskrequires "examples_debug", "jacket >= 0.2.0" let examples = @[ "dump_loops", "list_formats", "playfile_jack", "playfile_sdl", ] task examples, "Build examples (release)": for example in examples: echo "Building example '" & example & "'..." selfExec("compile -d:release -d:strip examples/" & example & ".nim") task examples_debug, "Build examples (debug)": for example in examples: echo "Building example '" & example & "' (debug)..." selfExec("compile examples/" & example & ".nim") ================================================ FILE: src/sndfile/api.nim ================================================ when defined(windows): const soname = "(|lib)sndfile(|-1|-2).dll" elif defined(macosx): const soname = "libsndfile.dylib" else: const soname = "libsndfile.so(|.1)" {.pragma: libsnd, cdecl, dynlib: soname.} const codingHistSize {.intdefine: "sfCodingHistSize".}: uint32 = 256 const maxTagTextSize {.intdefine: "sfMaxTagTextSize".}: uint32 = 256 type # The following three enums are one enum in soundfile.h # Separated here into three enums for clarity SFBool* {.pure, size: sizeof(cint).} = enum FALSE TRUE SFMode* {.pure, size: sizeof(cint).} = enum READ = 0x10 WRITE = 0x20 RDWR = 0x30 SFAmbisonic* {.pure, size: sizeof(cint).} = enum NONE = 0x40 B_FORMAT = 0x41 SFSeek* {.pure, size: sizeof(cint).} = enum SET CUR END SFErr* {.pure, size: sizeof(cint).} = enum NO_ERROR UNRECOGNISED_FORMAT SYSTEM MALFORMED_FILE UNSUPPORTED_ENCODING SFCommand* {.pure, size: sizeof(cint).} = enum GET_LIB_VERSION = 0x1000 GET_LOG_INFO = 0x1001 GET_CURRENT_SF_INFO = 0x1002 GET_NORM_DOUBLE = 0x1010 GET_NORM_FLOAT = 0x1011 SET_NORM_DOUBLE = 0x1012 SET_NORM_FLOAT = 0x1013 SET_SCALE_FLOAT_INT_READ = 0x1014 SET_SCALE_INT_FLOAT_WRITE = 0x1015 GET_SIMPLE_FORMAT_COUNT = 0x1020 GET_SIMPLE_FORMAT = 0x1021 GET_FORMAT_INFO = 0x1028 GET_FORMAT_MAJOR_COUNT = 0x1030 GET_FORMAT_MAJOR = 0x1031 GET_FORMAT_SUBTYPE_COUNT = 0x1032 GET_FORMAT_SUBTYPE = 0x1033 CALC_SIGNAL_MAX = 0x1040 CALC_NORM_SIGNAL_MAX = 0x1041 CALC_MAX_ALL_CHANNELS = 0x1042 CALC_NORM_MAX_ALL_CHANNELS = 0x1043 GET_SIGNAL_MAX = 0x1044 GET_MAX_ALL_CHANNELS = 0x1045 SET_ADD_PEAK_CHUNK = 0x1050 SET_ADD_HEADER_PAD_CHUNK = 0x1051 UPDATE_HEADER_NOW = 0x1060 SET_UPDATE_HEADER_AUTO = 0x1061 FILE_TRUNCATE = 0x1080 SET_RAW_START_OFFSET = 0x1090 SET_DITHER_ON_WRITE = 0x10A0 SET_DITHER_ON_READ = 0x10A1 GET_DITHER_INFO_COUNT = 0x10A2 GET_DITHER_INFO = 0x10A3 GET_EMBED_FILE_INFO = 0x10B0 SET_CLIPPING = 0x10C0 GET_CLIPPING = 0x10C1 GET_INSTRUMENT = 0x10D0 SET_INSTRUMENT = 0x10D1 GET_LOOP_INFO = 0x10E0 GET_BROADCAST_INFO = 0x10F0 SET_BROADCAST_INFO = 0x10F1 GET_CHANNEL_MAP_INFO = 0x1100 SET_CHANNEL_MAP_INFO = 0x1101 RAW_DATA_NEEDS_ENDSWAP = 0x1110 WAVEX_SET_AMBISONIC = 0x1200 WAVEX_GET_AMBISONIC = 0x1201 RF64_AUTO_DOWNGRADE = 0x1210 SET_VBR_ENCODING_QUALITY = 0x1300 SET_COMPRESSION_LEVEL = 0x1301 SET_OGG_PAGE_LATENCY_MS = 0x1302 SET_OGG_PAGE_LATENCY = 0x1303 GET_BITRATE_MODE = 0x1304 SET_BITRATE_MODE = 0x1305 GET_OGG_STREAM_SERIALNO = 0x1306 SET_CART_INFO = 0x1400 GET_CART_INFO = 0x1401 SET_ORIGINAL_SAMPLERATE = 0x1500 GET_ORIGINAL_SAMPLERATE = 0x1501 TEST_IEEE_FLOAT_REPLACE = 0x6001 SFFormat* {.pure, size: sizeof(cint).} = enum # Subtypes PCM_S8 = 0x0001 PCM_16 = 0x0002 PCM_24 = 0x0003 PCM_32 = 0x0004 PCM_U8 = 0x0005 FLOAT = 0x0006 DOUBLE = 0x0007 ULAW = 0x0010 ALAW = 0x0011 IMA_ADPCM = 0x0012 MS_ADPCM = 0x0013 GSM610 = 0x0020 VOX_ADPCM = 0x0021 NMS_ADPCM_16 = 0x0022 NMS_ADPCM_24 = 0x0023 NMS_ADPCM_32 = 0x0024 G721_32 = 0x0030 G723_24 = 0x0031 G723_40 = 0x0032 DWVW_12 = 0x0040 DWVW_16 = 0x0041 DWVW_24 = 0x0042 DWVW_N = 0x0043 DPCM_8 = 0x0050 DPCM_16 = 0x0051 VORBIS = 0x0060 OPUS = 0x0064 ALAC_16 = 0x0070 ALAC_20 = 0x0071 ALAC_24 = 0x0072 ALAC_32 = 0x0073 MPEG_LAYER_I = 0x0080 MPEG_LAYER_II = 0x0081 MPEG_LAYER_III = 0x0082 # Main types WAV = 0x010000 AIFF = 0x020000 AU = 0x030000 RAW = 0x040000 PAF = 0x050000 SVX = 0x060000 NIST = 0x070000 VOC = 0x080000 IRCAM = 0x0A0000 W64 = 0x0B0000 MAT4 = 0x0C0000 MAT5 = 0x0D0000 PVF = 0x0E0000 XI = 0x0F0000 HTK = 0x100000 SDS = 0x110000 AVR = 0x120000 WAVEX = 0x130000 SD2 = 0x160000 FLAC = 0x170000 CAF = 0x180000 WVE = 0x190000 OGG = 0x200000 MPC2K = 0x210000 RF64 = 0x220000 SFFormatMask* {.pure, size: sizeof(cint).} = enum SUBMASK = 0x0000FFFF TYPEMASK = 0x0FFF0000 ENDMASK = 0x30000000 # We put the endian-ness options in a separate enum, because Nim enums # don't allow duplicate values and values must be increasing. SFEndian* {.pure, size: sizeof(cint).} = enum FILE = 0x00000000 LITTLE = 0x10000000 BIG = 0x20000000 CPU = 0x30000000 SFStrType* {.pure, size: sizeof(cint).} = enum TITLE = 0x01 COPYRIGHT = 0x02 SOFTWARE = 0x03 ARTIST = 0x04 COMMENT = 0x05 DATE = 0x06 ALBUM = 0x07 LICENSE = 0x08 TRACKNUMBER = 0x09 GENRE = 0x10 SFChannelMap* {.pure, size: sizeof(cint).} = enum INVALID MONO LEFT RIGHT CENTER FRONT_LEFT FRONT_RIGHT FRONT_CENTER REAR_CENTER REAR_LEFT REAR_RIGHT LFE FRONT_LEFT_OF_CENTER FRONT_RIGHT_OF_CENTER SIDE_LEFT SIDE_RIGHT TOP_CENTER TOP_FRONT_LEFT TOP_FRONT_RIGHT TOP_FRONT_CENTER TOP_REAR_LEFT TOP_REAR_RIGHT TOP_REAR_CENTER AMBISONIC_B_W AMBISONIC_B_X AMBISONIC_B_Y AMBISONIC_B_Z MAX SFBitrateMode* {.pure, size: sizeof(cint).} = enum CONSTANT AVERAGE VARIABLE SFLoopMode* {.pure, size: sizeof(cint).} = enum NONE = 800 FORWARD BACKWARD ALTERNATING type SndFile* = distinct object SFCount* = int64 SFInfo* = object frames*: SFCount samplerate*: cint channels*: cint format*: cint sections*: cint seekable*: cint SFFormatInfo* = object format*: cint name*: cstring extension*: cstring SFEmbedFileInfo* = object offset*: SFCount length*: SFCount SFCuePoint* = object indx*: int32 position*: uint32 fccChunk*: int32 chunkStart*:int32 blockStart*: int32 sampleOffset*: uint32 name*: array[256, char] SFLoop* = object mode*: SFLoopMode startPos*: uint32 endPos*: uint32 # 'end' is a keyword in Nim count*: uint32 SFInstrument* = object gain*: cint basenote*: byte detune*: byte velocityLo*: byte velocityHi*: byte keyLo*: byte keyHi*: byte loopCount*: cint loops*: array[16, SFLoop] SFLoopInfo* = object timeSigNum*: cshort timeSigDen*: cshort loopMode*: SFLoopMode numBeats*: cint bpm*: cfloat rootKey*: cint future*: array[6, cint] SFBroadcastInfo* = object description*: array[256, char] originator*: array[32, char] originatorReference*: array[32, char] originationDate*: array[10, char] originationTime*: array[8, char] timeReferenceLow*: uint32 timeReferenceHigh*: uint32 version*: cshort umid*: array[64, char] loudnessValue*: int16 loudnessRange*: int16 maxTruePeakLevel*: int16 maxMomentaryLoudness*: int16 maxShorttermLoudness*: int16 reserved*: array[180, byte] codingHistorySize*: uint32 codingHistory*: array[codingHistSize, char] SFCartTimer* = object usage*: array[4, char] value*: int32 SFCartInfo* = object version*: array[4, char] title*: array[64, char] artist*: array[64, char] cutId*: array[64, char] clientId*: array[64, char] category*: array[64, char] classification*: array[64, char] outCue*: array[64, char] startDate*: array[10, char] startTime*: array[8, char] endDate*: array[10, char] endTime*: array[8, char] producerAppId*: array[64, char] producerAppVersion*: array[64, char] userDef*: array[64, char] levelReference*: int32 postTimers*: array[8, SFCartTimer] reserved*: array[276, char] url*: array[1024, char] tagTextSize*: uint32 tagText*: array[maxTagTextSize, char] SFChunkInfo* = object id*: array[64, char] idSize*: cuint dataLen*: cuint data*: ptr UncheckedArray[byte] SFChunkIterator* = distinct object proc versionString*(): cstring {.libsnd, importc: "sf_version_string".} proc open*(path: cstring, mode: SFMode, sfInfo: ptr SFInfo): ptr SndFile {.libsnd, importc: "sf_open".} proc close*(sndfile: ptr SndFile): cint {.libsnd, importc: "sf_close", discardable.} proc formatCheck*(info: ptr SFInfo): SFBool {.libsnd, importc: "sf_format_check".} proc seek*(sndfile: ptr SndFile, frames: SFCount, whence: SFSeek): SFCount {.libsnd, importc: "sf_seek".} proc command*(sndfile: ptr SndFile, cmd: SFCommand, data: pointer, datasize: cint): cint {.libsnd, importc: "sf_command", discardable.} proc error*(sndfile: ptr SndFile): cint {.libsnd, importc: "sf_error".} proc errorNumber*(errnum: int): cstring {.libsnd, importc: "sf_error_number".} proc strError*(sndfile: ptr SndFile): cstring {.libsnd, importc: "sf_strerror".} proc setString*(sndfile: ptr SndFile, strType: SFStrType, str: cstring): cint {.libsnd, importc: "sf_set_string".} proc getString*(sndfile: ptr SndFile, strType: SFStrType): cstring {.libsnd, importc: "sf_get_string".} proc currentByterate*(sndfile: ptr SndFile): cint {.libsnd, importc: "sf_current_byterate".} proc readShort*(sndfile: ptr SndFile, bufferPtr: ptr cshort, items: SFCount): SFCount {.libsnd, importc: "sf_read_short".} proc readInt*(sndfile: ptr SndFile, bufferPtr: ptr cint, items: SFCount): SFCount {.libsnd, importc: "sf_read_int".} proc readFloat*(sndfile: ptr SndFile, bufferPtr: ptr cfloat, items: SFCount): SFCount {.libsnd, importc: "sf_read_float".} proc readDouble*(sndfile: ptr SndFile, bufferPtr: ptr cdouble, items: SFCount): SFCount {.libsnd, importc: "sf_read_double".} proc readFShort*(sndfile: ptr SndFile, bufferPtr: ptr cshort, frames: SFCount): SFCount {.libsnd, importc: "sf_readf_short".} proc readFInt*(sndfile: ptr SndFile, bufferPtr: ptr cint, frames: SFCount): SFCount {.libsnd, importc: "sf_readf_int".} proc readFFloat*(sndfile: ptr SndFile, bufferPtr: ptr cfloat, frames: SFCount): SFCount {.libsnd, importc: "sf_readf_float".} proc readFDouble*(sndfile: ptr SndFile, bufferPtr: ptr cdouble, frames: SFCount): SFCount {.libsnd, importc: "sf_readf_double".} proc writeShort*(sndfile: ptr SndFile, bufferPtr: ptr cshort; items: SFCount): SFCount {.libsnd, importc: "sf_write_short".} proc writeInt*(sndfile: ptr SndFile, bufferPtr: ptr cint; items: SFCount): SFCount {.libsnd, importc: "sf_write_int".} proc writeFloat*(sndfile: ptr SndFile, bufferPtr: ptr cfloat; items: SFCount): SFCount {.libsnd, importc: "sf_write_float".} proc writeDouble*(sndfile: ptr SndFile, bufferPtr: ptr cdouble; items: SFCount): SFCount {.libsnd, importc: "sf_write_double".} proc writeFShort*(sndfile: ptr SndFile, bufferPtr: ptr cshort; frames: SFCount): SFCount {.libsnd, importc: "sf_writef_short".} proc writeFInt*(sndfile: ptr SndFile, bufferPtr: ptr cint; frames: SFCount): SFCount {.libsnd, importc: "sf_writef_int".} proc writeFFloat*(sndfile: ptr SndFile, bufferPtr: ptr cfloat; frames: SFCount): SFCount {.libsnd, importc: "sf_writef_float".} proc writeFDouble*(sndfile: ptr SndFile, bufferPtr: ptr cdouble; frames: SFCount): SFCount {.libsnd, importc: "sf_writef_double".} proc readRaw*(sndfile: ptr SndFile, bufferPtr: ptr byte, numBytes: SFCount): SFCount {.libsnd, importc: "sf_read_raw".} proc writeRaw*(sndfile: ptr SndFile, bufferPtr: ptr byte, numBytes: SFCount) {.libsnd, importc: "sf_write_raw".} proc writeSync*(sndfile: ptr SndFile) {.libsnd, importc: "sf_write_sync".} proc setChunk*(sndfile: ptr SndFile, chunkInfo: ptr SFChunkInfo): SFErr {.libsnd, importc: "sf_set_chunk".} proc getChunkIterator*(sndfile: ptr SndFile, chunkInfo: ptr SFChunkInfo): ptr SFChunkIterator {.libsnd, importc: "sf_get_chunk_iterator".} proc nextChunkIterator*(it: ptr SFChunkIterator): ptr SFChunkIterator {.libsnd, importc: "sf_next_chunk_iterator".} proc getChunkSize*(it: ptr SFChunkIterator, chunkInfo: ptr SFChunkInfo): SFErr {.libsnd, importc: "sf_get_chunk_size".} proc getChunkData*(it: ptr SFChunkIterator, chunkInfo: ptr SFChunkInfo): SFErr {.libsnd, importc: "sf_get_chunk_data".} ================================================ FILE: src/sndfile/chunks.nim ================================================ import api # Data types for WAV file 'smpl' chunks type ## A single sample loop SmplLoop* = object id*: int32 loopType*: int32 startPos*: int32 endPos*: int32 fraction*: int32 playCount*: int32 ## A 'smpl' chunk, containing information for sample player instruments, ## such as manufacturer and product ID of the instrument that is meant ## to use this sample, the MIDI root note, zero or more sample loop ## definitions and, optionally, extra sample player specific data. SmplChunk* = object manufacturer*: array[4, byte] product*: int32 samplePeriod*: int32 midiUnityNote*: int32 midiPitchFraction*: int32 smpteFormat*: int32 smpteOffset*: int32 loopCount*: int32 samplerDataLen*: int32 loops*: UncheckedArray[SmplLoop] ## ## Iterate over all chunks in the audio file, whose chunk ID starts with ## `chunk_id`. The loop variable is assigned a `seq[byte]` with the data of ## each matching chunk. ## iterator chunks*(sndfile: ptr SndFile, chunk_id: string): seq[byte] = var chunkInfo = SFChunkInfo() if chunk_id.len - 1 > chunkInfo.id.high: raise newException(ValueError, "Length of chunk_id must be <= 64.") chunkInfo.idSize = chunk_id.len.cuint chunkInfo.id[0.. 0: data.setLen(chunkInfo.dataLen) chunkInfo.data = cast[ptr UncheckedArray[byte]](data[0].addr) if getChunkData(it, chunkInfo.addr) != NO_ERROR: # XXX Better exception type and error message raise newException(IOError, "Could not read next chunk.") else: raise newException(IOError, "Could not get chunk size.") yield data it = nextChunkIterator(it) ================================================ FILE: src/sndfile.nim ================================================ import sndfile/api import sndfile/chunks export api export chunks ================================================ FILE: tests/config.nims ================================================ switch("path", "$projectDir/../src") ================================================ FILE: tests/test_chunks.nim ================================================ import std/[os, unittest] import sndfile proc check_smpl_chunk( sf: ptr SndFile, manufacturer: array[4, byte], samplePeriod: int32, midiUnityNote: int32, samplerDataLen: int32, loopCount: int32, loopType: int32, startPos: int32, endPos: int32 ): bool = var found = 0 for chunk in sf.chunks("smpl"): found += 1 check(typeof(chunk) is seq[byte]) if found == 1: # Only check the first 'smpl' chunk - there should be only one. var smpl = cast[ptr SmplChunk](chunk[0].addr) check(smpl.manufacturer == manufacturer) check(smpl.samplePeriod == samplePeriod) check(smpl.midiUnityNote == midiUnityNote) check(smpl.samplerDataLen == samplerDataLen) check(smpl.loopCount == loopCount) check(smpl.loops[0].loopType == loopType) check(smpl.loops[0].startPos == startPos) check(smpl.loops[0].endPos == endPos) let expectedSize = sizeof(SmplChunk) + smpl.samplerDataLen + smpl.loopCount * sizeof(SmplLoop) check(chunk.len == expectedSize) return found == 1 suite "Tests for reading chunks from a WAV file": test "test read smpl chunk": var info: SFInfo var sf: ptr SndFile info.format = 0 sf = open(cstring(getAppDir() / "test_chunks.wav"), READ, info.addr) check(not sf.isNil) defer: close(sf) check( check_smpl_chunk( sf, manufacturer = [0x47.byte, 0, 0, 1], samplePeriod = 22675, midiUnityNote = 1, samplerDataLen = 18, loopCount = 1, loopType = 0, startPos = 56252, endPos = 240748 ) ) test "test write and read back smpl chunk": var infoIn, infoOut: SFInfo var sfIn, sfOut: ptr SndFile infoIn.format = 0 sfIn = open(cstring(getAppDir() / "sine.wav"), READ, infoIn.addr) check(not sfIn.isNil) infoOut = infoIn sfOut = open(cstring(getAppDir() / "sineloop.wav"), WRITE, infoOut.addr) check(not sfOut.isNil) var ci = SFChunkInfo() ci.id[0..3] = "smpl" ci.idSize = 4 ci.dataLen = sizeof(SmplChunk) + sizeof(SmplLoop) ci.data = cast[ptr UncheckedArray[byte]](alloc0(ci.dataLen)) var smpl = cast[ptr SmplChunk](ci.data) smpl.manufacturer = [1.byte, 0, 0, 1] smpl.samplePeriod = int32(1_000_000_000 / infoIn.samplerate) smpl.midiUnityNote = 69 smpl.loopCount = 1 var loop = cast[ptr SmplLoop](smpl.loops[0].addr) loop.startPos = 0 loop.endPos = infoIn.frames.int32 check(sfOut.setChunk(ci.addr) == NO_ERROR) var frames, readcount: cint buf: array[1024, cfloat] frames = cint(1024 / infoOut.channels) readcount = frames while readcount > 0: readcount = sfIn.readFFloat(buf[0].addr, frames).cint discard sfOut.writeFFLoat(buf[0].addr, readcount) sfIn.close() sfOut.close() dealloc(ci.data) infoIn.format = 0 sfIn = open(cstring(getAppDir() / "sineloop.wav"), READ, infoIn.addr) check(not sfIn.isNil) check( check_smpl_chunk( sfIn, manufacturer = [1.byte, 0, 0, 1], samplePeriod = 20833, midiUnityNote = 69, samplerDataLen = 0, loopCount = 1, loopType = 0, startPos = 0, endPos = 48_000 ) ) sfIn.close() ================================================ FILE: tests/test_testwav.nim ================================================ import std/[os, unittest] import sndfile suite "Tests for reading a simple WAV file": test "test reading info": var info: SFInfo var sndFile: ptr SndFile info.format = 0 sndFile = open(cstring(getAppDir() / "sine.wav"), READ, info.addr) check(not sndFile.isNil) # expect info read from sound file header to be valid check(formatCheck(info.addr) == TRUE) check($info == "(frames: 48000, samplerate: 48000, channels: 1, format: 65538, sections: 1, seekable: 1)") check(seek(sndFile, 5, SFSeek.SET) == 5) discard seek(sndFile, 0, SFSeek.SET) let num_items = info.channels * info.frames check(num_items == 48_000) var buffer = newSeq[cint](num_items) let items_read = read_int(sndFile, buffer[0].addr, num_items) check(items_read == 48_000) close(sndFile) ================================================ FILE: tools/build_docs.sh ================================================ #!/bin/bash PROJECT="sndfile" REPO_URL="https://github.com/SpotlightKid/nim-sndfile" DOC_DIR="$(pwd)/.gh-pages" nimble doc \ --index:on \ --project \ --git.url:"$REPO_URL" \ --git.commit:master \ --out:"$DOC_DIR" \ src/$PROJECT.nim cp "$DOC_DIR"/$PROJECT.html "$DOC_DIR"/index.html xdg-open file://"$DOC_DIR"/index.html