1
0
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:
20kdc 2018-06-08 19:43:19 +01:00
parent 46d60df1ec
commit 4c12bb548a
5 changed files with 587 additions and 0 deletions

View 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

View File

@ -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'",

View File

@ -0,0 +1,2 @@
repository/apps/app-nbcompose.lua: 20kdc, Public Domain

View File

@ -0,0 +1,2 @@
repository/libs/knbs.lua: 20kdc, Public Domain

203
repository/libs/knbs.lua Normal file
View 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
}