RolandJansen/intermix.js

View on GitHub
demo/index.html

Summary

Maintainability
Test Coverage
<html>

<head>
    <title>Sequencer Demo</title>
    <link href="demo.css" rel="stylesheet">
</head>

<body>
    <h1>intermix Test App</h1>
    <fieldset>
        <legend>Step Sequencer</legend>
        <div id="sequencer"></div>
        <div class="transport">
            <button id="start" class="demo-button">Start</button>
            <button id="stop" class="demo-button">Stop</button>
            <button id="reset" class="demo-button">Reset</button>
            <input id="bpm" type="number" value="120">
        </div>
    </fieldset>
    <div class="synth-and-routing">
        <fieldset>
            <legend>Synth Parameter</legend>
            <div>
                <input type="range" id="synth-volume" name="synth-volume" min="0" max="127" value="127">
                <label for="synth-volume">Volume</label>
            </div>
            <div>
                <input type="range" id="synth-attack" name="synth-attack" min="0" max="1" step="0.01" value="0.1">
                <label for="synth-attack">Filter Attack</label>
            </div>
            <div>
                <input type="range" id="synth-decay" name="synth-decay" min="0" max="1" step="0.01" value="0.1">
                <label for="synth-decay">Filter Decay</label>
            </div>
        </fieldset>
        <fieldset>
            <legend>Routing</legend>
            <div class="routing">
                <label for="snare-out">Snare goes to</label>
                <select name="snare-out" id="snare-out">
                    <option value="destination">Soundcard</option>
                    <option value="delay">Delay FX</option>
                </select>
            </div>
            <div class="routing">
                <label for="hihat-out">Hihat goes to</label>
                <select name="hihat-out" id="hihat-out">
                    <option value="destination">Soundcard</option>
                    <option value="delay">Delay FX</option>
                </select>
            </div>
            <div class="routing">
                <label for="bass-out">Bass goes to</label>
                <select name="bass-out" id="bass-out">
                    <option value="destination">Soundcard</option>
                    <option value="delay">Delay FX</option>
                </select>
            </div>
        </fieldset>
    </div>
    <fieldset>
        <legend>Debugging</legend>
        <button id="resumeAudioContext">resume audio context</button>
        <button id="getstate">get current state</button>
    </fieldset>
    <footer>
        Drumsounds sampled from a <a target="_blank" href="http://www.vintagesynth.com/casio/sk1.php">Casio SK-1</a>.
    </footer>
</body>
<script>
    // redux is reading the 'process' global var
    // that doesn't exists in the browser
    const process = {
        env: {
            NODE_ENV: 'debug',
        }
    };
</script>
<script type="module">
    import * as intermix from "/dist/esm/intermix.esm.js";
    import StepSequencer from "./StepSequencer.js";

    const stepmodel = [
        [1, 0, 0, 0, 0, 0, 0, 0, 1, 0, 1, 0, 0, 0, 0, 0],
        [0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0],
        [0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0],
        [0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0],
        [1, 1, 0, 1, 0, 0, 1, 1, 0, 1, 1, 1, 0, 1, 1, 0],
    ];
    const synthNotes = [41, 41, 41, 29, 41, 41, 41, 42, 41, 30, 42, 44, 41, 42, 30, 41];
    const rownames = ["Bassdrum", "Snare", "Hihat Closed", "Hihat Open", "Bass"];

    const stepSequencer = new StepSequencer({
        parent: "sequencer",
        model: stepmodel,
        synthNotes,
        rownames,
    });

    const samples = [
        "/demo/assets/Casio SK-1/skkick.wav",
        "/demo/assets/Casio SK-1/sksnare.wav",
        "/demo/assets/Casio SK-1/skclhat.wav",
        "/demo/assets/Casio SK-1/skophat.wav",
    ]

    const instrumentUID = [];
    const seqPartUID = [];

    const sequencerUID = intermix.addPlugin('Sequencer');
    const delayUID = intermix.addPlugin('Delay');
    const synthUID = intermix.addPlugin('Synth');

    const seqActionCreators = initSequencer();
    initSamplers();
    const synthActionCreators = initSynth();
    initSequencerParts();

    /**
     * The callback that should be passed to the sequencer animation
     */
    function nextStep(step) {
        if (step % 4 === 0) {
            stepSequencer.next();
        }
    }

    /**
     * create the sequencer and initialize the UI animation
     */
    function initSequencer() {
        const actionCreators = intermix.getActionCreators(sequencerUID);
        actionCreators.animate(nextStep);
        setLoop(actionCreators);
        return actionCreators;
    }

    function setLoop(actionCreators) {
        actionCreators.loopEnd(63);
        actionCreators.loopActivate();
    }

    /**
     * create sequencer parts and add them to the score.
     */
    function initSequencerParts() {
        createSeqParts();
        addPartsToScore();
        populateSeqParts();
    }

    function createSeqParts() {
        const partCount = stepmodel.length;
        for (let i = 0; i < partCount; i++) {
            seqPartUID.push(intermix.addSeqPart());
        }
    }

    function addPartsToScore() {
        seqPartUID.forEach((seqPartId, index) => {
            seqActionCreators.addToScore([seqPartId, instrumentUID[index]])
        })
    }

    /**
     * map step-model to sequencer parts
     */
    function populateSeqParts() {
        stepmodel.forEach((seqRow, rowIndex) => {
            const seqPartId = seqPartUID[rowIndex];
            const seqPartActionCreators = intermix.getActionCreators(seqPartId);

            seqRow.forEach((seqStep, stepIndex) => {
                if (seqStep !== 0) {
                    let stepNoteNumber = 49;
                    if (rowIndex === stepmodel.length - 1) {
                        stepNoteNumber = synthNotes[stepIndex];
                    }
                    seqPartActionCreators.activeStep(stepIndex);
                    seqPartActionCreators.addNote(["note", stepNoteNumber, 1, 0.125, 0.0]);
                }
            })
        })
    }

    /**
     * create sampler plugins and a sequencer part for each sound
     */
    function initSamplers() {
        createSamplers();
        mapSamplesToSamplers();
    }

    function createSamplers() {
        for (let i = 0; i < 4; i++) {
            instrumentUID.push(intermix.addPlugin('Sampler'))
        }
    }

    /**
     * add a sample to each sampler
     */
    function mapSamplesToSamplers() {
        samples.forEach((sample, index) => {
            addAudioDataToSampler(instrumentUID[index], sample);
        })
    }

    /**
     * Load audio files, decode it and send it to the sampler instances
     */
    function addAudioDataToSampler(samplerUID, rawData) {
        intermix.loadFileFromServer(rawData).then((response) => {
            const ac = intermix.getAudioContext();
            return ac.decodeAudioData(response);
        }).then((decoded) => {
            const actionCreators = intermix.getActionCreators(samplerUID)
            actionCreators.audioData(decoded)
        })
    }

    function initSynth() {
        instrumentUID.push(synthUID);
        const actionCreators = intermix.getActionCreators(synthUID);

        const volume = 30;
        const attack = 0.2;
        const volumeSlider = document.getElementById("synth-volume");
        const attackSlider = document.getElementById("synth-attack");
        const decaySlider = document.getElementById("synth-decay");

        actionCreators.volume(volume);
        actionCreators.envAttack(["Envelope Attack", attack, 0]);
        volumeSlider.valueAsNumber = volume;
        attackSlider.valueAsNumber = attack;

        return actionCreators;
    }

    /**
     * This function will be called when a cell gets clicked.
     * It is used to modify sequencer parts
     */
    stepSequencer.onStepChange = (rowNumber, cellNumber, cellActive) => {
        const seqPartId = seqPartUID[rowNumber];
        const actionCreators = intermix.getActionCreators(seqPartId);
        actionCreators.activeStep(cellNumber);

        let noteNumber = 49;
        if (parseInt(rowNumber) === stepmodel.length - 1) { // synth has to be treated different
            const input = document.getElementById("note-input_" + cellNumber);
            noteNumber = input.valueAsNumber;
        }

        if (cellActive) {
            stepmodel[rowNumber][cellNumber] = noteNumber;
            actionCreators.addNote(["note", noteNumber, 1, 0.125, 0.0]);
        } else {
            noteNumber = stepmodel[rowNumber][cellNumber];
            actionCreators.removeNote(["note", noteNumber, 1, 0.125, 0.0]);
        }
    }

    /**
     * This function will be called when a note number gets changed.
     * Modifies the synthNotes array and the corresponding sequencer part.
     */
    stepSequencer.onNoteChange = (stepNumber, noteNumber) => {
        console.log("stepNumber: " + stepNumber);
        console.log("noteNumber: " + noteNumber);
        const oldNoteNumber = synthNotes[stepNumber];
        synthNotes[stepNumber] = noteNumber;
        const actionCreators = intermix.getActionCreators(seqPartUID[seqPartUID.length - 1]);

        actionCreators.activeStep(stepNumber);
        actionCreators.removeNote(["note", oldNoteNumber, 1, 1, 0]);
        actionCreators.addNote(["note", noteNumber, 1, 0.125, 0.0]);
    }

    //===============
    // event handler
    //===============
    document.getElementById("start").addEventListener("click", () => {
        seqActionCreators.start();
    })

    document.getElementById("stop").addEventListener("click", () => {
        seqActionCreators.stop();
    })

    document.getElementById("reset").addEventListener("click", () => {
        // seqActionCreators.stop();
        seqActionCreators.reset();
        stepSequencer.reset();
    })

    document.getElementById("bpm").addEventListener("change", (event) => {
        seqActionCreators.BPM(event.target.valueAsNumber);
    })

    document.getElementById("synth-volume").addEventListener("change", (event) => {
        synthActionCreators.volume(event.target.valueAsNumber);
    })

    document.getElementById("synth-attack").addEventListener("change", (event) => {
        const attack = event.target.valueAsNumber;
        const value = ["Envelope Attack", attack, 0];
        synthActionCreators.envAttack(value);
    })

    document.getElementById("synth-decay").addEventListener("change", (event) => {
        const decay = event.target.valueAsNumber;
        const value = ["Envelope Decay", decay, 0];
        synthActionCreators.envDecay(value);
    })

    document.getElementById("snare-out").addEventListener("change", (event) => {
        if (event.target.value === "delay") {
            wireInstrument(instrumentUID[1], delayUID);
        } else {
            wireInstrument(instrumentUID[1], "destination");
        }
    });

    document.getElementById("hihat-out").addEventListener("change", (event) => {
        if (event.target.value === "delay") {
            wireInstrument(instrumentUID[2], delayUID);
        } else {
            wireInstrument(instrumentUID[2], "destination");
        }
    });

    document.getElementById("bass-out").addEventListener("change", (event) => {
        if (event.target.value === "delay") {
            wireInstrument(instrumentUID[4], delayUID);
        } else {
            wireInstrument(instrumentUID[4], "destination");
        }
    });

    document.getElementById('resumeAudioContext').addEventListener('click', () => {
        intermix.resumeAudioContext();
        console.log("Audio Context Resumed");
    });

    document.getElementById("getstate").addEventListener('click', () => {
        console.log(intermix.getState());
    })

    function wireInstrument(sourceID, targetID) {
        const outNode = [sourceID, 0];
        const inNode = [targetID, 0];
        intermix.connectPlugins(outNode, inNode);
    }

</script>

</html>