-
Notifications
You must be signed in to change notification settings - Fork 3k
/
Copy pathSoundControl.java
327 lines (273 loc) · 10.2 KB
/
SoundControl.java
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
package mindustry.audio;
import arc.*;
import arc.audio.*;
import arc.audio.Filters.*;
import arc.files.*;
import arc.math.*;
import arc.math.geom.*;
import arc.struct.*;
import arc.util.*;
import mindustry.*;
import mindustry.content.*;
import mindustry.game.EventType.*;
import mindustry.gen.*;
import static mindustry.Vars.*;
/** Controls playback of multiple audio tracks.*/
public class SoundControl{
public float finTime = 120f, foutTime = 120f, musicInterval = 3f * Time.toMinutes, musicChance = 0.8f, musicWaveChance = 0.46f;
/** normal, ambient music, plays at any time */
public Seq<Music> ambientMusic = Seq.with();
/** darker music, used in times of conflict */
public Seq<Music> darkMusic = Seq.with();
/** music used explicitly after boss spawns */
public Seq<Music> bossMusic = Seq.with();
protected Music lastRandomPlayed;
protected Interval timer = new Interval(4);
protected long lastPlayed;
protected @Nullable Music current;
protected float fade;
protected boolean silenced;
protected AudioBus uiBus = new AudioBus();
protected boolean wasPlaying;
protected AudioFilter filter = new BiquadFilter(){{
set(0, 500, 1);
}};
protected ObjectMap<Sound, SoundData> sounds = new ObjectMap<>();
public SoundControl(){
Events.on(ClientLoadEvent.class, e -> reload());
//only run music 10 seconds after a wave spawns
Events.on(WaveEvent.class, e -> Time.run(Mathf.random(8f, 15f) * 60f, () -> {
boolean boss = state.rules.spawns.contains(group -> group.getSpawned(state.wave - 2) > 0 && group.effect == StatusEffects.boss);
if(boss){
playOnce(bossMusic.random(lastRandomPlayed));
}else if(Mathf.chance(musicWaveChance)){
playRandom();
}
}));
setupFilters();
Events.on(ResetEvent.class, e -> lastPlayed = Time.millis());
}
protected void setupFilters(){
Core.audio.soundBus.setFilter(0, filter);
Core.audio.soundBus.setFilterParam(0, Filters.paramWet, 0f);
}
protected void reload(){
current = null;
fade = 0f;
ambientMusic = Seq.with(Musics.game1, Musics.game3, Musics.game6, Musics.game8, Musics.game9, Musics.fine);
darkMusic = Seq.with(Musics.game2, Musics.game5, Musics.game7, Musics.game4);
bossMusic = Seq.with(Musics.boss1, Musics.boss2, Musics.game2, Musics.game5);
//setup UI bus for all sounds that are in the UI folder
for(var sound : Core.assets.getAll(Sound.class, new Seq<>())){
var file = Fi.get(Core.assets.getAssetFileName(sound));
if(file.parent().name().equals("ui")){
sound.setBus(uiBus);
}
}
Events.fire(new MusicRegisterEvent());
}
public void loop(Sound sound, float volume){
if(Vars.headless) return;
loop(sound, Core.camera.position, volume);
}
public void loop(Sound sound, Position pos, float volume){
if(Vars.headless) return;
float baseVol = sound.calcFalloff(pos.getX(), pos.getY());
float vol = baseVol * volume;
SoundData data = sounds.get(sound, SoundData::new);
data.volume += vol;
data.volume = Mathf.clamp(data.volume, 0f, 1f);
data.total += baseVol;
data.sum.add(pos.getX() * baseVol, pos.getY() * baseVol);
}
public void stop(){
silenced = true;
if(current != null){
current.stop();
current = null;
fade = 0f;
}
}
/** Update and play the right music track.*/
public void update(){
boolean paused = state.isGame() && Core.scene.hasDialog();
boolean playing = state.isGame();
//check if current track is finished
if(current != null && !current.isPlaying()){
current = null;
fade = 0f;
}
//fade the lowpass filter in/out, poll every 30 ticks just in case performance is an issue
if(timer.get(1, 30f)){
Core.audio.soundBus.fadeFilterParam(0, Filters.paramWet, paused ? 1f : 0f, 0.4f);
}
//play/stop ordinary effects
if(playing != wasPlaying){
wasPlaying = playing;
if(playing){
Core.audio.soundBus.play();
setupFilters();
}else{
//stopping a single audio bus stops everything else, yay!
Core.audio.soundBus.stop();
//play music bus again, as it was stopped above
Core.audio.musicBus.play();
Core.audio.soundBus.play();
}
}
Core.audio.setPaused(Core.audio.soundBus.id, state.isPaused());
if(state.isMenu()){
silenced = false;
if(ui.planet.isShown()){
play(ui.planet.state.planet.launchMusic);
}else if(ui.editor.isShown()){
play(Musics.editor);
}else{
play(Musics.menu);
}
}else if(state.rules.editor){
silenced = false;
play(Musics.editor);
}else{
//this just fades out the last track to make way for ingame music
silence();
if(Core.settings.getBool("alwaysmusic")){
if(current == null){
playRandom();
}
}else if(Time.timeSinceMillis(lastPlayed) > 1000 * musicInterval / 60f){
//chance to play it per interval
if(Mathf.chance(musicChance)){
lastPlayed = Time.millis();
playRandom();
}
}
}
updateLoops();
}
protected void updateLoops(){
//clear loops when in menu
if(!state.isGame()){
sounds.clear();
return;
}
float avol = Core.settings.getInt("ambientvol", 100) / 100f;
sounds.each((sound, data) -> {
data.curVolume = Mathf.lerpDelta(data.curVolume, data.volume * avol, 0.11f);
boolean play = data.curVolume > 0.01f;
float pan = Mathf.zero(data.total, 0.0001f) ? 0f : sound.calcPan(data.sum.x / data.total, data.sum.y / data.total);
if(data.soundID <= 0 || !Core.audio.isPlaying(data.soundID)){
if(play){
data.soundID = sound.loop(data.curVolume, 1f, pan);
Core.audio.protect(data.soundID, true);
}
}else{
if(data.curVolume <= 0.001f){
sound.stop();
data.soundID = -1;
return;
}
Core.audio.set(data.soundID, pan, data.curVolume);
}
data.volume = 0f;
data.total = 0f;
data.sum.setZero();
});
}
/** Plays a random track.*/
public void playRandom(){
if(state.boss() != null){
playOnce(bossMusic.random(lastRandomPlayed));
}else if(isDark()){
playOnce(darkMusic.random(lastRandomPlayed));
}else{
playOnce(ambientMusic.random(lastRandomPlayed));
}
}
/** Whether to play dark music.*/
protected boolean isDark(){
if(player.team().data().hasCore() && player.team().data().core().healthf() < 0.85f){
//core damaged -> dark
return true;
}
//it may be dark based on wave
if(Mathf.chance((float)(Math.log10((state.wave - 17f)/19f) + 1) / 4f)){
return true;
}
//dark based on enemies
return Mathf.chance(state.enemies / 70f + 0.1f);
}
/** Plays and fades in a music track. This must be called every frame.
* If something is already playing, fades out that track and fades in this new music.*/
protected void play(@Nullable Music music){
if(!shouldPlay()){
if(current != null){
current.setVolume(0);
}
fade = 0f;
return;
}
//update volume of current track
if(current != null){
current.setVolume(fade * Core.settings.getInt("musicvol") / 100f);
}
//do not update once the track has faded out completely, just stop
if(silenced){
return;
}
if(current == null && music != null){
//begin playing in a new track
current = music;
current.setLooping(true);
current.setVolume(fade = 0f);
current.play();
silenced = false;
}else if(current == music && music != null){
//fade in the playing track
fade = Mathf.clamp(fade + Time.delta /finTime);
}else if(current != null){
//fade out the current track
fade = Mathf.clamp(fade - Time.delta /foutTime);
if(fade <= 0.01f){
//stop current track when it hits 0 volume
current.stop();
current = null;
silenced = true;
if(music != null){
//play newly scheduled track
current = music;
current.setVolume(fade = 0f);
current.setLooping(true);
current.play();
silenced = false;
}
}
}
}
/** Plays a music track once and only once. If something is already playing, does nothing.*/
protected void playOnce(Music music){
if(current != null || music == null || !shouldPlay()) return; //do not interrupt already-playing tracks
//save last random track played to prevent duplicates
lastRandomPlayed = music;
//set fade to 1 and play it, stopping the current when it's done
fade = 1f;
current = music;
current.setVolume(1f);
current.setLooping(false);
current.play();
}
protected boolean shouldPlay(){
return Core.settings.getInt("musicvol") > 0;
}
/** Fades out the current track, unless it has already been silenced. */
protected void silence(){
play(null);
}
protected static class SoundData{
float volume;
float total;
Vec2 sum = new Vec2();
int soundID;
float curVolume;
}
}