Skip to content

Commit

Permalink
Add experimental SF2Pack support
Browse files Browse the repository at this point in the history
  • Loading branch information
spessasus committed Aug 8, 2024
1 parent 253732d commit c9a066c
Show file tree
Hide file tree
Showing 13 changed files with 190 additions and 112 deletions.
5 changes: 3 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -35,9 +35,10 @@ This repository contains both the library and a complete musical web application
- Suitable for both **real-time** and **offline** synthesis
- **Excellent SoundFont support:**
- **Generator Support**
- **Modulator Support:** _First (to my knowledge) JavaScript SoundFont synth with that feature!_
- **Modulator Support:** *First (to my knowledge) JavaScript SoundFont synth with that feature!*
- **SoundFont3 Support:** Play compressed SoundFonts!
- **Can load very large SoundFonts:** up to 4GB! _Note: Only Firefox handles this well; Chromium has a hard-coded memory limit_
- **Experimental SF2Pack Support:** Play soundfonts compressed with BASSMIDI! (*Note: only works with vorbis compression*)
- **Can load very large SoundFonts:** up to 4GB! *Note: Only Firefox handles this well; Chromium has a hard-coded memory limit*
- **Reverb and chorus support:** [customizable!](https://github.com/spessasus/SpessaSynth/wiki/Synthetizer-Class#effects-configuration-object)
- **Export audio files** using [OfflineAudioContext](https://developer.mozilla.org/en-US/docs/Web/API/OfflineAudioContext)
- **[Custom modulators for additional controllers](https://github.com/spessasus/SpessaSynth/wiki/Modulator-Class#default-modulators):** Why not?
Expand Down
2 changes: 1 addition & 1 deletion index.html
Original file line number Diff line number Diff line change
Expand Up @@ -89,7 +89,7 @@ <h1 id="title" translate-path='locale.demoTitleMessage'>SpessaSynth: Online Demo
<label id='export_button' translate-path-title='locale.exportAudio.button'>Export audio</label>

<label id="sf_upload"> <span translate-path='locale.demoSoundfontUploadButton'>Upload the soundfont</span>
<input type="file" accept=".sf2,.sf3" id="sf_file_input"><br/>
<input type="file" accept=".sf2,.sf3,.sfogg" id="sf_file_input"><br/>
</label>

<label>
Expand Down
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "SpessaSynth",
"version": "3.14.5",
"version": "3.15.0",
"type": "module",
"scripts": {
"start": "node src/website/server/server.js",
Expand Down
4 changes: 2 additions & 2 deletions src/spessasynth_lib/soundfont/read/presets.js
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import {RiffChunk} from "./riff_chunk.js";
import {PresetZone} from "./zones.js";
import {readBytesAsUintLittleEndian} from "../../utils/byte_functions/little_endian.js";
import {Sample} from "./samples.js";
import {LoadedSample} from "./samples.js";
import { Generator, generatorTypes } from './generators.js'
import { defaultModulators } from './modulators.js'
import { readBytesAsString } from '../../utils/byte_functions/string.js'
Expand Down Expand Up @@ -114,7 +114,7 @@ export class Preset {
* instrumentGenerators: Generator[],
* presetGenerators: Generator[],
* modulators: Modulator[],
* sample: Sample,
* sample: LoadedSample,
* sampleID: number,
* }} SampleAndGenerators
*/
Expand Down
83 changes: 53 additions & 30 deletions src/spessasynth_lib/soundfont/read/samples.js
Original file line number Diff line number Diff line change
Expand Up @@ -111,7 +111,8 @@ export class BasicSample
return;
}
// compress, always mono!
try {
try
{
this.compressedData = encodeVorbis([this.getAudioData()], 1, this.sampleRate, quality);
// flag as compressed
this.sampleType |= 0x10;
Expand All @@ -138,7 +139,7 @@ export class BasicSample
}
}

export class Sample extends BasicSample
export class LoadedSample extends BasicSample
{
/**
* Creates a sample
Expand All @@ -152,8 +153,10 @@ export class Sample extends BasicSample
* @param samplePitchCorrection {number}
* @param sampleLink {number}
* @param sampleType {number}
* @param smplArr {IndexedByteArray}
* @param smplArr {IndexedByteArray|Float32Array}
* @param sampleIndex {number} initial sample index when loading the sfont
* @param isDataRaw {boolean} if false, the data is decoded as float32.
* Used for SF2Pack support
*/
constructor(sampleName,
sampleStartIndex,
Expand All @@ -166,7 +169,8 @@ export class Sample extends BasicSample
sampleLink,
sampleType,
smplArr,
sampleIndex
sampleIndex,
isDataRaw,
)
{
super(
Expand Down Expand Up @@ -197,7 +201,7 @@ export class Sample extends BasicSample
this.sampleLoopEndIndex += this.sampleStartIndex;
this.sampleLength = 99999999; // set to 999999 before we decode it
}

this.isDataRaw = isDataRaw;
}

/**
Expand All @@ -218,6 +222,10 @@ export class Sample extends BasicSample
}
else
{
if(!this.isDataRaw)
{
throw new Error("Writing SF2Pack samples is not supported.");
}
const dataStartIndex = smplArr.currentIndex;
return smplArr.slice(dataStartIndex + this.sampleStartIndex, dataStartIndex + this.sampleEndIndex)
}
Expand Down Expand Up @@ -255,7 +263,24 @@ export class Sample extends BasicSample
if (!this.isSampleLoaded)
{
// start loading data if not loaded
return this.loadBufferData();
if (this.sampleLength < 1)
{
// eos, do not do anything
return new Float32Array(1);
}

if(this.isCompressed)
{
// if compressed, decode
this.decodeVorbis();
this.isSampleLoaded = true;
return this.sampleData;
}
else if(!this.isDataRaw)
{
return this.getUncompressedReadyData();
}
return this.loadUncompressedData();
}
return this.sampleData;
}
Expand Down Expand Up @@ -293,40 +318,36 @@ export class Sample extends BasicSample
/**
* @returns {Float32Array}
*/
loadBufferData()
getUncompressedReadyData()
{
if (this.sampleLength < 1)
{
// eos, do not do anything
return new Float32Array(1);
}

if(this.isCompressed)
{
this.decodeVorbis();
this.isSampleLoaded = true;
return this.sampleData;
}
return this.loadUncompressedData();
/**
* read the sample data
* @type {Float32Array}
*/
let audioData = this.sampleDataArray.slice(this.sampleStartIndex / 2, this.sampleEndIndex / 2);
this.sampleData = audioData;
this.isSampleLoaded = true;
return audioData;
}
}

/**
* Reads the generatorTranslator from the shdr read
* @param sampleHeadersChunk {RiffChunk}
* @param smplChunkData {IndexedByteArray}
* @returns {Sample[]}
* @param smplChunkData {IndexedByteArray|Float32Array}
* @param isSmplDataRaw {boolean}
* @returns {LoadedSample[]}
*/
export function readSamples(sampleHeadersChunk, smplChunkData)
export function readSamples(sampleHeadersChunk, smplChunkData, isSmplDataRaw = true)
{
/**
* @type {Sample[]}
* @type {LoadedSample[]}
*/
let samples = [];
let index = 0;
while(sampleHeadersChunk.chunkData.length > sampleHeadersChunk.chunkData.currentIndex)
{
const sample = readSample(index, sampleHeadersChunk.chunkData, smplChunkData);
const sample = readSample(index, sampleHeadersChunk.chunkData, smplChunkData, isSmplDataRaw);
samples.push(sample);
index++;
}
Expand All @@ -342,10 +363,11 @@ export function readSamples(sampleHeadersChunk, smplChunkData)
* Reads it into a sample
* @param index {number}
* @param sampleHeaderData {IndexedByteArray}
* @param smplArrayData {IndexedByteArray}
* @returns {Sample}
* @param smplArrayData {IndexedByteArray|Float32Array}
* @param isDataRaw {boolean} true means binary 16 bit data, false means float32
* @returns {LoadedSample}
*/
function readSample(index, sampleHeaderData, smplArrayData) {
function readSample(index, sampleHeaderData, smplArrayData, isDataRaw) {

// read the sample name
let sampleName = readBytesAsString(sampleHeaderData, 20);
Expand Down Expand Up @@ -383,7 +405,7 @@ function readSample(index, sampleHeaderData, smplArrayData) {



return new Sample(sampleName,
return new LoadedSample(sampleName,
sampleStartIndex,
sampleEndIndex,
sampleLoopStartIndex,
Expand All @@ -394,5 +416,6 @@ function readSample(index, sampleHeaderData, smplArrayData) {
sampleLink,
sampleType,
smplArrayData,
index);
index,
isDataRaw);
}
6 changes: 3 additions & 3 deletions src/spessasynth_lib/soundfont/read/zones.js
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ import {readBytesAsUintLittleEndian} from "../../utils/byte_functions/little_end
import {IndexedByteArray} from "../../utils/indexed_array.js";
import {RiffChunk} from "./riff_chunk.js";
import {Generator, generatorTypes} from "./generators.js";
import {Sample} from "./samples.js";
import {LoadedSample} from "./samples.js";
import {Instrument} from "./instruments.js";
import {Modulator} from "./modulators.js";

Expand Down Expand Up @@ -81,7 +81,7 @@ export class InstrumentZone {

/**
* Loads the zone's sample
* @param samples {Sample[]}
* @param samples {LoadedSample[]}
*/
getSample(samples)
{
Expand Down Expand Up @@ -126,7 +126,7 @@ export class InstrumentZone {
* @param zonesChunk {RiffChunk}
* @param instrumentGenerators {Generator[]}
* @param instrumentModulators {Modulator[]}
* @param instrumentSamples {Sample[]}
* @param instrumentSamples {LoadedSample[]}
* @returns {InstrumentZone[]}
*/
export function readInstrumentZones(zonesChunk, instrumentGenerators, instrumentModulators, instrumentSamples)
Expand Down
54 changes: 49 additions & 5 deletions src/spessasynth_lib/soundfont/soundfont.js
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ import { consoleColors } from '../utils/other.js'
import { SpessaSynthGroup, SpessaSynthGroupEnd, SpessaSynthInfo, SpessaSynthWarn } from '../utils/loggin.js'
import { readBytesAsString } from '../utils/byte_functions/string.js'
import { write } from './write/write.js'
import { stbvorbis } from "../externals/stbvorbis_sync/stbvorbis_sync.min.js";

/**
* soundfont.js
Expand Down Expand Up @@ -42,7 +43,13 @@ class SoundFont2
let firstChunk = readRIFFChunk(this.dataArray, false);
this.verifyHeader(firstChunk, "riff");

this.verifyText(readBytesAsString(this.dataArray,4), "sfbk");
const type = readBytesAsString(this.dataArray,4).toLowerCase();
if(type !== "sfbk" && type !== "sfpk")
{
SpessaSynthGroupEnd();
throw new SyntaxError(`Invalid soundFont! Expected "sfbk" or "sfpk" got "${type}"`);
}
const isSF2Pack = type === "sfpk";

// INFO
let infoChunk = readRIFFChunk(this.dataArray);
Expand Down Expand Up @@ -88,7 +95,39 @@ class SoundFont2
SpessaSynthInfo("%cVerifying smpl chunk...", consoleColors.warn)
let sampleDataChunk = readRIFFChunk(this.dataArray, false);
this.verifyHeader(sampleDataChunk, "smpl");
this.sampleDataStartIndex = this.dataArray.currentIndex;
/**
* @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(this.dataArray.buffer.slice(this.dataArray.currentIndex, this.dataArray.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 = this.dataArray;
this.sampleDataStartIndex = this.dataArray.currentIndex;
}

SpessaSynthInfo(`%cSkipping sample chunk, length: %c${sdtaChunk.size - 12}`,
consoleColors.info,
Expand Down Expand Up @@ -133,8 +172,8 @@ class SoundFont2
* read all the samples
* (the current index points to start of the smpl read)
*/
this.dataArray.currentIndex = this.sampleDataStartIndex
this.samples = readSamples(presetSamplesChunk, this.dataArray);
this.dataArray.currentIndex = this.sampleDataStartIndex;
this.samples = readSamples(presetSamplesChunk, sampleData, !isSF2Pack);

/**
* read all the instrument generators
Expand Down Expand Up @@ -195,6 +234,11 @@ class SoundFont2
consoleColors.recognized,
consoleColors.info);
SpessaSynthGroupEnd();

if(isSF2Pack)
{
delete this.dataArray;
}
}

removeUnusedElements()
Expand Down Expand Up @@ -224,7 +268,7 @@ class SoundFont2
}

/**
* @param sample {Sample}
* @param sample {LoadedSample}
*/
deleteSample(sample)
{
Expand Down
12 changes: 6 additions & 6 deletions src/spessasynth_lib/synthetizer/worklet_processor.min.js

Large diffs are not rendered by default.

Original file line number Diff line number Diff line change
Expand Up @@ -225,6 +225,10 @@ export function reloadSoundFont(buffer, isOverride = false)
this.clearSoundFont(false, isOverride);
if(!isOverride)
{
delete this.soundfont.dataArray;
this.soundfont.samples.length = 0;
this.soundfont.instruments.length = 0;
this.soundfont.presets.length = 0;
delete this.soundfont;
}
try
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -65,7 +65,7 @@ export function clearSamplesList()

function /**
* @param channel {number} channel hint for the processor to recalculate cursor positions
* @param sample {Sample}
* @param sample {LoadedSample}
* @param id {number}
* @param sampleDumpCallback {function({channel: number, sampleID: number, sampleData: Float32Array})}
*/
Expand Down Expand Up @@ -195,10 +195,16 @@ export function getWorkletVoices(channel,
}

// determine looping mode now. if the loop is too small, disable
const loopStart = (sampleAndGenerators.sample.sampleLoopStartIndex / 2) + (generators[generatorTypes.startloopAddrsOffset] + (generators[generatorTypes.startloopAddrsCoarseOffset] * 32768));
const loopEnd = (sampleAndGenerators.sample.sampleLoopEndIndex / 2) + (generators[generatorTypes.endloopAddrsOffset] + (generators[generatorTypes.endloopAddrsCoarseOffset] * 32768));
let loopStart = (sampleAndGenerators.sample.sampleLoopStartIndex / 2) + (generators[generatorTypes.startloopAddrsOffset] + (generators[generatorTypes.startloopAddrsCoarseOffset] * 32768));
let loopEnd = (sampleAndGenerators.sample.sampleLoopEndIndex / 2) + (generators[generatorTypes.endloopAddrsOffset] + (generators[generatorTypes.endloopAddrsCoarseOffset] * 32768));
let loopingMode = generators[generatorTypes.sampleModes];
if (loopEnd - loopStart < 1) {
const sampleLength = sampleAndGenerators.sample.getAudioData().length;
// clamp loop
loopStart = Math.min(Math.max(0, loopStart), sampleLength);
// clamp loop
loopEnd = Math.min(Math.max(0, loopEnd), sampleLength);
if (loopEnd - loopStart < 1)
{
loopingMode = 0;
}

Expand Down
Loading

0 comments on commit c9a066c

Please sign in to comment.