Files
Techmino/Zframework/bgm.lua
2023-12-04 20:02:12 +08:00

351 lines
11 KiB
Lua

local audio=love.audio
local effectsSupported=audio.isEffectsSupported()
local nameList={}
local srcLib={}-- Stored bgm objects: {name='foo', source=bar, ...}, more info at function _addFile()
local lastLoadNames={}
local nowPlay={}
local lastPlay=NONE-- Directly stored last played bgm name(s)
local defaultBGM=false
local maxLoadedCount=3
local volume=1
local function task_setVolume(obj,ve,time,stop)
local vs=obj.vol
local t=0
while true do
t=time~=0 and math.min(t+coroutine.yield()/time,1) or 1
local v=MATH.mix(vs,ve,t)
obj.vol=v
obj.source:setVolume(v*volume)
if t==1 then
obj.volChanging=false
break
end
end
if stop then
obj.source:stop()
end
obj.volChanging=false
return true
end
local function task_setPitch(obj,pe,time)
local ps=obj.pitch
local t=0
while true do
t=time~=0 and math.min(t+coroutine.yield()/time,1) or 1
local p=MATH.mix(ps,pe,t)
obj.pitch=p
obj.source:setPitch(p)
if t==1 then
obj.pitchChanging=false
return true
end
end
end
local function task_setLowgain(obj,pe,time)
local ps=obj.lowgain
local t=0
while true do
t=time~=0 and math.min(t+coroutine.yield()/time,1) or 1
local p=MATH.mix(ps,pe,t)
obj.lowgain=p
obj.source:setFilter{type='bandpass',lowgain=obj.lowgain^9.42,highgain=obj.highgain^9.42,volume=1}
if t==1 then
obj.lowgainChanging=false
return true
end
end
end
local function task_setHighgain(obj,pe,time)
local ps=obj.highgain
local t=0
while true do
t=time~=0 and math.min(t+coroutine.yield()/time,1) or 1
local p=MATH.mix(ps,pe,t)
obj.highgain=p
obj.source:setFilter{type='bandpass',lowgain=obj.lowgain^9.42,highgain=obj.highgain^9.42,volume=1}
if t==1 then
obj.highgainChanging=false
return true
end
end
end
local function _clearTask(obj,mode)
local taskFunc=
mode=='volume' and task_setVolume or
mode=='pitch' and task_setPitch or
mode=='lowgain' and task_setLowgain or
mode=='highgain' and task_setHighgain or
'any'
TASK.removeTask_iterate(function(task)
return task.args[1]==obj and (taskFunc=='any' or task.code==taskFunc)
end,obj)
end
local function _updateSources()
local n=#lastLoadNames
while #lastLoadNames>maxLoadedCount and n>0 do
local name=lastLoadNames[n]
if srcLib[name].source and not srcLib[name].source:isPlaying() then
srcLib[name].source=srcLib[name].source:release() and nil
_clearTask(srcLib[name],'any')
end
n=n-1
end
end
local function _addFile(name,path)
if not srcLib[name] then
table.insert(nameList,name)
srcLib[name]={
name=name,path=path,source=false,
vol=0,volChanging=false,
pitch=1,pitchChanging=false,
lowgain=1,lowgainChanging=false,
highgain=1,highgainChanging=false,
}
end
end
local function _tryLoad(name)
if srcLib[name] then
local obj=srcLib[name]
if obj.source then
return true
elseif love.filesystem.getInfo(obj.path) then
obj.source=audio.newSource(obj.path,'stream')
obj.source:setLooping(true)
table.insert(lastLoadNames,1,name)
return true
else
LOG(STRING.repD("Wrong path for BGM '$1': $2",obj.name,obj.path),5)
end
elseif name then
LOG("No BGM: "..name,5)
end
end
local BGM={}
function BGM.getList() return nameList end
function BGM.getCount() return #nameList end
function BGM.setDefault(bgms)
if type(bgms)=='string' then
bgms={bgms}
elseif type(bgms)=='table' then
for i=1,#bgms do assert(type(bgms[i])=='string',"BGM list must be list of strings") end
else
error("BGM.setDefault(bgms): bgms must be string or table")
end
defaultBGM=bgms
end
function BGM.setMaxSources(count)
assert(type(count)=='number' and count>0 and count%1==0,"BGM.setMaxSources(count): count must be positive integer")
maxLoadedCount=count
_updateSources()
end
function BGM.setVol(vol)
assert(type(vol)=='number' and vol>=0 and vol<=1,"BGM.setVol(vol): count must be in range 0~1")
volume=vol
for i=1,#nowPlay do
local bgm=nowPlay[i]
if not bgm.volChanging then
bgm.source:setVolume(bgm.vol*vol)
end
end
end
function BGM.init(name,path)
if type(name)=='table' then
for k,v in next,name do
_addFile(k,v)
end
else
_addFile(name,path)
end
table.sort(nameList)
LOG(BGM.getCount().." BGM files added")
end
function BGM.play(bgms,args)
if not args then args='' end
if not bgms then bgms=defaultBGM end
if not bgms then return end
if type(bgms)=='string' then bgms={bgms} end
assert(type(bgms)=='table',"BGM.play(name,args): name must be string or table")
if
TABLE.compare(lastPlay,bgms) and
srcLib[lastPlay[1]] and srcLib[lastPlay[1]].source and
srcLib[lastPlay[1]].source:isPlaying()
then
return
end
BGM.stop()
if not STRING.sArg(args,'-preLoad') then
lastPlay=bgms
end
for i=1,#bgms do
local bgm=bgms[i]
assert(type(bgm)=='string',"BGM list can only be list of string")
if _tryLoad(bgm) and not STRING.sArg(args,'-preLoad') then
local obj=srcLib[bgms[i]]
obj.vol=0
obj.pitch=1
obj.lowgain=1
obj.highgain=1
obj.volChanging=false
obj.pitchChanging=false
obj.lowgainChanging=false
obj.highgainChanging=false
_clearTask(obj)
local source=obj.source
source:setLooping(not STRING.sArg(args,'-noloop'))
source:setPitch(1)
source:seek(0)
source:setFilter()
if STRING.sArg(args,'-sdin') then
obj.vol=1
source:setVolume(volume)
BGM.set(bgm,'volume',1,0)
else
source:setVolume(0)
BGM.set(bgm,'volume',1,.626)
end
source:play()
table.insert(nowPlay,obj)
return true
end
end
_updateSources()
end
function BGM.stop(time)
if #nowPlay>0 then
for i=1,#nowPlay do
local obj=nowPlay[i]
_clearTask(obj,'volume')
if time==0 then
obj.source:stop()
obj.volChanging=false
else
TASK.new(task_setVolume,obj,0,time or .626,true)
obj.volChanging=true
end
end
TABLE.cut(nowPlay)
lastPlay=NONE
end
end
---@param mode
---| 'volume'
---| 'lowgain'
---| 'highgain'
---| 'volume'
---| 'pitch'
---| 'seek'
function BGM.set(bgms,mode,...)
if type(bgms)=='string' then
if bgms=='all' then
bgms=nowPlay
else
bgms={srcLib[bgms]}
end
elseif type(bgms)=='table' then
bgms=TABLE.shift(bgms)
for i=1,#bgms do
assert(type(bgms[i])=='string',"BGM list must be list of strings")
bgms[i]=srcLib[bgms[i]]
end
else
error("BGM.play(name,args): name must be string or table")
end
for i=1,#bgms do
local obj=bgms[i]
if obj.source then
if mode=='volume' then
_clearTask(obj,'volume')
local vol,time=...
if not time then time=1 end
assert(type(vol)=='number' and vol>=0 and vol<=1,"BGM.set(...,volume): volume must be in range 0~1")
assert(type(time)=='number' and time>=0,"BGM.set(...,time): time must be positive number")
TASK.new(task_setVolume,obj,vol,time)
elseif mode=='pitch' then
_clearTask(obj,'pitch')
local pitch,changeTime=...
if not pitch then pitch=1 end
if not changeTime then changeTime=1 end
assert(type(pitch)=='number' and pitch>0 and pitch<=32,"BGM.set(...,pitch): pitch must be in range 0~32")
assert(type(changeTime)=='number' and changeTime>=0,"BGM.set(...,time): time must be positive number")
TASK.new(task_setPitch,obj,pitch,changeTime)
elseif mode=='seek' then
local time=...
assert(type(time)=='number',"BGM.set(...,time): time must be number")
obj.source:seek(MATH.clamp(time,0,obj.source:getDuration()))
elseif mode=='lowgain' then
if effectsSupported then
_clearTask(obj,'lowgain')
local lowgain,changeTime=...
if not lowgain then lowgain=1 end
if not changeTime then changeTime=1 end
assert(type(lowgain)=='number' and lowgain>=0 and lowgain<=1,"BGM.set(...,lowgain,highgain): lowgain must be in range 0~1")
assert(type(changeTime)=='number' and changeTime>=0,"BGM.set(...,time): time must be positive number")
TASK.new(task_setLowgain,obj,lowgain,changeTime)
obj.lowgain=lowgain
obj.source:setFilter{type='bandpass',lowgain=obj.lowgain,highgain=obj.highgain,volume=1}
end
elseif mode=='highgain' then
if effectsSupported then
_clearTask(obj,'highgain')
local highgain,changeTime=...
if not highgain then highgain=1 end
if not changeTime then changeTime=1 end
assert(type(highgain)=='number' and highgain>=0 and highgain<=1,"BGM.set(...,lowgain,highgain): highgain must be in range 0~1")
assert(type(changeTime)=='number' and changeTime>=0,"BGM.set(...,time): time must be positive number")
TASK.new(task_setHighgain,obj,highgain,changeTime)
obj.highgain=highgain
obj.source:setFilter{type='bandpass',lowgain=obj.lowgain,highgain=obj.highgain,volume=1}
end
else
error("BGM.set(...,mode): mode must be 'volume', 'pitch', or 'seek'")
end
end
end
end
function BGM.getPlaying()
return TABLE.shift(lastPlay)
end
function BGM.isPlaying()
return #nowPlay>0 and nowPlay[1].source:isPlaying()
end
function BGM.tell()
if nowPlay[1] then
return nowPlay[1].source:tell()
end
end
function BGM.getDuration()
if nowPlay[1] then
return nowPlay[1].source:getDuration()
end
end
return BGM