BPMDetector 2 - Detect Every Beat In Any Song

Hello!,

I had made a old resource called BPMDetector a few months ago, right now i bring you the new and improved BPMDetector 2 (v0.0.1) , As always, this lets you detect bpm of any song , ofcourse you would still need to tweak and experiment with the parameters for different song genres, but most of the time is accurate.

This system uses NamedSignal By @nowodev

Most notable improvements :

  • frame rate independence
  • better queue system
  • BPM Clamping
  • Robust Median for missing or double beats
  • some new arguments

DISCLAIMER : This Only Works with the new AudioApi, so very Basic Understanding of it, it’s needed

Downloads

BPMDetector2v0.0.1.rbxm (9,8 KB) > First Release v0.0.1

Showcase

example code (small camera system on beat, Using AudioGraphV2) :

-- CutsceneCamera
local ReplicatedStorage = game:GetService("ReplicatedStorage")
local RunService   = game:GetService("RunService")
local Players = game:GetService("Players")

local Modules = ReplicatedStorage:WaitForChild("Modules")

local Audio = require(Modules.Audio)
local BPMDetector  = require(Modules.BPMDetectorv2)

local Camera       = workspace.CurrentCamera
local player       = Players.LocalPlayer
local AudioPlayer =  workspace:WaitForChild("AudioPlayer", math.huge)
local character    = player.Character or player.CharacterAdded:Wait()
local rootPart     = character:WaitForChild("HumanoidRootPart")

----------------------------------------------------------
local AudioDeviceOutput = Instance.new("AudioDeviceOutput")
AudioDeviceOutput.Parent = Camera

local Analyzer = Instance.new("AudioAnalyzer")
Analyzer.Parent = AudioPlayer

local PlayerGraph = Audio.NewGraph(AudioPlayer)
PlayerGraph:ConnectConsumer(AudioDeviceOutput)
PlayerGraph:ConnectConsumer(Analyzer)

PlayerGraph:Create{
	--["AudioPitchShifter"] = {
	--	Name = "DistortionPitch",
	--	WindowSize = "Medium",
	--	Pitch = 1,
	--	Bypass = false,
	--},
	["AudioReverb"] = {
		Name = "AudioReverb_",
		DecayRatio = 0.577,
		DecayTime = 0.23,
		Density = 0.53,
	},
}

AudioPlayer.Asset = "rbxassetid://112559061735366"
AudioPlayer.AutoPlay = false

-- Detector config — tuned for the onset-detection approach.
-- ThresholdMultiplier stays low (sensitive to level).
-- RiseSensitivity is the new gate that stops false fires.

local detector = BPMDetector.new({
	Analyzer             = Analyzer,
	MinGap               = 0.27,
	WindowSize           = 23,
	ThresholdMultiplier  = 0.14,   
	RiseSensitivity      = 0.18, 
	MinBPM               = 60,
	MaxBPM               = 200,
})

local ORBIT_RADIUS   = 5
local RNG            = Random.new()


local ANGLE_POOL = {
	0, math.pi / 4, math.pi / 2, 3 * math.pi / 4,
	math.pi, -math.pi / 4, -math.pi / 2, -3 * math.pi / 4,
}
local HEIGHT_POOL = { 1.0, 1.6, 2.4, 3.2 }

local orbitAngle  = 0
local orbitHeight = 1.6
local fromAngle   = 0
local toAngle     = 0
local fromHeight  = 1.6
local toHeight    = 1.6
local blendT      = 1
local beatTimer   = 0
local secondsPerBeat = 0.5  

local function easeInOutSine(t)
	return -(math.cos(math.pi * t) - 1) / 2
end

local function pickAngle(current)
	local candidates = {}
	for _, a in ipairs(ANGLE_POOL) do
		if math.abs(a - current) > math.pi / 6 then
			table.insert(candidates, a)
		end
	end
	return candidates[RNG:NextInteger(1, #candidates)]
end

detector.OnBeat:Connect(function()
	fromAngle  = orbitAngle
	fromHeight = orbitHeight
	toAngle    = pickAngle(orbitAngle)
	toHeight   = HEIGHT_POOL[RNG:NextInteger(1, #HEIGHT_POOL)]
	blendT     = 0
	beatTimer  = 0

	local bpm      = detector:GetBPM()
	local rms      = Analyzer.RmsLevel
	local formula  = (bpm / 60) * rms + 1

	Camera.FieldOfView = RNG:NextNumber(38, 50) + formula
end)

-- optional secondsPerBeat in sync with detected tempo
detector.OnBPMUpdate:Connect(function(bpm)
	secondsPerBeat = 60 / bpm
end)

-- smooth orbit interpolation
RunService.RenderStepped:Connect(function(dt)
	beatTimer += dt
	local progress = math.clamp(beatTimer / secondsPerBeat, 0, 1)
	blendT = easeInOutSine(progress)

	orbitAngle  = fromAngle  + (toAngle  - fromAngle)  * blendT
	orbitHeight = fromHeight + (toHeight - fromHeight) * blendT

	local root   = rootPart.Position
	local camX   = root.X + math.cos(orbitAngle) * ORBIT_RADIUS
	local camZ   = root.Z + math.sin(orbitAngle) * ORBIT_RADIUS
	local camPos = Vector3.new(camX, root.Y + orbitHeight, camZ)

	Camera.CFrame = CFrame.lookAt(camPos, root + Vector3.new(0, 1.0, 0))
end)

detector:Start()
task.delay(9 ,function()
	AudioPlayer:Play()
	AudioPlayer.TimePosition = 0
end)

player.CharacterRemoving:Connect(function()
	detector:Destroy()
end)

Documentation :

Here’s a table I’ve made explaining and listing some parameters

9 Likes