UnknownUniversal
Auto Piano and Piano scanner
- 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