Initial Commit

* Basic Game Implementation
This commit is contained in:
2024-08-07 02:48:17 +05:30
commit 6baf6c48b0
26 changed files with 2560 additions and 0 deletions

335
libs/lovebpm.lua Normal file
View File

@@ -0,0 +1,335 @@
--
-- lovebpm
--
-- Copyright (c) 2016 rxi
--
-- This library is free software; you can redistribute it and/or modify it
-- under the terms of the MIT license. See LICENSE for details.
--
local lovebpm = { _version = "0.0.0" }
local Track = {}
Track.__index = Track
function lovebpm.newTrack()
local self = setmetatable({}, Track)
self.source = nil
self.offset = 0
self.volume = 1
self.pitch = 1
self.looping = false
self.listeners = {}
self.period = 60 / 120
self.lastBeat = nil
self.lastUpdateTime = nil
self.lastSourceTime = 0
self.time = 0
self.totalTime = 0
self.dtMultiplier = 1
return self
end
function lovebpm.detectBPM(filename, opts)
-- Init options table
opts = opts or {}
local t = { minbpm = 75, maxbpm = 300 }
for k, v in pairs(t) do
t[k] = opts[k] or v
end
opts = t
-- Load data
local data = filename
if type(data) == "string" then
data = love.sound.newSoundData(data)
else
data = filename
end
local channels = data:getChannels()
local samplerate = data:getSampleRate()
-- Gets max amplitude over a number of samples at `n` seconds
local function getAmplitude(n)
local count = samplerate * channels / 200
local at = n * channels * samplerate
if at + count > data:getSampleCount() then
return 0
end
local a = 0
for i = 0, count - 1 do
a = math.max(a, math.abs( data:getSample(at + i) ))
end
return a
end
-- Get track duration and init results table
local dur = data:getDuration("seconds")
local results = {}
-- Get maximum allowed BPM
local step = 8
local n = (dur * opts.maxbpm / 60)
n = math.floor(n / step) * step
-- Fill table with BPMs and their average on-the-beat amplitude until the
-- minimum allowed BPM is reached
while true do
local bpm = n / dur * 60
if bpm < opts.minbpm then
break
end
local acc = 0
for i = 0, n - 1 do
acc = acc + getAmplitude(dur / n * i)
end
-- Round BPM to 3 decimal places
bpm = math.floor(bpm * 1000 + .5) / 1000
-- Add result to table
table.insert(results, { bpm = bpm, avg = acc / n })
n = n - step
end
-- Sort table by greatest average on-the-beat amplitude. The one with the
-- greatest average is assumed to be the correct bpm
table.sort(results, function(a, b) return a.avg > b.avg end)
return results[1].bpm
end
function Track:load(filename)
-- Deinit old source
self:stop()
-- Init new source
-- "static" mode is used here instead of "stream" as the time returned by
-- :tell() seems to go out of sync after the first loop otherwise
self.source = love.audio.newSource(filename, "static")
self:setLooping(self.looping)
self:setVolume(self.volume)
self:setPitch(self.pitch)
self.totalTime = self.source:getDuration("seconds")
self:stop()
return self
end
function Track:setBPM(n)
self.period = 60 / n
return self
end
function Track:setOffset(n)
self.offset = n or 0
return self
end
function Track:setVolume(volume)
self.volume = volume or 1
if self.source then
self.source:setVolume(self.volume)
end
return self
end
function Track:setPitch(pitch)
self.pitch = pitch or 1
if self.source then
self.source:setPitch(self.pitch)
end
return self
end
function Track:setLooping(loop)
self.looping = loop
if self.source then
self.source:setLooping(self.looping)
end
return self
end
function Track:on(name, fn)
self.listeners[name] = self.listeners[name] or {}
table.insert(self.listeners[name], fn)
return self
end
function Track:emit(name, ...)
if self.listeners[name] then
for i, fn in ipairs(self.listeners[name]) do
fn(...)
end
end
return self
end
function Track:play(restart)
if not self.source then return self end
if self.restart then
self:stop()
end
self.source:play()
return self
end
function Track:pause()
if not self.source then return self end
self.source:pause()
return self
end
function Track:stop()
self.lastBeat = nil
self.time = 0
self.lastUpdateTime = nil
self.lastSourceTime = 0
if self.source then
self.source:stop()
end
return self
end
function Track:setTime(n)
if not self.source then return end
self.source:seek(n)
self.time = n
self.lastSourceTime = n
self.lastBeat = self:getBeat() - 1
return self
end
function Track:setBeat(n)
return self:setTime(n * self.period)
end
function Track:getTotalTime()
return self.totalTime
end
function Track:getTotalBeats()
if not self.source then
return 0
end
return math.floor(self:getTotalTime() / self.period + 0.5)
end
function Track:getTime()
return self.time
end
function Track:getBeat(multiplier)
multiplier = multiplier or 1
local period = self.period * multiplier
return math.floor(self.time / period), (self.time % period) / period
end
function Track:update()
if not self.source then return self end
-- Get delta time: getTime() is used for time-keeping as the value returned by
-- :tell() is updated at a potentially lower rate than the framerate
local t = love.timer.getTime()
local dt = self.lastUpdateTime and (t - self.lastUpdateTime) or 0
self.lastUpdateTime = t
-- Set new time
local time
if self.source:isPlaying() then
time = self.time + dt * self.dtMultiplier * self.pitch
else
time = self.time
end
-- Get source time and apply offset
local sourceTime = self.source:tell("seconds")
sourceTime = sourceTime + self.offset
-- If the value returned by the :tell() function has updated we check to see
-- if we are in sync within an allowed threshold -- if we're out of sync we
-- adjust the dtMultiplier to resync gradually
if sourceTime ~= self.lastSourceTime then
local diff = time - sourceTime
-- Check if the difference is beyond the threshold -- If the difference is
-- too great we assume the track has looped and treat it as being within the
-- threshold
if math.abs(diff) > 0.01 and math.abs(diff) < self.totalTime / 2 then
self.dtMultiplier = math.max(0, 1 - diff * 2)
else
self.dtMultiplier = 1
end
self.lastSourceTime = sourceTime
end
-- Assure time is within proper bounds in case the offset or added
-- frame-delta-time made it overshoot
time = time % self.totalTime
-- Calculate deltatime and emit update event; set time
if self.lastBeat then
local t = time
if t < self.time then
t = t + self.totalTime
end
self:emit("update", t - self.time)
else
self:emit("update", 0)
end
self.time = time
-- Current beat doesn't match last beat?
local beat = self:getBeat()
local last = self.lastBeat
if beat ~= last then
-- Last beat is set here as one of the event handlers can potentially set it
-- by calling :setTime() or :setBeat()
self.lastBeat = beat
-- Assure that the `beat` event is done once for each beat, even in cases
-- where more than one beat has passed since the last update, or the song
-- has looped
local total = self:getTotalBeats()
local b = beat
local x = 0
if last then
x = last + 1
-- If the last beat is greater than the current beat then the song has
-- reached the end: if we're looping then set the current beat to after
-- the tracks's end so incrementing towards it still works.
if x > b then
if self.looping then
self:emit("loop")
b = b + total
else
self:emit("end")
self:stop()
end
end
end
-- Emit beat event for each passed beat
while x <= b do
self:emit("beat", x % total)
x = x + 1
end
end
return self
end
return lovebpm