@logue/sf2synth
Version:
SoundFont2 Synthesizer
1,804 lines (1,648 loc) • 147 kB
JavaScript
/**
* @logue/sf2synth
*
* @description SoundFont2 Synthesizer
* @author iyama, Logue
* @license MIT
* @version 0.7.4
* @see {@link https://github.com/logue/sf2synth.js}
*/
(function (global, factory) {
typeof exports === 'object' && typeof module !== 'undefined' ? module.exports = factory() :
typeof define === 'function' && define.amd ? define(factory) :
(global = typeof globalThis !== 'undefined' ? globalThis : global || self, global.SoundFont = factory());
})(this, (function () { 'use strict';
// This file is auto-generated by the build system.
const meta = {
version: '0.7.4',
date: '2025-06-22T14:50:40.627Z',
};
/**
* 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-06-22T14:40:43.637Z"
}, 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, th