mirror of
https://github.com/20kdc/OC-KittenOS.git
synced 2024-11-23 10:58:06 +11:00
Add app-nbcompose and the library for it "knbs".
This commit is contained in:
parent
46d60df1ec
commit
4c12bb548a
345
repository/apps/app-nbcompose.lua
Normal file
345
repository/apps/app-nbcompose.lua
Normal file
@ -0,0 +1,345 @@
|
||||
-- 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
|
||||
nb.playNote(iTranslation[defInst] or 0, nt - 33, file.layers[layer][2] / 100)
|
||||
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}
|
||||
nb.playNote(iTranslation[defInst] or 0, note, file.layers[layer][2] / 100)
|
||||
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, {"qT/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 * 4)), 65535)
|
||||
end
|
||||
return tostring(math.floor(file.tempo / 25))
|
||||
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] 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
|
@ -164,6 +164,24 @@ return {
|
||||
"docs/repoauthors/app-rsctrl"
|
||||
},
|
||||
},
|
||||
["app-nbcompose"] = {
|
||||
desc = "Music player/composer using the NBS format",
|
||||
v = 0,
|
||||
deps = {
|
||||
"neo",
|
||||
"lib-knbs",
|
||||
"zzz-license-pd"
|
||||
},
|
||||
dirs = {
|
||||
"apps",
|
||||
"docs",
|
||||
"docs/repoauthors"
|
||||
},
|
||||
files = {
|
||||
"apps/app-nbcompose.lua",
|
||||
"docs/repoauthors/app-nbcompose"
|
||||
},
|
||||
},
|
||||
["app-launchbar"] = {
|
||||
desc = "Application launcher bar",
|
||||
v = 0,
|
||||
@ -198,6 +216,23 @@ return {
|
||||
"docs/repoauthors/app-slaunch"
|
||||
},
|
||||
},
|
||||
-- libraries
|
||||
["lib-knbs"] = {
|
||||
desc = "NBS reader/writer library",
|
||||
v = 0,
|
||||
deps = {
|
||||
"zzz-license-pd"
|
||||
},
|
||||
dirs = {
|
||||
"libs",
|
||||
"docs",
|
||||
"docs/repoauthors"
|
||||
},
|
||||
files = {
|
||||
"libs/knbs.lua",
|
||||
"docs/repoauthors/lib-knbs"
|
||||
},
|
||||
},
|
||||
-- licenses (MUST BE IMMUTABLE)
|
||||
["zzz-license-pd"] = {
|
||||
desc = "license file 'Public Domain'",
|
||||
|
2
repository/docs/repoauthors/app-nbcompose
Normal file
2
repository/docs/repoauthors/app-nbcompose
Normal file
@ -0,0 +1,2 @@
|
||||
repository/apps/app-nbcompose.lua: 20kdc, Public Domain
|
||||
|
2
repository/docs/repoauthors/lib-knbs
Normal file
2
repository/docs/repoauthors/lib-knbs
Normal file
@ -0,0 +1,2 @@
|
||||
repository/libs/knbs.lua: 20kdc, Public Domain
|
||||
|
203
repository/libs/knbs.lua
Normal file
203
repository/libs/knbs.lua
Normal file
@ -0,0 +1,203 @@
|
||||
-- This is released into the public domain.
|
||||
-- No warranty is provided, implied or otherwise.
|
||||
|
||||
-- knbs.lua : Partial .nbs (Note Block Studio) R/W library
|
||||
-- Does not support custom instruments!
|
||||
-- Authors: 20kdc
|
||||
|
||||
local function dsu16(str)
|
||||
return
|
||||
str:byte(1) +
|
||||
(str:byte(2) * 256),
|
||||
str:sub(3)
|
||||
end
|
||||
local function dsu32(str)
|
||||
local a, str = dsu16(str)
|
||||
local b, str = dsu16(str)
|
||||
return a + (b * 0x10000), str
|
||||
end
|
||||
local function dsstr(str)
|
||||
local a, str = dsu32(str)
|
||||
return str:sub(1, a), str:sub(a + 1)
|
||||
end
|
||||
local function su16(i, wr)
|
||||
wr(string.char(i % 0x100, math.floor(i / 0x100)))
|
||||
end
|
||||
local function su32(i, wr)
|
||||
su16(i % 0x10000, wr)
|
||||
su16(math.floor(i / 0x10000), wr)
|
||||
end
|
||||
local function sstr(str, wr)
|
||||
su32(#str, wr)
|
||||
wr(str)
|
||||
end
|
||||
|
||||
return {
|
||||
new = function ()
|
||||
return {
|
||||
length = 1,
|
||||
height = 1,
|
||||
name = "New Song",
|
||||
transcriptor = "Mr.Anderson",
|
||||
songwriter = "Morpheus",
|
||||
description = "A blank song.",
|
||||
tempo = 200,
|
||||
autosave = 0,
|
||||
autosaveMin = 60,
|
||||
timeSignature = 4,
|
||||
usageMin = 0, usageLeft = 0, usageRight = 0, usageAdd = 0, usageRm = 0,
|
||||
importName = "",
|
||||
ci = "",
|
||||
ticks = {
|
||||
[0] = {
|
||||
[0] = {0, 33}
|
||||
}
|
||||
},
|
||||
layers = {
|
||||
[0] = {"L0", 100}
|
||||
}
|
||||
}
|
||||
end,
|
||||
deserialize = function (str)
|
||||
local nbs = {}
|
||||
nbs.length, str = dsu16(str)
|
||||
nbs.length = nbs.length + 1 -- hmph!
|
||||
nbs.height, str = dsu16(str)
|
||||
nbs.name, str = dsstr(str)
|
||||
nbs.transcriptor, str = dsstr(str)
|
||||
nbs.songwriter, str = dsstr(str)
|
||||
nbs.description, str = dsstr(str)
|
||||
nbs.tempo, str = dsu16(str)
|
||||
nbs.autosave, str = str:byte(), str:sub(2)
|
||||
nbs.autosaveMin, str = str:byte(), str:sub(2)
|
||||
nbs.timeSignature, str = str:byte(), str:sub(2)
|
||||
nbs.usageMin, str = dsu32(str)
|
||||
nbs.usageLeft, str = dsu32(str)
|
||||
nbs.usageRight, str = dsu32(str)
|
||||
nbs.usageAdd, str = dsu32(str)
|
||||
nbs.usageRm, str = dsu32(str)
|
||||
nbs.importName, str = dsstr(str)
|
||||
-- ticks[tick][layer] = key
|
||||
nbs.ticks = {}
|
||||
local tick = -1
|
||||
while true do
|
||||
local ntJ
|
||||
ntJ, str = dsu16(str)
|
||||
if ntJ == 0 then break end
|
||||
tick = tick + ntJ
|
||||
local tickData = {}
|
||||
nbs.ticks[tick] = tickData
|
||||
local layer = -1
|
||||
while true do
|
||||
local lJ
|
||||
lJ, str = dsu16(str)
|
||||
if lJ == 0 then break end
|
||||
layer = layer + lJ
|
||||
local ins = str:byte(1)
|
||||
local key = str:byte(2)
|
||||
str = str:sub(3)
|
||||
local layerData = {ins, key}
|
||||
if layer < nbs.height then
|
||||
tickData[layer] = layerData
|
||||
-- else: drop the invalid note
|
||||
end
|
||||
end
|
||||
end
|
||||
-- nbs.layers[layer] = {name, volume}
|
||||
nbs.layers = {}
|
||||
if str ~= "" then
|
||||
for i = 0, nbs.height - 1 do
|
||||
nbs.layers[i] = {}
|
||||
nbs.layers[i][1], str = dsstr(str)
|
||||
nbs.layers[i][2], str = str:byte(), str:sub(2)
|
||||
end
|
||||
else
|
||||
for i = 0, nbs.height - 1 do
|
||||
nbs.layers[i] = {"L" .. i, 100}
|
||||
end
|
||||
end
|
||||
nbs.ci = str
|
||||
return nbs
|
||||
end,
|
||||
resizeLayers = function (nbs, layers)
|
||||
-- make all layers after target layer go away
|
||||
for i = layers, nbs.height - 1 do
|
||||
nbs.layers[i] = nil
|
||||
end
|
||||
-- add layers up to target
|
||||
for i = nbs.height - 1, layers - 1 do
|
||||
nbs.layers[i] = {"L" .. i, 100}
|
||||
end
|
||||
-- clean up song
|
||||
for k, v in pairs(nbs.ticks) do
|
||||
for lk, lv in pairs(v) do
|
||||
if lk >= layers then
|
||||
v[lk] = nil
|
||||
end
|
||||
end
|
||||
end
|
||||
nbs.height = layers
|
||||
end,
|
||||
-- Corrects length, height (should not be necessary in correct applications!), and clears out unused tick columns.
|
||||
-- Returns the actual effective height, which can be passed to resizeLayers to remove dead weight.
|
||||
correctSongLH = function (nbs)
|
||||
nbs.length = 1
|
||||
nbs.height = 0
|
||||
for k, v in pairs(nbs.layers) do
|
||||
nbs.height = math.max(nbs.height, k + 1)
|
||||
end
|
||||
local eH = 0
|
||||
for k, v in pairs(nbs.ticks) do
|
||||
local ok = false
|
||||
for lk, lv in pairs(v) do
|
||||
ok = true
|
||||
eH = math.max(eH, lk + 1)
|
||||
end
|
||||
if not ok then
|
||||
nbs.ticks[k] = nil
|
||||
else
|
||||
nbs.length = math.max(nbs.length, k + 1)
|
||||
end
|
||||
end
|
||||
return eH
|
||||
end,
|
||||
serialize = function (nbs, wr)
|
||||
su16(math.max(0, nbs.length - 1), wr)
|
||||
su16(nbs.height, wr)
|
||||
sstr(nbs.name, wr)
|
||||
sstr(nbs.transcriptor, wr)
|
||||
sstr(nbs.songwriter, wr)
|
||||
sstr(nbs.description, wr)
|
||||
su16(nbs.tempo, wr)
|
||||
wr(string.char(nbs.autosave, nbs.autosaveMin, nbs.timeSignature))
|
||||
su32(nbs.usageMin, wr)
|
||||
su32(nbs.usageLeft, wr)
|
||||
su32(nbs.usageRight, wr)
|
||||
su32(nbs.usageAdd, wr)
|
||||
su32(nbs.usageRm, wr)
|
||||
sstr(nbs.importName, wr)
|
||||
local ptr = -1
|
||||
for i = 0, nbs.length - 1 do
|
||||
if nbs.ticks[i] then
|
||||
su16(i - ptr, wr)
|
||||
ptr = i
|
||||
local lp = -1
|
||||
for j = 0, nbs.height - 1 do
|
||||
local id = nbs.ticks[i][j]
|
||||
if id then
|
||||
su16(j - lp, wr)
|
||||
lp = j
|
||||
wr(string.char(id[1], id[2]))
|
||||
end
|
||||
end
|
||||
su16(0, wr)
|
||||
end
|
||||
end
|
||||
su16(0, wr)
|
||||
for i = 0, nbs.height - 1 do
|
||||
sstr(nbs.layers[i][1], wr)
|
||||
wr(string.char(nbs.layers[i][2]))
|
||||
end
|
||||
wr(nbs.ci)
|
||||
end
|
||||
}
|
Loading…
Reference in New Issue
Block a user