BPM Formula And BPM Timer Module

First resource, Maybe it can be useful to someone, I’ve found a way to calculate a constant-changing bpm on a song

This is using the new Audio api, but maybe it can be adapted to playbackloudness


local BPM_Timer: RBXScriptConnection

local BPMWindowSec = 36        -- how many seconds of beats we average over
local threshold    = 0.19       -- tune: minimum RMS (or Peak) to count as a beat
local minGap       = 0.43    -- minimum seconds between beats

local lastBeatTime = 0
local beatTimes    = {}

BPM_Timer = RunService.Heartbeat:Connect(function(dt)
	local now   = tick()
	local level = Analyzer.RmsLevel

	if level >= threshold and (now - lastBeatTime) >= minGap then
		lastBeatTime = now
		table.insert(beatTimes, now)
		
		warn("beat happened here")
		
	end

	if #beatTimes > 0 and (now - beatTimes[1]) > BPMWindowSec then
		table.remove(beatTimes, 1)
	end

	if #beatTimes >= 2 then
		local sumDt = 0
		for i = 2, #beatTimes do
			sumDt = sumDt + (beatTimes[i] - beatTimes[i-1])
		end
		local avgDt = sumDt / (#beatTimes - 1)
		local bpm   = 60 / avgDt
		print(string.format("≈ %.1f BPM", bpm))
	end
end)


Edit : maybe you will need to tune the bpm’er settings for specific songs, In my case, it works fine for every song (mostly)

Should I make it into a module?

  • Yes
  • No Need
0 voters

Edit 2 : So I decided to make it into a module, I’ve also tried to implement a auto-threshold changing, for notice , It may be unstable, But still

-- BPMDetectorModule.lua by spook
local RunService = game:GetService("RunService")
local Signal = require(script:WaitForChild("SimpleSignal"))

local BPMDetector = {}
BPMDetector.__index = BPMDetector

function BPMDetector.new(config)
	local self = setmetatable({}, BPMDetector)

	self.Analyzer = config.Analyzer
	assert(self.Analyzer, "AudioAnalyzer is required")

	self.MinGap = config.MinGap or 0.3
	self.WindowSize = config.WindowSize or 10
	self.ThresholdMultiplier = config.ThresholdMultiplier or 1.2

	self._beatTimes = {}
	self._rmsHistory = {}
	self._lastBeatTime = 0
	self._lastRms = 0
	self._running = false
	self._lastBpm = 0
	
	self.OnBeat = Signal.new()
	self.OnBPMUpdate = Signal.new()

	return self
end

function BPMDetector:Start()
	if self._running then return end
	self._running = true

	self._conn = RunService.Heartbeat:Connect(function()
		self:_Process()
	end)
end

function BPMDetector:Stop()
	if self._conn then
		self._conn:Disconnect()
		self._conn = nil
	end
	self._running = false
end

function BPMDetector:_Process()
	local now = tick()
	local rms = self.Analyzer.RmsLevel

	-- update RMS history
	table.insert(self._rmsHistory, rms)
	if #self._rmsHistory > self.WindowSize * 60 then
		table.remove(self._rmsHistory, 1)
	end

	local sum = 0
	for _, v in ipairs(self._rmsHistory) do
		sum += v
	end
	local mean = sum / math.max(1, #self._rmsHistory)
	local threshold = mean * self.ThresholdMultiplier

	-- detect beat
	if rms > threshold and (now - self._lastBeatTime) >= self.MinGap then
		self._lastBeatTime = now
		table.insert(self._beatTimes, now)
		self.OnBeat:Fire()

		while #self._beatTimes > 0 and (now - self._beatTimes[1]) > self.WindowSize do
			table.remove(self._beatTimes, 1)
		end

		if #self._beatTimes >= 2 then
			local total = 0
			for i = 2, #self._beatTimes do
				total += (self._beatTimes[i] - self._beatTimes[i - 1])
			end
			local avg = total / (#self._beatTimes - 1)
			local bpm = 60 / avg
			
			self._lastBpm = bpm
			self.OnBPMUpdate:Fire(bpm)
		end
	end
	
end

return BPMDetector

Example :

local detector = BpmTimer.new({
	Analyzer = Analyzer,
	MinGap = 0.21,
	WindowSize = 10, -- seconds
	ThresholdMultiplier = 0.25
})

detector.OnBeat:Connect(function()
	print("🎵")
	
	spr.stop(Camera, "FieldOfView")
	Camera.FieldOfView = RNG:NextNumber(39,50)
	spr.target(Camera, 1, 4.12, {
		FieldOfView = 70
	})
	
end)

detector.OnBPMUpdate:Connect(function(bpm : number)
	print(string.format("Detected BPM: %.1f", bpm))
end)

detector:Start()

Uses SimpleSignal

BpmModule.rbxm (6.5 KB)

15 Likes

Smart usage of the Audio API. Very cool stuff.

2 Likes

what’s spr?

what does that mean?

Not much related but it’s just a spring module, If you want, you can put your own logic or functions inside the OnBeat function

ahh okay, thanks.

Ngl, this is GREAT for certain things with sounds, like pulsing effects for visual elements.

Will it beat the ol’ reliable PlaybackLoudness? idk.

1 Like