A SoundFont2 synthesizer library, made for use with node.js. A fork of SpessaSynth.
npm install --save spessasynth_core
Looking for a browser version? Try SpessaSynth.
- SoundFont2 support (both modulators and generators)
- SoundFont3 support (vorbis compressed sf2)
- GS, XG, GM2, GM1 system exclusive support
- NRPN, RPN support
- Integrated sequencer
- Additional custom modulators
- Multi-port MIDIs support (more than 16 channels)
- No SoundFont size limit
- No dependencies
const fs = require('fs');
// spessasynth_core is an es6 module
import("spessasynth_core").then(core => {
// usage: node test.js <sf path> <midi path> <output path>
const [,, soundfontName, midiName, outputName] = process.argv;
// read the input files
const soundfont = fs.readFileSync(soundfontName);
const mid = new core.MIDI(fs.readFileSync(midiName))
// initialize synth and sequencer
const synth = new core.Synthesizer(soundfont, 44100);
const seq = new core.Sequencer(synth);
// load new song and disable the loop
seq.loadNewSongList([mid]);
seq.loop = false;
// calculate length and allocate buffers
const lengthSamples = mid.duration * 44100;
const outLeft = new Float32Array(lengthSamples);
const outRight = new Float32Array(lengthSamples);
// wait for sf3 support to load and render
synth.sf3supportReady.then(() => {
// note: this discards reverb and chorus outputs!
synth.render([outLeft, outRight]);
// write output data
const wav = core.rawDataToWav(44100, outLeft, outRight);
fs.writeFileSync(outputName, Buffer.from(wav));
});
});
via npm package speaker
const fs = require('fs');
const Speaker = require("speaker");
const SAMPLE_RATE = 44100; // hertz
const BLOCK_SIZE = 128; // samples
// usage: node test.js <sf path> <midi path>
import("spessasynth_core").then(core => {
// Disable logging
core.SpessaSynthLogging(false, false, false, false);
// Read arguments and load the input files
const [,, soundfontName, midiName] = process.argv;
const soundfont = fs.readFileSync(soundfontName);
const mid = new core.MIDI(fs.readFileSync(midiName)); // parse the midi
// Initialize synth and sequencer
const synth = new core.Synthesizer(soundfont, SAMPLE_RATE, BLOCK_SIZE);
const seq = new core.Sequencer(synth);
// Load new song and disable the loop
seq.loadNewSongList([mid]);
seq.loop = false;
// make the program stop after the sequence is finished
const time = seq.duration + 1
let isPlaying = true;
setTimeout(() => isPlaying = false, time * 1000);
console.log(`Playing "${mid.midiName || "<unnamed song>"}"`);
// Calculate length and allocate buffers
const outLeft = new Float32Array(BLOCK_SIZE);
const outRight = new Float32Array(BLOCK_SIZE);
// Wait for sf3 support to load and render
synth.sf3supportReady.then(() => {
const speakerOut = new Speaker({
channels: 2,
bitDepth: 16,
sampleRate: SAMPLE_RATE,
});
function render()
{
// Render audio samples from the synthesizer
synth.render([outLeft, outRight]);
// Prepare buffer for WAV output
const wavData = new Int16Array(outLeft.length * 2); // 2 channels
// Interleave audio data
let offset = 0;
for (let i = 0; i < outLeft.length; i++)
{
// Float ranges from -1 to 1, int16 ranges from -32768 to 32767, convert it here
const sampleL = Math.max(-1, Math.min(1, outLeft[i])) * 32767;
const sampleR = Math.max(-1, Math.min(1, outRight[i])) * 32767;
// Interleave data: L, R, L, R, etc...
wavData[offset++] = sampleL;
wavData[offset++] = sampleR;
}
// Write WAV data to speaker
speakerOut.write(wavData);
if(isPlaying)
{
setImmediate(render);
}
else
{
process.exit(0);
}
}
render();
});
});
spessasynth_core is an es6 package.
// es6
import {Synthesizer} from "spessasynth_core"
// commonjs
import("spessasynth_core").then(core => {
// use core.Synthesizer
})
The main synth module.
const synth = new Synthesizer(soundFontBuffer, sampleRate, blockSize)
- soundFontBuffer - a
Buffer
orArrayBufferLike
, represents the soundfont file. - sampleRate - number, the output sample rate in hertz.
- blockSize - optional, a number. Sets the interval of the synth updating parameters like the sequencer tick processing and modulation envelope. Default value is 128, and it's recommended to leave it as the default.
A promise that gets resolved when the vorbis decoder is ready. You must await it if you use sf3 soundfonts.
await synth.sf3supportReady;
Synthesizes audio the output buffers
synth.render(outputChannels, reverbOutputChannels, chorusOutputChannels);
- outputChannels - two
Float32Arrays
that get filled with the audio data. Left is the left channel and right is the right channel. Can be any length. (except zero) - reverbOutputChannels - two
Float32Arrays
that get filled with the unprocessed audio data for reverb processing. Left is the left channel and right is the right channel. Can be undefined. - reverbOutputChannels - two
Float32Arrays
that get filled with the unprocessed audio data for chorus processing. Left is the left channel and right is the right channel. Can be undefined.
All arrays must be the same length.
Plays the given note.
synth.noteOn(channel, midiNote, velocity, enableDebugging);
- channel - the MIDI channel to use. Usually ranges from 0 to 15, but it depends on the channel count.
- midiNote - the note to play. Ranges from 0 to 127.
- velocity - controls how loud the note is. 127 is normal loudness and 1 is the quietest. Note that velocity of 0 has the same effect as using
noteOff
. Ranges from 0 to 127. - enableDebugging - boolean, used only for debugging. When
true
, the console will print out tables of the soundfont generator data used to play the note.
Stops the given note.
synth.noteOff(channel, midiNote);
- channel - the MIDI channel to use. Usually ranges from 0 to 15, but it depends on the channel count.
- midiNote - the note to play. Ranges from 0 to 127.
To stop a note instantly, use
synth.killNote
(takes the same arguments)
Stops all notes. Equivalent of MIDI "panic".
synth.stopAllChannels(force);
- force -
boolean
, if true, ignores the release time and stops everything instantly. Defaults to false.
To stop all notes on a specific channel, use
synth.stopAll(channel, force)
. channel is the channel number.
Changes the preset for the given channel.
synth.programChange(channel, programNumber);
- channel - the MIDI channel to change. Usually ranges from 0 to 15, but it depends on the channel count.
- programNumber - the MIDI program number to use. Ranges from 0 to 127. To use other banks, go to controllerChange.
To lock the preset (prevent MIDI file from changing it) use
synth.workletProcessorChannels[channel].lockPreset = true;
Changes the channel's pitch, including the currently playing notes.
synth.pitchWheel(channel, MSB, LSB);
- channel - the MIDI channel to use. Usually ranges from 0 to 15, but it depends on the channel count.
- MSB and LSB. 7-bit numbers that form a 14-bit pitch bend value.
Handles a MIDI System Exclusive message.
synth.systemExclusive(messageData);
- message data - Uint8Array, the message byte data Excluding the 0xF0 byte!
Refer to this table for the list of supported System Exclusives.
Sets the main volume of the synthesizer.
synth.setMainVolume(volume);
- volume - the volume, ranges from 0 to 1.
Sets the master panning of the synthesizer.
synth.setMasterPan(pan);
- pan - ranges from -1 to 1, -1 is left, 0 is middle, 1 is right.
Causes the given midi channel to ignore controller messages for the given controller number.
synth.lockController(channel, controllerNumber, isLocked);
- channel - the channel to lock. Usually ranges from 0 to 15, but it depends on the channel count.
- controllerNumber - the MIDI CC to lock. Ranges from 0 to 127.
- isLocked - boolean, if true then locked, if false then unlocked.
Mutes or unmutes a given channel
synth.muteChannel(channel, isMuted);
- channel - the channel to mute/unmute. Usually ranges from 0 to 15, but it depends on the channel count.
- isMuted - if the channel should be muted. boolean.
Transposes the synth up or down in semitones. Floating point values can be used for more precise tuning.
synth.transposeAllChannels(semitones);
- semitones - the amount of semitones to transpose the synth by. Can be positive or negative or zero. Zero resets the pitch.
Sets a given MIDI controller to a given value.
synth.controllerChange(channel, controllerNumber, controllerValue);
- channel - the MIDI channel to use. Usually ranges from 0 to 15, but it depends on the channel count.
- controllerName - the MIDI CC number. Refer to this table for the list of controllers supported by default.
- controllerValue - the value to set the given controller to. Ranges from 0 to 127.
Note that theoreticallly all controllers are supported as it depends on the SoundFont's modulators.
Resets all controllers and programs to their default values. Also resets the system.
synth.resetAllControllers();
Adds a new channel.
synth.addNewChannel();
Changes the soundfont of a Synthesizer's instance.
synth.reloadSoundFont(soundFontBuffer);
- soundFont - the soundfont to change to, an
ArrayBuffer
instance of the file.
Sets the given channel to a drum channel.
synth.setDrums(channel, isDrum);
- channel - the channel to change. Usually ranges from 0 to 15, but it depends on the channel count.
- isDrum -
boolean
indicates if the channel should be a drum channel.
use synth.workletProcessorChannels
to get the current values. A single channel is defined as follows:
/**
* @typedef {Object} WorkletProcessorChannel
* @property {Int16Array} midiControllers - array of MIDI controller values
* @property {boolean[]} lockedControllers - array indicating if a controller is locked
* @property {boolean} holdPedal - indicates whether the hold pedal is active
* @property {boolean} drumChannel - indicates whether the channel is a drum channel
*
* @property {Preset} preset - the channel's preset
* @property {boolean} lockPreset - indicates whether the program on the channel is locked
*
* @property {boolean} lockVibrato - indicates whether the custom vibrato is locked
* @property {Object} channelVibrato - vibrato settings for the channel
* @property {number} channelVibrato.depth - depth of the vibrato effect (cents)
* @property {number} channelVibrato.delay - delay before the vibrato effect starts (seconds)
* @property {number} channelVibrato.rate - rate of the vibrato oscillation (Hz)
* @property {boolean} isMuted - indicates whether the channel is muted
* @property {WorkletVoice[]} voices - array of voices currently active on the channel
* @property {WorkletVoice[]} sustainedVoices - array of voices that are sustained on the channel
*/
Note: this definition is stripped from internal values.
const sequencer = new Sequencer(synthesizer);
- synthesizer - a
Synthesizer
instance to play to.
Loads a new song list.
sequencer.loadNewSongList(parsedMidis);
- parsedMidis - an array of
MIDI
instances representing the songs to play. If there's only one, the loop will be enabled.
Starts playing the sequence. If the sequence was paused, it won't change any controllers, but if it wasn't (ex. the time was changed) then it will go through all the controller changes from the start before playing. This function does NOT modify the current playback time!
sequencer.play(resetTime);
- resetTime - boolean, if set to
true
then the playback will start from 0. Defaults tofalse
;
Pauses the playback of the sequence.
sequencer.pause();
Stops the playback of the sequence. Currently only used internally by the pause
function.
sequencer.stop();
Plays the next song in the list.
sequencer.nextSong();
Plays the previous song in the list.
sequencer.previousSong();
Read-only boolean, indicating that if the sequencer's playback is paused.
if(sequencer.paused)
{
console.log("Sequencer paused!");
}
else
{
console.log("Sequencer playing or stopped!");
}
Boolean that controls if the sequencer loops.
sequencer.loop = false; // the playback will stop after reaching the end
Property used for changing and reading the current playback time.
Returns the current playback time in seconds.
console.log("The sequences is playing for"+sequencer.currentTime+" seconds.");
Sets the current playback time. Calls stop
and then play
internally.
sequencer.currentTime = 0; // go to the start
Length of the track in seconds. Equivalent of Audio.duration
;
console.log(`The track lasts for ${sequencer.duration} seconds!`);
See SoundFont2 on SpessaSynth wiki
By default, SpessaSynth prints out a lot of stuff to console. Here's how you can disable it:
// import (or require) here
SpessaSynthLogging(enableInfo, enableWarning, enableGroup, enableTable);
All the input variables are booleans corresponding to the things SpessaSynth logs.
- Info - all general info such as parsing soundfonts, midi files, RPN changes, etc.
- Warnings - all messages unrecognized by the synthesizer, other warnings
- group - the groups for parsing the soundfont and midi files.
- table - the debug table
when enableDebugging
is set totrue
forsynth.noteOn