Skip to content

spessasus/spessasynth_core

Folders and files

NameName
Last commit message
Last commit date

Latest commit

 

History

18 Commits
 
 
 
 
 
 
 
 
 
 
 
 

Repository files navigation

spessasynth_core

A SoundFont2 synthesizer library, made for use with node.js. A fork of SpessaSynth.

Jump to the API reference

npm install --save spessasynth_core

Looking for a browser version? Try SpessaSynth.

Features:

  • 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

Example: Render a MIDI file to a .wav file

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));
    });
});

Example 2: play a MIDI file to speakers

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();
    });
});

API reference

Contents

Importing the library

spessasynth_core is an es6 package.

// es6
import {Synthesizer} from "spessasynth_core"
// commonjs
import("spessasynth_core").then(core => {
    // use core.Synthesizer
})

Synthesizer

The main synth module.

Initialization

const synth = new Synthesizer(soundFontBuffer, sampleRate, blockSize)
  • soundFontBuffer - a Buffer or ArrayBufferLike, 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.

sf3supportReady

A promise that gets resolved when the vorbis decoder is ready. You must await it if you use sf3 soundfonts.

await synth.sf3supportReady;

render

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.

noteOn

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.

noteOff

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)

stopAllChannels

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.

programChange

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;

pitchWheel

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.

I highly recommend this article for more info.

systemExclusive

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.

setMainVolume

Sets the main volume of the synthesizer.

synth.setMainVolume(volume);
  • volume - the volume, ranges from 0 to 1.

setMasterPan

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.

lockController

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.

muteChannel

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.

transposeAllChannels

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.

controllerChange

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.

resetAllControllers

Resets all controllers and programs to their default values. Also resets the system.

synth.resetAllControllers();

addNewChannel

Adds a new channel.

synth.addNewChannel();

reloadSoundfont

Changes the soundfont of a Synthesizer's instance.

synth.reloadSoundFont(soundFontBuffer);
  • soundFont - the soundfont to change to, an ArrayBuffer instance of the file.

setDrums

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.

Accesing controller values

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.

Sequencer

Initialization

const sequencer = new Sequencer(synthesizer);
  • synthesizer - a Synthesizer instance to play to.

loadNewSongList

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.

play

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 to false;

pause

Pauses the playback of the sequence.

sequencer.pause();

stop

Stops the playback of the sequence. Currently only used internally by the pause function.

sequencer.stop();

nextSong

Plays the next song in the list.

sequencer.nextSong();

previousSong

Plays the previous song in the list.

sequencer.previousSong();

paused

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!");
}

loop

Boolean that controls if the sequencer loops.

sequencer.loop = false; // the playback will stop after reaching the end

currentTime

Property used for changing and reading the current playback time.

get

Returns the current playback time in seconds.

console.log("The sequences is playing for"+sequencer.currentTime+" seconds.");

set

Sets the current playback time. Calls stop and then play internally.

sequencer.currentTime = 0; // go to the start

duration

Length of the track in seconds. Equivalent of Audio.duration;

console.log(`The track lasts for ${sequencer.duration} seconds!`);

MIDI

See MIDI on SpessaSynth wiki

SoundFont2

See SoundFont2 on SpessaSynth wiki

Logging

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 to true for synth.noteOn