UNPKG

abcjs

Version:

Renderer for abc music notation

121 lines (107 loc) 4.7 kB
var soundsCache = require('./sounds-cache'); var pitchToNoteName = require('./pitch-to-note-name'); var centsToFactor = require("./cents-to-factor"); function placeNote(outputAudioBuffer, sampleRate, sound, startArray, volumeMultiplier, ofsMs, fadeTimeSec, noteEndSec, debugCallback) { // sound contains { instrument, pitch, volume, len, pan, tempoMultiplier // len is in whole notes. Multiply by tempoMultiplier to get seconds. // ofsMs is an offset to subtract from the note to line up programs that have different length onsets. var OfflineAC = window.OfflineAudioContext || window.webkitOfflineAudioContext; var len = sound.len * sound.tempoMultiplier; if (ofsMs) len +=ofsMs/1000; len -= noteEndSec; if (len < 0) len = 0.005; // Have some small audible length no matter how short the note is. var offlineCtx = new OfflineAC(2,Math.floor((len+fadeTimeSec)*sampleRate),sampleRate); var noteName = pitchToNoteName[sound.pitch]; if (!soundsCache[sound.instrument]) { // It shouldn't happen that the entire instrument cache wasn't created, but this has been seen in practice, so guard against it. if (debugCallback) debugCallback('placeNote skipped (instrument empty): '+sound.instrument+':'+noteName) return Promise.resolve(); } var noteBufferPromise = soundsCache[sound.instrument][noteName]; if (!noteBufferPromise) { // if the note isn't present then just skip it - it will leave a blank spot in the audio. if (debugCallback) debugCallback('placeNote skipped: '+sound.instrument+':'+noteName) return Promise.resolve(); } return noteBufferPromise .then(function (response) { // create audio buffer var source = offlineCtx.createBufferSource(); source.buffer = response.audioBuffer; // add gain // volume can be between 1 to 127. This translation to gain is just trial and error. // The smaller the first number, the more dynamic range between the quietest to loudest. // The larger the second number, the louder it will be in general. var volume = (sound.volume / 96) * volumeMultiplier; source.gainNode = offlineCtx.createGain(); // add pan if supported and present if (sound.pan && offlineCtx.createStereoPanner) { source.panNode = offlineCtx.createStereoPanner(); source.panNode.pan.setValueAtTime(sound.pan, 0); } source.gainNode.gain.value = volume; // Math.min(2, Math.max(0, volume)); source.gainNode.gain.linearRampToValueAtTime(source.gainNode.gain.value, len); source.gainNode.gain.linearRampToValueAtTime(0.0, len + fadeTimeSec); if (sound.cents) { source.playbackRate.value = centsToFactor(sound.cents); } // connect all the nodes if (source.panNode) { source.panNode.connect(offlineCtx.destination); source.gainNode.connect(source.panNode); } else { source.gainNode.connect(offlineCtx.destination); } source.connect(source.gainNode); // Do the process of creating the sound and placing it in the buffer source.start(0); if (source.noteOff) { source.noteOff(len + fadeTimeSec); } else { source.stop(len + fadeTimeSec); } var fnResolve; offlineCtx.oncomplete = function(e) { if (e.renderedBuffer && e.renderedBuffer.getChannelData) { // If the system gets overloaded or there are network problems then this can start failing. Just drop the note if so. for (var i = 0; i < startArray.length; i++) { //Math.floor(startArray[i] * sound.tempoMultiplier * sampleRate) var start = startArray[i] * sound.tempoMultiplier; if (ofsMs) start -=ofsMs/1000; if (start < 0) start = 0; // If the item that is moved back is at the very beginning of the buffer then don't move it back. To do that would be to push everything else forward. TODO-PER: this should probably be done at some point but then it would change timing in existing apps. start = Math.floor(start*sampleRate); copyToChannel(outputAudioBuffer, e.renderedBuffer, start); } } if (debugCallback) debugCallback('placeNote: '+sound.instrument+':'+noteName) fnResolve(); }; offlineCtx.startRendering(); return new Promise(function(resolve) { fnResolve = resolve; }); }) .catch(function (error) { if (debugCallback) debugCallback('placeNote catch: '+error.message) return Promise.reject(error) }); } var copyToChannel = function(toBuffer, fromBuffer, start) { for (var ch = 0; ch < 2; ch++) { var fromData = fromBuffer.getChannelData(ch); var toData = toBuffer.getChannelData(ch); // Mix the current note into the existing track for (var n = 0; n < fromData.length; n++) { toData[n + start] += fromData[n]; } } }; module.exports = placeNote;