Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Help changing player attributes dynamically. #1050

Open
abalter opened this issue Sep 16, 2024 · 3 comments
Open

Help changing player attributes dynamically. #1050

abalter opened this issue Sep 16, 2024 · 3 comments

Comments

@abalter
Copy link

abalter commented Sep 16, 2024

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/[email protected]/abcjs-audio.min.css">
    <script src="https://cdn.jsdelivr.net/npm/[email protected]/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
Copy link
Author

abalter commented Sep 16, 2024

@ZipBrandon
Copy link

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

@paulrosen
Copy link
Owner

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.)

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

No branches or pull requests

3 participants