Worker Synthesizer Example
This example shows how to use the new WorkerSynthesizer
class introduced in SpessaSynth 4.0. This example adapts the Advanced example.
worker_synth.html
<p id='message'>Please wait for the sound bank to load.</p>
<input accept='.mid, .rmi, .xmf, .mxmf' id='midi_input' multiple type='file'>
<br><br>
<input id='progress' max='1000' min='0' type='range' value='0'>
<br>
<button id='previous'>Previous song</button>
<button id='pause'>Pause</button>
<button id='next'>Next song</button>
<button id='render'>Render this song</button>
<button id='save_sf2'>Download the trimmed SF2 file</button>
<button id='save_dls'>Download the trimmed DLS file</button>
<button id='save_rmi'>Download the RMIDI file</button>
<script src='worker_synth.js' type='module'></script>
Nothing new here.
worker_synth.js
// import the modules
import {
audioBufferToWav,
Sequencer,
WorkerSynthesizer
} from "../../src/index.ts";
import { EXAMPLE_SOUND_BANK_PATH } from "../examples_common.js";
// load the sound bank
fetch(EXAMPLE_SOUND_BANK_PATH).then(async (response) => {
// load the sound bank into an array buffer
let sfBuffer = await response.arrayBuffer();
document.getElementById("message").innerText =
"Sound bank has been loaded!";
// create the context and add audio worklet
const context = new AudioContext();
await WorkerSynthesizer.registerPlaybackWorklet(context);
// create the worker
const worker = new Worker(
new URL("worker_synth_worker.js", import.meta.url)
);
// create the synthesizer and bind it to the worker
const synth = new WorkerSynthesizer(
context,
worker.postMessage.bind(worker)
);
worker.onmessage = (ev) => synth.handleWorkerMessage(ev.data);
// add a button for rendering the audio
document.getElementById("render").onclick = async () => {
// Render audio with a simple progress tracking function
const outputBuffer = await synth.renderAudio(44100, {
progressCallback: (progress, stage) => {
document.getElementById("message").innerText =
`Rendering ${Math.floor(progress * 100)}% Stage: ${stage}`;
}
});
document.getElementById("message").innerText = "Complete!";
// convert the buffer to a wave file and create URL for it
const wavFile = audioBufferToWav(outputBuffer[0]);
const fileURL = URL.createObjectURL(wavFile);
// create an audio element and add it
const audio = document.createElement("audio");
audio.controls = true;
audio.src = fileURL;
document
.getElementsByClassName("example_content")[0]
.appendChild(audio);
};
// add a button for saving the SF2 file
document.getElementById("save_sf2").onclick = async () => {
const outputBuffer = await synth.writeSF2({
trim: true,
bankID: "main",
progressFunction: (args) => {
document.getElementById("message").innerText =
`Saving sample "${args.sampleName}" (${args.sampleIndex} out of ${args.sampleCount})...`;
}
});
document.getElementById("message").innerText = "Complete!";
const blob = new Blob([outputBuffer.binary]);
const fileURL = URL.createObjectURL(blob);
// add an anchor for downloading the file
const a = document.createElement("a");
a.href = fileURL;
a.download = `${outputBuffer.fileName}.sf2`;
a.textContent = "Download SF2";
document.getElementsByClassName("example_content")[0].appendChild(a);
a.click();
};
// add a button for saving the DLS file
document.getElementById("save_dls").onclick = async () => {
const outputBuffer = await synth.writeDLS({
trim: true,
bankID: "main",
progressFunction: (args) => {
document.getElementById("message").innerText =
`Saving sample "${args.sampleName}" (${args.sampleIndex} out of ${args.sampleCount})...`;
}
});
document.getElementById("message").innerText = "Complete!";
const blob = new Blob([outputBuffer.binary]);
const fileURL = URL.createObjectURL(blob);
// add an anchor for downloading the file
const a = document.createElement("a");
a.href = fileURL;
a.download = `${outputBuffer.fileName}.dls`;
a.textContent = "Download DLS";
document.getElementsByClassName("example_content")[0].appendChild(a);
a.click();
};
// add a button for saving the RMIDI file
document.getElementById("save_rmi").onclick = async () => {
const outputBuffer = await synth.writeRMIDI({
trim: true,
bankID: "main",
progressFunction: (args) => {
document.getElementById("message").innerText =
`Saving sample "${args.sampleName}" (${args.sampleIndex} out of ${args.sampleCount})...`;
}
});
document.getElementById("message").innerText = "Complete!";
const blob = new Blob([outputBuffer]);
const fileURL = URL.createObjectURL(blob);
// add an anchor for downloading the file
const a = document.createElement("a");
a.href = fileURL;
a.download = `${seq.midiData.getName()}.rmi`;
a.textContent = "Download RMIDI";
document.getElementsByClassName("example_content")[0].appendChild(a);
a.click();
};
// the rest of the code works the same
synth.connect(context.destination);
await synth.isReady;
await synth.soundBankManager.addSoundBank(sfBuffer, "main");
let seq = new Sequencer(synth);
// add an event listener for the file inout
document
.getElementById("midi_input")
.addEventListener("change", async (event) => {
// check if any files are added
const target = /** @type {HTMLInputElement}*/ event.target;
if (target.files.length < 1) {
return;
}
// resume the context if paused
await context.resume();
// parse all the files
const parsedSongs = [];
for (let file of target.files) {
const buffer = await file.arrayBuffer();
parsedSongs.push({
binary: buffer, // binary: the binary data of the file
fileName: file.name // fileName: the fallback name if the MIDI doesn't have one. Here we set it to the file name
});
}
seq.loadNewSongList(parsedSongs); // load the song list
seq.play(); // play the midi
// make the slider move with the song
let slider = document.getElementById("progress");
setInterval(() => {
// slider ranges from 0 to 1000
slider.value = (seq.currentTime / seq.duration) * 1000;
}, 100);
// on song change, show the name
seq.eventHandler.addEvent(
"songChange",
"example-time-change",
(e) => {
document.getElementById("message").innerText =
"Now playing: " + e.getName();
}
); // make sure to add a unique id!
// add time adjustment
slider.onchange = () => {
// calculate the time
seq.currentTime = (slider.value / 1000) * seq.duration; // switch the time (the sequencer adjusts automatically)
};
// add button controls
document.getElementById("previous").onclick = () => {
seq.songIndex--; // go back by one song
};
// on pause click
document.getElementById("pause").onclick = () => {
if (seq.paused) {
document.getElementById("pause").innerText = "Pause";
seq.play(); // resume
} else {
document.getElementById("pause").innerText = "Resume";
seq.pause(); // pause
}
};
document.getElementById("next").onclick = () => {
seq.songIndex++; // go to the next song
};
});
});
Note how we have to create our own worker and pass its postMessage
bound to the Worker to the WorkerSynthesizer. We can also make use of the convenient renderAudio
method which renders the current sequence. Other than that, the code is identical.
Now let’s take a look at the worker itself:
worker_synth_worker.js
// this code is called in the worker
import { WorkerSynthesizerCore } from "../../src/index.js";
/**
* @type {WorkerSynthesizerCore}
*/
let workerSynthCore;
onmessage = (e) => {
if (e.ports[0]) {
workerSynthCore = new WorkerSynthesizerCore(
e.data,
e.ports[0],
postMessage.bind(this)
);
} else {
void workerSynthCore.handleMessage(e.data);
}
};
Since this is a simple example, we just forward the data to the worker, but it allows us to intercept the messages when needed, and to send our own.