--[[ Auto Piano Scanner v4 - "A" button (right side) -> show / hide the music panel - Scanner box keeps scanning + playing even while the panel is hidden - "BOX" button in the header -> show / hide the scanner box visuals only - Small red stop button appears next to "A" while a song is playing - Play presses the detected keys using TOUCH input ]] local Players = game:GetService("Players") local UIS = game:GetService("UserInputService") local GuiService = game:GetService("GuiService") local HttpService = game:GetService("HttpService") local TweenService = game:GetService("TweenService") local VIM = game:GetService("VirtualInputManager") local lp = Players.LocalPlayer local playerGui = lp:WaitForChild("PlayerGui") local httpRequest = request or http_request or (syn and syn.request) or (http and http.request) -- remove the previous instance if the script is executed again pcall(function() if getgenv and getgenv().AutoPianoGui then getgenv().AutoPianoGui:Destroy() end end) --------------------------------------------------------------------- -- MIDI parser -> list of {time = seconds, n = note number} --------------------------------------------------------------------- local function parseMidi(s) assert(s:sub(1, 4) == "MThd", "not a MIDI file") local hlen = string.unpack(">I4", s, 5) local _, _, division = string.unpack(">I2I2I2", s, 9) local pos = 9 + hlen local notes, tempos = {}, {} local function vlq() local v = 0 repeat local b = s:byte(pos) pos = pos + 1 v = v * 128 + (b % 128) until b < 128 return v end while pos + 7 <= #s do local id = s:sub(pos, pos + 3) local len = string.unpack(">I4", s, pos + 4) pos = pos + 8 local stop = pos + len if id == "MTrk" then local t, run = 0, nil while pos < stop do t = t + vlq() local st = s:byte(pos) if st >= 0x80 then run = st pos = pos + 1 else st = run end if st == 0xFF then local typ = s:byte(pos) pos = pos + 1 local l = vlq() if typ == 0x51 and l >= 3 then local a, b, c = s:byte(pos, pos + 2) tempos[#tempos + 1] = { t = t, us = a * 65536 + b * 256 + c } end pos = pos + l elseif st == 0xF0 or st == 0xF7 then pos = pos + vlq() else local hi = st - (st % 16) if hi == 0xC0 or hi == 0xD0 then pos = pos + 1 else local a, b = s:byte(pos, pos + 1) pos = pos + 2 -- note-on with velocity > 0, skip channel 10 (drums) if hi == 0x90 and b > 0 and (st % 16) ~= 9 then notes[#notes + 1] = { t = t, n = a } end end end end end pos = stop end table.sort(tempos, function(a, b) return a.t < b.t end) table.sort(notes, function(a, b) return a.t < b.t end) local ti, curTempo, lastTick, sec = 1, 500000, 0, 0 for _, n in ipairs(notes) do while tempos[ti] and tempos[ti].t <= n.t do sec = sec + (tempos[ti].t - lastTick) * curTempo / 1e6 / division lastTick = tempos[ti].t curTempo = tempos[ti].us ti = ti + 1 end sec = sec + (n.t - lastTick) * curTempo / 1e6 / division lastTick = n.t n.time = sec end return notes end --------------------------------------------------------------------- -- State --------------------------------------------------------------------- local panelOpen = false -- is the music panel visible? local boxOn = true -- is the scanner box visible? (scanning works either way) local scanActive = false -- becomes true after the first open, then scanning never stops local playing = false local detected = {} local conns = {} --------------------------------------------------------------------- -- Pressing keys: pure TOUCH input so Roblox keeps treating you as a touch device --------------------------------------------------------------------- local function singleChar(s) if type(s) == "string" and #s == 1 and s:match("^[%w!@#%$%%%^&%*%(%)]$") then return s end return nil end local function rawCenter(o) local sg = o:FindFirstAncestorOfClass("ScreenGui") local c = o.AbsolutePosition + o.AbsoluteSize / 2 if sg and not sg.IgnoreGuiInset then c = c + GuiService:GetGuiInset() end return c end local flash = function() end -- assigned after the GUI is built local touchOk = pcall(function() return VIM.SendTouchEvent end) local touchId = 0 local function tapKey(obj) if not obj.Parent then return end local c = rawCenter(obj) if touchOk then touchId = touchId % 200 + 1 local id = touchId local ok = pcall(function() VIM:SendTouchEvent(id, 0, c.X, c.Y) end) if ok then task.wait(0.035) pcall(function() VIM:SendTouchEvent(id, 2, c.X, c.Y) end) return end touchOk = false end -- fallback (executor without SendTouchEvent): mouse click VIM:SendMouseMoveEvent(c.X, c.Y, game) VIM:SendMouseButtonEvent(c.X, c.Y, 0, true, game, 0) task.wait(0.03) VIM:SendMouseButtonEvent(c.X, c.Y, 0, false, game, 0) end local function pressMany(keys) for _, k in ipairs(keys) do task.spawn(tapKey, k.obj) flash(k.i) end end --------------------------------------------------------------------- -- Note mapping: MIDI note -> detected key -- Chromatic: every key = 1 semitone | Diatonic: Do Re Mi Fa Sol La Si --------------------------------------------------------------------- local majorDegree = { [0] = 0, [1] = 0, [2] = 1, [3] = 1, [4] = 2, [5] = 3, [6] = 3, [7] = 4, [8] = 4, [9] = 5, [10] = 5, [11] = 6 } local inMajor = { [0] = true, [2] = true, [4] = true, [5] = true, [7] = true, [9] = true, [11] = true } local function detectTonic(notes) local hist = {} for i = 0, 11 do hist[i] = 0 end for _, n in ipairs(notes) do local pc = n.n % 12 hist[pc] = hist[pc] + 1 end local best, bestScore, bestTie = 0, -1, -1 for t = 0, 11 do local score = 0 for pc = 0, 11 do if inMajor[(pc - t) % 12] then score = score + hist[pc] end end local tie = hist[t] + hist[(t + 7) % 12] if score > bestScore or (score == bestScore and tie > bestTie) then best, bestScore, bestTie = t, score, tie end end return best end local function hasBlackKeys(keys) local minH, maxH, minY, maxY = math.huge, 0, math.huge, -math.huge for _, k in ipairs(keys) do minH = math.min(minH, k.h) maxH = math.max(maxH, k.h) minY = math.min(minY, k.y) maxY = math.max(maxY, k.y) end return (maxH / math.max(minH, 1) >= 1.25) or (maxY - minY > 0.15 * maxH) end local function buildMapper(notes, N, diatonic) local tonic = diatonic and detectTonic(notes) or 0 local unit = diatonic and 7 or 12 local function toVal(note) if diatonic then local d = note - tonic return math.floor(d / 12) * 7 + majorDegree[d % 12] end return note end local counts, vals = {}, {} local minV, maxV = math.huge, -math.huge for _, n in ipairs(notes) do local v = toVal(n.n) counts[v] = (counts[v] or 0) + 1 vals[#vals + 1] = v if v < minV then minV = v end if v > maxV then maxV = v end end table.sort(vals) local med = vals[math.ceil(#vals / 2)] or 0 -- pick the octave window that cuts off the fewest notes of the song local bestBase, bestOut, bestDist = 0, math.huge, math.huge for base = (math.floor(minV / unit) - 1) * unit, maxV, unit do local out = 0 for v, c in pairs(counts) do if v < base or v > base + N - 1 then out = out + c end end local dist = math.abs(base + N / 2 - med) if out < bestOut or (out == bestOut and dist < bestDist) then bestBase, bestOut, bestDist = base, out, dist end end return function(note) local idx = toVal(note) - bestBase if N >= unit then while idx < 0 do idx = idx + unit end while idx >= N do idx = idx - unit end else idx = idx % N end return idx -- 0-based end end --------------------------------------------------------------------- -- Theme & GUI helpers --------------------------------------------------------------------- local WHITE = Color3.fromRGB(255, 255, 255) local C = { bg = Color3.fromRGB(16, 17, 24), card = Color3.fromRGB(27, 29, 40), card2 = Color3.fromRGB(40, 43, 58), line = Color3.fromRGB(62, 66, 88), text = Color3.fromRGB(236, 238, 246), muted = Color3.fromRGB(148, 153, 176), accent = Color3.fromRGB(94, 234, 212), accent2 = Color3.fromRGB(129, 140, 248), good = Color3.fromRGB(74, 222, 128), warn = Color3.fromRGB(250, 204, 21), bad = Color3.fromRGB(248, 113, 113), dark = Color3.fromRGB(12, 14, 20), } local FONT, FONT_M, FONT_B = Enum.Font.Gotham, Enum.Font.GothamMedium, Enum.Font.GothamBold local function new(class, props, parent) local i = Instance.new(class) for k, v in pairs(props) do i[k] = v end i.Parent = parent return i end local function corner(o, r) return new("UICorner", { CornerRadius = UDim.new(0, r or 8) }, o) end local function stroke(o, color, th, tr) return new("UIStroke", { Color = color, Thickness = th or 1, Transparency = tr or 0, ApplyStrokeMode = Enum.ApplyStrokeMode.Border, }, o) end local function gradient(o, c1, c2, rot) return new("UIGradient", { Color = ColorSequence.new(c1, c2), Rotation = rot or 0 }, o) end local function pad(o, l, t, r, b) new("UIPadding", { PaddingLeft = UDim.new(0, l), PaddingTop = UDim.new(0, t or l), PaddingRight = UDim.new(0, r or l), PaddingBottom = UDim.new(0, b or t or l), }, o) end local function tween(o, t, props) TweenService:Create(o, TweenInfo.new(t, Enum.EasingStyle.Quad, Enum.EasingDirection.Out), props):Play() end local function label(parent, props) props.BackgroundTransparency = 1 props.Font = props.Font or FONT props.TextColor3 = props.TextColor3 or C.text props.TextSize = props.TextSize or 13 return new("TextLabel", props, parent) end local function button(parent, props, base, over) props.AutoButtonColor = false props.BackgroundColor3 = base props.Font = props.Font or FONT_B props.TextColor3 = props.TextColor3 or C.text props.TextSize = props.TextSize or 13 local b = new("TextButton", props, parent) b.MouseEnter:Connect(function() tween(b, 0.12, { BackgroundColor3 = over }) end) b.MouseLeave:Connect(function() tween(b, 0.12, { BackgroundColor3 = base }) end) return b end --------------------------------------------------------------------- -- ScreenGui --------------------------------------------------------------------- local gui = new("ScreenGui", { Name = "AutoPianoScanner", ResetOnSpawn = false, IgnoreGuiInset = true, ZIndexBehavior = Enum.ZIndexBehavior.Sibling, DisplayOrder = 999, }) local okParent = pcall(function() gui.Parent = (gethui and gethui()) or game:GetService("CoreGui") end) if not okParent or not gui.Parent then gui.Parent = playerGui end pcall(function() if getgenv then getgenv().AutoPianoGui = gui end end) gui.Destroying:Connect(function() playing = false for _, c in ipairs(conns) do c:Disconnect() end end) local vp = workspace.CurrentCamera.ViewportSize --------------------------------------------------------------------- -- Scanner box --------------------------------------------------------------------- local box = new("Frame", { Name = "Scanner", Position = UDim2.fromOffset(vp.X / 2 - 150, vp.Y / 2 - 55), Size = UDim2.fromOffset(300, 110), BackgroundColor3 = C.accent, BackgroundTransparency = 0.85, Active = true, Visible = false, -- becomes visible on the first open, then stays "on" so scanning never stops ZIndex = 5, }, gui) corner(box, 10) local boxStroke = stroke(box, C.accent, 2) local chip = new("Frame", { AutomaticSize = Enum.AutomaticSize.X, Size = UDim2.fromOffset(0, 22), Position = UDim2.new(0, 0, 0, -26), BackgroundColor3 = C.dark, BackgroundTransparency = 0.1, ZIndex = 6, }, box) corner(chip, 11) stroke(chip, C.accent, 1, 0.4) pad(chip, 10, 0) local boxLabel = new("TextLabel", { AutomaticSize = Enum.AutomaticSize.X, Size = UDim2.fromOffset(0, 22), BackgroundTransparency = 1, Text = "SCAN · 0 keys", Font = FONT_B, TextSize = 12, TextColor3 = C.accent, ZIndex = 7, }, chip) local grip = new("TextButton", { Size = UDim2.fromOffset(30, 30), AnchorPoint = Vector2.new(1, 1), Position = UDim2.new(1, 4, 1, 4), BackgroundColor3 = C.accent, Text = "◢", Font = FONT_B, TextSize = 15, TextColor3 = C.dark, AutoButtonColor = false, ZIndex = 8, }, box) corner(grip, 15) stroke(grip, C.dark, 2, 0.3) -- markers on detected keys local markers = {} local function getMarker(i) local m = markers[i] if not m then m = new("Frame", { Size = UDim2.fromOffset(10, 10), AnchorPoint = Vector2.new(0.5, 0.5), BackgroundColor3 = C.accent, BorderSizePixel = 0, Visible = false, ZIndex = 9, }, gui) corner(m, 5) stroke(m, C.dark, 1.5, 0.2) markers[i] = m end return m end local function updateMarkers() local show = scanActive and boxOn for i, k in ipairs(detected) do local m = getMarker(i) m.Position = UDim2.fromOffset(k.x, k.y) m.Visible = show end for i = #detected + 1, #markers do markers[i].Visible = false end end flash = function(i) local m = markers[i] if not m then return end m.Size = UDim2.fromOffset(18, 18) m.BackgroundColor3 = WHITE tween(m, 0.22, { Size = UDim2.fromOffset(10, 10), BackgroundColor3 = C.accent }) end -- box look: visible / ghost (invisible but still scanning) / playing local function styleBox() local show = boxOn box.Active = show and not playing chip.Visible = show grip.Visible = show local bgT = (not show) and 1 or (playing and 0.95 or 0.85) tween(box, 0.2, { BackgroundTransparency = bgT }) tween(boxStroke, 0.2, { Color = playing and C.good or C.accent, Transparency = show and 0 or 1, }) end --------------------------------------------------------------------- -- Music panel --------------------------------------------------------------------- local PW = math.min(292, vp.X - 70) local PH = math.min(430, vp.Y - 24) local panel = new("Frame", { Name = "Panel", Position = UDim2.fromOffset(math.max(8, vp.X - 62 - PW), math.max(8, (vp.Y - PH) / 2)), Size = UDim2.fromOffset(PW, PH), BackgroundColor3 = C.bg, Active = true, Visible = false, ClipsDescendants = true, ZIndex = 10, }, gui) corner(panel, 14) stroke(panel, C.line, 1.5) local scale = new("UIScale", { Scale = 1 }, panel) -- header local header = new("Frame", { Size = UDim2.new(1, 0, 0, 46), BackgroundColor3 = WHITE, BorderSizePixel = 0, Active = true, }, panel) gradient(header, Color3.fromRGB(54, 58, 98), C.card, 0) label(header, { Position = UDim2.fromOffset(14, 6), Size = UDim2.new(1, -110, 0, 20), Text = "🎹 Auto Piano", Font = FONT_B, TextSize = 15, TextXAlignment = Enum.TextXAlignment.Left, }) label(header, { Position = UDim2.fromOffset(14, 25), Size = UDim2.new(1, -110, 0, 14), Text = "Drag here to move", TextSize = 11, TextColor3 = C.muted, TextXAlignment = Enum.TextXAlignment.Left, }) local hideBtn = new("TextButton", { AnchorPoint = Vector2.new(1, 0.5), Position = UDim2.new(1, -10, 0.5, 0), Size = UDim2.fromOffset(28, 28), BackgroundColor3 = C.card2, Text = "—", Font = FONT_B, TextSize = 14, TextColor3 = C.text, AutoButtonColor = false, }, header) corner(hideBtn, 14) hideBtn.MouseEnter:Connect(function() tween(hideBtn, 0.12, { BackgroundColor3 = C.line }) end) hideBtn.MouseLeave:Connect(function() tween(hideBtn, 0.12, { BackgroundColor3 = C.card2 }) end) local boxBtn = new("TextButton", { AnchorPoint = Vector2.new(1, 0.5), Position = UDim2.new(1, -44, 0.5, 0), Size = UDim2.fromOffset(46, 28), BackgroundColor3 = C.accent, Text = "BOX", Font = FONT_B, TextSize = 11, TextColor3 = C.dark, AutoButtonColor = false, }, header) corner(boxBtn, 14) -- body (scrollable on small screens) local body = new("ScrollingFrame", { Position = UDim2.fromOffset(0, 46), Size = UDim2.new(1, 0, 1, -46), BackgroundTransparency = 1, BorderSizePixel = 0, ScrollBarThickness = 3, ScrollBarImageColor3 = C.line, CanvasSize = UDim2.new(), AutomaticCanvasSize = Enum.AutomaticSize.Y, }, panel) pad(body, 10) new("UIListLayout", { Padding = UDim.new(0, 8), SortOrder = Enum.SortOrder.LayoutOrder }, body) local function section(text, order) return label(body, { Size = UDim2.new(1, 0, 0, 14), Text = text, Font = FONT_B, TextSize = 11, TextColor3 = C.muted, TextXAlignment = Enum.TextXAlignment.Left, LayoutOrder = order, }) end -- status local statusCard = new("Frame", { Size = UDim2.new(1, 0, 0, 40), BackgroundColor3 = C.card, LayoutOrder = 1 }, body) corner(statusCard, 10) local dot = new("Frame", { Size = UDim2.fromOffset(10, 10), Position = UDim2.new(0, 12, 0.5, -5), BackgroundColor3 = C.bad, }, statusCard) corner(dot, 5) local status = label(statusCard, { Position = UDim2.fromOffset(30, 0), Size = UDim2.new(1, -38, 1, 0), Text = "Move the box over the piano keys", TextSize = 12, TextWrapped = true, TextXAlignment = Enum.TextXAlignment.Left, }) local lockUntil = 0 local function setStatus(text, kind, lock) status.Text = text local col = ({ good = C.good, play = C.accent, warn = C.warn, bad = C.bad })[kind] or C.muted tween(dot, 0.15, { BackgroundColor3 = col }) if lock then lockUntil = os.clock() + lock end end -- mode (segmented control) section("MODE", 2) local seg = new("Frame", { Size = UDim2.new(1, 0, 0, 34), BackgroundColor3 = C.card, LayoutOrder = 3 }, body) corner(seg, 10) pad(seg, 3) new("UIListLayout", { FillDirection = Enum.FillDirection.Horizontal, Padding = UDim.new(0, 3), SortOrder = Enum.SortOrder.LayoutOrder, }, seg) local modeNames = { "Auto", "Chromatic", "Diatonic" } local modeBtns = {} local modeIdx = 1 local function refreshModes() for i, b in ipairs(modeBtns) do local on = (i == modeIdx) tween(b, 0.15, { BackgroundColor3 = on and C.accent or C.card, TextColor3 = on and C.dark or C.muted }) end end for i, n in ipairs(modeNames) do local b = new("TextButton", { Size = UDim2.new(1 / 3, -2, 1, 0), BackgroundColor3 = C.card, Text = n, Font = FONT_B, TextSize = 12, TextColor3 = C.muted, AutoButtonColor = false, LayoutOrder = i, }, seg) corner(b, 8) modeBtns[i] = b b.MouseButton1Click:Connect(function() modeIdx = i refreshModes() end) end refreshModes() -- speed section("SPEED", 4) local speedRow = new("Frame", { Size = UDim2.new(1, 0, 0, 36), BackgroundColor3 = C.card, LayoutOrder = 5 }, body) corner(speedRow, 10) local minus = button(speedRow, { Position = UDim2.fromOffset(4, 4), Size = UDim2.new(0, 44, 1, -8), Text = "−", TextSize = 18, }, C.card2, C.line) corner(minus, 8) local plus = button(speedRow, { AnchorPoint = Vector2.new(1, 0), Position = UDim2.new(1, -4, 0, 4), Size = UDim2.new(0, 44, 1, -8), Text = "+", TextSize = 18, }, C.card2, C.line) corner(plus, 8) local speedLabel = label(speedRow, { Position = UDim2.fromOffset(52, 0), Size = UDim2.new(1, -104, 1, 0), Text = "1.0x", Font = FONT_B, TextSize = 15, }) local speed = 1 local function setSpeed(v) speed = math.clamp(math.floor(v * 10 + 0.5) / 10, 0.1, 5) speedLabel.Text = string.format("%.1fx", speed) end minus.MouseButton1Click:Connect(function() setSpeed(speed - 0.1) end) plus.MouseButton1Click:Connect(function() setSpeed(speed + 0.1) end) -- search + song list local search = new("TextBox", { Size = UDim2.new(1, 0, 0, 32), BackgroundColor3 = C.card, PlaceholderText = "🔎 Search songs…", Text = "", ClearTextOnFocus = false, Font = FONT, TextSize = 13, TextColor3 = C.text, PlaceholderColor3 = C.muted, TextXAlignment = Enum.TextXAlignment.Left, LayoutOrder = 6, }, body) corner(search, 10) pad(search, 10, 0) stroke(search, C.line, 1, 0.5) local list = new("ScrollingFrame", { Size = UDim2.new(1, 0, 0, 118), BackgroundColor3 = C.card, BorderSizePixel = 0, ScrollBarThickness = 3, ScrollBarImageColor3 = C.line, CanvasSize = UDim2.new(), AutomaticCanvasSize = Enum.AutomaticSize.Y, LayoutOrder = 7, }, body) corner(list, 10) pad(list, 4) new("UIListLayout", { Padding = UDim.new(0, 3), SortOrder = Enum.SortOrder.LayoutOrder }, list) -- now playing local np = new("Frame", { Size = UDim2.new(1, 0, 0, 54), BackgroundColor3 = C.card, LayoutOrder = 8 }, body) corner(np, 10) local songLabel = label(np, { Position = UDim2.fromOffset(12, 9), Size = UDim2.new(1, -24, 0, 18), Text = "No song selected", Font = FONT_M, TextSize = 13, TextColor3 = C.accent, TextXAlignment = Enum.TextXAlignment.Left, TextTruncate = Enum.TextTruncate.AtEnd, }) local barBg = new("Frame", { Position = UDim2.new(0, 12, 1, -17), Size = UDim2.new(1, -24, 0, 6), BackgroundColor3 = C.card2, }, np) corner(barBg, 3) local barFill = new("Frame", { Size = UDim2.new(0, 0, 1, 0), BackgroundColor3 = WHITE }, barBg) corner(barFill, 3) gradient(barFill, C.accent, C.accent2, 0) -- play / stop local row = new("Frame", { Size = UDim2.new(1, 0, 0, 40), BackgroundTransparency = 1, LayoutOrder = 9 }, body) local playBtn = button(row, { Size = UDim2.new(0.62, -4, 1, 0), Text = "▶ Play", TextSize = 14, TextColor3 = C.dark, }, C.accent, Color3.fromRGB(150, 245, 228)) corner(playBtn, 10) local stopBtn = button(row, { AnchorPoint = Vector2.new(1, 0), Position = UDim2.new(1, 0, 0, 0), Size = UDim2.new(0.38, -4, 1, 0), Text = "■ Stop", TextSize = 14, }, C.card2, C.bad) corner(stopBtn, 10) --------------------------------------------------------------------- -- "A" button (right side) + mini stop button --------------------------------------------------------------------- local togBg = new("Frame", { Size = UDim2.fromOffset(50, 50), AnchorPoint = Vector2.new(1, 0.5), Position = UDim2.new(1, -8, 0.5, 0), BackgroundColor3 = WHITE, ZIndex = 20, }, gui) corner(togBg, 25) gradient(togBg, C.accent, C.accent2, 45) local togStroke = stroke(togBg, WHITE, 2, 0.55) local toggle = new("TextButton", { Name = "Toggle", Size = UDim2.fromOffset(50, 50), AnchorPoint = Vector2.new(1, 0.5), Position = UDim2.new(1, -8, 0.5, 0), BackgroundTransparency = 1, Text = "A", Font = FONT_B, TextSize = 26, TextColor3 = C.dark, AutoButtonColor = false, ZIndex = 21, }, gui) local miniStop = new("TextButton", { Name = "MiniStop", Size = UDim2.fromOffset(36, 36), AnchorPoint = Vector2.new(1, 0.5), Position = UDim2.new(1, -15, 0.5, -48), BackgroundColor3 = C.bad, Text = "■", Font = FONT_B, TextSize = 14, TextColor3 = WHITE, AutoButtonColor = false, Visible = false, ZIndex = 20, }, gui) corner(miniStop, 18) stroke(miniStop, WHITE, 1.5, 0.5) -- refresh everything that depends on `playing` local function refreshPlayingUI() miniStop.Visible = playing tween(togStroke, 0.2, { Color = playing and C.good or WHITE, Transparency = playing and 0 or 0.55, }) styleBox() end local function setPanel(v) panelOpen = v panel.Visible = v if v then if not scanActive then scanActive = true -- from now on scanning keeps running, even with the panel hidden box.Visible = true styleBox() end scale.Scale = 0.9 tween(scale, 0.18, { Scale = 1 }) end updateMarkers() end local function setBox(v) boxOn = v tween(boxBtn, 0.15, { BackgroundColor3 = v and C.accent or C.card2, TextColor3 = v and C.dark or C.muted, }) styleBox() updateMarkers() end toggle.MouseButton1Click:Connect(function() setPanel(not panelOpen) end) hideBtn.MouseButton1Click:Connect(function() setPanel(false) end) boxBtn.MouseButton1Click:Connect(function() setBox(not boxOn) end) miniStop.MouseButton1Click:Connect(function() playing = false end) --------------------------------------------------------------------- -- Drag & resize --------------------------------------------------------------------- local function makeDrag(handle, begin, update, allowed) local active, origin, state handle.InputBegan:Connect(function(i) if (i.UserInputType == Enum.UserInputType.MouseButton1 or i.UserInputType == Enum.UserInputType.Touch) and (not allowed or allowed()) then active = true origin = i.Position state = begin() i.Changed:Connect(function() if i.UserInputState == Enum.UserInputState.End then active = false end end) end end) conns[#conns + 1] = UIS.InputChanged:Connect(function(i) if active and (i.UserInputType == Enum.UserInputType.MouseMovement or i.UserInputType == Enum.UserInputType.Touch) then update(state, i.Position - origin) end end) end makeDrag(box, function() return box.Position end, function(p, d) box.Position = UDim2.fromOffset(p.X.Offset + d.X, p.Y.Offset + d.Y) end, function() return not playing and boxOn end) makeDrag(grip, function() return box.AbsoluteSize end, function(s, d) box.Size = UDim2.fromOffset(math.max(40, s.X + d.X), math.max(30, s.Y + d.Y)) end, function() return not playing and boxOn end) makeDrag(header, function() return panel.Position end, function(p, d) panel.Position = UDim2.fromOffset(p.X.Offset + d.X, p.Y.Offset + d.Y) end) --------------------------------------------------------------------- -- Key detection inside the box --------------------------------------------------------------------- local function isShown(o) local p = o while p and p ~= playerGui do if p:IsA("GuiObject") then if not p.Visible then return false end elseif p:IsA("ScreenGui") then if not p.Enabled then return false end end p = p.Parent end return true end local function scan() local bp, bs = box.AbsolutePosition, box.AbsoluteSize local found = {} for _, o in ipairs(playerGui:GetDescendants()) do if o:IsA("GuiObject") and not o:IsDescendantOf(gui) then local isBtn = o:IsA("GuiButton") local isLbl = o:IsA("TextLabel") and singleChar(o.Text) ~= nil and not o:FindFirstAncestorWhichIsA("GuiButton") if (isBtn or isLbl) and o.AbsoluteSize.X > 0 and o.AbsoluteSize.Y > 0 then local c = rawCenter(o) if c.X >= bp.X and c.X <= bp.X + bs.X and c.Y >= bp.Y and c.Y <= bp.Y + bs.Y and isShown(o) then found[#found + 1] = { obj = o, x = c.X, y = c.Y, h = o.AbsoluteSize.Y } end end end end table.sort(found, function(a, b) if math.abs(a.x - b.x) < 1 then return a.y < b.y end return a.x < b.x end) for i, k in ipairs(found) do k.i = i end detected = found boxLabel.Text = "SCAN · " .. #found .. " keys" end -- scanner loop: runs after the first open, regardless of panel visibility task.spawn(function() while gui.Parent do if scanActive and not playing then pcall(scan) updateMarkers() if os.clock() > lockUntil then if #detected > 0 then setStatus("Detected " .. #detected .. " keys · ready to play", "good") else setStatus("No keys found. Move/resize the box over the piano.", "bad") end end end task.wait(0.25) end end) --------------------------------------------------------------------- -- Song list (NanoMIDI) --------------------------------------------------------------------- local songs = {} local selected = nil local loadState = "loading" local btnSong = {} local function renderList() for _, c in ipairs(list:GetChildren()) do if c:IsA("GuiObject") then c:Destroy() end end btnSong = {} local q = search.Text:lower() local n = 0 for _, s in ipairs(songs) do if q == "" or s.label:lower():find(q, 1, true) then n += 1 if n > 80 then break end local b = new("TextButton", { Size = UDim2.new(1, -4, 0, 28), BackgroundColor3 = (s == selected) and C.accent2 or C.card2, Text = s.label, Font = FONT, TextSize = 12, TextColor3 = C.text, TextXAlignment = Enum.TextXAlignment.Left, TextTruncate = Enum.TextTruncate.AtEnd, AutoButtonColor = false, LayoutOrder = n, }, list) corner(b, 7) pad(b, 10, 0) btnSong[b] = s b.MouseButton1Click:Connect(function() selected = s songLabel.Text = "🎵 " .. s.label for btn, sg in pairs(btnSong) do tween(btn, 0.12, { BackgroundColor3 = (sg == selected) and C.accent2 or C.card2 }) end end) end end if n == 0 then local msg = (loadState == "loading" and "Loading song list…") or (loadState == "failed" and "Failed to load song list") or "No results" label(list, { Size = UDim2.new(1, 0, 0, 40), Text = msg, TextSize = 12, TextColor3 = C.muted }) end end search:GetPropertyChangedSignal("Text"):Connect(renderList) renderList() task.spawn(function() if not httpRequest then loadState = "failed" renderList() setStatus("Your executor does not support request()", "bad", 6) return end local ok, res = pcall(httpRequest, { Url = "https://api.nanomidi.net/api/midiData", Method = "GET" }) if ok and res and res.StatusCode == 200 then local okJ, data = pcall(HttpService.JSONDecode, HttpService, res.Body) if okJ then for _, s in ipairs(data) do songs[#songs + 1] = { label = tostring(s.name) .. " - " .. tostring(s.artists), file = s.midiFilename, } end loadState = "ok" renderList() return end end loadState = "failed" renderList() setStatus("Failed to load song list", "bad", 6) end) --------------------------------------------------------------------- -- Play / Stop --------------------------------------------------------------------- local function finish() playing = false refreshPlayingUI() tween(barFill, 0.3, { Size = UDim2.new(0, 0, 1, 0) }) end local function play() if playing then return end if not selected then setStatus("Select a song first.", "warn", 3) return end pcall(scan) updateMarkers() if #detected == 0 then setStatus("No keys detected. Move the box over the piano.", "bad", 3) return end playing = true refreshPlayingUI() local keys = detected local N = #keys setStatus("Downloading MIDI…", "play") local url = "https://api.nanomidi.net/api/midis/" .. (selected.file:gsub(" ", "%%20")) local ok, res = pcall(httpRequest, { Url = url, Method = "GET" }) if not playing then setStatus("Stopped", "warn", 3) return finish() end if not ok or not res or res.StatusCode ~= 200 then setStatus("Download failed.", "bad", 4) return finish() end local okP, notes = pcall(parseMidi, res.Body) if not okP or #notes == 0 then setStatus("MIDI is empty or unreadable.", "bad", 4) return finish() end local diatonic if modeIdx == 1 then diatonic = not hasBlackKeys(keys) else diatonic = (modeIdx == 3) end local mapper = buildMapper(notes, N, diatonic) local maxChord = (N <= 15) and 2 or 4 -- few keys: keep only the highest notes of a chord local spd = speed local total = math.max(notes[#notes].time, 0.001) songLabel.Text = "▶ " .. selected.label setStatus((diatonic and "Diatonic" or "Chromatic") .. " · " .. N .. " keys · " .. string.format("%.1fx", spd), "play") local t0 = os.clock() local i = 1 while i <= #notes and playing do local target = notes[i].time / spd local now = os.clock() - t0 if target > now then task.wait(target - now) end if not playing then break end local group = {} local j = i while j <= #notes and notes[j].time / spd - target < 0.01 do group[#group + 1] = notes[j].n j = j + 1 end table.sort(group, function(a, b) return a > b end) local chord, seen = {}, {} for _, nn in ipairs(group) do local idx = mapper(nn) if not seen[idx] and keys[idx + 1] then seen[idx] = true chord[#chord + 1] = keys[idx + 1] if #chord >= maxChord then break end end end task.spawn(pressMany, chord) barFill.Size = UDim2.new(math.clamp(notes[i].time / total, 0, 1), 0, 1, 0) i = j end songLabel.Text = "🎵 " .. selected.label setStatus(playing and "Done ✓" or "Stopped", playing and "good" or "warn", 3) finish() end playBtn.MouseButton1Click:Connect(function() task.spawn(play) end) stopBtn.MouseButton1Click:Connect(function() playing = false end)