local GUI = require("GUI") local screen = require("Screen") local filesystem = require("Filesystem") local color = require("Color") local image = require("Image") local paths = require("Paths") local system = require("System") local text = require("Text") local internet = require("Internet") local event = require("Event") -------------------------------- Config ------------------------------------------------ local configPath = paths.user.applicationData .. "Pioneer/Config.cfg" local saveConfigUptime local config = { tapes = { }, timeMode = 0, navigationSensivity = 0, jogResistance = 0, gain = 1, tempo = 0.5, tempoIndex = 0, } -- Loading config if it exists if filesystem.exists(configPath) then local loadedConfig = filesystem.readTable(configPath) -- Older versions support for key, value in pairs(config) do if not loadedConfig[key] then loadedConfig[key] = value end end config = loadedConfig end local function delaySaveConfig() saveConfigUptime = computer.uptime() + 1 end -------------------------------- Core ------------------------------------------------ local sampleRate = 1500 * 4 local IOBufferSize = 8192 local blinkState = false local blinkUptime = 0 local blinkInterval = 0.5 local loopInButton local loopOutButton local tapes local tapeIndex local tape local tapeConfig local tapePosition = 0 local currentScriptDirectory = filesystem.path(system.getCurrentScript()) local function invoke(...) return component.invoke(tape.address, ...) end local function setPosition(value) tapePosition = value invoke("seek", value - invoke("getPosition")) end local function updateTapePosition() tapePosition = invoke("getPosition") end local function navigate(direction) -- Min seek is 100 ms & max is 10 s invoke("seek", math.floor((0.1 + (10 * config.navigationSensivity)) * sampleRate * direction)) updateTapePosition() end local function getTapeTempo() local tempo = 2 * config.tempo - 1 if config.tempoIndex == 0 then tempo = tempo * 0.25 elseif config.tempoIndex == 1 then tempo = tempo * 0.5 elseif config.tempoIndex == 2 then tempo = tempo * 0.75 end return tempo end local function updateTempo(address) component.invoke(address, "setSpeed", 1 + getTapeTempo() * 0.75) end local function updateGain(address) component.invoke(address, "setVolume", config.gain) end local function updateTempoForAll() for i = 1, #tapes do updateTempo(tapes[i].address) end end local function updateGainForAll() for i = 1, #tapes do updateGain(tapes[i].address) end end local function updateLoop() loopInButton.blinking = tapeConfig.loopIn and tapeConfig.loopOn loopOutButton.blinking = tapeConfig.loopOut and tapeConfig.loopOn end local function updateTape() tape = tapes[tapeIndex] tapeConfig = config.tapes[tape.address] config.lastTape = tape.address updateLoop() end local function updateTapes() tapes = {} tapeIndex = 1 tape = nil tapeConfig = nil local counter = 1 for address in component.list("tape_drive") do table.insert(tapes, { address = address, size = component.invoke(address, "getSize") }) if not config.tapes[address] then config.tapes[address] = { cue = 0, cues = {}, cueIndex = 1, hotCues = {} } end if config.lastTape == address then tapeIndex = counter end updateTempo(address) updateGain(address) counter = counter + 1 end if #tapes > 0 then updateTape() end end local function loadImage(name) local result, reason = image.load(currentScriptDirectory .. "Images/" .. name .. ".pic") if not result then GUI.alert(reason) end return result end -------------------------------- Window ------------------------------------------------ local workspace, window, menu = system.addWindow(GUI.window(1, 1, 78, 49)) window.drawShadow = false local backgroundImage = loadImage("Background") local powerButton -------------------------------- Overlay ------------------------------------------------ local overlay = window:addChild(GUI.object(1, 1, window.width, window.height)) local displayWidth, displayHeight = 33, 10 local jogImages = {} for i = 1, 12 do jogImages[i] = loadImage("Jog" .. i) end local knobImages = {} for i = 1, 8 do knobImages[i] = loadImage("Knob" .. i) end local jogIndex = 1 local jogIncrementTempoMin = 0.05 local jogIncrementTempoMax = 2 local jogIncrementUptime = 0 local function incrementJogIndex(value) jogIndex = jogIndex + value if jogIndex > #jogImages then jogIndex = 1 elseif jogIndex < 1 then jogIndex = #jogImages end end local function invalidateJogIncrementUptime(uptime) jogIncrementUptime = uptime + (1 - config.tempo) * (jogIncrementTempoMax - jogIncrementTempoMin) end local function displayDrawProgressBar(x, y, width, progress) local progressActiveWidth = math.floor(progress * width) screen.drawText(x, y, 0xE1E1E1, string.rep("━", progressActiveWidth)) screen.drawText(x + progressActiveWidth, y, 0x4B4B4B, string.rep("━", width - progressActiveWidth)) end overlay.draw = function(overlay) screen.drawImage(overlay.x, overlay.y, backgroundImage) -- Power indicator screen.drawText(overlay.x + 73, overlay.y + 3, powerButton.pressed and 0xFF0000 or 0x330000, "⠒") -- SD-card indicator screen.drawText(overlay.x + 5, overlay.y + 10, powerButton.pressed and 0xFFB640 or 0x332400, "⠒") -- Ignoring if power is off if not powerButton.pressed then return end -- Tempo slider indicator screen.drawText(overlay.x + 68, overlay.y + 39, 0xFFDB40, "⠆") -- Jog screen.drawImage(overlay.x + 33, overlay.y + 29, jogImages[jogIndex]) -- Display local displayX, displayY = overlay.x + 22, overlay.y + 3 -- Label local label = tape and invoke("getLabel") or "No tape" if not label or #label == 0 then label = "Untitled tape" end screen.drawRectangle(displayX, displayY, displayWidth, 1, 0x004980, 0xE1E1E1, " ") screen.drawText(displayX + 1, displayY, 0xE1E1E1, text.limit("♪ " .. label, displayWidth - 3)) if tape then -- Stats local statsX = displayX + 2 local statsY = displayY + displayHeight - 5 -- Track index screen.drawText(statsX, statsY, 0xE1E1E1, "Track") screen.drawText(statsX, statsY + 1, 0xE1E1E1, string.format("%02d", tapeIndex)) -- Time local timeSecondsTotal = (config.timeMode == 0 and tapePosition or (config.timeMode == 1 and tape.size - tapePosition or tape.size)) / sampleRate local timeMinutes = math.floor(timeSecondsTotal / 60) local timeSeconds, timeMilliseconds = math.modf(timeSecondsTotal - timeMinutes * 60) local timeString = string.format("%02d", timeMinutes) .. "m:" .. string.format("%02d", timeSeconds) .. "s".. string.format("%02d", math.floor(timeMilliseconds * 100)) screen.drawText(statsX + 10, statsY + 1, 0xE1E1E1, config.timeMode == 1 and "-" .. timeString or timeString) -- Tempo screen.drawText(statsX + 24, statsY, 0xE1E1E1, "Tempo") screen.drawText(statsX + 26, statsY + 1, 0xE1E1E1, string.format("%02d", math.floor(getTapeTempo() * 100)) .. "%") -- Tempo index -- Track local trackWidth = displayWidth - 4 local trackHeight = 3 statsY = statsY + 2 screen.drawRectangle( statsX, statsY + 1, trackWidth, trackHeight - 2, 0x2D2D2D, 0xE1E1E1, " " ) -- Loop if tapeConfig.loopIn or tapeConfig.loopOut then local loopX = tapeConfig.loopIn and math.floor(tapeConfig.loopIn / tape.size * trackWidth) or 0 screen.drawRectangle( statsX + loopX, statsY + 1, 1 + (tapeConfig.loopOut and math.floor(tapeConfig.loopOut / tape.size * trackWidth) or trackWidth) - loopX, 1, tapeConfig.loopOn and 0x5A5A5A or 0x3C3C3C, 0xE1E1E1, " " ) end -- Position screen.drawText( math.floor(statsX + (tape.size == 0 and 0 or tapePosition / tape.size) * trackWidth), statsY + 1, 0xE1E1E1, "│" ) -- Memory cues local cueY = statsY for i = 1, #tapeConfig.cues do screen.drawText( statsX + math.floor(tapeConfig.cues[i] / tape.size * trackWidth), cueY, i == tapeConfig.cueIndex and 0xE1E1E1 or 0xCC0000, "•" ) end -- Hot cues for name, position in pairs(tapeConfig.hotCues) do screen.drawText( statsX + math.floor(position / tape.size * trackWidth), cueY, 0x66FF40, "•" .. name ) end -- Current cue cueY = statsY + trackHeight - 1 screen.drawText( statsX + math.floor(tapeConfig.cue / tape.size * trackWidth), cueY, 0xFFB640, "•" ) end end -------------------------------- Power button ------------------------------------------------ powerButton = window:addChild(GUI.object(75, 2, 4, 2)) powerButton.pressed = false powerButton.draw = function() screen.drawText(powerButton.x, powerButton.y, 0x1E1E1E, powerButton.pressed and "⣠⣤⣄" or "⣸⣿⣇") end powerButton.eventHandler = function(workspace, powerButton, e1) if e1 == "touch" then powerButton.pressed = not powerButton.pressed if powerButton.pressed then jogIndex = 1 end workspace:draw() computer.beep(20, 0.01) -- Stopping playback if not powerButton.pressed then for i = 1, #tapes do component.invoke(tapes[i].address, "stop") end end end end -------------------------------- Round mini button ------------------------------------------------ local function roundMiniButtonDraw(button) local bg, fg = button.animationCurrentBackground, (powerButton.pressed or button.ignoresPower) and button.animationCurrentText or 0x0 -- Background screen.drawRectangle(button.x + 1, button.y + 1, button.width - 2, button.height - 2, bg, fg, " ") -- Upper screen.drawText(button.x + 1, button.y, bg, string.rep("⣀", button.width - 2)) -- Left screen.drawText(button.x, button.y + 1, bg, "⢸") -- Middle screen.drawText(math.floor(button.x + button.width / 2 - #button.text / 2), button.y + 1, fg, button.text) -- Right screen.drawText(button.x + button.width - 1, button.y + 1, bg, "⡇") -- Lower screen.drawText(button.x + 1, button.y + button.height - 1, bg, string.rep("⠉", button.width - 2)) end local function roundMiniButtonEventHandler(workspace, button, e1, e2, e3, e4, e5) if e1 == "touch" then button:press() end end local function newRoundMiniButton(x, y, ...) local button = GUI.button(x, y, 4, 3, ...) button.draw = roundMiniButtonDraw button.eventHandler = roundMiniButtonEventHandler return button end local function roundTinyButtonDraw(button) local bg, fg = button.animationCurrentBackground, (powerButton.pressed or button.ignoresPower) and button.animationCurrentText or 0x0 -- Left screen.drawText(button.x, button.y, bg, "⢰") -- Middle screen.drawRectangle(button.x + 1, button.y, 2, 1, bg, fg, " ") screen.drawText(button.x + 1, button.y, fg, button.text) -- Right screen.drawText(button.x + 3, button.y, bg, "⡆") -- Lower screen.drawText(button.x, button.y + 1, bg, "⠈⠛⠛⠁") end local function newRoundTinyButton(x, y, ...) local button = GUI.button(x, y, 4, 2, ...) button.draw = roundTinyButtonDraw button.eventHandler = roundMiniButtonEventHandler return button end -------------------------------- ImageButton ------------------------------------------------ local function imageButtonDraw(button) screen.drawImage(button.x, button.y, (powerButton.pressed and (not button.blinking or blinkState)) and button.imageOn or button.imageOff) end local function newImageButton(x, y, width, height, name) local button = GUI.object(x, y, width, height) button.imageOn = loadImage(name .. "On") button.imageOff = loadImage(name .. "Off") button.draw = imageButtonDraw return button end -------------------------------- Tempo slider ------------------------------------------------ local tempoSlider = window:addChild(GUI.object(71, 33, 5, 15)) local tempoSliderImage = loadImage("TempoSlider") tempoSlider.value = config.tempo tempoSlider.draw = function(tempoSlider) screen.drawImage( tempoSlider.x, tempoSlider.y + math.floor(tempoSlider.value * tempoSlider.height) - math.floor(tempoSlider.value * 3), tempoSliderImage ) end tempoSlider.eventHandler = function(workspace, tempoSlider, e1, e2, e3, e4) if (e1 == "touch" or e1 == "drag") then if e4 == tempoSlider.y + tempoSlider.height - 1 then tempoSlider.value = 1 elseif e4 == math.floor(tempoSlider.y + tempoSlider.height / 2) then tempoSlider.value = 0.5 else tempoSlider.value = (e4 - tempoSlider.y) / tempoSlider.height end config.tempo = tempoSlider.value workspace:draw() updateTempoForAll() delaySaveConfig() end end -------------------------------- Display buttons ------------------------------------------------ local function displayButtonDraw(button) local bg, fg = button.animationCurrentBackground, (powerButton.pressed or button.ignoresPower) and button.animationCurrentText or 0x4B4B4B -- Background screen.drawRectangle(button.x + 1, button.y + 1, button.width - 2, button.height - 2, bg, fg, " ") -- Upper screen.drawText(button.x, button.y, fg, "⢀" .. string.rep("⣀", button.width - 2) .. "⡀") -- Left screen.drawText(button.x, button.y + 1, fg, "⢸") -- Middle screen.drawText(math.floor(button.x + button.width / 2 - unicode.len(button.text) / 2), button.y + 1, fg, button.text) -- Right screen.drawText(button.x + button.width - 1, button.y + 1, fg, "⡇") -- Lower screen.drawText(button.x, button.y + button.height - 1, fg, "⠈" .. string.rep("⠉", button.width - 2) .. "⠁") end local function newDisplayButton(x, y, width, ...) local button = GUI.button(x, y, width, 3, ...) button.pressed = false button.draw = displayButtonDraw return button end local helpButton = window:addChild(newDisplayButton(14, 1, 7, 0x0F0F0F, 0xF0F0F0, 0x0, 0xA5A5A5, "Help")) helpButton.onTouch = function() if not powerButton.pressed then return end local container = GUI.addBackgroundContainer(workspace, true, true, "Help") container.layout:removeChildren() local lines = { "Pioneer CDJ-2000 nexus", " ", "Pro-grade digital DJ deck for Computronics", "tape drives and DFPWM audio codec.", "To convert your favorite tracks, use", "https://music.madefor.cc", " ", "Designed by Pioneer Corporation in Japan", " ", "Developed and adapted for MineOS by", "Igor Timofeev, vk.com/id7799889", "Maxim Afonin, vk.com/id258714476" } local textBox = container.layout:addChild(GUI.textBox(1, 1, container.layout.width, #lines, nil, 0xB4B4B4, lines, 1, 0, 0)) textBox:setAlignment(GUI.ALIGNMENT_HORIZONTAL_CENTER, GUI.ALIGNMENT_VERTICAL_TOP) textBox.eventHandler = container.panel.eventHandler workspace:draw() end local closeButton = window:addChild(newDisplayButton(14, 4, 7, 0x0F0F0F, 0x4B4B4B, 0x0, 0x3349FF, "Close")) closeButton.onTouch = function() window:remove() end local function checkForFreeSpace(requiredSize) if requiredSize > tape.size - invoke("getPosition") then GUI.alert("Not enough space on tape") return false end return true end local wipeButton = window:addChild(newDisplayButton(14, 7, 7, 0x0F0F0F, 0x4B4B4B, 0x0, 0xFFDB40, "Wipe")) local fileButton = window:addChild(newDisplayButton(23, 1, 10, 0x0F0F0F, 0x4B4B4B, 0x0, 0xFFDB40, "File")) fileButton.onTouch = function() if not tape or not powerButton.pressed then return end local filesystemDialog = GUI.addFilesystemDialog(workspace, true, 50, math.floor(window.height * 0.8), "Confirm", "Cancel", "File name", "/") filesystemDialog:setMode(GUI.IO_MODE_OPEN, GUI.IO_MODE_FILE) filesystemDialog:addExtensionFilter(".dfpwm") filesystemDialog:expandPath(paths.user.desktop) filesystemDialog:show() filesystemDialog.onSubmit = function(path) local fileSize = filesystem.size(path) if not checkForFreeSpace(fileSize) then return end local container = GUI.addBackgroundContainer(workspace, true, true, "Writing track via file") local progressBar = container.layout:addChild(GUI.progressBar(1, 1, 36, 0x66DB80, 0x0, 0xE1E1E1, 0, true, true, "", "%")) local oldPosition = invoke("getPosition") invoke("stop") workspace:draw() local file, reason = filesystem.open(path, "rb") if file then local bytesWritten, chunk = 0 while true do chunk = file:read(IOBufferSize) if not chunk then break end if not invoke("isReady") then GUI.alert("Tape was removed during writing") break end invoke("write", chunk) bytesWritten = bytesWritten + #chunk progressBar.value = math.floor(bytesWritten / fileSize * 100) workspace:draw() end file:close() else GUI.alert(reason or "Unable to open file for writing") end setPosition(oldPosition) container:remove() workspace:draw() end end local urlButton = window:addChild(newDisplayButton(34, 1, 10, 0x0F0F0F, 0x4B4B4B, 0x0, 0xFFDB40, "Url")) urlButton.onTouch = function() if not tape or not powerButton.pressed or not component.isAvailable("internet") then return end local container = GUI.addBackgroundContainer(workspace, true, true, "Downloading track via URL") local input = container.layout:addChild(GUI.input(1, 1, 36, 3, 0xE1E1E1, 0x696969, 0x969696, 0xE1E1E1, 0x2D2D2D, "", "https://example.com/track.dfpwm", false)) container.panel.eventHandler = function(workspace, panel, e1) if e1 == "touch" then if #input.text > 0 then input:remove() container.label.text = "Downloading track" local progressBar = container.layout:addChild(GUI.progressBar(1, 1, 36, 0x66DB80, 0x0, 0xE1E1E1, 0, true, true, "", "%")) workspace:draw() invoke("stop") local handle, reason = component.get("internet").request( input.text, nil, { ["User-Agent"] = "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_13_3) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/64.0.3282.119 Safari/537.36", ["Content-Type"] = "application/x-www-form-urlencoded" }, "GET" ) if handle then local deadline, code, message, headers = computer.uptime() + 5 repeat code, message, headers = handle:response() until headers or computer.uptime() >= deadline if headers then if headers["Content-Length"] then local fileSize = tonumber(headers["Content-Length"][1]) local oldPosition = invoke("getPosition") if checkForFreeSpace(fileSize) then local bytesWritten, chunk, reason = 0 while true do chunk, reason = handle.read(IOBufferSize) if not chunk then if reason then GUI.alert(reason) end break end if not invoke("isReady") then GUI.alert("Tape was removed during writing") break end invoke("write", chunk) bytesWritten = bytesWritten + #chunk progressBar.value = math.floor(bytesWritten / fileSize * 100) workspace:draw() end setPosition(oldPosition) end else GUI.alert("Web-server didn't respont with Content-Length header") end else GUI.alert("Too long without response") end handle:close() else GUI.alert(reason or "Invalid URL-address") end end container:remove() workspace:draw() end end end local labelButton = window:addChild(newDisplayButton(45, 1, 10, 0x0F0F0F, 0x4B4B4B, 0x0, 0xFFDB40, "Label")) labelButton.onTouch = function() if not tape or not powerButton.pressed then return end local container = GUI.addBackgroundContainer(workspace, true, true, "Change tape label") local input = container.layout:addChild(GUI.input(1, 1, 36, 3, 0xE1E1E1, 0x696969, 0x969696, 0xE1E1E1, 0x2D2D2D, invoke("getLabel") or "", "The Algortithm - Superscalar", false)) input.onInputFinished = function() invoke("setLabel", input.text) workspace:draw() end container.panel.onTouch = function() container:remove() workspace:draw() end workspace:draw() end -------------------------------- Quantize/time buttons ------------------------------------------------ local _ = window:addChild(newRoundTinyButton(14, 12, 0x0F0F0F, 0xFF0000, 0x0, 0x440000, "⢠⡄")) local timeModeButton = window:addChild(newRoundTinyButton(18, 12, 0x0F0F0F, 0x1E1E1E, 0x0, 0x0F0F0F, "⢠⡄")) timeModeButton.onTouch = function() if not tape or not powerButton.pressed then return end config.timeMode = config.timeMode + 1 if config.timeMode > 2 then config.timeMode = 0 end workspace:draw() delaySaveConfig() end -------------------------------- Needle search ------------------------------------------------ local needleSearch = window:addChild(GUI.object(25, 15, 29, 2)) needleSearch.draw = function() screen.drawText(needleSearch.x, needleSearch.y, powerButton.pressed and 0xE1E1E1 or 0x0, "▲ ╷ ╷ ╷ ╷ ╷ ╷ ╷ ╷ ╷ ╷ ╷ ╷ ╷ ▲") end needleSearch.eventHandler = function(workspace, needleSearch, e1, e2, e3, e4) if (e1 == "touch" or e1 == "drag") and powerButton.pressed and tape then setPosition(math.floor((e3 - needleSearch.x) / (needleSearch.width - 1) * tape.size)) workspace:draw() end end -------------------------------- Jog ------------------------------------------------ local jog = window:addChild(GUI.object(15, 20, 52, 26)) local jogAngleOld jog.eventHandler = function(workspace, jog, e1, e2, e3, e4, e5, ...) if not tape or not powerButton.pressed then return end local function jogNavigate(direction) incrementJogIndex(direction) invalidateJogIncrementUptime(computer.uptime()) navigate(direction) workspace:draw() end if e1 == "drag" then local angleNew = math.atan2( e3 - jog.x - jog.width / 2, e4 - jog.y - jog.height / 2 ) if jogAngleOld then jogNavigate(jogAngleOld - angleNew >= 0 and 1 or -1) end jogAngleOld = angleNew elseif e1 == "drop" then jogAngleOld = nil elseif e1 == "scroll" then jogNavigate(e5 >= 0 and 1 or -1) end end -------------------------------- Knobs ------------------------------------------------ local function knobDraw(knob) screen.drawImage(knob.x, knob.y, knobImages[1 + math.floor(knob.value * (#knobImages - 1))]) end local function knobEventHandler(workspace, knob, e1, e2, e3, e4, e5, ...) local function increment(upper) local tempo = 0.1 local newValue = math.max(0, math.min(1, knob.value + (upper and tempo or -tempo))) if newValue == knob.value then return end knob.value = newValue workspace:draw() if knob.onValueChanged then knob.onValueChanged(knob) end end if e1 == "touch" then knob.touchX, knob.touchY = e3, e4 elseif e1 == "drag" and knob.touchX then local dx, dy = e3 - knob.touchX, e4 - knob.touchY knob.touchX, knob.touchY = e3, e4 increment((math.abs(dx) > math.abs(dy) and dx or dy) >= 0) elseif e1 == "drop" then knob.touchX, knob.touchY = nil, nil elseif e1 == "scroll" then increment(e5 >= 0) end end local function newKnob(x, y, value) local knob = GUI.object(x, y, 5, 3) knob.value = value knob.draw = knobDraw knob.eventHandler = knobEventHandler return knob end -- Jog resistance local jogResistanceKnob = window:addChild(newKnob(61, 21, config.jogResistance)) jogResistanceKnob.onValueChanged = function() config.jogResistance = jogResistanceKnob.value delaySaveConfig() end -- Gain gainKnob = window:addChild(newKnob(72, 12, config.gain)) gainKnob.onValueChanged = function() config.gain = gainKnob.value updateGainForAll() delaySaveConfig() end -- Navigation sensivity local navigationSensivityKnob = window:addChild(newKnob(72, 16, config.navigationSensivity)) navigationSensivityKnob.onValueChanged = function() config.navigationSensivity = navigationSensivityKnob.value delaySaveConfig() end -------------------------------- Pref/next tape button ------------------------------------------------ local tapePrevButton = window:addChild(newRoundMiniButton(2, 30, 0x2D2D2D, 0xFFB600, 0x0F0F0F, 0xCC9200, "<<")) local tapeNextButton = window:addChild(newRoundMiniButton(7, 30, 0x2D2D2D, 0xFFB600, 0x0F0F0F, 0xCC9200, ">>")) local function incrementTape(next) if not tape or not powerButton.pressed then return end tapeIndex = tapeIndex + (next and 1 or -1) if tapeIndex > #tapes then tapeIndex = 1 elseif tapeIndex < 1 then tapeIndex = #tapes end updateTape() workspace:draw() delaySaveConfig() end tapePrevButton.onTouch = function() incrementTape(false) end tapeNextButton.onTouch = function() incrementTape(true) end -------------------------------- Pref/next search button ------------------------------------------------ local searchPrevButton = window:addChild(newRoundMiniButton(2, 34, 0x2D2D2D, 0xFFB600, 0x0F0F0F, 0xCC9200, "<<")) local searchNextButton = window:addChild(newRoundMiniButton(7, 34, 0x2D2D2D, 0xFFB600, 0x0F0F0F, 0xCC9200, ">>")) local function incrementFromSearchButton(direction) if not tape or not powerButton.pressed then return end navigate(direction) workspace:draw() end searchPrevButton.onTouch = function() incrementFromSearchButton(-1) end searchNextButton.onTouch = function() incrementFromSearchButton(1) end -------------------------------- Hot cue buttons ------------------------------------------------ local hotCueRecCallButton local function hotCueButtonDraw(button) local bg = button.animationCurrentBackground local fg = (powerButton.pressed and tape and (hotCueRecCallButton.rec or tapeConfig.hotCues[button.text])) and button.animationCurrentText or 0x2D2D2D -- Upper screen.drawText(button.x, button.y, bg, "⢀" .. string.rep("⣀", button.width - 2) .. "⡀") -- Left screen.drawText(button.x, button.y + 1, bg, "⢸") -- Middle screen.set(button.x + 1, button.y + 1, 0x2D2D2D, 0x5A5A5A, "⣤") screen.set(button.x + 2, button.y + 1, bg, 0x787878, "⠤") screen.set(button.x + 3, button.y + 1, bg, fg, button.text) screen.set(button.x + 4, button.y + 1, bg, 0x787878, "⠒") screen.set(button.x + 5, button.y + 1, 0x2D2D2D, 0x5A5A5A, "⠛") -- Right screen.drawText(button.x + button.width - 1, button.y + 1, bg, "⡇") -- Lower screen.drawText(button.x, button.y + button.height - 1, bg, "⠈" .. string.rep("⠉", button.width - 2) .. "⠁") end local hotCueButtons = {} local function newHotCueButton(x, y, index, text) local button = GUI.button(x, y, 7, 3, 0x1E1E1E, 0x66FF40, 0x0, 0x336D00, text) button.draw = hotCueButtonDraw button.onTouch = function() if not tape or not powerButton.pressed then return end local hotCuePosition = tapeConfig.hotCues[button.text] if hotCueRecCallButton.rec then local position = invoke("getPosition") tapeConfig.hotCues[button.text] = (not hotCuePosition or hotCuePosition ~= position) and position or nil workspace:draw() delaySaveConfig() elseif hotCuePosition then setPosition(hotCuePosition) workspace:draw() end end table.insert(hotCueButtons, button) return button end -- local hotCueButtonA = window:addChild(newHotCueButton(3, 13, 0x66FF40, 0x336D00, "A")) -- local hotCueButtonB = window:addChild(newHotCueButton(3, 16, 0xFFB600, 0x664900, "B")) -- local hotCueButtonB = window:addChild(newHotCueButton(3, 19, 0xFF2440, 0x660000, "C")) local hotCueButtonA = window:addChild(newHotCueButton(3, 13, 1, "A")) local hotCueButtonB = window:addChild(newHotCueButton(3, 16, 2, "B")) local hotCueButtonB = window:addChild(newHotCueButton(3, 19, 3, "C")) hotCueRecCallButton = window:addChild(GUI.button(3, 23, 7, 3, 0x1E1E1E, 0x1E1E1E, 0x0, 0x0, "⠶")) hotCueRecCallButton.rec = false hotCueRecCallButton.draw = hotCueButtonDraw hotCueRecCallButton.onTouch = function() hotCueRecCallButton.rec = not hotCueRecCallButton.rec -- Updating buttons color scheme local button for i = 1, #hotCueButtons do button = hotCueButtons[i] button.colors.default.text = hotCueRecCallButton.rec and 0xFF2440 or 0x66FF40 button.colors.pressed.text = hotCueRecCallButton.rec and 0x660000 or 0x336D00 button.animationCurrentText = button.colors.default.text end workspace:draw() end -------------------------------- Loop buttons ------------------------------------------------ local function loopButtonDraw(button) local border, color1, color2, color3, color4 if powerButton.pressed and (not button.blinking or blinkState) then if button.pressed then border, color1, color2, color3, color4 = 0x332400, 0x996D00, 0x996D00, 0x996D00, 0x996D00 else border, color1, color2, color3, color4 = 0x332400, 0xFFDB80, 0xFFDB40, 0xFFB680, 0xFFB640 end else border, color1, color2, color3, color4 = 0x0F0F0F, 0x332400, 0x332400, 0x332400, 0x332400 end -- 1 screen.drawText(button.x, button.y, border, "⢰") screen.set(button.x + 1, button.y, color1, border, "⠉") screen.set(button.x + 2, button.y, color2, border, "⠉") screen.set(button.x + 3, button.y, color3, border, "⠉") screen.drawText(button.x + 4, button.y, border, "⡆") -- 2 screen.drawText(button.x, button.y + 1, border, "⠸") screen.set(button.x + 1, button.y + 1, color4, border, "⣀") screen.set(button.x + 2, button.y + 1, color4, border, "⣀") screen.set(button.x + 3, button.y + 1, color3, border, "⣀") screen.drawText(button.x + 4, button.y + 1, border, "⠇") end local function loopButtonEventHandler(workspace, button, e1, e2, e3, e4, e5) if e1 == "touch" then button.pressed = true workspace:draw() event.sleep(0.2) button.pressed = false workspace:draw() if button.onTouch then button.onTouch() end end end local function newLoopButton(x, y) local button = GUI.object(x, y, 5, 2) button.pressed = false button.draw = loopButtonDraw button.eventHandler = loopButtonEventHandler return button end loopInButton = window:addChild(newLoopButton(13, 18)) loopOutButton = window:addChild(newLoopButton(19, 18)) local reloopButton = window:addChild(newRoundTinyButton(26, 18, 0x2D2D2D, 0xFFB640, 0x1E1E1E, 0x996D00, "⢠⡄")) loopInButton.onTouch = function() if not tape or not powerButton.pressed then return end local position = invoke("getPosition") tapeConfig.loopIn = position ~= tapeConfig.loopIn and position or nil if tapeConfig.loopIn then tapeConfig.loopOn = true if tapeConfig.loopOut and tapeConfig.loopIn >= tapeConfig.loopOut then tapeConfig.loopOut = nil end else tapeConfig.loopOn = tapeConfig.loopOut ~= nil end updateLoop() workspace:draw() delaySaveConfig() end loopOutButton.onTouch = function() if not tape or not powerButton.pressed then return end local position = invoke("getPosition") tapeConfig.loopOut = position ~= tapeConfig.loopOut and position or nil if tapeConfig.loopOut then tapeConfig.loopOn = true if tapeConfig.loopIn and tapeConfig.loopIn >= tapeConfig.loopOut then tapeConfig.loopIn = nil end else tapeConfig.loopOn = tapeConfig.loopIn ~= nil end updateLoop() workspace:draw() delaySaveConfig() end reloopButton.onTouch = function() if not tape or not powerButton.pressed or (not tapeConfig.loopIn and not tapeConfig.loopOut) then return end tapeConfig.loopOn = not tapeConfig.loopOn updateLoop() workspace:draw() if tapeConfig.loopOn then setPosition(tapeConfig.loopIn or 0) end delaySaveConfig() end -------------------------------- Cue memory buttons ------------------------------------------------ local function incrementCueIndex(value) if not tape or not powerButton.pressed then return end if #tapeConfig.cues == 0 then tapeConfig.cueIndex = 1 return end tapeConfig.cueIndex = tapeConfig.cueIndex + value if tapeConfig.cueIndex < 1 then tapeConfig.cueIndex = #tapeConfig.cues elseif tapeConfig.cueIndex > #tapeConfig.cues then tapeConfig.cueIndex = 1 end tapeConfig.cue = tapeConfig.cues[tapeConfig.cueIndex] setPosition(tapeConfig.cue) workspace:draw() delaySaveConfig() end local cuePrevButton = window:addChild(newRoundTinyButton(50, 18, 0x0F0F0F, 0xFFB640, 0x0, 0x996D00, "⢔ ")) cuePrevButton.onTouch = function() incrementCueIndex(-1) end local cueNextButton = window:addChild(newRoundTinyButton(54, 18, 0x0F0F0F, 0xFFB640, 0x0, 0x996D00, " ⡢")) cueNextButton.onTouch = function() incrementCueIndex(1) end local cueDelButton = window:addChild(newRoundTinyButton(59, 18, 0x0F0F0F, 0x4B4B4B, 0x0, 0x2D2D2D, " ")) cueDelButton.onTouch = function() if not tape or not powerButton.pressed or #tapeConfig.cues == 0 then return end table.remove(tapeConfig.cues, tapeConfig.cueIndex) if tapeConfig.cueIndex > #tapeConfig.cues then tapeConfig.cueIndex = math.max(tapeConfig.cueIndex - 1, 1) end delaySaveConfig() end local cueMemButton = window:addChild(newRoundTinyButton(63, 18, 0x0F0F0F, 0x4B4B4B, 0x0, 0x2D2D2D, " ")) cueMemButton.onTouch = function() if not tape or not powerButton.pressed then return end local cue for i = 1, #tapeConfig.cues do cue = tapeConfig.cues[i] if cue == tapeConfig.cue then return end end table.insert(tapeConfig.cues, tapeConfig.cue) table.sort(tapeConfig.cues) delaySaveConfig() end -------------------------------- Cue / play buttons ------------------------------------------------ local cueButton = window:addChild(newImageButton(2, window.height - 11, 9, 5, "Cue")) local playButton = window:addChild(newImageButton(2, window.height - 5, 9, 5, "Play")) playButton.blinking = true cueButton.eventHandler = function(workspace, cueButton, e1) if not tape or not powerButton.pressed then return end if e1 == "touch" then cueButton.touchUptime = computer.uptime() + 10 if playButton.blinking then tapeConfig.cue = invoke("getPosition") cueButton.blinking = false workspace:draw() delaySaveConfig() else setPosition(tapeConfig.cue) workspace:draw() end end end playButton.eventHandler = function(workspace, playButton, e1) if e1 == "touch" and tape and powerButton.pressed then playButton.blinking = not playButton.blinking if not playButton.blinking then cueButton.blinking = false end invoke(playButton.blinking and "stop" or "play") workspace:draw() end end -------------------------------- Jog mode ------------------------------------------------ local jogModeDisplay = window:addChild(GUI.object(71, 20, 3, 2)) jogModeDisplay.draw = function(jogModeDisplay) local vinyl = (powerButton.pressed and not config.jogModeCdj) and 0x00B6FF or 0x002440 local cdj = (powerButton.pressed and config.jogModeCdj) and 0xFFFF40 or 0x332400 screen.drawText(jogModeDisplay.x, jogModeDisplay.y, vinyl, "⢀⣀⡀") screen.drawRectangle(jogModeDisplay.x, jogModeDisplay.y + 1, 3, 1, vinyl, cdj, "⣤") screen.drawText(jogModeDisplay.x, jogModeDisplay.y + 2, cdj, "⠈⠉⠁") end local jogModeButton = window:addChild(newRoundMiniButton(74, 20, 0x2D2D2D, 0x696969, 0x1E1E1E, 0x3C3C3C, "JM")) jogModeButton.ignoresPower = true jogModeButton.onTouch = function() if not tape or not powerButton.pressed then return end config.jogModeCdj = not config.jogModeCdj workspace:draw() delaySaveConfig() end -------------------------------- Right beat buttons ------------------------------------------------ local beatSyncButton = window:addChild(newRoundMiniButton(70, 24, 0xB4B4B4, 0x0F0F0F, 0x787878, 0x0F0F0F, "Sy")) beatSyncButton.ignoresPower = true local beatSyncMasterButton = window:addChild(newRoundMiniButton(74, 24, 0xB4B4B4, 0x0F0F0F, 0x787878, 0x0F0F0F, "Ms")) beatSyncMasterButton.ignoresPower = true -------------------------------- Right tempo buttons ------------------------------------------------ local tempoButton = window:addChild(newRoundTinyButton(72, 28, 0x0F0F0F, 0x2D2D2D, 0x0, 0xFF2440, " ")) local masterTempoButton = window:addChild(newRoundTinyButton(72, 31, 0x0F0F0F, 0x2D2D2D, 0x0F0F0F, 0xFF0000, "⢠⡄")) masterTempoButton.switchMode = true masterTempoButton:press() tempoButton.onTouch = function() config.tempoIndex = config.tempoIndex + 1 if config.tempoIndex > 3 then config.tempoIndex = 1 end workspace:draw() updateTempoForAll() delaySaveConfig() end -------------------------------- Cyka ------------------------------------------------ local overrideWindowEventHandler = window.eventHandler window.eventHandler = function(workspace, window, e1, e2, e3, ...) if (e1 == "component_added" or e1 == "component_removed") and e3 == "tape_drive" then updateTapes() else overrideWindowEventHandler(workspace, window, e1, e2, e3, ...) if e1 then return end local uptime = computer.uptime() if tape and powerButton.pressed then updateTapePosition() local shouldDraw = false local isPlaying = invoke("getState") == "PLAYING" -- Cheching if play button state was changed if isPlaying == playButton.blinking then playButton.blinking = not playButton.blinking invalidateJogIncrementUptime(uptime) shouldDraw = true end -- Cue button local cueButtonBlinking = playButton.blinking and tapeConfig.cue ~= tapePosition if cueButtonBlinking ~= cueButton.blinking then cueButton.blinking = cueButtonBlinking shouldDraw = true end -- Jog if isPlaying and uptime > jogIncrementUptime then incrementJogIndex(1) invalidateJogIncrementUptime(uptime) shouldDraw = true end -- Blink state if uptime > blinkUptime then blinkUptime = uptime + blinkInterval blinkState = not blinkState shouldDraw = true end -- Loop if isPlaying and tapeConfig.loopOn then if tapePosition >= (tapeConfig.loopOut or tape.size) then setPosition(tapeConfig.loopIn or 0) shouldDraw = true end end if shouldDraw then workspace:draw() end end -- If THE TIME HAS COME - saving config file if saveConfigUptime and uptime >= saveConfigUptime then saveConfigUptime = nil filesystem.writeTable(configPath, config) end end end updateTapes() workspace:draw()