Created
April 7, 2025 19:39
-
-
Save ggorlen/fb77ee7856ad5f9e79cd3cfa568b6e39 to your computer and use it in GitHub Desktop.
Guitar tuner
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="color-scheme" content="dark light"> | |
<link rel="icon" href="data:image/svg+xml,<svg xmlns='http://www.w3.org/2000/svg' width='48' height='48' viewBox='0 0 16 16'><text x='0' y='14'>🎸</text></svg>" /> | |
<title>Guitar Tuner</title> | |
<style> | |
body { | |
font-family: sans-serif; | |
padding: 2rem; | |
} | |
h1 { | |
margin-bottom: 1rem; | |
} | |
.note-buttons { | |
margin-top: 1rem; | |
} | |
button { | |
margin: 0.3rem; | |
padding: 0.5rem 1rem; | |
font-size: 1rem; | |
} | |
button.active { | |
background-color: #4caf50; | |
color: white; | |
} | |
select { | |
font-size: 1rem; | |
padding: 0.4rem; | |
} | |
</style> | |
</head> | |
<body> | |
<!-- https://chatgpt.com/c/67f40eff-996c-8012-981f-6c5082a85dd2 --> | |
<h1>Guitar Tuner</h1> | |
<label for="tuning-select">Tuning:</label> | |
<select id="tuning-select" onchange="loadTuning()"> | |
<option value="standard">Standard E</option> | |
<option value="dropD">Drop D</option> | |
<option value="halfStepDown">Half Step Down</option> | |
<option value="fullStepDown">Full Step Down</option> | |
<option value="openD">Open D</option> | |
</select> | |
<div class="note-buttons" id="note-buttons"></div> | |
<script> | |
let audioContext; | |
let currentOscillator = null; | |
let currentGain = null; | |
let currentButton = null; | |
const noteMap = { | |
C: 0, 'C#': 1, Db: 1, D: 2, 'D#': 3, Eb: 3, E: 4, F: 5, | |
'F#': 6, Gb: 6, G: 7, 'G#': 8, Ab: 8, A: 9, 'A#': 10, Bb: 10, B: 11 | |
}; | |
function noteToFrequency(note) { | |
const match = note.match(/^([A-Ga-g#b]+)(-?\d+)$/); | |
if (!match) throw new Error(`Invalid note: ${note}`); | |
let [, pitch, octave] = match; | |
pitch = pitch.toUpperCase(); | |
octave = parseInt(octave, 10); | |
const semitoneOffset = noteMap[pitch]; | |
if (semitoneOffset === undefined) throw new Error(`Invalid pitch: ${pitch}`); | |
const noteNumber = (octave + 1) * 12 + semitoneOffset; | |
return 440 * Math.pow(2, (noteNumber - 69) / 12); | |
} | |
const tunings = { | |
standard: ['E2', 'A2', 'D3', 'G3', 'B3', 'E4'], | |
dropD: ['D2', 'A2', 'D3', 'G3', 'B3', 'E4'], | |
halfStepDown: ['Eb2', 'Ab2', 'Db3', 'Gb3', 'Bb3', 'Eb4'], | |
fullStepDown: ['D2', 'G2', 'C3', 'F3', 'A3', 'D4'], | |
openD: ['D2', 'A2', 'D3', 'F#3', 'A3', 'D4'] | |
}; | |
function stopCurrentTone() { | |
if (currentOscillator) { | |
currentOscillator.stop(); | |
currentOscillator.disconnect(); | |
currentGain.disconnect(); | |
currentOscillator = null; | |
currentGain = null; | |
} | |
if (currentButton) { | |
currentButton.classList.remove('active'); | |
currentButton = null; | |
} | |
} | |
function toggleTone(button, frequency) { | |
if (button === currentButton) { | |
stopCurrentTone(); | |
return; | |
} | |
stopCurrentTone(); | |
if (!audioContext) { | |
audioContext = new (window.AudioContext || window.webkitAudioContext)(); | |
} | |
const oscillator = audioContext.createOscillator(); | |
const gainNode = audioContext.createGain(); | |
oscillator.type = 'sine'; | |
oscillator.frequency.setValueAtTime(frequency, audioContext.currentTime); | |
oscillator.connect(gainNode); | |
gainNode.connect(audioContext.destination); | |
oscillator.start(); | |
currentOscillator = oscillator; | |
currentGain = gainNode; | |
currentButton = button; | |
button.classList.add('active'); | |
} | |
function loadTuning() { | |
stopCurrentTone(); | |
const tuningName = document.getElementById('tuning-select').value; | |
const tuningNotes = tunings[tuningName]; | |
const container = document.getElementById('note-buttons'); | |
container.innerHTML = ''; | |
tuningNotes.forEach(note => { | |
const btn = document.createElement('button'); | |
btn.textContent = note; | |
btn.onclick = () => { | |
const freq = noteToFrequency(note); | |
toggleTone(btn, freq); | |
}; | |
container.appendChild(btn); | |
}); | |
} | |
loadTuning(); | |
</script> | |
</body> | |
</html> |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment