326 lines
8.7 KiB
JavaScript
326 lines
8.7 KiB
JavaScript
|
/*
|
||
|
----------------------------------------------------------
|
||
|
Web Audio API - OGG or MPEG Soundbank
|
||
|
----------------------------------------------------------
|
||
|
http://webaudio.github.io/web-audio-api/
|
||
|
----------------------------------------------------------
|
||
|
*/
|
||
|
|
||
|
(function(root) { 'use strict';
|
||
|
|
||
|
window.AudioContext && (function() {
|
||
|
var audioContext = null; // new AudioContext();
|
||
|
var useStreamingBuffer = false; // !!audioContext.createMediaElementSource;
|
||
|
var midi = root.WebAudio = {api: 'webaudio'};
|
||
|
var ctx; // audio context
|
||
|
var sources = {};
|
||
|
var effects = {};
|
||
|
var masterVolume = 127;
|
||
|
var audioBuffers = {};
|
||
|
///
|
||
|
midi.audioBuffers = audioBuffers;
|
||
|
midi.send = function(data, delay) { };
|
||
|
midi.setController = function(channelId, type, value, delay) { };
|
||
|
|
||
|
midi.setVolume = function(channelId, volume, delay) {
|
||
|
if (delay) {
|
||
|
setTimeout(function() {
|
||
|
masterVolume = volume;
|
||
|
}, delay * 1000);
|
||
|
} else {
|
||
|
masterVolume = volume;
|
||
|
}
|
||
|
};
|
||
|
|
||
|
midi.programChange = function(channelId, program, delay) {
|
||
|
// if (delay) {
|
||
|
// return setTimeout(function() {
|
||
|
// var channel = root.channels[channelId];
|
||
|
// channel.instrument = program;
|
||
|
// }, delay);
|
||
|
// } else {
|
||
|
var channel = root.channels[channelId];
|
||
|
channel.instrument = program;
|
||
|
// }
|
||
|
};
|
||
|
|
||
|
midi.pitchBend = function(channelId, program, delay) {
|
||
|
// if (delay) {
|
||
|
// setTimeout(function() {
|
||
|
// var channel = root.channels[channelId];
|
||
|
// channel.pitchBend = program;
|
||
|
// }, delay);
|
||
|
// } else {
|
||
|
var channel = root.channels[channelId];
|
||
|
channel.pitchBend = program;
|
||
|
// }
|
||
|
};
|
||
|
|
||
|
midi.noteOn = function(channelId, noteId, velocity, delay) {
|
||
|
delay = delay || 0;
|
||
|
|
||
|
/// check whether the note exists
|
||
|
var channel = root.channels[channelId];
|
||
|
var instrument = channel.instrument;
|
||
|
var bufferId = instrument + '' + noteId;
|
||
|
var buffer = audioBuffers[bufferId];
|
||
|
if (!buffer) {
|
||
|
// console.log(MIDI.GM.byId[instrument].id, instrument, channelId);
|
||
|
return;
|
||
|
}
|
||
|
|
||
|
/// convert relative delay to absolute delay
|
||
|
if (delay < ctx.currentTime) {
|
||
|
delay += ctx.currentTime;
|
||
|
}
|
||
|
|
||
|
/// create audio buffer
|
||
|
if (useStreamingBuffer) {
|
||
|
var source = ctx.createMediaElementSource(buffer);
|
||
|
} else { // XMLHTTP buffer
|
||
|
var source = ctx.createBufferSource();
|
||
|
source.buffer = buffer;
|
||
|
}
|
||
|
|
||
|
/// add effects to buffer
|
||
|
if (effects) {
|
||
|
var chain = source;
|
||
|
for (var key in effects) {
|
||
|
chain.connect(effects[key].input);
|
||
|
chain = effects[key];
|
||
|
}
|
||
|
}
|
||
|
|
||
|
/// add gain + pitchShift
|
||
|
var gain = (velocity / 127) * (masterVolume / 127) * 2 - 1;
|
||
|
source.connect(ctx.destination);
|
||
|
source.playbackRate.value = 1; // pitch shift
|
||
|
source.gainNode = ctx.createGain(); // gain
|
||
|
source.gainNode.connect(ctx.destination);
|
||
|
source.gainNode.gain.value = Math.min(1.0, Math.max(-1.0, gain));
|
||
|
source.connect(source.gainNode);
|
||
|
///
|
||
|
if (useStreamingBuffer) {
|
||
|
if (delay) {
|
||
|
return setTimeout(function() {
|
||
|
buffer.currentTime = 0;
|
||
|
buffer.play()
|
||
|
}, delay * 1000);
|
||
|
} else {
|
||
|
buffer.currentTime = 0;
|
||
|
buffer.play()
|
||
|
}
|
||
|
} else {
|
||
|
source.start(delay || 0);
|
||
|
}
|
||
|
///
|
||
|
sources[channelId + '' + noteId] = source;
|
||
|
///
|
||
|
return source;
|
||
|
};
|
||
|
|
||
|
midi.noteOff = function(channelId, noteId, delay) {
|
||
|
delay = delay || 0;
|
||
|
|
||
|
/// check whether the note exists
|
||
|
var channel = root.channels[channelId];
|
||
|
var instrument = channel.instrument;
|
||
|
var bufferId = instrument + '' + noteId;
|
||
|
var buffer = audioBuffers[bufferId];
|
||
|
if (buffer) {
|
||
|
if (delay < ctx.currentTime) {
|
||
|
delay += ctx.currentTime;
|
||
|
}
|
||
|
///
|
||
|
var source = sources[channelId + '' + noteId];
|
||
|
if (source) {
|
||
|
if (source.gainNode) {
|
||
|
// @Miranet: 'the values of 0.2 and 0.3 could of course be used as
|
||
|
// a 'release' parameter for ADSR like time settings.'
|
||
|
// add { 'metadata': { release: 0.3 } } to soundfont files
|
||
|
var gain = source.gainNode.gain;
|
||
|
gain.linearRampToValueAtTime(gain.value, delay);
|
||
|
gain.linearRampToValueAtTime(-1.0, delay + 0.3);
|
||
|
}
|
||
|
///
|
||
|
if (useStreamingBuffer) {
|
||
|
if (delay) {
|
||
|
setTimeout(function() {
|
||
|
buffer.pause();
|
||
|
}, delay * 1000);
|
||
|
} else {
|
||
|
buffer.pause();
|
||
|
}
|
||
|
} else {
|
||
|
if (source.noteOff) {
|
||
|
source.noteOff(delay + 0.5);
|
||
|
} else {
|
||
|
source.stop(delay + 0.5);
|
||
|
}
|
||
|
}
|
||
|
///
|
||
|
delete sources[channelId + '' + noteId];
|
||
|
///
|
||
|
return source;
|
||
|
}
|
||
|
}
|
||
|
};
|
||
|
|
||
|
midi.chordOn = function(channel, chord, velocity, delay) {
|
||
|
var res = {};
|
||
|
for (var n = 0, note, len = chord.length; n < len; n++) {
|
||
|
res[note = chord[n]] = midi.noteOn(channel, note, velocity, delay);
|
||
|
}
|
||
|
return res;
|
||
|
};
|
||
|
|
||
|
midi.chordOff = function(channel, chord, delay) {
|
||
|
var res = {};
|
||
|
for (var n = 0, note, len = chord.length; n < len; n++) {
|
||
|
res[note = chord[n]] = midi.noteOff(channel, note, delay);
|
||
|
}
|
||
|
return res;
|
||
|
};
|
||
|
|
||
|
midi.stopAllNotes = function() {
|
||
|
for (var sid in sources) {
|
||
|
var delay = 0;
|
||
|
if (delay < ctx.currentTime) {
|
||
|
delay += ctx.currentTime;
|
||
|
}
|
||
|
var source = sources[sid];
|
||
|
source.gain.linearRampToValueAtTime(1, delay);
|
||
|
source.gain.linearRampToValueAtTime(0, delay + 0.3);
|
||
|
if (source.noteOff) { // old api
|
||
|
source.noteOff(delay + 0.3);
|
||
|
} else { // new api
|
||
|
source.stop(delay + 0.3);
|
||
|
}
|
||
|
delete sources[sid];
|
||
|
}
|
||
|
};
|
||
|
|
||
|
midi.setEffects = function(list) {
|
||
|
if (ctx.tunajs) {
|
||
|
for (var n = 0; n < list.length; n ++) {
|
||
|
var data = list[n];
|
||
|
var effect = new ctx.tunajs[data.type](data);
|
||
|
effect.connect(ctx.destination);
|
||
|
effects[data.type] = effect;
|
||
|
}
|
||
|
} else {
|
||
|
return console.log('Effects module not installed.');
|
||
|
}
|
||
|
};
|
||
|
|
||
|
midi.connect = function(opts) {
|
||
|
root.setDefaultPlugin(midi);
|
||
|
midi.setContext(ctx || createAudioContext(), opts.onsuccess);
|
||
|
};
|
||
|
|
||
|
midi.getContext = function() {
|
||
|
return ctx;
|
||
|
};
|
||
|
|
||
|
midi.setContext = function(newCtx, onload, onprogress, onerror) {
|
||
|
ctx = newCtx;
|
||
|
|
||
|
/// tuna.js effects module - https://github.com/Dinahmoe/tuna
|
||
|
if (typeof Tuna !== 'undefined' && !ctx.tunajs) {
|
||
|
ctx.tunajs = new Tuna(ctx);
|
||
|
}
|
||
|
|
||
|
/// loading audio files
|
||
|
var urls = [];
|
||
|
var notes = root.keyToNote;
|
||
|
for (var key in notes) urls.push(key);
|
||
|
///
|
||
|
var waitForEnd = function(instrument) {
|
||
|
for (var key in bufferPending) { // has pending items
|
||
|
if (bufferPending[key]) return;
|
||
|
}
|
||
|
///
|
||
|
if (onload) { // run onload once
|
||
|
onload();
|
||
|
onload = null;
|
||
|
}
|
||
|
};
|
||
|
///
|
||
|
var requestAudio = function(soundfont, instrumentId, index, key) {
|
||
|
var url = soundfont[key];
|
||
|
if (url) {
|
||
|
bufferPending[instrumentId] ++;
|
||
|
loadAudio(url, function(buffer) {
|
||
|
buffer.id = key;
|
||
|
var noteId = root.keyToNote[key];
|
||
|
audioBuffers[instrumentId + '' + noteId] = buffer;
|
||
|
///
|
||
|
if (-- bufferPending[instrumentId] === 0) {
|
||
|
var percent = index / 87;
|
||
|
// console.log(MIDI.GM.byId[instrumentId], 'processing: ', percent);
|
||
|
soundfont.isLoaded = true;
|
||
|
waitForEnd(instrument);
|
||
|
}
|
||
|
}, function(err) {
|
||
|
// console.log(err);
|
||
|
});
|
||
|
}
|
||
|
};
|
||
|
///
|
||
|
var bufferPending = {};
|
||
|
for (var instrument in root.Soundfont) {
|
||
|
var soundfont = root.Soundfont[instrument];
|
||
|
if (soundfont.isLoaded) {
|
||
|
continue;
|
||
|
}
|
||
|
///
|
||
|
var synth = root.GM.byName[instrument];
|
||
|
var instrumentId = synth.number;
|
||
|
///
|
||
|
bufferPending[instrumentId] = 0;
|
||
|
///
|
||
|
for (var index = 0; index < urls.length; index++) {
|
||
|
var key = urls[index];
|
||
|
requestAudio(soundfont, instrumentId, index, key);
|
||
|
}
|
||
|
}
|
||
|
///
|
||
|
setTimeout(waitForEnd, 1);
|
||
|
};
|
||
|
|
||
|
/* Load audio file: streaming | base64 | arraybuffer
|
||
|
---------------------------------------------------------------------- */
|
||
|
function loadAudio(url, onload, onerror) {
|
||
|
if (useStreamingBuffer) {
|
||
|
var audio = new Audio();
|
||
|
audio.src = url;
|
||
|
audio.controls = false;
|
||
|
audio.autoplay = false;
|
||
|
audio.preload = false;
|
||
|
audio.addEventListener('canplay', function() {
|
||
|
onload && onload(audio);
|
||
|
});
|
||
|
audio.addEventListener('error', function(err) {
|
||
|
onerror && onerror(err);
|
||
|
});
|
||
|
document.body.appendChild(audio);
|
||
|
} else if (url.indexOf('data:audio') === 0) { // Base64 string
|
||
|
var base64 = url.split(',')[1];
|
||
|
var buffer = Base64Binary.decodeArrayBuffer(base64);
|
||
|
ctx.decodeAudioData(buffer, onload, onerror);
|
||
|
} else { // XMLHTTP buffer
|
||
|
var request = new XMLHttpRequest();
|
||
|
request.open('GET', url, true);
|
||
|
request.responseType = 'arraybuffer';
|
||
|
request.onload = function() {
|
||
|
ctx.decodeAudioData(request.response, onload, onerror);
|
||
|
};
|
||
|
request.send();
|
||
|
}
|
||
|
};
|
||
|
|
||
|
function createAudioContext() {
|
||
|
return new (window.AudioContext || window.webkitAudioContext)();
|
||
|
};
|
||
|
})();
|
||
|
})(MIDI);
|