spessasynth_lib documentation
    Preparing search index...

    Adding Visualizations Example

    See this demo live

    Let's spice up our demo a bit! This is a very simplified version of the SpessaSynth Web App visualization, but feel free to expand upon it to create something amazing!

    <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 />
    <canvas height="500" id="canvas" width="1000"></canvas>
    <table style="border-collapse: collapse">
    <tbody>
    <tr id="keyboard"></tr>
    </tbody>
    </table>

    <!-- note the type="module" -->
    <script src="visualizer.js" type="module"></script>

    We need to add the canvas and our "keyboard".

    We use two functions of the API to achieve this:

    synth.connectChannel(audioNode, channel);
    

    This connects the AnalyserNodes to the synthesizer, allowing visualizations for specific MIDI channels.

    synth.eventHandler.addEvent("noteOn", "my-note-on-listener", (event) => {
    /*...*/
    });

    SynthEventHandler allows us to hook up events (in this case, note on and note off to visualize key presses). The second argument is a unique ID, which can later be used to remove the listener.

    // Import the modules
    import { Sequencer, WorkletSynthesizer } from "../../src";
    import {
    EXAMPLE_SOUND_BANK_PATH,
    EXAMPLE_WORKLET_PATH
    } from "../examples_common";

    // Add different colors to channels!
    const channelColors = [
    "rgba(255, 99, 71, 1)", // Tomato
    "rgba(255, 165, 0, 1)", // Orange
    "rgba(255, 215, 0, 1)", // Gold
    "rgba(50, 205, 50, 1)", // Limegreen
    "rgba(60, 179, 113, 1)", // Mediumseagreen
    "rgba(0, 128, 0, 1)", // Green
    "rgba(0, 191, 255, 1)", // Deepskyblue
    "rgba(65, 105, 225, 1)", // Royalblue
    "rgba(138, 43, 226, 1)", // Blueviolet
    "rgba(50, 120, 125, 1)", // Percussion color
    "rgba(255, 0, 255, 1)", // Magenta
    "rgba(255, 20, 147, 1)", // Deeppink
    "rgba(218, 112, 214, 1)", // Orchid
    "rgba(240, 128, 128, 1)", // Lightcoral
    "rgba(255, 192, 203, 1)", // Pink
    "rgba(255, 255, 0, 1)" // Yellow
    ];

    // Adjust this to your liking
    const VISUALIZER_GAIN = 2;

    // Create a keyboard
    const keyboard = document.querySelector("#keyboard")!;
    // Create an array of 128 keys
    const keys: HTMLTableCellElement[] = [];
    for (let index = 0; index < 128; index++) {
    const key = document.createElement("td");
    key.style.width = "5px";
    key.style.height = "50px";
    key.style.border = "solid black 1px";
    keyboard.append(key);
    keys.push(key);
    }

    // Load the sound bank
    const response = await fetch(EXAMPLE_SOUND_BANK_PATH);
    // Load the sound bank into an array buffer
    const sfFile = await response.arrayBuffer();
    document.querySelector("#message")!.textContent = "Sound bank has been loaded!";

    // Create the context and add audio worklet
    const context = new AudioContext();
    await context.audioWorklet.addModule(EXAMPLE_WORKLET_PATH);
    // Create the synthesizer
    const synth = new WorkletSynthesizer(context);
    synth.connect(context.destination);
    await synth.soundBankManager.addSoundBank(sfFile, "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 input = event.target as HTMLInputElement;
    // Check if any files are added
    const file = input.files?.[0];
    if (!file) {
    return;
    }
    await context.resume();
    const midiFile = await file.arrayBuffer(); // Convert the file to array buffer
    seq.loadNewSongList([{ binary: midiFile, fileName: file.name }]);
    seq.play();

    const canvas = document.querySelector<HTMLCanvasElement>("#canvas")!; // Get canvas
    const drawingContext = canvas.getContext("2d")!;
    // Create the AnalyserNodes for the channels
    const analysers: AnalyserNode[] = [];
    for (let index = 0; index < 16; index++) {
    // Create analyzer and connect it
    const analyzer = context.createAnalyser();
    analysers.push(analyzer);
    synth.connectChannel(analyzer, index);
    }

    // Render analyzers in a 4x4 grid
    function render() {
    // Clear the rectangle
    drawingContext.clearRect(0, 0, canvas.width, canvas.height);
    for (const [channelIndex, analyser] of analysers.entries()) {
    // Calculate positions
    const width = canvas.width / 4;
    const height = canvas.height / 4;
    const step = width / analyser.frequencyBinCount;
    const x = width * (channelIndex % 4); // ChannelIndex % 4 gives us 0 to 2 range
    const y = height * Math.floor(channelIndex / 4) + height / 2;

    // Get the data from analyzer
    const waveData = new Float32Array(analyser.frequencyBinCount);
    analyser.getFloatTimeDomainData(waveData);
    // Set the color
    drawingContext.strokeStyle =
    channelColors[channelIndex % channelColors.length];
    // Draw the waveform
    drawingContext.moveTo(x, y);
    drawingContext.beginPath();
    for (const [index, waveDatum] of waveData.entries()) {
    drawingContext.lineTo(
    x + step * index,
    y + waveDatum * height * VISUALIZER_GAIN
    );
    }
    drawingContext.stroke();
    }

    // Draw again
    requestAnimationFrame(render);
    }

    render();

    // Add listeners to show keys being pressed
    // Add note on listener
    synth.eventHandler.addEvent(
    "noteOn",
    "demo-keyboard-note-on",
    (event) => {
    keys[event.midiNote].style.background =
    channelColors[event.channel % channelColors.length];
    }
    );

    // Add note off listener
    synth.eventHandler.addEvent(
    "noteOff",
    "demo-keyboard-note-off",
    (event) => {
    keys[event.midiNote].style.background = "";
    }
    );

    // Add stop-all listener
    synth.eventHandler.addEvent("stopAll", "demo-keyboard-stop-all", () => {
    for (const key of keys) key.style.background = "";
    });
    });

    Quite cool, isn't it?