Direct Audio Engine Access - spessasus/spessasynth_lib GitHub Wiki
Caution
This is for the advanced users only.
Important
This demo only runs well in Firefox. Chrome seems to have trouble with AudioBufferSourceNodes. It is recommended to use a simple playback Audio Worklet such as this one.
Sometimes, it is necessary for the script to have direct access to the synthesizer's audio engine for various reasons.
While one can use spesasynth_core
directly, this will require implementing the audio effects manually.
This page is intended to show how to use both spessasynth_core
and spessasynth_lib
to maintain full feature set of the Synthesizer
class,
while rendering in the main thread and having the full access to the audio engine.
spessasynth_lib
exposes both audio processors, allowing us to connect them to the synthesizer directly.
A simple audio loop that achieves this is as follows:
- Create the
Float32Array
buffers for the dry, chorus and reverb outputs. - Perform any custom tasks needed and then render the audio
- Send the processed audio to playback nodes, like a custom audio worklet or
AudioBufferSourceNode
s - The node/s play back to the target node and the effect processors (three
BufferSource
s for three nodes) - The effects are connected to the target node as well, so they process the audio as needed
Below is an example that shows the list of active voices currently playing,
which is something that cannot be achieved with just the Synthetizer
class.
Nothing special here.
<label for='soundfont_input'>Upload the soundfont.</label>
<input accept='.sf2, .sf3, .dls' id='soundfont_input' type='file'>
<label for='midi_input'>Select the MIDI file</label>
<input accept='.midi, .mid, .rmi, .smf' id='midi_input' type='file'>
<h2>Voice list</h2>
<div id='voice_list' style='display: flex; width: 100%; justify-content: space-evenly'></div>
<!-- note the type="module" -->
<script src='main_thread_rendering.js' type='module'></script>
The audio loop presented in this script is very similar to the one shown above:
- Make sure that the synthesizer is not too far ahead
- Create the buffers
- Process the MIDI playback and render audio
- Create buffer sources and play back the rendered chunks through them
There is another loop that displays all the voices. It is independent of the audio loop.
import { loadSoundFont, MIDI, SpessaSynthProcessor, SpessaSynthSequencer } from "spessasynth_core";
import { FancyChorus } from "../../src/synthetizer/audio_effects/fancy_chorus.js";
import { getReverbProcessor } from "../../src/synthetizer/audio_effects/reverb.js";
// create a new audio context
const context = new AudioContext({
sampleRate: 44100
});
// wait for the user to upload the soundfont
document.getElementById("soundfont_input").onchange = async e =>
{
// if no file is selected, exit early
const files = e.target?.files;
if (!files[0])
{
return;
}
// resume the audio context so audio processing can begin
await context.resume();
// read the uploaded file into an ArrayBuffer
const fontBuffer = await files[0].arrayBuffer();
// create an instance of the synthesizer and load it with the sound bank
const synth = new SpessaSynthProcessor(44100);
synth.soundfontManager.reloadManager(loadSoundFont(fontBuffer));
// initialize the sequencer for MIDI playback
const seq = new SpessaSynthSequencer(synth);
// initialize the audio effects and connect them to the destination
const chorusProcessor = new FancyChorus(context.destination);
const reverbProcessor = getReverbProcessor(context).conv;
reverbProcessor.connect(context.destination);
// THE MAIN AUDIO RENDERING LOOP IS HERE
setInterval(() =>
{
// get the synthesizer’s internal current time
const synTime = synth.currentSynthTime;
// if the synth time is significantly ahead of the context time, skip rendering
// (wait for the context to catch up)
if (synTime > context.currentTime + 0.1)
{
return;
}
// create empty stereo buffers for dry signal, reverb, and chorus outputs
const BUFFER_SIZE = 512;
const output = [new Float32Array(BUFFER_SIZE), new Float32Array(BUFFER_SIZE)];
const reverb = [new Float32Array(BUFFER_SIZE), new Float32Array(BUFFER_SIZE)];
const chorus = [new Float32Array(BUFFER_SIZE), new Float32Array(BUFFER_SIZE)];
// play back the MIDI file
seq.processTick();
// render the next chunk of audio into the provided buffers
synth.renderAudio(output, reverb, chorus);
// function to play a given stereo buffer to a specified output node
const playAudio = (arr, output) =>
{
// create an AudioBuffer to hold the sample data
const outBuffer = new AudioBuffer({
numberOfChannels: 2,
length: 512,
sampleRate: 44100
});
// copy the left and right channel data into the audio buffer
outBuffer.copyToChannel(arr[0], 0);
outBuffer.copyToChannel(arr[1], 1);
// create a source node from the buffer and connect it to the desired output
const source = new AudioBufferSourceNode(context, {
buffer: outBuffer
});
source.connect(output);
// schedule the buffer to play at the synth’s current time
source.start(synTime);
};
// play the dry audio to the main output
playAudio(output, context.destination);
// play the reverb signal through the reverb effect chain
playAudio(reverb, reverbProcessor);
// play the chorus signal through the chorus processor’s input
playAudio(chorus, chorusProcessor.input);
});
// list all the voices currently playing
const list = document.getElementById("voice_list");
/**
* @type {HTMLPreElement[]}
* create and store a <pre> element for each of the 16 MIDI channels
* each one will be used to display information about active voices on a given channel
*/
const voiceListElements = [];
for (let i = 0; i < 16; i++)
{
const el = document.createElement("pre");
voiceListElements.push(el);
list.appendChild(el);
}
// set up an interval to regularly update the voice display for each channel
setInterval(() =>
{
// loop through each MIDI channel in the synth
synth.midiAudioChannels.forEach((c, chanNum) =>
{
// get the corresponding element for this channel
const channelList = voiceListElements[chanNum];
// start building the display string with the channel number
let text = `Channel ${chanNum + 1}:\n`;
// append a line for each currently active voice with its MIDI note
c.voices.forEach(v =>
{
text += `note: ${v.midiNote}\n`;
});
// update the DOM with the new voice info
channelList.textContent = text;
});
}, 100);
// set up the MIDI player
document.getElementById("midi_input").onchange = async e =>
{
// verify if the file is really there
if (!e.target?.files[0])
{
return;
}
// parse and play the file
const file = e.target.files[0];
const midi = new MIDI(await file.arrayBuffer());
seq.loadNewSongList([midi]);
seq.play();
};
};