Skip to content

Instantly share code, notes, and snippets.

@Danetag
Last active July 23, 2019 10:09
Show Gist options
  • Save Danetag/2b1e8c8e776dec75c931b77671a8a5e1 to your computer and use it in GitHub Desktop.
Save Danetag/2b1e8c8e776dec75c931b77671a8a5e1 to your computer and use it in GitHub Desktop.
Sound Manager using Web Audio API
export const SOUNDS = [
{
id: ID_OF_THE_SOUND,
url: '/static/sounds/a_sound.mp3',
url_ogg: '/static/sounds/a_sound.ogg',
loop: false
}
]
export const loadSound = (url, context) => {
if (!context || !url) return;
return new Promise((resolve, reject) => {
const request = new XMLHttpRequest();
request.open('GET', url, true);
request.responseType = 'arraybuffer';
// Decode asynchronously
request.onload = () => {
context.decodeAudioData(request.response, buffer => {
resolve(buffer);
}, (err) => {
console.log('error loading sound', url, err);
reject();
});
}
request.send();
});
}
// Constants
import { SOUNDS } from './constants';
// Utils
import { loadSound } from 'utils/load';
import { IS_SAFARI, IS_IE } from 'utils/misc';
class Sound {
constructor(options) {
super(options);
// Custom way of "watching" a volume value change from a redux store
this.watchers = {
'sound.volume': this.onVolumeChanged,
};
const a = document.createElement('audio');
this.OGG = !!(a.canPlayType && a.canPlayType('audio/mpeg;').replace(/no/, ''));
if (IS_SAFARI || IS_IE) this.OGG = false;
// Fix up prefixing
window.AudioContext = window.AudioContext || window.webkitAudioContext;
this.context = new window.AudioContext();
this.unlockAudioContext();
// Set the name of the hidden property and the change event for visibility
this.hidden = null;
let visibilityChange = null;
if (typeof document.hidden !== "undefined") { // Opera 12.10 and Firefox 18 and later support
this.hidden = "hidden";
visibilityChange = "visibilitychange";
} else if (typeof document.msHidden !== "undefined") {
this.hidden = "msHidden";
visibilityChange = "msvisibilitychange";
} else if (typeof document.webkitHidden !== "undefined") {
this.hidden = "webkitHidden";
visibilityChange = "webkitvisibilitychange";
}
// Visibility API to turn the sound off when locking phones
if (typeof document.addEventListener !== "undefined" && this.hidden !== null && visibilityChange !== null) {
document.addEventListener(visibilityChange, this.handleVisibilityChange, false);
}
}
unlockAudioContext() {
if (this.context.state !== 'suspended') return;
const b = document.body;
const unlock = () => { this.context.resume().then(clean);}
const clean = () => { events.forEach(e => b.removeEventListener(e, unlock)); }
const events = ['touchstart','touchend', 'mousedown','keydown'];
events.forEach(e => b.addEventListener(e, unlock, false));
}
handleVisibilityChange = () => {
if (document[this.hidden]) {
this.onWindowBlur();
} else {
this.onWindowFocus();
}
}
onWindowFocus = () => {
if (this.context.state === 'suspended') return;
// fade to current state
const volume = this.getState().get('sound').get('volume');
if (volume) this.onVolumeChanged(volume);
}
onWindowBlur = () => {
if (this.context.state === 'suspended') return;
// fade to 0 anyway
this.onVolumeChanged(0)
}
// Preload all UI sounds so they are already available
loadUISounds() {
const promises = SOUNDS
.filter( sound => !sound.loop)
.map((sound) => loadSound(this.OGG ? sound.url_ogg : sound.url, this.context).then(buffer => this.onSoundLoaded(sound, buffer)));
return Promise.all(promises);
}
onSoundLoaded(sound, buffer) {
sound.buffer = buffer;
}
// Creates source
createBufferSource(sound) {
// destroy current one if one
this.destroyBufferSource(sound);
const source = this.context.createBufferSource();
source.buffer = sound.buffer;
source.loop = sound.loop !== undefined ? sound.loop : false;
// gain node
const gainNode = this.context.createGain();
gainNode.gain.value = 0;
if (sound.loop) gainNode.gain.setValueAtTime(0, this.context.currentTime);
source.connect(gainNode);
gainNode.connect(this.context.destination);
source.onended = () => {
if (!sound.loop) this.destroyBufferSource(sound);
};
sound.gainNode = gainNode;
sound.source = source;
return sound;
}
destroyBufferSource(sound){
if (sound.source) {
sound.gainNode.gain.value = 0;
sound.gainNode.disconnect();
sound.source.disconnect();
sound.source.onended = null;
}
sound.source = null;
sound.gainNode = null;
}
onVolumeChanged = (volume) => {
SOUNDS
.filter(sound => sound.gainNode)
.forEach(sound => {
if (sound.loop){
sound.gainNode.gain.setValueAtTime(volume === 0 ? 1 : 0, this.context.currentTime);
sound.gainNode.gain.linearRampToValueAtTime(volume, this.context.currentTime + 0.5);
} else
sound.gainNode.gain.value = volume;
})
}
getSound(soundID) {
const sounds = SOUNDS.filter(sound => sound.id === soundID);
return sounds.length ? sounds[0] : null;
}
async play(soundID) {
// get sound
const sound = this.getSound(soundID);
if (!sound || sound.source) return;
// not loaded yet
if (!sound.buffer) await loadSound(this.OGG ? sound.url_ogg : sound.url, this.context).then(buffer => this.onSoundLoaded(sound, buffer));
const volume = this.getState().get('sound').get('volume');
// sound now has a source and gain
this.createBufferSource(sound);
// nice transition if ambient sound (loop = false)
if (sound.loop){
sound.gainNode.gain.value = 0;
sound.gainNode.gain.setValueAtTime(0, this.context.currentTime);
sound.gainNode.gain.linearRampToValueAtTime(volume, this.context.currentTime + 1);
}
else
sound.gainNode.gain.value = volume;
let playPosition = 0;
if (sound.randomPlayPosition) {
playPosition = Math.floor(Math.random() * sound.buffer.duration);
}
if (sound.source.start) {
sound.source.start(0, playPosition);
} else if (sound.source.play) {
sound.source.play(0, playPosition);
} else if (sound.source.noteOn) {
sound.source.noteOn(0, playPosition);
}
}
stop(soundID) {
// get sound
const sound = this.getSound(soundID);
if (!sound || !sound.source) return;
if (sound.loop)
sound.gainNode.gain.linearRampToValueAtTime(0, this.context.currentTime + 1);
else
sound.gainNode.gain.value = 0;
setTimeout(() => {
if (!sound.source) return;
sound.source.stop();
this.destroyBufferSource(sound);
}, 1000)
}
}
export default new Sound();
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment