spessasynth_core
Version:
MIDI and SoundFont2/DLS library with no compromises
452 lines (401 loc) • 16.1 kB
JavaScript
import { IndexedByteArray } from "../../utils/indexed_array.js";
import { readSamples } from "./samples.js";
import { readLittleEndian } from "../../utils/byte_functions/little_endian.js";
import { readGenerators } from "./generators.js";
import { applyPresetZones } from "./preset_zones.js";
import { readPresets } from "./presets.js";
import { readInstruments } from "./instruments.js";
import { readModulators } from "./modulators.js";
import { readRIFFChunk, RiffChunk } from "../basic_soundfont/riff_chunk.js";
import { consoleColors } from "../../utils/other.js";
import { SpessaSynthGroup, SpessaSynthGroupEnd, SpessaSynthInfo } from "../../utils/loggin.js";
import { readBytesAsString } from "../../utils/byte_functions/string.js";
import { stbvorbis } from "../../externals/stbvorbis_sync/stbvorbis_sync.min.js";
import { BasicSoundBank } from "../basic_soundfont/basic_soundbank.js";
import { Generator } from "../basic_soundfont/generator.js";
import { Modulator } from "../basic_soundfont/modulator.js";
import { applyInstrumentZones, InstrumentZone } from "./instrument_zones.js";
import { readZoneIndexes } from "./zones.js";
/**
* soundfont.js
* purpose: parses a soundfont2 file
*/
export class SoundFont2 extends BasicSoundBank
{
/**
* @type {Instrument[]}
*/
instruments = [];
/**
* @type {Preset[]}
*/
presets = [];
/**
* Initializes a new SoundFont2 Parser and parses the given data array
* @param arrayBuffer {ArrayBuffer}
* @param warnDeprecated {boolean}
*/
constructor(arrayBuffer, warnDeprecated = true)
{
super();
if (warnDeprecated)
{
console.warn("Using the constructor directly is deprecated. Use loadSoundFont instead.");
}
const mainFileArray = new IndexedByteArray(arrayBuffer);
SpessaSynthGroup("%cParsing SoundFont...", consoleColors.info);
if (!mainFileArray)
{
SpessaSynthGroupEnd();
this.parsingError("No data provided!");
}
// read the main chunk
let firstChunk = readRIFFChunk(mainFileArray, false);
this.verifyHeader(firstChunk, "riff");
const type = readBytesAsString(mainFileArray, 4).toLowerCase();
if (type !== "sfbk" && type !== "sfpk")
{
SpessaSynthGroupEnd();
throw new SyntaxError(`Invalid soundFont! Expected "sfbk" or "sfpk" got "${type}"`);
}
/*
Some SF2Pack description:
this is essentially sf2, but the entire smpl chunk is compressed (we only support Ogg Vorbis here)
and the only other difference is that the main chunk isn't "sfbk" but rather "sfpk"
*/
const isSF2Pack = type === "sfpk";
// INFO
let infoChunk = readRIFFChunk(mainFileArray);
this.verifyHeader(infoChunk, "list");
const infoString = readBytesAsString(infoChunk.chunkData, 4);
if (infoString !== "INFO")
{
SpessaSynthGroupEnd();
throw new SyntaxError(`Invalid soundFont! Expected "INFO" or "${infoString}"`);
}
/**
* @type {RiffChunk|undefined}
*/
let xdtaChunk = undefined;
while (infoChunk.chunkData.length > infoChunk.chunkData.currentIndex)
{
let chunk = readRIFFChunk(infoChunk.chunkData);
let text;
// special cases
switch (chunk.header.toLowerCase())
{
case "ifil":
case "iver":
text = `${readLittleEndian(chunk.chunkData, 2)}.${readLittleEndian(chunk.chunkData, 2)}`;
this.soundFontInfo[chunk.header] = text;
break;
case "icmt":
text = readBytesAsString(chunk.chunkData, chunk.chunkData.length, false);
this.soundFontInfo[chunk.header] = text;
break;
// dmod: default modulators
case "dmod":
const newModulators = readModulators(chunk);
text = `Modulators: ${newModulators.length}`;
// override default modulators
this.defaultModulators = newModulators;
this.customDefaultModulators = true;
this.soundFontInfo[chunk.header] = text;
break;
case "list":
// possible xdta
const listType = readBytesAsString(chunk.chunkData, 4);
if (listType === "xdta")
{
SpessaSynthInfo("%cExtended SF2 found!", consoleColors.recognized);
xdtaChunk = chunk;
}
break;
default:
text = readBytesAsString(chunk.chunkData, chunk.chunkData.length);
this.soundFontInfo[chunk.header] = text;
}
SpessaSynthInfo(
`%c"${chunk.header}": %c"${text}"`,
consoleColors.info,
consoleColors.recognized
);
}
// https://github.com/spessasus/soundfont-proposals/blob/main/extended_limits.md
const isExtended = xdtaChunk !== undefined;
/**
* @type {{
* phdr: RiffChunk,
* pbag: RiffChunk,
* pmod: RiffChunk,
* pgen: RiffChunk,
* inst: RiffChunk,
* ibag: RiffChunk,
* imod: RiffChunk,
* igen: RiffChunk,
* shdr: RiffChunk,
* }}
*/
let xChunks = {};
if (isExtended)
{
// read the hydra chunks
xChunks.phdr = readRIFFChunk(xdtaChunk.chunkData);
xChunks.pbag = readRIFFChunk(xdtaChunk.chunkData);
xChunks.pmod = readRIFFChunk(xdtaChunk.chunkData);
xChunks.pgen = readRIFFChunk(xdtaChunk.chunkData);
xChunks.inst = readRIFFChunk(xdtaChunk.chunkData);
xChunks.ibag = readRIFFChunk(xdtaChunk.chunkData);
xChunks.imod = readRIFFChunk(xdtaChunk.chunkData);
xChunks.igen = readRIFFChunk(xdtaChunk.chunkData);
xChunks.shdr = readRIFFChunk(xdtaChunk.chunkData);
}
// SDTA
const sdtaChunk = readRIFFChunk(mainFileArray, false);
this.verifyHeader(sdtaChunk, "list");
this.verifyText(readBytesAsString(mainFileArray, 4), "sdta");
// smpl
SpessaSynthInfo("%cVerifying smpl chunk...", consoleColors.warn);
let sampleDataChunk = readRIFFChunk(mainFileArray, false);
this.verifyHeader(sampleDataChunk, "smpl");
/**
* @type {IndexedByteArray|Float32Array}
*/
let sampleData;
// SF2Pack: the entire data is compressed
if (isSF2Pack)
{
SpessaSynthInfo(
"%cSF2Pack detected, attempting to decode the smpl chunk...",
consoleColors.info
);
try
{
/**
* @type {Float32Array}
*/
sampleData = stbvorbis.decode(mainFileArray.buffer.slice(
mainFileArray.currentIndex,
mainFileArray.currentIndex + sdtaChunk.size - 12
)).data[0];
}
catch (e)
{
SpessaSynthGroupEnd();
throw new Error(`SF2Pack Ogg Vorbis decode error: ${e}`);
}
SpessaSynthInfo(
`%cDecoded the smpl chunk! Length: %c${sampleData.length}`,
consoleColors.info,
consoleColors.value
);
}
else
{
/**
* @type {IndexedByteArray}
*/
sampleData = mainFileArray;
this.sampleDataStartIndex = mainFileArray.currentIndex;
}
SpessaSynthInfo(
`%cSkipping sample chunk, length: %c${sdtaChunk.size - 12}`,
consoleColors.info,
consoleColors.value
);
mainFileArray.currentIndex += sdtaChunk.size - 12;
// PDTA
SpessaSynthInfo("%cLoading preset data chunk...", consoleColors.warn);
let presetChunk = readRIFFChunk(mainFileArray);
this.verifyHeader(presetChunk, "list");
readBytesAsString(presetChunk.chunkData, 4);
// read the hydra chunks
const phdrChunk = readRIFFChunk(presetChunk.chunkData);
this.verifyHeader(phdrChunk, "phdr");
const pbagChunk = readRIFFChunk(presetChunk.chunkData);
this.verifyHeader(pbagChunk, "pbag");
const pmodChunk = readRIFFChunk(presetChunk.chunkData);
this.verifyHeader(pmodChunk, "pmod");
const pgenChunk = readRIFFChunk(presetChunk.chunkData);
this.verifyHeader(pgenChunk, "pgen");
const instChunk = readRIFFChunk(presetChunk.chunkData);
this.verifyHeader(instChunk, "inst");
const ibagChunk = readRIFFChunk(presetChunk.chunkData);
this.verifyHeader(ibagChunk, "ibag");
const imodChunk = readRIFFChunk(presetChunk.chunkData);
this.verifyHeader(imodChunk, "imod");
const igenChunk = readRIFFChunk(presetChunk.chunkData);
this.verifyHeader(igenChunk, "igen");
const shdrChunk = readRIFFChunk(presetChunk.chunkData);
this.verifyHeader(shdrChunk, "shdr");
/**
* read all the samples
* (the current index points to start of the smpl read)
*/
mainFileArray.currentIndex = this.sampleDataStartIndex;
const samples = readSamples(shdrChunk, sampleData, !isExtended);
if (isExtended)
{
// apply extensions to samples
const xSamples = readSamples(xChunks.shdr, new Float32Array(1), false);
if (xSamples.length === samples.length)
{
samples.forEach((s, i) =>
{
s.sampleName += xSamples[i].sampleName;
s.linkedSampleIndex |= xSamples[i].linkedSampleIndex << 16;
});
}
}
// trim names
samples.forEach(s => s.sampleName = s.sampleName.trim());
this.samples.push(...samples);
/**
* read all the instrument generators
* @type {Generator[]}
*/
let instrumentGenerators = readGenerators(igenChunk);
/**
* read all the instrument modulators
* @type {Modulator[]}
*/
let instrumentModulators = readModulators(imodChunk);
const instruments = readInstruments(instChunk);
if (isExtended)
{
// apply extensions to instruments
const xInst = readInstruments(xChunks.inst);
if (xInst.length === instruments.length)
{
instruments.forEach((inst, i) =>
{
inst.instrumentName += xInst[i].instrumentName;
inst.zoneStartIndex |= xInst[i].zoneStartIndex;
});
// adjust zone counts
instruments.forEach((inst, i) =>
{
if (i < instruments.length - 1)
{
inst.zonesCount = instruments[i + 1].zoneStartIndex - inst.zoneStartIndex;
}
});
}
}
// trim names
instruments.forEach(i => i.instrumentName = i.instrumentName.trim());
this.instruments.push(...instruments);
const ibagIndexes = readZoneIndexes(ibagChunk);
if (isExtended)
{
const extraIndexes = readZoneIndexes(xChunks.ibag);
for (let i = 0; i < ibagIndexes.mod.length; i++)
{
ibagIndexes.mod[i] |= extraIndexes.mod[i] << 16;
}
for (let i = 0; i < ibagIndexes.gen.length; i++)
{
ibagIndexes.gen[i] |= extraIndexes.gen[i] << 16;
}
}
/**
* read all the instrument zones (and apply them)
* @type {InstrumentZone[]}
*/
applyInstrumentZones(
ibagIndexes,
instrumentGenerators,
instrumentModulators,
this.samples,
this.instruments
);
/**
* read all the preset generators
* @type {Generator[]}
*/
let presetGenerators = readGenerators(pgenChunk);
/**
* Read all the preset modulatorrs
* @type {Modulator[]}
*/
let presetModulators = readModulators(pmodChunk);
const presets = readPresets(phdrChunk, this);
if (isExtended)
{
// apply extensions to presets
const xPreset = readPresets(xChunks.phdr, this);
if (xPreset.length === presets.length)
{
presets.forEach((pres, i) =>
{
pres.presetName += xPreset[i].presetName;
pres.zoneStartIndex |= xPreset[i].zoneStartIndex;
});
// adjust zone counts
presets.forEach((preset, i) =>
{
if (i < presets.length - 1)
{
preset.zonesCount = presets[i + 1].zoneStartIndex - preset.zoneStartIndex;
}
});
}
}
// trim names
presets.forEach(p => p.presetName === p.presetName.trim());
this.addPresets(...presets);
const pbagIndexes = readZoneIndexes(pbagChunk);
if (isExtended)
{
const extraIndexes = readZoneIndexes(xChunks.pbag);
for (let i = 0; i < pbagIndexes.mod.length; i++)
{
pbagIndexes.mod[i] |= extraIndexes.mod[i] << 16;
}
for (let i = 0; i < pbagIndexes.gen.length; i++)
{
pbagIndexes.gen[i] |= extraIndexes.gen[i] << 16;
}
}
applyPresetZones(pbagIndexes, presetGenerators, presetModulators, this.instruments, this.presets);
this.flush();
SpessaSynthInfo(
`%cParsing finished! %c"${this.soundFontInfo["INAM"]}"%c has %c${this.presets.length} %cpresets,
%c${this.instruments.length}%c instruments and %c${this.samples.length}%c samples.`,
consoleColors.info,
consoleColors.recognized,
consoleColors.info,
consoleColors.recognized,
consoleColors.info,
consoleColors.recognized,
consoleColors.info,
consoleColors.recognized,
consoleColors.info
);
SpessaSynthGroupEnd();
}
/**
* @param chunk {RiffChunk}
* @param expected {string}
*/
verifyHeader(chunk, expected)
{
if (chunk.header.toLowerCase() !== expected.toLowerCase())
{
SpessaSynthGroupEnd();
this.parsingError(`Invalid chunk header! Expected "${expected.toLowerCase()}" got "${chunk.header.toLowerCase()}"`);
}
}
/**
* @param text {string}
* @param expected {string}
*/
verifyText(text, expected)
{
if (text.toLowerCase() !== expected.toLowerCase())
{
SpessaSynthGroupEnd();
this.parsingError(`Invalid FourCC: Expected "${expected.toLowerCase()}" got "${text.toLowerCase()}"\``);
}
}
}