diff --git a/metronome.html b/metronome.html
new file mode 100644
index 0000000..41ae93c
--- /dev/null
+++ b/metronome.html
@@ -0,0 +1,1056 @@
+
+
+
+
+
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
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
Your tunes
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
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 @@
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();