Working page

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}`;
  }
}