Scripting - Making a Music Queue

Hello, my fellow developers! Today, I wanna answer a
question that has been asked many times, How do you make a music player? Finding efficiency in your code makes you feel amazing, at least to me. And let me tell you right now, using wait() in your code to play songs isn’t efficient at all. So today, I’m gonna teach you an awesome way of doing it without ever modifying the script again unless you wish to add new features. :smile:

Edited Post

I have now edited this post to include skipping songs!

Step 1:
You want to make a folder in the ServerStorage called “Songs”. In that folder, you will be putting the songs you wish to play in your music player. Each one will be an IntValue. The name of the IntValue will be the name of the song and its value will be the ROBLOX asset ID of the song.

image

Step 2:
Make a sound object in the SoundService. Call it whatever you wish. I recommend calling it “Music” or something similar to that. That’s how we are going to play the songs.

image (This might also be called Soundspace so look for either those two.)

Step 3
After creating the required objects to play our songs, we start coding. You want to insert a Script inside the ServerScriptService. This script is where we will handle our code for the music player. You can call it whatever you want, I will call it “MusicHandler”.

image

Step 4
A wise bird (Scripting - Making a Music Queue) told me that it’s a good idea to use BindableEvents for our functions. So let’s do that, shall we? We will be making two BindableEvents inside our script. One will be for adding the songs to a queue and the other for playing the next song in that queue. Name those bindable functions playSong and addSongs

image

Alright, let’s get to coding, shall we? In our code, we want to type this:

local music = game.Soundscape.Music -- The path to the sound object you created in the SoundService. If that doesn't work, try replacing Soundscape with SoundService.
local songs = game.ServerStorage.Songs:GetChildren() -- The :GetChildren() method returns a lua table containing the objects inside the instance we used it on. In this case, that's the Songs folder we created in the ServerStorage.
local queue = {} -- Here, we will be putting all the songs in the songs folder and play them all in order. We will also "shuffle" the queue.

The comments I provided for the lines explain what they do. Just for your information. :wink:

Now, we want to bind our addSongs event to a function. This will add all the songs in the songs folder to our queue table. We will be using it later.

script.addSongs.Event:Connect(function()
	for _,song in pairs(songs) do -- This for loop will iterate over every object in our songs folder.
		if song:IsA("IntValue") then -- We check if the song is an IntValue to make sure it's a song.
			table.insert(queue, {
				["songName"] = song.Name, -- The name of the song. As we mentioned, it is the name of the IntValue.
				["songID"] = "rbxassetid://"..tostring(song.Value) -- The ID of the song. As we mentioned, it is the value of the IntValue. We add rbxassetid:// before it because sound objects have it in that format.
			}) -- This will insert a table inside our queue table containing the information for our song.
		end	
	end
	
	for i = 1, #queue - 1 do
		local r = math.random(i,#queue)
		queue[i], queue[r] = queue[r], queue[i]
	end -- This shuffles the table. This method of shuffling is called the "Fisher-Yates Shuffle". If you wish to find out how it works, go search it up!
end)

That table shuffling method is not made by me! I actually searched up a way to do this in the developer forums and found this: https://devforum.roblox.com/t/table-scrambling/13784/5?u=slipperyisawesome_a

Now, we want to bind our playSong event to a function. This will be used to play the next song in the queue. We will also be using this later.

script.playSong.Event:Connect(function()
	if #queue == 0 then script.addSongs:Fire() end -- Here, we check if there are no more songs in the queue table. If there aren't, then we add them again.
	
	local songToPlay = queue[1] -- This is the next song in the queue table. Further into this function, we remove that from the table as it's already playing.
	music.SoundId = songToPlay.songID -- We set the sound ID property of the sound object to the songID we defined in the song to play.
	music:Play() -- This plays the song.
	
	table.remove(queue, 1) -- This removes the song from the table as it's already playing.
	
	music.Ended:Wait() -- We wait for the song to finish, and then we play the next one.
	
	local delayBeforeNextSongPlays = 3 -- The amount of time we will wait before the next song plays after the current one is done.
	
	wait(delayBeforeNextSongPlays)
	
	script.playSong:Fire() -- We run this function again.
end)

Alright, we have done our primary methods! Now that we have finished these, we can easily call them all the way at the bottom of the code. :smiley:

script.addSongs:Fire()-- Calls the addSongs() function which as we said, adds the songs into the queue table.
script.playSong:Fire() -- Plays the next song available in the queue. :)

Alright! Now, we will be adding a feature to be able to skip songs with a command you can type in chat called !skip. This command will only be executable by players of your liking. :slight_smile:
To make this event listener work, we must put it BEFORE calling our playSong() and addSongs() functions. If you put it after, it will not listen out for the event due to functions yielding.

local allowedPlayers = {"coolmam515", "anotherCoolMan516"} -- The people who are allowed to execute the !skip command. If you want, you can also check for a rank instead.

game.Players.PlayerAdded:Connect(function(player) -- The code inside this event listener will happen when a player joins. It passes in the player parameter which is the player who joined.
	player.Chatted:Connect(function(msg) -- The code inside this event listener will happen when the player who joined chats. The msg parameter is the message they chatted.
		if string.lower(msg) == "!skip" then -- We check if the lowercase version of the message they chatted is "!skip". string.lower(string) returns the lowercase version of the string.
			if table.find(allowedPlayers, player.Name) then -- We check if the player's name exists in the allowedPlayers table we created earlier.
				-- IF YOU WANT TO CHECK FOR A RANK, you can easily replace table.find(allowedPlayers, player.Name) with player:GetRankInGroup(groupID) which will return the numerical rank of the player in that group instead of checking if their username exists in the table.
				--[[ Example: 
					if player:GetRankInGroup(000000) > 5 then
						nice code
					end
				--]]
				
				
				table.remove(queue, 1) -- This will remove the currently playing song from the queue table to be able to play the next one.
				script.playSong:Fire() -- We then simply fire the playSong() function as we already know that handles everything.
			end
		end
	end)
end)

And that is it! We have created our music player. I hope this didn’t only teach you to make a music player, but to find efficiency to your code and apply it. Doing this is a lot better than using multiple wait()s in your code.

If you wish to add songs, you can easily insert an IntValue to the Songs folder we created earlier. Set the name of the IntValue to the name of the song and then set the value of it to its ROBLOX asset ID.

image

If I ever want to, I might teach you to add the ability to request/skip songs. :smiley:

I hope you learned something new today. Thanks for reading my post. :slight_smile:

41 Likes

Nice tutorial!

Just a quick point: Unfortunately, Roblox removed proper tailcalling from Lua. Because of this, you will end up getting a stackoverflow error eventually after recursively calling playSong too much.

This can be fixed easily in two different ways:

  1. Hook up playSong to a BindableEvent and fire the event in order to call the function. This will effectively do the same thing that a tailcall tries to accomplish.
  2. Just use a while loop and continuously loop through all the songs.
18 Likes

Oh really? I never knew that. I’ve used this code in many ways and that has never happened to me. Thanks though! I’ll edit the post and include your reply in there. :slight_smile:

5 Likes

There, I edited my post. :smiley: Thanks for the suggestion. I really appreciate the feedback.

5 Likes

You could use a table for song list instead of intvalues in a folder and use getproductinfo for the name of the song.

nice tutorial :slight_smile:

3 Likes

Yes I completely agree. However, it’s also a good idea to leave the name of the songs that can be displayed in-game to the game editor. That way, it doesn’t include things like “[200+ FAVORITES!]” or anything like that. Thanks for the feedback though.

2 Likes

I just want to say brilliant work. Love the way it’s made.

2 Likes

Thanks! Love your feedback. :heart:

1 Like

Lovely! I have to questions. First off, is this music queue local? Does everyone hear the same song at the same time? My second question is, are you going to add how to request/skip a song?

2 Likes

For the first question, yes it is. Since we’re storing the sound object in the workspace and the scripts in the server, it should be synced across clients. Sometimes, it may be a few seconds off, but it fixes itself when a new song plays. For the second question, I don’t really know. I might add this functionality to the tutorial later on.

Thanks for the reply. :stuck_out_tongue:

2 Likes

Thank you, this is going to help me so much! Adding in a skip command would be so helpful!

2 Likes

I added skip command functionality! You can check it out. :smiley:

2 Likes

Awesome! Great feature! Now we just need a pause command! :slight_smile:

2 Likes

Use SoundService instead of putting the sounds in Workspace, as SoundService is meant for sounds.

2 Likes

Well, I made a Music System, that’s open-sourced.
But do you want to add a Blacklisted Music, just incase.

1 Like

Great tutorial!

However, I have a concern with your implementation of those bindable events. In the line below:

if #queue == 0 then script.addSongs:Fire() end

could you not run into possible undefined behavior? The way the code is set-up, the addSongs event will fire as the rest of that current playSong event thread is running. I feel like potentially you could be indexing the queue before it has been refilled. There may be something I’m missing though.

A possible solution to this could be to yield the playSong thread until addSongs has finished executing.

I’m interested to hear your thoughts on this. :slightly_smiling_face:

4 Likes

Thanks for the suggestion. I don’t really mess around with sound objects a lot. I’ll edit my post. :smiley:

2 Likes

ROBLOX automatically yields when you fire events I believe. The code below that if statement will run after the function has finished running its code, I’m not entirely sure. Thanks for the reply.

EDIT: I just found out in the developer page that it doesn’t. Thanks for noticing that! A possible way to fix this is to use a BindableFunction instead and simply return any value when the code is done. I do know that BindableFunctions do yield as ROBLOX waits for them to return a value.

1 Like

It’s if player:GetRankInGroup(000000) >= 5

1 Like

It doesn’t matter. That’s up to the user writing the script to do. It doesn’t have to be >= 5

1 Like