In the first version I had to manually sync lyrics in lua tables that looked like this:
Lyrics = {
{seconds = 7.44, lyrics = "Don't do love, don't do friends"},
{seconds = 10.82, lyrics = "I'm only after success"},
...
{seconds = 172.74, lyrics = "Oh, oh no! oh no! oh no!"}
}
The data above is basically the final result in the final version of the lyrics system, a lua table with the lyrics and timestamps that gets run through some code which triggers the correct subtitles to appear at the correct time (in response to the Sound.TimePosition
of the song changing).
That should cover your question but I have more to share.
One of the main things to know about the final system is that the lyrics are stored as .lrc files. An .lrc file is a lyrics file used for kareoke machines. They can be found online and there are tools for creating them by throwing in the lyrics and pressing a button as you listen to the song play.
An .lrc file looks like this, it’s just plaintext:
"""
[ar: Marina and the Diamonds]
[ti: Oh no!]
[al: no information]
[id: szqhwqci]
[00:07.15]Don't do love, don't do friends
[00:10.75]I'm only after success
...
[02:52.11]Oh, oh no, oh no, oh no
"""
The whole point in using .lrc files is so that I could just grab them from online if somebody already made one for the specific song I had in the game. If I couldn’t find one I would use an online .lrc tool like mentioned above.
Here's the code I wrote to parse the .lrc files into a lua table.
-- Code by @Razorter
-- Imperfect LRC text parser.
local TagLinePattern = "%[(.+):(.+)%]$"
local TimestampPattern = "%[(%d+):(%d+)%.(%d+)%]"
local LyricsLinePattern = "^"..TimestampPattern.."(.*)$"
local function IsStringBlank(s)
return s == nil or string.match(s, "%S") == nil
end
local function ParseTimestamp(Minutes, Seconds, Centiseconds)
return tonumber(Minutes) * 60 + tonumber(Seconds) + tonumber(Centiseconds) / 100
end
local function LRCParser(LRCText)
local Result = {
Tags = {},
Lyrics = {}
}
-- Split text by newlines.
for Line in string.gmatch(LRCText, "[^\r\n]+") do
local Minutes, Seconds, Centiseconds, Text = string.match(Line, LyricsLinePattern)
if Minutes then
if not IsStringBlank(Text) then -- Some fools like to put blank timestamped lines.
table.insert(
Result.Lyrics,
{
Text = Text,
Start = ParseTimestamp(Minutes, Seconds, Centiseconds)
}
)
end
else
local Tag, Value = string.match(Line, TagLinePattern)
if Tag then
Result.Tags[Tag] = Value
end
end
end
return Result
end
return LRCParser
In the final version of the music system a song would be represented like this.
{
TrackID = 8, -- A track ID to refer to the song with. (For the different stations)
Title = "Oh No!", -- The name of the song.
Artist = {"Marina And The Diamonds"}, -- The artists.
-- Multiple artists would be displayed as "A - SongName (ft. B, C) in the full song title.
-- The source of the .lrc file.
RawLyrics = "pastebin.com/raw/4THHhFwc",
-- This decides which type of videos to play onscreen.
-- There is also "Chill" and "Sad".
VisualGroup = "Upbeat",
-- The system supported multiple backups of the same song.
-- If one gets deleted for some reason, the system would try to play the next variant.
Variants = {
{
URL = "rbxassetid://4212393947", -- The SoundId of the song.
SongStart = 0, -- Settings to sync the song to the lyrics.
SubtitleOffset = 0,
SubtitleScaling = 1,
-- Sometimes people would upload songs at a different speed that the original, we can fix that here.
PlaybackSpeed = 0.93,
Volume = 0.3,
-- If someone uploaded a song that cuts out abruptly we can fade it out.
FadeOut = 0.1
},
{
URL = "a different sound Id for the same song",
...
}
}
},
More details:
- The .lrc files were stored online on pastebin-like websites.
- I decided this was a better idea than storing all the lyrics in the place file.
- The contents would be downloaded with
HttpService
.
- The lyrics would be filtered line by line before they were ready to be displayed to the players.
- There is a custom prefiltering step before it goes through the Roblox filter. This would usually give a better final result from the Roblox filter.
- The final filtered result is cached so it doesn’t have to go through the Roblox filter twice.
- LRC downloading and filtering is happens conservatively.
- The .lrc download and filtering process only happens once the song is the current or next song in a radio station queue.
- If there are no players listening to a station the song is playing on the song lyrics would not be loaded until someone tuned into the station.
- The system supports multiple sound Ids for a song.
- If a Roblox sound is detected to be deleted it would try to use the next version as a backup.