Hello, I made this DAW music maker piano roll thing for my upcoming project and the problem is that the notes/what are played are slightly delayed or choppy, how wouild I be able to fix this?
Roblox sample, as you can see very delayed in many and choppy BPM
Smooth Fl Studio version of same pattern, not choppy and not delayed
Roblox full DAW Localscript in screengui (very long)
local Players = game:GetService("Players")
local SoundService = game:GetService("SoundService")
local ReplicatedStorage = game:GetService("ReplicatedStorage")
local player = Players.LocalPlayer
local gui = script.Parent
local pianoRollFrame = gui:WaitForChild("PianoRollFrame")
local playButton = gui:WaitForChild("PlayButton")
local bpmSlider = gui:WaitForChild("BPMSlider")
local playhead = pianoRollFrame:WaitForChild("Playhead")
local addPatternButton = gui:WaitForChild("AddPatternButton")
local renderButton = gui:WaitForChild("RenderButton")
local patternsFrame = gui:WaitForChild("PatternsFrame")
local sampleSelectorFrame = gui:WaitForChild("SampleSelectorFrame")
-- Ensure Sample folder exists in ReplicatedStorage
local samplesFolder = ReplicatedStorage:FindFirstChild("Samples") or Instance.new("Folder")
samplesFolder.Name = "Samples"
samplesFolder.Parent = ReplicatedStorage
-- Constants
local START_NOTE = 36 -- C2
local END_NOTE = 96 -- C8
local NUM_NOTES = END_NOTE - START_NOTE + 1
local STEPS = 64
local STEP_WIDTH = 30
local NOTE_HEIGHT = 20
local STEPS_PER_BEAT = 4
local BEATS_PER_BAR = 4
local STEPS_PER_BAR = BEATS_PER_BAR * STEPS_PER_BEAT
-- Pattern system
local patterns = {
[1] = {
name = "Pattern 1",
notes = {},
color = Color3.fromRGB(70, 130, 255), -- Blue for pattern 1
sample = nil -- Will be set when we create the sample selector
}
}
local currentPattern = 1
local patternButtons = {}
-- Playback state
local isPlaying = false
local currentStep = 0
local stopPlayback = false
-- Helper functions
local function getPlaybackSpeed(midiNote)
local semitoneOffset = midiNote - 60
return 2 ^ (semitoneOffset / 12)
end
local function midiToNoteName(midi)
local names = {"C", "C#", "D", "D#", "E", "F", "F#", "G", "G#", "A", "A#", "B"}
local octave = math.floor(midi / 12) - 1
return names[midi % 12 + 1] .. tostring(octave)
end
local function getLastUsedStep(notesTable)
local maxStep = 0
for x, column in pairs(notesTable) do
if next(column) ~= nil then
maxStep = math.max(maxStep, x)
end
end
local barsNeeded = math.ceil((maxStep + 1) / STEPS_PER_BAR)
return barsNeeded * STEPS_PER_BAR
end
-- Create the sample selector UI
local function createSampleSelector()
-- Clear existing buttons
for _, child in ipairs(sampleSelectorFrame:GetChildren()) do
if child:IsA("TextButton") then
child:Destroy()
end
end
-- Create buttons for each sample
local yPosition = 0
for _, sample in ipairs(samplesFolder:GetChildren()) do
if sample:IsA("Sound") then
local sampleBtn = Instance.new("TextButton")
sampleBtn.Size = UDim2.new(0, 120, 0, 30)
sampleBtn.Position = UDim2.new(0, 0, 0, yPosition)
sampleBtn.Text = sample.Name
sampleBtn.BackgroundColor3 = patterns[currentPattern].sample == sample and Color3.fromRGB(0, 170, 0) or Color3.fromRGB(45, 77, 218)
sampleBtn.Parent = sampleSelectorFrame
sampleBtn.MouseButton1Click:Connect(function()
patterns[currentPattern].sample = sample
-- Update all sample buttons to show which is selected
for _, btn in ipairs(sampleSelectorFrame:GetChildren()) do
if btn:IsA("TextButton") then
btn.BackgroundColor3 = btn.Text == sample.Name and Color3.fromRGB(0, 170, 0) or Color3.fromRGB(50, 50, 50)
end
end
end)
yPosition = yPosition + 35
end
end
-- Add a button to refresh the sample list
local refreshBtn = Instance.new("TextButton")
refreshBtn.Size = UDim2.new(0, 120, 0, 30)
refreshBtn.Position = UDim2.new(0, 0, 0, yPosition)
refreshBtn.Text = "Refresh Samples"
refreshBtn.BackgroundColor3 = Color3.fromRGB(70, 70, 70)
refreshBtn.Parent = sampleSelectorFrame
refreshBtn.MouseButton1Click:Connect(function()
createSampleSelector()
end)
end
-- Create the piano roll grid
for y = 0, NUM_NOTES - 1 do
for x = 0, STEPS - 1 do
local midiNote = START_NOTE + y
local btn = Instance.new("TextButton")
btn.Size = UDim2.new(0, STEP_WIDTH, 0, NOTE_HEIGHT)
btn.Position = UDim2.new(0, x * STEP_WIDTH, 0, y * NOTE_HEIGHT)
btn.BackgroundColor3 = Color3.fromRGB(50, 50, 50)
btn.TextColor3 = Color3.new(1, 1, 1)
btn.TextSize = 10
btn.Font = Enum.Font.SourceSansBold
btn.Text = x == 0 and midiToNoteName(midiNote) or ""
btn.BorderSizePixel = 1
btn.Name = x .. "," .. y
btn.Parent = pianoRollFrame
btn.MouseButton1Click:Connect(function()
patterns[currentPattern].notes[x] = patterns[currentPattern].notes[x] or {}
if patterns[currentPattern].notes[x][y] then
patterns[currentPattern].notes[x][y] = nil
btn.BackgroundColor3 = Color3.fromRGB(50, 50, 50)
else
patterns[currentPattern].notes[x][y] = true
btn.BackgroundColor3 = patterns[currentPattern].color
end
end)
end
end
-- Create time signature labels
local timeSignatureLabelFrame = gui.PianoRollFrame:WaitForChild("TimeSignatureLabelsFrame")
for i = 0, STEPS - 1, 16 do
local label = Instance.new("TextLabel")
label.Size = UDim2.new(0, 16 * STEP_WIDTH, 0, 20)
label.Position = UDim2.new(0, i * STEP_WIDTH, 0, 0)
label.BackgroundTransparency = 1
label.Text = tostring(i // 16 + 1)
label.TextColor3 = Color3.new(1, 1, 1)
label.TextSize = 14
label.Font = Enum.Font.SourceSansBold
label.TextXAlignment = Enum.TextXAlignment.Center
label.Parent = timeSignatureLabelFrame
end
-- Pattern management
local function updateGridDisplay()
-- Clear all note colors
for _, btn in ipairs(pianoRollFrame:GetChildren()) do
if btn:IsA("TextButton") and btn.Name:match(",") then
btn.BackgroundColor3 = Color3.fromRGB(50, 50, 50)
end
end
-- Highlight notes in current pattern
local currentNotes = patterns[currentPattern].notes
for x, column in pairs(currentNotes) do
for y, _ in pairs(column) do
local btn = pianoRollFrame:FindFirstChild(x .. "," .. y)
if btn then
btn.BackgroundColor3 = patterns[currentPattern].color
end
end
end
-- Update sample selector to show current pattern's sample
createSampleSelector()
end
local function addPattern()
local newPatternIndex = #patterns + 1
local patternColors = {
Color3.fromRGB(255, 100, 100), -- Red
Color3.fromRGB(100, 255, 100), -- Green
Color3.fromRGB(255, 255, 100), -- Yellow
Color3.fromRGB(255, 100, 255), -- Purple
Color3.fromRGB(100, 255, 255) -- Cyan
}
patterns[newPatternIndex] = {
name = "Pattern " .. newPatternIndex,
notes = {},
color = patternColors[(newPatternIndex - 1) % #patternColors + 1],
sample = nil -- No sample selected by default
}
-- Create pattern button
local patternBtn = Instance.new("TextButton")
patternBtn.Size = UDim2.new(0, 80, 0, 30)
patternBtn.Position = UDim2.new(0, 0, 0, (newPatternIndex-1)*35)
patternBtn.Text = patterns[newPatternIndex].name
patternBtn.BackgroundColor3 = Color3.fromRGB(50, 50, 50)
patternBtn.Parent = patternsFrame
patternBtn.MouseButton1Click:Connect(function()
currentPattern = newPatternIndex
updateGridDisplay()
for i, btn in ipairs(patternButtons) do
btn.BackgroundColor3 = i == currentPattern and Color3.fromRGB(0, 170, 0) or Color3.fromRGB(50, 50, 50)
end
end)
table.insert(patternButtons, patternBtn)
return newPatternIndex
end
-- Create the initial pattern button
local pattern1Btn = Instance.new("TextButton")
pattern1Btn.Size = UDim2.new(0, 80, 0, 30)
pattern1Btn.Position = UDim2.new(0, 0, 0, 0)
pattern1Btn.Text = "Pattern 1"
pattern1Btn.BackgroundColor3 = Color3.fromRGB(0, 170, 0) -- Green for active
pattern1Btn.Parent = patternsFrame
table.insert(patternButtons, pattern1Btn)
pattern1Btn.MouseButton1Click:Connect(function()
currentPattern = 1
updateGridDisplay()
for i, btn in ipairs(patternButtons) do
btn.BackgroundColor3 = i == currentPattern and Color3.fromRGB(0, 170, 0) or Color3.fromRGB(50, 50, 50)
end
end)
local RunService = game:GetService("RunService")
local soundPool = {}
local function getSoundFromPool(sample)
for _, sound in ipairs(soundPool) do
if not sound.IsPlaying and sound.SoundId == sample.SoundId then
return sound
end
end
local newSound = sample:Clone()
newSound.Parent = SoundService
table.insert(soundPool, newSound)
return newSound
end
local function playSinglePattern()
if isPlaying then return end
isPlaying = true
playButton.Text = "Pause"
stopPlayback = false
local bpm = tonumber(bpmSlider.Text) or 120
local beatDuration = 60 / bpm
local stepTime = beatDuration / STEPS_PER_BEAT
local pattern = patterns[currentPattern]
if not pattern then
warn("Pattern not found")
isPlaying = false
playButton.Text = "Play"
return
end
local sample = pattern.sample or samplesFolder:FindFirstChild("C5Sample") or samplesFolder:FindFirstChildOfClass("Sound")
if not sample then
warn("Sample not found")
isPlaying = false
playButton.Text = "Play"
return
end
local notes = pattern.notes or {}
local lastStep = getLastUsedStep(notes)
lastStep = math.max(lastStep, 2 * STEPS_PER_BAR)
-- Calculate starting step from playhead position
local startStep = math.floor(playhead.Position.X.Offset / STEP_WIDTH)
local nextStepTime = os.clock() -- When the next step should play
currentStep = startStep
playhead.Visible = true
local connection
connection = RunService.Heartbeat:Connect(function()
if stopPlayback then
connection:Disconnect()
isPlaying = false
playButton.Text = "Play"
return
end
local currentTime = os.clock()
-- Check if it's time to advance to the next step
if currentTime >= nextStepTime then
-- Play notes for this step
if notes[currentStep] then
for y, _ in pairs(notes[currentStep]) do
local midiNote = START_NOTE + y
local sound = sample:Clone()
sound.PlaybackSpeed = getPlaybackSpeed(midiNote)
sound.Parent = SoundService
sound:Play()
game:GetService("Debris"):AddItem(sound, 2)
end
end
-- Update playhead position
playhead.Position = UDim2.new(0, currentStep * STEP_WIDTH, 0, 0)
-- Set exact time for next step
currentStep = currentStep + 1
nextStepTime = nextStepTime + stepTime
-- End playback if we've reached the end
if currentStep > lastStep then
connection:Disconnect()
isPlaying = false
playButton.Text = "Play"
playhead.Position = UDim2.new(0, 0, 0, 0)
return
end
end
end)
end
local function playAllPatterns()
if isPlaying then return end
isPlaying = true
renderButton.Text = "Pause"
stopPlayback = false
local bpm = tonumber(bpmSlider.Text) or 120
local beatDuration = 60 / bpm
local stepTime = beatDuration / STEPS_PER_BEAT
local lastStepToPlay = 0
-- Find the longest pattern
for _, pattern in ipairs(patterns) do
local patternLastStep = getLastUsedStep(pattern.notes)
lastStepToPlay = math.max(lastStepToPlay, patternLastStep)
end
lastStepToPlay = math.max(lastStepToPlay, 2 * STEPS_PER_BAR)
-- Precise timing with accumulator
local timeAccumulator = 0
local lastTime = tick()
currentStep = 0
local connection
connection = RunService.Heartbeat:Connect(function()
if stopPlayback then
connection:Disconnect()
isPlaying = false
renderButton.Text = "Render"
return
end
local currentTime = tick()
local deltaTime = currentTime - lastTime
lastTime = currentTime
timeAccumulator = timeAccumulator + deltaTime
while timeAccumulator >= stepTime and currentStep <= lastStepToPlay do
timeAccumulator = timeAccumulator - stepTime
-- Update playhead
playhead.Position = UDim2.new(0, currentStep * STEP_WIDTH, 0, 0)
-- Play all patterns
for _, pattern in ipairs(patterns) do
local column = pattern.notes[currentStep]
local patternSample = pattern.sample or samplesFolder:FindFirstChild("C5Sample") or samplesFolder:FindFirstChildOfClass("Sound")
if column and patternSample then
for y, _ in pairs(column) do
local midiNote = START_NOTE + y
local sound = getSoundFromPool(patternSample)
sound.PlaybackSpeed = getPlaybackSpeed(midiNote)
sound.Parent = SoundService
sound:Play()
end
end
end
currentStep += 1
if currentStep > lastStepToPlay then
connection:Disconnect()
isPlaying = false
renderButton.Text = "Render"
playhead.Position = UDim2.new(0, 0, 0, 0)
currentStep = 0
return
end
end
end)
end
local function pausePlayback()
stopPlayback = true
isPlaying = false
playButton.Text = "Play"
renderButton.Text = "Render"
end
-- UI Connections
playButton.MouseButton1Click:Connect(function()
if isPlaying then
pausePlayback()
else
playSinglePattern()
end
end)
renderButton.MouseButton1Click:Connect(function()
if isPlaying then
pausePlayback()
else
playAllPatterns()
end
end)
addPatternButton.MouseButton1Click:Connect(addPattern)
-- Draggable playhead
local UIS = game:GetService("UserInputService")
local dragging = false
local dragOffsetX = 0
playhead.InputBegan:Connect(function(input)
if input.UserInputType == Enum.UserInputType.MouseButton1 then
dragging = true
dragOffsetX = input.Position.X - playhead.AbsolutePosition.X
end
end)
UIS.InputEnded:Connect(function(input)
if input.UserInputType == Enum.UserInputType.MouseButton1 then
dragging = false
end
end)
UIS.InputChanged:Connect(function(input)
if dragging and input.UserInputType == Enum.UserInputType.MouseMovement then
local mouseX = input.Position.X - pianoRollFrame.AbsolutePosition.X - dragOffsetX
playhead.Position = UDim2.new(0, math.clamp(mouseX, 0, STEPS * STEP_WIDTH), 0, 0)
end
end)
-- Initialize the sample selector
createSampleSelector()
Thank you please help