Skip to content

Instantly share code, notes, and snippets.

@abalter
Created September 16, 2024 07:46
Show Gist options
  • Save abalter/10ddda721aa21e6e6ee4819adfb8c04b to your computer and use it in GitHub Desktop.
Save abalter/10ddda721aa21e6e6ee4819adfb8c04b to your computer and use it in GitHub Desktop.
<!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>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment