mirror of
https://github.com/20kdc/OC-KittenOS.git
synced 2024-11-23 19:08:05 +11:00
350 lines
9.3 KiB
Lua
350 lines
9.3 KiB
Lua
-- 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
|