@logue/sf2synth
Version:
SoundFont2 Synthesizer
1,926 lines (1,768 loc) • 141 kB
JavaScript
/**
* @logue/sf2synth
*
* @description SoundFont2 Synthesizer
* @author iyama, Logue
* @license MIT
* @version 0.7.5
* @see {@link https://github.com/logue/sf2synth.js}
*/
// This file is auto-generated by the build system.
const meta = {
version: '0.7.5',
date: '2025-12-13T06:52:47.408Z',
};
/**
* Riff Parser class
*
* @author imaya
*/
class Riff {
/**
* @param {ArrayBuffer} input Input buffer.
* @param {Object} [optParams] Option parameters.
*/
constructor(input, optParams = {}) {
/** @type {ArrayBuffer} */
this.input = input;
/** @type {number} */
this.ip = optParams.index || 0;
/** @type {number} */
this.length = optParams.length || input.byteLength - this.ip;
/** @type {RiffChunk[]} */
this.chunkList = [];
/** @type {number} */
this.offset = this.ip;
/** @type {boolean} */
this.padding = optParams.padding !== undefined ? optParams.padding : true;
/** @type {boolean} */
this.bigEndian =
optParams.bigEndian !== undefined ? optParams.bigEndian : false;
}
/** @returns {void} */
parse() {
/** @type {number} */
const length = this.length + this.offset;
this.chunkList = [];
while (this.ip < length) {
this.parseChunk();
}
}
/** @returns {void} */
parseChunk() {
/** @type {ArrayBuffer} */
const input = this.input;
/** @type {number} */
let ip = this.ip;
/** @type {number} */
let size;
this.chunkList.push(
new RiffChunk(
String.fromCharCode(input[ip++], input[ip++], input[ip++], input[ip++]),
(size = this.bigEndian
? ((input[ip++] << 24) |
(input[ip++] << 16) |
(input[ip++] << 8) |
input[ip++]) >>>
0
: (input[ip++] |
(input[ip++] << 8) |
(input[ip++] << 16) |
(input[ip++] << 24)) >>>
0),
ip
)
);
ip += size;
// padding
if (this.padding && ((ip - this.offset) & 1) === 1) {
ip++;
}
this.ip = ip;
}
/**
* @param {number} index Chunk index.
* @returns {RiffChunk | null}
*/
getChunk(index) {
/** @type {RiffChunk} */
const chunk = this.chunkList[index];
return chunk !== undefined ? chunk : null;
}
/** @returns {number} */
getNumberOfChunks() {
return this.chunkList.length;
}
}
/**
* Riff Chunk Structure
*
* @interface
*/
class RiffChunk {
/**
* @param {string} type
* @param {number} size
* @param {number} offset
*/
constructor(type, size, offset) {
/** @type {string} */
this.type = type;
/** @type {number} */
this.size = size;
/** @type {number} */
this.offset = offset;
}
}
/**
* SoundFont Parser Class
*
* @author imaya
*/
class Parser {
/**
* @param {Uint8Array} input
* @param {Object} [optParams]
*/
constructor(input, optParams = {}) {
/** @type {Uint8Array} */
this.input = input;
/** @type {object} */
this.parserOption = optParams.parserOption || {};
/** @type {number} */
this.sampleRate = optParams.sampleRate || 22050; // よくわからんが、OSで指定されているサンプルレートを入れないと音が切れ切れになる。
/** @type {object[]} */
this.presetHeader = [];
/** @type {object[]} */
this.presetZone = [];
/** @type {object[]} */
this.presetZoneModulator = [];
/** @type {object[]} */
this.presetZoneGenerator = [];
/** @type {object[]} */
this.instrument = [];
/** @type {object[]} */
this.instrumentZone = [];
/** @type {object[]} */
this.instrumentZoneModulator = [];
/** @type {object[]} */
this.instrumentZoneGenerator = [];
/** @type {object[]} */
this.sampleHeader = [];
/** @type {string[]} */
this.GeneratorEnumeratorTable = Object.keys(Parser.getGeneratorTable());
}
/** @return {Record<string, number?>} ジェネレータとデフォルト値 */
static getGeneratorTable() {
return Object.freeze({
/** @type {number} サンプルヘッダの音声波形データ開始位置に加算されるオフセット(下位16bit) */
startAddrsOffset: 0,
/** @type {number} サンプルヘッダの音声波形データ終了位置に加算されるオフセット(下位16bit) */
endAddrsOffset: 0,
/** @type {number} サンプルヘッダの音声波形データループ開始位置に加算されるオフセット(下位16bit) */
startloopAddrsOffset: 0,
/** @type {number} サンプルヘッダの音声波形データループ開始位置に加算されるオフセット(下位16bit) */
endloopAddrsOffset: 0,
/** @type {number} サンプルヘッダの音声波形データ開始位置に加算されるオフセット(上位16bit) */
startAddrsCoarseOffset: 0,
/** @type {number} LFOによるピッチの揺れ幅 */
modLfoToPitch: 0,
/** @type {number} モジュレーションホイール用LFOからピッチに対しての影響量 */
vibLfoToPitch: 0,
/** @type {number} フィルタ・ピッチ用エンベロープからピッチに対しての影響量 */
modEnvToPitch: 0,
/** @type {number} フィルタのカットオフ周波数 */
initialFilterFc: 13500,
/** @type {number} フィルターのQ値(レゾナンス) */
initialFilterQ: 0,
/** @type {number} LFOによるフィルターカットオフ周波数の揺れ幅 */
modLfoToFilterFc: 0,
/** @type {number} フィルタ・ピッチ用エンベロープからフィルターカットオフに対しての影響量 */
modEnvToFilterFc: 0,
/** @type {number} サンプルヘッダの音声波形データ終了位置に加算されるオフセット(上位16bit) */
endAddrsCoarseOffset: 0,
/** @type {number} LFOによるボリュームの揺れ幅 */
modLfoToVolume: 0,
/** @type {undefined} 未使用1 */
unused1: undefined, // 14
/** @type {number} コーラスエフェクトのセンドレベル */
chorusEffectsSend: 0,
/** @type {number} リバーブエフェクトのセンドレベル */
reverbEffectsSend: 0,
/** @type {number} パンの位置 */
pan: 0,
/** @type {undefined} 未使用2 */
unused2: undefined,
/** @type {undefined} 未使用3 */
unused3: undefined,
/** @type {undefined} 未使用4 */
unused4: undefined,
/** @type {number} LFOの揺れが始まるまでの時間 */
delayModLFO: -12e3,
/** @type {number} LFOの揺れの周期 */
freqModLFO: 0,
/** @type {number} ホイールの揺れが始まるまでの時間 */
delayVibLFO: -12e3,
/** @type {number} ホイールの揺れの周期 */
freqVibLFO: 0,
/** @type {number} フィルタ・ピッチ用エンベロープのディレイ(アタックが始まるまでの時間) */
delayModEnv: -12e3,
/** @type {number} フィルタ・ピッチ用エンベロープのアタック時間 */
attackModEnv: -12e3,
/** @type {number} フィルタ・ピッチ用エンベロープのホールド時間(アタックが終わってからディケイが始まるまでの時間) */
holdModEnv: -12e3,
/** @type {number} フィルタ・ピッチ用エンベロープのディケイ時間 */
decayModEnv: -12e3,
/** @type {number} フィルタ・ピッチ用エンベロープのサステイン量 */
sustainModEnv: 0,
/** @type {number} フィルタ・ピッチ用エンベロープのリリース時間 */
releaseModEnv: -12e3,
/** @type {number} キー(ノートNo)によるフィルタ・ピッチ用エンベロープのホールド時間への影響 */
keynumToModEnvHold: 0,
/** @type {number} キー(ノートNo)によるフィルタ・ピッチ用エンベロープのディケイ時間への影響 */
keynumToModEnvDecay: 0,
/** @type {number} アンプ用エンベロープのディレイ(アタックが始まるまでの時間) */
delayVolEnv: -12e3,
/** @type {number} アンプ用エンベロープのアタック時間 */
attackVolEnv: -12e3,
/** @type {number} アンプ用エンベロープのホールド時間(アタックが終わってからディケイが始まるまでの時間) */
holdVolEnv: -12e3,
/** @type {number} アンプ用エンベロープのディケイ時間 */
decayVolEnv: -12e3,
/** @type {number} アンプ用エンベロープのサステイン量 */
sustainVolEnv: 0,
/** @type {number} アンプ用エンベロープのリリース時間 */
releaseVolEnv: -12e3,
/** @type {number} キー(ノートNo)によるアンプ用エンベロープのホールド時間への影響 */
keynumToVolEnvHold: 0,
/** @type {number} キー(ノートNo)によるアンプ用エンベロープのディケイ時間への影響 */
keynumToVolEnvDecay: 0,
/** @type {number} 割り当てるインストルメント(楽器) */
instrument: null,
/** @type {undefined} 予約済み1 */
reserved1: undefined, // 42
/** @type {number} マッピングするキー(ノートNo)の範囲 */
keyRange: null,
/** @type {number} マッピングするベロシティの範囲 */
velRange: null,
/** @type {number} サンプルヘッダの音声波形データループ開始位置に加算されるオフセット(上位16bit) */
startloopAddrsCoarseOffset: 0,
/** @type {number} どのキー(ノートNo)でも強制的に指定したキー(ノートNo)に変更する */
keynum: null,
/** @type {number} どのベロシティでも強制的に指定したベロシティに変更する */
velocity: null,
/** @type {number} 調整する音量 */
initialAttenuation: 0,
/** @type {undefined} 予約済み2 */
reserved2: undefined, // 49
/** @type {number} サンプルヘッダの音声波形データループ終了位置に加算されるオフセット(上位16bit) */
endloopAddrsCoarseOffset: 0,
/** @type {number} 半音単位での音程の調整 */
coarseTune: 0,
/** @type {number} cent単位での音程の調整 */
fineTune: 0,
/** @type {number} 割り当てるサンプル(音声波形) */
sampleID: null,
/** @type {number} サンプル(音声波形)をループさせるか等のフラグ */
sampleModes: 0,
/** @type {undefined} 予約済み3 */
reserved3: undefined, // 55
/** @type {number} キー(ノートNo)が+1されるごとに音程を何centあげるかの音階情報 */
scaleTuning: 100,
/** @type {number} 同時に音を鳴らさないようにするための排他ID(ハイハットのOpen、Close等に使用) */
exclusiveClass: null,
/** @type {number} サンプル(音声波形)の音程の上書き情報 */
overridingRootKey: null,
/** @type {undefined} 未使用5 */
unuded5: undefined, // 59
/** @type {undefined} 最後を示すオペレータ */
endOper: undefined,
});
}
/** @export */
parse() {
/** @type {Riff} */
const parser = new Riff(this.input, this.parserOption);
// parse RIFF chunk
parser.parse();
if (parser.chunkList.length !== 1) {
throw new Error('wrong chunk length');
}
/** @type {import('./riff.js').RiffChunk | null} */
const chunk = parser.getChunk(0);
if (chunk === null) {
throw new Error('chunk not found');
}
this.parseRiffChunk(chunk);
this.input = null;
}
/** @param {import('./riff.js').RiffChunk} chunk */
parseRiffChunk(chunk) {
/** @type {ArrayBuffer} */
const data = this.input;
/** @type {number} */
let ip = chunk.offset;
// check parse target
if (chunk.type !== 'RIFF') {
throw new Error('invalid chunk type:' + chunk.type);
}
// check signature
/** @type {string} */
const signature = String.fromCharCode(
data[ip++],
data[ip++],
data[ip++],
data[ip++]
);
if (signature !== 'sfbk') {
throw new Error('invalid signature:' + signature);
}
// read structure
/** @type {import('./riff.js').Riff} */
const parser = new Riff(data, { index: ip, length: chunk.size - 4 });
parser.parse();
if (parser.getNumberOfChunks() !== 3) {
throw new Error('invalid sfbk structure');
}
// INFO-list
this.parseInfoList(
/** @type {import('./riff.js').RiffChunk} */ (parser.getChunk(0))
);
// sdta-list
this.parseSdtaList(
/** @type {import('./riff.js').RiffChunk} */ (parser.getChunk(1))
);
// pdta-list
this.parsePdtaList(
/** @type {import('./riff.js').RiffChunk} */ (parser.getChunk(2))
);
}
/** @param {import('./riff.js').RiffChunk} chunk */
parseInfoList(chunk) {
/** @type {ArrayBuffer} */
const data = this.input;
/** @type {number} */
let ip = chunk.offset;
// check parse target
if (chunk.type !== 'LIST') {
throw new Error('invalid chunk type:' + chunk.type);
}
// check signature
/** @type {string} */
const signature = String.fromCharCode(
data[ip++],
data[ip++],
data[ip++],
data[ip++]
);
if (signature !== 'INFO') {
throw new Error('invalid signature:' + signature);
}
// read structure
/** @type {import('./riff.js').Riff} */
const parser = new Riff(data, { index: ip, length: chunk.size - 4 });
parser.parse();
}
/** @param {import('./riff.js').RiffChunk} chunk */
parseSdtaList(chunk) {
/** @type {ArrayBuffer} */
const data = this.input;
/** @type {number} */
let ip = chunk.offset;
// check parse target
if (chunk.type !== 'LIST') {
throw new Error('invalid chunk type:' + chunk.type);
}
// check signature
/** @type {string} */
const signature = String.fromCharCode(
data[ip++],
data[ip++],
data[ip++],
data[ip++]
);
if (signature !== 'sdta') {
throw new Error('invalid signature:' + signature);
}
// read structure
/** @type {import('./riff.js').Riff} */
const parser = new Riff(data, { index: ip, length: chunk.size - 4 });
parser.parse();
if (parser.chunkList.length !== 1) {
throw new Error('TODO');
}
this.samplingData =
/** @type {{ type: string; size: number; offset: number }} */
(parser.getChunk(0));
}
/** @param {import('./riff.js').RiffChunk} chunk */
parsePdtaList(chunk) {
/** @type {Uint8Array} */
const data = this.input;
/** @type {number} */
let ip = chunk.offset;
// check parse target
if (chunk.type !== 'LIST') {
throw new Error('invalid chunk type:' + chunk.type);
}
// check signature
/** @type {string} */
const signature = String.fromCharCode(
data[ip++],
data[ip++],
data[ip++],
data[ip++]
);
if (signature !== 'pdta') {
throw new Error('invalid signature:' + signature);
}
// read structure
/** @type {import('./riff.js').Riff} */
const parser = new Riff(data, { index: ip, length: chunk.size - 4 });
parser.parse();
// check number of chunks
if (parser.getNumberOfChunks() !== 9) {
throw new Error('invalid pdta chunk');
}
this.parsePhdr(
/** @type {import('./riff.js').RiffChunk} */ (parser.getChunk(0))
);
this.parsePbag(
/** @type {import('./riff.js').RiffChunk} */ (parser.getChunk(1))
);
this.parsePmod(
/** @type {import('./riff.js').RiffChunk} */ (parser.getChunk(2))
);
this.parsePgen(
/** @type {import('./riff.js').RiffChunk} */ (parser.getChunk(3))
);
this.parseInst(
/** @type {import('./riff.js').RiffChunk} */ (parser.getChunk(4))
);
this.parseIbag(
/** @type {import('./riff.js').RiffChunk} */ (parser.getChunk(5))
);
this.parseImod(
/** @type {import('./riff.js').RiffChunk} */ (parser.getChunk(6))
);
this.parseIgen(
/** @type {import('./riff.js').RiffChunk} */ (parser.getChunk(7))
);
this.parseShdr(
/** @type {import('./riff.js').RiffChunk} */ (parser.getChunk(8))
);
}
/** @param {import('./riff.js').RiffChunk} chunk */
parsePhdr(chunk) {
/** @type {Uint8Array} */
const data = this.input;
/** @type {number} */
let ip = chunk.offset;
/** @type {Object[]} */
const presetHeader = (this.presetHeader = []);
/** @type {number} */
const size = chunk.offset + chunk.size;
// check parse target
if (chunk.type !== 'phdr') {
throw new Error('invalid chunk type:' + chunk.type);
}
while (ip < size) {
presetHeader.push({
presetName: String.fromCharCode.apply(
null,
data.subarray(ip, (ip += 20))
),
preset: data[ip++] | (data[ip++] << 8),
bank: data[ip++] | (data[ip++] << 8),
presetBagIndex: data[ip++] | (data[ip++] << 8),
library:
(data[ip++] |
(data[ip++] << 8) |
(data[ip++] << 16) |
(data[ip++] << 24)) >>>
0,
genre:
(data[ip++] |
(data[ip++] << 8) |
(data[ip++] << 16) |
(data[ip++] << 24)) >>>
0,
morphology:
(data[ip++] |
(data[ip++] << 8) |
(data[ip++] << 16) |
(data[ip++] << 24)) >>>
0,
});
}
}
/** @param {import('./riff.js').RiffChunk} chunk */
parsePbag(chunk) {
/** @type {ArrayBuffer} */
const data = this.input;
/** @type {number} */
let ip = chunk.offset;
/** @type {Object[]} */
const presetZone = (this.presetZone = []);
/** @type {number} */
const size = chunk.offset + chunk.size;
// check parse target
if (chunk.type !== 'pbag') {
throw new Error('invalid chunk type:' + chunk.type);
}
while (ip < size) {
presetZone.push({
presetGeneratorIndex: data[ip++] | (data[ip++] << 8),
presetModulatorIndex: data[ip++] | (data[ip++] << 8),
});
}
}
/** @param {import('./riff.js').RiffChunk} chunk */
parsePmod(chunk) {
// check parse target
if (chunk.type !== 'pmod') {
throw new Error('invalid chunk type:' + chunk.type);
}
this.presetZoneModulator = this.parseModulator(chunk);
}
/** @param {import('./riff.js').RiffChunk} chunk */
parsePgen(chunk) {
// check parse target
if (chunk.type !== 'pgen') {
throw new Error('invalid chunk type:' + chunk.type);
}
this.presetZoneGenerator = this.parseGenerator(chunk);
}
/** @param {import('./riff.js').RiffChunk} chunk */
parseInst(chunk) {
/** @type {Uint8Array} */
const data = this.input;
/** @type {number} */
let ip = chunk.offset;
/** @type {Object[]} */
const instrument = (this.instrument = []);
/** @type {number} */
const size = chunk.offset + chunk.size;
// check parse target
if (chunk.type !== 'inst') {
throw new Error('invalid chunk type:' + chunk.type);
}
while (ip < size) {
instrument.push({
instrumentName: String.fromCharCode.apply(
null,
data.subarray(ip, (ip += 20))
),
instrumentBagIndex: data[ip++] | (data[ip++] << 8),
});
}
}
/** @param {import('./riff.js').RiffChunk} chunk */
parseIbag(chunk) {
/** @type {ArrayBuffer} */
const data = this.input;
/** @type {number} */
let ip = chunk.offset;
/** @type {Object[]} */
const instrumentZone = (this.instrumentZone = []);
/** @type {number} */
const size = chunk.offset + chunk.size;
// check parse target
if (chunk.type !== 'ibag') {
throw new Error('invalid chunk type:' + chunk.type);
}
while (ip < size) {
instrumentZone.push({
instrumentGeneratorIndex: data[ip++] | (data[ip++] << 8),
instrumentModulatorIndex: data[ip++] | (data[ip++] << 8),
});
}
}
/** @param {import('./riff.js').RiffChunk} chunk */
parseImod(chunk) {
// check parse target
if (chunk.type !== 'imod') {
throw new Error('invalid chunk type:' + chunk.type);
}
this.instrumentZoneModulator = this.parseModulator(chunk);
}
/** @param {import('./riff.js').RiffChunk} chunk */
parseIgen(chunk) {
// check parse target
if (chunk.type !== 'igen') {
throw new Error('invalid chunk type:' + chunk.type);
}
this.instrumentZoneGenerator = this.parseGenerator(chunk);
}
/** @param {import('./riff.js').RiffChunk} chunk */
parseShdr(chunk) {
/** @type {Uint8Array} */
const data = this.input;
/** @type {number} */
let ip = chunk.offset;
/** @type {Object[]} */
const samples = (this.sample = []);
/** @type {Object[]} */
const sampleHeader = (this.sampleHeader = []);
/** @type {number} */
const size = chunk.offset + chunk.size;
/** @type {string} */
let sampleName;
/** @type {number} */
let start;
/** @type {number} */
let end;
/** @type {number} */
let startLoop;
/** @type {number} */
let endLoop;
/** @type {number} */
let sampleRate;
/** @type {number} */
let originalPitch;
/** @type {number} */
let pitchCorrection;
/** @type {number} */
let sampleLink;
/** @type {number} */
let sampleType;
// check parse target
if (chunk.type !== 'shdr') {
throw new Error('invalid chunk type:' + chunk.type);
}
while (ip < size) {
sampleName = String.fromCharCode.apply(
null,
data.subarray(ip, (ip += 20))
);
start =
((data[ip++] << 0) |
(data[ip++] << 8) |
(data[ip++] << 16) |
(data[ip++] << 24)) >>>
0;
end =
((data[ip++] << 0) |
(data[ip++] << 8) |
(data[ip++] << 16) |
(data[ip++] << 24)) >>>
0;
startLoop =
((data[ip++] << 0) |
(data[ip++] << 8) |
(data[ip++] << 16) |
(data[ip++] << 24)) >>>
0;
endLoop =
((data[ip++] << 0) |
(data[ip++] << 8) |
(data[ip++] << 16) |
(data[ip++] << 24)) >>>
0;
sampleRate =
((data[ip++] << 0) |
(data[ip++] << 8) |
(data[ip++] << 16) |
(data[ip++] << 24)) >>>
0;
originalPitch = data[ip++];
pitchCorrection = (data[ip++] << 24) >> 24;
sampleLink = data[ip++] | (data[ip++] << 8);
sampleType = data[ip++] | (data[ip++] << 8);
let sample = new Int16Array(
new Uint8Array(
data.subarray(
this.samplingData.offset + start * 2,
this.samplingData.offset + end * 2
)
).buffer
);
startLoop -= start;
endLoop -= start;
if (sampleRate > 0) {
const adjust = this.adjustSampleData(sample, sampleRate);
sample = adjust.sample;
sampleRate *= adjust.multiply;
startLoop *= adjust.multiply;
endLoop *= adjust.multiply;
}
samples.push(sample);
sampleHeader.push({
sampleName,
start,
end,
startLoop,
endLoop,
sampleRate,
originalPitch,
pitchCorrection,
sampleLink,
sampleType,
});
}
}
/**
* @param {Int16Array} sample
* @param {number} sampleRate
* @return {object}
*/
adjustSampleData(sample, sampleRate) {
/** @type {Int16Array} */
let newSample;
/** @type {number} */
let i;
/** @type {number} */
let il;
/** @type {number} */
let j;
/** @type {number} */
let multiply = 1;
// buffer
while (sampleRate < this.sampleRate) {
// AudioContextのサンプルレートに変更
newSample = new Int16Array(sample.length * 2);
for (i = j = 0, il = sample.length; i < il; ++i) {
newSample[j++] = sample[i];
newSample[j++] = sample[i];
}
sample = newSample;
multiply *= 2;
sampleRate *= 2;
}
return {
sample,
multiply,
};
}
/**
* @param {import('./riff.js').RiffChunk} chunk
* @return {Object[]}
*/
parseModulator(chunk) {
/** @type {ArrayBuffer} */
const data = this.input;
/** @type {number} */
let ip = chunk.offset;
/** @type {number} */
const size = chunk.offset + chunk.size;
/** @type {number} */
let code;
/** @type {string} */
let key;
/** @type {Object[]} */
const output = [];
while (ip < size) {
// Src Oper
// TODO
ip += 2;
// Dest Oper
code = data[ip++] | (data[ip++] << 8);
key = this.GeneratorEnumeratorTable[code];
if (!key) {
// Amount
output.push({
type: key,
value: {
code,
amount: data[ip] | (((data[ip + 1] << 8) << 16) >> 16),
lo: data[ip++],
hi: data[ip++],
},
});
} else {
// Amount
switch (key) {
case 'keyRange':
/* FALLTHROUGH */
case 'velRange':
/* FALLTHROUGH */
case 'keynum':
/* FALLTHROUGH */
case 'velocity':
output.push({
type: key,
value: {
amount: null,
lo: data[ip++],
hi: data[ip++],
},
});
break;
default:
output.push({
type: key,
value: {
amount: data[ip++] | (((data[ip++] << 8) << 16) >> 16),
},
});
break;
}
}
// AmtSrcOper
// TODO
ip += 2;
// Trans Oper
// TODO
ip += 2;
}
return output;
}
/**
* @param {import('./riff.js').RiffChunk} chunk
* @return {Object[]}
*/
parseGenerator(chunk) {
/** @type {ArrayBuffer} */
const data = this.input;
/** @type {number} */
let ip = chunk.offset;
/** @type {number} */
const size = chunk.offset + chunk.size;
/** @type {number} */
let code;
/** @type {string} */
let key;
/** @type {Object[]} */
const output = [];
while (ip < size) {
code = data[ip++] | (data[ip++] << 8);
key = this.GeneratorEnumeratorTable[code];
if (!key) {
output.push({
type: key,
value: {
code,
amount: data[ip] | (((data[ip + 1] << 8) << 16) >> 16),
lo: data[ip++],
hi: data[ip++],
},
});
continue;
}
switch (key) {
case 'keynum':
/* FALLTHROUGH */
case 'keyRange':
/* FALLTHROUGH */
case 'velRange':
/* FALLTHROUGH */
case 'velocity':
output.push({
type: key,
value: {
amount: null,
lo: data[ip++],
hi: data[ip++],
},
});
break;
default:
output.push({
type: key,
value: {
amount: data[ip++] | (((data[ip++] << 8) << 16) >> 16),
},
});
break;
}
}
return output;
}
/** @return {object[]} */
createInstrument() {
/** @type {Object[]} */
const instrument = this.instrument;
/** @type {Object[]} */
const zone = this.instrumentZone;
/** @type {Object[]} */
const output = [];
/** @type {number} */
let bagIndex;
/** @type {number} */
let bagIndexEnd;
/** @type {Object[]} */
let zoneInfo;
/** @type {{ generator: Object; generatorInfo: Object[] }} */
let instrumentGenerator;
/** @type {{ modulator: Object; modulatorInfo: Object[] }} */
let instrumentModulator;
/** @type {number} */
let i;
/** @type {number} */
let il;
/** @type {number} */
let j;
/** @type {number} */
let jl;
// instrument -> instrument bag -> generator / modulator
for (i = 0, il = instrument.length; i < il; ++i) {
bagIndex = instrument[i].instrumentBagIndex;
bagIndexEnd = instrument[i + 1]
? instrument[i + 1].instrumentBagIndex
: zone.length;
zoneInfo = [];
// instrument bag
for (j = bagIndex, jl = bagIndexEnd; j < jl; ++j) {
instrumentGenerator = this.createInstrumentGenerator_(zone, j);
instrumentModulator = this.createInstrumentModulator_(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;
}
/** @return {object[]} */
createPreset() {
/** @type {Object[]} */
const preset = this.presetHeader;
/** @type {Object[]} */
const zone = this.presetZone;
/** @type {Object[]} */
const output = [];
/** @type {number} */
let bagIndex;
/** @type {number} */
let bagIndexEnd;
/** @type {Object[]} */
let zoneInfo;
/** @type {number} */
let instrument;
/** @type {{ generator: Object; generatorInfo: Object[] }} */
let presetGenerator;
/** @type {{ modulator: Object; modulatorInfo: Object[] }} */
let presetModulator;
/** @type {number} */
let i;
/** @type {number} */
let il;
/** @type {number} */
let j;
/** @type {number} */
let jl;
// preset -> preset bag -> generator / modulator
for (i = 0, il = preset.length; i < il; ++i) {
bagIndex = preset[i].presetBagIndex;
bagIndexEnd = preset[i + 1] ? preset[i + 1].presetBagIndex : zone.length;
zoneInfo = [];
// preset bag
for (j = bagIndex, jl = bagIndexEnd; j < jl; ++j) {
presetGenerator = this.createPresetGenerator_(zone, j);
presetModulator = this.createPresetModulator_(zone, j);
zoneInfo.push({
generator: presetGenerator.generator,
generatorSequence: presetGenerator.generatorInfo,
modulator: presetModulator.modulator,
modulatorSequence: presetModulator.modulatorInfo,
});
instrument =
presetGenerator.generator.instrument !== undefined
? presetGenerator.generator.instrument.amount
: presetModulator.modulator.instrument !== undefined
? presetModulator.modulator.instrument.amount
: null;
}
output.push({
name: preset[i].presetName,
info: zoneInfo,
header: preset[i],
instrument,
});
}
return output;
}
/**
*
* @private
* @param {Object[]} zone
* @param {number} index
* @returns {{ generator: Object; generatorInfo: Object[] }}
*/
createInstrumentGenerator_(zone, index) {
const modgen = this.createBagModGen_(
zone,
zone[index].instrumentGeneratorIndex,
zone[index + 1]
? zone[index + 1].instrumentGeneratorIndex
: this.instrumentZoneGenerator.length,
this.instrumentZoneGenerator
);
return {
generator: modgen.modgen,
generatorInfo: modgen.modgenInfo,
};
}
/**
*
* @private
* @param {Object[]} zone
* @param {number} index
* @returns {{ modulator: Object; modulatorInfo: Object[] }}
*/
createInstrumentModulator_(zone, index) {
const modgen = this.createBagModGen_(
zone,
zone[index].presetModulatorIndex,
zone[index + 1]
? zone[index + 1].instrumentModulatorIndex
: this.instrumentZoneModulator.length,
this.instrumentZoneModulator
);
return {
modulator: modgen.modgen,
modulatorInfo: modgen.modgenInfo,
};
}
/**
*
* @private
* @param {Object[]} zone
* @param {number} index
* @returns {{ generator: Object; generatorInfo: Object[] }}
*/
createPresetGenerator_(zone, index) {
const modgen = this.createBagModGen_(
zone,
zone[index].presetGeneratorIndex,
zone[index + 1]
? zone[index + 1].presetGeneratorIndex
: this.presetZoneGenerator.length,
this.presetZoneGenerator
);
return {
generator: modgen.modgen,
generatorInfo: modgen.modgenInfo,
};
}
/**
*
* @private
* @param {Object[]} zone
* @param {number} index
* @returns {{ modulator: Object; modulatorInfo: Object[] }}
*/
createPresetModulator_(zone, index) {
/** @type {{ modgen: Object; modgenInfo: Object[] }} */
const modgen = this.createBagModGen_(
zone,
zone[index].presetModulatorIndex,
zone[index + 1]
? zone[index + 1].presetModulatorIndex
: this.presetZoneModulator.length,
this.presetZoneModulator
);
return {
modulator: modgen.modgen,
modulatorInfo: modgen.modgenInfo,
};
}
/**
*
* @private
* @param {Object[]} _zone
* @param {number} indexStart
* @param {number} indexEnd
* @param {Array} zoneModGen
* @returns {{ modgen: Object; modgenInfo: Object[] }}
*/
createBagModGen_(_zone, indexStart, indexEnd, zoneModGen) {
/** @type {Object[]} */
const modgenInfo = [];
/** @type {Object} */
const modgen = {
unknown: [],
keyRange: {
amount: null,
hi: 127,
lo: 0,
},
}; // TODO
/** @type {Object} */
let info;
/** @type {number} */
let i;
/** @type {number} */
let il;
for (i = indexStart, il = indexEnd; i < il; ++i) {
info = zoneModGen[i];
modgenInfo.push(info);
if (info.type === 'unknown') {
modgen.unknown.push(info.value);
} else {
modgen[info.type] = info.value;
}
}
return {
modgen,
modgenInfo,
};
}
}
/**
* @classdesc File Loader Class
* @private
* @author Logue <logue@hotmail.co.jp>
*/
class Loader {
/** キャッシュの名前空間 */
static CACHE_NAME = 'wml';
/**
* コンストラクタ
*
* @constructor
* @param {string} url
* @param {HTMLDivElement} placeholder
* @param {boolean} cache
* @param {Function} callback
*/
constructor(url, placeholder, cache, callback) {
this.url = url;
this.cache = cache;
this.callback = callback;
/** @type {HTMLDivElement} */
this.alert = document.createElement('div');
this.alert.className = 'alert alert-warning';
/** @type {HTMLParagraphElement} */
this.message = document.createElement('p');
this.message.innerText = 'Now Loading...';
/** @type {HTMLDivElement} */
this.progressOuter = document.createElement('div');
this.progressOuter.className = 'progress';
this.progressOuter.role = 'progressbar';
this.progressOuter.ariaLabel = 'Loading Progress';
this.progressOuter.ariaValueMin = '0';
this.progressOuter.ariaValueNow = '0';
this.progressOuter.ariaValueMax = '100';
/** @type {HTMLDivElement} */
this.progress = document.createElement('div');
this.progress.className = 'progress-bar';
this.progressOuter.appendChild(this.progress);
this.alert.appendChild(this.message);
this.alert.appendChild(this.progressOuter);
placeholder.appendChild(this.alert);
}
/**
* ダウンロード中のハンドラ
* @param {number} current
* @param {number} total
* @private
*/
onProgress(current, total) {
const percentCompleted = Math.floor((current / total) * 100);
this.progress.style.width = `${percentCompleted}%`;
this.progress.innerText = `${percentCompleted}%`;
}
/**
* ロード完了時のハンドラ
*
* @param {Uint8Array} buffer
* @private
*/
onComplete(buffer) {
this.alert.className = 'alert alert-info';
this.message.innerText = 'Initializing...';
this.progress.className =
'progress-bar progress-bar-striped progress-bar-animated';
this.progress.style.width = '100%';
// コールバック実行
this.callback(new Uint8Array(buffer));
}
/**
* エラー時のハンドラ
*
* @param {Error | undefined} error エラー内容
* @private
*/
onError(error = undefined) {
requestAnimationFrame(() => {
this.alert.className = 'alert alert-danger';
this.message.innerText =
'An error occurred while loading SoundFont. See the console log for details. In addition, it may be cured by deleting the cache of the browser.';
this.progressOuter.style.display = 'none';
});
}
/**
* データ取得
* @public
*/
async fetch() {
/** @type {Cache} */
const cache = await window.caches.open(Loader.CACHE_NAME);
/** @type {Response} */
const cached = await cache.match(this.url);
if (this.cache && cached) {
// キャッシュが存在する場合、キャッシュの値を返す
this.onComplete(new Uint8Array(await cached.arrayBuffer()));
return;
}
/** @type {void | Response} キャッシュがない場合Fetchで取得 */
const response = await fetch(this.url, {
method: 'GET',
}).catch(e => this.onError(e));
if (!response || (response && !response.ok)) {
return;
}
/** @type {Response} キャッシュ用レスポンス */
const cloned = response.clone();
/** @type {number} ファイルの容量 */
const contentLength = parseInt(response.headers.get('Content-Length'));
/** @type {ReadableStreamDefaultReader<Uint8Array>} ファイルリーダー */
const reader = cloned.body.getReader();
/** @type {number} 読み込まれたチャンクの長さ */
let receivedLength = 0;
/** @type {Uint8Array[]} 受信したバイナリチャンクの配列(本文を構成します) */
const chunks = [];
while (true) {
// 最後のチャンクも場合、done は true。
// value はチャンクバイトの Uint8Array
const { done, value } = await reader.read();
if (done) {
break;
}
chunks.push(value);
receivedLength += value.length;
this.message.innerText = `Now Loading... (${receivedLength} of ${contentLength} byte)`;
// Content lengthヘッダーが出力されている場合プログレスバーを表示
this.onProgress(receivedLength, contentLength);
}
/** @type {Uint8Array} 全チャンク */
const chunksAll = new Uint8Array(receivedLength);
/** @type {number} 現在の読み込んだチャンク位置 */
let position = 0;
for (const chunk of chunks) {
chunksAll.set(chunk, position);
position += chunk.length;
}
// キャッシュへ保存
await cache.put(this.url, response);
// 完了時のイベントを実行
this.onComplete(chunksAll);
}
}
const INV_MAX = 1 / 2 ** 32;
class ARandom {
float(norm = 1) {
return this.int() * INV_MAX * norm;
}
probability(p) {
return this.float() < p;
}
norm(norm = 1) {
return (this.int() * INV_MAX - 0.5) * 2 * norm;
}
normMinMax(min, max) {
const x = this.minmax(min, max);
return this.float() < 0.5 ? x : -x;
}
minmax(min, max) {
return this.float() * (max - min) + min;
}
minmaxInt(min, max) {
min |= 0;
const range = (max | 0) - min;
return range ? min + this.int() % range : min;
}
minmaxUint(min, max) {
min >>>= 0;
const range = (max >>> 0) - min;
return range ? min + this.int() % range : min;
}
}
class WrappedRandom extends ARandom {
constructor(rnd) {
super();
this.rnd = rnd;
}
float(norm = 1) {
return this.rnd() * norm;
}
norm(norm = 1) {
return (this.rnd() - 0.5) * 2 * norm;
}
int() {
return this.rnd() * 4294967296 >>> 0;
}
}
const SYSTEM = new WrappedRandom(Math.random);
const DEFAULT_OPTS = {
bins: 2,
scale: 1,
rnd: SYSTEM
};
const preseed = (n, scale, rnd) => {
const state = new Array(n);
for (let i = 0; i < n; i++) {
state[i] = rnd.norm(scale);
}
return state;
};
const sum = (src) => src.reduce((sum2, x) => sum2 + x, 0);
function* interleave(a, b) {
const src = [a[Symbol.iterator](), b[Symbol.iterator]()];
for (let i = 0; true; i ^= 1) {
const next = src[i].next();
if (next.done) return;
yield next.value;
}
}
function* blue(opts) {
const { bins, scale, rnd } = { ...DEFAULT_OPTS, ...opts };
const state = preseed(bins, scale, rnd);
state.forEach((x, i) => state[i] = i & 1 ? x : -x);
const invN = 1 / bins;
let acc = sum(state);
for (let i = 0, sign = -1; true; ++i >= bins && (i = 0)) {
acc -= state[i];
acc += state[i] = sign * rnd.norm(scale);
sign ^= 4294967294;
yield sign * acc * invN;
}
}
const green = (opts) => interleave(blue(opts), blue(opts));
const ctz32 = (x) => {
let c = 32;
x &= -x;
x && c--;
x & 65535 && (c -= 16);
x & 16711935 && (c -= 8);
x & 252645135 && (c -= 4);
x & 858993459 && (c -= 2);
x & 1431655765 && (c -= 1);
return c;
};
function* pink(opts) {
const { bins = 8, scale, rnd } = { ...DEFAULT_OPTS, ...opts };
const state = preseed(bins, scale, rnd);
const invN = 1 / bins;
let acc = sum(state);
for (let i = 0; true; i = i + 1 >>> 0) {
const id = ctz32(i) % bins;
acc -= state[id];
acc += state[id] = rnd.norm(scale);
yield acc * invN;
}
}
function* red(opts) {
const { bins, scale, rnd } = { ...DEFAULT_OPTS, ...opts };
const state = preseed(bins, scale, rnd);
const invN = 1 / bins;
let acc = sum(state);
for (let i = 0; true; ++i >= bins && (i = 0)) {
acc -= state[i];
acc += state[i] = rnd.norm(scale);
yield acc * invN;
}
}
const violet = (opts) => interleave(red(opts), red(opts));
function* white(opts) {
const { scale, rnd } = { ...DEFAULT_OPTS, ...opts };
while (true) {
yield rnd.norm(scale);
}
}
const implementsFunction = (x, fn) => typeof x?.[fn] === "function";
const ensureTransducer = (x) => implementsFunction(x, "xform") ? x.xform() : x;
const isIterable = (x) => typeof x?.[Symbol.iterator] === "function";
const identity = (x) => x;
class Reduced {
value;
constructor(val) {
this.value = val;
}
deref() {
return this.value;
}
}
const reduced = (x) => new Reduced(x);
const isReduced = (x) => x instanceof Reduced;
const ensureReduced = (x) => x instanceof Reduced ? x : new Reduced(x);
const unreduced = (x) => x instanceof Reduced ? x.deref() : x;
const reducer = (init, rfn) => [init, identity, rfn];
function push(src) {
return src ? [...src] : reducer(
() => [],
(acc, x) => (acc.push(x), acc)
);
}
function* iterator(xform, src) {
const rfn = ensureTransducer(xform)(push());
const complete = rfn[1];
const reduce = rfn[2];
for (let x of src) {
const y = reduce([], x);
if (isReduced(y)) {
yield* unreduced(complete(y.deref()));
return;
}
if (y.length) {
yield* y;
}
}
yield* unreduced(complete([]));
}
const compR = (rfn, fn) => [rfn[0], rfn[1], fn];
function take(n, src) {
return isIterable(src) ? iterator(take(n), src) : (rfn) => {
const r = rfn[2];
let m = n;
return compR(
rfn,
(acc, x) => --m > 0 ? r(acc, x) : m === 0 ? ensureReduced(r(acc, x)) : reduced(acc)
);
};
}
/**
* @logue/reverb
*
* @description JavaScript Reverb effect class
* @author Logue <logue@hotmail.co.jp>
* @copyright 2019-2025 By Masashi Yoshikawa All rights reserved.
* @license MIT
* @version 1.4.1
* @see {@link https://github.com/logue/Reverb.js}
*/
const c = {
version: "1.4.1",
date: "2025-07-12T06:07:47.985Z"
}, y = {
noise: "white",
scale: 1,
peaks: 2,
randomAlgorithm: SYSTEM,
decay: 2,
delay: 0,
reverse: false,
time: 2,
filterType: "allpass",
filterFreq: 2200,
filterQ: 1,
mix: 0.5,
once: false
};
class s {
/** Version strings */
static version = c.version;
/** Build date */
static build = c.date;
/** AudioContext */
ctx;
/** Wet Level (Reverberated node) */
wetGainNode;
/** Dry Level (Original sound node) */
dryGainNode;
/** Impulse response filter */
filterNode;
/** Convolution node for applying impulse response */
convolverNode;
/** Output gain node */
outputNode;
/** Option */
options;
/** Connected flag */
isConnected;
/** Noise Generator */
noise = white;
/**
* Map of noise types to their respective generator functions.
*/
noiseMap = {
blue: blue,
green: green,
pink: pink,
red: red,
brown: red,
// brown is an alias for red
violet: violet,
white: white
};
/**
* Constructor
*
* @param ctx - Root AudioContext
* @param options - Configure
*/
constructor(e, t) {
this.ctx = e, this.options = Object.assign(y, t), this.wetGainNode = this.ctx.createGain(), this.dryGainNode = this.ctx.createGain(), this.filterNode = this.ctx.createBiquadFilter(), this.convolverNode = this.ctx.createConvolver(), this.outputNode = this.ctx.createGain(), this.isConnected = false, this.filterType(this.options.filterType), this.setNoise(this.options.noise), this.buildImpulse(), this.mix(this.options.mix);
}
/**
* Connect the node for the reverb effect to the original sound node.
*
* @param sourceNode - Input source node
*/
connect(e) {
return this.isConnected && this.options.once ? (this.isConnected = false, this.outputNode) : (this.convolverNode.connect(this.filterNode), this.filterNode.connect(this.wetGainNode), e.connect(this.convolverNode), e.connect(this.dryGainNode), e.connect(this.wetGainNode), this.dryGainNode.connect(this.outputNode), this.wetGainNode.connect(this.outputNode), this.isConnected = true, this.outputNode);
}
/**
* Disconnect the reverb node
*
* @param sourceNode - Input source node
*/
disconnect(e) {
return this.isConnected && (this.convolverNode.disconnect(this.filterNode), this.filterNode.disconnect(this.wetGainNode)), this.isConnected = false, e;
}
/**
* Dry/Wet ratio
*
* @param mix - Ratio (0~1)
*/
mix(e) {
if (!s.inRange(e, 0, 1))
throw new RangeError("[Reverb.js] Dry/Wet ratio must be between 0 to 1.");
this.options.mix = e, this.dryGainNode.gain.value = 1 - e, this.wetGainNode.gain.value = e;
}
/**
* Set Impulse Response time length (second)
*
* @param value - IR length
*/
time(e) {
if (!s.inRange(e, 1, 50))
throw new RangeError(
"[Reverb.js] Time length of impulse response must be less than 50sec."
);
this.options.time = e, this.buildImpulse();
}
/**
* Impulse response decay rate.
*
* @param value - Decay value
*/
decay(e) {
if (!s.inRange(e, 0, 100))
throw new RangeError(
"[Reverb.js] Impulse Response decay level must be less than 100."
);
this.options.decay = e, this.buildImpulse();
}
/**
* Delay before reverberation starts
*
* @param value - Time[ms]
*/
delay(e) {
if (!s.inRange(e, 0, 100))
throw new RangeError(
"[Reverb.js] Impulse Response delay time must be less than 100."
);
this.options.delay = e, this.buildImpulse();
}
/**
* Reverse the impulse response.
*
* @param reverse - Reverse IR
*/
reverse(e) {
this.options.reverse = e, this.buildImpulse();
}
/**
* Filter for impulse response
*
* @param type - Filiter Type
*/
filterType(e = "allpass") {
this.filterNode.type = this.options.filterType = e;
}
/**
* Filter frequency applied to impulse response
*
* @param freq - Frequency
*/
filterFreq(e) {
if (!s.inRange(e, 20, 2e4))
throw new RangeError(
"[Reverb.js] Filter frequrncy must be between 20 and 20000."
);
this.options.filterFreq = e, this.filterNode.frequency.value = this.options.filterFreq;
}
/**
* Filter quality.
*
* @param q - Quality
*/
filterQ(e) {
if (!s.inRange(e, 0, 10))
throw new RangeError(
"[Reverb.js] Filter Q value must be between 0 and 10."
);
this.options.filterQ = e, this.filterNode.Q.value = this.options.filterQ;
}
/**
* set IR source noise peaks
*
* @param p - Peaks
*/
peaks(e) {
this.options.peaks = e, this.buildImpulse();
}
/**
* set IR source noise scale.
*
* @param s - Scale
*/
scale(e) {
this.options.scale = e, this.buildImpulse();
}
/**
* Noise source
*
* @param duration - length of IR.
*/
getNoise(e) {
return [
...take(
e,
this.noise({
bins: this.options.peaks,
scale: this.options.scale,
rnd: this.options.randomAlgorithm
})
)
];
}
/**
* Inpulse Response Noise algorithm.
*
* @param type - IR noise algorithm type.
*/
setNoise(e) {
this.options.noise = e, this.noise = this.noiseMap[e] || white, this.buildImpulse();
}
/**
* Set Random Algorythm
*
* @param algorithm - Algorythm
*/
setRandomAlgorithm(e) {
this.options.randomAlgorithm = e, this.buildImpulse();
}
/**
* Return true if in range, otherwise false
*
* @param x - Target value
* @param min - Minimum value
* @param max - Maximum value
*/
static inRange(e, t, o) {
return e >= t && e <= o;
}
/** Utility function for building an impulse response from the module parameters. */
buildImpulse() {
const e = this.ctx.sampleRate, t = Math.max(e * this.options.time, 1), o = e * this.options.delay, r = this.ctx.createBuffer(2, t, e), a = new Float32Array(t), h = new Float32Array(t), p = this.getNoise(t), u = this.getNoise(t);
for (let i = 0; i < t; i++) {
let n = 0;
i < o ? (a[i] = 0, h[i] = 0, n = this.options.reverse ?? false ? t - (i - o) : i - o) : n = this.options.reverse ?? false ? t - i : i, a[i] = (p[i] ?? 0) * (1 - n / t) ** this.options.decay, h[i] = (u[i] ?? 0) * (1 - n / t) ** this.options.decay;
}
r.getChannelData(0).set(a), r.getChannelData(1).set(h), this.convolverNode.buffer = r;
}
}
/**
* SynthesizerNote Class
*
* @author imaya
* @private
*/
class SynthesizerNote {
// Constants
static SEMITONE_RATIO = 1.0594630943592953; // 2^(1/12)
static MIDI_CENTER_VALUE = 64;
static MIDI_MAX_VALUE = 127;
static CENTS_PER_OCTAVE = 1200;
static DEFAULT_Q_VALUE = 10;
static Q_DIVISOR = 200;
/**
* @param {AudioContext} ctx
* @param {AudioNode} destination
* @param {{
* channel: number;
* key: number;
* velocity: number;
* sample: Uint8Array;
* basePlaybackRate: number;
* loopStart: number;
* loopEnd: number;
* sampleRate: number;
* volume: number;
* panpot: number;
* pitchBend: number;
* pitchBendSensitivity: number;
* modEnvToPitch: number;
* expression: number;
* modulation: number;
* cutOffFrequency: number;
* hermonicContent: number;
* reverb: import('@logue/reverb').default;
* volDelay: number;
* modDelay: number;
* volAttack: number;
* modAttack: number;
* volHold: number;
* modHold: number;
* volDecay: number;
* modDecay: number;
* releaseTime: number;
* volRelease: number;
* modRelease: number;
* start: number;
* end: number;
* pan: number;
* sampleModes: number;
* initialAttenuation: number;
* volSustain:number;
* modSustain:number;
* initialFilterFc :number;
* modEnvToFilterFc:number;
* initialFilterQ: number;
* mute: number;
* scaleTuning: number;
* }} instrument
*/
constructor(ctx, destination, instrument) {
/** @type {AudioContext} */
this.ctx = ctx;
/** @type {AudioNode} */
this.destination = destination;
/** @type {Object} */
this.instrument = instrument;
// Instrument properties
const {
channel,
key,
velocity,
sample,
basePlaybackRate,
loopStart,
loopEnd,
sampleRate,
volume,
panpot,
pitchBend,
pitchBendSensitivity,
modEnvToPitch,
expression,
modulation,
cutOffFrequency,
hermonicContent,
reverb,
} = instrument;
this.channel = channel;
this.key = key;
this.velocity = velocity;
this.buffer = sample;
this.playbackRate = basePlaybackRate;
this.loopStart = loopStart;
this.loopEnd = loopEnd;
this.sampleRate = sampleRate;
this.volume = volume;
this.panpot = panpot;
this.pitchBend = pitchBend;
this.pitchBendSensitivity = pitchBendSensitivity;
this.modEnvToPitch = modEnvToPitch;
this.expression = expression;
this.modula