music21j-port
Version:
A toolkit for computer-aided musicology, Javascript version
328 lines (288 loc) • 9.64 kB
JavaScript
/*
----------------------------------------------------------
Web Audio API - OGG or MPEG Soundbank
----------------------------------------------------------
http://webaudio.github.io/web-audio-api/
----------------------------------------------------------
*/
import { DEBUG } from './debug.js';
import { channels, GM, keyToNote, noteToKey } from './gm.js';
import * as Base64Binary from './shim/Base64binary.js';
// information to share with loader...
export const shared_root_info = {};
const audioContext = null; // new AudioContext();
let useStreamingBuffer = false; // !!audioContext.createMediaElementSource;
let ctx; // audio context
const sources = {};
const effects = {};
let masterVolume = 127;
const audioBuffers = {};
export const send = (data, delay) => { };
export const setController = (channelId, type, value, delay) => { };
export const setVolume = (channelId, volume, delay) => {
if (delay) {
setTimeout(() => {
masterVolume = volume;
}, delay * 1000);
} else {
masterVolume = volume;
}
};
export const programChange = (channelId, program, delay) => {
// delay is ignored
const channel = channels[channelId];
if (channel) {
channel.program = program;
}
};
export const pitchBend = function(channelId, bend, delay) {
// delay is ignored
const channel = channels[channelId];
if (channel) {
if (delay) {
setTimeout(() => channel.pitchBend = bend,
delay);
} else {
channel.pitchBend = bend;
}
}
};
export const noteOn = (channelId, noteId, velocity, delay) => {
delay = delay || 0;
// check whether the note exists
const channel = channels[channelId];
const program = channel.program;
const bufferId = program + 'x' + noteId;
const buffer = audioBuffers[bufferId];
if (!buffer) {
if (DEBUG) {
console.log('no buffer', GM.byId[program].id, program, channelId);
}
return;
}
// convert relative delay to absolute delay
if (delay < ctx.currentTime) {
delay += ctx.currentTime;
}
// create audio buffer
let source;
if (useStreamingBuffer) {
source = ctx.createMediaElementSource(buffer);
} else { // XMLHTTP buffer
source = ctx.createBufferSource();
source.buffer = buffer;
}
// add effects to buffer
if (effects) {
let chain = source;
for (let key in effects) {
chain.connect(effects[key].input);
chain = effects[key];
}
}
// add gain + pitchShift
const 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(() => {
buffer.currentTime = 0;
buffer.play()
}, delay * 1000);
} else {
buffer.currentTime = 0;
buffer.play()
}
} else {
source.start(delay || 0);
}
sources[channelId + 'x' + noteId] = source;
return source;
};
export const noteOff = (channelId, noteId, delay) => {
delay = delay || 0;
// check whether the note exists
const channel = channels[channelId];
const program = channel.program;
const bufferId = program + 'x' + noteId;
const buffer = audioBuffers[bufferId];
if (buffer) {
if (delay < ctx.currentTime) {
delay += ctx.currentTime;
}
let source = sources[channelId + 'x' + 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
const gain = source.gainNode.gain;
gain.linearRampToValueAtTime(gain.value, delay);
gain.linearRampToValueAtTime(-1.0, delay + 0.3);
}
if (useStreamingBuffer) {
if (delay) {
setTimeout(() => {
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 + 'x' + noteId];
return source;
}
}
};
export const chordOn = (channel, chord, velocity, delay) => {
const res = {};
for (const note of chord) {
res[note] = noteOn(channel, note, velocity, delay);
}
return res;
};
export const chordOff = (channel, chord, delay) => {
const res = {};
for (const note of chord) {
res[note] = noteOff(channel, note, delay);
}
return res;
};
export const stopAllNotes = () => {
for (let sid in sources) {
let delay = 0;
if (delay < ctx.currentTime) {
delay += ctx.currentTime;
}
const 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];
}
};
export const setEffects = list => {
if (ctx && ctx.tunajs) {
for (const data of list) {
const effect = new ctx.tunajs[data.type](data);
effect.connect(ctx.destination);
effects[data.type] = effect;
}
} else {
return console.log('Effects module not installed.');
}
};
export const connect = opts => {
setContext(ctx || createAudioContext(), opts.onsuccess);
};
export const getContext = () => {
return ctx;
};
export const setContext = (newCtx, onsuccess, onprogress, onerror) => {
ctx = newCtx;
// tuna.js effects module - https://github.com/Dinahmoe/tuna
if (typeof Tuna !== 'undefined' && !(ctx.tunajs instanceof Tuna)) {
ctx.tunajs = new Tuna(ctx);
}
// loading audio files
const urls = [];
for (let key in keyToNote) {
urls.push(key);
}
const waitForEnd = instrument => {
for (const key of Object.keys(bufferPending)) { // has pending items
if (bufferPending[key]) {
return;
}
}
if (onsuccess) { // run onsuccess once
onsuccess();
onsuccess = null;
}
};
const requestAudio = (soundfont, programId, index, key) => {
const url = soundfont[key];
if (url) {
bufferPending[programId] ++;
loadAudio(url, buffer => {
buffer.id = key;
const noteId = keyToNote[key];
audioBuffers[programId + 'x' + noteId] = buffer;
if (--bufferPending[programId] === 0) {
const percent = index / 87;
if (DEBUG) {
console.log(GM.byId[programId], 'processing: ', percent);
}
soundfont.isLoaded = true;
waitForEnd(programId);
}
}, err => {
console.log(err);
});
}
};
const bufferPending = {};
for (const [instrument, soundfont] of Object.entries(shared_root_info.Soundfont)) {
if (soundfont.isLoaded) {
continue;
}
const spec = GM.byName[instrument];
if (spec) {
const programId = spec.program;
bufferPending[programId] = 0;
for (let index = 0; index < urls.length; index++) {
const key = urls[index];
requestAudio(soundfont, programId, index, key);
}
}
}
setTimeout(waitForEnd, 1);
};
/* Load audio file: streaming | base64 | arraybuffer
---------------------------------------------------------------------- */
export const loadAudio = (url, onsuccess, onerror) => {
if (useStreamingBuffer) {
const audio = new Audio();
audio.src = url;
audio.controls = false;
audio.autoplay = false;
audio.preload = false;
audio.addEventListener('canplay', () => {
onsuccess && onsuccess(audio);
});
audio.addEventListener('error', err => {
onerror && onerror(err);
});
document.body.appendChild(audio);
} else if (url.indexOf('data:audio') === 0) { // Base64 string
const base64 = url.split(',')[1];
const buffer = Base64Binary.decodeArrayBuffer(base64);
return ctx.decodeAudioData(buffer, onsuccess, onerror);
} else { // XMLHTTP buffer
const request = new XMLHttpRequest();
request.open('GET', url, true);
request.responseType = 'arraybuffer';
request.onsuccess = () => {
return ctx.decodeAudioData(request.response, onsuccess, onerror);
};
request.send();
}
};
export const createAudioContext = () => {
return new (window.AudioContext || window.webkitAudioContext)();
};