UNPKG

@logue/sf2synth

Version:
1,804 lines (1,648 loc) 151 kB
/** * @logue/sf2synth * * @description SoundFont2 Synthesizer * @author iyama, Logue * @license MIT * @version 0.7.5 * @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.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, th