Files
2018-07-17 14:15:37 +02:00

350 lines
7.5 KiB
ActionScript 3

const float fadeInTime = 1000.f;
const float fadeOutTime = 3000.f;
//Delay (seconds) to avoid playing music after the track ends
float silenceTime = 0;
//Calculated volume for the music tracks
double musicVolume = 1.0;
class Layer {
Movement@ movement;
Sound@ track;
float currentVolume = 1;
float volumeGoal = 1;
float fadeTime = 0;
string name;
Layer(Movement@ moveTrack, const string& file, float Volume = 1.f, bool loop = false) {
name = file;
@movement = moveTrack;
//Sounds don't play if the volume is 0
if(soundVolume <= 0)
return;
@track = playTrack(file, loop, true);
currentVolume = Volume;
volumeGoal = Volume;
if(track !is null) {
track.volume = Volume;
}
else {
error("Could not load track " + file);
}
}
void volumeFade(float goal, float seconds) {
volumeGoal = goal;
fadeTime = seconds;
}
void stop() {
if(track !is null)
track.stop();
@track = null;
@movement = null;
}
void play() {
if(track !is null)
track.paused = false;
}
void updateVolume() {
track.volume = currentVolume * musicVolume;
}
bool update(double time) {
if(track is null)
return true;
if(currentVolume != volumeGoal) {
if(time >= fadeTime) {
currentVolume = volumeGoal;
fadeTime = 0;
}
else {
currentVolume += (volumeGoal - currentVolume) * (time/fadeTime);
fadeTime -= time;
}
track.volume = currentVolume * musicVolume;
}
if(track.playing) {
return false;
}
else {
@track = null;
return true;
}
}
};
array<string> tracks;
int lastTrack = -1;
string get_nextTrack() {
int nextIndex;
do {
nextIndex = randomi(0,tracks.length-1);
} while(nextIndex == lastTrack && tracks.length != 1);
lastTrack = nextIndex;
return tracks[nextIndex];
}
Namespace musicVars;
array<Arrangement@> arrangements, queued;
array<Layer@> layers;
StatTracker winTracker(stat::ShipsDestroyed, "ships_destroyed"), lossTracker(stat::ShipsLost, "ships_lost");
class Movement {
string name;
string track;
bool loop = false;
Formula@ volume;
int volVar, timeVar, playingVar;
Movement(const string& Name) {
name = Name;
volVar = musicVars.lookup(name + ".volume", true);
timeVar = musicVars.lookup(name + ".time", true);
playingVar = musicVars.lookup(name + ".playing", true);
}
};
class Arrangement {
array<Movement@> movements;
double playOrder = 0;
string name;
int opCmp(const Arrangement& other) {
if(playOrder < other.playOrder)
return -1;
else if(playOrder == other.playOrder)
return 0;
else
return 1;
}
void prepare() {
//Load up all tracks, then start them when they've all been loaded
for(uint i = 0; i < movements.length; ++i) {
Movement@ movement = movements[i];
layers.insertLast(Layer(movement, movement.track, 0, movement.loop));
musicVars.setConstant(movement.playingVar, 1.0);
}
for(uint i = 0; i < layers.length; ++i)
layers[i].play();
}
};
void init() {
if(!soundEnabled)
return;
//Load arrangements
ReadFile file(resolve("data/music/orchestra.txt"));
Arrangement@ arrange;
Movement@ movement;
string key, value;
while(file++) {
key = file.key;
value = file.value;
if(key == "Arrangement") {
@arrange = Arrangement();
arrange.name = value;
arrangements.insertLast(arrange);
}
else if(arrange !is null) {
if(key == "Era") {
if(value == "Industrial")
arrange.playOrder = randomd(0.75, 1.25);
else if(value == "Imperial")
arrange.playOrder = randomd(1.2, 1.5);
else
arrange.playOrder = randomd(0.0, 1.0);
}
else if(key == "Movement") {
@movement = Movement(value);
arrange.movements.insertLast(movement);
}
else if(movement !is null) {
if(key == "Track") {
movement.track = value;
}
else if(key == "Volume") {
@movement.volume = Formula(value);
}
else if(key == "Loop") {
movement.loop = toBool(value);
}
}
}
}
//If we're starting a new-ish game, play the music in a semi-ordered way
if(gameTime < 30.0 * 60.0) {
queued = arrangements;
queued.sortAsc();
uint remove = min(uint(max(double(queued.length) * gameTime / (30.0 / 60.0), 0.0)), queued.length);
for(uint i = 0; i < remove; ++i)
queued.removeAt(0);
}
if(soundVolume > 0)
playNewArrangement();
}
Arrangement@ prevArrangement;
void randomize() {
queued = arrangements;
for(uint i = 0, cnt = queued.length; i < cnt; ++i) {
uint o = randomi(i, cnt-1);
if(o != i) {
auto@ a = queued[i];
@queued[i] = queued[o];
@queued[o] = a;
}
}
if(prevArrangement !is null && prevArrangement is queued[0] && queued.length > 1) {
uint i = randomi(1, queued.length-1);
auto@ a = queued[0];
@queued[0] = queued[i];
@queued[i] = a;
}
}
void playNewArrangement() {
if(arrangements.length == 0)
return;
else if(arrangements.length == 1) {
arrangements[0].prepare();
return;
}
else if(queued.length == 0) {
randomize();
}
Arrangement@ nextArrangement = queued[0];
queued.removeAt(0);
nextArrangement.prepare();
@prevArrangement = nextArrangement;
}
void deinit() {
for(uint i = 0; i < layers.length; ++i)
layers[i].stop();
}
double prevMusicVol = 1.0, prevMasterVol = 1.0, prevSFXVol = 1.0;
void tick(double time) {
if(!soundEnabled)
return;
//Update volume settings
if(prevMusicVol != settings::dMusicVolume || prevSFXVol != settings::dSFXVolume || prevMasterVol != settings::dMasterVolume) {
if(settings::dSFXVolume < 0.01)
musicVolume = settings::dMusicVolume;
else
musicVolume = settings::dMusicVolume / settings::dSFXVolume;
for(int i = layers.length - 1; i >= 0; --i)
layers[i].updateVolume();
prevMusicVol = settings::dMusicVolume;
prevMasterVol = settings::dMasterVolume;
prevSFXVol = settings::dSFXVolume;
}
if(layers.length == 0) {
silenceTime -= time;
if(soundVolume > 0 && silenceTime <= 0.0) {
playNewArrangement();
//Silence music for 3 seconds after we attempt to resume the tracks
// Mostly, this avoids spamming errors when failing to play trakcs for some reason
silenceTime = 3.0;
}
else {
return;
}
}
winTracker.update();
lossTracker.update();
bool activeTrack = false;
for(int i = layers.length - 1; i >= 0; --i) {
Layer@ layer = layers[i];
if(layer.update(time)) {
musicVars.setConstant(layer.movement.playingVar, 0.0);
layers.removeAt(i);
}
else {
if(!layer.movement.loop || layer.currentVolume > 0.01)
activeTrack = true;
musicVars.setConstant(layer.movement.playingVar, 1.0);
musicVars.setConstant(layer.movement.timeVar, double(layer.track.playPosition) / 1000.0);
musicVars.setConstant(layer.movement.volVar, layer.currentVolume);
layer.volumeFade(layer.movement.volume.evaluate(musicVars), 2);
}
}
if(!activeTrack) {
for(int i = layers.length - 1; i >= 0; --i)
layers[i].stop();
layers.length = 0;
}
}
class StatTracker {
stat::EmpireStat stat;
string name;
int accVar;
uint lastTime = 0;
int lastVal = 0;
int accumulation = 0;
float decay = 0.7;
StatTracker(stat::EmpireStat Stat, string Name) {
stat = Stat;
name = Name;
accVar = musicVars.lookup(name + ".acc", true);
}
void update() {
if(lastTime + 1 >= (systemTime/1000))
return;
lastTime = systemTime/1000;
if(accumulation > 0)
accumulation = int(ceil(float(accumulation) * decay)) - 1;
int value = 0;
{
StatHistory history(playerEmpire, stat);
if(history.advance(-1))
value = history.intVal;
}
//Accumulate events
accumulation = max(accumulation + (value - lastVal),0);
lastVal = value;
musicVars.setConstant(accVar, double(accumulation));
}
};