waves/public/assets/g/adarkroom/script/audio.js
2025-04-09 17:11:14 -05:00

283 lines
12 KiB
JavaScript

/**
* Module that takes care of audio playback
*/
var AudioEngine = {
FADE_TIME: 1,
AUDIO_BUFFER_CACHE: {},
_audioContext: null,
_master: null,
_currentBackgroundMusic: null,
_currentEventAudio: null,
_currentSoundEffectAudio: null,
_initialized: false,
init: function () {
AudioEngine._initAudioContext();
// AudioEngine._preloadAudio(); // removed to save bandwidth
AudioEngine._initialized = true;
},
_preloadAudio: function () {
// start loading music and events early
// ** could be used later if we specify a better set of
// audio files to preload -- i.e. we probably don't need to load
// the later villages or events audio, and esp. not the ending
for (var key in AudioLibrary) {
if (
key.toString().indexOf('MUSIC_') > -1 ||
key.toString().indexOf('EVENT_') > -1) {
AudioEngine.loadAudioFile(AudioLibrary[key]);
}
}
},
_initAudioContext: function () {
AudioEngine._audioContext = new (window.AudioContext || window.webkitAudioContext);
AudioEngine._createMasterChannel();
},
_createMasterChannel: function () {
// create master
AudioEngine._master = AudioEngine._audioContext.createGain();
AudioEngine._master.gain.setValueAtTime(1.0, AudioEngine._audioContext.currentTime);
AudioEngine._master.connect(AudioEngine._audioContext.destination);
},
_getMissingAudioBuffer: function () {
// plays beeping sound to indicate missing audio
var buffer = AudioEngine._audioContext.createBuffer(
1,
AudioEngine._audioContext.sampleRate,
AudioEngine._audioContext.sampleRate
);
// Fill the buffer
var bufferData = buffer.getChannelData(0);
for (var i = 0; i < buffer.length / 2; i++) {
bufferData[i] = Math.sin(i * 0.05) / 4; // max .25 gain value
}
return buffer;
},
_playSound: function (buffer) {
if (AudioEngine._currentSoundEffectAudio &&
AudioEngine._currentSoundEffectAudio.source.buffer == buffer) {
return;
}
var source = AudioEngine._audioContext.createBufferSource();
source.buffer = buffer;
source.onended = function(event) {
// dereference current sound effect when finished
if (AudioEngine._currentSoundEffectAudio &&
AudioEngine._currentSoundEffectAudio.source.buffer == buffer) {
AudioEngine._currentSoundEffectAudio = null;
}
};
source.connect(AudioEngine._master);
source.start();
AudioEngine._currentSoundEffectAudio = {
source: source
};
},
_playBackgroundMusic: function (buffer) {
var source = AudioEngine._audioContext.createBufferSource();
source.buffer = buffer;
source.loop = true;
var envelope = AudioEngine._audioContext.createGain();
envelope.gain.setValueAtTime(0.0, AudioEngine._audioContext.currentTime);
var fadeTime = AudioEngine._audioContext.currentTime + AudioEngine.FADE_TIME;
// fade out current background music
if (AudioEngine._currentBackgroundMusic &&
AudioEngine._currentBackgroundMusic.source &&
AudioEngine._currentBackgroundMusic.source.playbackState !== 0) {
var currentBackgroundGainValue = AudioEngine._currentBackgroundMusic.envelope.gain.value;
AudioEngine._currentBackgroundMusic.envelope.gain.cancelScheduledValues(AudioEngine._audioContext.currentTime);
AudioEngine._currentBackgroundMusic.envelope.gain.setValueAtTime(currentBackgroundGainValue, AudioEngine._audioContext.currentTime);
AudioEngine._currentBackgroundMusic.envelope.gain.linearRampToValueAtTime(0.0, fadeTime);
AudioEngine._currentBackgroundMusic.source.stop(fadeTime + 0.3); // make sure fade has completed
}
// fade in new backgorund music
source.connect(envelope);
envelope.connect(AudioEngine._master);
source.start();
envelope.gain.linearRampToValueAtTime(1.0, fadeTime);
// update current background music
AudioEngine._currentBackgroundMusic = {
source: source,
envelope: envelope
};
},
_playEventMusic: function (buffer) {
var source = AudioEngine._audioContext.createBufferSource();
source.buffer = buffer;
source.loop = true;
var envelope = AudioEngine._audioContext.createGain();
envelope.gain.setValueAtTime(0.0, AudioEngine._audioContext.currentTime);
var fadeTime = AudioEngine._audioContext.currentTime + AudioEngine.FADE_TIME * 2;
// turn down current background music
if (AudioEngine._currentBackgroundMusic != null) {
var currentBackgroundGainValue = AudioEngine._currentBackgroundMusic.envelope.gain.value;
AudioEngine._currentBackgroundMusic.envelope.gain.cancelScheduledValues(AudioEngine._audioContext.currentTime);
AudioEngine._currentBackgroundMusic.envelope.gain.setValueAtTime(currentBackgroundGainValue, AudioEngine._audioContext.currentTime);
AudioEngine._currentBackgroundMusic.envelope.gain.linearRampToValueAtTime(0.2, fadeTime);
}
// fade in event music
source.connect(envelope);
envelope.connect(AudioEngine._master);
source.start();
envelope.gain.linearRampToValueAtTime(1.0, fadeTime);
// update reference
AudioEngine._currentEventAudio = {
source: source,
envelope: envelope
};
},
_stopEventMusic: function () {
var fadeTime = AudioEngine._audioContext.currentTime + AudioEngine.FADE_TIME * 2;
// fade out event music and stop
if (AudioEngine._currentEventAudio &&
AudioEngine._currentEventAudio.source &&
AudioEngine._currentEventAudio.source.buffer) {
var currentEventGainValue = AudioEngine._currentEventAudio.envelope.gain.value;
AudioEngine._currentEventAudio.envelope.gain.cancelScheduledValues(AudioEngine._audioContext.currentTime);
AudioEngine._currentEventAudio.envelope.gain.setValueAtTime(currentEventGainValue, AudioEngine._audioContext.currentTime);
AudioEngine._currentEventAudio.envelope.gain.linearRampToValueAtTime(0.0, fadeTime);
AudioEngine._currentEventAudio.source.stop(fadeTime + 1); // make sure fade has completed
AudioEngine._currentEventAudio = null;
}
// turn up background music
if (AudioEngine._currentBackgroundMusic) {
var currentBackgroundGainValue = AudioEngine._currentBackgroundMusic.envelope.gain.value;
AudioEngine._currentBackgroundMusic.envelope.gain.cancelScheduledValues(AudioEngine._audioContext.currentTime);
AudioEngine._currentBackgroundMusic.envelope.gain.setValueAtTime(currentBackgroundGainValue, AudioEngine._audioContext.currentTime);
AudioEngine._currentBackgroundMusic.envelope.gain.linearRampToValueAtTime(1.0, fadeTime);
}
},
isAudioContextRunning: function () {
return AudioEngine._audioContext.state !== 'suspended';
},
tryResumingAudioContext: function() {
if (AudioEngine._audioContext.state === 'suspended') {
AudioEngine._audioContext.resume();
}
},
playBackgroundMusic: function (src) {
if (!AudioEngine._initialized) {
return;
}
AudioEngine.loadAudioFile(src)
.then(function (buffer) {
AudioEngine._playBackgroundMusic(buffer);
});
},
playEventMusic: function (src) {
if (!AudioEngine._initialized) {
return;
}
AudioEngine.loadAudioFile(src)
.then(function (buffer) {
AudioEngine._playEventMusic(buffer);
});
},
stopEventMusic: function () {
if (!AudioEngine._initialized) {
return;
}
AudioEngine._stopEventMusic();
},
playSound: function (src) {
if (!AudioEngine._initialized) {
return;
}
AudioEngine.loadAudioFile(src)
.then(function (buffer) {
AudioEngine._playSound(buffer);
});
},
loadAudioFile: function (src) {
if (src.indexOf('http') === -1) {
src = window.location + src;
}
if (AudioEngine.AUDIO_BUFFER_CACHE[src]) {
return new Promise(function (resolve, reject) {
resolve(AudioEngine.AUDIO_BUFFER_CACHE[src]);
});
} else {
var request = new Request(src);
return fetch(request).then(function (response) {
return response.arrayBuffer();
}).then(function (buffer) {
if (buffer.byteLength === 0) {
console.error('cannot load audio from ' + src);
return AudioEngine._getMissingAudioBuffer();
}
var decodeAudioDataPromise = AudioEngine._audioContext.decodeAudioData(buffer, function (decodedData) {
AudioEngine.AUDIO_BUFFER_CACHE[src] = decodedData;
return AudioEngine.AUDIO_BUFFER_CACHE[src];
});
// Safari WebAudio does not return a promise based API for
// decodeAudioData, so we need to fake it if we want to play
// audio immediately on first fetch
if (decodeAudioDataPromise) {
return decodeAudioDataPromise;
} else {
return new Promise(function (resolve, reject) {
var fakePromiseId = setInterval(function() {
if (AudioEngine.AUDIO_BUFFER_CACHE[src]) {
resolve(AudioEngine.AUDIO_BUFFER_CACHE[src]);
clearInterval(fakePromiseId);
}
}, 20);
});
}
});
}
},
setBackgroundMusicVolume: function (volume, s) {
if (AudioEngine._master == null) return; // master may not be ready yet
if (volume === undefined) {
volume = 1.0;
}
if (s === undefined) {
s = 1.0;
}
// cancel any current schedules and then ramp
var currentBackgroundGainValue = AudioEngine._currentBackgroundMusic.envelope.gain.value;
AudioEngine._currentBackgroundMusic.envelope.gain.cancelScheduledValues(AudioEngine._audioContext.currentTime);
AudioEngine._currentBackgroundMusic.envelope.gain.setValueAtTime(currentBackgroundGainValue, AudioEngine._audioContext.currentTime);
AudioEngine._currentBackgroundMusic.envelope.gain.linearRampToValueAtTime(
volume,
AudioEngine._audioContext.currentTime + s
);
},
setMasterVolume: function (volume, s) {
if (AudioEngine._master == null) return; // master may not be ready yet
if (volume === undefined) {
volume = 1.0;
}
if (s === undefined) {
s = 1.0;
}
// cancel any current schedules and then ramp
var currentGainValue = AudioEngine._master.gain.value;
AudioEngine._master.gain.cancelScheduledValues(AudioEngine._audioContext.currentTime);
AudioEngine._master.gain.setValueAtTime(currentGainValue, AudioEngine._audioContext.currentTime);
AudioEngine._master.gain.linearRampToValueAtTime(
volume,
AudioEngine._audioContext.currentTime + s
);
}
};