orion-engine
Version:
A simple and lightweight web based game development library
292 lines (291 loc) • 11.2 kB
JavaScript
"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
exports.AudioEngine = void 0;
class AudioEngine {
_context;
_sources;
_cached_buffers;
/**
* Creates a new AudioEngine instance.
* @param to_cache URLs of the sounds to cache if any.
*/
constructor(to_cache = []) {
this._context = new AudioContext();
this._sources = [];
this._cached_buffers = new Map();
if (to_cache.length > 0) {
this.cache_audio(to_cache);
}
}
/**
* Caches the sounds with the given URLs.
* @param urls URLs of the sounds to cache.
*/
async cache_audio(urls) {
const promises = urls.map((url) => {
return new Promise(async (resolve) => {
const response = await fetch(url);
const arrayBuffer = await response.arrayBuffer();
const audioBuffer = await this.context.decodeAudioData(arrayBuffer);
this._cached_buffers.set(url, audioBuffer);
resolve();
});
});
await Promise.all(promises);
}
get context() {
return this._context;
}
get sources() {
return this._sources;
}
get cached_buffers() {
return this._cached_buffers;
}
/**
* Plays the sound with the given URL.
* @param url URL of the sound to play.
* @param use_fade Whether to fade the sound out after it has finished playing.
* @returns
*/
async play_sound(url, use_fade) {
let audioBuffer;
// Check if audio buffer is cached
if (this._cached_buffers.has(url)) {
audioBuffer = this._cached_buffers.get(url);
}
else {
// Fetch and decode audio data
const response = await fetch(url);
const arrayBuffer = await response.arrayBuffer();
audioBuffer = await this.context.decodeAudioData(arrayBuffer);
// Cache the audio buffer
this._cached_buffers.set(url, audioBuffer);
}
// Create new AudioBufferSourceNode and connect it to the audio context
const source = this.context.createBufferSource();
source.buffer = audioBuffer;
source.connect(this.context.destination);
source.start();
this.sources.push(source);
if (use_fade) {
this.fade_sound_after_delay(this.sources.indexOf(source), source.buffer.duration);
}
return this.sources.indexOf(source);
}
/**
* Fade the sound with the given source ID.
* @param source_id Source ID of the sound to fade.
* @param duration Duration of the fade in seconds.
* @returns
*/
fade_sound(source_id, duration) {
if (this.sources[source_id] === undefined) {
return;
}
const source = this.sources[source_id];
const currentTime = this.context.currentTime;
const gainNode = this.context.createGain();
source.connect(gainNode);
gainNode.connect(this.context.destination);
gainNode.gain.setValueAtTime(1, currentTime);
gainNode.gain.linearRampToValueAtTime(0, currentTime + duration);
setTimeout(() => {
source.stop();
}, (currentTime + duration - this.context.currentTime) * 1000);
}
/**
* Fade the sound with the given source ID after the duration of the sound.
* @param source_id Source ID of the sound to fade.
* @param duration Duration of the fade in seconds.
* @returns
*/
fade_sound_after_delay(source_id, duration) {
if (this.sources[source_id] === undefined) {
return;
}
const source = this.sources[source_id];
const currentTime = this.context.currentTime;
const gainNode = this.context.createGain();
source.connect(gainNode);
gainNode.connect(this.context.destination);
gainNode.gain.setValueAtTime(1, currentTime);
gainNode.gain.linearRampToValueAtTime(0, currentTime + duration);
setTimeout(() => {
source?.stop();
}, (currentTime + duration - this.context.currentTime) * 1000);
}
/**
* Stops the sound with the given source ID.
* @param source_id Source ID of the sound to stop.
* @returns
*/
stop_sound(source_id) {
if (this.sources[source_id] === undefined) {
return;
}
const source = this.sources[source_id];
source.stop();
}
/**
* Stops all sounds that are currently playing.
*/
stop_all_sounds() {
this._sources.forEach((source) => {
source.stop();
});
this._sources = [];
}
/**
* Set the pitch of the sound with the given source ID.
* @param source_id Source ID of the sound to modify the pitch.
* @param pitch Pitch of the sound. Should be a positive number.
*/
set_pitch(source_id, pitch) {
if (this.sources[source_id] === undefined) {
return;
}
const source = this.sources[source_id];
source.playbackRate.value = pitch;
}
/**
* Enable or disable looping for the sound with the given source ID.
* @param source_id Source ID of the sound to enable or disable looping.
* @param loop Whether to enable or disable looping for the sound.
*/
set_loop(source_id, loop) {
if (this.sources[source_id] === undefined) {
return;
}
const source = this.sources[source_id];
source.loop = loop;
}
/**
* Apply distortion effect to the sound with the given source ID.
* @param source_id Source ID of the sound to apply the distortion effect.
* @param amount Amount of distortion to apply to the sound.
*/
apply_distortion(source_id, amount) {
if (this.sources[source_id] === undefined) {
return;
}
const source = this.sources[source_id];
const distortion = this.context.createWaveShaper();
distortion.curve = this.make_distortion_curve(amount);
source.connect(distortion);
distortion.connect(this.context.destination);
}
/**
* Helper function to create a distortion curve
* @param amount
* @returns
*/
make_distortion_curve(amount) {
const k = typeof amount === 'number' ? amount : 50;
const n_samples = 44100;
const curve = new Float32Array(n_samples);
const deg = Math.PI / 180;
let x;
for (let i = 0; i < n_samples; ++i) {
x = (i * 2) / n_samples - 1;
curve[i] = ((3 + k) * x * 20 * deg) / (Math.PI + k * Math.abs(x));
}
return curve;
}
/**
* Apply reverb effect to the sound with the given source ID.
* @param source_id Source ID of the sound to apply the reverb effect.
* @param duration Duration of the reverb effect in seconds.
* @param decay Rate at which the reverb trails off after the sound stops.
*/
apply_reverb(source_id, duration, decay) {
if (this.sources[source_id] === undefined) {
return;
}
const source = this.sources[source_id];
const convolver = this.context.createConvolver();
const rate = this.context.sampleRate;
const length = rate * duration;
const impulse = this.context.createBuffer(2, length, rate);
const impulseL = impulse.getChannelData(0);
const impulseR = impulse.getChannelData(1);
for (let i = 0; i < length; i++) {
const t = i / rate;
impulseL[i] = (Math.random() * 2 - 1) * Math.pow(1 - t / duration, decay);
impulseR[i] = (Math.random() * 2 - 1) * Math.pow(1 - t / duration, decay);
}
convolver.buffer = impulse;
source.connect(convolver);
convolver.connect(this.context.destination);
}
/**
* Apply chorus effect to the sound with the given source ID.
* @param source_id Source ID of the sound to apply the chorus effect.
* @param delay Time delay between the original sound and the chorus sound in seconds.
* @param depth Depth of the chorus effect. Should be between 0 and 1.
* @param rate Frequency of the LFO that modulates the delay time in Hz.
*/
apply_chorus(source_id, delay, depth, rate) {
if (this.sources[source_id] === undefined) {
return;
}
const source = this.sources[source_id];
if (!source.buffer) {
return;
}
const dryGain = this.context.createGain();
const wetGain = this.context.createGain();
const lfo = this.context.createOscillator();
const delayNode = this.context.createDelay();
// Connect the source to the dry and wet gains
source.connect(dryGain);
source.connect(wetGain);
// Set up the LFO
lfo.type = 'sine';
lfo.frequency.value = rate;
lfo.start();
// Set up the delay node
delayNode.delayTime.value = delay;
// Connect the LFO to the delay time and connect the delay node to the wet gain
lfo.connect(delayNode.delayTime);
delayNode.connect(wetGain);
// Set up the dry and wet gains and connect them to the destination
dryGain.connect(this.context.destination);
wetGain.connect(this.context.destination);
dryGain.gain.value = 1 - depth;
wetGain.gain.value = depth;
// Connect the wet gain to the delay node
wetGain.connect(delayNode);
// Start the source and the LFO
source.start();
// Stop the source and the LFO after the sound has finished playing
setTimeout(() => {
source.stop();
lfo.stop();
}, source.buffer.duration * 1000);
}
/**
* Apply an equalizer effect to the sound with the given source ID.
* @param source_id Source ID of the sound to apply the equalizer effect.
* @param bands An array of gain values for each frequency band.
*/
apply_equalizer(source_id, bands) {
if (this.sources[source_id] === undefined) {
return;
}
const source = this.sources[source_id];
const eq = this.context.createBiquadFilter();
eq.type = 'peaking';
const frequencies = [60, 170, 350, 1000, 3500, 10000];
const q = 2.5;
// Set up the frequency and Q values for each band
for (let i = 0; i < bands.length; i++) {
eq.frequency.setValueAtTime(frequencies[i], this.context.currentTime);
eq.gain.setValueAtTime(bands[i], this.context.currentTime);
eq.Q.setValueAtTime(q, this.context.currentTime);
source.connect(eq);
}
eq.connect(this.context.destination);
}
}
exports.AudioEngine = AudioEngine;