This example shows how to use the new WorkerSynthesizer class introduced in SpessaSynth 4.0. This example adapts the Advanced Example.
<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="render_split">Render this song (separate channels)</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.
// Import the modules
import {
audioBufferToWav,
type BasicSynthesizerReturnMessage,
Sequencer,
WorkerSynthesizer
} from "../../src";
import { EXAMPLE_SOUND_BANK_PATH } from "../examples_common";
// Load the sound bank
const response = await fetch(EXAMPLE_SOUND_BANK_PATH);
// Load the sound bank into an array buffer
const sfBuffer = await response.arrayBuffer();
document.querySelector("#message")!.textContent = "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.addEventListener(
"message",
(event: MessageEvent<BasicSynthesizerReturnMessage[]>) =>
synth.handleWorkerMessage(event.data)
);
// Create an audio element for a rendered buffer
function addAudioElement(buffer: AudioBuffer, title: string) {
const wavFile = audioBufferToWav(buffer);
const fileURL = URL.createObjectURL(wavFile);
const audio = document.createElement("audio");
audio.controls = true;
audio.src = fileURL;
const text = document.createElement("span");
text.textContent = title;
const parent = document.querySelector(".example_content")!;
parent.append(text);
parent.append(audio);
}
// Render with a simple progress tracking function
function renderProgress(progress: number, stage: number) {
const message = `Rendering ${Math.floor(progress * 100)}% Stage: ${stage}`;
document.querySelector("#message")!.textContent = message;
console.info(message);
}
// Add a button for rendering the audio
document.querySelector("#render")!.addEventListener("click", async () => {
const outputBuffer = await synth.renderAudio(44_100, {
progressCallback: renderProgress
});
document.querySelector("#message")!.textContent = "Complete!";
addAudioElement(outputBuffer, "Rendered audio: ");
});
// Add a button for rendering the audio with separate channels
document.querySelector("#render_split")!.addEventListener("click", async () => {
const rendered = await synth.renderAudioSplit(44_100, {
progressCallback: renderProgress
});
document.querySelector("#message")!.textContent = "Complete!";
for (let index = 0; index < rendered.visual.length; index++) {
addAudioElement(rendered.visual[index], `Channel ${index + 1}`);
}
addAudioElement(rendered.output, "Output");
});
// Add a button for saving the SF2 file
document.querySelector("#save_sf2")!.addEventListener("click", async () => {
const outputBuffer = await synth.writeSF2({
trim: true,
bankID: "main",
progressFunction: (progress) => {
document.querySelector("#message")!.textContent =
`Saving SF2... (${progress * 100}%)`;
}
});
document.querySelector("#message")!.textContent = "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.querySelector(".example_content")!.append(a);
a.click();
});
// Add a button for saving the DLS file
document.querySelector("#save_dls")!.addEventListener("click", async () => {
const outputBuffer = await synth.writeDLS({
trim: true,
bankID: "main",
progressFunction: (progress) => {
document.querySelector("#message")!.textContent =
`Saving DLS... (${progress * 100}%)`;
}
});
document.querySelector("#message")!.textContent = "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.querySelector(".example_content")!.append(a);
a.click();
});
// Add a button for saving the RMIDI file
document.querySelector("#save_rmi")!.addEventListener("click", async () => {
const outputBuffer = await synth.writeRMIDI({
trim: true,
bankID: "main",
progressFunction: (progress) => {
document.querySelector("#message")!.textContent =
`Saving RMIDI... (${progress * 100}%)`;
}
});
document.querySelector("#message")!.textContent = "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.querySelector(".example_content")!.append(a);
a.click();
});
// The rest of the code works the same
synth.connect(context.destination);
await synth.isReady;
await synth.soundBankManager.addSoundBank(sfBuffer, "main");
const seq = new Sequencer(synth);
// Add an event listener for the file input
document
.querySelector("#midi_input")!
.addEventListener("change", async (event) => {
// Check if any files are added
const target = event.target as HTMLInputElement;
const files = target.files;
if (files === null || files.length === 0) {
return;
}
// Resume the context if paused
await context.resume();
// Parse all the files
const parsedSongs: { binary: ArrayBuffer; fileName: string }[] = [];
for (const file of 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
const slider = document.querySelector<HTMLInputElement>("#progress")!;
setInterval(() => {
// Slider ranges from 0 to 1000
slider.value = String((seq.currentTime / seq.duration) * 1000);
}, 100);
// On song change, show the name
seq.eventHandler.addEvent(
"songChange",
"example-time-change",
(event) => {
document.querySelector("#message")!.textContent =
"Now playing: " + event.midiData.getName();
}
); // Make sure to add a unique id!
// Add time adjustment
slider.addEventListener("change", () => {
// Calculate the time
seq.currentTime = (Number(slider.value) / 1000) * seq.duration; // Switch the time (the sequencer adjusts automatically)
});
// Add button controls
document.querySelector("#previous")!.addEventListener("click", () => {
seq.songIndex--; // Go back by one song
});
// On pause click
document.querySelector("#pause")!.addEventListener("click", () => {
if (seq.paused) {
document.querySelector("#pause")!.textContent = "Pause";
seq.play(); // Resume
} else {
document.querySelector("#pause")!.textContent = "Resume";
seq.pause(); // Pause
}
});
document.querySelector("#next")!.addEventListener("click", () => {
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 WorkerSynthesizer.renderAudio and WorkerSynthesizer.renderAudioSplit methods,
which render the current sequence directly in the worker.
Other than that, the code is identical.
Now let's take a look at the worker itself:
// This code is called in the worker
import {
type BasicSynthesizerMessage,
type SynthCoreConfig,
WorkerSynthesizerCore
} from "../../src";
let workerSynthCore: WorkerSynthesizerCore;
onmessage = (event) => {
if (event.ports[0]) {
workerSynthCore = new WorkerSynthesizerCore(
event.data as SynthCoreConfig,
event.ports[0],
postMessage.bind(
globalThis
) as unknown as typeof Worker.prototype.postMessage
);
} else {
void workerSynthCore.handleMessage(
event.data as BasicSynthesizerMessage
);
}
};
Since this is a simple example, the worker just forwards the messages to the WorkerSynthesizerCore, but this setup allows us to intercept the messages when needed, and to send our own.