├── .gitignore ├── LICENSE.md ├── README.md ├── config.nims ├── mosh.nimble └── src ├── convert.nim ├── convertutils.nim ├── markov.nim ├── mosh.nim ├── unsignedint24.nim └── wav.nim /.gitignore: -------------------------------------------------------------------------------- 1 | .DS_Store 2 | reader 3 | mosh 4 | /test 5 | *.wav 6 | *.maxhelp 7 | *.raw 8 | *.exe 9 | -------------------------------------------------------------------------------- /LICENSE.md: -------------------------------------------------------------------------------- 1 | BSD 3-Clause License 2 | 3 | Copyright (c) 2021, James Bradbury 4 | All rights reserved. 5 | 6 | Redistribution and use in source and binary forms, with or without 7 | modification, are permitted provided that the following conditions are met: 8 | 9 | 1. Redistributions of source code must retain the above copyright notice, this 10 | list of conditions and the following disclaimer. 11 | 12 | 2. Redistributions in binary form must reproduce the above copyright notice, 13 | this list of conditions and the following disclaimer in the documentation 14 | and/or other materials provided with the distribution. 15 | 16 | 3. Neither the name of the copyright holder nor the names of its 17 | contributors may be used to endorse or promote products derived from 18 | this software without specific prior written permission. 19 | 20 | THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" 21 | AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE 22 | IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE 23 | DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE 24 | FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL 25 | DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR 26 | SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER 27 | CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, 28 | OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE 29 | OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 30 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | This repository is now archived. Please use https://github.com/jamesb93/bend-rust 2 | 3 | # bend 4 | 5 | bend is a small command-line application written in Nim for 'bending' data into audio. There is also an experimental markov-based synthesiser that can help you generate audio based on the results of your conversion process. 6 | 7 | 8 | bend is fast, small and bare bones and can be compiled to any platform that Nim can compile to. This includes MacOS, Windows, Linux, iOS, Android. 9 | 10 | 11 | This project stems from my own perverse use of [SoX](http://sox.sourceforge.net) to _bend_ raw data into audio with the command: 12 | 13 | `sox -r 44100 -b 8 -c 1 -e unsigned-integer input.raw output.wav` 14 | 15 | 16 | I wanted to write a small application that could implement this functionality but without the overhead of having to install SoX, a tool suited to performing a number of other DSP tasks that I didn't need. This was also a fun project to learn more about the structure of WAVE audio as well as experimenting with the Nim language. 17 | 18 | 19 | [Francesco Cameli](github.com/vitreo12) is a significant contributor, particularly in optimising the code to be fast (from 100ms to less than 5ms!). I'd like to also thank him for his patience and guidance on all things Nim. 20 | 21 | 22 | # Installation 23 | 24 | First, it is most convenient to have access to the `nim` compiler as well as `nimble`. To install both of these you can use [choosenim](https://github.com/dom96/choosenim#installation). 25 | 26 | Once you have `choosenim` you can `git clone` this repo, `cd` to it and run the following `nimble` command: 27 | 28 | `nimble install` 29 | 30 | This will give you an executable inside the project folder that you can use in place or move to your `$PATH`. 31 | 32 | # Usage 33 | 34 | You can pass a folder or file as the input and the executable will convert all files in the folder recursively or a single file and place it at the output which is a directory or file. 35 | 36 | 37 | an input file/folder and an output file/folder name. Example: 38 | 39 | `bend cat.png output.wav` 40 | 41 | Which could convert the `cat.png` file to a new wav file `output.wav`. 42 | 43 | or 44 | 45 | `bend foo bar` 46 | 47 | Which would convert all the files inside the directory `foo` recursively into a new folder called `bar`. 48 | 49 | ## Help and issues 50 | --- 51 | 52 | 53 | Detailed help can be found by running `bend -h` or `bend convert -h` or `bend generate -h`. 54 | 55 | If you have any issues or questions please raise one on the github! 56 | -------------------------------------------------------------------------------- /config.nims: -------------------------------------------------------------------------------- 1 | #danger 2 | --define:danger 3 | --objChecks:off 4 | --fieldChecks:off 5 | --rangeChecks:off 6 | --boundChecks:off 7 | --overflowChecks:off 8 | --assertions:off 9 | --stacktrace:off 10 | --linetrace:off 11 | --debugger:off 12 | --lineDir:off 13 | --deadCodeElim:on 14 | --nilchecks:off 15 | 16 | #release 17 | --gc:refc 18 | --define:release 19 | --excessiveStackTrace:off 20 | --opt:speed 21 | 22 | #threading 23 | --threads:on 24 | 25 | when defined(nimHasNilChecks): 26 | --nilchecks:off 27 | -------------------------------------------------------------------------------- /mosh.nimble: -------------------------------------------------------------------------------- 1 | version = "1.0.0" 2 | author = "James Bradbury" 3 | description = "bend can turn any input file into audio files in the wav format. It also has functionality for generating new 8bit audio with Markov processes." 4 | license = "MIT" 5 | 6 | srcDir = "src" 7 | bin = @["bend"] 8 | 9 | requires "nim >= 2.0.0" 10 | requires "cligen == 1.6.13" 11 | -------------------------------------------------------------------------------- /src/convert.nim: -------------------------------------------------------------------------------- 1 | import std/[os, threadpool, terminal, math], system 2 | import convertutils 3 | {. experimental: "parallel" .} 4 | 5 | proc conversion*( 6 | input: string, 7 | output: string, 8 | bitDepth: uint16 = 8, 9 | numChans: uint16 = 1, 10 | sampRate: uint32 = 44100, 11 | limit: float = 5000, 12 | maxSize: float = 4096, 13 | dc: bool = true 14 | ): void = 15 | 16 | let iPath = sanitisePath(input) 17 | let oPath = sanitisePath(output) 18 | #-- Discern the input/output information --# 19 | let iType: FileType = discernFile(iPath) 20 | 21 | #-- Make the output directory if it does not exist --# 22 | if iType == dir: 23 | checkMake(output) 24 | 25 | #-- Make sure that the input and output are not the same file --# 26 | if sameFile(iPath, oPath): 27 | echo "You cannot set the same input and output file for safety reasons" 28 | quit() 29 | 30 | #-- Operate on single files --# 31 | if iType == file: 32 | if getFileSize(iPath) != 0: 33 | createOutputFile( 34 | iPath, 35 | oPath, 36 | dc, 37 | sampRate, 38 | bitDepth, 39 | numChans, 40 | ) 41 | else: 42 | echo "Input file is 0 bytes!" 43 | quit() 44 | 45 | #-- Operate on folders --# 46 | if iType == dir: 47 | var mbAccum: float = 0 48 | var files: seq[string] 49 | 50 | for inputFilePath in walkDirRec(input): 51 | if mbAccum < limit and inputFilePath.parentDir() != oPath: 52 | try: 53 | var sizeMb = getFileSize(inputFilePath).float / (1024 * 1024).float # to mb 54 | if sizeMb < maxSize and sizeMb != 0: # Check for size boundaries 55 | files.add(inputFilePath) 56 | mbAccum += sizeMb 57 | except OSError: 58 | discard 59 | 60 | 61 | for i, iFile in files: 62 | # Terminal Writing 63 | let percentage: float = round((i / files.len) * 100.0) 64 | stdout.eraseLine() 65 | stdout.write(percentage) 66 | stdout.flushFile 67 | 68 | var oFile = oPath / iFile.extractFilename().formatDotFile().changeFileExt("wav") 69 | if not fileExists(oFile): 70 | parallel: spawn createOutputFile( 71 | iFile, 72 | oFile, 73 | dc, 74 | sampRate, 75 | bitDepth, 76 | numChans 77 | ) 78 | 79 | if iType == none: 80 | echo "There was an error with your input or output arguments." 81 | quit() -------------------------------------------------------------------------------- /src/convertutils.nim: -------------------------------------------------------------------------------- 1 | import memfiles, os 2 | import wav, unsignedint24 3 | 4 | type FileType* = enum 5 | file, 6 | dir, 7 | none 8 | 9 | proc formatDotFile*(input: string): string = 10 | if input[0] == '.': 11 | return input[1..^1] 12 | else: 13 | return input 14 | 15 | proc checkMake*(path: string) : void = 16 | if not dirExists(path): 17 | createDir(path) 18 | echo path, " did not exist and was created for you." 19 | 20 | proc discernFile*(path: string) : FileType = 21 | if fileExists(path): 22 | return file 23 | elif dirExists(path): 24 | return dir 25 | else: 26 | return none 27 | 28 | proc sanitisePath*(path: string) : string = 29 | return path.normalizedPath().expandTilde().absolutePath() 30 | 31 | proc ensureParity*(input: FileType, output: FileType) : bool = 32 | if input == output: 33 | return true 34 | else: 35 | echo "There was a mismatch between the type of input and output arguments" 36 | echo "They should both be either a file or folder." 37 | return false 38 | 39 | proc openRawFile*(path: string) : MemFile = 40 | return memfiles.open(path, fmRead) 41 | 42 | proc applyDCFilter*(dataDC : pointer, dataMem : pointer, dataSize : Natural, bitDepth : typedesc) : void = 43 | var 44 | xPrev = 0.0 45 | yPrev = 0.0 46 | dataArr = cast[ptr UncheckedArray[bitDepth]](dataMem) 47 | dataDCArr = cast[ptr UncheckedArray[bitDepth]](dataDC) 48 | 49 | let 50 | filterFB = 0.995 51 | scaleAmplitude = 0.4 52 | 53 | when bitDepth is uint8: 54 | let scaledSize = dataSize 55 | elif bitDepth is uint16: 56 | let scaledSize = int(dataSize / 2) 57 | elif bitDepth is uint24: 58 | let scaledSize = int(dataSize / 3) 59 | elif bitDepth is uint32: 60 | let scaledSize = int(dataSize / 4) 61 | else: 62 | {.fatal: "Invalid unsigned int type: " & $T.} 63 | 64 | for index in 0.. output file --# 92 | var 93 | inputData: MemFile = memfiles.open( 94 | inputFilePath, 95 | fmRead 96 | ) 97 | 98 | inputDataMem = inputData.mem 99 | inputDataSize = inputData.size 100 | 101 | dataDC: pointer 102 | 103 | header: wavHeader = createHeader( 104 | uint32(inputDataSize), 105 | sampRate, 106 | bitDepth, 107 | numChans 108 | ) 109 | 110 | #Create the output file 111 | var outputFile : File 112 | if not outputFile.open(outputFilePath, fmWrite): 113 | echo "ERROR: Could not create ", outputFilePath 114 | return 115 | 116 | #Apply DC filter 117 | if dcFilter: 118 | #raw bytes allocation 119 | dataDC = alloc(inputDataSize) 120 | 121 | if dataDC.isNil: 122 | echo "ERROR: Could not allocate data for DC filter" 123 | return 124 | 125 | case bitDepth: 126 | of 8: 127 | dataDC.applyDCFilter(inputDataMem, inputDataSize, uint8) 128 | of 16: 129 | dataDC.applyDCFilter(inputDataMem, inputDataSize, uint16) 130 | of 24: 131 | dataDC.applyDCFilter(inputDataMem, inputDataSize, uint24) 132 | of 32: 133 | dataDC.applyDCFilter(inputDataMem, inputDataSize, uint32) 134 | else: 135 | echo "ERROR: Invalid bitDepth: ", $bitDepth 136 | return 137 | 138 | #-- Write header --# 139 | for value in header.fields: 140 | when value is array: 141 | for arrayVal in value: 142 | discard outputFile.writeBuffer(unsafeAddr(arrayVal), sizeof(arrayVal)) 143 | else: 144 | discard outputFile.writeBuffer(unsafeAddr(value), sizeof(value)) 145 | 146 | #-- Write data to output --# 147 | if dcFilter: 148 | discard outputFile.writeBuffer(dataDC, inputDataSize) 149 | dataDC.dealloc 150 | else: 151 | discard outputFile.writeBuffer(inputDataMem, inputDataSize) 152 | 153 | # Close files 154 | outputFile.close() 155 | inputData.close() 156 | -------------------------------------------------------------------------------- /src/markov.nim: -------------------------------------------------------------------------------- 1 | import tables, random, os 2 | import wav 3 | 4 | proc buildChain(inputData:seq[uint8], order:int): Table[seq[uint8], seq[uint8]] = 5 | 6 | var stateGraph = initTable[seq[uint8], seq[uint8]]() 7 | echo "Analysing input data" 8 | var iterationLength = (len(inputData)-1) - order 9 | for i in 0..iterationLength: 10 | var mem = inputData[i..i+order] 11 | var key:seq[uint8] = mem[0..order-1] 12 | var pair:seq[uint8] = @[mem[order]] 13 | 14 | if not stateGraph.hasKey(key): 15 | stateGraph[key] = pair 16 | else: 17 | stateGraph[key].add(pair) 18 | return stateGraph 19 | 20 | proc generateFromChain( 21 | stateGraph:Table, 22 | iterations:int, 23 | order:int, 24 | originalData:seq[uint8], 25 | ): seq[uint8] = 26 | var res:seq[uint8] 27 | 28 | var 29 | randomPoint:int = rand(0..len(originalData)-order-1) 30 | randomSlice:seq[uint8] = originalData[randomPoint..randomPoint+(order-1)] 31 | previousStates:seq[uint8] = randomSlice 32 | echo "Generating new samples" 33 | for i in 0..iterations: 34 | var sampleSelection:seq[uint8] 35 | 36 | if stateGraph.hasKey(previousStates): 37 | sampleSelection = stateGraph[previousStates] 38 | else: 39 | var randomPoint:int = rand(0..len(originalData)-order-1) 40 | randomSlice = originalData[randomPoint..randomPoint+(order-1)] 41 | sampleSelection = stateGraph[randomSlice] 42 | 43 | var nextState = sample(sampleSelection) 44 | 45 | 46 | var nextKey:seq[uint8] = previousStates[1..previousStates.len()-1] 47 | nextKey.add(nextState) 48 | previousStates = nextKey 49 | 50 | res.add(nextState) 51 | return res 52 | 53 | proc doMarkov*( 54 | input:string, 55 | output:string, 56 | order:int, 57 | length:int, 58 | ): void = 59 | randomize() 60 | var data = wavToArray( 61 | expandTilde(input) 62 | ) 63 | var chain = buildChain(data, order) 64 | var newStates = generateFromChain(chain, length * 44100, order, data) 65 | discard arrayToWav( 66 | expandTilde(output), 67 | newStates 68 | ) -------------------------------------------------------------------------------- /src/mosh.nim: -------------------------------------------------------------------------------- 1 | import convert 2 | from std/tables import toTable 3 | 4 | when isMainModule: 5 | import cligen 6 | 7 | clCfg.version = "0.5.0" 8 | 9 | const Help = { 10 | "input" : "Input folder or file containing data.", 11 | "output" : "Output folder or a file.", 12 | "bitdepth" : "Bit-depth to render to.", 13 | "numchans" : "Number of channels to write to.", 14 | "samprate" : "Output samplerate", 15 | "limit" : "Limit in megabytes of folder contents to write", 16 | "maxsize" : "Maximum size of any individual file", 17 | "dc" : "Apply a DC filter to the output" 18 | }.toTable() 19 | 20 | dispatch(conversion, help=Help) 21 | -------------------------------------------------------------------------------- /src/unsignedint24.nim: -------------------------------------------------------------------------------- 1 | #-- 24 bit unsigned int --# 2 | type 3 | ## This type represents an unsigned 24 bit integer 4 | uint24* {.packed.} = object 5 | bit1 : uint8 6 | bit2 : uint8 7 | bit3 : uint8 8 | 9 | #https://stackoverflow.com/questions/7416699/how-to-define-24bit-data-type-in-c 10 | proc assignUInt24*(val : SomeUnsignedInt) : uint24 = 11 | #Store as little endian (the way WAV wants it) 12 | result.bit3 = uint8(val shr 16 and 0xff) 13 | result.bit2 = uint8(val shr 8 and 0xff) 14 | result.bit1 = uint8(val and 0xff) 15 | 16 | proc asUnsigned32Bit*(obj : uint24) : uint32 = 17 | return (uint32(obj.bit1)) or (uint32(obj.bit2) shl 8) or (uint32(obj.bit3) shl 16) 18 | 19 | -------------------------------------------------------------------------------- /src/wav.nim: -------------------------------------------------------------------------------- 1 | import streams, sequtils 2 | 3 | const fixedSize : uint32 = 36 4 | 5 | type wavHeader* = object 6 | chunkID*: array[4, char] 7 | chunkSize*: uint32 8 | format*: array[4, char] 9 | subChunk1ID*: array[4, char] 10 | subChunk1Size*: uint32 11 | audioFormat*: uint16 12 | numChannels*: uint16 13 | sampleRate*: uint32 14 | byteRate*: uint32 15 | blockAlign*: uint16 16 | bitDepth*: uint16 17 | subChunk2ID*: array[4, char] 18 | subChunk2Size*: uint32 19 | 20 | proc createHeader*( 21 | binarySize: uint32, 22 | kSampleRate: uint32, 23 | kBitDepth: uint16, 24 | kNumChannels: uint16 25 | ): wavHeader = 26 | # Wav header structure 27 | result.chunkID = ['R','I','F','F'] 28 | result.chunkSize = binarySize + fixedSize 29 | result.format = ['W','A','V','E'] 30 | result.subChunk1ID = ['f','m','t',' '] 31 | result.subChunk1Size = 16 32 | if kBitDepth == 32: 33 | result.audioFormat = 3 34 | else: 35 | result.audioFormat = 1 36 | result.numChannels = kNumChannels 37 | result.sampleRate = kSampleRate 38 | result.byteRate = kSampleRate * kNumChannels * kBitDepth div 8 39 | result.blockAlign = kNumChannels * kBitDepth div 8 40 | result.bitDepth = kBitDepth 41 | result.subChunk2ID = ['d','a','t','a'] 42 | result.subChunk2Size = binarySize 43 | 44 | proc arrayToWav*(outputFilePath:string, someData:seq[uint8]): bool = 45 | var header: wavHeader = createHeader( 46 | uint32(somedata.len()), 47 | uint32(44100), 48 | uint16(8), 49 | uint16(1) 50 | ) 51 | var outputFile : File 52 | if not outputFile.open(outPutFilePath, fmWrite): 53 | echo ("ERROR: Could not create", outputFilePath) 54 | return 55 | 56 | #-- Write Header Information --# 57 | for value in header.fields: 58 | when value is array: 59 | for arrayVal in value: 60 | discard outputFile.writeBuffer(unsafeAddr(arrayVal), sizeof(arrayVal)) 61 | else: 62 | discard outputFile.writeBuffer(unsafeAddr(value), sizeof(value)) 63 | 64 | # -- Write Data -- # 65 | for sample in someData: 66 | discard outputFile.writeBuffer(unsafeAddr(sample), sizeof(sample)) 67 | 68 | outputFile.close() 69 | 70 | return true 71 | 72 | 73 | proc wavToArray*(filePath: string): seq[uint8]= 74 | var container: seq[uint8] 75 | var data = newFileStream(filePath, mode=fmRead) 76 | data.setPosition(0) 77 | var chunkID = readStr(data, 4) 78 | var chunkSize = readUint32(data) 79 | var format = readStr(data, 4) 80 | 81 | # fmt sub-chunk 82 | var subChunk1ID = readStr(data, 4) 83 | while subChunk1ID != "fmt ": 84 | subChunk1ID = readStr(data, 4) 85 | assert subChunk1ID == "fmt " 86 | var subChunk1Size = readUint32(data) 87 | var audioFormat = readUint16(data) 88 | var numChannels = readUint16(data) 89 | var sampleRate = readUint32(data) 90 | var byteRate = readUint32(data) 91 | var blockAlign = readUint16(data) 92 | var bitDepth = readUint16(data) 93 | var subChunk2ID = readStr(data, 4) 94 | while subChunk2ID != "data": 95 | subChunk2ID = readStr(data, 4) 96 | assert subChunk2ID == "data" 97 | var subChunk2Size = readUint32(data) 98 | 99 | while not data.atEnd(): 100 | container.add( 101 | readUint8(data) 102 | ) 103 | var duplicatedArray:seq[uint8] = concat(container, container) 104 | return duplicatedArray --------------------------------------------------------------------------------