diff --git a/nbs5.lua b/nbs5.lua new file mode 100644 index 0000000..2307fff --- /dev/null +++ b/nbs5.lua @@ -0,0 +1,303 @@ +--[[ +NoteBlock Song API +by MysticT +Twekaed by Soni +Modified for NBS v5 by LDDestroier (WIP) +--]] + +local nbs5 = {} + +-- yield to avoid error +local function yield() + os.queueEvent("yield") + os.pullEvent("yield") +end + +-- read short integer (16-bit) from file +local function readShort(file) + return file.read() + file.read() * 256 +end +_G.readShort = readShort + + +-- read integer (32-bit) from file +local function readInt(file) + return file.read() + file.read() * 256 + file.read() * 65536 + file.read() * 16777216 +end + +-- read string from file +local function readString(file) + local s, c = "" + local len = readInt(file) + for i = 1, len do + c = file.read() + if not c then + break + end + s = s .. string.char(c) + end + return s +end + +local function makeBitNum(num, bytes) + local output = "" + for i = 0, bytes - 1 do + output = output .. string.char(bit32.band(bit32.rshift(num, i * 8), 0xFF)) + end + return output +end + +-- write short integer (16-bit) to file +local function writeShort(file, int) + file.write(makeBitNum(int, 2)) +end + +-- write integer (32-bit) to file +local function writeInt(file, int) + file.write(makeBitNum(int, 4)) +end + +-- write string to file +local function writeString(file, str) + writeInt(file, #str) + file.write(str) +end + +-- read nbs file header +local function readNBSHeader(file) + local header = {} + header.lenght = readShort(file) + if header.lenght > 0 then + -- old NBS + header.nbs_version = 0 + header.height = readShort(file) + header.name = readString(file) + if header.name == "" then + header.name = "Untitled" + end + header.author = readString(file) + if header.author == "" then + header.author = "Unknown" + end + header.original_author = readString(file) + if header.original_author == "" then + header.original_author = "Unknown" + end + header.description = readString(file) + header.tempo = readShort(file) / 100 + header.autosave = file.read() + header.autosave_duration = file.read() + header.time_signature = file.read() + header.minutes_spent = readInt(file) + header.left_clicks = readInt(file) + header.right_clicks = readInt(file) + header.blocks_added = readInt(file) + header.blocks_removed = readInt(file) + header.filename = readString(file) + + else + -- NBS v5 + header.nbs_version = file.read() + header.vanilla_instruments = file.read() + header.lenght = readShort(file) + header.height = readShort(file) + header.name = readString(file) + if header.name == "" then + header.name = "Untitled" + end + header.author = readString(file) + if header.author == "" then + header.author = "Unknown" + end + header.original_author = readString(file) + if header.original_author == "" then + header.original_author = "Unknown" + end + header.description = readString(file) + header.tempo = readShort(file) / 100 + header.autosave = file.read() + header.autosave_duration = file.read() + header.time_signature = file.read() + header.minutes_spent = readInt(file) + header.left_clicks = readInt(file) + header.right_clicks = readInt(file) + header.blocks_added = readInt(file) + header.blocks_removed = readInt(file) + header.filename = readString(file) + header.looping = file.read() + header.max_looping = file.read() + header.loop_start = readShort(file) + + end + + return header +end + +-- jump to the next tick in the file +local function nextTick(file, tSong) + local jump = readShort(file) + for i = 1, jump - 1 do + tSong[#tSong + 1] = {} + end + return jump > 0 +end + +-- read the notes in a tick +-- TODO cleanup, move checks to player:nbs.lua +local function readTick(file) + local t = {} + local n = 0 + local jump = readShort(file) + + while jump > 0 do + n = n + jump + write(".") + local instrument = file.read() + 1 + if instrument > 16 then + return nil, "(v5) Can't convert custom instruments" + end + + local note = file.read() - 33 + local velocity = file.read() + local pan = file.read() + local pitch_mod = readShort(file) + + if note < 0 or note > 24 then + return nil, "(v5) Notes must be in Minecraft's 2 octaves (" .. note .. ")" + end + + if not t[instrument] then + t[instrument] = {} + end + + t[instrument][n] = note + jump = readShort(file) + end + + return t +end + +-- API functions + +-- save a converted song to a file +function nbs5.saveSong(tSong, sPath) + local file = fs.open(sPath, "w") + if file then + file.write(textutils.serialize(tSong)) + file.close() + return true + end + + return false, "Error opening file "..sPath +end + +-- save a song as an NBS v5 file +function nbs5.saveSongNBS(tSong, sPath) + +end + +-- load and convert an .nbs file and save it +function nbs5.load(sPath, bVerbose) + local file = fs.open(sPath, "rb") + if file then + if bVerbose then + print("Reading header...") + end + + local tSong = {} + local header = readNBSHeader(file) + tSong.header = header + tSong.name = header.name + tSong.author = header.author + tSong.original_author = header.original_author + tSong.lenght = header.lenght / header.tempo + tSong.delay = 1 / header.tempo + + if bVerbose then + print("Reading ticks...") + end + + if header.nbs_version == 0 then + + while nextTick(file, tSong) do + local tick, err = readTick(file, tSong) + if tick then + table.insert(tSong, tick) + else + file.close() + return nil, err + end + yield() + end + + pcall(function() + local layers = {} + for i=1, header.height do + table.insert(layers, { + name=readString(file), + volume=file.read() + 0 + }) + end + tSong.layers = layers + + local insts = {} + for i=1, file.read() + 0 do + insts[#insts + 1] = { + name = readString(file), + file = readString(file), + pitch = file.read() + 0, + key = (file.read() + 0) ~= 0 + } + end + tSong.instruments = insts + end) + + else + + while nextTick(file, tSong) do + local tick, err = readTick(file, tSong) + if tick then + table.insert(tSong, tick) + else + file.close() + return nil, err + end + yield() + end + + pcall(function() + local layers = {} + for i=1, header.height do + table.insert(layers, { + name = readString(file), + lock = file.read(), + volume = file.read(), + pan = file.read() + }) + end + tSong.layers = layers + + local insts = {} + for i=1, file.read() + 0 do + insts[#insts + 1] = { + name = readString(file), + file = readString(file), + pitch = file.read() + 0, + key = (file.read() + 0) ~= 0 + } + end + tSong.instruments = insts + end) + + end + + file.close() + + return tSong + end + + return nil, "Error opening file "..sPath + +end + +return nbs5