BPM Timing Module

a personal module I made for myself. (so don’t expect it to be perfect)
it takes your sound and bpm, and then allows you to set callbacks for every time a beat or step happens, useful for rhythm games and such.

Code

local timing = {}
timing.__index = timing

local runService = game:FindService("RunService")

function timing.new(soundInstance: Sound)
    return setmetatable({
        bpm = 60,
        
        beat = 0,
        step = 0, -- beat * 4
        
        prevBeat = 0,
        prevStep = 0, -- beat * 4        
        
        soundInstance = soundInstance,
        playing = false,
    }, timing)
end

function timing:setBPM(bpm)
    if type(bpm) ~= "number" then
        return warn("inputted bpm isn't a number!!")  -- why would you even do this anyway LOL    
    end
    
    self.bpm = bpm
end

function timing:Play()
    self.playing = true
    self.soundInstance:Play()
end

function timing:Stop()
    self.playing = false
    self.soundInstance:Stop()
end

function timing:Update()    
    local connection = runService.RenderStepped:Connect(function(deltaTime)
        if not self.playing then return warn("no sound playing!! can't update.") end
        
        local songPosition = self.soundInstance.TimePosition * 1000
        
        local beat = (songPosition / 1000) * (self.bpm / 60)
        local roundedBeat = math.round(beat)

        local step = beat * 4
        local roundedStep = math.round(step)
        
        if self.prevBeat ~= roundedBeat then
            task.spawn(self.beatCallback, roundedBeat)
        end
        
        if self.prevStep ~= roundedStep then
            task.spawn(self.stepCallback, roundedStep)
        end
        
        self.beat = roundedBeat
        self.step = roundedStep

        self.prevBeat = self.beat
        self.prevStep = self.step
    end)
end

function timing:SetBeatCallback(callback)
    if type(callback) ~= "function" then
        return warn("callback isn't a function!!") -- again, why
    end    
    self.beatCallback = callback
end

function timing:SetStepCallback(callback)
    if type(callback) ~= "function" then
        return warn("callback isn't a function!!") -- AGAIN, WHY????
    end    
    self.stepCallback = callback
end

return timing

Example

local ReplicatedStorage = game:GetService("ReplicatedStorage")
local TweenService = game:GetService("TweenService")

local Timer = require(ReplicatedStorage.timer)

local Frame = script.Parent.Frame
local Scale = Frame.UIScale

local NewTimer = Timer.new(script.Parent.Song)
NewTimer:setBPM(115)
NewTimer:Update()

NewTimer:SetBeatCallback(function(beat)
    Scale.Scale = 1.25
    TweenService:Create(Scale, TweenInfo.new(0.45, Enum.EasingStyle.Exponential, Enum.EasingDirection.Out), {
        Scale = 1
    }):Play()
end)

NewTimer:SetStepCallback(function(step)
    Frame.TextLabel.Text = `BPM: {NewTimer.bpm}\n Beat: {NewTimer.beat}\n Step: {step}`
end)

NewTimer:Play()

Preview

10 Likes