Browse Source

tweak

master
rdrew 1 day ago
parent
commit
319c8931df
  1. 520
      Robs Metronome.html

520
Robs Metronome.html

@ -1,106 +1,110 @@
<!DOCTYPE html> <!doctype html>
<html lang="en"> <html lang="en">
<head> <head>
<meta charset="UTF-8"> <meta charset="UTF-8" />
<title>Session Tempo</title> <title>Session Tempo</title>
<meta name="viewport" content="width=device-width, initial-scale=1"> <meta name="viewport" content="width=device-width, initial-scale=1" />
<style> <style>
@import url('https://fonts.googleapis.com/css2?family=Fraunces:opsz,wght@9..144,300..700&family=Work+Sans:wght@400;500;600&display=swap'); @import url("https://fonts.googleapis.com/css2?family=Fraunces:opsz,wght@9..144,300..700&family=Work+Sans:wght@400;500;600&display=swap");
:root{ :root {
--parchment: #EDE3D3; --parchment: #ede3d3;
--panel: #F7F1E3; --panel: #f7f1e3;
--ink: #2E2418; --ink: #2e2418;
--ink-soft: #5A4B39; --ink-soft: #5a4b39;
--walnut: #4A3222; --walnut: #4a3222;
--brass: #A9793C; --brass: #a9793c;
--brass-light: #C9A16A; --brass-light: #c9a16a;
--moss: #5F6F52; --moss: #5f6f52;
--rule: rgba(46,36,24,0.18); --rule: rgba(46, 36, 24, 0.18);
} }
*{ box-sizing: border-box; } * {
box-sizing: border-box;
body{ }
margin:0;
body {
margin: 0;
background: var(--parchment); background: var(--parchment);
color: var(--ink); color: var(--ink);
font-family: 'Work Sans', sans-serif; font-family: "Work Sans", sans-serif;
display:flex; display: flex;
justify-content:center; justify-content: center;
padding: 32px 16px 64px; padding: 32px 16px 64px;
} }
.page{ .page {
width:100%; width: 100%;
max-width: 560px; max-width: 560px;
} }
h1{ h1 {
font-family:'Fraunces', serif; font-family: "Fraunces", serif;
font-weight:500; font-weight: 500;
font-size: 1.6rem; font-size: 1.6rem;
text-align:center; text-align: center;
margin: 0 0 4px; margin: 0 0 4px;
letter-spacing: 0.01em; letter-spacing: 0.01em;
} }
.subtitle{ .subtitle {
text-align:center; text-align: center;
color: var(--ink-soft); color: var(--ink-soft);
font-size: 0.9rem; font-size: 0.9rem;
margin: 0 0 28px; margin: 0 0 28px;
} }
.hero{ .hero {
background: var(--panel); background: var(--panel);
border: 1px solid var(--rule); border: 1px solid var(--rule);
border-radius: 4px; border-radius: 4px;
padding: 32px 24px 28px; padding: 32px 24px 28px;
text-align:center; text-align: center;
} }
.bpm-num{ .bpm-num {
font-family:'Fraunces', serif; font-family: "Fraunces", serif;
font-variant-numeric: tabular-nums; font-variant-numeric: tabular-nums;
font-weight: 600; font-weight: 600;
font-size: 5rem; font-size: 5rem;
line-height: 1; line-height: 1;
color: var(--walnut); color: var(--walnut);
user-select:none; user-select: none;
} }
.bpm-label{ .bpm-label {
font-size: 0.8rem; font-size: 0.8rem;
color: var(--ink-soft); color: var(--ink-soft);
margin-top: 2px; margin-top: 2px;
margin-bottom: 20px; margin-bottom: 20px;
} }
.tap-pad{ .tap-pad {
width: 120px; width: 120px;
height: 120px; height: 120px;
margin: 0 auto 24px; margin: 0 auto 24px;
border-radius: 50%; border-radius: 50%;
border: 2px solid var(--brass); border: 2px solid var(--brass);
color: var(--walnut); color: var(--walnut);
font-family:'Work Sans'; font-family: "Work Sans";
font-size: 0.85rem; font-size: 0.85rem;
background: transparent; background: transparent;
cursor:pointer; cursor: pointer;
transition: transform 0.06s ease, background 0.15s ease; transition:
transform 0.06s ease,
background 0.15s ease;
} }
.tap-pad.pulse{ .tap-pad.pulse {
background: var(--brass-light); background: var(--brass-light);
transform: scale(0.96); transform: scale(0.96);
} }
.row{ .row {
display:flex; display: flex;
align-items:center; align-items: center;
justify-content:center; justify-content: center;
gap: 14px; gap: 14px;
margin-bottom: 22px; margin-bottom: 22px;
} }
.stepper{ .stepper {
width: 40px; width: 40px;
height: 40px; height: 40px;
border-radius: 50%; border-radius: 50%;
@ -108,27 +112,33 @@
background: var(--parchment); background: var(--parchment);
color: var(--walnut); color: var(--walnut);
font-size: 1.2rem; font-size: 1.2rem;
cursor:pointer; cursor: pointer;
} }
.stepper:active{ background: var(--brass-light); } .stepper:active {
input[type="range"]{ background: var(--brass-light);
flex:1; }
input[type="range"] {
flex: 1;
max-width: 220px; max-width: 220px;
accent-color: var(--brass); accent-color: var(--brass);
} }
.meta-row{ .meta-row {
display:flex; display: flex;
justify-content:center; justify-content: center;
gap: 24px; gap: 24px;
margin-bottom: 22px; margin-bottom: 22px;
font-size: 0.85rem; font-size: 0.85rem;
color: var(--ink-soft); color: var(--ink-soft);
} }
.meta-row label{ display:flex; align-items:center; gap:6px; } .meta-row label {
.meta-row input[type="number"]{ display: flex;
align-items: center;
gap: 6px;
}
.meta-row input[type="number"] {
width: 46px; width: 46px;
text-align:center; text-align: center;
border: 1px solid var(--rule); border: 1px solid var(--rule);
background: var(--parchment); background: var(--parchment);
color: var(--ink); color: var(--ink);
@ -137,86 +147,90 @@
font-family: inherit; font-family: inherit;
} }
.start-btn{ .start-btn {
font-family:'Work Sans'; font-family: "Work Sans";
font-weight:600; font-weight: 600;
font-size: 0.95rem; font-size: 0.95rem;
color: var(--parchment); color: var(--parchment);
background: var(--walnut); background: var(--walnut);
border: none; border: none;
border-radius: 4px; border-radius: 4px;
padding: 12px 36px; padding: 12px 36px;
cursor:pointer; cursor: pointer;
letter-spacing: 0.02em; letter-spacing: 0.02em;
} }
.start-btn.playing{ .start-btn.playing {
background: var(--moss); background: var(--moss);
} }
.divider{ .divider {
border:none; border: none;
border-top: 1px solid var(--rule); border-top: 1px solid var(--rule);
margin: 36px 0 18px; margin: 36px 0 18px;
} }
h2{ h2 {
font-family:'Fraunces', serif; font-family: "Fraunces", serif;
font-weight:500; font-weight: 500;
font-size: 1.1rem; font-size: 1.1rem;
margin: 0 0 14px; margin: 0 0 14px;
} }
.tune-list{ .tune-list {
display:flex; display: flex;
flex-direction:column; flex-direction: column;
} }
.tune-row{ .tune-row {
display:flex; display: flex;
align-items:center; align-items: center;
gap: 10px; gap: 10px;
padding: 10px 0; padding: 10px 0;
border-bottom: 1px solid var(--rule); border-bottom: 1px solid var(--rule);
} }
.tune-row:last-child{ border-bottom:none; } .tune-row:last-child {
.tune-name{ border-bottom: none;
flex:1; }
.tune-name {
flex: 1;
font-size: 0.95rem; font-size: 0.95rem;
} }
.tune-bpm{ .tune-bpm {
font-variant-numeric: tabular-nums; font-variant-numeric: tabular-nums;
color: var(--ink-soft); color: var(--ink-soft);
font-size: 0.9rem; font-size: 0.9rem;
min-width: 60px; min-width: 60px;
text-align:right; text-align: right;
} }
.tune-btn{ .tune-btn {
background:none; background: none;
border: 1px solid var(--rule); border: 1px solid var(--rule);
color: var(--ink); color: var(--ink);
font-size: 0.78rem; font-size: 0.78rem;
padding: 5px 10px; padding: 5px 10px;
border-radius: 3px; border-radius: 3px;
cursor:pointer; cursor: pointer;
font-family:'Work Sans'; font-family: "Work Sans";
}
.tune-btn:hover {
border-color: var(--brass);
} }
.tune-btn:hover{ border-color: var(--brass); } .tune-del {
.tune-del{ background: none;
background:none; border: none;
border:none;
color: var(--ink-soft); color: var(--ink-soft);
font-size: 1rem; font-size: 1rem;
cursor:pointer; cursor: pointer;
line-height:1; line-height: 1;
padding: 0 4px; padding: 0 4px;
} }
.add-row{ .add-row {
display:flex; display: flex;
gap: 8px; gap: 8px;
margin-top: 16px; margin-top: 16px;
} }
.add-row input[type="text"]{ .add-row input[type="text"] {
flex:1; flex: 1;
border: 1px solid var(--rule); border: 1px solid var(--rule);
background: var(--panel); background: var(--panel);
padding: 9px 10px; padding: 9px 10px;
@ -225,35 +239,35 @@
font-size: 0.9rem; font-size: 0.9rem;
color: var(--ink); color: var(--ink);
} }
.add-row button{ .add-row button {
background: var(--brass); background: var(--brass);
color: var(--panel); color: var(--panel);
border:none; border: none;
border-radius: 3px; border-radius: 3px;
padding: 0 16px; padding: 0 16px;
font-family:'Work Sans'; font-family: "Work Sans";
font-weight:500; font-weight: 500;
font-size: 0.85rem; font-size: 0.85rem;
cursor:pointer; cursor: pointer;
} }
.empty{ .empty {
color: var(--ink-soft); color: var(--ink-soft);
font-size: 0.88rem; font-size: 0.88rem;
padding: 8px 0; padding: 8px 0;
} }
.status{ .status {
text-align:center; text-align: center;
font-size: 0.78rem; font-size: 0.78rem;
color: var(--ink-soft); color: var(--ink-soft);
margin-top: 10px; margin-top: 10px;
min-height: 1em; min-height: 1em;
} }
</style> </style>
</head> </head>
<body> <body>
<div class="page"> <div class="page">
<h1>Session Tempo</h1> <h1>Rob's Metronome</h1>
<p class="subtitle">A metronome that remembers the tune</p> <p class="subtitle">A metronome that remembers the tune</p>
<div class="hero"> <div class="hero">
@ -264,19 +278,31 @@
<div class="row"> <div class="row">
<button class="stepper" id="bpmDown"></button> <button class="stepper" id="bpmDown"></button>
<input type="range" id="bpmSlider" min="30" max="300" value="120"> <input type="range" id="bpmSlider" min="30" max="300" value="120" />
<button class="stepper" id="bpmUp">+</button> <button class="stepper" id="bpmUp">+</button>
</div> </div>
<div class="meta-row"> <div class="meta-row">
<label>beats per bar <input type="number" id="beatsPerBar" min="1" max="12" value="4"></label> <label
<label>volume <input type="range" id="volumeSlider" min="0" max="100" value="70" style="width:80px;"></label> >beats per bar
<input type="number" id="beatsPerBar" min="1" max="12" value="4"
/></label>
<label
>volume
<input
type="range"
id="volumeSlider"
min="0"
max="100"
value="70"
style="width: 80px"
/></label>
</div> </div>
<button class="start-btn" id="startBtn">Start</button> <button class="start-btn" id="startBtn">Start</button>
</div> </div>
<hr class="divider"> <hr class="divider" />
<h2>Your tunes</h2> <h2>Your tunes</h2>
<div class="tune-list" id="tuneList"> <div class="tune-list" id="tuneList">
@ -284,18 +310,31 @@
</div> </div>
<div class="add-row"> <div class="add-row">
<input type="text" id="newTuneName" placeholder="Tune name (e.g. Drowsy Maggie)"> <input
type="text"
id="newTuneName"
placeholder="Tune name (e.g. Drowsy Maggie)"
/>
<button id="saveTuneBtn">Save at current tempo</button> <button id="saveTuneBtn">Save at current tempo</button>
</div> </div>
<div class="add-row" style="margin-top:8px;"> <div class="add-row" style="margin-top: 8px">
<button class="tune-btn" id="exportBtn" style="flex:1;">Export tunes.json</button> <button class="tune-btn" id="exportBtn" style="flex: 1">
<button class="tune-btn" id="importBtn" style="flex:1;">Import tunes.json</button> Export tunes.json
<input type="file" id="importFile" accept="application/json" style="display:none;"> </button>
<button class="tune-btn" id="importBtn" style="flex: 1">
Import tunes.json
</button>
<input
type="file"
id="importFile"
accept="application/json"
style="display: none"
/>
</div> </div>
<div class="status" id="statusMsg"></div> <div class="status" id="statusMsg"></div>
</div> </div>
<script> <script>
// ---------- Metronome engine ---------- // ---------- Metronome engine ----------
let audioCtx = null; let audioCtx = null;
let isPlaying = false; let isPlaying = false;
@ -308,16 +347,20 @@
const lookahead = 25; // ms const lookahead = 25; // ms
const scheduleAheadTime = 0.1; // seconds const scheduleAheadTime = 0.1; // seconds
function ensureAudio(){ function ensureAudio() {
if(!audioCtx) audioCtx = new (window.AudioContext || window.webkitAudioContext)(); if (!audioCtx)
audioCtx = new (window.AudioContext || window.webkitAudioContext)();
} }
function playClick(time, accented){ function playClick(time, accented) {
const osc = audioCtx.createOscillator(); const osc = audioCtx.createOscillator();
const gain = audioCtx.createGain(); const gain = audioCtx.createGain();
osc.frequency.value = accented ? 1500 : 1000; osc.frequency.value = accented ? 1500 : 1000;
gain.gain.setValueAtTime(0.0001, time); gain.gain.setValueAtTime(0.0001, time);
gain.gain.exponentialRampToValueAtTime(Math.max(0.0002, (accented ? 0.5 : 0.3) * volume), time + 0.002); gain.gain.exponentialRampToValueAtTime(
Math.max(0.0002, (accented ? 0.5 : 0.3) * volume),
time + 0.002,
);
gain.gain.exponentialRampToValueAtTime(0.0001, time + 0.05); gain.gain.exponentialRampToValueAtTime(0.0001, time + 0.05);
osc.connect(gain); osc.connect(gain);
gain.connect(audioCtx.destination); gain.connect(audioCtx.destination);
@ -325,8 +368,8 @@
osc.stop(time + 0.06); osc.stop(time + 0.06);
} }
function scheduler(){ function scheduler() {
while(nextNoteTime < audioCtx.currentTime + scheduleAheadTime){ while (nextNoteTime < audioCtx.currentTime + scheduleAheadTime) {
playClick(nextNoteTime, currentBeat === 0); playClick(nextNoteTime, currentBeat === 0);
const beatToFlash = currentBeat; const beatToFlash = currentBeat;
const delay = (nextNoteTime - audioCtx.currentTime) * 1000; const delay = (nextNoteTime - audioCtx.currentTime) * 1000;
@ -337,186 +380,199 @@
schedulerTimer = setTimeout(scheduler, lookahead); schedulerTimer = setTimeout(scheduler, lookahead);
} }
function flashPad(accented){ function flashPad(accented) {
tapPad.classList.add('pulse'); tapPad.classList.add("pulse");
setTimeout(() => tapPad.classList.remove('pulse'), accented ? 110 : 70); setTimeout(() => tapPad.classList.remove("pulse"), accented ? 110 : 70);
} }
function startMetronome(){ function startMetronome() {
ensureAudio(); ensureAudio();
if(audioCtx.state === 'suspended') audioCtx.resume(); if (audioCtx.state === "suspended") audioCtx.resume();
currentBeat = 0; currentBeat = 0;
nextNoteTime = audioCtx.currentTime + 0.05; nextNoteTime = audioCtx.currentTime + 0.05;
scheduler(); scheduler();
isPlaying = true; isPlaying = true;
startBtn.textContent = 'Stop'; startBtn.textContent = "Stop";
startBtn.classList.add('playing'); startBtn.classList.add("playing");
} }
function stopMetronome(){ function stopMetronome() {
clearTimeout(schedulerTimer); clearTimeout(schedulerTimer);
isPlaying = false; isPlaying = false;
startBtn.textContent = 'Start'; startBtn.textContent = "Start";
startBtn.classList.remove('playing'); startBtn.classList.remove("playing");
} }
// ---------- UI wiring ---------- // ---------- UI wiring ----------
const bpmNum = document.getElementById('bpmNum'); const bpmNum = document.getElementById("bpmNum");
const bpmSlider = document.getElementById('bpmSlider'); const bpmSlider = document.getElementById("bpmSlider");
const bpmDown = document.getElementById('bpmDown'); const bpmDown = document.getElementById("bpmDown");
const bpmUp = document.getElementById('bpmUp'); const bpmUp = document.getElementById("bpmUp");
const beatsPerBarInput = document.getElementById('beatsPerBar'); const beatsPerBarInput = document.getElementById("beatsPerBar");
const startBtn = document.getElementById('startBtn'); const startBtn = document.getElementById("startBtn");
const tapPad = document.getElementById('tapPad'); const tapPad = document.getElementById("tapPad");
const statusMsg = document.getElementById('statusMsg'); const statusMsg = document.getElementById("statusMsg");
function setBpm(val){ function setBpm(val) {
bpm = Math.max(30, Math.min(300, Math.round(val))); bpm = Math.max(30, Math.min(300, Math.round(val)));
bpmNum.textContent = bpm; bpmNum.textContent = bpm;
bpmSlider.value = bpm; bpmSlider.value = bpm;
} }
bpmSlider.addEventListener('input', e => setBpm(e.target.value)); bpmSlider.addEventListener("input", (e) => setBpm(e.target.value));
bpmDown.addEventListener('click', () => setBpm(bpm - 1)); bpmDown.addEventListener("click", () => setBpm(bpm - 1));
bpmUp.addEventListener('click', () => setBpm(bpm + 1)); bpmUp.addEventListener("click", () => setBpm(bpm + 1));
beatsPerBarInput.addEventListener('change', e => { beatsPerBarInput.addEventListener("change", (e) => {
beatsPerBar = Math.max(1, Math.min(12, parseInt(e.target.value) || 4)); beatsPerBar = Math.max(1, Math.min(12, parseInt(e.target.value) || 4));
}); });
const volumeSlider = document.getElementById('volumeSlider'); const volumeSlider = document.getElementById("volumeSlider");
volumeSlider.addEventListener('input', e => { volumeSlider.addEventListener("input", (e) => {
volume = parseInt(e.target.value) / 100; volume = parseInt(e.target.value) / 100;
}); });
startBtn.addEventListener('click', () => { startBtn.addEventListener("click", () => {
if(isPlaying) stopMetronome(); else startMetronome(); if (isPlaying) stopMetronome();
else startMetronome();
}); });
// Tap tempo // Tap tempo
let tapTimes = []; let tapTimes = [];
tapPad.addEventListener('click', () => { tapPad.addEventListener("click", () => {
const now = performance.now(); const now = performance.now();
tapTimes.push(now); tapTimes.push(now);
tapTimes = tapTimes.filter(t => now - t < 2500); tapTimes = tapTimes.filter((t) => now - t < 2500);
if(tapTimes.length > 1){ if (tapTimes.length > 1) {
const intervals = []; const intervals = [];
for(let i=1;i<tapTimes.length;i++) intervals.push(tapTimes[i]-tapTimes[i-1]); for (let i = 1; i < tapTimes.length; i++)
const avg = intervals.reduce((a,b)=>a+b,0)/intervals.length; intervals.push(tapTimes[i] - tapTimes[i - 1]);
setBpm(60000/avg); const avg = intervals.reduce((a, b) => a + b, 0) / intervals.length;
setBpm(60000 / avg);
} }
flashPad(false); flashPad(false);
}); });
// ---------- Tune storage (browser-local cache + JSON export/import) ---------- // ---------- Tune storage (browser-local cache + JSON export/import) ----------
const tuneList = document.getElementById('tuneList'); const tuneList = document.getElementById("tuneList");
const emptyMsg = document.getElementById('emptyMsg'); const emptyMsg = document.getElementById("emptyMsg");
const newTuneName = document.getElementById('newTuneName'); const newTuneName = document.getElementById("newTuneName");
const saveTuneBtn = document.getElementById('saveTuneBtn'); const saveTuneBtn = document.getElementById("saveTuneBtn");
const exportBtn = document.getElementById('exportBtn'); const exportBtn = document.getElementById("exportBtn");
const importBtn = document.getElementById('importBtn'); const importBtn = document.getElementById("importBtn");
const importFile = document.getElementById('importFile'); const importFile = document.getElementById("importFile");
const STORAGE_KEY = 'session-tempo-tunes'; const STORAGE_KEY = "session-tempo-tunes";
let tunes = []; let tunes = [];
function loadTunes(){ function loadTunes() {
try{ try {
const raw = localStorage.getItem(STORAGE_KEY); const raw = localStorage.getItem(STORAGE_KEY);
tunes = raw ? JSON.parse(raw) : []; tunes = raw ? JSON.parse(raw) : [];
}catch(err){ } catch (err) {
tunes = []; tunes = [];
} }
renderTunes(); renderTunes();
} }
function persistTunes(){ function persistTunes() {
try{ try {
localStorage.setItem(STORAGE_KEY, JSON.stringify(tunes)); localStorage.setItem(STORAGE_KEY, JSON.stringify(tunes));
}catch(err){ } catch (err) {
showStatus('Could not save locally — try again.'); showStatus("Could not save locally — try again.");
} }
} }
exportBtn.addEventListener('click', () => { exportBtn.addEventListener("click", () => {
const blob = new Blob([JSON.stringify(tunes, null, 2)], { type: 'application/json' }); const blob = new Blob([JSON.stringify(tunes, null, 2)], {
type: "application/json",
});
const url = URL.createObjectURL(blob); const url = URL.createObjectURL(blob);
const a = document.createElement('a'); const a = document.createElement("a");
a.href = url; a.href = url;
a.download = 'tunes.json'; a.download = "tunes.json";
a.click(); a.click();
URL.revokeObjectURL(url); URL.revokeObjectURL(url);
showStatus('Exported tunes.json — move it into your repo and commit.'); showStatus("Exported tunes.json — move it into your repo and commit.");
}); });
importBtn.addEventListener('click', () => importFile.click()); importBtn.addEventListener("click", () => importFile.click());
importFile.addEventListener('change', () => { importFile.addEventListener("change", () => {
const file = importFile.files[0]; const file = importFile.files[0];
if(!file) return; if (!file) return;
const reader = new FileReader(); const reader = new FileReader();
reader.onload = () => { reader.onload = () => {
try{ try {
const parsed = JSON.parse(reader.result); const parsed = JSON.parse(reader.result);
if(!Array.isArray(parsed)) throw new Error('not an array'); if (!Array.isArray(parsed)) throw new Error("not an array");
tunes = parsed; tunes = parsed;
persistTunes(); persistTunes();
renderTunes(); renderTunes();
showStatus('Imported ' + tunes.length + ' tune' + (tunes.length === 1 ? '' : 's') + '.'); showStatus(
}catch(err){ "Imported " +
showStatus('That file didn\'t look like a valid tunes.json.'); tunes.length +
" tune" +
(tunes.length === 1 ? "" : "s") +
".",
);
} catch (err) {
showStatus("That file didn't look like a valid tunes.json.");
} }
}; };
reader.readAsText(file); reader.readAsText(file);
importFile.value = ''; importFile.value = "";
}); });
function showStatus(msg){ function showStatus(msg) {
statusMsg.textContent = msg; statusMsg.textContent = msg;
setTimeout(() => { if(statusMsg.textContent === msg) statusMsg.textContent = ''; }, 2500); setTimeout(() => {
} if (statusMsg.textContent === msg) statusMsg.textContent = "";
}, 2500);
function renderTunes(){ }
tuneList.innerHTML = '';
if(tunes.length === 0){ function renderTunes() {
const p = document.createElement('div'); tuneList.innerHTML = "";
p.className = 'empty'; if (tunes.length === 0) {
p.textContent = 'No tunes saved yet — set a tempo above and save it below.'; const p = document.createElement("div");
p.className = "empty";
p.textContent =
"No tunes saved yet — set a tempo above and save it below.";
tuneList.appendChild(p); tuneList.appendChild(p);
return; return;
} }
tunes.forEach(tune => { tunes.forEach((tune) => {
const row = document.createElement('div'); const row = document.createElement("div");
row.className = 'tune-row'; row.className = "tune-row";
const name = document.createElement('div'); const name = document.createElement("div");
name.className = 'tune-name'; name.className = "tune-name";
name.textContent = tune.name; name.textContent = tune.name;
const bpmSpan = document.createElement('div'); const bpmSpan = document.createElement("div");
bpmSpan.className = 'tune-bpm'; bpmSpan.className = "tune-bpm";
bpmSpan.textContent = tune.bpm + ' bpm'; bpmSpan.textContent = tune.bpm + " bpm";
const loadBtn = document.createElement('button'); const loadBtn = document.createElement("button");
loadBtn.className = 'tune-btn'; loadBtn.className = "tune-btn";
loadBtn.textContent = 'Load'; loadBtn.textContent = "Load";
loadBtn.addEventListener('click', () => setBpm(tune.bpm)); loadBtn.addEventListener("click", () => setBpm(tune.bpm));
const updateBtn = document.createElement('button'); const updateBtn = document.createElement("button");
updateBtn.className = 'tune-btn'; updateBtn.className = "tune-btn";
updateBtn.textContent = 'Update'; updateBtn.textContent = "Update";
updateBtn.addEventListener('click', () => { updateBtn.addEventListener("click", () => {
tune.bpm = bpm; tune.bpm = bpm;
persistTunes(); persistTunes();
renderTunes(); renderTunes();
showStatus(tune.name + ' updated to ' + bpm + ' bpm.'); showStatus(tune.name + " updated to " + bpm + " bpm.");
}); });
const delBtn = document.createElement('button'); const delBtn = document.createElement("button");
delBtn.className = 'tune-del'; delBtn.className = "tune-del";
delBtn.textContent = '×'; delBtn.textContent = "×";
delBtn.title = 'Remove'; delBtn.title = "Remove";
delBtn.addEventListener('click', () => { delBtn.addEventListener("click", () => {
tunes = tunes.filter(t => t.id !== tune.id); tunes = tunes.filter((t) => t.id !== tune.id);
persistTunes(); persistTunes();
renderTunes(); renderTunes();
}); });
@ -530,26 +586,26 @@
}); });
} }
saveTuneBtn.addEventListener('click', () => { saveTuneBtn.addEventListener("click", () => {
const name = newTuneName.value.trim(); const name = newTuneName.value.trim();
if(!name){ if (!name) {
showStatus('Give the tune a name first.'); showStatus("Give the tune a name first.");
return; return;
} }
tunes.push({ id: Date.now().toString(36), name, bpm }); tunes.push({ id: Date.now().toString(36), name, bpm });
persistTunes(); persistTunes();
renderTunes(); renderTunes();
newTuneName.value = ''; newTuneName.value = "";
showStatus(name + ' saved at ' + bpm + ' bpm.'); showStatus(name + " saved at " + bpm + " bpm.");
}); });
newTuneName.addEventListener('keydown', e => { newTuneName.addEventListener("keydown", (e) => {
if(e.key === 'Enter') saveTuneBtn.click(); if (e.key === "Enter") saveTuneBtn.click();
}); });
// init // init
setBpm(120); setBpm(120);
loadTunes(); loadTunes();
</script> </script>
</body> </body>
</html> </html>

Loading…
Cancel
Save