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)