Files
adventures-of-kuroneko/lib/impact/sound.js
T
2018-09-30 06:17:31 +02:00

516 lines
11 KiB
JavaScript

ig.module(
'impact.sound'
)
.defines(function(){ "use strict";
ig.SoundManager = ig.Class.extend({
clips: {},
volume: 1,
format: null,
init: function() {
// Quick sanity check if the Browser supports the Audio tag
if( !ig.Sound.enabled || !window.Audio ) {
ig.Sound.enabled = false;
return;
}
// Probe sound formats and determine the file extension to load
var probe = new Audio();
for( var i = 0; i < ig.Sound.use.length; i++ ) {
var format = ig.Sound.use[i];
if( probe.canPlayType(format.mime) ) {
this.format = format;
break;
}
}
// No compatible format found? -> Disable sound
if( !this.format ) {
ig.Sound.enabled = false;
}
// Create WebAudio Context
if( ig.Sound.enabled && ig.Sound.useWebAudio ) {
this.audioContext = new AudioContext();
this.boundWebAudioUnlock = this.unlockWebAudio.bind(this);
document.addEventListener('touchstart', this.boundWebAudioUnlock, false);
}
},
unlockWebAudio: function() {
document.removeEventListener('touchstart', this.boundWebAudioUnlock, false);
// create empty buffer
var buffer = this.audioContext.createBuffer(1, 1, 22050);
var source = this.audioContext.createBufferSource();
source.buffer = buffer;
source.connect(this.audioContext.destination);
source.start(0);
},
load: function( path, multiChannel, loadCallback ) {
if( multiChannel && ig.Sound.useWebAudio ) {
// Requested as Multichannel and we're using WebAudio?
return this.loadWebAudio( path, multiChannel, loadCallback );
}
else {
// Oldschool HTML5 Audio - always used for Music
return this.loadHTML5Audio( path, multiChannel, loadCallback );
}
},
loadWebAudio: function( path, multiChannel, loadCallback ) {
// Path to the soundfile with the right extension (.ogg or .mp3)
var realPath = ig.prefix + path.replace(/[^\.]+$/, this.format.ext) + ig.nocache;
if( this.clips[path] ) {
return this.clips[path];
}
var audioSource = new ig.Sound.WebAudioSource()
this.clips[path] = audioSource;
var request = new XMLHttpRequest();
request.open('GET', realPath, true);
request.responseType = 'arraybuffer';
var that = this;
request.onload = function(ev) {
that.audioContext.decodeAudioData(request.response,
function(buffer) {
audioSource.buffer = buffer;
loadCallback( path, true, ev );
},
function(ev) {
loadCallback( path, false, ev );
}
);
};
request.onerror = function(ev) {
loadCallback( path, false, ev );
};
request.send();
return audioSource;
},
loadHTML5Audio: function( path, multiChannel, loadCallback ) {
// Path to the soundfile with the right extension (.ogg or .mp3)
var realPath = ig.prefix + path.replace(/[^\.]+$/, this.format.ext) + ig.nocache;
// Sound file already loaded?
if( this.clips[path] ) {
// Loaded as WebAudio, but now requested as HTML5 Audio? Probably Music?
if( this.clips[path] instanceof ig.Sound.WebAudioSource ) {
return this.clips[path];
}
// Only loaded as single channel and now requested as multichannel?
if( multiChannel && this.clips[path].length < ig.Sound.channels ) {
for( var i = this.clips[path].length; i < ig.Sound.channels; i++ ) {
var a = new Audio( realPath );
a.load();
this.clips[path].push( a );
}
}
return this.clips[path][0];
}
var clip = new Audio( realPath );
if( loadCallback ) {
// The canplaythrough event is dispatched when the browser determines
// that the sound can be played without interuption, provided the
// download rate doesn't change.
// Mobile browsers stubbornly refuse to preload HTML5, so we simply
// ignore the canplaythrough event and immediately "fake" a successful
// load callback
if( ig.ua.mobile ) {
setTimeout(function(){
loadCallback( path, true, null );
}, 0);
}
else {
clip.addEventListener( 'canplaythrough', function cb(ev){
clip.removeEventListener('canplaythrough', cb, false);
loadCallback( path, true, ev );
}, false );
clip.addEventListener( 'error', function(ev){
loadCallback( path, false, ev );
}, false);
}
}
clip.preload = 'auto';
clip.load();
this.clips[path] = [clip];
if( multiChannel ) {
for( var i = 1; i < ig.Sound.channels; i++ ) {
var a = new Audio(realPath);
a.load();
this.clips[path].push( a );
}
}
return clip;
},
get: function( path ) {
// Find and return a channel that is not currently playing
var channels = this.clips[path];
// Is this a WebAudio source? We only ever have one for each Sound
if( channels && channels instanceof ig.Sound.WebAudioSource ) {
return channels;
}
// Oldschool HTML5 Audio - find a channel that's not currently
// playing or, if all are playing, rewind one
for( var i = 0, clip; clip = channels[i++]; ) {
if( clip.paused || clip.ended ) {
if( clip.ended ) {
clip.currentTime = 0;
}
return clip;
}
}
// Still here? Pause and rewind the first channel
channels[0].pause();
channels[0].currentTime = 0;
return channels[0];
}
});
ig.Music = ig.Class.extend({
tracks: [],
namedTracks: {},
currentTrack: null,
currentIndex: 0,
random: false,
_volume: 1,
_loop: false,
_fadeInterval: 0,
_fadeTimer: null,
_endedCallbackBound: null,
init: function() {
this._endedCallbackBound = this._endedCallback.bind(this);
Object.defineProperty(this,"volume", {
get: this.getVolume.bind(this),
set: this.setVolume.bind(this)
});
Object.defineProperty(this,"loop", {
get: this.getLooping.bind(this),
set: this.setLooping.bind(this)
});
},
add: function( music, name ) {
if( !ig.Sound.enabled ) {
return;
}
var path = music instanceof ig.Sound ? music.path : music;
var track = ig.soundManager.load(path, false);
// Did we get a WebAudio Source? This is suboptimal; Music should be loaded
// as HTML5 Audio so it can be streamed
if( track instanceof ig.Sound.WebAudioSource ) {
// Since this error will likely occour at game start, we stop the game
// to not produce any more errors.
ig.system.stopRunLoop();
throw(
"Sound '"+path+"' loaded as Multichannel but used for Music. " +
"Set the multiChannel param to false when loading, e.g.: new ig.Sound(path, false)"
);
}
track.loop = this._loop;
track.volume = this._volume;
track.addEventListener( 'ended', this._endedCallbackBound, false );
this.tracks.push( track );
if( name ) {
this.namedTracks[name] = track;
}
if( !this.currentTrack ) {
this.currentTrack = track;
}
},
next: function() {
if( !this.tracks.length ) { return; }
this.stop();
this.currentIndex = this.random
? Math.floor(Math.random() * this.tracks.length)
: (this.currentIndex + 1) % this.tracks.length;
this.currentTrack = this.tracks[this.currentIndex];
this.play();
},
pause: function() {
if( !this.currentTrack ) { return; }
this.currentTrack.pause();
},
stop: function() {
if( !this.currentTrack ) { return; }
this.currentTrack.pause();
this.currentTrack.currentTime = 0;
},
play: function( name ) {
// If a name was provided, stop playing the current track (if any)
// and play the named track
if( name && this.namedTracks[name] ) {
var newTrack = this.namedTracks[name];
if( newTrack != this.currentTrack ) {
this.stop();
this.currentTrack = newTrack;
}
}
else if( !this.currentTrack ) {
return;
}
this.currentTrack.play();
},
getLooping: function() {
return this._loop;
},
setLooping: function( l ) {
this._loop = l;
for( var i in this.tracks ) {
this.tracks[i].loop = l;
}
},
getVolume: function() {
return this._volume;
},
setVolume: function( v ) {
this._volume = v.limit(0,1);
for( var i in this.tracks ) {
this.tracks[i].volume = this._volume;
}
},
fadeOut: function( time ) {
if( !this.currentTrack ) { return; }
clearInterval( this._fadeInterval );
this.fadeTimer = new ig.Timer( time );
this._fadeInterval = setInterval( this._fadeStep.bind(this), 50 );
},
_fadeStep: function() {
var v = this.fadeTimer.delta()
.map(-this.fadeTimer.target, 0, 1, 0)
.limit( 0, 1 )
* this._volume;
if( v <= 0.01 ) {
this.stop();
this.currentTrack.volume = this._volume;
clearInterval( this._fadeInterval );
}
else {
this.currentTrack.volume = v;
}
},
_endedCallback: function() {
if( this._loop ) {
this.play();
}
else {
this.next();
}
}
});
ig.Sound = ig.Class.extend({
path: '',
volume: 1,
currentClip: null,
multiChannel: true,
_loop: false,
init: function( path, multiChannel ) {
this.path = path;
this.multiChannel = (multiChannel !== false);
Object.defineProperty(this,"loop", {
get: this.getLooping.bind(this),
set: this.setLooping.bind(this)
});
this.load();
},
getLooping: function() {
return this._loop;
},
setLooping: function( loop ) {
this._loop = loop;
if( this.currentClip ) {
this.currentClip.loop = loop;
}
},
load: function( loadCallback ) {
if( !ig.Sound.enabled ) {
if( loadCallback ) {
loadCallback( this.path, true );
}
return;
}
if( ig.ready ) {
ig.soundManager.load( this.path, this.multiChannel, loadCallback );
}
else {
ig.addResource( this );
}
},
play: function() {
if( !ig.Sound.enabled ) {
return;
}
this.currentClip = ig.soundManager.get( this.path );
this.currentClip.loop = this._loop;
this.currentClip.volume = ig.soundManager.volume * this.volume;
this.currentClip.play();
},
stop: function() {
if( this.currentClip ) {
this.currentClip.pause();
this.currentClip.currentTime = 0;
}
}
});
ig.Sound.WebAudioSource = ig.Class.extend({
sources: [],
gain: null,
buffer: null,
_loop: false,
init: function() {
this.gain = ig.soundManager.audioContext.createGain();
this.gain.connect(ig.soundManager.audioContext.destination);
Object.defineProperty(this,"loop", {
get: this.getLooping.bind(this),
set: this.setLooping.bind(this)
});
Object.defineProperty(this,"volume", {
get: this.getVolume.bind(this),
set: this.setVolume.bind(this)
});
},
play: function() {
if( !this.buffer ) { return; }
var source = ig.soundManager.audioContext.createBufferSource();
source.buffer = this.buffer;
source.connect(this.gain);
source.loop = this._loop;
// Add this new source to our sources array and remove it again
// later when it has finished playing.
var that = this;
this.sources.push(source);
source.onended = function(){ that.sources.erase(source); }
source.start(0);
},
pause: function() {
for( var i = 0; i < this.sources.length; i++ ) {
try{
this.sources[i].stop();
} catch(err){}
}
},
getLooping: function() {
return this._loop;
},
setLooping: function( loop ) {
this._loop = loop;
for( var i = 0; i < this.sources.length; i++ ) {
this.sources[i].loop = loop;
}
},
getVolume: function() {
return this.gain.gain.value;
},
setVolume: function( volume ) {
this.gain.gain.value = volume;
}
});
ig.Sound.FORMAT = {
MP3: {ext: 'mp3', mime: 'audio/mpeg'},
M4A: {ext: 'm4a', mime: 'audio/mp4; codecs=mp4a'},
OGG: {ext: 'ogg', mime: 'audio/ogg; codecs=vorbis'},
WEBM: {ext: 'webm', mime: 'audio/webm; codecs=vorbis'},
CAF: {ext: 'caf', mime: 'audio/x-caf'}
};
ig.Sound.use = [ig.Sound.FORMAT.OGG, ig.Sound.FORMAT.MP3];
ig.Sound.channels = 4;
ig.Sound.enabled = true;
ig.normalizeVendorAttribute(window, 'AudioContext');
ig.Sound.useWebAudio = !!window.AudioContext && !window.nwf;
});