Advertisement
Not a member of Pastebin yet?
Sign Up,
it unlocks many cool features!
- -- Consts
- local PIPE_V = string.char(124)
- local PIPE_H = string.char(173)
- local PIPE_C = string.char(143)
- local HIGHLIGHT = string.char(127)
- local WIDTH, HEIGHT = term.getSize()
- local KEY_TO_NOTE = {
- [keys.backspace] = -1,
- [keys.delete] = -1,
- [keys.z] = 0,
- [keys.s] = 1,
- [keys.x] = 2,
- [keys.d] = 3,
- [keys.c] = 4,
- [keys.v] = 5,
- [keys.g] = 6,
- [keys.b] = 7,
- [keys.h] = 8,
- [keys.n] = 9,
- [keys.j] = 10,
- [keys.m] = 11,
- [keys.q] = 12,
- [keys.two] = 13,
- [keys.w] = 14,
- [keys.three] = 15,
- [keys.e] = 16,
- [keys.r] = 17,
- [keys.five] = 18,
- [keys.t] = 19,
- [keys.six] = 20,
- [keys.y] = 21,
- [keys.seven] = 22,
- [keys.u] = 23,
- [keys.i] = 24
- }
- local NOTE_NAMES = {
- [0] = "C1",
- "C#1",
- "D1",
- "D#1",
- "E1",
- "F1",
- "F#1",
- "G1",
- "G#1",
- "A1",
- "A#1",
- "B1",
- "C2",
- "C#2",
- "D2",
- "D#2",
- "E2",
- "F2",
- "F#2",
- "G2",
- "G#2",
- "A2",
- "A#2",
- "B2",
- "C3"
- }
- -- Song info
- local meta = {
- ["name"] = nil,
- ["speed"] = nil,
- ["patternLength"] = 16
- }
- local tracks = {
- {name = "basedrum", disp = "kick drum", notes = {}},
- {name = "snare", disp = "snare", notes = {}},
- {name = "hat", disp = "hat", notes = {}},
- {name = "bass", disp = "bass", notes = {}},
- {name = "guitar", disp = "guitar", notes = {}},
- {name = "flute", disp = "flute", notes = {}},
- {name = "chime", disp = "chime", notes = {}},
- {name = "bell", disp = "bells", notes = {}},
- {name = "xylophone", disp = "xylophone", notes = {}}
- }
- -- Working vars
- local pos = 1 -- selected pattern
- local subPos = 1 -- cursor position in pattern
- local trackPos = 1 -- selected track
- local relPos = 1 -- cursor position on screen
- local scrollPos = 1 -- top row shown on screen
- local trackPositions = {} -- x positions of tracks
- local trackWidth -- width of a track
- local windowWidth -- width of disp window
- local windowHeight -- height of disp window
- local linesHeight -- number of lines shown at once
- local isPlaying = false -- is the song playing
- local speaker -- speaker peripheral
- local draw
- -- Clear the terminal screen
- local function clear()
- term.clear()
- term.setCursorPos(1,1)
- end
- -- Display a simple dialog box
- local function dialog(x, y, message)
- term.setCursorPos(x, y)
- term.write(PIPE_C .. string.rep(PIPE_H, string.len(message)) .. PIPE_C)
- term.setCursorPos(x, y + 3)
- term.write(PIPE_C .. string.rep(PIPE_H, string.len(message)) .. PIPE_C)
- for i = 1, 2 do
- term.setCursorPos(x, y + i)
- term.write(PIPE_V .. string.rep(" ", string.len(message)) .. PIPE_V)
- end
- term.setCursorPos(x + 1, y + 1)
- term.write(message)
- term.setCursorPos(x + 1, y + 2)
- term.write("> OK")
- local event, param
- while param ~= keys.enter do
- event, param = os.pullEvent("key")
- end
- end
- -- Prompt the user for number input
- local function numberPrompt(x, y, message, min, max)
- local ans
- repeat
- term.setCursorPos(x, y)
- term.write(PIPE_C .. string.rep(PIPE_H, string.len(message)) .. PIPE_C)
- term.setCursorPos(x, y + 3)
- term.write(PIPE_C .. string.rep(PIPE_H, string.len(message)) .. PIPE_C)
- for i = 1, 2 do
- term.setCursorPos(x, y + i)
- term.write(PIPE_V .. string.rep(" ", string.len(message)) .. PIPE_V)
- end
- term.setCursorPos(x + 1, y + 1)
- term.write(message)
- term.setCursorPos(x + 1, y + 2)
- ans = read()
- if not tonumber(ans) or tonumber(ans) < min or tonumber(ans) > max then
- dialog(x, y, "Invalid. Enter a number between " .. min .. " and " .. max .. ".")
- draw()
- end
- until tonumber(ans) and tonumber(ans) >= min and tonumber(ans) <= max
- return tonumber(ans)
- end
- -- Constructor for a menu option
- local function menuOption(name, func)
- return {["name"] = name, ["func"] = func}
- end
- -- Display a menu with certain options
- local function menu(x, y, curOpt, ...)
- local arg = {...}
- local longest = 0
- for i, opt in ipairs(arg) do
- if string.len(opt.name) + 2 > longest then
- longest = string.len(opt.name) + 2
- end
- end
- longest = longest
- term.setCursorPos(x, y)
- term.write(PIPE_C .. string.rep(PIPE_H, longest) .. PIPE_C)
- term.setCursorPos(x, y + #arg + 1)
- term.write(PIPE_C .. string.rep(PIPE_H, longest) .. PIPE_C)
- for i, opt in ipairs(arg) do
- term.setCursorPos(x, y + i)
- term.write(PIPE_V .. string.rep(" ", longest) .. PIPE_V)
- end
- local event, param
- while param ~= keys.enter do
- for i, opt in ipairs(arg) do
- term.setCursorPos(x + 1, y + i)
- if curOpt == i then
- term.write("> " .. opt.name)
- else
- term.write(" " .. opt.name)
- end
- end
- event, param = os.pullEvent("key")
- if param == keys.up then
- curOpt = curOpt - 1
- if curOpt < 1 then
- curOpt = #arg
- end
- elseif param == keys.down then
- curOpt = curOpt + 1
- if curOpt > #arg then
- curOpt = 1
- end
- end
- end
- arg[curOpt].func()
- end
- -- Add pattern
- local function addPattern()
- for i = 1, #tracks do
- local pattern = {}
- for n = 1, meta.patternLength do
- pattern[n] = -1
- end
- tracks[i].notes[#tracks[i].notes + 1] = pattern
- end
- end
- -- New file menu
- local function new()
- local accept = false
- local name
- while not accept do
- clear()
- print("Name your file.")
- name = read()
- print("The file will be saved at " .. name .. ".song. Is this OK?")
- menu(
- 2, 5,
- 1,
- menuOption("Yes", function() accept = true end),
- menuOption("No", function() return end))
- end
- meta.name = name .. ".song"
- meta.speed = 3
- meta.patternLength = 16
- addPattern()
- end
- -- Open file menu
- local function open()
- local name
- repeat
- clear()
- print("What file do you want to open? Note that the program does not check filetypes, and as such opening a file of the incorrect format may cause a crash.")
- name = read()
- if not fs.exists(name) then
- print("That file does not exist.")
- os.sleep(1.5)
- end
- until fs.exists(name)
- local file = fs.open(name, "r")
- meta.name = name
- meta.speed = tonumber(file.readLine())
- meta.patternLength = tonumber(file.readLine())
- local patternCount = file.readLine()
- for n = 1, #tracks do
- tracks[n].notes = {}
- end
- for n = 1, patternCount do
- addPattern()
- end
- for n,track in ipairs(tracks) do
- for i = 1, patternCount do
- for note = 1, meta.patternLength do
- local s = file.readLine()
- noteV = tonumber(s)
- track.notes[i][note] = noteV
- end
- end
- end
- file.close()
- end
- -- Save file
- local function save()
- local file = fs.open(meta.name, "w")
- file.writeLine(tostring(meta.speed))
- file.writeLine(tostring(meta.patternLength))
- file.writeLine(tostring(#tracks[1].notes))
- for i,track in ipairs(tracks) do
- for n,pattern in ipairs(track.notes) do
- for q,note in ipairs(pattern) do
- file.writeLine(tostring(note))
- end
- end
- end
- file.close()
- dialog(2, 2, "Saved successfully at " .. meta.name)
- end
- -- Determine values for ui dists
- local function uiSetup()
- trackWidth = math.floor((WIDTH - 3.0) / #tracks)
- windowWidth = trackWidth * #tracks + 3
- windowHeight = math.min(HEIGHT - 3, meta.patternLength + 3)
- linesHeight = windowHeight - 3
- for i, track in ipairs(tracks) do
- trackPositions[#trackPositions + 1] = trackWidth * (i - 1) + 4
- end
- end
- -- Update scrollPos/relPos based on subPos
- local function updateRelPos()
- if subPos < scrollPos then
- scrollPos = subPos
- relPos = 1
- elseif subPos > scrollPos + linesHeight - 1 then
- scrollPos = subPos - linesHeight + 1
- relPos = linesHeight
- else
- relPos = subPos - scrollPos + 1
- end
- end
- -- Draw everything
- draw = function()
- -- Draw borders and track names
- term.setCursorPos(3, 1)
- term.write(PIPE_V)
- term.setCursorPos(1, 2)
- term.write(PIPE_H .. PIPE_H .. PIPE_C)
- term.setCursorPos(1, windowHeight)
- term.write(PIPE_H .. PIPE_H .. PIPE_C)
- for i = 3, windowHeight - 1 do
- term.setCursorPos(3, i)
- term.write(PIPE_V)
- end
- term.setCursorPos(3, windowHeight)
- term.write(PIPE_C)
- for i,track in ipairs(tracks) do
- term.setCursorPos(trackPositions[i], 1)
- term.write(string.sub(track.disp, 1, math.min(trackWidth - 1, string.len(track.name))))
- term.setCursorPos(trackPositions[i], 2)
- term.write(string.rep(PIPE_H, trackWidth - 1))
- term.setCursorPos(trackPositions[i], windowHeight)
- term.write(string.rep(PIPE_H, trackWidth - 1))
- local sepX = trackWidth * i + 3
- for i = 1, windowHeight do
- term.setCursorPos(sepX, i)
- if i == 2 or i == windowHeight then
- term.write(PIPE_C)
- else
- term.write(PIPE_V)
- end
- end
- end
- -- Draw spaces after last column to clear any unwanted chars
- for i = 1, windowHeight do
- term.setCursorPos(windowWidth + 1, i)
- term.write(" ")
- end
- -- Draw line numbers
- for i = 1, linesHeight do
- term.setCursorPos(1, 2 + i)
- local s = tostring(scrollPos + i - 1)
- if string.len(s) == 1 then
- s = "0" .. s
- end
- term.write(s)
- end
- -- Draw notes
- for i, track in ipairs(tracks) do
- local pattern = track.notes[pos]
- for n = scrollPos, math.min(#pattern, scrollPos + linesHeight - 1) do
- term.setCursorPos(trackPositions[i], 3 + n - scrollPos)
- local noteName = NOTE_NAMES[pattern[n]]
- if not noteName then noteName = "" end
- term.write(noteName .. string.rep(" ",trackWidth - 1 - string.len(noteName)))
- end
- end
- -- Draw meta info below window
- term.setCursorPos(1, HEIGHT - 2)
- term.write("Current track: " .. tracks[trackPos].disp .. " ")
- term.setCursorPos(1, HEIGHT - 1)
- term.write("Speed: " .. meta.speed ..
- " -- MeasLength: " .. meta.patternLength ..
- " -- Measure: " .. pos .. "/" .. #tracks[1].notes .. " ")
- term.setCursorPos(1, HEIGHT)
- term.write("[Press left ctrl for menu] ")
- -- Draw selected position
- if isPlaying then
- term.setCursorPos(1, HEIGHT)
- term.write("[PLAYING... Press Enter to stop]")
- local selY = 2 + relPos
- term.setCursorPos(1, selY)
- term.write(string.rep(HIGHLIGHT, windowWidth))
- else
- local selY = 2 + relPos
- term.setCursorPos(1, selY)
- term.write("=")
- for i, track in ipairs(tracks) do
- term.setCursorPos(trackPositions[i] - 1, selY)
- term.write("=")
- end
- term.setCursorPos(trackPositions[#tracks] + trackWidth - 1, selY)
- term.write("=")
- term.setCursorPos(trackPositions[trackPos] - 2, selY)
- term.write(">>")
- if trackPos == #tracks then
- term.setCursorPos(trackPositions[trackPos] + trackWidth - 1, selY)
- else
- term.setCursorPos(trackPositions[trackPos + 1] - 1, selY)
- end
- term.write("<<")
- end
- end
- -- Play the song
- local function play()
- isPlaying = true
- subPos = 1
- while pos <= #tracks[1].notes do
- updateRelPos()
- draw()
- for i,track in ipairs(tracks) do
- local note = track.notes[pos][subPos]
- if note ~= -1 then
- speaker.playNote(track.name, 3, note)
- end
- end
- os.startTimer(.05 * meta.speed)
- local event, param
- repeat
- event, param = os.pullEvent()
- until event == "timer" or (event == "key" and param == keys.enter)
- if event == "key" then
- updateRelPos()
- isPlaying = false
- draw()
- return
- end
- subPos = subPos + 1
- if subPos > meta.patternLength then
- subPos = 1
- pos = pos + 1
- end
- end
- pos = pos - 1
- subPos = meta.patternLength
- updateRelPos()
- isPlaying = false
- draw()
- end
- -- Get speaker peripheral
- clear()
- for i, v in ipairs(peripheral.getNames()) do
- if peripheral.getType(v) == "speaker" then
- speaker = peripheral.wrap(v)
- end
- end
- if not speaker then
- print("No speaker present...")
- return
- end
- print("Connected to speaker. Initializing...")
- os.sleep(1.5)
- local exitRequested, toMenuRequested
- local function main()
- -- Open main menu
- clear()
- exitRequested = false
- toMenuRequested = false
- menu(
- 2, 2,
- 1,
- menuOption("New", new),
- menuOption("Open", open),
- menuOption("Exit", function() exitRequested = true end)
- )
- if exitRequested then
- clear()
- return
- end
- -- Main loop
- clear()
- uiSetup()
- local event, param
- while not exitRequested and not toMenuRequested do
- draw()
- event, param = os.pullEvent("key")
- if param == keys.up then
- subPos = subPos - 1
- if subPos < 1 then
- subPos = meta.patternLength
- end
- elseif param == keys.down then
- subPos = subPos + 1
- if subPos > meta.patternLength then
- subPos = 1
- end
- elseif param == keys.left then
- trackPos = trackPos - 1
- if trackPos < 1 then
- trackPos = #tracks
- end
- elseif param == keys.right then
- trackPos = trackPos + 1
- if trackPos > #tracks then
- trackPos = 1
- end
- elseif param == keys.comma then
- pos = pos - 1
- if pos < 1 then
- pos = #tracks[1].notes
- end
- elseif param == keys.period then
- pos = pos + 1
- if pos > #tracks[1].notes then
- pos = 1
- end
- elseif param == keys.enter then
- -- Play the song
- play()
- elseif param == keys.leftCtrl then
- -- Menu
- menu(
- 2,2,
- 1,
- menuOption("Save", save),
- menuOption("Copy",
- function()
- local valid = false
- while not valid do
- local start = numberPrompt(2,2,"Start measure:",1,#tracks[1].notes)
- local stop = numberPrompt(2,2,"End measure:",1,#tracks[1].notes)
- local dest = numberPrompt(2,2,"Paste at measure:",1,#tracks[1].notes)
- if stop < start then
- dialog(2,2,"End measure must be >= start measure.")
- else
- valid = true
- for i = 1, stop - start + 1 do
- for t, track in ipairs(tracks) do
- for n = 1, meta.patternLength do
- track.notes[dest + i - 1][n] = track.notes[start + i - 1][n]
- end
- end
- end
- end
- end
- end),
- menuOption("Copy (Current Track)",
- function()
- local valid = false
- while not valid do
- local start = numberPrompt(2,2,"Start measure:",1,#tracks[1].notes)
- local stop = numberPrompt(2,2,"End measure:",1,#tracks[1].notes)
- local dest = numberPrompt(2,2,"Paste at measure:",1,#tracks[1].notes)
- if stop < start then
- dialog(2,2,"End measure must be >= start measure.")
- else
- valid = true
- local track = tracks[trackPos]
- for i = 1, stop - start + 1 do
- for n = 1, meta.patternLength do
- track.notes[dest + i - 1][n] = track.notes[start + i - 1][n]
- end
- end
- end
- end
- end),
- menuOption("Add Measures",
- function()
- local n = numberPrompt(2,2,"How many to add?",0,64)
- for i = 1, n do
- addPattern()
- end
- end),
- menuOption("Remove Measures",
- function()
- if #tracks[1].notes == 1 then
- dialog("Can't remove with only 1 measure.")
- return
- end
- local valid = false
- while not valid do
- local start = numberPrompt(2,2,"Start measure:",1,#tracks[1].notes)
- local stop = numberPrompt(2,2,"End measure:",1,#tracks[1].notes)
- if stop < start then
- dialog("End measure must be >= start measure.")
- else
- valid = true
- for i = start, stop do
- for t = 1, #tracks do
- for n = start, #tracks[t].notes do
- tracks[t].notes[n] = tracks[t].notes[n + 1]
- end
- end
- end
- end
- end
- if pos > #tracks[1].notes then
- pos = #tracks[1].notes
- end
- end),
- menuOption("Set Measure Length",
- function()
- local n = numberPrompt(2,2,"Set the new measure length:",4,32)
- meta.patternLength = n
- for t, track in ipairs(tracks) do
- for p, pattern in ipairs(track.notes) do
- if #pattern > n then
- pattern[n + 1] = nil
- elseif #pattern < n then
- for i = #pattern + 1, n do
- pattern[i] = -1
- end
- end
- end
- end
- uiSetup()
- clear()
- end),
- menuOption("Set Speed",
- function()
- local n = numberPrompt(2,2,"Set the new speed:",1,16)
- meta.speed = n
- end),
- menuOption("Back to Editing", function() end),
- menuOption("Exit", function() toMenuRequested = true end)
- )
- elseif KEY_TO_NOTE[param] then
- -- Note input
- tracks[trackPos].notes[pos][subPos] = KEY_TO_NOTE[param]
- if tracks[trackPos].notes[pos][subPos] > -1 then
- speaker.playNote(tracks[trackPos].name, 3, tracks[trackPos].notes[pos][subPos])
- end
- end
- updateRelPos()
- end
- clear()
- return
- end
- while not exitRequested do
- main()
- end
Advertisement
Add Comment
Please, Sign In to add comment
Advertisement