How to create a Server Sided Music System

This is my first tutorial, so if anything is wrong please correct me.

Today I’ll be going over how to make a simple music system, which loops through songs in a a table and plays them.

Preparing for the tutorial

We’re going to use 3 scripts for this tutorial, 2 module scripts, and 1 server script.

  1. First, create a Script inside ServerScriptService: Let’s call it “Music”.
  2. Create 2 ModuleScripts inside of your Music script that you’ve just created. Call one of the ModuleScripts Songs, and the other playSong

Great! Now you should have something like the following:
image
If not, make sure to match yours up with mine.

"Songs" ModuleScript

This script won’t do run code, or run functions, ect. Instead, it will return a table that has all the Song ID’s.

When you first open the Module, you will probably see the following:

local module = {}

return module

We don’t need this, so delete it. We need the script to just return a table, which is what the script was just doing, but we don’t need the top part.

Instead, now write

return {}

Easy! Let’s call this table “Returner”. That is what the original code was doing, but we’ve simplified it a lot. Now open the Returner table onto a new line, and we can add our ID’s. This can be done many ways, we’re going to do it like this:

[1] = {
	Name = "", --// Name of the Song
	Composer = "", --// Composer of the Song
	ID = "", --// Song ID
}

You don’t need the Name and Composer variables, but we’re using them so we can show the client what song is playing.

Add that table into your Returner table, like this:

return {
	[1] = {
		Name = "",
		Composer = "",
		ID = "", 
	}
}

The [1] doesn’t really matter, you can put anything there. It’s just so we have a Dictionary to return.

And you’re done with the Songs script! You can add as many songs as you would like, here’s an example of what I’m using in my game:

My Songs Module
return {
	[1] = {
		Name = "Catch My Vibe",
		Composer = "APM Music",
		ID = "rbxassetid://1843663426"
	};
	
	[2] = {
		Name = "Clap for the Runner Up",
		Composer = "APM Music",
		ID = "rbxassetid://1843663202"
	};
	
	[3] = {
		Name = "Dreamers Paradise",
		Composer = "APM Music",
		ID = "rbxassetid://1843689215"
	};
	
	[4] = {
		Name = "Party Like We're Young Forever",
		Composer = "APM Music",
		ID = "rbxassetid://1843650802"
	},
	
	[5] = {
		Name = "Set a High Bar",
		Composer = "APM Music",
		ID = "rbxassetid://1843662935"
	},
	
	[6] = {
		Name = "So Much More",
		Composer = "APM Music",
		ID = "rbxassetid://1843652367"
	};
	
	[7] = {
		Name = "This Time I Want it All",
		Composer = "APM Music",
		ID = "rbxassetid://1843651937"
	};
	
	[8] = {
		Name = "Catch My Vibe",
		Composer = "APM Music",
		ID = "rbxassetid://1843663426"
	}
}
"playSong" ModuleScript

First, delete everything inside of the Module.

This module is going to have 2 functions:

  1. playNextSong()
  2. getPlaying()

Let’s define some variables first, before writing these functions.

First we’re going to need the Songs to play, luckily we just wrote them! So we can get them by doing this:

local Songs = require(script.Parent.Songs)

require basically just tells Roblox to give us that module and what it’s returning

Now we’re actually going to need basic module script stuff, create a variable like this:

local m = {}

Then finally, create another variable like this:

local currentSong = 1

playNextSong()

Now lets create the functions! We’ll start with playNextSong().

Tell the script you want to create a function, by writing the following:

function m:playNextSong()

end

This function requires no arguments, so we’ll leave the brackets empty.
Now lets code! All of the following code will be inside of the function.

Lets first change the current song, so the script doesn’t loop the same song.

	if currentSong == #Songs then --// Checks if the current song is the last song we have
		currentSong = 1 --// Loops back to the start
	else --// If it's not the last song
		currentSong += 1 --// Move onto the next song
	end

Great, now let’s get our song.

local NextSong = Songs[currentSong]

Since Songs is a table, we can get the song using the square brackets [] and the index. Now that variable is one of our songs, like this:

    {
		Name = "",
		Composer = "",
		ID = "", 
	}

So for example, to get our Song name, we can do

NextSong.Name -- output: STRING

But we don’t need the name right now, we need the ID. Create a sound in workspace called CurrentSong. Now define it in your function:

local CurrentSongInst = game:GetService("Workspace").CurrentSong

Now we need to stop what ever is currently playing, set the new SongID, and then play it.

	CurrentSongInst:Stop() -- Stops whatever song is current playing if any

	CurrentSongInst.SoundId = NextSong.ID -- Sets new ID
	CurrentSongInst:Play() -- Plays New Song

Cool! That’s basically it for our playNextSong function. This last step is optional, however you can fire an event to all players so the players have access to the next song table too!

game.ReplicatedStorage.Events.NewSong:FireAllClients(NextSong)

Please note that a folder named “Events” is needed with a child that is a RemoteEvent named NewSong

Final playNextSong() function
function m:playNextSong()
		if currentSong == #Songs then
			currentSong = 1
		else
			currentSong += 1
		end
	
	local NextSong = Songs[currentSong]
	local CurrentSongInst = game:GetService("Workspace").CurrentSong
	print(NextSong)

	CurrentSongInst:Stop()

	CurrentSongInst.SoundId = NextSong.ID
	CurrentSongInst:Play()

	game.ReplicatedStorage.Events.NewSong:FireAllClients(NextSong)

	return NextSong
end
getPlaying()

This is simple

Create a new function called getPlaying()

function m:getPlaying()
	
end

Then inside the function, return what song is playing:

function m:getPlaying()
	return Songs[currentSong]
end

And you’re done!

"Music" Script

First require both module scripts we just wrote.

local Songs = require(script.Songs)
local m = require(script.playSong)

You know what require does now, so I don’t need to explain it.

Now write the following under those two variables.

m:playRandomSong()

If you’ve done everything correctly, this should initialize everything and play a new song.

To loop play songs, you can detect when the song has ended, and run that function again.

game.Workspace.CurrentSong.Ended:Connect(function()
	m:playNextSong()
end)

Sound.Ended

And you’re done!


Thanks for reading, I hope this helped you out in someway!

FAQ
  1. Why didn’t you just use GetProductInfo() to get the name and composer?

Because sometimes audio’s have really odd names, and about 80% of the time the person that posted the audio isn’t the composer.

this took like an hour to write due to how confusing raw formatting looks

17 Likes

I love this tutorial but I think there’s a typo. About halfway through the tutorial on the "playSong" ModuleScript and also on the "Music" Script, it goes from NextSong to RandomSong.

Edit: You forgot one in the "Music" Script:

ahh yes

when i wrote this i was using the variable RandomSong, and kept copy and pasting the code from roblox to here then editing the name

i’ll fix that now, thanks for letting me know

edit: all typos should be fixed

1 Like

When I run my code, it throws an error saying Module code did not return exactly one value.

Where is this error coming from?

It doesn’t say, it just says studio.

Are you recieving any other errors, or just that one?

Just that one.

Would it be possible for you to send a place file that you’re using here or in Dev-Forum private messages so I can help a bit quicker and easier?

1 Like

It gives me that error too…

the error indicates that a module has been required but did not return 1 value (no less, no more); and I assume that you forgot to return the “playSong” module because the OP has not

local module = {}

return module -- valid
local module = {}

return module, module -- returns 2 values, is invalid
local module = {}

-- did not return any value, is invalid
1 Like

I did… Thanks for the reply, it works now!

1 Like

I have made a similar module in the past. It has all the features that you might need for controlling the music. I know that not all stuff is optimized, and I have used bad methods, so do take note that you should only use the code as an example rather than a finished product because this is not a finished product. It features playing, preloading, skipping, skipping to the previous song, autoplay, set timestamps, subtitles, etc.


If you are interested in the code, it’s going to be below. Also, I embedded the subtitle reader in my video player module. I actually code most of the stuff related to audio on my audio player, then embed the code into my other modules. I share this because I want to see other people get inspired and create more useful resources like this one. Also, this is a small addition to this tutorial for people seeking to implement other functions other than just playing and stopping; please only use this as an example and not a finished product. The code is not optimized well and can’t be scaled; I made too many functions when I could do it with fewer.


  • The whole code:

Disclaimer: the code below was meant to be ran on the client, if you wish to change it to the server you might have to do a little more manual edits.

local Tracks = 
	{
		[1] = {
			["Original"] = 13697265197,
			["Name"] = "Tomodachi no Kioku ver.b"
		},
		[2] = {
			["Original"] = 13697280194,
			["Name"] = "Modokashii Kimochi"
		},
		[3] = {
			["Original"] = 13697288644,
			["Name"] = "Hiruyasumi no Okujou"
		},
		[4] = {
			["Original"] = 13697299240,
			["Name"] = "Lunch for Two People"
		},
		[5] = {
			["Original"] = 13697306954,
			["Name"] = "My Precious Friend"
		},
		[6] = {
			["Original"] = 13697311245,
			["Name"] = "Fujimiya-san to Oshaberi"
		},
		[7] = {
			["Original"] = 13697350772,
			["Name"] = "Fujimiya-san ni Dokidoki"
		},
		[8] = {
			["Original"] = 13697369399,
			["Name"] = "Hase and Kiru"
		},
		[9] = {
			["Original"] = 13697394991,
			["Name"] = "After School Fun ver.a"
		},
		[10] = {
			["Original"] = 13697401626,
			["Name"] = "Gentle"
		},
		[11] = {
			["Original"] = 13697427739,
			["Name"] = "Atarashii Tomodachi"
		},
		[12] = {
			["Original"] = 13697433460,
			["Name"] = "Arm-in-Arm"
		},
		[13] = {
			["Original"] = 13697470886,
			["Name"] = "Don't Get Along So Well!"
		},
		[14] = {
			["Original"] = 13697474677,
			["Name"] = "Yakimoki Hase-kun"
		},
		[15] = {
			["Original"] = 13697480645,
			["Name"] = "After School Fun ver.b"
		},
		[16] = {
			["Original"] = 13697488249,
			["Name"] = "Gakkou no Kaerimichi"
		},
		[17] = {
			["Original"] = 13697496239,
			["Name"] = "Asa no Hikari no Naka de"
		},
		[18] = {
			["Original"] = 13697506835,
			["Name"] = "Fujimiya-san no Nikki"
		},
		[19] = {
			["Original"] = 13697512986,
			["Name"] = "Kienai Omoi, Itsumademo"
		},
		[20] = {
			["Original"] = 13697521310,
			["Name"] = "Fuan na Hibi"
		},
		[21] = {
			["Original"] = 13697526457,
			["Name"] = "Kako no Dekigoto"
		},
		[22] = {
			["Original"] = 13697533176,
			["Name"] = "Boukyaku no Kanata ni"
		},
		[23] = {
			["Original"] = 13697539583,
			["Name"] = "Monday"
		},
		[24] = {
			["Original"] = 13697559668,
			["Name"] = "Memory Loss"
		},
		[25] = {
			["Original"] = 13697567690,
			["Name"] = "False Memories"
		},
		[26] = {
			["Original"] = 13697571942,
			["Name"] = "Tomodachi ni Naritai"
		},
		[27] = {
			["Original"] = 13697576190,
			["Name"] = "Odayaka na Toki"
		},
		[28] = {
			["Original"] = 13697586283,
			["Name"] = "Emotions, Overflowing"
		},
		[29] = {
			["Original"] = 13697589994,
			["Name"] = "Tomodachi no Kioku ver.a"
		},
		[30] = {
			["Original"] = 13690557806,
			["Translated"] = {
				["Russian"] = 13728422986,
				["Subtitles"] = {
					["Russian"] = {
						{0, ""},
						{8, "Опять прийдёт новый рассвет - хочу встречать его с тобой,"},
						{15, "Напополам делить секунды, радость и печаль."},
						{22, "Бережно переверну страницу книги, где я воспоминания"},
						{32, "Собираю и храню."},
						{35.8, "Может быть, словами не выходит о чувствах рассказать ничего,"},
						{43, "Но, забыв про страх, попытаюсь сделать вперёд шаг!"},
						{50, "Протяни ладонь и в небо воспарим"},
						{54, "Безграничное и столь прекрасное."},
						{58, "По мосту из радуги отправимся"},
						{61, "В грядущий день, что с нетерпеньем ждёт."},
						{65, "Поборов сомнения, смогу найти"},
						{69, "То, чем дорожил давно когда-то ты,"},
						{72, "Улыбкою твоей согреюсь вновь и теперь хочу"},
						{78, "Любоваться ею всегда!"},
						{81, "Друзья на неделю"},
						{88, ""}
					}
				}
			},
			["Alternative"] = 13697594306, -- Piano
			["Subtitles"] = {
				{0, ""},
				{8, "I want to be with you when the new dawn comes again,"},
				{15, "I want to share the seconds, the joys and the sorrows."},
				{22, "I'll carefully turn the page of the book where I'll"},
				{32, "Collect and keep my memories."},
				{35.8, "Maybe words can't tell how I feel,"},
				{43, "But forgetting the fear, I'll try to take a step forward!"},
				{50, "Hold out your palm and you'll soar into the sky"},
				{54, "So boundless and so beautiful"},
				{58, "Over the bridge of the rainbow"},
				{61, "To the day to come That's waiting impatiently"},
				{65, "I'll overcome my doubts, I'll find"},
				{69, "What you once treasured"},
				{72, "I'll be warmed by your smile and now I want to"},
				{78, "Always be able to admire it!"},
				{81, "One Week Friends"},
				{88, ""}
			},
			["Name"] = "Niji no Kakera"
		},
		[31] = {
			["Original"] = 13697613040,
			["Subtitles"] = {},
			["Name"] = "Kanade"
		}
	}

local TweenService = game:GetService("TweenService")
local currentTrack
local currentVolume = 0.5
local isAutoPlay = false
local isSubtitles = false
local TracksList = {}

local TrackControl = {}
function TrackControl:Play()
	if not game.Workspace:FindFirstChild("Tracks") then
		local TracksFolder = Instance.new("Folder")
		TracksFolder.Name = "Tracks"
		TracksFolder.Parent = game.Workspace
		for _, v in Tracks do
			local Track = Instance.new("Sound")
			Track.Volume = 0
			Track.SoundId = "rbxassetid://".. v["Original"]
			Track.Name = v["Name"]
			Track.Parent = TracksFolder
			table.insert(TracksList, Track)
			local Equalizer = Instance.new("EqualizerSoundEffect")
			Equalizer.HighGain = 2
			Equalizer.MidGain = 2
			Equalizer.LowGain = 5
			Equalizer.Parent = Track
		end
		currentTrack = math.random(1, 29)
		game:GetService("ContentProvider"):PreloadAsync(TracksList)
	end
	
	TracksList[currentTrack]:Resume()
	TweenService:Create(TracksList[currentTrack], TweenInfo.new(0.5, Enum.EasingStyle.Cubic, Enum.EasingDirection.In), {Volume = currentVolume}):Play()
end

function TrackControl:Pause()
	if not game.Workspace:FindFirstChild("Tracks") then return end
	TweenService:Create(TracksList[currentTrack], TweenInfo.new(0.5, Enum.EasingStyle.Cubic, Enum.EasingDirection.Out), {Volume = 0}):Play()
	task.wait(0.5)
	TracksList[currentTrack]:Pause()
end

function TrackControl:Rewind()
	if not game.Workspace:FindFirstChild("Tracks") then return end	
	TweenService:Create(TracksList[currentTrack], TweenInfo.new(0.5, Enum.EasingStyle.Cubic, Enum.EasingDirection.Out), {Volume = 0}):Play()
	task.wait(0.5)
	TracksList[currentTrack]:Stop()
	if currentTrack - 1 > table.maxn(TracksList) then currentTrack = 32 else currentTrack -= 1 end
	TracksList[currentTrack]:Play()
	TweenService:Create(TracksList[currentTrack], TweenInfo.new(0.5, Enum.EasingStyle.Cubic, Enum.EasingDirection.In), {Volume = currentVolume}):Play()
end

function TrackControl:Skip()
	if not game.Workspace:FindFirstChild("Tracks") then return end	
	TweenService:Create(TracksList[currentTrack], TweenInfo.new(0.5, Enum.EasingStyle.Cubic, Enum.EasingDirection.Out), {Volume = 0}):Play()
	task.wait(0.5)
	TracksList[currentTrack]:Stop() 
	if currentTrack + 1 > table.maxn(TracksList) then currentTrack = 1 else currentTrack += 1 end
	TracksList[currentTrack]:Play()
	TweenService:Create(TracksList[currentTrack], TweenInfo.new(0.5, Enum.EasingStyle.Cubic, Enum.EasingDirection.In), {Volume = currentVolume}):Play()
end

function TrackControl:Stamp(newStamp)
	if not game.Workspace:FindFirstChild("Tracks") then return end	
	TracksList[currentTrack].TimePosition = newStamp
end

function TrackControl:Volume(newVolume)
	if not game.Workspace:FindFirstChild("Tracks") then return end
	currentVolume = newVolume
	TweenService:Create(TracksList[currentTrack], TweenInfo.new(0.1, Enum.EasingStyle.Cubic, Enum.EasingDirection.InOut), {Volume = newVolume}):Play()
end

function TrackControl:ChangeTrack(newTrack)
	if not game.Workspace:FindFirstChild("Tracks") then return end
	TweenService:Create(TracksList[currentTrack], TweenInfo.new(0.5, Enum.EasingStyle.Cubic, Enum.EasingDirection.Out), {Volume = 0}):Play()
	task.wait(0.5)
	TracksList[currentTrack]:Stop()
	currentTrack = newTrack
	TracksList[currentTrack]:Play()
	TweenService:Create(TracksList[currentTrack], TweenInfo.new(0.5, Enum.EasingStyle.Cubic, Enum.EasingDirection.In), {Volume = currentVolume}):Play()
end

function TrackControl:GetTrack()
	if not game.Workspace:FindFirstChild("Tracks") then return end
	return TracksList[currentTrack]
end

function TrackControl:ChangeAutoPlay(newValue: boolean)
	if not game.Workspace:FindFirstChild("Tracks") then return end
	if newValue == true then
		isAutoPlay = true
		task.spawn(function()
			while task.wait() do
				if not isAutoPlay then break end
				if not TracksList[currentTrack].Playing then TrackControl:Skip() return end
				TracksList[currentTrack].Ended:Wait()
				TrackControl:Skip()
			end
		end)
	else
		isAutoPlay = false
	end
end

function TrackControl:ChangeToAlternative(newTrack)
	if not game.Workspace:FindFirstChild("Tracks") then return end
	if not TracksList[newTrack] then return end
	if not Tracks[newTrack]["Alternative"] then return end
	if newTrack == currentTrack then
		TweenService:Create(TracksList[currentTrack], TweenInfo.new(0.5, Enum.EasingStyle.Cubic, Enum.EasingDirection.Out), {Volume = 0}):Play()
		task.wait(0.5)
		TracksList[currentTrack]:Stop()
		currentTrack = newTrack
		TracksList[currentTrack].SoundId = "rbxassetid://".. Tracks[newTrack]["Alternative"]
		game:GetService("ContentProvider"):PreloadAsync({TracksList[currentTrack]})
		TracksList[currentTrack]:Play()
		TweenService:Create(TracksList[currentTrack], TweenInfo.new(0.5, Enum.EasingStyle.Cubic, Enum.EasingDirection.In), {Volume = currentVolume}):Play()
	else
		TracksList[newTrack].SoundId = "rbxassetid://".. Tracks[newTrack]["Alternative"]
		game:GetService("ContentProvider"):PreloadAsync({TracksList[newTrack]})
	end
end

function TrackControl:ChangeToLanguage(newTrack, newLanguage)
	if not game.Workspace:FindFirstChild("Tracks") then return end
	if not TracksList[newTrack] then return end
	if not Tracks[newTrack]["Translated"][newLanguage] then return end
	if newTrack == currentTrack then
		TweenService:Create(TracksList[currentTrack], TweenInfo.new(0.5, Enum.EasingStyle.Cubic, Enum.EasingDirection.Out), {Volume = 0}):Play()
		task.wait(0.5)
		TracksList[currentTrack]:Stop()
		currentTrack = newTrack
		TracksList[currentTrack].SoundId = "rbxassetid://".. Tracks[currentTrack]["Translated"][newLanguage]
		game:GetService("ContentProvider"):PreloadAsync({TracksList[currentTrack]})
		TracksList[currentTrack]:Play()
		TweenService:Create(TracksList[currentTrack], TweenInfo.new(0.5, Enum.EasingStyle.Cubic, Enum.EasingDirection.In), {Volume = currentVolume}):Play()
	else
		TracksList[newTrack].SoundId = "rbxassetid://".. Tracks[newTrack]["Translated"][newLanguage]
		game:GetService("ContentProvider"):PreloadAsync({TracksList[newTrack]})
	end
end

function TrackControl:EnableSubtitles(newTrack, subtitleContainer, translatedLanguage)
	if not game.Workspace:FindFirstChild("Tracks") then return end
	if currentTrack == newTrack and TracksList[currentTrack].Playing == true then return end
	task.spawn(function()
		repeat task.wait() until currentTrack == newTrack and TracksList[currentTrack].Playing == true
		if Tracks[newTrack]["Translated"]["Subtitles"][translatedLanguage] then
			for i, v in Tracks[newTrack]["Translated"]["Subtitles"][translatedLanguage] do
				subtitleContainer.Text = v[2]
				if not Tracks[newTrack]["Translated"]["Subtitles"][translatedLanguage][i+1] then return end
				task.wait(Tracks[newTrack]["Translated"]["Subtitles"][translatedLanguage][i+1][1] - v[1])
			end
		else
			for i, v in Tracks[newTrack]["Subtitles"] do
				subtitleContainer.Text = v[2]
				if not Tracks[newTrack]["Subtitles"][i+1] then return end
				task.wait(Tracks[newTrack]["Subtitles"][i+1][1] - v[1])
			end
		end
	end)
end

return TrackControl

It’s throwing an error saying ServerScriptService.Music:4: attempt to call missing method 'playNextSong' of table. I tried to fix it but it doesn’t work. Here’s my music script:

local Songs = require(script.Songs)
local m = require(script.playSong)

m:playNextSong()

game.Workspace.CurrentSong.Ended:Connect(function()
	m:playNextSong()
end)