forked from 26F-Studio/Zenitha
-
Notifications
You must be signed in to change notification settings - Fork 0
/
Copy pathbgm.lua
438 lines (395 loc) · 12.7 KB
/
bgm.lua
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
---@class Zenitha.BgmObj
---@field name string
---@field path string
---@field source love.Source|false
---@field vol number
---@field volChanging boolean
---@field pitch number
---@field pitchChanging boolean
---@field lowgain number
---@field lowgainChanging boolean
---@field highgain number
---@field highgainChanging boolean
local audio=love.audio
local effectsSupported=audio.isEffectsSupported()
local ins=table.insert
---@type string[]
local nameList={}
---@type string[]
local lastLoadNames={}
---@type string[]
local lastPlay=NONE
---@type table<string, Zenitha.BgmObj>
local srcLib={}
---@type Zenitha.BgmObj[]
local nowPlay={}
---@type false|string|string[]
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.lerp(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.lerp(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.lerp(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.lerp(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:release()
srcLib[name].source=false
_clearTask(srcLib[name],'any')
end
n=n-1
end
end
local function _addFile(name,path)
if not srcLib[name] then
ins(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)
ins(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={}
---Get the loaded BGMs' name list, READ ONLY
---@return table
function BGM.getList() return nameList end
---Get the loaded BGMs' count
---@return number
function BGM.getCount() return #nameList end
---Set the default BGM(s) to play when `BGM.play()` is called without arguments
---@param bgms string|string[]
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.setDefault(bgms): Need string|list<string>") end
else
error("BGM.setDefault(bgms): Need string|list<string>")
end
defaultBGM=bgms
end
---Set the max count of loaded BGMs
---
---When loaded BGMs' count exceeds this value, some not-playing BGM source will be released
---@param count number
function BGM.setMaxSources(count)
assert(type(count)=='number' and count>0 and count%1==0,"BGM.setMaxSources(count): Need int >=1")
maxLoadedCount=count
_updateSources()
end
---Set BGM volume
---@param vol number
function BGM.setVol(vol)
assert(type(vol)=='number' and vol>=0 and vol<=1,"BGM.setVol(vol): Need in [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
---Load BGM(s) from file(s)
---@param name string|string[]
---@param path string
---@overload fun(map:table<string, string>)
function BGM.load(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
---Play BGM(s), stop previous playing BGM(s) if exists
---Multi-channel BGMs must be exactly same length, all sources will be set to loop mode
---@param bgms? false|string|string[]
---@param args? string|'-preLoad'|'-noloop'|'-sdin'
function BGM.play(bgms,args)
if not bgms then bgms=defaultBGM end
if not bgms then return end
if not args then args='' end
bgms=type(bgms)=='string' and {bgms} or type(bgms)=='table' and bgms or {false}
for i=1,#bgms do
if type(bgms[i])~='string' then
error("BGM.play(bgms,args): bgms need string|list<string>")
end
end
if
TABLE.equal(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
if STRING.sArg(args,'-preLoad') then
for _,bgm in next,bgms do
_tryLoad(bgm)
end
else
local sourceReadyToPlay={}
for _,bgm in next,bgms do
if _tryLoad(bgm) then
local obj=srcLib[bgm]
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)
---@type love.Source
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
ins(sourceReadyToPlay,source)
table.insert(nowPlay,obj)
end
end
for i=1,#sourceReadyToPlay do
sourceReadyToPlay[i]:play()
end
end
_updateSources()
return true
end
---Stop current playing BGM(s), fade out if time is given
---@param time? nil|number
function BGM.stop(time)
assert(time==nil or type(time)=='number' and time>=0,"BGM.stop(time): Need >=0")
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.clear(nowPlay)
lastPlay=NONE
end
end
---Set (current playing) BGM(s) states
---@param bgms 'all'|string|string[]
---@param mode 'volume'|'lowgain'|'highgain'|'volume'|'pitch'|'seek'
---@param ... any
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.copy(bgms)
for i=1,#bgms do
if type(bgms[i])~='string' then
error("BGM.set(bgms,mode,...): bgms need string|list<string>")
end
bgms[i]=srcLib[bgms[i]]
end
else
error("BGM.set(bgms,mode,...): bgms need string|list<string>")
end
for i=1,#bgms do
local obj=bgms[i]
if obj and 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): Need in [0,1]")
assert(type(time)=='number' and time>=0,"BGM.set(...,time): Need >=0")
TASK.new(task_setVolume,obj,vol,time)
elseif mode=='pitch' then
_clearTask(obj,'pitch')
local pitch,timeUse=...
if not pitch then pitch=1 end
if not timeUse then timeUse=1 end
assert(type(pitch)=='number' and pitch>0 and pitch<=32,"BGM.set(...,pitch): Need in (0,32]")
assert(type(timeUse)=='number' and timeUse>=0,"BGM.set(...,time): Need >=0")
TASK.new(task_setPitch,obj,pitch,timeUse)
elseif mode=='seek' then
local time=...
assert(type(time)=='number',"BGM.set(...,time): Need number")
obj.source:seek(MATH.clamp(time,0,obj.source:getDuration()))
elseif mode=='lowgain' then
if effectsSupported then
_clearTask(obj,'lowgain')
local lowgain,timeUse=...
if not lowgain then lowgain=1 end
if not timeUse then timeUse=1 end
assert(type(lowgain)=='number' and lowgain>=0 and lowgain<=1,"BGM.set(...,lowgain): Need in [0,1]")
assert(type(timeUse)=='number' and timeUse>=0,"BGM.set(...,time): Need >=0")
TASK.new(task_setLowgain,obj,lowgain,timeUse)
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,timeUse=...
if not highgain then highgain=1 end
if not timeUse then timeUse=1 end
assert(type(highgain)=='number' and highgain>=0 and highgain<=1,"BGM.set(...,highgain): Need in [0,1]")
assert(type(timeUse)=='number' and timeUse>=0,"BGM.set(...,time): Need >=0")
TASK.new(task_setHighgain,obj,highgain,timeUse)
obj.highgain=highgain
obj.source:setFilter{type='bandpass',lowgain=obj.lowgain,highgain=obj.highgain,volume=1}
end
else
error("BGM.set(...,mode): Need 'volume'|'lowgain'|'highgain'|'volume'|'pitch'|'seek'")
end
end
end
end
---Get (current playing) BGM(s) name list
---@return string[]
function BGM.getPlaying()
return TABLE.copy(lastPlay)
end
---Get if BGM playing now
---@return boolean
function BGM.isPlaying()
return #nowPlay>0 and nowPlay[1].source:isPlaying()
end
---Get time of BGM playing now, 0 if not exists
---@return number|0
function BGM.tell()
local src=nowPlay[1] and nowPlay[1].source
if src then
return src:tell()%src:getDuration() -- bug of love2d, tell() may return value greater than duration
else
return 0
end
end
---Get duration of BGM playing now, 0 if not exists
---@return number|0
function BGM.getDuration()
if nowPlay[1] then
return nowPlay[1].source:getDuration()
else
return 0
end
end
return BGM