Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
78 changes: 40 additions & 38 deletions bin/sf2synth.min.js

Large diffs are not rendered by default.

6 changes: 3 additions & 3 deletions bin/sf2synth.min.js.map

Large diffs are not rendered by default.

2 changes: 1 addition & 1 deletion closure-primitives/deps.js
Original file line number Diff line number Diff line change
Expand Up @@ -9,4 +9,4 @@ goog.addDependency('../src/sound_font_synth.js', ['SoundFont.Synthesizer'], ['So
goog.addDependency('../src/sound_font_synth_note.js', ['SoundFont.SynthesizerNote'], []);
goog.addDependency('../src/typedef.js', ['Typedef'], []);
goog.addDependency('../src/wml.js', ['SoundFont.WebMidiLink'], ['SoundFont.Synthesizer']);
goog.addDependency('../tmp/sf2synth_note.js', ['SoundFontSynthNote'], []);
//goog.addDependency('../tmp/sf2synth_note.js', ['SoundFontSynthNote'], []);
153 changes: 131 additions & 22 deletions src/sound_font_synth.js
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,12 @@ SoundFont.Synthesizer = function(input) {
[0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0];
this.channelPitchBendSensitivity =
[0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0];
/** @type {Array.<number>} */
this.expression =
[0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0];
/** @type {Array.<number>} */
this.reverb =
[40, 40, 40, 40, 40, 40, 40, 40, 40, 40 ,40, 40, 40, 40, 40, 40];
/** @type {Array.<Array.<SoundFont.SynthesizerNote>>} */
this.currentNoteOn = [
[], [], [], [], [], [], [], [],
Expand All @@ -49,6 +55,19 @@ SoundFont.Synthesizer = function(input) {
this.baseVolume = 1 / 0x8000;
/** @type {number} */
this.masterVolume = 16384;

/** @type {boolean} **/
this.isXG;
/** @type {boolean} **/
this.isGS;
/** @type {boolean} **/
this.isReset;
/** @type {Array} */
this.bankMsb =
[0, 0, 0, 0, 0, 0, 0, 0, 128, 0, 0, 0, 0, 0, 0, 0];
/** @type {Array} */
this.bankLsb =
[0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0];

/** @type {HTMLTableElement} */
this.table;
Expand Down Expand Up @@ -215,19 +234,28 @@ SoundFont.Synthesizer.ProgramNames = [
SoundFont.Synthesizer.prototype.init = function() {
/** @type {number} */
var i;
this.isXG = false;
this.isGS = false;
this.isReset = true;

this.parser = new SoundFont.Parser(this.input);
this.bankSet = this.createAllInstruments();

for (i = 0; i < 16; ++i) {
this.programChange(i, i);
this.programChange(i, i==9 ? 0 : i);
this.volumeChange(i, 0x64);
this.panpotChange(i, 0x40);
this.Expression(i, 0x7F);
this.pitchBend(i, 0x00, 0x40); // 8192
this.pitchBendSensitivity(i, 2);
this.bankSelectMsb(i, i==9 ? 0x7F : 0x00);
this.bankSelectLsb(i, 0x00);
this.reverbSend(i, 0x28);
this.allNoteOff(i);
}
};


/**
* @param {Uint8Array} input
*/
Expand Down Expand Up @@ -567,28 +595,53 @@ SoundFont.Synthesizer.prototype.createTableLine = function(array, isTitleLine) {
* @param {number} velocity 強さ.
*/
SoundFont.Synthesizer.prototype.noteOn = function(channel, key, velocity) {
var bankNum = this.bankMsb[channel];
if (this.isXG) {
// XG音源は、MSB→LSBの優先順でバンクセレクトをする。SoundFontではSFXがバンク125に入っているため小細工
// Bank Select MSB #0 (Voice Type: Normal)
// Bank Select MSB #64 (Voice Type: SFX)
// Bank Select MSB #126 (Voice Type: Drum)
// Bank Select MSB #127 (Voice Type: Drum)
if (this.bankMsb[channel] === 64){
bankNum = 125;
}else if (this.bankLsb[channel] !== 0 && this.bankMsb[channel] !== 0 && this.bankMsb[channel] !== 126 && this.bankMsb[channel] !== 127){
bankNum = this.bankLsb[channel];
}
}else if (!this.isGS){
bankNum = 0;
}
if (channel == 9) bankNum = this.isXG ? 127 : 128;

//console.log(this.bank, this.bankSelect);
/** @type {Object} */
var bank = this.bankSet[channel === 9 ? 128 : this.bank];
var bank = this.bankSet[bankNum] ? this.bankSet[bankNum] : this.bankSet[channel == 9 ? bankNum : 0];
/** @type {Object} */
var instrument = bank[this.channelInstrument[channel]];
var instrument = bank[this.channelInstrument[channel]] ? bank[this.channelInstrument[channel]] : this.bankSet[0][this.channelInstrument[channel]];
/** @type {Object} */
var instrumentKey;
/** @type {SoundFont.SynthesizerNote} */
var note;

if (this.table) {
this.table.querySelector(
var element = this.table.querySelector(
'tbody > ' +
'tr:nth-child(' + (channel+1) + ') > ' +
'td:nth-child(' + (SoundFont.Synthesizer.TableHeader.length+key+1) + ')'
).classList.add('note-on');
);
element.classList.add('note-on');
element.style.opacity = velocity / 127; // 強弱をつける
}

if ((bankNum === 127 || bankNum === 128) && (key === 42 || key === 44) ){
// ドラムパートの時にハイハットを閉じる
this.noteOff(channel, 46, 0);
}

if (!instrument) {
// TODO
// バンク0にも音がない場合はさすがに警告を出す
goog.global.console.warn(
"instrument not found: bank=%s instrument=%s channel=%s",
channel === 9 ? 128 : this.bank,
bankNum,
this.channelInstrument[channel],
channel
);
Expand All @@ -601,7 +654,7 @@ SoundFont.Synthesizer.prototype.noteOn = function(channel, key, velocity) {
// TODO
goog.global.console.warn(
"instrument not found: bank=%s instrument=%s channel=%s key=%s",
channel === 9 ? 128 : this.bank,
bankNum,
this.channelInstrument[channel],
channel,
key
Expand All @@ -617,9 +670,10 @@ SoundFont.Synthesizer.prototype.noteOn = function(channel, key, velocity) {
instrumentKey['key'] = key;
instrumentKey['velocity'] = velocity;
instrumentKey['panpot'] = panpot;
instrumentKey['volume'] = this.channelVolume[channel] / 127;
instrumentKey['volume'] = Math.pow((this.channelVolume[channel] / 127) * (this.expression[channel] / 127),2);
instrumentKey['pitchBend'] = this.channelPitchBend[channel] - 8192;
instrumentKey['pitchBendSensitivity'] = this.channelPitchBendSensitivity[channel];
instrumentKey['reverb'] = this.reverb[channel]

// note on
note = new SoundFont.SynthesizerNote(this.ctx, this.gainMaster, instrumentKey);
Expand All @@ -633,10 +687,11 @@ SoundFont.Synthesizer.prototype.noteOn = function(channel, key, velocity) {
* @param {number} velocity 強さ.
*/
SoundFont.Synthesizer.prototype.noteOff = function(channel, key, velocity) {
//console.log(this.bank, this.bankSelect);
/** @type {Object} */
var bank = this.bankSet[channel === 9 ? 128 : this.bank];
var bank = this.bankSet[channel == 9 ? 127 : 0]; // 音を鳴らすわけではないのでいい加減だ・・・
/** @type {Object} */
var instrument = bank[this.channelInstrument[channel]];
var instrument = bank[this.channelInstrument[channel]] ? bank[this.channelInstrument[channel]] :bank[0][0];
/** @type {number} */
var i;
/** @type {number} */
Expand All @@ -647,11 +702,13 @@ SoundFont.Synthesizer.prototype.noteOff = function(channel, key, velocity) {
var note;

if (this.table) {
this.table.querySelector(
var element = this.table.querySelector(
'tbody > ' +
'tr:nth-child(' + (channel+1) + ') > ' +
'td:nth-child(' + (key+SoundFont.Synthesizer.TableHeader.length+1) + ')'
).classList.remove('note-on');
'tr:nth-child(' + (channel+1) + ') > ' +
'td:nth-child(' + (SoundFont.Synthesizer.TableHeader.length+key+1) + ')'
);
element.classList.remove('note-on');
element.style.opacity = 1;
}

if (!instrument) {
Expand All @@ -669,6 +726,7 @@ SoundFont.Synthesizer.prototype.noteOff = function(channel, key, velocity) {
}
};


/**
* @param {number} channel 音色を変更するチャンネル.
* @param {number} instrument 音色番号.
Expand All @@ -679,11 +737,10 @@ SoundFont.Synthesizer.prototype.programChange = function(channel, instrument) {
this.table.querySelector('tbody > tr:nth-child(' + (channel+1) + ') > td:first-child > select').selectedIndex = instrument;
}
}
// リズムトラックは無視する
if (channel === 9) {
return;
}

// GM音源の場合リズムトラックは無視する
// if (channel === 9 && !(this.isXG || this.isGS)) {
// return;
// }
this.channelInstrument[channel] = instrument;
};

Expand Down Expand Up @@ -739,6 +796,14 @@ SoundFont.Synthesizer.prototype.pitchBend = function(channel, lowerByte, higherB
this.channelPitchBend[channel] = bend;
};

/**
* @param {number} channel expression を変更するチャンネル.
* @param {number} depth depth(0-127).
*/
SoundFont.Synthesizer.prototype.Expression = function(channel, depth) {
this.expression[channel] = depth;
};

/**
* @param {number} channel pitch bend sensitivity を変更するチャンネル.
* @param {number} sensitivity
Expand All @@ -752,21 +817,65 @@ SoundFont.Synthesizer.prototype.pitchBendSensitivity = function(channel, sensiti
};

/**
* 発音中の音をすべてオフ
* @param {number} channel 音を消すチャンネル.
*/
SoundFont.Synthesizer.prototype.allSoundOff = function(channel) {
/** @type {number} */
var i;

for (i = 0; i < 127; ++i) {
this.noteOff(channel, i, 0);
}
};

/**
* ノートオンしているノートをすべてオフ
* @param {number} channel 音を消すチャンネル.
*/
SoundFont.Synthesizer.prototype.allNoteOff = function(channel) {
/** @type {Array.<SoundFont.SynthesizerNote>} */
var currentNoteOn = this.currentNoteOn[channel];

while (currentNoteOn.length > 0) {
this.noteOff(channel, currentNoteOn[0].key, 0);
}
};

}
/**
* @param {number} channel リセットするチャンネル
*/
SoundFont.Synthesizer.prototype.resetAllControl = function(channel) {
this.pitchBend(channel, 0x00, 0x40); // 8192
};

/**
* @param {number} channel チャンネルのバンクセレクトMSB
* @param {number} value 値
*/
SoundFont.Synthesizer.prototype.bankSelectMsb = function(channel, value) {
this.bankMsb[channel] = value;
};

/**
* @param {number} channel チャンネルのバンクセレクトLSB
* @param {number} value 値
*/
SoundFont.Synthesizer.prototype.bankSelectLsb = function(channel, value) {
this.bankLsb[channel] = value;
};
/**
* @param {number} channel チャンネルのリバーブエフェクト量
* @param {number} value 値
*/
SoundFont.Synthesizer.prototype.reverbSend = function(channel, value) {
this.reverb[channel] = value;
};

Array.prototype.in_array = function(val) {
for(var i = 0, l = this.length; i < l; i++) {
if(this[i] == val) {
return true;
}
}
return false;
}
36 changes: 33 additions & 3 deletions src/sound_font_synth_note.js
Original file line number Diff line number Diff line change
Expand Up @@ -54,6 +54,8 @@ SoundFont.SynthesizerNote = function(ctx, destination, instrument) {
this.pitchBendSensitivity = instrument['pitchBendSensitivity'];
/** @type {number} */
this.modEnvToPitch = instrument['modEnvToPitch'];
/** @type {number} */
this.reverb = instrument['reverb'];

// state
/** @type {number} */
Expand Down Expand Up @@ -129,6 +131,11 @@ SoundFont.SynthesizerNote.prototype.noteOn = function() {
var peekFreq;
/** @type {number} */
var sustainFreq;
/** @type {number} */
var reverb = instrument['reverb'];

var convolverGain;
var convolver;

sample = sample.subarray(0, sample.length + instrument['end']);
buffer = this.audioBuffer = ctx.createBuffer(1, sample.length, this.sampleRate);
Expand All @@ -143,6 +150,16 @@ SoundFont.SynthesizerNote.prototype.noteOn = function() {
bufferSource.loopEnd = loopEnd;
this.updatePitchBend(this.pitchBend);

// Create convolver for effect
convolver = ctx.createConvolver();
convolver.buffer = this.impulseResponse();

// Effect node
convolverGain = ctx.createGain();
convolverGain.gain.value = reverb / 127;
convolver.connect(convolverGain);
convolverGain.connect(this.destination);

// audio node
panner = this.panner = ctx.createPanner();
output = this.gainOutput = ctx.createGainNode();
Expand All @@ -164,8 +181,8 @@ SoundFont.SynthesizerNote.prototype.noteOn = function() {
// Attack, Decay, Sustain
//---------------------------------------------------------------------------
outputGain.setValueAtTime(0, now);
outputGain.linearRampToValueAtTime(this.volume * (this.velocity / 127), volAttack);
outputGain.linearRampToValueAtTime(this.volume * (1 - instrument['volSustain']), volDecay);
outputGain.linearRampToValueAtTime(this.volume * Math.pow(this.velocity/127, 2), volAttack); // 指数関数使わないとカーブが急すぎる

filter.Q.setValueAtTime(instrument['initialFilterQ'] * Math.pow(10, 200), now);
baseFreq = amountToFreq(instrument['initialFilterFc']);
Expand All @@ -174,7 +191,7 @@ SoundFont.SynthesizerNote.prototype.noteOn = function() {
filter.frequency.setValueAtTime(baseFreq, now);
filter.frequency.linearRampToValueAtTime(peekFreq, modAttack);
filter.frequency.linearRampToValueAtTime(sustainFreq, modDecay);

/**
* @param {number} val
* @returns {number}
Expand All @@ -187,6 +204,7 @@ SoundFont.SynthesizerNote.prototype.noteOn = function() {
bufferSource.connect(filter);
filter.connect(panner);
panner.connect(output);
// output.connect(convolver);
output.connect(this.destination);

// fire
Expand Down Expand Up @@ -285,4 +303,16 @@ SoundFont.SynthesizerNote.prototype.updatePitchBend = function(pitchBend) {
) * this.instrument['scaleTuning']
);
this.schedulePlaybackRate();
};
};

/**
* impulse responce
*/
SoundFont.SynthesizerNote.prototype.impulseResponse = function(){
var source = window.atob('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');
var array = new Uint8Array(new ArrayBuffer(source.length));
for(var i = 0; i < source.length; i++) {
array[i] = source.charCodeAt(i);
}
return array;
}
Loading