Advertisement
DragonZBW

tracker

Mar 22nd, 2025
180
0
Never
Not a member of Pastebin yet? Sign Up, it unlocks many cool features!
Lua 20.45 KB | Source Code | 0 0
  1. -- Consts
  2. local PIPE_V = string.char(124)
  3. local PIPE_H = string.char(173)
  4. local PIPE_C = string.char(143)
  5. local HIGHLIGHT = string.char(127)
  6. local WIDTH, HEIGHT = term.getSize()
  7.  
  8. local KEY_TO_NOTE = {
  9.     [keys.backspace] = -1,
  10.     [keys.delete] = -1,
  11.     [keys.z] = 0,
  12.     [keys.s] = 1,
  13.     [keys.x] = 2,
  14.     [keys.d] = 3,
  15.     [keys.c] = 4,
  16.     [keys.v] = 5,
  17.     [keys.g] = 6,
  18.     [keys.b] = 7,
  19.     [keys.h] = 8,
  20.     [keys.n] = 9,
  21.     [keys.j] = 10,
  22.     [keys.m] = 11,
  23.     [keys.q] = 12,
  24.     [keys.two] = 13,
  25.     [keys.w] = 14,
  26.     [keys.three] = 15,
  27.     [keys.e] = 16,
  28.     [keys.r] = 17,
  29.     [keys.five] = 18,
  30.     [keys.t] = 19,
  31.     [keys.six] = 20,
  32.     [keys.y] = 21,
  33.     [keys.seven] = 22,
  34.     [keys.u] = 23,
  35.     [keys.i] = 24
  36. }
  37.  
  38. local NOTE_NAMES = {
  39.     [0] = "C1",
  40.     "C#1",
  41.     "D1",
  42.     "D#1",
  43.     "E1",
  44.     "F1",
  45.     "F#1",
  46.     "G1",
  47.     "G#1",
  48.     "A1",
  49.     "A#1",
  50.     "B1",
  51.     "C2",
  52.     "C#2",
  53.     "D2",
  54.     "D#2",
  55.     "E2",
  56.     "F2",
  57.     "F#2",
  58.     "G2",
  59.     "G#2",
  60.     "A2",
  61.     "A#2",
  62.     "B2",
  63.     "C3"
  64. }
  65.  
  66. -- Song info
  67. local meta = {
  68.     ["name"] = nil,
  69.     ["speed"] = nil,
  70.     ["patternLength"] = 16
  71. }
  72. local tracks = {
  73.     {name = "basedrum", disp = "kick drum", notes = {}},
  74.     {name = "snare", disp = "snare", notes = {}},
  75.     {name = "hat", disp = "hat", notes = {}},
  76.     {name = "bass", disp = "bass", notes = {}},
  77.     {name = "guitar", disp = "guitar", notes = {}},
  78.     {name = "flute", disp = "flute", notes = {}},
  79.     {name = "chime", disp = "chime", notes = {}},
  80.     {name = "bell", disp = "bells", notes = {}},
  81.     {name = "xylophone", disp = "xylophone", notes = {}}
  82. }
  83.  
  84. -- Working vars
  85. local pos = 1 -- selected pattern
  86. local subPos = 1 -- cursor position in pattern
  87. local trackPos = 1 -- selected track
  88. local relPos = 1 -- cursor position on screen
  89. local scrollPos = 1 -- top row shown on screen
  90.  
  91. local trackPositions = {} -- x positions of tracks
  92. local trackWidth -- width of a track
  93. local windowWidth -- width of disp window
  94. local windowHeight -- height of disp window
  95. local linesHeight -- number of lines shown at once
  96.  
  97. local isPlaying = false -- is the song playing
  98.  
  99. local speaker -- speaker peripheral
  100.  
  101. local draw
  102.  
  103. -- Clear the terminal screen
  104. local function clear()
  105.     term.clear()
  106.     term.setCursorPos(1,1)
  107. end
  108.  
  109. -- Display a simple dialog box
  110. local function dialog(x, y, message)
  111.     term.setCursorPos(x, y)
  112.     term.write(PIPE_C .. string.rep(PIPE_H, string.len(message)) .. PIPE_C)
  113.     term.setCursorPos(x, y + 3)
  114.     term.write(PIPE_C .. string.rep(PIPE_H, string.len(message)) .. PIPE_C)
  115.     for i = 1, 2 do
  116.         term.setCursorPos(x, y + i)
  117.         term.write(PIPE_V .. string.rep(" ", string.len(message)) .. PIPE_V)
  118.     end
  119.     term.setCursorPos(x + 1, y + 1)
  120.     term.write(message)
  121.     term.setCursorPos(x + 1, y + 2)
  122.     term.write("> OK")
  123.     local event, param
  124.     while param ~= keys.enter do
  125.         event, param = os.pullEvent("key")
  126.     end
  127. end
  128.  
  129. -- Prompt the user for number input
  130. local function numberPrompt(x, y, message, min, max)
  131.     local ans
  132.     repeat
  133.         term.setCursorPos(x, y)
  134.         term.write(PIPE_C .. string.rep(PIPE_H, string.len(message)) .. PIPE_C)
  135.         term.setCursorPos(x, y + 3)
  136.         term.write(PIPE_C .. string.rep(PIPE_H, string.len(message)) .. PIPE_C)
  137.         for i = 1, 2 do
  138.             term.setCursorPos(x, y + i)
  139.             term.write(PIPE_V .. string.rep(" ", string.len(message)) .. PIPE_V)
  140.         end
  141.         term.setCursorPos(x + 1, y + 1)
  142.         term.write(message)
  143.         term.setCursorPos(x + 1, y + 2)
  144.         ans = read()
  145.         if not tonumber(ans) or tonumber(ans) < min or tonumber(ans) > max then
  146.             dialog(x, y, "Invalid. Enter a number between " .. min .. " and " .. max .. ".")
  147.             draw()
  148.         end
  149.     until tonumber(ans) and tonumber(ans) >= min and tonumber(ans) <= max
  150.     return tonumber(ans)
  151. end
  152.  
  153. -- Constructor for a menu option
  154. local function menuOption(name, func)
  155.     return {["name"] = name, ["func"] = func}
  156. end
  157.  
  158. -- Display a menu with certain options
  159. local function menu(x, y, curOpt, ...)
  160.     local arg = {...}
  161.     local longest = 0
  162.     for i, opt in ipairs(arg) do
  163.         if string.len(opt.name) + 2 > longest then
  164.             longest = string.len(opt.name) + 2
  165.         end
  166.     end
  167.     longest = longest
  168.     term.setCursorPos(x, y)
  169.     term.write(PIPE_C .. string.rep(PIPE_H, longest) .. PIPE_C)
  170.     term.setCursorPos(x, y + #arg + 1)
  171.     term.write(PIPE_C .. string.rep(PIPE_H, longest) .. PIPE_C)
  172.     for i, opt in ipairs(arg) do
  173.         term.setCursorPos(x, y + i)
  174.         term.write(PIPE_V .. string.rep(" ", longest) .. PIPE_V)
  175.     end
  176.     local event, param
  177.     while param ~= keys.enter do
  178.         for i, opt in ipairs(arg) do
  179.             term.setCursorPos(x + 1, y + i)
  180.             if curOpt == i then
  181.                 term.write("> " .. opt.name)
  182.             else
  183.                 term.write("  " .. opt.name)
  184.             end
  185.         end
  186.         event, param = os.pullEvent("key")
  187.         if param == keys.up then
  188.             curOpt = curOpt - 1
  189.             if curOpt < 1 then
  190.                 curOpt = #arg
  191.             end
  192.         elseif param == keys.down then
  193.             curOpt = curOpt + 1
  194.             if curOpt > #arg then
  195.                 curOpt = 1
  196.             end
  197.         end
  198.     end
  199.     arg[curOpt].func()
  200. end
  201.  
  202. -- Add pattern
  203. local function addPattern()
  204.     for i = 1, #tracks do
  205.         local pattern = {}
  206.         for n = 1, meta.patternLength do
  207.             pattern[n] = -1
  208.         end
  209.         tracks[i].notes[#tracks[i].notes + 1] = pattern
  210.     end
  211. end
  212.  
  213. -- New file menu
  214. local function new()
  215.     local accept = false
  216.     local name
  217.     while not accept do
  218.         clear()
  219.         print("Name your file.")
  220.         name = read()
  221.         print("The file will be saved at " .. name .. ".song. Is this OK?")
  222.         menu(
  223.             2, 5,
  224.             1,
  225.             menuOption("Yes", function() accept = true end),
  226.             menuOption("No", function() return end))
  227.     end
  228.     meta.name = name .. ".song"
  229.     meta.speed = 3
  230.     meta.patternLength = 16
  231.     addPattern()
  232. end
  233.  
  234. -- Open file menu
  235. local function open()
  236.     local name
  237.     repeat
  238.         clear()
  239.         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.")
  240.         name = read()
  241.         if not fs.exists(name) then
  242.             print("That file does not exist.")
  243.             os.sleep(1.5)
  244.         end
  245.     until fs.exists(name)
  246.     local file = fs.open(name, "r")
  247.     meta.name = name
  248.     meta.speed = tonumber(file.readLine())
  249.     meta.patternLength = tonumber(file.readLine())
  250.     local patternCount = file.readLine()
  251.     for n = 1, #tracks do
  252.         tracks[n].notes = {}
  253.     end
  254.     for n = 1, patternCount do
  255.         addPattern()
  256.     end
  257.     for n,track in ipairs(tracks) do
  258.         for i = 1, patternCount do
  259.             for note = 1, meta.patternLength do
  260.                 local s = file.readLine()
  261.                 noteV = tonumber(s)
  262.                 track.notes[i][note] = noteV
  263.             end
  264.         end
  265.     end
  266.     file.close()
  267. end
  268.  
  269. -- Save file
  270. local function save()
  271.     local file = fs.open(meta.name, "w")
  272.     file.writeLine(tostring(meta.speed))
  273.     file.writeLine(tostring(meta.patternLength))
  274.     file.writeLine(tostring(#tracks[1].notes))
  275.     for i,track in ipairs(tracks) do
  276.         for n,pattern in ipairs(track.notes) do
  277.             for q,note in ipairs(pattern) do
  278.                 file.writeLine(tostring(note))
  279.             end
  280.         end
  281.     end
  282.     file.close()
  283.     dialog(2, 2, "Saved successfully at " .. meta.name)
  284. end
  285.  
  286. -- Determine values for ui dists
  287. local function uiSetup()
  288.     trackWidth = math.floor((WIDTH - 3.0) / #tracks)
  289.     windowWidth = trackWidth * #tracks + 3
  290.     windowHeight = math.min(HEIGHT - 3, meta.patternLength + 3)
  291.     linesHeight = windowHeight - 3
  292.     for i, track in ipairs(tracks) do
  293.         trackPositions[#trackPositions + 1] = trackWidth * (i - 1) + 4
  294.     end
  295. end
  296.  
  297. -- Update scrollPos/relPos based on subPos
  298. local function updateRelPos()
  299.    if subPos < scrollPos then
  300.        scrollPos = subPos
  301.        relPos = 1
  302.    elseif subPos > scrollPos + linesHeight - 1 then
  303.        scrollPos = subPos - linesHeight + 1
  304.        relPos = linesHeight
  305.    else
  306.        relPos = subPos - scrollPos + 1
  307.    end
  308. end
  309.    
  310. -- Draw everything
  311. draw = function()
  312.     -- Draw borders and track names
  313.     term.setCursorPos(3, 1)
  314.     term.write(PIPE_V)
  315.     term.setCursorPos(1, 2)
  316.     term.write(PIPE_H .. PIPE_H .. PIPE_C)
  317.     term.setCursorPos(1, windowHeight)
  318.     term.write(PIPE_H .. PIPE_H .. PIPE_C)
  319.     for i = 3, windowHeight - 1 do
  320.         term.setCursorPos(3, i)
  321.         term.write(PIPE_V)
  322.     end
  323.     term.setCursorPos(3, windowHeight)
  324.     term.write(PIPE_C)
  325.     for i,track in ipairs(tracks) do
  326.         term.setCursorPos(trackPositions[i], 1)
  327.         term.write(string.sub(track.disp, 1, math.min(trackWidth - 1, string.len(track.name))))
  328.         term.setCursorPos(trackPositions[i], 2)
  329.         term.write(string.rep(PIPE_H, trackWidth - 1))
  330.         term.setCursorPos(trackPositions[i], windowHeight)
  331.         term.write(string.rep(PIPE_H, trackWidth - 1))
  332.         local sepX = trackWidth * i + 3
  333.         for i = 1, windowHeight do
  334.             term.setCursorPos(sepX, i)
  335.             if i == 2 or i == windowHeight then
  336.                 term.write(PIPE_C)
  337.             else
  338.                 term.write(PIPE_V)
  339.             end
  340.         end
  341.     end
  342.    
  343.     -- Draw spaces after last column to clear any unwanted chars
  344.     for i = 1, windowHeight do
  345.         term.setCursorPos(windowWidth + 1, i)
  346.         term.write(" ")
  347.     end
  348.    
  349.     -- Draw line numbers
  350.     for i = 1, linesHeight do
  351.         term.setCursorPos(1, 2 + i)
  352.         local s = tostring(scrollPos + i - 1)
  353.         if string.len(s) == 1 then
  354.             s = "0" .. s
  355.         end
  356.         term.write(s)
  357.     end
  358.    
  359.     -- Draw notes
  360.     for i, track in ipairs(tracks) do
  361.         local pattern = track.notes[pos]
  362.         for n = scrollPos, math.min(#pattern, scrollPos + linesHeight - 1) do
  363.             term.setCursorPos(trackPositions[i], 3 + n - scrollPos)
  364.             local noteName = NOTE_NAMES[pattern[n]]
  365.             if not noteName then noteName = "" end
  366.             term.write(noteName .. string.rep(" ",trackWidth - 1 - string.len(noteName)))
  367.         end
  368.     end
  369.    
  370.     -- Draw meta info below window
  371.     term.setCursorPos(1, HEIGHT - 2)
  372.     term.write("Current track: " .. tracks[trackPos].disp .. "          ")
  373.     term.setCursorPos(1, HEIGHT - 1)
  374.     term.write("Speed: " .. meta.speed ..
  375.         " -- MeasLength: " .. meta.patternLength ..
  376.         " -- Measure: " .. pos .. "/" .. #tracks[1].notes .. "          ")
  377.     term.setCursorPos(1, HEIGHT)
  378.     term.write("[Press left ctrl for menu]                 ")
  379.    
  380.     -- Draw selected position
  381.     if isPlaying then
  382.         term.setCursorPos(1, HEIGHT)
  383.         term.write("[PLAYING... Press Enter to stop]")
  384.         local selY = 2 + relPos
  385.         term.setCursorPos(1, selY)
  386.         term.write(string.rep(HIGHLIGHT, windowWidth))
  387.     else
  388.         local selY = 2 + relPos
  389.         term.setCursorPos(1, selY)
  390.         term.write("=")
  391.         for i, track in ipairs(tracks) do
  392.             term.setCursorPos(trackPositions[i] - 1, selY)
  393.             term.write("=")
  394.         end
  395.         term.setCursorPos(trackPositions[#tracks] + trackWidth - 1, selY)
  396.         term.write("=")
  397.         term.setCursorPos(trackPositions[trackPos] - 2, selY)
  398.         term.write(">>")
  399.         if trackPos == #tracks then
  400.             term.setCursorPos(trackPositions[trackPos] + trackWidth - 1, selY)
  401.         else
  402.             term.setCursorPos(trackPositions[trackPos + 1] - 1, selY)
  403.         end
  404.         term.write("<<")
  405.     end
  406. end
  407.  
  408. -- Play the song
  409. local function play()
  410.     isPlaying = true
  411.     subPos = 1
  412.     while pos <= #tracks[1].notes do
  413.         updateRelPos()
  414.         draw()
  415.         for i,track in ipairs(tracks) do
  416.             local note = track.notes[pos][subPos]
  417.             if note ~= -1 then
  418.                 speaker.playNote(track.name, 3, note)
  419.             end
  420.         end
  421.         os.startTimer(.05 * meta.speed)
  422.         local event, param
  423.         repeat
  424.             event, param = os.pullEvent()
  425.         until event == "timer" or (event == "key" and param == keys.enter)
  426.         if event == "key" then
  427.             updateRelPos()
  428.             isPlaying = false
  429.             draw()
  430.             return
  431.         end
  432.         subPos = subPos + 1
  433.         if subPos > meta.patternLength then
  434.             subPos = 1
  435.             pos = pos + 1
  436.         end
  437.     end
  438.     pos = pos - 1
  439.     subPos = meta.patternLength
  440.     updateRelPos()
  441.     isPlaying = false
  442.     draw()
  443. end
  444.  
  445. -- Get speaker peripheral
  446. clear()
  447. for i, v in ipairs(peripheral.getNames()) do
  448.     if peripheral.getType(v) == "speaker" then
  449.         speaker = peripheral.wrap(v)
  450.     end
  451. end
  452.  
  453. if not speaker then
  454.     print("No speaker present...")
  455.     return
  456. end
  457. print("Connected to speaker. Initializing...")
  458. os.sleep(1.5)
  459.  
  460. local exitRequested, toMenuRequested
  461. local function main()
  462.     -- Open main menu
  463.     clear()
  464.     exitRequested = false
  465.     toMenuRequested = false
  466.     menu(
  467.         2, 2,
  468.         1,
  469.         menuOption("New", new),
  470.         menuOption("Open", open),
  471.         menuOption("Exit", function() exitRequested = true end)
  472.     )
  473.     if exitRequested then
  474.         clear()
  475.         return
  476.     end
  477.    
  478.     -- Main loop
  479.     clear()
  480.     uiSetup()
  481.     local event, param
  482.     while not exitRequested and not toMenuRequested do
  483.         draw()
  484.         event, param = os.pullEvent("key")
  485.         if param == keys.up then
  486.             subPos = subPos - 1
  487.             if subPos < 1 then
  488.                 subPos = meta.patternLength
  489.             end
  490.         elseif param == keys.down then
  491.             subPos = subPos + 1
  492.             if subPos > meta.patternLength then
  493.                 subPos = 1
  494.             end
  495.         elseif param == keys.left then
  496.             trackPos = trackPos - 1
  497.             if trackPos < 1 then
  498.                 trackPos = #tracks
  499.             end
  500.         elseif param == keys.right then
  501.             trackPos = trackPos + 1
  502.             if trackPos > #tracks then
  503.                 trackPos = 1
  504.             end
  505.         elseif param == keys.comma then
  506.             pos = pos - 1
  507.             if pos < 1 then
  508.                 pos = #tracks[1].notes
  509.             end
  510.         elseif param == keys.period then
  511.             pos = pos + 1
  512.             if pos > #tracks[1].notes then
  513.                 pos = 1
  514.             end
  515.         elseif param == keys.enter then
  516.             -- Play the song
  517.             play()
  518.         elseif param == keys.leftCtrl then
  519.             -- Menu
  520.             menu(
  521.                 2,2,
  522.                 1,
  523.                 menuOption("Save", save),
  524.                 menuOption("Copy",
  525.                     function()
  526.                         local valid = false
  527.                         while not valid do
  528.                             local start = numberPrompt(2,2,"Start measure:",1,#tracks[1].notes)
  529.                             local stop = numberPrompt(2,2,"End measure:",1,#tracks[1].notes)
  530.                             local dest = numberPrompt(2,2,"Paste at measure:",1,#tracks[1].notes)
  531.                             if stop < start then
  532.                                 dialog(2,2,"End measure must be >= start measure.")
  533.                             else
  534.                                 valid = true
  535.                                 for i = 1, stop - start + 1 do
  536.                                     for t, track in ipairs(tracks) do
  537.                                         for n = 1, meta.patternLength do
  538.                                             track.notes[dest + i - 1][n] = track.notes[start + i - 1][n]
  539.                                         end
  540.                                     end
  541.                                 end
  542.                             end
  543.                         end
  544.                     end),
  545.                 menuOption("Copy (Current Track)",
  546.                     function()
  547.                         local valid = false
  548.                         while not valid do
  549.                             local start = numberPrompt(2,2,"Start measure:",1,#tracks[1].notes)
  550.                             local stop = numberPrompt(2,2,"End measure:",1,#tracks[1].notes)
  551.                             local dest = numberPrompt(2,2,"Paste at measure:",1,#tracks[1].notes)
  552.                             if stop < start then
  553.                                 dialog(2,2,"End measure must be >= start measure.")
  554.                             else
  555.                                 valid = true
  556.                                 local track = tracks[trackPos]
  557.                                 for i = 1, stop - start + 1 do
  558.                                     for n = 1, meta.patternLength do
  559.                                         track.notes[dest + i - 1][n] = track.notes[start + i - 1][n]
  560.                                     end
  561.                                 end
  562.                             end
  563.                         end
  564.                     end),
  565.                 menuOption("Add Measures",
  566.                     function()
  567.                         local n = numberPrompt(2,2,"How many to add?",0,64)
  568.                         for i = 1, n do
  569.                             addPattern()
  570.                         end
  571.                     end),
  572.                 menuOption("Remove Measures",
  573.                     function()
  574.                         if #tracks[1].notes == 1 then
  575.                             dialog("Can't remove with only 1 measure.")
  576.                             return
  577.                         end
  578.                         local valid = false
  579.                         while not valid do
  580.                             local start = numberPrompt(2,2,"Start measure:",1,#tracks[1].notes)
  581.                             local stop = numberPrompt(2,2,"End measure:",1,#tracks[1].notes)
  582.                             if stop < start then
  583.                                 dialog("End measure must be >= start measure.")
  584.                             else
  585.                                 valid = true
  586.                                 for i = start, stop do
  587.                                     for t = 1, #tracks do
  588.                                         for n = start, #tracks[t].notes do
  589.                                             tracks[t].notes[n] = tracks[t].notes[n + 1]
  590.                                         end
  591.                                     end
  592.                                 end
  593.                             end
  594.                         end
  595.                         if pos > #tracks[1].notes then
  596.                             pos = #tracks[1].notes
  597.                         end
  598.                     end),
  599.                 menuOption("Set Measure Length",
  600.                     function()
  601.                         local n = numberPrompt(2,2,"Set the new measure length:",4,32)
  602.                         meta.patternLength = n
  603.                         for t, track in ipairs(tracks) do
  604.                             for p, pattern in ipairs(track.notes) do
  605.                                 if #pattern > n then
  606.                                     pattern[n + 1] = nil
  607.                                 elseif #pattern < n then
  608.                                     for i = #pattern + 1, n do
  609.                                         pattern[i] = -1
  610.                                     end
  611.                                 end
  612.                             end
  613.                         end
  614.                         uiSetup()
  615.                         clear()
  616.                     end),  
  617.                 menuOption("Set Speed",
  618.                     function()
  619.                         local n = numberPrompt(2,2,"Set the new speed:",1,16)
  620.                         meta.speed = n
  621.                     end),
  622.                 menuOption("Back to Editing", function() end),
  623.                 menuOption("Exit", function() toMenuRequested = true end)
  624.             )
  625.         elseif KEY_TO_NOTE[param] then
  626.             -- Note input
  627.             tracks[trackPos].notes[pos][subPos] = KEY_TO_NOTE[param]
  628.             if tracks[trackPos].notes[pos][subPos] > -1 then
  629.                 speaker.playNote(tracks[trackPos].name, 3, tracks[trackPos].notes[pos][subPos])
  630.             end
  631.         end
  632.    
  633.         updateRelPos()
  634.     end
  635.     clear()
  636.     return
  637. end
  638.  
  639. while not exitRequested do
  640.     main()
  641. end
  642.  
Advertisement
Add Comment
Please, Sign In to add comment
Advertisement