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)

10 Likes

Smart usage of the Audio API. Very cool stuff.

1 Like