Hello.
I am working on (and have mostly finished) a playlist system. I have no practical implementation for this because why would you play songs from Roblox versus on Spotify or Apple Music or literally anything else?
This is my first OOP script, so I am expecting quite a bit of incorrect practices or little things I missed.
Also, I know getNameFromId() is a horribly slow function when adding songs in bulk. I’m not sure what I will replace it with in the future, but for now I’m just gonna leave it since it works.
Code:
local MPS = game:GetService("MarketplaceService")
local audioFolder = script.Parent.AudioPlayers
local audioPlayer = audioFolder.AudioPlayer
local pitchShifter = audioPlayer.AudioPitchShifter
local function getNameFromId(id: number): string
local success, result = pcall(function()
return MPS:GetProductInfo(id, Enum.InfoType.Asset).Name
end)
if not success then
warn(result)
end
return success and tostring(result) or "???"
end
local function shuffle(list: {[string]: {}}): {any}
local t = {}
--convert to list
local i = 0
for k, _ in list do
i += 1
t[i] = k
end
--shuffle
for i = #t, 1, -1 do
local rng = math.random(1, i)
t[i], t[rng] = t[rng], t[i]
end
return t
end
type song = {
id: number,
copyrighted: boolean,
}
type Playlist = {
songs: {[string]: song},
name: string,
songLoop: thread,
currentSong: string,
state: string,
stateChanged: BindableEvent,
songEndedCon: RBXScriptConnection,
}
local Playlists = {}
Playlists.__index = Playlists
--constructor
function Playlists.New(name: string)
local self = {} :: Playlist
setmetatable(self, Playlists)
self.songs = {}
self.songLoop = nil
self.currentSong = ""
self.state = ""
self.songEndedCon = nil
self.name = name and tostring(name) or "???"
self.stateChanged = Instance.new("BindableEvent")
return self
end
--modifiers
--adds a song to the playlist, copyrighted is only for defaultPlaylist, not for user usage... for now ;)
function Playlists:AddSong(id: number, copyrighted: boolean)
if type(id) ~= "number" then error("Parameter id expected number, got "..type(id)) end
copyrighted = copyrighted or false
self.songs[getNameFromId(id)] = {["id"] = id, ["copyrighted"] = copyrighted}
end
--adds multiple songs to the playlist, copyrighted paramater assumes all songs are copyrighted (true) or not (false)
function Playlists:AddSongs(ids: {number}, copyrighted: boolean)
if type(ids) ~= "table" then error("Parameter id expected table, got "..type(ids)) end
copyrighted = copyrighted or false
for _, id in ids do
self:AddSong(id, copyrighted)
end
end
--removes a song from the playlist, MIGHT CHANGE LATER TO USE NAMES INSTEAD OF IDS TO FIND SONG
function Playlists:RemoveSong(id: number)
if type(id) ~= "number" then error("Parameter id expected number, got "..type(id)) end
self.songs[getNameFromId(id)] = nil
end
--removes multiple songs from the playlist
function Playlists:RemoveSongs(ids: {number})
if type(ids) ~= "table" then error("Parameter id expected table, got "..type(ids)) end
for _, v in ids do
self:RemoveSong(v)
end
end
--playback
--plays the song from <code>TimePosition = 0</code>
function Playlists:PlaySong(name: string, looped: boolean)
local song = self.songs[name]
if not song then warn(`Song "{name}" is not part of the playlist`) return end
self.currentSong = name
self:SetState("Playing")
local id: number = song["id"]
local copyrighted: boolean = song["copyrighted"]
audioPlayer.Asset = "rbxassetid://"..id
audioPlayer.TimePosition = 0
pitchShifter.Bypass = not copyrighted
audioPlayer:Play()
if looped then
self.songLoop = coroutine.create(function()
while self.state == "Playing" do
coroutine.yield()
self:Replay()
end
end)
coroutine.resume(self.songLoop)
self.songEndedCon = audioPlayer.Ended:Connect(function()
if self.state == "Playing" then
if self.songLoop and coroutine.status(self.songLoop) == "suspended" then
coroutine.resume(self.songLoop)
end
else
self.songEndedCon:Disconnect()
end
end)
end
end
--plays all songs, shuffled and/or looped if specified
function Playlists:PlaySongs(shuffled: boolean, looped: boolean)
local finished: boolean = false
local list = self.songs
if shuffled then
list = shuffle(list)
end
self:SetState("Playing")
self.songLoop = coroutine.create(function()
while self.state == "Playing" do
finished = false
for i, name in list do
self:PlaySong(type(i) == "string" and i or name) --if it is not shuffled, `i` will be the name, otherwise use `name`
coroutine.yield()
end
finished = true
if not looped then
return
end
end
end)
coroutine.resume(self.songLoop)
self.songEndedCon = audioPlayer.Ended:Connect(function()
if self.songLoop and coroutine.status(self.songLoop) == "suspended" then
coroutine.resume(self.songLoop)
end
if not looped and finished then
self:Stop()
return
end
end)
end
--pause current song
function Playlists:Pause()
audioPlayer:Stop()
self:SetState("Paused")
end
--resume current song
function Playlists:Resume()
audioPlayer:Play()
self:SetState("Playing")
end
--replay current song
function Playlists:Replay()
audioPlayer.TimePosition = 0
audioPlayer:Play()
end
--stop playback of songs; exits loop
function Playlists:Stop()
audioPlayer:Stop()
audioPlayer.TimePosition = 0
self.currentSong = ""
self:SetState("")
if self.songLoop and coroutine.status(self.songLoop) ~= "running" then
pcall(function() coroutine.close(self.songLoop) end)
end
self.songLoop = nil
if self.songEndedCon then
self.songEndedCon:Disconnect()
self.songEndedCon = nil
end
end
--playlist setting and getting
--returns the state of the song ("Playing" or "Paused")
function Playlists:GetState()
return self.state
end
--set the current state of the playlist
function Playlists:SetState(newState: string)
local oldState = self.state
if oldState ~= newState then
self.state = newState
self.stateChanged:Fire(oldState, newState)
end
end
--[[fires when the state changes
(will not fire if the state changes to the same state is was before, e.g. <code>"Playing" -> "Playing"</code> will not fire this event)]]
function Playlists:onStateChanged(func: (old: string, new: string) -> any)
self.stateChanged.Event:Connect(func)
end
--returns the currently playing song
function Playlists:GetPlayingSong()
return self.currentSong
end
--returns the name of the playlist
function Playlists:GetName()
return self.name
end
--deletion
--[[will delete the playlist, if any function runs on a deleted playlist (including <code>Delete()</code>), the code WILL ERROR
it is recommended to set the variable you store the playlist with to <code>nil</code> to prevent this from happening]]
function Playlists:Delete()
if getmetatable(self) == nil then print("Deletion attempted more than once") return end
self:Stop()
self.stateChanged:Destroy()
setmetatable(self, nil)
for k, _ in pairs(self) do
self[k] = nil
end
self = nil
end
--debug; prints all songs in a playlist along with the respective id and copyright status for each element/song
function Playlists:Output()
for name, t in self.songs do
print("Name:", name)
print("ID:", t["id"])
print("Copyrighted:", t["copyrighted"])
print("----------------------------")
end
end
--debug; prints current song loop
function Playlists:SongLoop()
print(self.songLoop)
end
return Playlists