diff --git a/metronome.html b/metronome.html new file mode 100644 index 0000000..41ae93c --- /dev/null +++ b/metronome.html @@ -0,0 +1,1056 @@ + + + + + Session Tempo + + + + +
+

Rob's Metronome

+

A metronome that remembers the tune

+ +
+
120
+
beats per minute
+ + + +
+ + + +
+ +
+ + +
+ + +
+ +
+ +

Tuner

+
+

Tap a string to play its tone. Tap again to stop.

+ +
+ +

Chords

+ +
+ + + +
+ +

+
+ +
+ +
0 st
+ +
+ +

+
+ +
+ +

Your tunes

+
+
Loading…
+
+ +
+ + +
+
+ + + +
+ +
+
+ + + + diff --git a/robs_metronome.html b/robs_metronome.html index 66cf287..bbda734 100644 --- a/robs_metronome.html +++ b/robs_metronome.html @@ -312,6 +312,84 @@ color: var(--ink-soft); margin-top: 12px; } + + .chord-controls { + display: flex; + justify-content: center; + gap: 12px; + flex-wrap: wrap; + margin-bottom: 18px; + } + .chord-controls label { + display: flex; + flex-direction: column; + gap: 4px; + font-size: 0.78rem; + color: var(--ink-soft); + } + .chord-controls select { + font-family: "Work Sans"; + font-size: 0.9rem; + color: var(--ink); + background: var(--panel); + border: 1px solid var(--rule); + border-radius: 4px; + padding: 6px 8px; + } + + .chord-block-label { + font-size: 0.78rem; + color: var(--ink-soft); + margin: 0 0 8px; + text-align: center; + } + .chord-row { + display: grid; + grid-template-columns: repeat(7, 1fr); + gap: 6px; + margin-bottom: 8px; + } + .chord-box { + display: flex; + flex-direction: column; + align-items: center; + gap: 2px; + background: var(--panel); + border: 1px solid var(--rule); + border-radius: 6px; + padding: 10px 2px; + } + .chord-box .chord-name { + font-family: "Fraunces", serif; + font-weight: 600; + font-size: 1.05rem; + color: var(--walnut); + } + .chord-box .chord-roman { + font-size: 0.68rem; + color: var(--ink-soft); + } + .chord-row.transposed .chord-box { + background: var(--brass-light); + border-color: var(--brass); + } + + .transpose-controls { + display: flex; + align-items: center; + justify-content: center; + gap: 12px; + margin: 18px 0 20px; + } + .transpose-amount { + font-family: "Fraunces", serif; + font-weight: 600; + font-size: 1.1rem; + color: var(--walnut); + min-width: 72px; + text-align: center; + font-variant-numeric: tabular-nums; + } @@ -361,6 +439,40 @@
+

Chords

+ +
+ + + +
+ +

+
+ +
+ +
0 st
+ +
+ +

+
+ +
+

Your tunes

Loading…
@@ -583,6 +695,207 @@ tunerGrid.appendChild(btn); }); + // ---------- Diatonic chords / transposer ---------- + const NOTE_NAMES = [ + "C", + "C#", + "D", + "D#", + "E", + "F", + "F#", + "G", + "G#", + "A", + "A#", + "B", + ]; + const FLAT_NAMES = [ + "C", + "Db", + "D", + "Eb", + "E", + "F", + "Gb", + "G", + "Ab", + "A", + "Bb", + "B", + ]; + + const SCALES = { + Ionian: { label: "Major (Ionian)", steps: [0, 2, 4, 5, 7, 9, 11] }, + Dorian: { label: "Dorian", steps: [0, 2, 3, 5, 7, 9, 10] }, + Phrygian: { label: "Phrygian", steps: [0, 1, 3, 5, 7, 8, 10] }, + Lydian: { label: "Lydian", steps: [0, 2, 4, 6, 7, 9, 11] }, + Mixolydian: { label: "Mixolydian", steps: [0, 2, 4, 5, 7, 9, 10] }, + Aeolian: { label: "Minor (Aeolian)", steps: [0, 2, 3, 5, 7, 8, 10] }, + Locrian: { label: "Locrian", steps: [0, 1, 3, 5, 6, 8, 10] }, + }; + + let spellingMode = "sharp"; + + function noteName(index) { + const table = spellingMode === "flat" ? FLAT_NAMES : NOTE_NAMES; + return table[((index % 12) + 12) % 12]; + } + + // Build the 7 diatonic triads for a given root index (0-11) and scale key. + function buildDiatonicChords(rootIndex, scaleKey) { + const steps = SCALES[scaleKey].steps; + const scaleNotes = steps.map((s) => (rootIndex + s) % 12); + + return scaleNotes.map((deg, i) => { + const root = scaleNotes[i]; + const third = scaleNotes[(i + 2) % 7]; + const fifth = scaleNotes[(i + 4) % 7]; + + const rootToThird = (third - root + 12) % 12; + const thirdToFifth = (fifth - third + 12) % 12; + + let quality, romanBase; + if (rootToThird === 4 && thirdToFifth === 3) { + quality = ""; + romanBase = "maj"; + } else if (rootToThird === 3 && thirdToFifth === 4) { + quality = "m"; + romanBase = "min"; + } else if (rootToThird === 3 && thirdToFifth === 3) { + quality = "dim"; + romanBase = "dim"; + } else if (rootToThird === 4 && thirdToFifth === 4) { + quality = "aug"; + romanBase = "aug"; + } else { + quality = ""; + romanBase = "maj"; + } + + const numerals = ["I", "II", "III", "IV", "V", "VI", "VII"]; + let roman = numerals[i]; + if (romanBase === "min") roman = roman.toLowerCase(); + if (romanBase === "dim") roman = roman.toLowerCase() + "°"; + if (romanBase === "aug") roman = roman + "+"; + + return { + name: noteName(root) + quality, + roman: roman, + }; + }); + } + + const keyRootSelect = document.getElementById("keyRoot"); + const scaleTypeSelect = document.getElementById("scaleType"); + const spellingModeSelect = document.getElementById("spellingMode"); + const originalChordRow = document.getElementById("originalChordRow"); + const transposedChordRow = document.getElementById("transposedChordRow"); + const originalKeyLabel = document.getElementById("originalKeyLabel"); + const transposedKeyLabel = document.getElementById("transposedKeyLabel"); + const transposeAmountEl = document.getElementById("transposeAmount"); + const transposeDownBtn = document.getElementById("transposeDown"); + const transposeUpBtn = document.getElementById("transposeUp"); + + function populateKeyOptions() { + const prevValue = keyRootSelect.value; + keyRootSelect.innerHTML = ""; + NOTE_NAMES.forEach((_, i) => { + const opt = document.createElement("option"); + opt.value = i; + opt.textContent = noteName(i); + keyRootSelect.appendChild(opt); + }); + if (prevValue !== "") keyRootSelect.value = prevValue; + } + + populateKeyOptions(); + + Object.keys(SCALES).forEach((key) => { + const opt = document.createElement("option"); + opt.value = key; + opt.textContent = SCALES[key].label; + scaleTypeSelect.appendChild(opt); + }); + + let chordRootIndex = 0; // C + let chordScaleKey = "Ionian"; + let transposeSemitones = 0; + + function renderChordRow(container, chords) { + container.innerHTML = ""; + chords.forEach((c) => { + const box = document.createElement("div"); + box.className = "chord-box"; + const name = document.createElement("div"); + name.className = "chord-name"; + name.textContent = c.name; + const roman = document.createElement("div"); + roman.className = "chord-roman"; + roman.textContent = c.roman; + box.appendChild(name); + box.appendChild(roman); + container.appendChild(box); + }); + } + + function renderChords() { + const scaleLabel = SCALES[chordScaleKey].label; + + const originalChords = buildDiatonicChords( + chordRootIndex, + chordScaleKey, + ); + originalKeyLabel.textContent = + noteName(chordRootIndex) + " " + scaleLabel; + renderChordRow(originalChordRow, originalChords); + + const transposedRoot = chordRootIndex + transposeSemitones; + const transposedChords = buildDiatonicChords( + transposedRoot, + chordScaleKey, + ); + transposedKeyLabel.textContent = + noteName(transposedRoot) + + " " + + scaleLabel + + " (" + + (transposeSemitones > 0 ? "+" : "") + + transposeSemitones + + " semitones)"; + renderChordRow(transposedChordRow, transposedChords); + + transposeAmountEl.textContent = + (transposeSemitones > 0 ? "+" : "") + transposeSemitones + " st"; + } + + keyRootSelect.addEventListener("change", (e) => { + chordRootIndex = parseInt(e.target.value, 10); + renderChords(); + }); + + scaleTypeSelect.addEventListener("change", (e) => { + chordScaleKey = e.target.value; + renderChords(); + }); + + spellingModeSelect.addEventListener("change", (e) => { + spellingMode = e.target.value; + populateKeyOptions(); + keyRootSelect.value = chordRootIndex; + renderChords(); + }); + + transposeDownBtn.addEventListener("click", () => { + transposeSemitones = Math.max(-24, transposeSemitones - 1); + renderChords(); + }); + + transposeUpBtn.addEventListener("click", () => { + transposeSemitones = Math.min(24, transposeSemitones + 1); + renderChords(); + }); + // ---------- Tune storage (browser-local cache + JSON export/import) ---------- const tuneList = document.getElementById("tuneList"); const emptyMsg = document.getElementById("emptyMsg"); @@ -737,6 +1050,7 @@ // init setBpm(120); loadTunes(); + renderChords(); diff --git a/tunes.json b/tunes.json index fc2b9ac..4556205 100644 --- a/tunes.json +++ b/tunes.json @@ -13,5 +13,15 @@ "id": "mu3deuwo", "name": "Scales in 1/4 & 1/16", "bpm": 95 + }, + { + "id": "mu4pewvu", + "name": "scales in triplets", + "bpm": 130 + }, + { + "id": "mu4q2q2l", + "name": "RH Carl Patterns", + "bpm": 84 } ] \ No newline at end of file