Custom Roblox Music Maker (DAW) system delayed playback

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

1 Like

is this possibe to be fixed or it is just a roblox issue

try enabling playonremove and then clone then destroy the sound to play it

hello friend, you could try using SoundService:PlayLocalSound().

You can spam this method instead of creating multiple instances

Does this work outside of studio in game

yeah, works anywhere on a client localscript

It doesnt seem to remove the delay

Solution please hello please help me I still need help on this matters