Current implementation of the soundFont2.js
/**
* Description: This is a SoundFont2 parser.
*
* @package webSynth
* @category module js
* @author Mrtenz <https://github.com/Mrtenz/soundfont2>
* @copyright soundfont2
* @license GPL-2.0-or-later
*/
class SoundFont2 {
static from(file) {
return new SoundFont2(file)
}
/**
* Принимает объект структуры чанков и File или Blob
* @param {Object} chunk
* @param {File|Blob} fileObject
*/
constructor(chunk, fileObject) {
// Сохраняем ссылку на файл (sourceFile — для вашего старого кода, fileObject — для плеера)
this.sourceFile = fileObject;
this.fileObject = fileObject;
// Защита: если на Шаге 6 мы передали структуру как sf2StructureChunk напрямую
this.chunk = chunk;
// Вытаскиваем подчанки (с проверкой на случай, если структура вложена)
const subChunks = chunk.subChunks || [];
if (subChunks.length !== 3) {
throw new ParseError("Invalid sfbk structure", "3 chunks", `${subChunks.length} chunks`);
}
this.metaData = subChunks[0].getMetaData();
this.sampleData = subChunks[1].getSampleData(); // Тут теперь лежит только { globalOffset }
this.presetData = subChunks[2].getPresetData();
this.samples = this.getSamples(); // Настроит ленивые геттеры getAudioData()
this.instruments = this.getInstruments();
this.presets = this.getPresets();
this.banks = this.getBanks();
}
getKeyData(
memoizedBankNumber = 0,
memoizedPresetNumber = 0,
memoizedKeyNumber
) {
return memoize((keyNumber, bankNumber, presetNumber) => {
const bank = this.banks[bankNumber]
if (bank) {
const preset = bank.presets[presetNumber]
if (preset) {
const presetZones = preset.zones.filter(zone =>
this.isKeyInRange(zone, keyNumber)
)
if (presetZones.length > 0) {
for (const presetZone of presetZones) {
const instrument = presetZone.instrument
const instrumentZones = instrument.zones.filter(zone =>
this.isKeyInRange(zone, keyNumber)
)
if (instrumentZones.length > 0) {
for (const instrumentZone of instrumentZones) {
const sample = instrumentZone.sample
const generators = {
...presetZone.generators,
...instrumentZone.generators
}
const modulators = {
...presetZone.modulators,
...instrumentZone.modulators
}
return {
keyNumber,
preset,
instrument,
sample, // Объект сэмпла, содержащий метод чтения
generators,
modulators
}
}
}
}
}
}
}
return null
})(memoizedKeyNumber, memoizedBankNumber, memoizedPresetNumber)
}
isKeyInRange(zone, keyNumber) {
return (
zone.keyRange === undefined ||
(zone.keyRange.lo <= keyNumber && zone.keyRange.hi >= keyNumber)
)
}
getBanks() {
return this.presets.reduce((target, preset) => {
const bankNumber = preset.header.bank
if (!target[bankNumber]) {
target[bankNumber] = { presets: [] }
}
target[bankNumber].presets[preset.header.preset] = preset
return target
}, [])
}
getPresets() {
const { presetHeaders, presetZones, presetGenerators, presetModulators } = this.presetData
const presets = getItemsInZone(
presetHeaders, presetZones, presetModulators, presetGenerators,
this.instruments, GeneratorType.Instrument
)
return presets
.filter(preset => preset.header.name !== "EOP")
.map(preset => {
return {
header: preset.header,
globalZone: preset.globalZone,
zones: preset.zones.map(zone => {
return {
keyRange: zone.keyRange,
generators: zone.generators,
modulators: zone.modulators,
instrument: zone.reference
}
})
}
})
}
getInstruments() {
const { instrumentHeaders, instrumentZones, instrumentModulators, instrumentGenerators } = this.presetData
const instruments = getItemsInZone(
instrumentHeaders, instrumentZones, instrumentModulators, instrumentGenerators,
this.samples, GeneratorType.SampleId
)
return instruments
.filter(instrument => instrument.header.name !== "EOI")
.map(instrument => {
return {
header: instrument.header,
globalZone: instrument.globalZone,
zones: instrument.zones.map(zone => {
return {
keyRange: zone.keyRange,
generators: zone.generators,
modulators: zone.modulators,
sample: zone.reference
}
})
}
})
}
/**
* Оптимизированный метод: ОЗУ не расходуется вообще!
*/
getSamples() {
const self = this;
const smplOffset = this.sampleData.globalOffset; // Узнаем, где в файле лежит блок звуков
return this.presetData.sampleHeaders
.filter(sample => sample.name !== "EOS")
.map(header => {
if (header.name !== "EOS" && header.sampleRate <= 0) {
throw new Error(`Illegal sample rate of ${header.sampleRate} hz in sample '${header.name}'`)
}
if (header.originalPitch >= 128 && header.originalPitch <= 254) {
header.originalPitch = 60
}
header.startLoop -= header.start
header.endLoop -= header.start
// Считаем абсолютные границы сэмпла внутри File/Blob объекта:
// Начало = Смещение блока smpl + смещение конкретного сэмпла (умножаем на 2, т.к. данные 16-битные)
const startByte = smplOffset + (header.start * 2);
const endByte = smplOffset + (header.end * 2);
return {
header,
// Метод для ленивой загрузки ноты по требованию с диска
getAudioData: async function() {
// Вырезаем кусочек из сохраненного в конструкторе File/Blob
const blobSlice = self.sourceFile.slice(startByte, endByte);
const buffer = await blobSlice.arrayBuffer();
return new Int16Array(buffer);
}
}
})
}
// ==========================================
// НОВЫЕ МЕТОДЫ ДЛЯ СИСТЕМЫ УМНОЙ ПРЕДЗАГРУЗКИ
// ==========================================
/**
* Мгновенно выгружает все сэмплы из ОЗУ.
* Вызывайте перед загрузкой нового списка инструментов.
*/
unloadAllSamples() {
//LiteWebSynth: Очистка ОЗУ от старых сэмплов...
this.samples.forEach(sample => {
if (sample.data) {
sample.data = null; // Обнуляем бинарный массив, сборщик мусора очистит память
}
});
}
/**
* Принимает массив объектов [{channel, bank, preset}, ...] и превентивно кэширует их.
* @param {Array<{channel: number, bank: number, instr: preset}>} activeInstruments
*/
async preloadActiveInstruments(activeInstruments) {
//LiteWebSynth: Старт превентивной умной загрузки инструментов...
// 1. Фильтруем дубликаты инструментов по банку и пресету
const uniqueInstruments = [];
const seen = new Set();
for (const item of activeInstruments) {
const key = `${item.bank}_${item.instr}`;
if (!seen.has(key)) {
seen.add(key);
uniqueInstruments.push({ bank: item.bank, instr: item.instr });
}
}
// 2. Итерируемся по уникальным инструментам и подгружаем сэмплы
for (const { bank, instr } of uniqueInstruments) {
const bankObj = this.banks[bank];
if (!bankObj) continue;
const preset = bankObj.presets[instr];
if (!preset) continue;
// Накапливаем промисы загрузки для текущего пресета, чтобы качать их параллельно
const loadPromises = [];
for (const zone of preset.zones) {
const instrument = zone.instrument;
if (instrument && instrument.zones) {
for (const instZone of instrument.zones) {
const sample = instZone.sample;
// Если сэмпл найден, еще не загружен и у него есть ленивый метод чтения
if (sample && !sample.data && typeof sample.getAudioData === 'function') {
// Запускаем асинхронное вырезание из Blob и сохраняем прямо в объект сэмпла
const promise = sample.getAudioData().then(binaryData => {
sample.data = binaryData;
});
loadPromises.push(promise);
}
}
}
}
await Promise.all(loadPromises);
console.log(`LiteWebSynth Cache: Instrument [banr: ${bank}, preset: ${instr}] (${preset.header.name}) загружен.`);
}
let msg = this.displaySoundFontMemory();
if(msg) console.log(msg);
}
/**
* Считает, сколько мегабайт занимают активные сэмплы в ОЗУ,
* и выводит результат на информационную панель.
*/
displaySoundFontMemory() {
const informPanel = document.getElementById("inform_panel");
if (!soundFont || !soundFont.samples) {
informPanel.innerHTML = "SoundFont RAM: 0.00 MB (No active samples)";
return;
}
let totalBytes = 0;
// Пробегаем по всем сэмплам структуры
soundFont.samples.forEach(sample => {
// Если сэмпл сейчас загружен в оперативную память и имеет бинарный буфер
if (sample.data && sample.data.byteLength) {
totalBytes += sample.data.byteLength; // Суммируем вес буфера в байтах
}
});
// Переводим байты в Мегабайты
const megabytes = totalBytes / (1024 * 1024);
const sizeStr = megabytes.toFixed(2) + " MB";
// Выводим информацию на лицевую панель плагина
informPanel.style.color = "#A0A378";
informPanel.innerHTML = `SoundFont loaded RAM: ${sizeStr}`;
}
}