There are two similar libraries: spessasynth_lib and spessasynth_core:
spessasynth_core is the main library that contains all MIDI, SF2,DLS parsing and synthesis engine. It can run in any JS environment.
spessasynth_lib builds on top of spessasynth_core,
relying on WebAudioAPI and WebMIDIAPI
to add easy-to-use wrappers for the SpessaSynthProcessor and SpessaSynthSequencer.
spessasynth_core provides very raw access to the audio data, outputting float PCM samples.
These samples can then be sent to speakers, saved somewhere or processed, for example in an AudioWorklet's process method.
constbufferSize=128;while(true){sequencer.processTick();constsamplesL=newFloat32Array(bufferSize);constsamplesR=newFloat32Array(bufferSize);processor.process(samplesL,samplesR);// process the audio here}
This loop processes the sequencer before rendering the audio to the buffers.
Also keep in mind that the buffer size should not be larger than 256,
as the process function calculates the envelopes and LFOs once,
so buffer size represents the shortest amount of time between those changes.
// divisible by 128constoutL=newFloat32Array(2048);constoutR=newFloat32Array(2048);// 2048 / 128 = 16;for(leti=0;i<16;i++){// start rendering at a given offset and render 128 samplesprocessor.process(outL,outR,i*128,128);}
import*asfsfrom"node:fs/promises";import{audioToWav,BasicMIDI,SoundBankLoader,SpessaLog,SpessaSynthProcessor,SpessaSynthSequencer}from"../src";// Process argumentsconstargs=process.argv.slice(2);if(args.length!==3){console.info("Usage: tsx index.ts <soundbank path> <midi path> <wav output path>");process.exit();}// Read MIDI and sound bankconstsf=awaitfs.readFile(args[0]);constmid=awaitfs.readFile(args[1]);// Parse the MIDI and sound bankconstmidi=BasicMIDI.fromArrayBuffer(mid.buffer);constsoundBank=SoundBankLoader.fromArrayBuffer(sf.buffer);// Initialize the synthesizerconstsampleRate=48_000;constsynth=newSpessaSynthProcessor(sampleRate,{eventsEnabled:false});synth.soundBankManager.addSoundBank(soundBank,"main");awaitsynth.processorInitialized;// Enable verbose information during renderSpessaLog.setLogLevel(true,true,true);// Enable uncapped voice countsynth.setSystemParameter("autoAllocateVoices",true);// Initialize the sequencerconstseq=newSpessaSynthSequencer(synth);seq.loadNewSongList([midi]);seq.play();// Prepare the output buffersconstsampleCount=Math.ceil(sampleRate*(midi.duration+2));constoutLeft=newFloat32Array(sampleCount);constoutRight=newFloat32Array(sampleCount);conststart=performance.now();letfilledSamples=0;// Note: buffer size is recommended to be very small, as this is the interval between modulator updates and LFO updatesconstBUFFER_SIZE=128;leti=0;constdurationRounded=Math.floor(seq.midiData!.duration*100)/100;while(filledSamples<sampleCount){// Process sequencerseq.processTick();// RenderconstbufferSize=Math.min(BUFFER_SIZE,sampleCount-filledSamples);synth.process(outLeft,outRight,filledSamples,bufferSize);filledSamples+=bufferSize;i++;// Log progressif(i%100===0){console.info("Rendered",Math.floor(seq.currentTime*100)/100,"/",durationRounded);}}constrendered=Math.floor(performance.now()-start);console.info("Rendered in",rendered,`ms (${Math.floor(((midi.duration*1000)/rendered)*100)/100}x)`);constwave=audioToWav([outLeft,outRight],sampleRate);awaitfs.writeFile(args[2],newUint8Array(wave));console.info(`File written to ${args[2]}`);