paulrosen / abcjs

javascript for rendering abc music notation
Other
1.94k stars 285 forks source link

Help changing player attributes dynamically. #1050

Open abalter opened 2 months ago

abalter commented 2 months 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 2 months ago

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

ZipBrandon commented 2 weeks ago

This seems to do nothing to change the instrument that actually plays or any of the other settings.

paulrosen commented 1 week ago

The editor does the renderAbc internally, so you don't need to listen for changes. So the basic setup is described here: https://paulrosen.github.io/abcjs/interactive/interactive-editor.html#constructor

You can set up the synth by passing the parameters into that. Then when your control changes you can update:

const editor = new ABCJS.Editor("abc", abcjsParams)
// after something changes
editor.paramChanged(abcjsParams)

I'm not sure what you mean by "alter the reference pitch". Do you mean transpose, for instance for clarinet? You can do that. See the parameter "midiTranspose" here: https://paulrosen.github.io/abcjs/audio/synthesized-sound.html#settune-visualobj-useraction-audioparams

(I know the API isn't particularly clear - sorry about that, but it grew over years when handling different use cases.)