diff --git a/residentSf2Synth/residentSf2Synth.js b/residentSf2Synth/residentSf2Synth.js index 9e2b15b..97ffc63 100644 --- a/residentSf2Synth/residentSf2Synth.js +++ b/residentSf2Synth/residentSf2Synth.js @@ -420,10 +420,11 @@ WebMIDI.residentSf2Synth = (function(window) { var bnkIndex = (channel === 9) ? 128 : bankIndex, bank = bankSet[bnkIndex], - instrument, - instrumentKey, + presetLayers, + keyLayers, note, - panpot = channelPanpot[channel] - 64, + midi = {}, + pan = channelPanpot[channel] - 64, bankIndexStr, instrStr, channelStr; // *Setting* the pitchBendSensitivity should be done by @@ -450,38 +451,38 @@ WebMIDI.residentSf2Synth = (function(window) return; } - instrument = bank[channelInstrument[channel]]; - if(instrument === undefined) + presetLayers = bank[channelInstrument[channel]]; + if(presetLayers === undefined) { bankIndexStr = bnkIndex.toString(10); instrStr = (channelInstrument[channel]).toString(10); channelStr = channel.toString(10); - console.warn("instrument not found: bank=" + bankIndexStr + " instrument=" + instrStr + " channel=" + channelStr); + console.warn("presetLayers not found: bank=" + bankIndexStr + " presetLayers=" + instrStr + " channel=" + channelStr); return; } - instrumentKey = instrument[key]; - if(!instrumentKey) + keyLayers = presetLayers[key]; + if(!keyLayers) { bankIndexStr = bnkIndex.toString(10); instrStr = (channelInstrument[channel]).toString(10); channelStr = channel.toString(10); - console.warn("instrument key not found: bank=" + bankIndexStr + " instrument=" + instrStr + " channel=" + channelStr + " key=" + key); + console.warn("presetLayers[key] not found: bank=" + bankIndexStr + " presetLayers=" + instrStr + " channel=" + channelStr + " key=" + key); return; } - panpot /= panpot < 0 ? 64 : 63; + pan /= pan < 0 ? 64 : 63; - instrumentKey.channel = channel; - instrumentKey.key = key; - instrumentKey.velocity = velocity; - instrumentKey.panpot = panpot; - instrumentKey.volume = channelVolume[channel] / 127; - instrumentKey.pitchBend = channelPitchBend[channel] - 8192; - instrumentKey.pitchBendSensitivity = getPitchBendSensitivity(channel); + midi.channel = channel; + midi.key = key; + midi.velocity = velocity; + midi.pan = pan; + midi.volume = channelVolume[channel] / 127; + midi.pitchBend = channelPitchBend[channel] - 8192; + midi.pitchBendSensitivity = getPitchBendSensitivity(channel); // note on - note = new WebMIDI.soundFontSynthNote.SoundFontSynthNote(ctx, gainMaster, instrumentKey); + note = new WebMIDI.soundFontSynthNote.SoundFontSynthNote(ctx, gainMaster, keyLayers, midi); note.noteOn(); currentNoteOns[channel].push(note); }; @@ -534,9 +535,9 @@ WebMIDI.residentSf2Synth = (function(window) channelVolume[channel] = volume; }; - ResidentSf2Synth.prototype.panpotChange = function(channel, panpot) + ResidentSf2Synth.prototype.panpotChange = function(channel, pan) { - channelPanpot[channel] = panpot; + channelPanpot[channel] = pan; }; ResidentSf2Synth.prototype.pitchBend = function(channel, lowerByte, higherByte) diff --git a/residentSf2Synth/soundFont.js b/residentSf2Synth/soundFont.js index 0048480..5a40690 100644 --- a/residentSf2Synth/soundFont.js +++ b/residentSf2Synth/soundFont.js @@ -57,7 +57,7 @@ WebMIDI.soundFont = (function() }; }, - createPresetModulator_ = function(parser, zone, index) + getPresetModulator_ = function(parser, zone, index) { var modgen = createBagModGen_( zone[index].presetModulatorIndex, @@ -71,7 +71,7 @@ WebMIDI.soundFont = (function() }; }, - createPresetGenerator_ = function(parser, zone, index) + getPresetGenerator_ = function(parser, zone, index) { var modgen = parser.createBagModGen_( zone, @@ -116,110 +116,163 @@ WebMIDI.soundFont = (function() }; }, - getModGenAmount = function(generator, enumeratorType, optDefault) - { - if (optDefault === undefined) { - optDefault = 0; - } - - return generator[enumeratorType] ? generator[enumeratorType].amount : optDefault; - }, - - createNoteInfo = function(parser, generator, preset, bankIndex, patchIndex) - { - var - sampleId, - sampleHeader, - volAttack, - volDecay, - volSustain, - volRelease, - modAttack, - modDecay, - modSustain, - modRelease, - loop, // ji - tune, - scale, - freqVibLFO, - i; - - if (generator.keyRange === undefined || generator.sampleID === undefined) { - return; - } - - volAttack = getModGenAmount(generator, 'attackVolEnv', -12000); - volDecay = getModGenAmount(generator, 'decayVolEnv', -12000); - volSustain = getModGenAmount(generator, 'sustainVolEnv'); - volRelease = getModGenAmount(generator, 'releaseVolEnv', -12000); - modAttack = getModGenAmount(generator, 'attackModEnv', -12000); - modDecay = getModGenAmount(generator, 'decayModEnv', -12000); - modSustain = getModGenAmount(generator, 'sustainModEnv'); - modRelease = getModGenAmount(generator, 'releaseModEnv', -12000); - loop = getModGenAmount(generator, 'sampleModes', 0); // ji - - tune = ( - getModGenAmount(generator, 'coarseTune') + - getModGenAmount(generator, 'fineTune') / 100 - ); - scale = getModGenAmount(generator, 'scaleTuning', 100) / 100; - freqVibLFO = getModGenAmount(generator, 'freqVibLFO'); - - for(i = generator.keyRange.lo; i <= generator.keyRange.hi; ++i) - { - if(preset[i] === undefined) - { - sampleId = getModGenAmount(generator, 'sampleID'); - sampleHeader = parser.sampleHeader[sampleId]; - - preset[i] = { - 'sample': parser.sample[sampleId], - 'sampleRate': sampleHeader.sampleRate, - 'basePlaybackRate': Math.pow( - Math.pow(2, 1 / 12), - ( - i - - getModGenAmount(generator, 'overridingRootKey', sampleHeader.originalPitch) + - tune + (sampleHeader.pitchCorrection / 100) - ) * scale - ), - 'modEnvToPitch': getModGenAmount(generator, 'modEnvToPitch') / 100, - 'scaleTuning': scale, - 'start': - getModGenAmount(generator, 'startAddrsCoarseOffset') * 32768 + - getModGenAmount(generator, 'startAddrsOffset'), - 'end': - getModGenAmount(generator, 'endAddrsCoarseOffset') * 32768 + - getModGenAmount(generator, 'endAddrsOffset'), - 'doLoop': (loop === 1 || loop === 3), // ji - 'loopStart': ( - //(sampleHeader.startLoop - sampleHeader.start) + - (sampleHeader.startLoop) + - getModGenAmount(generator, 'startloopAddrsCoarseOffset') * 32768 + - getModGenAmount(generator, 'startloopAddrsOffset') - ), - 'loopEnd': ( - //(sampleHeader.endLoop - sampleHeader.start) + - (sampleHeader.endLoop) + - getModGenAmount(generator, 'endloopAddrsCoarseOffset') * 32768 + - getModGenAmount(generator, 'endloopAddrsOffset') - ), - 'volAttack': Math.pow(2, volAttack / 1200), - 'volDecay': Math.pow(2, volDecay / 1200), - 'volSustain': volSustain / 1000, - 'volRelease': Math.pow(2, volRelease / 1200), - 'modAttack': Math.pow(2, modAttack / 1200), - 'modDecay': Math.pow(2, modDecay / 1200), - 'modSustain': modSustain / 1000, - 'modRelease': Math.pow(2, modRelease / 1200), - 'initialFilterFc': getModGenAmount(generator, 'initialFilterFc', 13500), - 'modEnvToFilterFc': getModGenAmount(generator, 'modEnvToFilterFc'), - 'initialFilterQ': getModGenAmount(generator, 'initialFilterQ'), - 'freqVibLFO': freqVibLFO ? Math.pow(2, freqVibLFO / 1200) * 8.176 : undefined - }; - } - } - }, + createKeyInfo = function(parser, generator, preset) + { + var + sampleId, + sampleHeader, + loopMode, + panAmount, + tune, + scale, + freqVibLFO, + keyIndex, + keyLayers; + + function getModGenAmount(generator, enumeratorType, defaultValue) + { + if(defaultValue === undefined) + { + throw "The default value must be defined."; + } + + return generator[enumeratorType] ? generator[enumeratorType].amount : defaultValue; + } + + function volModParamValue(generator, enumeratorType, defaultValue) + { + let rVal = getModGenAmount(generator, enumeratorType, defaultValue); + + rVal = (rVal === 0) ? 0 : Math.pow(2, rVal / 1200); + + return rVal; + } + + function range0to1(generator, enumeratorType, defaultValue) + { + let rVal = getModGenAmount(generator, enumeratorType, defaultValue); + + return (rVal > 1000 ? 1 : (rVal <= 0 ? 0 : rVal / 1000)); + } + + if(generator.keyRange === undefined || generator.sampleID === undefined) + { + return; + } + + // See sf2 spec §8.1.3 for the default values set in this function. + loopMode = getModGenAmount(generator, 'sampleModes', 0); + panAmount = getModGenAmount(generator, 'pan', 0), + tune = ( + getModGenAmount(generator, 'coarseTune', 0) + + getModGenAmount(generator, 'fineTune', 0) / 100 + ); + scale = getModGenAmount(generator, 'scaleTuning', 100) / 100; + freqVibLFO = getModGenAmount(generator, 'freqVibLFO', 0); + + for(keyIndex = generator.keyRange.lo; keyIndex <= generator.keyRange.hi; ++keyIndex) + { + // ji - August 2017 + // The terms presetZone, layer and keylayer: + // The sfspec says that a "presetZone" is "A subset of a preset containing generators, modulators, and an instrument." + // The sfspec also says that "layer" is an obsolete term for a "presetZone". + // The Awave soundfont editor says that a "layer" is "a set of regions with non-overlapping key ranges". + // The Arachno soundFont contains two "presetZones" in the Grand Piano preset. The first has a pan + // setting of -500, the second a pan setting of +500. + // I am therefore assuming that a "presetZone" is a preset-level "channel", that is sent at the same time + // as other "presetZones" in the same preset, so as to create a fuller sound. + // I use the term "keyLayer" to mean the subsection of a presetZone associated with a single key. + // A keyLayer contains a single audio sample and the parameters (generators) for playing it. + // There will always be a single MIDI output channel, whose pan position is realised by combining the + // channel's current pan value with the pan values of the key's (note's) "keyLayers". + // The sfspec allows an unlimited number of "presetZones" in the pbag chunk, so the number of "keyLayers" + // is also unlimted. + keyLayers = preset[keyIndex]; + if(keyLayers === undefined) + { + keyLayers = []; + preset[keyIndex] = keyLayers; + } + + // the first channel for this key is always at keyLayers[0], i.e. preset[keyIndex][0]. + sampleId = getModGenAmount(generator, 'sampleID', 0); + sampleHeader = parser.sampleHeader[sampleId]; + + keyLayers.push({ + 'sample': parser.sample[sampleId], + 'sampleRate': sampleHeader.sampleRate, + 'basePlaybackRate': Math.pow( + Math.pow(2, 1 / 12), + ( + keyIndex - + getModGenAmount(generator, 'overridingRootKey', sampleHeader.originalPitch) + + tune + (sampleHeader.pitchCorrection / 100) + ) * scale + ), + 'modEnvToPitch': volModParamValue(generator, 'modEnvToPitch', -12000) / 100, + 'scaleTuning': scale, + 'start': + getModGenAmount(generator, 'startAddrsCoarseOffset', 0) * 32768 + + getModGenAmount(generator, 'startAddrsOffset', 0), + 'end': + getModGenAmount(generator, 'endAddrsCoarseOffset', 0) * 32768 + + getModGenAmount(generator, 'endAddrsOffset', 0), + 'doLoop': (loopMode === 1 || loopMode === 3), // ji + 'loopStart': ( + //(sampleHeader.startLoop - sampleHeader.start) + + (sampleHeader.startLoop) + + getModGenAmount(generator, 'startloopAddrsCoarseOffset', 0) * 32768 + + getModGenAmount(generator, 'startloopAddrsOffset', 0) + ), + 'loopEnd': ( + //(sampleHeader.endLoop - sampleHeader.start) + + (sampleHeader.endLoop) + + getModGenAmount(generator, 'endloopAddrsCoarseOffset', 0) * 32768 + + getModGenAmount(generator, 'endloopAddrsOffset', 0) + ), + + 'volDelay': volModParamValue(generator, 'delayVolEnv', -12000), + 'volAttack': volModParamValue(generator, 'attackVolEnv', -12000), + 'volHold': volModParamValue(generator, 'holdVolEnv', -12000), + 'volDecay': volModParamValue(generator, 'decayVolEnv', -12000), + 'volSustain': range0to1(generator, 'sustainVolEnv', 0), // see spec + 'volRelease': volModParamValue(generator, 'releaseVolEnv', -12000), + + 'modDelay': volModParamValue(generator, 'delayModEnv', -12000), + 'modAttack': volModParamValue(generator, 'attackModEnv', -12000), + 'modHold': volModParamValue(generator, 'holdModEnv', -12000), + 'modDecay': volModParamValue(generator, 'decayModEnv', -12000), + 'modSustain': range0to1(generator, 'sustainModEnv', 0), // see spec + 'modRelease': volModParamValue(generator, 'releaseModEnv', -12000), + + 'initialFilterFc': getModGenAmount(generator, 'initialFilterFc', 13500), + 'modEnvToFilterFc': getModGenAmount(generator, 'modEnvToFilterFc', 0) / 100, + 'initialFilterQ': getModGenAmount(generator, 'initialFilterQ', 0) / 100, + 'freqVibLFO': freqVibLFO ? Math.pow(2, freqVibLFO / 1200) * 8.176 : undefined, + + // the following were not set by gree + 'modLfoToPitch': volModParamValue(generator, 'modLfoToPitch', -12000) / 100, + 'vibLfoToPitch': volModParamValue(generator, 'vibLfoToPitch', -12000) / 100, + 'modLfoToFilterFc': getModGenAmount(generator, 'modLfoToFilterFc', 0) / 100, + 'modLfoToVolume': getModGenAmount(generator, 'modLfoToVolume', 0) / 100, + 'chorusEffectsSend': range0to1(generator, 'chorusEffectsSend', 0) / 100, + 'reverbEffectsSend': range0to1(generator, 'reverbEffectsSend', 0) / 100, + 'pan' : (panAmount + 500) / 1000, + 'delayModLFO': volModParamValue(generator, 'delayModLFO', -12000), // in Arachno Grand Piano + 'freqModLFO': getModGenAmount(generator, 'freqModLFO', 0), + 'delayVibLFO': volModParamValue(generator, 'delayVibLFO', -12000), // in Arachno Grand Piano + 'keynumToModEnvHold': getModGenAmount(generator, 'keynumToModEnvHold', 0), + 'keynumToModEnvDecay': getModGenAmount(generator, 'keynumToModEnvDecay', 0), + 'keynumToVolEnvHold': getModGenAmount(generator, 'keynumToVolEnvHold', 0), + 'keynumToVolEnvDecay': getModGenAmount(generator, 'keynumToVolEnvDecay', 0), + 'velRange': generator['velRange'], // undefined, or an object like generator.keyRange having lo and hi values + 'keynum': getModGenAmount(generator, 'keynum', -1), + 'velocity': getModGenAmount(generator, 'velocity', -1), + 'initialAttenuation': getModGenAmount(generator, 'initialAttenuation', 0), + 'exclusiveClass': getModGenAmount(generator, 'exclusiveClass', 0) + }); // end push + } + }, // Parses the Uin8Array to create this soundFont's banks. getBanks = function(uint8Array, nRequiredPresets) @@ -229,11 +282,13 @@ WebMIDI.soundFont = (function() function createBanks(parser, nRequiredPresets) { var i, j, k, - presets, parsersInstruments, instruments, + presets, instruments, presetName, patchIndex, bankIndex, instrument, banks = [], bank, instr; - function createPreset(parser) + // Gets the preset level info that the parser has found in the phdr, pbag, pMod and pGen chunks + // This is similar to the getInstrumentBags function (inside the getInstruments function below, but at the preset level. + function getPresets(parser) { var i, j, preset = parser.presetHeader, @@ -256,8 +311,8 @@ WebMIDI.soundFont = (function() // preset bag for(j = bagIndex; j < bagIndexEnd; ++j) { - presetGenerator = createPresetGenerator_(parser, zone, j); - presetModulator = createPresetModulator_(parser, zone, j); + presetGenerator = getPresetGenerator_(parser, zone, j); + presetModulator = getPresetModulator_(parser, zone, j); zoneInfo.push({ generator: presetGenerator.generator, @@ -266,13 +321,6 @@ WebMIDI.soundFont = (function() modulatorSequence: presetModulator.modulatorInfo }); - //instrument = - // presetGenerator.generator['instrument'] !== void 0 ? - // presetGenerator.generator['instrument'].amount : - // presetModulator.modulator['instrument'] !== void 0 ? - // presetModulator.modulator['instrument'].amount : - // null; - if(presetGenerator.generator.instrument !== undefined) { instrument = presetGenerator.generator.instrument.amount; @@ -298,134 +346,167 @@ WebMIDI.soundFont = (function() return output; } - // ji: This is the original gree function, edited to comply with my programming style. - // I don't know why it returns all the instrument zones as a single instrument. - // It seems to work okay, but needs checking to see that nothing irregular is happening. - // Maybe its because the parser is expecting to be given a full set of presets, and only geting a - // selection. - function createInstrument(parser) - { - var i, j, - instrument = parser.instrument, - zone = parser.instrumentZone, - output = [], - bagIndex, - bagIndexEnd, - zoneInfo, - instrumentGenerator, - instrumentModulator; - - // instrument -> instrument bag -> generator / modulator - for(i = 0; i < instrument.length; ++i) - { - bagIndex = instrument[i].instrumentBagIndex; - bagIndexEnd = instrument[i + 1] ? instrument[i + 1].instrumentBagIndex : zone.length; - zoneInfo = []; - - // instrument bag - for(j = bagIndex; j < bagIndexEnd; ++j) - { - instrumentGenerator = createInstrumentGenerator_(parser, zone, j); - instrumentModulator = createInstrumentModulator_(parser, zone, j); - - zoneInfo.push({ - generator: instrumentGenerator.generator, - generatorSequence: instrumentGenerator.generatorInfo, - modulator: instrumentModulator.modulator, - modulatorSequence: instrumentModulator.modulatorInfo - }); - } - - output.push({ - name: instrument[i].instrumentName, - info: zoneInfo - }); - } - - return output; - } - // ji function: // This function returns an array containing one array per preset. Each preset array contains - // a list of instrumentZones, but without a terminating 'EOI' entry. I don't think the terminating - // 'EOI' entry is important here, but I may be wrong. - function getInstruments(parsersInstruments) + // a list of instrumentZones. The end of the list is marked by an empty entry. + function getInstruments(parser) { - var i = 0, instrIndex = -1, instruments = [], zoneIndexStr, instrZone, zoneName; - - // zoneName has invisible 0 charCodes beyond the end of the visible string, so the usual - // .length property does not work as expected. Is this unicode, or is the parser simply - // setting the name wrongly? - // This getZoneIndexString function takes account of the above problem, and returns a - // normal string containing the numeric characters visible at the end of the zoneName - // argument. The returned string can be empty if there are no visible numeric characters - // at the end of zoneName. Numeric characters _inside_ zoneName are _not_ returned. - function getZoneIndexString(zoneName) + var i = 0, parsersInstrumentBags, + instrIndex = -1, instruments = [], instrBagIndexString, instrBag, instrBagName; + + // ji: This is the original gree "creatInstrument()" function, edited to comply with my programming style. + // + // Useful Definitions: + // A Zone has a single sample, and is associated with a contiguous set of MIDI keys. + // An instrumentBag is a list of (single-channel) Zones. + // The Arachno Grand Piano, for example, has two instrumentBags, each of which contains + // the 20 Zones, for two (mono, left and right) channels. + // The returned records therefore contain *two* entries for each (stereo) preset zone. + // For example: "Grand Piano0 " (left channel) and "GrandPiano1 " (right channel) + // for the Grand Piano preset. + // + // This function returns the instrument level info that the parser has found in the inst, ibag, + // iMod and iGen chunks as a list of records (one record per mono Zone -- see definitions above: + // { + // name; // instrumentBag name + // info[]; + // } + // where info is a sub-list of records of the form: + // { + // generator[], + // generatorSequence[], + // modulator[], + // modulatorSequence[] + // } + // The generator[] and generatorSequence[] contain the values of the Generator Enumerators + // (delayModEnv etc. -- see spec) associated with each Zone in the instrumentBag + // The generator entry contains the same information as the generatorSequence entry, except that + // the generatorSequence consists of {string, value} objects, while the generator entry has + // named subentries: i.e.: The value of generator.decayModEnv is the generatorSequence.value.amount + // of the generatorSequence whose generatorSequence.type === "decayModEnv". + // Are both generator[] and generatorSequence[] returned because the order of the sequence in + // generatorSequence is important, while the values in generator[] are more accessible?? + // All this is done similarly for modulator and modulatorSequence. + function getInstrumentBags(parser) + { + var i, j, + instrument = parser.instrument, + zone = parser.instrumentZone, + output = [], + bagIndex, + bagIndexEnd, + zoneInfo, + instrumentGenerator, + instrumentModulator; + + // instrument -> instrument bag -> generator / modulator + for(i = 0; i < instrument.length; ++i) + { + bagIndex = instrument[i].instrumentBagIndex; + bagIndexEnd = instrument[i + 1] ? instrument[i + 1].instrumentBagIndex : zone.length; + zoneInfo = []; + + // instrument bag + for(j = bagIndex; j < bagIndexEnd; ++j) + { + instrumentGenerator = createInstrumentGenerator_(parser, zone, j); + instrumentModulator = createInstrumentModulator_(parser, zone, j); + + zoneInfo.push({ + generator: instrumentGenerator.generator, + generatorSequence: instrumentGenerator.generatorInfo, + modulator: instrumentModulator.modulator, + modulatorSequence: instrumentModulator.modulatorInfo + }); + } + + output.push({ + name: instrument[i].instrumentName, + info: zoneInfo + }); + } + + return output; + } + + // The parser leaves instrBagName with invisible 0 charCodes beyond the end of the visible string + // (instrBagName always has 20 chars in the soundFont file), so the usual .length property does + // not work as expected. + // This getBagIndexString function takes account of this problem, and returns a + // normal string containing the numeric characters visible at the end of the instrBagName. + // The returned string can be empty if there are no visible numeric characters + // at the end of instrBagName. Numeric characters _inside_ instrBagName are _not_ returned. + function getBagIndexString(instrBagName) + { + var i, char, charCode, rval = "", lastNumCharIndex = -1, lastAlphaCharIndex = -1; + + // instrBagName is an unusual string... (unicode?) + // console.log("instrBagName=", instrBagName); + for(i = instrBagName.length - 1; i >= 0; --i) + { + charCode = instrBagName.charCodeAt(i); + // console.log("i=", i, " charCode=", charCode); + // ignore trailing 0 charCodes + if(charCode !== 0) + { + if(lastNumCharIndex === -1) + { + lastNumCharIndex = i; + } + if(!(charCode >= 48 && charCode <= 57)) // chars '0' to '9' + { + lastAlphaCharIndex = i; + break; + } + } + } + + if(lastAlphaCharIndex < lastNumCharIndex) + { + for(i = lastAlphaCharIndex + 1; i <= lastNumCharIndex; ++i) + { + char = (instrBagName.charCodeAt(i) - 48).toString(); + // console.log("char=", char); + rval = rval.concat(char); + // console.log("rval=", rval); + } + } + return rval; + } + + // See comment at top of the getInstrumentBags function. + parsersInstrumentBags = getInstrumentBags(parser); + // See comment at top of the getInstruments function + + for(i = 0; i < parsersInstrumentBags.length; ++i) { - var i, char, charCode, rval = "", lastNumCharIndex = -1, lastAlphaCharIndex = -1; + instrBag = parsersInstrumentBags[i]; + instrBagName = instrBag.name.toString(); - // zoneName is an unusual string... (unicode?) - // console.log("zoneName=", zoneName); - for(i = zoneName.length - 1; i >= 0; --i) - { - charCode = zoneName.charCodeAt(i); - // console.log("i=", i, " charCode=", charCode); - // ignore trailing 0 charCodes - if(charCode !== 0) - { - if(lastNumCharIndex === -1) - { - lastNumCharIndex = i; - } - if(!(charCode >= 48 && charCode <= 57)) // chars '0' to '9' - { - lastAlphaCharIndex = i; - break; - } - } - } - - if(lastAlphaCharIndex < lastNumCharIndex) - { - for(i = lastAlphaCharIndex + 1; i <= lastNumCharIndex; ++i) - { - char = (zoneName.charCodeAt(i) - 48).toString(); - // console.log("char=", char); - rval = rval.concat(char); - // console.log("rval=", rval); - } - } - return rval; - } - - for(i = 0; i < parsersInstruments.length; ++i) - { - instrZone = parsersInstruments[i]; - zoneName = instrZone.name.toString(); - - if(i === parsersInstruments.length - 1) + if(i === parsersInstrumentBags.length - 1) { break; } - zoneIndexStr = getZoneIndexString(zoneName); - // zoneIndexStr contains only the visible, trailing numeric characters, if any. - if(zoneIndexStr.length === 0 || parseInt(zoneIndexStr, 10) === 0) + instrBagIndexString = getBagIndexString(instrBagName); + // instrBagIndexString contains only the visible, trailing numeric characters, if any. + if(instrBagIndexString.length === 0 || parseInt(instrBagIndexString, 10) === 0) { instrIndex++; instruments[instrIndex] = []; } - instruments[instrIndex].push(instrZone); + instruments[instrIndex].push(instrBag); } return instruments; } - presets = createPreset(parser); - // ji: I'm unsure about this function. See comment on function. - parsersInstruments = createInstrument(parser); - // ji: I'm unsure about this function. See comment on function. - instruments = getInstruments(parsersInstruments); + // Get the preset level info that the parser has found in the phdr, pbag, pMod and pGen chunks + presets = getPresets(parser); + + // Get the instrument level info that the parser has found in the inst, ibag, iMod and iGen chunks + // Each instrument now contains an array containing its instrumenBags (stereo). + instruments = getInstruments(parser); // the final entry in presets is 'EOP' if(nRequiredPresets !== (presets.length - 1)) @@ -455,7 +536,7 @@ WebMIDI.soundFont = (function() instr = instrument[j]; for(k = 0; k < instr.info.length; ++k) { - createNoteInfo(parser, instr.info[k].generator, bank[patchIndex], bankIndex, patchIndex); + createKeyInfo(parser, instr.info[k].generator, bank[patchIndex]); } } } diff --git a/residentSf2Synth/soundFontParser.js b/residentSf2Synth/soundFontParser.js index b43d052..2cf6e33 100644 --- a/residentSf2Synth/soundFontParser.js +++ b/residentSf2Synth/soundFontParser.js @@ -90,13 +90,13 @@ WebMIDI.soundFontParser = (function() throw new Error('invalid sfbk structure'); } - // INFO-list + // INFO-list (metadata) this.parseInfoList(parser.getChunk(0)); - // sdta-list + // sdta-list (audio sample data) this.parseSdtaList(parser.getChunk(1)); - // pdta-list + // pdta-list (preset data -- generators, modulators etc.) this.parsePdtaList(parser.getChunk(2)); }; @@ -167,15 +167,15 @@ WebMIDI.soundFontParser = (function() throw new Error('invalid pdta chunk'); } - this.parsePhdr(parser.getChunk(0)); - this.parsePbag(parser.getChunk(1)); - this.parsePmod(parser.getChunk(2)); - this.parsePgen(parser.getChunk(3)); - this.parseInst(parser.getChunk(4)); - this.parseIbag(parser.getChunk(5)); - this.parseImod(parser.getChunk(6)); - this.parseIgen(parser.getChunk(7)); - this.parseShdr(parser.getChunk(8)); + this.parsePhdr(parser.getChunk(0)); // the preset headers chunk + this.parsePbag(parser.getChunk(1)); // the preset index list chunk + this.parsePmod(parser.getChunk(2)); // the preset modulator list chunk + this.parsePgen(parser.getChunk(3)); // the preset generator list chunk + this.parseInst(parser.getChunk(4)); // the instrument names and indices chunk + this.parseIbag(parser.getChunk(5)); // the instrument index list chunk + this.parseImod(parser.getChunk(6)); // the instrument modulator list chunk + this.parseIgen(parser.getChunk(7)); // the instrument generator list chunk + this.parseShdr(parser.getChunk(8)); // the sample headers chunk }; SoundFontParser.prototype.parsePhdr = function(chunk) @@ -234,7 +234,7 @@ WebMIDI.soundFontParser = (function() throw new Error('invalid chunk type:' + chunk.type); } - this.presetZoneModulator = this.parseGeneratorOrModulator(chunk, 1); + this.presetZoneModulator = this.parseGenorModChunk(chunk, 1); }; SoundFontParser.prototype.parsePgen = function(chunk) @@ -243,7 +243,7 @@ WebMIDI.soundFontParser = (function() { throw new Error('invalid chunk type:' + chunk.type); } - this.presetZoneGenerator = this.parseGeneratorOrModulator(chunk, 0); + this.presetZoneGenerator = this.parseGenorModChunk(chunk, 0); }; SoundFontParser.prototype.parseInst = function(chunk) @@ -297,7 +297,7 @@ WebMIDI.soundFontParser = (function() throw new Error('invalid chunk type:' + chunk.type); } - this.instrumentZoneModulator = this.parseGeneratorOrModulator(chunk, 1); + this.instrumentZoneModulator = this.parseGenorModChunk(chunk, 1); }; SoundFontParser.prototype.parseIgen = function(chunk) @@ -307,7 +307,7 @@ WebMIDI.soundFontParser = (function() throw new Error('invalid chunk type:' + chunk.type); } - this.instrumentZoneGenerator = this.parseGeneratorOrModulator(chunk, 0); + this.instrumentZoneGenerator = this.parseGenorModChunk(chunk, 0); }; SoundFontParser.prototype.parseShdr = function(chunk) @@ -409,7 +409,7 @@ WebMIDI.soundFontParser = (function() }; }; - SoundFontParser.prototype.parseGeneratorOrModulator = function(chunk, doParseModulator) + SoundFontParser.prototype.parseGenorModChunk = function(chunk, doParseModChunk) { var code, key, output = [], data = this.input, @@ -418,14 +418,25 @@ WebMIDI.soundFontParser = (function() while(ip < size) { - if(doParseModulator > 0) + if(doParseModChunk > 0) { - // Src Oper - // TODO - ip += 2; + + // get sfModSrcOper (a 16-bit SFModulator value) + // begin gree + // // TODO + // ip += 2; + // end gree + + // begin ji + // See sf2 spec §8.2 for how to interpret the bits in an SFModulator + code = data[ip++] | (data[ip++] << 8); + key = "sfModSrcOper"; + output.push({ type: key, value: code }); + // end ji } - // Dest Oper + // sfModDestOper or sfGenOper + // and the following 2-byte value (modAmount or genAmount) code = data[ip++] | (data[ip++] << 8); key = SoundFontParser.GeneratorEnumeratorTable[code]; if(key === undefined) @@ -434,7 +445,7 @@ WebMIDI.soundFontParser = (function() // code is one of the following generator indices: 14, 18,19,20, 42, 49, 55, 59 // The spec says these should be ignored if encountered. - // Amount (gree comment) + // modAmount or genAmount output.push({ type: key, value: { @@ -447,7 +458,7 @@ WebMIDI.soundFontParser = (function() } else { - // Amount + // modAmount or genAmount switch(key) { case 'keyRange': // generator index 43, optional, lo is highest valid key, hi is lowest valid key @@ -477,15 +488,32 @@ WebMIDI.soundFontParser = (function() } } - if(doParseModulator > 0) + if(doParseModChunk > 0) { - // AmtSrcOper - // TODO - ip += 2; - - // Trans Oper - // TODO - ip += 2; + // get sfModAmtSrcOper (a 16-bit SFModulator value) + // begin gree + // // TODO + // ip += 2; + // end gree + // begin ji + // See sf2 spec §8.2 for how to interpret the bits in an SFModulator + code = data[ip++] | (data[ip++] << 8); + key = "sfModAmtSrcOper"; + output.push({ type: key, value: code }); + // end ji + + // get sfModTransOper (a 16-bit SFTransform value) + // begin gree + // // TODO + // ip += 2; + // end gree + // begin ji + // See sf2 spec §8.3 for how to interpret this value. + // The value must be either either 0 (=linear) or 2 (=absolute value). + code = data[ip++] | (data[ip++] << 8); + key = "sfModTransOper"; + output.push({ type: key, value: code }); + // end ji } } diff --git a/residentSf2Synth/soundFontSynthNote.js b/residentSf2Synth/soundFontSynthNote.js index 25c351c..1463498 100644 --- a/residentSf2Synth/soundFontSynthNote.js +++ b/residentSf2Synth/soundFontSynthNote.js @@ -9,263 +9,290 @@ * * The WebMIDI.soundFontSynthNote namespace containing the following constructor: * -* SoundFontSynthNote(ctx, gainMaster, instrument) -* +* SoundFontSynthNote(ctx, gainMaster, keyLayers) */ -/*global WebMIDI */ - WebMIDI.namespace('WebMIDI.soundFontSynthNote'); WebMIDI.soundFontSynthNote = (function() { "use strict"; - var - SoundFontSynthNote = function(ctx, gainMaster, instrument) + + var + SoundFontSynthNote = function(ctx, gainMaster, keyLayers, midi) { - this.ctx = ctx; - this.gainMaster = gainMaster; - this.instrument = instrument; - this.channel = instrument.channel; - this.key = instrument.key; - this.velocity = instrument.velocity; - this.buffer = instrument.sample; - this.playbackRate = instrument.basePlaybackRate; - this.sampleRate = instrument.sampleRate; - this.volume = instrument.volume; - this.panpot = instrument.panpot; - this.pitchBend = instrument.pitchBend; - this.pitchBendSensitivity = instrument.pitchBendSensitivity; - this.modEnvToPitch = instrument.modEnvToPitch; - - // state - this.startTime = ctx.currentTime; - this.computedPlaybackRate = this.playbackRate; - - // audio node - this.audioBuffer = null; - this.bufferSource = null; - this.panner = null; - this.gainOutput = null; - - //console.log(instrument.modAttack, instrument.modDecay, instrument.modSustain, instrument.modRelease); + this.ctx = ctx; + this.gainMaster = gainMaster; + this.keyLayers = keyLayers; + + this.channel = midi.channel; + this.key = midi.key; + this.velocity = midi.velocity; + this.pan = midi.pan; + this.volume = midi.volume; + this.pitchBend = midi.pitchBend; + this.pitchBendSensitivity = midi.pitchBendSensitivity; + + this.buffer = keyLayers[0].sample; + this.playbackRate = keyLayers[0].basePlaybackRate; + this.sampleRate = keyLayers[0].sampleRate; + this.modEnvToPitch = keyLayers[0].modEnvToPitch; + + // state + this.startTime = ctx.currentTime; + this.computedPlaybackRate = this.playbackRate; + + // audio node + this.audioBuffer = null; + this.bufferSource = null; + this.panner = null; + this.gainOutput = null; }, API = { - SoundFontSynthNote: SoundFontSynthNote // constructor + SoundFontSynthNote: SoundFontSynthNote // constructor }; - SoundFontSynthNote.prototype.noteOn = function() - { - var + SoundFontSynthNote.prototype.noteOn = function() + { + // KeyLayers are "subChannels" associated with a particular key in this preset, + // i.e. they are "subChannels" associated with this particular Note. + // All the keyLayers have been read correctly (as far as I know) from the SoundFont file, + // but this file ignores all but the first (keyLayers[0]). + // (The Arachno SoundFont's preset 0 -- Grand Piano -- has two layers, in which + // keyLayer[0].pan is always -500 and keylayer[1].pan is always 500.) + // This version of soundFontSynthNote.js: + // 1) ignores all but the first keyLayer, and + // 2) ignores the first keyLayer's *pan* attribute. + // 3) plays the layer at the position set by the value of *this.pan* (see midi.pan above). + // TODO 1: Implement the playing of stereo samples, using stereo Web Audio buffers. + // + // Each keyLayer has an entry for every soundFont "generator" in the spec, except those + // whose value has been used to calculate the values of the other "generator"s and should + // no longer be needed. + // If a soundFont "generator" was not present in the soundFont, it will have its default + // value in the keyLayer. + // + // The following "generator"s are present in the Arachno Grand Piano preset, and are + // the same for every key in the preset, but are not used by this file: + // chorusEffectsSend (soundfile amount: 50, value here: 0.05) + // reverbEffectsSend (soundfile amount: 200, value here: 0.20) + // pan (layer 0 (left) soundfile amount: -500, value here: 0 -- completely left + // layer 1 (right)soundfile amount: 500, value here: 1 -- completely right) + // delayModLFO (soundfile amount: -7973, value here: 0.01) + // delayVibLFO (soundfile amount: -7973, value here: 0.01) + // TODO 2: Implement the playing of *all* the soundFont "generator"s, especially these five. + // N.B. the returned value of such unused generators is probably correct, but should be checked + // in soundFont.js. The position of the decimal point should be specially carefully checked. + + var buffer, channelData, bufferSource, filter, panner, output, outputGain, baseFreq, peekFreq, sustainFreq, ctx = this.ctx, - instrument = this.instrument, + keyLayers = this.keyLayers, sample = this.buffer, now = this.ctx.currentTime, - volAttack = now + instrument.volAttack, - modAttack = now + instrument.modAttack, + // The following keylayers[0] attributes are the *durations* of their respective envelope phases in seconds: + // volDelay, volAttack, volHold, volDelay, volRelease, + // modDelay, modAttack, modHold, modDelay, modRelease, + // The volSustain and modSustain attributes are *factors* in the range [1.00 .. 0.00] (inclusive). + // In general, all keyLayer attributes have directly usable values here in this file. + // The conversions from the integer amounts in the soundFont have been done earlier. + volDelay = now + keyLayers[0].volDelay, + volAttack = volDelay + keyLayers[0].volAttack, + volHold = volAttack + keyLayers[0].volHold, + volDecay = volHold + (keyLayers[0].volDecay * keyLayers[0].volSustain), // see spec! ji + + modDelay = now + keyLayers[0].modDelay, + modAttack = modDelay + keyLayers[0].modAttack, + modHold = modAttack + keyLayers[0].modHold, + modDecay = modHold + (keyLayers[0].modDecay * keyLayers[0].modSustain), // see spec! ji + volLevel = this.volume * Math.pow((this.velocity / 127), 2), // ji 21.08.2017 - volDecay = volAttack + instrument.volDecay, - modDecay = modAttack + instrument.modDecay, + loopStart = 0, loopEnd = 0, - startTime = instrument.start / this.sampleRate; - - function amountToFreq(val) - { - return Math.pow(2, (val - 6900) / 1200) * 440; - } - - if(instrument.doLoop === true) - { - loopStart = instrument.loopStart / this.sampleRate; - loopEnd = instrument.loopEnd / this.sampleRate; - } - sample = sample.subarray(0, sample.length + instrument.end); - this.audioBuffer = ctx.createBuffer(1, sample.length, this.sampleRate); - buffer = this.audioBuffer; - channelData = buffer.getChannelData(0); - channelData.set(sample); - - // buffer source - this.bufferSource = ctx.createBufferSource(); - bufferSource = this.bufferSource; - bufferSource.buffer = buffer; - /* ji begin changes December 2015 */ - // This line was originally: - // bufferSource.loop = (this.channel !== 9); - bufferSource.loop = (this.channel !== 9) && (instrument.doLoop === true); - /* ji end changes December 2015 */ - bufferSource.loopStart = loopStart; - bufferSource.loopEnd = loopEnd; - this.updatePitchBend(this.pitchBend); - - // audio node - this.panner = ctx.createPanner(); - panner = this.panner; - this.gainOutput = ctx.createGain(); - output = this.gainOutput; - outputGain = output.gain; - - // filter - this.filter = ctx.createBiquadFilter(); - filter = this.filter; - filter.type = 'lowpass'; - - // panpot - panner.panningModel = 'HRTF'; - panner.setPosition( - Math.sin(this.panpot * Math.PI / 2), + startTime = keyLayers[0].start / this.sampleRate; + + function amountToFreq(val) + { + return Math.pow(2, (val - 6900) / 1200) * 440; + } + + if(keyLayers[0].doLoop === true) + { + loopStart = keyLayers[0].loopStart / this.sampleRate; + loopEnd = keyLayers[0].loopEnd / this.sampleRate; + } + sample = sample.subarray(0, sample.length + keyLayers[0].end); + this.audioBuffer = ctx.createBuffer(1, sample.length, this.sampleRate); + buffer = this.audioBuffer; + channelData = buffer.getChannelData(0); + channelData.set(sample); + + // buffer source + this.bufferSource = ctx.createBufferSource(); + bufferSource = this.bufferSource; + bufferSource.buffer = buffer; + /* ji begin changes December 2015 */ + // This line was originally: + // bufferSource.loop = (this.channel !== 9); + bufferSource.loop = (this.channel !== 9) && (keyLayers[0].doLoop === true); + /* ji end changes December 2015 */ + bufferSource.loopStart = loopStart; + bufferSource.loopEnd = loopEnd; + this.updatePitchBend(this.pitchBend); + + // audio node + this.panner = ctx.createPanner(); + panner = this.panner; + this.gainOutput = ctx.createGain(); + output = this.gainOutput; + outputGain = output.gain; + + // filter + this.filter = ctx.createBiquadFilter(); + filter = this.filter; + filter.type = 'lowpass'; + + // pan + panner.panningModel = 'HRTF'; + panner.setPosition( + Math.sin(this.pan * Math.PI / 2), 0, - Math.cos(this.panpot * Math.PI / 2) + Math.cos(this.pan * Math.PI / 2) ); - //--------------------------------------------------------------------------- - // Attack, Decay, Sustain - //--------------------------------------------------------------------------- - outputGain.setValueAtTime(0, now); - - // begin original gree - //outputGain.linearRampToValueAtTime(this.volume * (this.velocity / 127), volAttack); - //outputGain.linearRampToValueAtTime(this.volume * (1 - instrument.volSustain), volDecay); - // end original gree - - // begin ji changes August 2017 - // volLevel is a new variable, defined in the vars above. - outputGain.linearRampToValueAtTime(volLevel, volAttack); - // For the following line see https://github.com/notator/WebMIDISynthHost/issues/29 - // Thanks @timjrd ! - // ji -- the instrument.volSustain attribute is a level parameter, not like the other - // instrument.vol... attributes (which are time values). - outputGain.linearRampToValueAtTime(volLevel * (1 - instrument.volSustain), volDecay); - // end ji changes August 2017 - - // begin ji changes November 2015. - // The following original line was a (deliberate, forgotten?) bug that threw an out-of-range - // exception when instrument['initialFilterQ'] > 0: - // filter.Q.setValueAtTime(instrument['initialFilterQ'] * Math.pow(10, 200), now); - // The following line seems to work, but is it realy correct? - filter.Q.setValueAtTime(instrument.initialFilterQ, now); - // end ji ji changes November 2015 - - baseFreq = amountToFreq(instrument.initialFilterFc); - peekFreq = amountToFreq(instrument.initialFilterFc + instrument.modEnvToFilterFc); - sustainFreq = baseFreq + (peekFreq - baseFreq) * (1 - instrument.modSustain); - filter.frequency.setValueAtTime(baseFreq, now); - filter.frequency.linearRampToValueAtTime(peekFreq, modAttack); - filter.frequency.linearRampToValueAtTime(sustainFreq, modDecay); - - // connect - bufferSource.connect(filter); - filter.connect(panner); - panner.connect(output); - output.connect(this.gainMaster); - - // fire - bufferSource.start(0, startTime); - }; - - SoundFontSynthNote.prototype.noteOff = function() - { - var - instrument = this.instrument, + //--------------------------------------------------------------------------- + // Attack, Decay, Sustain + //--------------------------------------------------------------------------- + + outputGain.setValueAtTime(0, now); + if(volDelay > now) + { + outputGain.linearRampToValueAtTime(0, volDelay); + } + outputGain.linearRampToValueAtTime(volLevel, volAttack); + outputGain.linearRampToValueAtTime(volLevel, volHold); + outputGain.linearRampToValueAtTime(volLevel * (1 - keyLayers[0].volSustain), volDecay); + + // begin ji changes November 2015. + // The following original line was a (deliberate, forgotten?) gree bug that threw an out-of-range + // exception when keyLayers[0]['initialFilterQ'] > 0: + // filter.Q.setValueAtTime(keyLayers[0]['initialFilterQ'] * Math.pow(10, 200), now); + // The following line seems to work, but is it realy correct? + filter.Q.setValueAtTime(keyLayers[0].initialFilterQ, now); + // end ji ji changes November 2015 + + baseFreq = amountToFreq(keyLayers[0].initialFilterFc); + peekFreq = amountToFreq(keyLayers[0].initialFilterFc + keyLayers[0].modEnvToFilterFc); + sustainFreq = baseFreq + ((peekFreq - baseFreq) * (1 - keyLayers[0].modSustain)); + + filter.frequency.setValueAtTime(baseFreq, now); + if(modDelay > now) + { + filter.frequency.linearRampToValueAtTime(baseFreq, modDelay); + } + filter.frequency.linearRampToValueAtTime(peekFreq, modAttack); + filter.frequency.linearRampToValueAtTime(peekFreq, modHold); + filter.frequency.linearRampToValueAtTime(sustainFreq, modDecay); + + // connect + bufferSource.connect(filter); + filter.connect(panner); + panner.connect(output); + output.connect(this.gainMaster); + + // fire + bufferSource.start(0, startTime); + }; + + // current ji noteOff function + SoundFontSynthNote.prototype.noteOff = function() + { + var + keyLayers = this.keyLayers, bufferSource = this.bufferSource, output = this.gainOutput, now = this.ctx.currentTime, + volRelease = keyLayers[0].volRelease, + modRelease = keyLayers[0].modRelease, + volEndTime = now + volRelease, + modEndTime = now + modRelease; - // begin gree - // volEndTime = now + instrument.volRelease, - // modEndTime = now + instrument.modRelease; - // end gree + if(!this.audioBuffer) + { + return; + } + + //--------------------------------------------------------------------------- + // Release + //--------------------------------------------------------------------------- + // begin original gree + //output.gain.cancelScheduledValues(0); + //output.gain.linearRampToValueAtTime(0, volEndTime); + //bufferSource.playbackRate.cancelScheduledValues(0); + //bufferSource.playbackRate.linearRampToValueAtTime(this.computedPlaybackRate, modEndTime); + // end original gree // begin ji - // instrument.volRelease is 3.08 in preset 0 (grand piano) in the Arachno font. - // It cannot be the case that a piano note only stops 3.08 seconds after a - // noteOff arrives. - // The following line limits the value to 0.05 seconds. - // This is a temporary kludge, pending the proper solution to the problem... - volRelease = (instrument.volRelease > 0.05) ? 0.05 : instrument.volRelease, - modRelease = (instrument.modRelease > 0.05) ? 0.05 : instrument.modRelease, - volEndTime = now + volRelease, - modEndTime = now + modRelease; - // end ji - - if(!this.audioBuffer) - { - return; - } - - //--------------------------------------------------------------------------- - // Release - //--------------------------------------------------------------------------- - // begin original gree - //output.gain.cancelScheduledValues(0); - //output.gain.linearRampToValueAtTime(0, volEndTime); - //bufferSource.playbackRate.cancelScheduledValues(0); - //bufferSource.playbackRate.linearRampToValueAtTime(this.computedPlaybackRate, modEndTime); - // end original gree - - // begin ji - // latest changes: - // 1. use setTargetAtTime() instead of linearRampToValueAtTime(0, volEndTime). (Suggested by Timothée Jourde on GitHub). - // 2. call cancelScheduledValues(...) _after_ setting the envelopes, not before. - output.gain.setTargetAtTime(0, now, volRelease); - output.gain.cancelScheduledValues(volEndTime); - bufferSource.playbackRate.linearRampToValueAtTime(this.computedPlaybackRate, modEndTime); - bufferSource.playbackRate.cancelScheduledValues(modEndTime); - // end ji - - bufferSource.loop = false; - bufferSource.stop(volEndTime); - - // disconnect - setTimeout( + // 1. use setTargetAtTime() instead of linearRampToValueAtTime(0, volEndTime). (Suggested by Timothée Jourde on GitHub). + // 2. call cancelScheduledValues(...) _after_ setting the envelopes, not before. + output.gain.setTargetAtTime(0, now, volRelease); + output.gain.cancelScheduledValues(volEndTime); + bufferSource.playbackRate.linearRampToValueAtTime(this.computedPlaybackRate, modEndTime); + bufferSource.playbackRate.cancelScheduledValues(modEndTime); + // end ji + + bufferSource.loop = false; + bufferSource.stop(volEndTime); + + // disconnect + setTimeout( (function(note) - { - return function() - { - note.bufferSource.disconnect(0); - note.panner.disconnect(0); - note.gainOutput.disconnect(0); - }; - }(this)), - instrument.volRelease * 1000 + { + return function() + { + note.bufferSource.disconnect(0); + note.panner.disconnect(0); + note.gainOutput.disconnect(0); + }; + }(this)), + keyLayers[0].volRelease ); - }; + }; - SoundFontSynthNote.prototype.schedulePlaybackRate = function() - { - var + SoundFontSynthNote.prototype.schedulePlaybackRate = function() + { + var playbackRate = this.bufferSource.playbackRate, computed = this.computedPlaybackRate, start = this.startTime, - instrument = this.instrument, - modAttack = start + instrument.modAttack, - modDecay = modAttack + instrument.modDecay, - peekPitch = computed * Math.pow(Math.pow(2, 1 / 12), this.modEnvToPitch * this.instrument.scaleTuning); - - playbackRate.cancelScheduledValues(0); - playbackRate.setValueAtTime(computed, start); - playbackRate.linearRampToValueAtTime(peekPitch, modAttack); - playbackRate.linearRampToValueAtTime(computed + (peekPitch - computed) * (1 - instrument.modSustain), modDecay); - }; - - SoundFontSynthNote.prototype.updatePitchBend = function(pitchBend) - { - this.computedPlaybackRate = this.playbackRate * Math.pow( + keyLayers = this.keyLayers, + modAttack = start + keyLayers[0].modAttack, + modDecay = modAttack + keyLayers[0].modDecay, + peekPitch = computed * Math.pow(Math.pow(2, 1 / 12), this.modEnvToPitch * keyLayers[0].scaleTuning); + + playbackRate.cancelScheduledValues(0); + playbackRate.setValueAtTime(computed, start); + playbackRate.linearRampToValueAtTime(peekPitch, modAttack); + playbackRate.linearRampToValueAtTime(computed + (peekPitch - computed) * (1 - keyLayers[0].modSustain), modDecay); + }; + + SoundFontSynthNote.prototype.updatePitchBend = function(pitchBend) + { + this.computedPlaybackRate = this.playbackRate * Math.pow( Math.pow(2, 1 / 12), ( this.pitchBendSensitivity * ( pitchBend / (pitchBend < 0 ? 8192 : 8191) ) - ) * this.instrument.scaleTuning + ) * this.keyLayers[0].scaleTuning ); - this.schedulePlaybackRate(); - }; + this.schedulePlaybackRate(); + }; - return API; + return API; }());