Skip to content
RoRoScripts
Auto Piano and Piano scanner preview
UnknownUniversal

Auto Piano and Piano scanner

Script
Universal ScriptFunkyScriptz
Views
321
Likes
0
Dislikes
0
Added
Script
--[[
    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)

Features

  • Good for Aura farming
Auto Piano and Piano scanner · RoScripts