paulrosen / abcjs

javascript for rendering abc music notation
Other
1.91k stars 281 forks source link

Help changing player attributes dynamically. #1050

Open abalter opened 1 week ago

abalter commented 1 week ago

I looked over the examples and while all of the kinds of things I want to do are included in one or more examples, the design patterns they use (how the render the ABC, how they set up the synth player, how the handle callbacks, etc.) are so different I just feel very confused.

I started with the simple example of editor and player. I added to the page a set of controls to edit tempo, swing, key (transposition), pitch, instrument, and chords. (It may not currently be possible to alter the reference pitch.) These controls are connected to a callback function called updatePlayerSettings that doesn't actually do anything other than print the values of the controls.

Could someone show me how I can actually alter the player in this callback?

<!DOCTYPE html>
<html lang="en">

<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>Tune Learning App with ABC Editor and Player</title>
    <link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/sakura.css/css/sakura.css" type="text/css">
    <link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/abcjs@6.4.3/abcjs-audio.min.css">
    <script src="https://cdn.jsdelivr.net/npm/abcjs@6.4.3/dist/abcjs-basic-min.min.js"></script>
</head>
<style>
    body {
        font-family: Arial, sans-serif;
        max-width: 800px;
        margin: 0 auto;
        padding: 20px;
    }

    .control-group {
        margin-bottom: 20px;
    }

    .slider-container {
        display: flex;
        align-items: center;
    }

    .slider-container input[type="range"] {
        flex-grow: 1;
        margin: 0 10px;
    }

    #abc-editor {
        width: 100%;
        height: 200px;
        font-family: monospace;
        margin-bottom: 20px;
    }

    #control-values {
        white-space: pre-wrap;
        font-family: monospace;
        background-color: #f0f0f0;
        padding: 10px;
        border-radius: 5px;
        color: black;
    }
</style>
<script src="abcjs-basic.js"></script>
</head>

<body>
    <h1>Tune Learning App</h1>

    <!-- ABC Editor -->
    <textarea id="abc-editor">X:1
T:Tune
M:4/4
L:1/8 
K:D 
|"D"DFAF|"G"GBdG|"A"cAEC|"D"D4 z4||</textarea>

    <!-- ABC Player -->
    <div id="paper"></div>
    <div id="audio"></div>

    <!-- Controls -->
    <form id="controls">
        <div id="swing-control"></div>
        <div id="tempo-control"></div>
        <div id="pitch-control"></div>
        <div id="octave-control"></div>
        <div id="key-control"></div>
        <div id="instrument-control"></div>
        <div id="chords-control" class="control-group">
            <label for="play-chords">Play Chords:</label>
            <input type="checkbox" id="play-chords" name="play-chords">
        </div>
    </form>

    <h2>Control Values:</h2>
    <div id="control-values"></div>

    <script>
        let synthControl;

        // Reusable function to create a slider+arrows+entry control
        function sliderControl(low, high, small_increment, large_increment, defaultValue, name, has_toggle, initial_toggle = false) {
            name = name.toLowerCase();
            const toggle = has_toggle ? `<input type="checkbox" id="${name}-toggle" name="${name}-toggle" ${initial_toggle ? "checked" : ""}>` : "";

            const sliderHTML = `
                <div class="control-group">
                    <label for="${name}">${name.charAt(0).toUpperCase() + name.slice(1)}</label>
                    <div class="slider-container">
                        ${toggle}
                        <button type="button" id="${name}-big-dec">&lt;&lt;</button>
                        <button type="button" id="${name}-small-dec">&lt;</button>
                        <input type="range" id="${name}" name="${name}" min="${low}" max="${high}" value="${defaultValue}" ${initial_toggle ? "" : "disabled"}>
                        <button type="button" id="${name}-small-inc">&gt;</button>
                        <button type="button" id="${name}-big-inc">&gt;&gt;</button>
                        <input type="number" id="${name}-value" min="${low}" max="${high}" value="${defaultValue}" ${initial_toggle ? "" : "disabled"}>
                    </div>
                </div>`;

            const tempDiv = document.createElement('div');
            tempDiv.innerHTML = sliderHTML.trim();
            return tempDiv.firstChild;
        }

        // Add key and instrument selection programmatically
        function addKeyControl() {
            const keys = ['C', 'C#', 'D', 'D#', 'E', 'F', 'F#', 'G', 'G#', 'A', 'A#', 'B'];
            const keyControl = document.createElement('div');
            keyControl.classList.add('control-group');
            keyControl.innerHTML = `
                <label for="key">Key:</label>
                <select id="key"></select>
                <button type="button" id="key-down">&lt;</button>
                <button type="button" id="key-up">&gt;</button>
            `;

            document.getElementById('key-control').appendChild(keyControl);

            const keySelect = document.getElementById('key');
            keys.forEach(key => {
                const option = document.createElement('option');
                option.value = key;
                option.textContent = key;
                keySelect.appendChild(option);
            });

            document.getElementById('key-down').addEventListener('click', function () {
                const currentIndex = keys.indexOf(keySelect.value);
                if (currentIndex > 0) {
                    keySelect.value = keys[currentIndex - 1];
                    updatePlayerSettings();
                }
            });

            document.getElementById('key-up').addEventListener('click', function () {
                const currentIndex = keys.indexOf(keySelect.value);
                if (currentIndex < keys.length - 1) {
                    keySelect.value = keys[currentIndex + 1];
                    updatePlayerSettings();
                }
            });
        }

        function addInstrumentControl() {
            const instruments = ['piano', 'violin', 'flute'];
            const instrumentControl = document.createElement('div');
            instrumentControl.classList.add('control-group');
            instrumentControl.innerHTML = `
                <label for="instrument">Instrument:</label>
                <select id="instrument"></select>
            `;

            document.getElementById('instrument-control').appendChild(instrumentControl);

            const instrumentSelect = document.getElementById('instrument');
            instruments.forEach(inst => {
                const option = document.createElement('option');
                option.value = inst;
                option.textContent = inst.charAt(0).toUpperCase() + inst.slice(1);
                instrumentSelect.appendChild(option);
            });

            instrumentSelect.addEventListener('change', updatePlayerSettings);
        }

        // Function to handle all increment and decrement actions
        function addIncrementDecrementEvents(name, slider, stepValues) {
            const { small, big } = stepValues;
            const min = parseInt(slider.min);
            const max = parseInt(slider.max);

            const updateValue = (step) => updateControlValue(name, step, min, max);

            document.getElementById(`${name}-big-dec`).addEventListener('click', () => updateValue(-big));
            document.getElementById(`${name}-small-dec`).addEventListener('click', () => updateValue(-small));
            document.getElementById(`${name}-small-inc`).addEventListener('click', () => updateValue(small));
            document.getElementById(`${name}-big-inc`).addEventListener('click', () => updateValue(big));
        }

        // Function to add event listeners to controls
        function addSliderEvents(name) {
            const slider = document.getElementById(name);
            const numberInput = document.getElementById(`${name}-value`);

            // Sync slider with number input
            slider.addEventListener('input', function () {
                numberInput.value = slider.value;
                updatePlayerSettings();
            });

            numberInput.addEventListener('change', function () {
                slider.value = numberInput.value;
                updatePlayerSettings();
            });

            // Increment and decrement buttons
            addIncrementDecrementEvents(name, slider, { small: 1, big: 10 });

            // Optional toggle handling
            const toggle = document.getElementById(`${name}-toggle`);
            if (toggle) {
                toggle.addEventListener('change', function () {
                    slider.disabled = !this.checked;
                    numberInput.disabled = !this.checked;
                    updatePlayerSettings();
                });
            }
        }

        // Add sliders to the form
        document.getElementById('swing-control').appendChild(sliderControl(0, 100, 1, 10, 0, 'Swing', true, false));
        document.getElementById('tempo-control').appendChild(sliderControl(60, 200, 1, 10, 120, 'Tempo', false, false));
        document.getElementById('pitch-control').appendChild(sliderControl(-100, 100, 1, 10, 0, 'Pitch', false, false));
        document.getElementById('octave-control').appendChild(sliderControl(-2, 2, 1, 2, 0, 'Octave', false, false));

        // Add key and instrument controls
        addKeyControl();
        addInstrumentControl();
        document.getElementById('play-chords').addEventListener('change', updatePlayerSettings);

        // Add events to the sliders
        ['swing', 'tempo', 'pitch', 'octave'].forEach(addSliderEvents);

        // Generic control value update function
        function updateControlValue(id, step, min, max) {
            const slider = document.getElementById(id);
            const numberInput = document.getElementById(`${id}-value`);
            let newValue = parseInt(slider.value) + step;
            newValue = Math.max(min, Math.min(max, newValue));
            slider.value = newValue;
            numberInput.value = newValue;
            updatePlayerSettings();
        }

        /*~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~*/
        /* Actual ABCJS Stuff */

        // Function to update player with values from the UI.
        // This currently does nothing other than print the values.
        function updatePlayerSettings() {
            const controlValues = {
                swing: document.getElementById('swing').value,
                tempo: document.getElementById('tempo').value,
                pitch: document.getElementById('pitch').value,
                octave: document.getElementById('octave').value,
                key: document.getElementById('key').value,
                instrument: document.getElementById('instrument').value,
                playChords: document.getElementById('play-chords').checked
            };
            document.getElementById('control-values').textContent = JSON.stringify(controlValues, null, 2);
        }

        // ABCJS rendering
        const abcEditor = document.getElementById('abc-editor');
        function renderAbc() {
            const abcOptions = { add_classes: true };
            const visualObj = ABCJS.renderAbc("paper", abcEditor.value, abcOptions)[0];
            if (ABCJS.synth.supportsAudio()) {
                if (!synthControl) {
                    synthControl = new ABCJS.synth.SynthController();
                    synthControl.load("#audio", null, { displayLoop: true, displayPlay: true });
                }
                synthControl.setTune(visualObj, false, { qpm: document.getElementById('tempo').value }).then(function () {
                    console.log("Audio loaded");
                }).catch(function (error) {
                    console.warn("Audio problem:", error);
                });
            }
        }

        // ABC editor change event
        abcEditor.addEventListener('input', renderAbc);

        // Initial ABC render
        renderAbc();

        // Initial update of control values
        updatePlayerSettings();
    </script>
</body>

</html>
abalter commented 1 week ago

https://gist.github.com/abalter/10ddda721aa21e6e6ee4819adfb8c04b