|
|
|
|
@ -263,6 +263,55 @@
|
|
|
|
|
margin-top: 10px; |
|
|
|
|
min-height: 1em; |
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
.tuner-grid { |
|
|
|
|
display: grid; |
|
|
|
|
grid-template-columns: repeat(6, 1fr); |
|
|
|
|
gap: 8px; |
|
|
|
|
} |
|
|
|
|
.string-btn { |
|
|
|
|
display: flex; |
|
|
|
|
flex-direction: column; |
|
|
|
|
align-items: center; |
|
|
|
|
gap: 2px; |
|
|
|
|
background: var(--panel); |
|
|
|
|
border: 1px solid var(--rule); |
|
|
|
|
border-radius: 6px; |
|
|
|
|
padding: 12px 4px; |
|
|
|
|
cursor: pointer; |
|
|
|
|
font-family: "Work Sans"; |
|
|
|
|
color: var(--ink); |
|
|
|
|
transition: |
|
|
|
|
background 0.12s ease, |
|
|
|
|
border-color 0.12s ease; |
|
|
|
|
} |
|
|
|
|
.string-btn .note { |
|
|
|
|
font-family: "Fraunces", serif; |
|
|
|
|
font-size: 1.3rem; |
|
|
|
|
font-weight: 600; |
|
|
|
|
color: var(--walnut); |
|
|
|
|
} |
|
|
|
|
.string-btn .hz { |
|
|
|
|
font-size: 0.68rem; |
|
|
|
|
color: var(--ink-soft); |
|
|
|
|
font-variant-numeric: tabular-nums; |
|
|
|
|
} |
|
|
|
|
.string-btn:hover { |
|
|
|
|
border-color: var(--brass); |
|
|
|
|
} |
|
|
|
|
.string-btn.active { |
|
|
|
|
background: var(--brass-light); |
|
|
|
|
border-color: var(--brass); |
|
|
|
|
} |
|
|
|
|
.string-btn.active .note { |
|
|
|
|
color: var(--walnut); |
|
|
|
|
} |
|
|
|
|
.tuner-hint { |
|
|
|
|
text-align: center; |
|
|
|
|
font-size: 0.8rem; |
|
|
|
|
color: var(--ink-soft); |
|
|
|
|
margin-top: 12px; |
|
|
|
|
} |
|
|
|
|
</style> |
|
|
|
|
</head> |
|
|
|
|
<body> |
|
|
|
|
@ -304,6 +353,14 @@
|
|
|
|
|
|
|
|
|
|
<hr class="divider" /> |
|
|
|
|
|
|
|
|
|
<h2>Tuner</h2> |
|
|
|
|
<div class="tuner-grid" id="tunerGrid"></div> |
|
|
|
|
<p class="tuner-hint"> |
|
|
|
|
Tap a string to play its tone. Tap again to stop. |
|
|
|
|
</p> |
|
|
|
|
|
|
|
|
|
<hr class="divider" /> |
|
|
|
|
|
|
|
|
|
<h2>Your tunes</h2> |
|
|
|
|
<div class="tune-list" id="tuneList"> |
|
|
|
|
<div class="empty" id="emptyMsg">Loading…</div> |
|
|
|
|
@ -330,10 +387,8 @@
|
|
|
|
|
accept="application/json" |
|
|
|
|
style="display: none" |
|
|
|
|
/> |
|
|
|
|
<a href="https://tuner-online.com/#sounds" target="_blank"> |
|
|
|
|
<button>Guitar Tuner</button> |
|
|
|
|
</a> |
|
|
|
|
</div> |
|
|
|
|
|
|
|
|
|
<div class="status" id="statusMsg"></div> |
|
|
|
|
</div> |
|
|
|
|
|
|
|
|
|
@ -455,6 +510,79 @@
|
|
|
|
|
flashPad(false); |
|
|
|
|
}); |
|
|
|
|
|
|
|
|
|
// ---------- Guitar tuner ---------- |
|
|
|
|
const GUITAR_STRINGS = [ |
|
|
|
|
{ note: "E", octave: 2, freq: 82.41 }, |
|
|
|
|
{ note: "A", octave: 2, freq: 110.0 }, |
|
|
|
|
{ note: "D", octave: 3, freq: 146.83 }, |
|
|
|
|
{ note: "G", octave: 3, freq: 196.0 }, |
|
|
|
|
{ note: "B", octave: 3, freq: 246.94 }, |
|
|
|
|
{ note: "E", octave: 4, freq: 329.63 }, |
|
|
|
|
]; |
|
|
|
|
|
|
|
|
|
const tunerGrid = document.getElementById("tunerGrid"); |
|
|
|
|
let tuneOsc = null; |
|
|
|
|
let tuneGain = null; |
|
|
|
|
let activeStringBtn = null; |
|
|
|
|
|
|
|
|
|
function stopTunerTone() { |
|
|
|
|
if (tuneOsc) { |
|
|
|
|
const g = tuneGain; |
|
|
|
|
const now = audioCtx.currentTime; |
|
|
|
|
g.gain.cancelScheduledValues(now); |
|
|
|
|
g.gain.setValueAtTime(g.gain.value, now); |
|
|
|
|
g.gain.exponentialRampToValueAtTime(0.0001, now + 0.06); |
|
|
|
|
tuneOsc.stop(now + 0.08); |
|
|
|
|
} |
|
|
|
|
if (activeStringBtn) activeStringBtn.classList.remove("active"); |
|
|
|
|
tuneOsc = null; |
|
|
|
|
tuneGain = null; |
|
|
|
|
activeStringBtn = null; |
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
function playTunerTone(freq, btn) { |
|
|
|
|
ensureAudio(); |
|
|
|
|
if (audioCtx.state === "suspended") audioCtx.resume(); |
|
|
|
|
|
|
|
|
|
// Toggle off if this string is already sounding |
|
|
|
|
if (activeStringBtn === btn) { |
|
|
|
|
stopTunerTone(); |
|
|
|
|
return; |
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
stopTunerTone(); |
|
|
|
|
|
|
|
|
|
const osc = audioCtx.createOscillator(); |
|
|
|
|
const gain = audioCtx.createGain(); |
|
|
|
|
osc.type = "sine"; |
|
|
|
|
osc.frequency.value = freq; |
|
|
|
|
const now = audioCtx.currentTime; |
|
|
|
|
gain.gain.setValueAtTime(0.0001, now); |
|
|
|
|
gain.gain.exponentialRampToValueAtTime(0.28, now + 0.03); |
|
|
|
|
osc.connect(gain); |
|
|
|
|
gain.connect(audioCtx.destination); |
|
|
|
|
osc.start(); |
|
|
|
|
|
|
|
|
|
tuneOsc = osc; |
|
|
|
|
tuneGain = gain; |
|
|
|
|
activeStringBtn = btn; |
|
|
|
|
btn.classList.add("active"); |
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
GUITAR_STRINGS.forEach((s) => { |
|
|
|
|
const btn = document.createElement("button"); |
|
|
|
|
btn.className = "string-btn"; |
|
|
|
|
btn.innerHTML = |
|
|
|
|
'<span class="note">' + |
|
|
|
|
s.note + |
|
|
|
|
s.octave + |
|
|
|
|
'</span><span class="hz">' + |
|
|
|
|
s.freq.toFixed(2) + |
|
|
|
|
" Hz</span>"; |
|
|
|
|
btn.addEventListener("click", () => playTunerTone(s.freq, btn)); |
|
|
|
|
tunerGrid.appendChild(btn); |
|
|
|
|
}); |
|
|
|
|
|
|
|
|
|
// ---------- Tune storage (browser-local cache + JSON export/import) ---------- |
|
|
|
|
const tuneList = document.getElementById("tuneList"); |
|
|
|
|
const emptyMsg = document.getElementById("emptyMsg"); |
|
|
|
|
|