spessasynth_core
Version:
MIDI and SoundFont2/DLS library with no compromises
287 lines (272 loc) • 9.5 kB
JavaScript
import { IndexedByteArray } from "../../../utils/indexed_array.js";
import { writeRIFFChunkParts, writeRIFFChunkRaw } from "../riff_chunk.js";
import { getStringBytes } from "../../../utils/byte_functions/string.js";
import { consoleColors } from "../../../utils/other.js";
import { getIGEN } from "./igen.js";
import { getSDTA } from "./sdta.js";
import { getSHDR } from "./shdr.js";
import { getIMOD } from "./imod.js";
import { getIBAG } from "./ibag.js";
import { getINST } from "./inst.js";
import { getPGEN } from "./pgen.js";
import { getPMOD } from "./pmod.js";
import { getPBAG } from "./pbag.js";
import { getPHDR } from "./phdr.js";
import { writeLittleEndian, writeWord } from "../../../utils/byte_functions/little_endian.js";
import { SpessaSynthGroupCollapsed, SpessaSynthGroupEnd, SpessaSynthInfo } from "../../../utils/loggin.js";
import { MOD_BYTE_SIZE } from "../modulator.js";
import { fillWithDefaults } from "../../../utils/fill_with_defaults.js";
/**
* @typedef {function} ProgressFunction
* @param {string} sampleName - the written sample name.
* @param {number} sampleIndex - the sample's index.
* @param {number} sampleCount - the total sample count for progress displaying.
*/
/**
* @typedef {Object} SoundFont2WriteOptions
* @property {boolean} compress - if the soundfont should be compressed with a given function.
* @property {SampleEncodingFunction} compressionFunction -
* the encode vorbis function. It can be undefined if not compressed.
* @property {ProgressFunction} progressFunction - a function to show progress for writing large banks. It can be undefined.
* @property {boolean} writeDefaultModulators - if the DMOD chunk should be written.
* Recommended.
* @property {boolean} writeExtendedLimits - if the xdta chunk should be written to allow virtually infinite parameters.
* Recommended.
* @property {boolean} decompress - if an sf3 bank should be decompressed back to sf2. Not recommended.
*/
/**
* @typedef {Object} ReturnedExtendedSf2Chunks
* @property {IndexedByteArray} pdta - the pdta part of the chunk
* @property {IndexedByteArray} xdta - the xdta (https://github.com/spessasus/soundfont-proposals/blob/main/extended_limits.md) part of the chunk
* @property {number} highestIndex - the highest index written (0 if not applicable). Used for determining whether the xdta chunk is necessary.
*/
/**
* @type {SoundFont2WriteOptions}
*/
const DEFAULT_WRITE_OPTIONS = {
compress: false,
compressionQuality: 0.5,
compressionFunction: undefined,
progressFunction: undefined,
writeDefaultModulators: true,
writeExtendedLimits: true,
decompress: false
};
/**
* Write the soundfont as an .sf2 file
* @this {BasicSoundBank}
* @param {Partial<SoundFont2WriteOptions>} options
* @returns {Uint8Array}
*/
export async function write(options = DEFAULT_WRITE_OPTIONS)
{
options = fillWithDefaults(options, DEFAULT_WRITE_OPTIONS);
if (options?.compress)
{
if (typeof options?.compressionFunction !== "function")
{
throw new Error("No compression function supplied but compression enabled.");
}
if (options?.decompress)
{
throw new Error("Decompressed and compressed at the same time.");
}
}
SpessaSynthGroupCollapsed(
"%cSaving soundfont...",
consoleColors.info
);
SpessaSynthInfo(
`%cCompression: %c${options?.compress || "false"}%c quality: %c${options?.compressionQuality || "none"}`,
consoleColors.info,
consoleColors.recognized,
consoleColors.info,
consoleColors.recognized
);
SpessaSynthInfo(
"%cWriting INFO...",
consoleColors.info
);
/**
* Write INFO
* @type {IndexedByteArray[]}
*/
const infoArrays = [];
this.soundFontInfo["ISFT"] = "SpessaSynth"; // ( ͡° ͜ʖ ͡°)
if (options?.compress || this.samples.some(s => s.isCompressed))
{
this.soundFontInfo["ifil"] = "3.0"; // set version to 3
}
if (options?.decompress)
{
this.soundFontInfo["ifil"] = "2.4"; // set version to 2.04
}
if (options?.writeDefaultModulators)
{
// trigger the DMOD write
this.soundFontInfo["DMOD"] = `${this.defaultModulators.length} Modulators`;
this.customDefaultModulators = true;
}
else
{
delete this.soundFontInfo["DMOD"];
}
for (const [type, data] of Object.entries(this.soundFontInfo))
{
if (type === "ifil" || type === "iver")
{
const major = parseInt(data.split(".")[0]);
const minor = parseInt(data.split(".")[1]);
const ckdata = new IndexedByteArray(4);
writeWord(ckdata, major);
writeWord(ckdata, minor);
infoArrays.push(writeRIFFChunkRaw(type, ckdata));
}
else if (type === "DMOD")
{
const mods = this.defaultModulators;
SpessaSynthInfo(
`%cWriting %c${mods.length}%c default modulators...`,
consoleColors.info,
consoleColors.recognized,
consoleColors.info
);
let dmodsize = MOD_BYTE_SIZE + mods.length * MOD_BYTE_SIZE;
const dmoddata = new IndexedByteArray(dmodsize);
for (const mod of mods)
{
writeWord(dmoddata, mod.getSourceEnum());
writeWord(dmoddata, mod.modulatorDestination);
writeWord(dmoddata, mod.transformAmount);
writeWord(dmoddata, mod.getSecSrcEnum());
writeWord(dmoddata, mod.transformType);
}
// terminal modulator, is zero
writeLittleEndian(dmoddata, 0, MOD_BYTE_SIZE);
infoArrays.push(writeRIFFChunkRaw(type, dmoddata));
}
else
{
infoArrays.push(writeRIFFChunkRaw(
type,
getStringBytes(data, true, true) // pad with zero and ensure even length
));
}
}
SpessaSynthInfo(
"%cWriting SDTA...",
consoleColors.info
);
// write sdta
const smplStartOffsets = [];
const smplEndOffsets = [];
const sdtaChunk = await getSDTA.call(
this,
smplStartOffsets,
smplEndOffsets,
options.compress,
options.decompress,
options?.compressionFunction,
options?.progressFunction
);
SpessaSynthInfo(
"%cWriting PDTA...",
consoleColors.info
);
// write pdta
// go in reverse so the indexes are correct
// instruments
SpessaSynthInfo(
"%cWriting SHDR...",
consoleColors.info
);
const shdrChunk = getSHDR.call(this, smplStartOffsets, smplEndOffsets);
SpessaSynthInfo(
"%cWriting IGEN...",
consoleColors.info
);
const igenChunk = getIGEN.call(this);
SpessaSynthInfo(
"%cWriting IMOD...",
consoleColors.info
);
const imodChunk = getIMOD.call(this);
SpessaSynthInfo(
"%cWriting IBAG...",
consoleColors.info
);
const ibagChunk = getIBAG.call(this);
SpessaSynthInfo(
"%cWriting INST...",
consoleColors.info
);
const instChunk = getINST.call(this);
SpessaSynthInfo(
"%cWriting PGEN...",
consoleColors.info
);
// presets
const pgenChunk = getPGEN.call(this);
SpessaSynthInfo(
"%cWriting PMOD...",
consoleColors.info
);
const pmodChunk = getPMOD.call(this);
SpessaSynthInfo(
"%cWriting PBAG...",
consoleColors.info
);
const pbagChunk = getPBAG.call(this);
SpessaSynthInfo(
"%cWriting PHDR...",
consoleColors.info
);
const phdrChunk = getPHDR.call(this);
/**
* @type {ReturnedExtendedSf2Chunks[]}
*/
const chunks = [phdrChunk, pbagChunk, pmodChunk, pgenChunk, instChunk, ibagChunk, imodChunk, igenChunk, shdrChunk];
// combine in the sfspec order
const pdtaChunk = writeRIFFChunkParts(
"pdta",
chunks.map(c => c.pdta),
true
);
const maxIndex = Math.max(
...chunks.map(c => c.highestIndex)
);
const writeXdta = options.writeExtendedLimits && (
maxIndex > 0xFFFF
|| this.presets.some(p => p.presetName.length > 20)
|| this.instruments.some(i => i.instrumentName.length > 20)
|| this.samples.some(s => s.sampleName.length > 20)
);
if (writeXdta)
{
SpessaSynthInfo(
`%cWriting the xdta chunk! Max index: %c${maxIndex}`,
consoleColors.info,
consoleColors.value
);
// https://github.com/spessasus/soundfont-proposals/blob/main/extended_limits.md
const xpdtaChunk = writeRIFFChunkParts("xdta", chunks.map(c => c.xdta), true);
infoArrays.push(xpdtaChunk);
}
const infoChunk = writeRIFFChunkParts("INFO", infoArrays, true);
SpessaSynthInfo(
"%cWriting the output file...",
consoleColors.info
);
// finally, combine everything
const main = writeRIFFChunkParts(
"RIFF",
[getStringBytes("sfbk"), infoChunk, sdtaChunk, pdtaChunk]
);
SpessaSynthInfo(
`%cSaved succesfully! Final file size: %c${main.length}`,
consoleColors.info,
consoleColors.recognized
);
SpessaSynthGroupEnd();
return main;
}