falco/drumsheet/drumsheet.lua

413 lines
9.2 KiB
Lua

VERSION = '2019-10-14'
print("DRUMSHEET " .. VERSION)
require "iuplua"
require "iupluacontrols"
require "midiwire"
-- now, this can actually play Blue monday!
-- see https://www.youtube.com/watch?v=Y_hk5P1X7sc&t=164s
LEN = 33
step = 1
mout = midiwire.open_out(0)
active_pat = 1
chan = 10
instruments = {
[1] = {"clap", 39},
[2] = {"open hat", 46},
[3] = {"snare", 40},
[4] = {"bass", 36},
}
patterns = {
{},
{},
{},
{}
}
-- GUI definition
pat_a = iup.button{title="PAT A", fontstyle="Bold"}
pat_b = iup.button{title="PAT B"}
pat_c = iup.button{title="PAT C"}
pat_d = iup.button{title="PAT D"}
pat_rec = iup.button{title="REC"}
pat_clean = iup.button{title="ZAP"}
pat_load = iup.button{title="load"}
pat_save = iup.button{title="save"}
matrix = iup.matrix{numcol=32, numlin=9, widthdef=10}
matrix.resizematrix = "YES"
function print_instruments()
for i=1,32 do
matrix:setcell(0,i,i)
end
for i=1,8 do
matrix:setcell(i,0,"")
end
for i=1,#instruments do
matrix:setcell(i,0,instruments[i][1])
end
end
matrix:setcell(0,0,"DRUMsheet")
print_instruments()
playbtn = iup.button{title="play"}
stopbtn = iup.button{title="stop"}
midi_setup = iup.button{title="MIDI output"}
-- TODO - maskovat tyhle věci a validovat ať to necrashuje
bpm = iup.text{spin="YES", value="120", spinmin=30, spinmax=240} -- todo: min max sensible
factor = iup.text{spin="YES", value="4", spinmin=1, spinmax=8}
len = iup.text{spin="YES", value="32", spinmin=8, spinmax=32}
dlg = iup.dialog{
iup.vbox{
iup.hbox{pat_a, pat_b, pat_c, pat_d, pat_rec, pat_clean, pat_load, pat_save; gap=5},
iup.hbox{matrix},
iup.hbox{playbtn, stopbtn, iup.label{title="tempo: "} , bpm, iup.label{title="/"}, factor, iup.label{title=" || len: "}, len, midi_setup; alignment="ACENTER", gap="5"}
}
; title = "DRUMsheet " .. VERSION, size = "560x170"
}
-- GUI actions
timer = iup.timer{time=500}
timer.run="NO"
function timer:action_cb()
-- ONE STEP of sequencer
if step==1 then
matrix:setcell(9, LEN-1, "")
else
matrix:setcell(9, step-1, "") -- clean from previous step
end
matrix:setcell(9, step, "^")
for i=1,#instruments do
if matrix:getcell(i, step)=="x" then
midiwire.note_on(mout, instruments[i][2], 100, chan)
end
if matrix:getcell(i, step)=="*" then
midiwire.note_on(mout, instruments[i][2], 127, chan)
end
end
step = ((step + 1) % LEN)
if step==0 then
step=1
end
matrix.redraw="YES"
end
function silence()
for i=1,#instruments do
midiwire.note_off(mout, instruments[i][2], chan)
end
end
function playbtn:action()
bpm.active="NO"
factor.active="NO"
len.active="NO"
timer.run="YES"
end
function stopbtn:action()
bpm.active="YES"
factor.active="YES"
len.active="YES"
timer.run="NO"
-- todo - tohle lépe
silence()
if step==1 then
matrix:setcell(9, LEN-1, "")
else
matrix:setcell(9, step-1, "") -- clean from previous step
end
matrix.redraw="YES"
step = 1
end
function adjust_time(bpm, factor)
timer.time = math.floor((1000*60) / (bpm*factor) )
end
function bpm:valuechanged_cb()
if tonumber(bpm.value) and tonumber(factor.value) then
adjust_time(tonumber(bpm.value), tonumber(factor.value))
end
end
function factor:valuechanged_cb()
if tonumber(bpm.value) and tonumber(factor.value) then
adjust_time(tonumber(bpm.value), tonumber(factor.value))
end
end
function adjust_len(len)
LEN = len + 1
for i=1,LEN do
matrix:setcell(0, i, i)
end
if LEN<33 then
for i=LEN,32 do
matrix:setcell(0, i, "")
end
end
end
function len:valuechanged_cb()
if tonumber(len.value) then
adjust_len(tonumber(len.value))
matrix.redraw="YES"
end
end
-- TODO - tohle přepsat na jedno okno
function midi_setup:action()
local devices , sel2id = {}, {}
for i=0,midiwire.device_count()-1 do
if midiwire.name(i) then
devices[#devices+1] = midiwire.name(i)
sel2id[#devices] = i
end
end
local selected = iup.ListDialog(1, "select MIDI output", #devices, devices, 1, 1, #devices)
if selected>-1 and sel2id[selected+1] then
midiwire.close_out(mout)
if sel2id[selected+1]==0 then
-- Microsoft GS Wavetable Synth
mout = midiwire.open_out(sel2id[selected+1])
chan = 10
else
mout = midiwire.open_out(sel2id[selected+1])
chan = iup.Scanf("Select port\nchannel number%2.2%d\n", 1)
end
end
end
-- easier editing of patterns
function matrix:action_cb(key, x, y, edition, value)
if key==iup.K_SP then
matrix:setcell(x, y, "")
matrix.redraw="YES"
return iup.IGNORE
elseif key==iup.K_X or key==iup.K_x then
matrix:setcell(x, y, "x")
matrix.redraw="YES"
return iup.IGNORE
end
end
-- patterns changing, saving and loading
function pat_rec:action()
patterns[active_pat] = {}
for x=1,8 do
patterns[active_pat][x] = {}
for y=1,32 do
patterns[active_pat][x][y] = matrix:getcell(x,y) or ""
end
-- print(table.concat(patterns[active_pat][x], ","))
end
end
function pat_clean:action()
for x=1,8 do
for y=1,32 do
matrix:setcell(x,y, "")
end
end
matrix.redraw = "YES"
end
function pat_recall(patno)
if not patterns[patno] then return end
local was_running = timer.run == "YES"
timer.run="NO" -- pause while recalling to not glitch
silence()
for x=1,8 do
patterns[patno][x] = patterns[patno][x] or {}
for y=1,32 do
matrix:setcell(x, y, patterns[patno][x][y] or "")
end
-- print(table.concat(patterns[active_pat][x], ","))
end
active_pat = patno
if was_running then
step = 1
timer.run="YES"
end
matrix.redraw="YES"
end
function pat_a:action()
pat_a.fontstyle="Bold"; pat_b.fontstyle=""; pat_c.fontstyle=""; pat_d.fontstyle=""
pat_recall(1)
end
function pat_b:action()
pat_a.fontstyle=""; pat_b.fontstyle="Bold"; pat_c.fontstyle=""; pat_d.fontstyle=""
pat_recall(2)
end
function pat_c:action()
pat_a.fontstyle=""; pat_b.fontstyle=""; pat_c.fontstyle="Bold"; pat_d.fontstyle=""
pat_recall(3)
end
function pat_d:action()
pat_a.fontstyle=""; pat_b.fontstyle=""; pat_c.fontstyle=""; pat_d.fontstyle="Bold"
pat_recall(4)
end
-- saving & loading
FILEFORMAT_VERSION = 1
function _pat_write(fh, patno)
fh:write("PAT,"..patno.."\n")
for i=1,#patterns[patno] do
fh:write(table.concat(patterns[patno][i],",") .. "\n")
end
end
function pat_save:action()
local fname, status = iup.GetFile("./*.csv")
if status > -1 then
local fh = io.open(fname, "w+")
fh:write("DRUMSHEET," .. FILEFORMAT_VERSION .. "," .. VERSION .. "\n")
fh:write("bpm,"..bpm.value.."\n")
fh:write("factor,"..factor.value.."\n")
fh:write("len,"..len.value.."\n")
fh:write("INSTRUMENTS\n")
for i=1,#instruments do
fh:write(instruments[i][1] .. "," .. instruments[i][2] .. "\n")
end
-- todo pat_write nebo loop
if #patterns[1]>1 then _pat_write(fh, 1) end
if #patterns[2]>1 then _pat_write(fh, 2) end
if #patterns[3]>1 then _pat_write(fh, 3) end
if #patterns[4]>1 then _pat_write(fh, 4) end
fh:close()
end
end
function explode(div,str) -- credit: http://richard.warburton.it
if (div=='') then return false end
local pos,arr = 0,{}
-- for each divider found
for st,sp in function() return string.find(str,div,pos,true) end do
table.insert(arr,string.sub(str,pos,st-1)) -- Attach chars left of current divider
pos = sp + 1 -- Jump past current divider
end
table.insert(arr,string.sub(str,pos)) -- Attach chars right of last divider
return arr
end
function sheet_read(fname)
local data = { instruments={}, patterns = {} }
local mode = ""
local is_header = {bpm=true, factor=true, len=true}
local i = 0
local patno = 0
for line in io.lines(fname) do
local fields = explode(',', line)
-- reads header:
if mode=="DRUMSHEET" and is_header[fields[1]] then
data[fields[1]] = tonumber(fields[2])
end
-- switch to PAT mode in INSTRUMENTS
if mode=="INSTRUMENTS" and fields[1]=="PAT" then
mode = "PAT"
end
-- reads instruments:
if mode=="INSTRUMENTS" and fields[1]~="PAT" then
i = i + 1
data.instruments[i] = {fields[1], tonumber(fields[2])}
end
-- read patterns:
if mode=="PAT" and fields[1]=="PAT" then
patno = tonumber(fields[2])
data.patterns[patno] = {}
i = 0
elseif mode=="PAT" then
i = i + 1
data.patterns[patno][i] = fields
end
if mode=="" and fields[1]=="DRUMSHEET" then
-- todo - kontrolovat verzi drumsheetu
if not fields[2] or not tonumber(fields[2]) then
return nil, "File version not found!"
end
if tonumber(fields[2])>FILEFORMAT_VERSION then
return nil, "File too new!"
end
mode = "DRUMSHEET"
end
if mode=="DRUMSHEET" and fields[1]=="INSTRUMENTS" then
mode = "INSTRUMENTS"
i = 0
end
end
return data
end
function pat_load:action()
local fname, status = iup.GetFile("./*.csv")
if status > -1 then
local data, error = sheet_read(fname)
if error then
iup.Message("File not loaded", error)
else
bpm.value = data.bpm
factor.value = data.factor
len.value = data.len
adjust_time(data.bpm, data.factor)
adjust_len(data.len)
instruments = data.instruments or {}
print_instruments()
patterns = data.patterns or { {}, {}, {}, {} }
if patterns[1] then
pat_a.fontstyle="Bold"; pat_b.fontstyle=""; pat_c.fontstyle=""; pat_d.fontstyle=""
pat_recall(1)
end
matrix.redraw = "YES"
end
end
end
-- main program
adjust_time(120, 4)
dlg:showxy( iup.CENTER, iup.CENTER )
if midiwire.device_count()<2 then
midi_setup.active = "NO"
end
if (iup.MainLoopLevel()==0) then
iup.MainLoop()
end