├── .gitattributes ├── README.md └── wave.lua /.gitattributes: -------------------------------------------------------------------------------- 1 | # Auto detect text files and perform LF normalization 2 | * text=auto 3 | 4 | # Custom for Visual Studio 5 | *.cs diff=csharp 6 | 7 | # Standard to msysgit 8 | *.doc diff=astextplain 9 | *.DOC diff=astextplain 10 | *.docx diff=astextplain 11 | *.DOCX diff=astextplain 12 | *.dot diff=astextplain 13 | *.DOT diff=astextplain 14 | *.pdf diff=astextplain 15 | *.PDF diff=astextplain 16 | *.rtf diff=astextplain 17 | *.RTF diff=astextplain 18 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # wave 2 | A flexible audio API for ComputerCraft 3 | 4 | Features: 5 | - Support for Command Computer and Iron Note Block ([MinimalPeripherals](https://github.com/justync7/MinimalPeripherals)) outputs 6 | - Support for Note Block Studio tracks 7 | - Instrument filters (for example: bass + base drum on one output, other instruments on another) 8 | - Multiple track playback at the same time 9 | - Volume control 10 | - Throttling 11 | 12 | For more information, visit the [wiki](https://github.com/CrazedProgrammer/wave/wiki). 13 | -------------------------------------------------------------------------------- /wave.lua: -------------------------------------------------------------------------------- 1 | --[[ 2 | wave version 0.1.5 3 | 4 | The MIT License (MIT) 5 | Copyright (c) 2020 CrazedProgrammer 6 | 7 | Permission is hereby granted, free of charge, to any person obtaining a copy of this software and 8 | associated documentation files (the "Software"), to deal in the Software without restriction, 9 | including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, 10 | and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do 11 | so, subject to the following conditions: 12 | 13 | The above copyright notice and this permission notice shall be included in all copies or 14 | substantial portions of the Software. 15 | 16 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, 17 | INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A 18 | PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR 19 | COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN 20 | AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION 21 | WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 22 | ]] 23 | 24 | local wave = { } 25 | wave.version = "0.1.5" 26 | 27 | wave._oldSoundMap = {"harp", "bassattack", "bd", "snare", "hat"} 28 | wave._newSoundMap = {"harp", "bass", "basedrum", "snare", "hat"} 29 | wave._defaultThrottle = 99 30 | wave._defaultClipMode = 1 31 | wave._maxInterval = 1 32 | wave._isNewSystem = false 33 | if _HOST then 34 | wave._isNewSystem = _HOST:sub(15, #_HOST) >= "1.80" 35 | end 36 | 37 | wave.context = { } 38 | wave.output = { } 39 | wave.track = { } 40 | wave.instance = { } 41 | 42 | function wave.createContext(clock, volume) 43 | clock = clock or os.clock() 44 | volume = volume or 1.0 45 | 46 | local context = setmetatable({ }, {__index = wave.context}) 47 | context.outputs = { } 48 | context.instances = { } 49 | context.vs = {0, 0, 0, 0, 0} 50 | context.prevClock = clock 51 | context.volume = volume 52 | return context 53 | end 54 | 55 | function wave.context:addOutput(...) 56 | local output = wave.createOutput(...) 57 | self.outputs[#self.outputs + 1] = output 58 | return output 59 | end 60 | 61 | function wave.context:addOutputs(...) 62 | local outs = {...} 63 | if #outs == 1 then 64 | if not getmetatable(outs) then 65 | outs = outs[1] 66 | else 67 | if getmetatable(outs).__index ~= wave.outputs then 68 | outs = outs[1] 69 | end 70 | end 71 | end 72 | for i = 1, #outs do 73 | self:addOutput(outs[i]) 74 | end 75 | end 76 | 77 | function wave.context:removeOutput(out) 78 | if type(out) == "number" then 79 | table.remove(self.outputs, out) 80 | return 81 | elseif type(out) == "table" then 82 | if getmetatable(out).__index == wave.output then 83 | for i = 1, #self.outputs do 84 | if out == self.outputs[i] then 85 | table.remove(self.outputs, i) 86 | return 87 | end 88 | end 89 | return 90 | end 91 | end 92 | for i = 1, #self.outputs do 93 | if out == self.outputs[i].native then 94 | table.remove(self.outputs, i) 95 | return 96 | end 97 | end 98 | end 99 | 100 | function wave.context:addInstance(...) 101 | local instance = wave.createInstance(...) 102 | self.instances[#self.instances + 1] = instance 103 | return instance 104 | end 105 | 106 | function wave.context:removeInstance(instance) 107 | if type(instance) == "number" then 108 | table.remove(self.instances, instance) 109 | else 110 | for i = 1, #self.instances do 111 | if self.instances == instance then 112 | table.remove(self.instances, i) 113 | return 114 | end 115 | end 116 | end 117 | end 118 | 119 | function wave.context:playNote(note, pitch, volume) 120 | volume = volume or 1.0 121 | 122 | self.vs[note] = self.vs[note] + volume 123 | for i = 1, #self.outputs do 124 | self.outputs[i]:playNote(note, pitch, volume * self.volume) 125 | end 126 | end 127 | 128 | function wave.context:update(interval) 129 | local clock = os.clock() 130 | interval = interval or (clock - self.prevClock) 131 | 132 | self.prevClock = clock 133 | if interval > wave._maxInterval then 134 | interval = wave._maxInterval 135 | end 136 | for i = 1, #self.outputs do 137 | self.outputs[i].notes = 0 138 | end 139 | for i = 1, 5 do 140 | self.vs[i] = 0 141 | end 142 | if interval > 0 then 143 | for i = 1, #self.instances do 144 | local notes = self.instances[i]:update(interval) 145 | for j = 1, #notes / 3 do 146 | self:playNote(notes[j * 3 - 2], notes[j * 3 - 1], notes[j * 3]) 147 | end 148 | end 149 | end 150 | end 151 | 152 | 153 | 154 | function wave.createOutput(out, volume, filter, throttle, clipMode) 155 | volume = volume or 1.0 156 | filter = filter or {true, true, true, true, true} 157 | throttle = throttle or wave._defaultThrottle 158 | clipMode = clipMode or wave._defaultClipMode 159 | 160 | local output = setmetatable({ }, {__index = wave.output}) 161 | output.native = out 162 | output.volume = volume 163 | output.filter = filter 164 | output.notes = 0 165 | output.throttle = throttle 166 | output.clipMode = clipMode 167 | if type(out) == "function" then 168 | output.nativePlayNote = out 169 | output.type = "custom" 170 | return output 171 | elseif type(out) == "string" then 172 | if peripheral.getType(out) == "iron_noteblock" then 173 | if wave._isNewSystem then 174 | local nb = peripheral.wrap(out) 175 | output.type = "iron_noteblock" 176 | function output.nativePlayNote(note, pitch, volume) 177 | if output.volume * volume > 0 then 178 | nb.playSound("minecraft:block.note."..wave._newSoundMap[note], volume, math.pow(2, (pitch - 12) / 12)) 179 | end 180 | end 181 | return output 182 | end 183 | elseif peripheral.getType(out) == "speaker" then 184 | if wave._isNewSystem then 185 | local nb = peripheral.wrap(out) 186 | output.type = "speaker" 187 | function output.nativePlayNote(note, pitch, volume) 188 | if output.volume * volume > 0 then 189 | nb.playNote(wave._newSoundMap[note], volume, pitch) 190 | end 191 | end 192 | return output 193 | end 194 | end 195 | elseif type(out) == "table" then 196 | if out.execAsync then 197 | output.type = "commands" 198 | if wave._isNewSystem then 199 | function output.nativePlayNote(note, pitch, volume) 200 | out.execAsync("playsound minecraft:block.note."..wave._newSoundMap[note].." record @a ~ ~ ~ "..tostring(volume).." "..tostring(math.pow(2, (pitch - 12) / 12))) 201 | end 202 | else 203 | function output.nativePlayNote(note, pitch, volume) 204 | out.execAsync("playsound note."..wave._oldSoundMap[note].." @a ~ ~ ~ "..tostring(volume).." "..tostring(math.pow(2, (pitch - 12) / 12))) 205 | end 206 | end 207 | return output 208 | elseif getmetatable(out) then 209 | if getmetatable(out).__index == wave.output then 210 | return out 211 | end 212 | end 213 | end 214 | end 215 | 216 | function wave.scanOutputs() 217 | local outs = { } 218 | if commands then 219 | outs[#outs + 1] = wave.createOutput(commands) 220 | end 221 | local sides = peripheral.getNames() 222 | for i = 1, #sides do 223 | if peripheral.getType(sides[i]) == "iron_noteblock" then 224 | outs[#outs + 1] = wave.createOutput(sides[i]) 225 | elseif peripheral.getType(sides[i]) == "speaker" then 226 | outs[#outs + 1] = wave.createOutput(sides[i]) 227 | end 228 | end 229 | return outs 230 | end 231 | 232 | function wave.output:playNote(note, pitch, volume) 233 | volume = volume or 1.0 234 | 235 | if self.clipMode == 1 then 236 | if pitch < 0 then 237 | pitch = 0 238 | elseif pitch > 24 then 239 | pitch = 24 240 | end 241 | elseif self.clipMode == 2 then 242 | if pitch < 0 then 243 | while pitch < 0 do 244 | pitch = pitch + 12 245 | end 246 | elseif pitch > 24 then 247 | while pitch > 24 do 248 | pitch = pitch - 12 249 | end 250 | end 251 | end 252 | if self.filter[note] and self.notes < self.throttle then 253 | self.nativePlayNote(note, pitch, volume * self.volume) 254 | self.notes = self.notes + 1 255 | end 256 | end 257 | 258 | 259 | 260 | function wave.loadTrack(path) 261 | local track = setmetatable({ }, {__index = wave.track}) 262 | local handle = fs.open(path, "rb") 263 | if not handle then return end 264 | 265 | local function readInt(size) 266 | local num = 0 267 | for i = 0, size - 1 do 268 | local byte = handle.read() 269 | if not byte then -- dont leave open file handles no matter what 270 | handle.close() 271 | return 272 | end 273 | num = num + byte * (256 ^ i) 274 | end 275 | return num 276 | end 277 | local function readStr() 278 | local length = readInt(4) 279 | if not length then return end 280 | local data = { } 281 | for i = 1, length do 282 | data[i] = string.char(handle.read()) 283 | end 284 | return table.concat(data) 285 | end 286 | 287 | -- Part #1: Metadata 288 | track.length = readInt(2) -- song length (ticks) 289 | track.height = readInt(2) -- song height 290 | track.name = readStr() -- song name 291 | track.author = readStr() -- song author 292 | track.originalAuthor = readStr() -- original song author 293 | track.description = readStr() -- song description 294 | track.tempo = readInt(2) / 100 -- tempo (ticks per second) 295 | track.autoSaving = readInt(1) == 0 and true or false -- auto-saving 296 | track.autoSavingDuration = readInt(1) -- auto-saving duration 297 | track.timeSignature = readInt(1) -- time signature (3 = 3/4) 298 | track.minutesSpent = readInt(4) -- minutes spent 299 | track.leftClicks = readInt(4) -- left clicks 300 | track.rightClicks = readInt(4) -- right clicks 301 | track.blocksAdded = readInt(4) -- blocks added 302 | track.blocksRemoved = readInt(4) -- blocks removed 303 | track.schematicFileName = readStr() -- midi/schematic file name 304 | 305 | -- Part #2: Notes 306 | track.layers = { } 307 | for i = 1, track.height do 308 | track.layers[i] = {name = "Layer "..i, volume = 1.0} 309 | track.layers[i].notes = { } 310 | end 311 | 312 | local tick = 0 313 | while true do 314 | local tickJumps = readInt(2) 315 | if tickJumps == 0 then break end 316 | tick = tick + tickJumps 317 | local layer = 0 318 | while true do 319 | local layerJumps = readInt(2) 320 | if layerJumps == 0 then break end 321 | layer = layer + layerJumps 322 | if layer > track.height then -- nbs can be buggy 323 | for i = track.height + 1, layer do 324 | track.layers[i] = {name = "Layer "..i, volume = 1.0} 325 | track.layers[i].notes = { } 326 | end 327 | track.height = layer 328 | end 329 | local instrument = readInt(1) 330 | local key = readInt(1) 331 | if instrument <= 4 then -- nbs can be buggy 332 | track.layers[layer].notes[tick * 2 - 1] = instrument + 1 333 | track.layers[layer].notes[tick * 2] = key - 33 334 | end 335 | end 336 | end 337 | 338 | -- Part #3: Layers 339 | for i = 1, track.height do 340 | local name = readStr() 341 | if not name then break end -- if layer data doesnt exist, abort 342 | track.layers[i].name = name 343 | track.layers[i].volume = readInt(1) / 100 344 | end 345 | 346 | handle.close() 347 | return track 348 | end 349 | 350 | 351 | 352 | function wave.createInstance(track, volume, playing, loop) 353 | volume = volume or 1.0 354 | playing = (playing == nil) or playing 355 | loop = (loop ~= nil) and loop 356 | 357 | if getmetatable(track).__index == wave.instance then 358 | return track 359 | end 360 | local instance = setmetatable({ }, {__index = wave.instance}) 361 | instance.track = track 362 | instance.volume = volume or 1.0 363 | instance.playing = playing 364 | instance.loop = loop 365 | instance.tick = 1 366 | return instance 367 | end 368 | 369 | function wave.instance:update(interval) 370 | local notes = { } 371 | if self.playing then 372 | local dticks = interval * self.track.tempo 373 | local starttick = self.tick 374 | local endtick = starttick + dticks 375 | local istarttick = math.ceil(starttick) 376 | local iendtick = math.ceil(endtick) - 1 377 | for i = istarttick, iendtick do 378 | for j = 1, self.track.height do 379 | if self.track.layers[j].notes[i * 2 - 1] then 380 | notes[#notes + 1] = self.track.layers[j].notes[i * 2 - 1] 381 | notes[#notes + 1] = self.track.layers[j].notes[i * 2] 382 | notes[#notes + 1] = self.track.layers[j].volume 383 | end 384 | end 385 | end 386 | self.tick = self.tick + dticks 387 | 388 | if endtick > self.track.length then 389 | self.tick = 1 390 | self.playing = self.loop 391 | end 392 | end 393 | return notes 394 | end 395 | 396 | 397 | 398 | return wave 399 | --------------------------------------------------------------------------------