How to find and loop sections of a music track in your game

Who is this for?

This tutorial is geared towards newer developers who may have used Roblox stock audio as background music, but would like to get more sophisticated with music in their game.

What this tutorial shows

We show how to take a beat-based song (like a track of dance music), find the loop points of the sections of the song, and play those sections as loops in different areas of your game, allowing gameplay with continuous music that changes when you move from location to location, but with the continuity of the same song. For example, triggering the "intro" in one area, the "verse" in another, and the "chorus" the last. Like this video:

In the tutorial we use Audioscape’s generative music, but our code works for Roblox stock music as well. @jackjenningsdev has a great post on finding music in Roblox’s audio library and getting details from the APM site.

Video

Feel free to skip down to the Video below. It has all the information you need.

Overview: looping beat-based music

Most beat-based songs like dance music are based on 8 bar phrases. You can figure out the timing of the phrases from the tempo of the song. In this example below, a tempo of 120bpm (beats per minute) results in 8 bar phrases being 16 seconds long. Here is what a song at 120 bpm might look like:

image

The intro section loops from 0 to 16 seconds, the verse from 16 to 32 seconds, and the chorus from 32 to 48 seconds. So in the sample code (see below), if we had 3 versions of the same track and wanted these three different loop points, we would set the appropriate LoopRegions:

tracks[1].LoopRegion = NumberRange.new(0, 16) — Intro
tracks[2].LoopRegion = NumberRange.new(16, 32) — Verse
tracks[3].LoopRegion = NumberRange.new(32, 48) — Chorus

Video Tutorial

Here is the YouTube video that takes you through all the steps (scroll down for an index and our sample code).

VideoThumbnail

(skip to 4:37 if you are using Roblox stock music)

00:00 Intro
00:16 Logging into Audioscape
00:19 Making a track in Audioscape
01:00 Downloading the song
01:56 Getting our sample code
02:28 Getting the loop points from our help page
02:37 Roblox Studio – starting a blank Faceplate game
03:05 Uploading your track into Roblox Studio
03:40 Inserting the track using SoundService and making copies
04:37 Grabbing our sample code and pasting it into a new script
05:04 Changing the Audioscape code to use your uploaded track
05:19 Setting up loop regions for your track
06:22 Setting up game triggers (checkpoints) to play the track sections
07:38 Testing our sound triggers in your game
08:10 Making your game triggers transparent
08:48 Conclusion

Sample Code

Sample code for integrating loop-based music in your game
local fullSound = 1 -- Loudest volume a track should play
local minSound = 0.01 -- Volume of a track that is not currently in focus
local transitionSeconds = 5.0 -- How long to fade in/out clips when you touch an object that changes the track
local slowDownSeconds = 5 -- How long to slow the song over (on death, for example)
local fadeInTime = 5 -- How long to start fading in a track with itself (self looped cross-fade)
local fadeOutTime = 5 -- How long to start fading out a track with itself (self looped cross-fade)


-- Initialize the tracks list, setting up each to be looped and all play at the same time
local SoundService = game:GetService("SoundService")

-- List here all of the tracks/versions of tracks you have imported to the SoundService
local tracks = {
    SoundService.NAME_OF_YOUR_FIRST_TRACK,
    SoundService.NAME_OF_YOUR_SECOND_TRACK,
    SoundService.NAME_OF_YOUR_THIRD_TRACK,
}
-- Get the loop regions from our help pages. This is one example for a cinematic track
tracks[1].LoopRegion = NumberRange.new(197, 265) -- Field
tracks[2].LoopRegion = NumberRange.new(0, 88) -- Bridge
tracks[3].LoopRegion = NumberRange.new(0, 195) -- Door 64 sec and 128

for index, track in (tracks) do
    track.Volume = minSound
    track.Looped = true
    track.PlaybackRegionsEnabled = true
    track.TimePosition = track.LoopRegion.Min
    track.Volume = 0
    track:Play()
end

local lastNumberPlayed = 0 -- Prevents re-setting play head when sitting at a block



-- Set a track (by index in tracks) to a target speed over t seconds
function slowDown(soundIndex, targetSpeed, t)
    local tweenservice = game:GetService("TweenService") -- getting the tween service
    -- Increase/decrease the playback speed to targetSpeed over t seconds
    local tween = tweenservice:Create(tracks[soundIndex], TweenInfo.new(t), {PlaybackSpeed = targetSpeed})
    tween:Play() -- Starting the background fading
end

-- Used to slow down the sound of all tracks, good to play for death events (like hitting a kill brick)
-- Sets the track speed to target speed over t seconds
function slowAllTracks(targetSpeed, t)
    for index, track in tracks do
   	 slowDown(index, targetSpeed, t)
    end
end

-- Increase the sound volume of one of the tracks, while decreasing the volume of all other tracks
function increaseSoundVolume(toIncrease, duration)
    local tweenservice = game:GetService("TweenService") -- getting the tween service
    for index, track in tracks do
   	 local targetVolume = minSound
   	 if (index == toIncrease) then
   		 print("Increasing the volume of track ", index, track)
   		 targetVolume = fullSound
   	 end
   	 -- Increase/decrease the volume over the rampUpTime for the track
   	 local tween = tweenservice:Create(track, TweenInfo.new(transitionSeconds), {Volume = targetVolume})
   	 tween:Play() -- Starting the background fading
    end
end


-- Fading a loop in with itself, uses lastNumberPlayed local variable to keep track of the sound that is playing
local runService = game:GetService("RunService")
local isSelfFading = false
local selfFadingNumber = 0
runService.Heartbeat:Connect(function (dt)
    if (lastNumberPlayed ~= 0) then
   	 local currentTrack = tracks[lastNumberPlayed]
   	 local timeRemaining = currentTrack.LoopRegion.Max - (currentTrack.TimePosition)

   	 local offset = currentTrack.LoopRegion.Min
   	 local currentTime = currentTrack.TimePosition - offset
   	 local tweenservice = game:GetService("TweenService") -- getting the tween service

   	 if (selfFadingNumber ~= lastNumberPlayed or not isSelfFading) then
   		 -- Fade in
   		 if currentTime <= fadeInTime then
   			 -- Fade in the track for fadeInTime seconds
   			 local tween = tweenservice:Create(currentTrack, TweenInfo.new(fadeInTime), {Volume = fullSound}) -- making the tween a variable for further use
   			 tween.Completed:Connect(function()
   				 isSelfFading = false
   				 print("Tween completed!")
   			 end)
   			 tween:Play() -- playing the tween
   		 elseif timeRemaining <= fadeOutTime then
   			 -- Fade out the track before it ends
   			 local tween = tweenservice:Create(currentTrack, TweenInfo.new(timeRemaining), {Volume = minSound}) -- making the tween a variable for further use
   			 tween.Completed:Connect(function()
   				 isSelfFading = false
   				 print("Tween completed!")
   			 end)
   			 tween:Play() -- playing the tween
   		 end
   		 isSelfFading = true
   		 selfFadingNumber = lastNumberPlayed
   	 end


    end
end)

-- Attach tracks to different objects named "Section1", "Section2", etc, for as many tracks/positions you want. Put these in Workspace -> "Checkpoints" folder -> Section1, Section2,......
local workspace = script.Parent

for key, value in (workspace.Checkpoints:GetChildren()) do
    local number = tonumber(string.split(value.Name, "Section")[2]) -- Get number from the checkpoint name, Checkpoint1 plays the second section
    -- When a player touches the block with that value (SectionX), if that song section isn't the one playing, fade it in
    value.Touched:connect(function(hit)
   	 if hit and hit.Parent and hit.Parent:FindFirstChild("Humanoid") then
   		 --slowAllTracks(1, 0.1), use this to slow tracks down over time, like if you hit a kill brick (see "ObbyoScape" example)
   		 if (lastNumberPlayed ~= number) then
   			 increaseSoundVolume(number, transitionSeconds)
   		 end
   		 lastNumberPlayed = number -- Store the value of the current playing track
   	 end
    end)
end

More ideas?

I'd love to hear your feedback -- there may be better efficient ways to code this. I can also post a tutorial on how to find the loop points in an audio track that isn't at 120BPM. Let me know.

Thanks!

OffGridDude

11 Likes

To use other bpm would you just take 60,000 divided by the bpm then multiply it by 4 for a bar length in 4/4 time?

2 Likes

Assuming the song sections are 8 are long (sometimes they are 16), you can calculate the length of song sections using this formula below.

In this case, we are assuming the music has 4 beats per bar, and each section is 8 bars long.

If a song has a tempo of 96bpm the calculation would look like this:

And each section would be 20 seconds long.

Note, if you want to look up the BPM of a Roblox MarketplaceAPM track, you need to go to the APM site, look up the track, and find its BPM there (see @jackjenningsdev’s post for instructions on how to look up a track on the APM website. There, in the track info, you’ll find the BPM).

Happy Looping, @kbcubezz !

2 Likes

Thanks. Great add on to the tutorial. Most people will need that extra bit.

2 Likes