Skip to content

Commit

Permalink
Fix sf3 loading issues
Browse files Browse the repository at this point in the history
different way, but this one actually works
adding git LFS
  • Loading branch information
spessasus committed May 17, 2024
1 parent 7ecd99e commit d4cacf0
Show file tree
Hide file tree
Showing 6 changed files with 75 additions and 25 deletions.
2 changes: 2 additions & 0 deletions .gitattributes
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
*.sf3 filter=lfs diff=lfs merge=lfs -text
*.sf2 filter=lfs diff=lfs merge=lfs -text
Binary file modified soundfonts/GeneralUser_GS.sf3
Binary file not shown.
19 changes: 14 additions & 5 deletions src/spessasynth_lib/soundfont/chunk/samples.js
Original file line number Diff line number Diff line change
Expand Up @@ -124,6 +124,7 @@ export class Sample {
this.sampleLength = this.sampleEndIndex - this.sampleStartIndex;
this.indexRatio = 1;
this.sampleDataArray = smplArr;
this.sampleData = new Float32Array(0);
this.sampleLengthSeconds = this.sampleLength / (this.sampleRate * 2);
this.loopAllowed = this.sampleLoopStartIndex !== this.sampleLoopEndIndex;
this.isCompressed = (this.sampleType & 0x10) > 0;
Expand Down Expand Up @@ -198,15 +199,21 @@ export class Sample {
});
}

get isSampleLoaded()
{
return this.sampleData.length > 0;
}

/**
* creates a sample sampleData and stores it for reuse
* @param startAddrOffset {number}
* @param endAddrOffset {number}
* @returns {Float32Array}
* @returns {Promise<Float32Array>} The audioData
*/
async getAudioData(startAddrOffset = 0, endAddrOffset = 0) {
if (!this.sampleData) {
this.sampleData = await this.loadBufferData(this.sampleDataArray);
if (!this.isSampleLoaded) {
// start loading data if not loaded
return await this.loadBufferData(this.sampleDataArray).then();
}
// if no offset, return saved sampleData
if (this.sampleData && startAddrOffset === 0 && endAddrOffset === 0) {
Expand All @@ -224,8 +231,10 @@ export class Sample {
* @returns {Promise<AudioBuffer>}
*/
async getAudioBuffer(context, startAddrOffset, endAddrOffset) {
if (!this.sampleData) {
this.sampleData = await this.loadBufferData(this.sampleDataArray);
// no sample data means no buffe
if (!this.isSampleLoaded) {
// start loading the buffer if no data
await this.loadBufferData(this.sampleDataArray)

// if it was compressed, the length has changed
if(this.sampleLength / 2 + 1 !== this.buffer.length)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -143,6 +143,15 @@ class ChannelProcessor extends AudioWorkletProcessor {

case workletMessageType.sampleDump:
this.samples[data.sampleID] = data.sampleData;
// the sample maybe was loaded after the voice was sent... adjust the end position!
this.voices.forEach(v => {
if(v.sample.sampleID !== data.sampleID)
{
return;
}
v.sample.end = data.sampleData.length + v.generators[generatorTypes.endAddrOffset] + (v.generators[generatorTypes.endAddrsCoarseOffset] * 32768);
})

break;

case workletMessageType.ccReset:
Expand Down Expand Up @@ -256,9 +265,9 @@ class ChannelProcessor extends AudioWorkletProcessor {
*/
renderVoice(voice, output, reverbOutput, chorusOutput)
{
if(!this.samples[voice.sample.sampleID])
// if no matching sample, perhaps it's still being loaded..? worklet_channel.js line 256
if(this.samples[voice.sample.sampleID] === undefined)
{
voice.finished = true;
return;
}

Expand Down
61 changes: 45 additions & 16 deletions src/spessasynth_lib/synthetizer/worklet_system/worklet_channel.js
Original file line number Diff line number Diff line change
Expand Up @@ -250,13 +250,43 @@ export class WorkletChannel {
}
}

/**
* This is how the logic works: since sf3 is compressed, we rely on an async decoder.
* So, if the sample isn't loaded yet:
* send the workletVoice (generators, modulators, etc) and the WorkletSample(sampleID + end offset + loop)
* once the voice is done, then we dump it.
*
* on the WorkletScope side:
* skip the voice if sampleID isn't valid
* once we receive a sample dump, adjust all voice endOffsets (loop is already correct in sf3)
* now the voice starts playing, yay!
* @param sample {Sample}
* @param id {number}
*/
dumpSample(sample, id)
{
// flag as dumped so other calls won't dump it again
this.dumpedSamples.add(id);

// load the data
sample.getAudioData().then(sampleData => {
this.post({
messageType: workletMessageType.sampleDump,
messageData: {
sampleID: id,
sampleData: sampleData
}
});
})
}

/**
* @param midiNote {number}
* @param velocity {number}
* @param debug {boolean}
* @returns {Promise<WorkletVoice[]>}
* @returns {WorkletVoice[]}
*/
async getWorkletVoices(midiNote, velocity, debug=false)
getWorkletVoices(midiNote, velocity, debug=false)
{
/**
* @type {WorkletVoice[]}
Expand All @@ -273,21 +303,18 @@ export class WorkletChannel {
}
else
{
let canCache = true;
/**
* @returns {WorkletVoice}
*/
workletVoices = await Promise.all(this.preset.getSamplesAndGenerators(midiNote, velocity).map(async sampleAndGenerators => {
workletVoices = this.preset.getSamplesAndGenerators(midiNote, velocity).map(sampleAndGenerators => {

// dump the sample if haven't already
if (!this.dumpedSamples.has(sampleAndGenerators.sampleID)) {
this.post({
messageType: workletMessageType.sampleDump,
messageData: {
sampleID: sampleAndGenerators.sampleID,
sampleData: await sampleAndGenerators.sample.getAudioData()
}
});
this.dumpedSamples.add(sampleAndGenerators.sampleID);
this.dumpSample(sampleAndGenerators.sample, sampleAndGenerators.sampleID);

// can't cache the voice as the end in workletSample maybe is incorrect (the sample is still loading)
canCache = false;
}

// create the generator list
Expand Down Expand Up @@ -330,7 +357,7 @@ export class WorkletChannel {
rootKey: rootKey,
loopStart: loopStart,
loopEnd: loopEnd,
end: Math.floor(sampleAndGenerators.sample.sampleLength / 2) + (generators[generatorTypes.endAddrOffset] + (generators[generatorTypes.endAddrsCoarseOffset] * 32768)),
end: Math.floor( sampleAndGenerators.sample.sampleData.length) + (generators[generatorTypes.endAddrOffset] + (generators[generatorTypes.endAddrsCoarseOffset] * 32768)),
loopingMode: loopingMode
};

Expand Down Expand Up @@ -388,10 +415,12 @@ export class WorkletChannel {
currentTuningCents: 0
};

}));
});

// cache the voice
this.cachedWorkletVoices[midiNote][velocity] = workletVoices;
if(canCache) {
this.cachedWorkletVoices[midiNote][velocity] = workletVoices;
}
}
return workletVoices;
}
Expand All @@ -402,7 +431,7 @@ export class WorkletChannel {
* @param debug {boolean}
* @returns {number} the number of voices that this note adds
*/
async playNote(midiNote, velocity, debug = false) {
playNote(midiNote, velocity, debug = false) {
if(!velocity)
{
throw "No velocity given!";
Expand All @@ -418,7 +447,7 @@ export class WorkletChannel {
return 0;
}

let workletVoices = await this.getWorkletVoices(midiNote, velocity, debug);
let workletVoices = this.getWorkletVoices(midiNote, velocity, debug);

this.post({
messageType: workletMessageType.noteOn,
Expand Down
5 changes: 3 additions & 2 deletions src/website/ui/midi_keyboard.js
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ export class MidiKeyboard
* @type {"light"|"dark"}
*/
this.mode = "light";
this.enableDebugging = true;

/**
* @type {{min: number, max: number}}
Expand Down Expand Up @@ -122,7 +123,7 @@ export class MidiKeyboard
const relativeMouseY = e.clientY - rect.top;
const keyHeight = rect.height;
const velocity = Math.floor(relativeMouseY / keyHeight * 127);
this.synth.noteOn(this.channel, midiNote, velocity, true);
this.synth.noteOn(this.channel, midiNote, velocity, this.enableDebugging);
}

keyElement.onpointerdown = e =>
Expand All @@ -134,7 +135,7 @@ export class MidiKeyboard
const relativeMouseY = e.clientY - rect.top;
const keyHeight = rect.height;
const velocity = Math.floor(relativeMouseY / keyHeight * 127);
this.synth.noteOn(this.channel, midiNote, velocity, true);
this.synth.noteOn(this.channel, midiNote, velocity, this.enableDebugging);
}

keyElement.onpointerout = () => {
Expand Down

0 comments on commit d4cacf0

Please sign in to comment.