-- This is released into the public domain. -- No warranty is provided, implied or otherwise. -- app-nbcompose.lua : Music! -- Authors: 20kdc local nb = neo.requireAccess("c.iron_noteblock", "noteblocks").list()() local ic = neo.requireAccess("x.neo.pub.base", "fs") local event = require("event")(neo) local neoux = require("neoux")(event, neo) local iTranslation = { [0] = 0, -- piano / air (def) 4, -- double bass / wood (def) 1, -- bass drum / stone (def) 2, -- snare drum / sand (def) 3, -- click / glass (def) -- JUST GIVE UP 4, -- guitar / wool 5, -- flute / clay 6, -- bell / gold 6, -- chime / pice 6, -- xylo / bone } local instKey = { [2] = 0, [3] = 1, [4] = 2, [5] = 3, [6] = 4, [144] = 5, [7] = 5, [8] = 6, [9] = 7, [10] = 8, [11] = 9 } local noteKey = "1q2w3er5t6yu8i9o0pzsxdcvg" -- Application State local fileData local uptime = os.uptime() local songPosition = 0 local selectionL, selectionR = -8, -9 local running = true local playing = false local timerExistsFlag = false local window local defInst = 0 -- local tick -- Tick function for timer making local file = require("knbs").new() -- Window width is always 50. Height is layers + 3, for the top bar. local theStatusBar, theNotePane, genMain local function updateStatusAndPane() if theStatusBar.update then theStatusBar.update(window) end if theNotePane then for _, v in ipairs(theNotePane) do v.update(window) end end end local function commonKey(a, c, f) if a == 32 then playing = not playing theStatusBar.update(window) if playing then if not timerExistsFlag then uptime = os.uptime() event.runAt(uptime, tick) timerExistsFlag = true end end elseif a == 91 then selectionL = songPosition updateStatusAndPane() elseif a == 93 then selectionR = songPosition updateStatusAndPane() elseif c == 203 and (f.shift or f.rshift) then songPosition = 0 updateStatusAndPane() elseif c == 205 and (f.shift or f.rshift) then songPosition = file.length updateStatusAndPane() elseif c == 203 then songPosition = math.max(0, songPosition - 1) updateStatusAndPane() elseif c == 205 then songPosition = songPosition + 1 updateStatusAndPane() end end theStatusBar = { x = 1, y = 3, w = 50, h = 1, selectable = true, line = function (window, x, y, lined, bg, fg, selected) if selected then bg, fg = fg, bg end window.span(x, y, ((playing and "Playing") or "Paused") .. " (SPACE) ; " .. (songPosition + 1) .. "/" .. file.length .. " ([Shift-]←/→)", bg, fg) end, key = function (window, update, a, c, d, f) if not d then return end commonKey(a, c, f) end } local function genLayers() theStatusBar.update = nil theNotePane = nil local layers = {} for i = 1, file.height do local layer = i - 1 table.insert(layers, neoux.tcfield(1, i + 1, 40, function (tx) file.layers[layer][1] = tx or file.layers[layer][1] return file.layers[layer][1] end)) table.insert(layers, neoux.tcrawview(42, i + 1, {"Vol."})) table.insert(layers, neoux.tcfield(46, i + 1, 5, function (tx) if tx then file.layers[layer][2] = math.max(0, math.min(255, math.floor(tonumber(tx) or 0))) end return tostring(file.layers[layer][2]) end)) end return 50, file.height + 1, nil, neoux.tcwindow(50, file.height + 1, { neoux.tcbutton(1, 1, "Purge Extra Layers", function (w) local knbs = require("knbs") local layerCount = knbs.correctSongLH(file) knbs.resizeLayers(file, layerCount) w.reset(genLayers()) end), neoux.tcbutton(21, 1, "Del.Last", function (w) require("knbs").resizeLayers(file, file.height - 1) w.reset(genLayers()) end), neoux.tcbutton(31, 1, "Append", function (w) require("knbs").resizeLayers(file, file.height + 1) w.reset(genLayers()) end), table.unpack(layers) }, function (w) w.reset(genMain()) end, 0xFFFFFF, 0) end function genMain() theNotePane = {} for l = 1, file.height do local layer = l - 1 theNotePane[l] = { x = 1, y = 3 + l, w = 50, h = 1, selectable = true, line = function (window, x, y, lined, bg, fg, selected) if selected then bg, fg = fg, bg end local text = "" for i = 1, 5 do local noteL, noteR = " ", " " local tick = songPosition + i - 3 if songPosition == tick then noteL = "[" noteR = "]" end if selectionR >= selectionL then if selectionL == tick then noteL = "{" end if selectionR == tick then noteR = "}" end end text = text .. noteL local fd = file.ticks[tick] fd = fd and fd[layer] if fd then text = text .. string.format(" %02i/%02i", fd[1], fd[2]) else text = text .. " " end text = text .. noteR end window.span(x, y, text, bg, fg) end, key = function (window, update, a, c, d, f) if not d then return end commonKey(a, c, f) if a == 8 then if file.ticks[songPosition] then file.ticks[songPosition][layer] = nil require("knbs").correctSongLH(file) update() theStatusBar.update(window) end elseif instKey[c] and (f.shift or f.rshift) then file.ticks[songPosition] = file.ticks[songPosition] or {} defInst = instKey[c] local nt = 45 if file.ticks[songPosition][layer] then file.ticks[songPosition][layer][1] = defInst nt = file.ticks[songPosition][layer][2] end if nb then nb.playNote(iTranslation[defInst] or 0, nt - 33, file.layers[layer][2] / 100) end require("knbs").correctSongLH(file) update() theStatusBar.update(window) elseif a >= 0 and a < 256 and noteKey:find(string.char(a), 1, true) then file.ticks[songPosition] = file.ticks[songPosition] or {} local note = noteKey:find(string.char(a), 1, true) - 1 file.ticks[songPosition][layer] = {defInst, note + 33} if nb then nb.playNote(iTranslation[defInst] or 0, note, file.layers[layer][2] / 100) end require("knbs").correctSongLH(file) update() theStatusBar.update(window) elseif a == 123 then if selectionR >= selectionL then local storage = {} for i = selectionL, selectionR do storage[i] = file.ticks[i] and file.ticks[i][layer] and {table.unpack(file.ticks[i][layer])} end for i = selectionL, selectionR do local p = songPosition + (i - selectionL) file.ticks[p] = file.ticks[p] or {} file.ticks[p][layer] = storage[i] end require("knbs").correctSongLH(file) update() theStatusBar.update(window) end end end } end -- We totally lie about the height here to tcwindow. "Bit of a cheat, but who's counting", anyone? -- It is explicitly documented that the width and height are for background drawing, BTW. return 50, file.height + 3, nil, neoux.tcwindow(50, 3, { neoux.tcfield(1, 1, 20, function (tx) file.name = tx or file.name return file.name end), neoux.tcfield(21, 1, 15, function (tx) file.transcriptor = tx or file.transcriptor return file.transcriptor end), neoux.tcfield(36, 1, 15, function (tx) file.songwriter = tx or file.songwriter return file.songwriter end), neoux.tcbutton(1, 2, "New", function (w) file = require("knbs").new() songPosition = 0 playing = false window.reset(genMain()) end), neoux.tcbutton(6, 2, "Load", function (w) neoux.fileDialog(false, function (f) if not f then return end file = nil file = require("knbs").deserialize(f.read("*a")) f.close() songPosition = 0 playing = false window.reset(genMain()) end) end), neoux.tcbutton(12, 2, "Save", function (w) neoux.fileDialog(true, function (f) if not f then return end require("knbs").serialize(file, f.write) f.close() end) end), neoux.tcbutton(18, 2, "Ds.L", function (w) neoux.fileDialog(false, function (f) if not f then return end file.description = f.read("*a") f.close() end) end), neoux.tcbutton(24, 2, "Ds.S", function (w) neoux.fileDialog(true, function (f) if not f then return end f.write(file.description) f.close() end) end), neoux.tcbutton(30, 2, "Layers", function (w) window.reset(genLayers()) end), neoux.tcrawview(39, 2, {"cT/S"}), neoux.tcfield(43, 2, 8, function (tx) if tx then local txn = tonumber(tx) or 0 file.tempo = math.min(math.max(0, math.floor(txn)), 65535) end return tostring(file.tempo) end), theStatusBar, table.unpack(theNotePane) }, function (w) w.close() running = false end, 0xFFFFFF, 0) end function tick() if playing then -- Stop the user from entering such a low tempo that stuff freezes by: -- 1. Stopping tempo from going too low to cause /0 -- 2. Ensuring timer is at most 1 second away local temp = 1 / math.max(file.tempo / 100, 0.01) if os.uptime() >= uptime + temp then -- execute at this song position if file.ticks[songPosition] and nb then for i = 0, file.height - 1 do local tck = file.ticks[songPosition][i] if tck then nb.playNote(iTranslation[tck[1]] or 0, tck[2] - 33, file.layers[i][2] / 100) end end end songPosition = songPosition + 1 if songPosition >= file.length then songPosition = 0 end updateStatusAndPane() uptime = uptime + temp end event.runAt(math.min(os.uptime() + 1, uptime + temp), tick) else timerExistsFlag = false end end window = neoux.create(genMain()) while running do event.pull() end