Created
September 16, 2024 07:46
-
-
Save abalter/10ddda721aa21e6e6ee4819adfb8c04b to your computer and use it in GitHub Desktop.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
<!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"><<</button> | |
<button type="button" id="${name}-small-dec"><</button> | |
<input type="range" id="${name}" name="${name}" min="${low}" max="${high}" value="${defaultValue}" ${initial_toggle ? "" : "disabled"}> | |
<button type="button" id="${name}-small-inc">></button> | |
<button type="button" id="${name}-big-inc">>></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"><</button> | |
<button type="button" id="key-up">></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