https://chat.deepseek.com/a/chat/s/346ce61b-a344-4be4-a943-95cf736ce8e1
Full Upgraded Webpage — 9 Rhythm Versions + Bass + Chords
Save as .html and open in a browser. Includes:
- 9 rhythm versions (dropdown)
- Auto bass line (root notes)
- Auto chord accompaniment (block chords per bar)
- Live highlighting of notes, bars, and beat cursor
- Tempo, loop, and instrument controls
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>Do–Mi–So Motive — 9 Rhythm Variations</title>
<style>
:root {
--bg: #0f1420;
--panel: #1a2233;
--panel2: #232d42;
--accent: #e94560;
--accent-soft: rgba(233,69,96,.18);
--accent2: #4a7bc8;
--bass: #6b4e9e;
--chord: #2f8f7a;
--text: #eaeaea;
--muted: #8892b0;
--grid: #2a3a5c;
}
* { box-sizing: border-box; }
body {
margin: 0;
font-family: 'Segoe UI', system-ui, -apple-system, sans-serif;
background: radial-gradient(circle at 50% 0%, #1a2540 0%, var(--bg) 70%);
color: var(--text);
min-height: 100vh;
display: flex;
align-items: flex-start;
justify-content: center;
padding: 20px;
}
.container {
width: 100%;
max-width: 1080px;
background: var(--panel);
border-radius: 16px;
padding: 28px;
box-shadow: 0 20px 60px rgba(0,0,0,.6);
}
h1 {
margin: 0 0 4px;
font-size: 1.55rem;
letter-spacing: .4px;
}
.sub {
color: var(--muted);
font-size: .88rem;
margin-bottom: 20px;
}
.sub b { color: var(--accent); }
.controls {
display: flex;
gap: 14px;
flex-wrap: wrap;
align-items: center;
margin-bottom: 18px;
padding-bottom: 18px;
border-bottom: 1px solid var(--grid);
}
.controls .group {
display: flex;
align-items: center;
gap: 8px;
font-size: .85rem;
color: var(--muted);
}
button {
background: var(--accent);
color: white;
border: none;
padding: 11px 22px;
border-radius: 8px;
font-size: .95rem;
font-weight: 600;
cursor: pointer;
transition: transform .1s, background .2s, opacity .2s;
font-family: inherit;
}
button:hover:not(:disabled) { background: #ff5c78; }
button:active:not(:disabled) { transform: scale(.97); }
button:disabled { opacity: .4; cursor: not-allowed; }
button.secondary {
background: transparent;
border: 1px solid var(--muted);
color: var(--muted);
}
button.secondary:hover:not(:disabled) {
border-color: var(--accent); color: var(--accent); background: transparent;
}
select, input[type="range"] {
background: var(--panel2);
color: var(--text);
border: 1px solid var(--grid);
border-radius: 6px;
padding: 7px 10px;
font-size: .85rem;
font-family: inherit;
cursor: pointer;
}
input[type="range"] { accent-color: var(--accent); width: 110px; padding: 0; }
input[type="checkbox"] { accent-color: var(--accent); }
.info {
display: flex;
gap: 10px;
flex-wrap: wrap;
margin-bottom: 16px;
font-size: .78rem;
}
.badge {
background: var(--panel2);
border: 1px solid var(--grid);
padding: 4px 10px;
border-radius: 20px;
color: var(--muted);
}
.badge b { color: var(--text); }
.staff {
display: flex;
flex-direction: column;
gap: 6px;
margin-bottom: 18px;
max-height: 460px;
overflow-y: auto;
padding-right: 6px;
}
.staff::-webkit-scrollbar { width: 8px; }
.staff::-webkit-scrollbar-track { background: var(--panel2); border-radius: 4px; }
.staff::-webkit-scrollbar-thumb { background: var(--grid); border-radius: 4px; }
.bar {
display: flex;
gap: 4px;
align-items: stretch;
padding: 6px;
border-radius: 8px;
transition: background .15s;
min-height: 62px;
position: relative;
}
.bar.active { background: var(--accent-soft); }
.bar-label {
flex: 0 0 34px;
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
color: var(--muted);
font-size: .72rem;
font-weight: 600;
border-right: 1px solid var(--grid);
margin-right: 4px;
}
.bar-label .chord {
color: var(--chord);
font-weight: 700;
font-size: .78rem;
}
.bar-label .num {
color: var(--muted);
font-size: .65rem;
}
.note {
flex: 1;
min-width: 34px;
background: var(--accent2);
border-radius: 6px;
padding: 6px 2px;
text-align: center;
font-weight: 600;
font-size: .82rem;
transition: all .12s;
display: flex;
flex-direction: column;
justify-content: center;
align-items: center;
position: relative;
overflow: hidden;
}
.note .name { line-height: 1.1; }
.note .solfege {
font-size: .6rem;
color: rgba(255,255,255,.65);
font-weight: 400;
margin-top: 2px;
}
.note.playing {
background: var(--accent);
transform: translateY(-2px) scale(1.04);
box-shadow: 0 4px 14px rgba(233,69,96,.55);
z-index: 2;
}
.note.rest {
background: transparent;
border: 1px dashed var(--grid);
color: var(--muted);
}
.note.motive {
border-left: 3px solid var(--accent);
}
.note.tuplet::after {
content: attr(data-tuplet);
position: absolute;
top: 2px;
right: 4px;
font-size: .55rem;
color: rgba(255,255,255,.5);
}
/* Beat cursor line */
.cursor {
position: absolute;
top: 4px; bottom: 4px;
width: 2px;
background: rgba(255,255,255,.5);
pointer-events: none;
transition: left .06s linear;
box-shadow: 0 0 8px rgba(255,255,255,.6);
}
.bass-row, .chord-row {
display: flex;
gap: 4px;
padding: 4px 6px 4px 44px;
align-items: center;
font-size: .7rem;
color: var(--muted);
margin-top: -4px;
margin-bottom: 4px;
}
.bass-row .label, .chord-row .label {
flex: 0 0 40px;
font-weight: 600;
letter-spacing: .5px;
}
.bass-row .cell, .chord-row .cell {
flex: 1;
text-align: center;
padding: 3px 2px;
border-radius: 4px;
background: rgba(107,78,158,.15);
font-weight: 600;
transition: all .12s;
}
.bass-row .cell.playing {
background: var(--bass);
color: white;
transform: scale(1.05);
}
.chord-row .cell {
background: rgba(47,143,122,.15);
}
.chord-row .cell.playing {
background: var(--chord);
color: white;
transform: scale(1.05);
}
.legend {
display: flex;
gap: 18px;
flex-wrap: wrap;
font-size: .75rem;
color: var(--muted);
border-top: 1px solid var(--grid);
padding-top: 14px;
margin-top: 8px;
}
.legend span::before {
content: '';
display: inline-block;
width: 10px; height: 10px;
border-radius: 3px;
margin-right: 6px;
vertical-align: middle;
}
.legend .m::before { background: var(--accent); }
.legend .n::before { background: var(--accent2); }
.legend .b::before { background: var(--bass); }
.legend .c::before { background: var(--chord); }
.status {
margin-top: 12px;
font-size: .8rem;
color: var(--muted);
min-height: 18px;
font-variant-numeric: tabular-nums;
}
</style>
</head>
<body>
<div class="container">
<h1>Do–Mi–So Motive · 9 Rhythm Variations</h1>
<div class="sub">
Key of <b>C Major</b> · recurring <b>1–3–5</b> arpeggiation · optional bass & chords
</div>
<div class="controls">
<button id="playBtn">▶ Play</button>
<button id="stopBtn" class="secondary" disabled>■ Stop</button>
<div class="group">
<label for="version">Rhythm:</label>
<select id="version"></select>
</div>
<div class="group">
<label>Tempo
<input type="range" id="tempo" min="40" max="160" value="88">
<span id="tempoVal">88</span> BPM
</label>
</div>
<div class="group">
<label><input type="checkbox" id="withBass" checked> Bass</label>
<label><input type="checkbox" id="withChords" checked> Chords</label>
</div>
<div class="group">
<label><input type="checkbox" id="loop"> Loop</label>
</div>
</div>
<div class="info">
<span class="badge">Meter: <b id="infoMeter">4/4</b></span>
<span class="badge">Bars: <b id="infoBars">8</b></span>
<span class="badge">Duration: <b id="infoDur">~22s</b></span>
<span class="badge">Version: <b id="infoVer">A</b></span>
</div>
<div class="staff" id="staff"></div>
<div class="legend">
<span class="m">Motive (1–3–5)</span>
<span class="n">Continuation</span>
<span class="b">Bass</span>
<span class="c">Chord</span>
</div>
<div class="status" id="status">Ready.</div>
</div>
<script>
/* =========================================================
1. DURATION TABLE (in beats)
========================================================= */
const DUR = {
quarter: 1,
eighth: 0.5,
sixteenth: 0.25,
half: 2,
dotted: 1.5,
'dotted-half': 3,
triplet: 1 / 3,
swingLong: 0.66,
swingShort: 0.34,
};
/* =========================================================
2. HARMONY SKELETON (per bar)
========================================================= */
const HARMONY = [
{ bass: 'C3', chord: ['C4','E4','G4'], label: 'C', roman: 'I' },
{ bass: 'G2', chord: ['B3','D4','G4'], label: 'G', roman: 'V' },
{ bass: 'C3', chord: ['C4','E4','G4'], label: 'C', roman: 'I' },
{ bass: 'F2', chord: ['F3','A3','C4'], label: 'F', roman: 'IV' },
{ bass: 'C3', chord: ['C4','E4','G4'], label: 'C', roman: 'I' },
{ bass: 'F2', chord: ['F3','A3','C4'], label: 'F', roman: 'IV' },
{ bass: 'G2', chord: ['B3','D4','G4'], label: 'G', roman: 'V' },
{ bass: 'C3', chord: ['C4','E4','G4'], label: 'C', roman: 'I' },
];
/* =========================================================
3. MELODY VERSIONS
Each bar = array of events { note, solfege, motive?, dur?/beats?, rest?, tuplet? }
`dur` = name in DUR, or `beats` = explicit length in beats.
A bar is padded to its meter length if total < meterBeats.
========================================================= */
const VERSIONS = {
A: {
name: 'A · Plain quarters (4/4)',
meter: 4,
bars: [
[ {note:'C4',solfege:'Do',motive:true}, {note:'E4',solfege:'Mi',motive:true}, {note:'G4',solfege:'So',motive:true}, {note:'A4',solfege:'La'} ],
[ {note:'G4',solfege:'So'}, {note:'F4',solfege:'Fa'}, {note:'E4',solfege:'Mi'}, {note:'D4',solfege:'Re'} ],
[ {note:'E4',solfege:'Mi',motive:true}, {note:'G4',solfege:'So',motive:true}, {note:'C5',solfege:'Do',motive:true}, {note:'B4',solfege:'Ti'} ],
[ {note:'A4',solfege:'La'}, {note:'G4',solfege:'So'}, {note:'F4',solfege:'Fa'}, {note:'E4',solfege:'Mi'} ],
[ {note:'C4',solfege:'Do',motive:true}, {note:'E4',solfege:'Mi',motive:true}, {note:'G4',solfege:'So',motive:true}, {note:'E4',solfege:'Mi'} ],
[ {note:'F4',solfege:'Fa'}, {note:'G4',solfege:'So'}, {note:'A4',solfege:'La'}, {note:'B4',solfege:'Ti'} ],
[ {note:'G4',solfege:'So',motive:true}, {note:'B4',solfege:'Ti',motive:true}, {note:'D5',solfege:'Re',motive:true}, {note:'C5',solfege:'Do'} ],
[ {note:'G4',solfege:'So'}, {note:'E4',solfege:'Mi'}, {note:'C4',solfege:'Do',dur:'half'}, {note:null,solfege:'–',rest:true} ],
],
},
B: {
name: 'B · Swing eighths (4/4)',
meter: 4,
bars: [
[ {note:'C4',solfege:'Do',motive:true,dur:'swingLong'}, {note:'E4',solfege:'Mi',motive:true,dur:'swingShort'},
{note:'G4',solfege:'So',motive:true,dur:'swingLong'}, {note:'A4',solfege:'La',dur:'swingShort'},
{note:'G4',solfege:'So',dur:'swingLong'}, {note:'F4',solfege:'Fa',dur:'swingShort'},
{note:'E4',solfege:'Mi',dur:'swingLong'}, {note:'D4',solfege:'Re',dur:'swingShort'} ],
[ {note:'E4',solfege:'Mi',dur:'swingLong'}, {note:'G4',solfege:'So',dur:'swingShort'},
{note:'F4',solfege:'Fa',dur:'swingLong'}, {note:'E4',solfege:'Mi',dur:'swingShort'},
{note:'D4',solfege:'Re',dur:'swingLong'}, {note:'C4',solfege:'Do',dur:'swingShort'},
{note:'D4',solfege:'Re',dur:'swingLong'}, {note:'E4',solfege:'Mi',dur:'swingShort'} ],
[ {note:'E4',solfege:'Mi',motive:true,dur:'swingLong'}, {note:'G4',solfege:'So',motive:true,dur:'swingShort'},
{note:'C5',solfege:'Do',motive:true,dur:'swingLong'}, {note:'B4',solfege:'Ti',dur:'swingShort'},
{note:'A4',solfege:'La',dur:'swingLong'}, {note:'G4',solfege:'So',dur:'swingShort'},
{note:'F4',solfege:'Fa',dur:'swingLong'}, {note:'E4',solfege:'Mi',dur:'swingShort'} ],
[ {note:'D4',solfege:'Re',dur:'swingLong'}, {note:'E4',solfege:'Mi',dur:'swingShort'},
{note:'F4',solfege:'Fa',dur:'swingLong'}, {note:'G4',solfege:'So',dur:'swingShort'},
{note:'A4',solfege:'La',dur:'swingLong'}, {note:'G4',solfege:'So',dur:'swingShort'},
{note:'F4',solfege:'Fa',dur:'swingLong'}, {note:'E4',solfege:'Mi',dur:'swingShort'} ],
[ {note:'C4',solfege:'Do',motive:true,dur:'swingLong'}, {note:'E4',solfege:'Mi',motive:true,dur:'swingShort'},
{note:'G4',solfege:'So',motive:true,dur:'swingLong'}, {note:'E4',solfege:'Mi',dur:'swingShort'},
{note:'F4',solfege:'Fa',dur:'swingLong'}, {note:'G4',solfege:'So',dur:'swingShort'},
{note:'A4',solfege:'La',dur:'swingLong'}, {note:'B4',solfege:'Ti',dur:'swingShort'} ],
[ {note:'C5',solfege:'Do',dur:'swingLong'}, {note:'B4',solfege:'Ti',dur:'swingShort'},
{note:'A4',solfege:'La',dur:'swingLong'}, {note:'G4',solfege:'So',dur:'swingShort'},
{note:'F4',solfege:'Fa',dur:'swingLong'}, {note:'E4',solfege:'Mi',dur:'swingShort'},
{note:'D4',solfege:'Re',dur:'swingLong'}, {note:'C4',solfege:'Do',dur:'swingShort'} ],
[ {note:'G4',solfege:'So',motive:true,dur:'swingLong'}, {note:'B4',solfege:'Ti',motive:true,dur:'swingShort'},
{note:'D5',solfege:'Re',motive:true,dur:'swingLong'}, {note:'C5',solfege:'Do',dur:'swingShort'},
{note:'B4',solfege:'Ti',dur:'swingLong'}, {note:'A4',solfege:'La',dur:'swingShort'},
{note:'G4',solfege:'So',dur:'swingLong'}, {note:'F4',solfege:'Fa',dur:'swingShort'} ],
[ {note:'E4',solfege:'Mi',dur:'swingLong'}, {note:'D4',solfege:'Re',dur:'swingShort'},
{note:'C4',solfege:'Do',dur:'half'},
{note:null,solfege:'–',rest:true,dur:'quarter'} ],
],
},
C: {
name: 'C · Triplet arpeggio (4/4)',
meter: 4,
bars: [
[ {note:'C4',solfege:'Do',motive:true,beats:1/3,tuplet:'3'}, {note:'E4',solfege:'Mi',motive:true,beats:1/3,tuplet:'3'}, {note:'G4',solfege:'So',motive:true,beats:1/3,tuplet:'3'},
{note:'A4',solfege:'La'}, {note:null,solfege:'–',rest:true}, {note:null,solfege:'–',rest:true} ],
[ {note:'G4',solfege:'So'}, {note:'F4',solfege:'Fa'}, {note:'E4',solfege:'Mi'}, {note:'D4',solfege:'Re'} ],
[ {note:'E4',solfege:'Mi',motive:true,beats:1/3,tuplet:'3'}, {note:'G4',solfege:'So',motive:true,beats:1/3,tuplet:'3'}, {note:'C5',solfege:'Do',motive:true,beats:1/3,tuplet:'3'},
{note:'B4',solfege:'Ti'}, {note:null,solfege:'–',rest:true}, {note:null,solfege:'–',rest:true} ],
[ {note:'A4',solfege:'La'}, {note:'G4',solfege:'So'}, {note:'F4',solfege:'Fa'}, {note:'E4',solfege:'Mi'} ],
[ {note:'C4',solfege:'Do',motive:true,beats:1/3,tuplet:'3'}, {note:'E4',solfege:'Mi',motive:true,beats:1/3,tuplet:'3'}, {note:'G4',solfege:'So',motive:true,beats:1/3,tuplet:'3'},
{note:'E4',solfege:'Mi'}, {note:null,solfege:'–',rest:true}, {note:null,solfege:'–',rest:true} ],
[ {note:'F4',solfege:'Fa'}, {note:'G4',solfege:'So'}, {note:'A4',solfege:'La'}, {note:'B4',solfege:'Ti'} ],
[ {note:'G4',solfege:'So',motive:true,beats:1/3,tuplet:'3'}, {note:'B4',solfege:'Ti',motive:true,beats:1/3,tuplet:'3'}, {note:'D5',solfege:'Re',motive:true,beats:1/3,tuplet:'3'},
{note:'C5',solfege:'Do'}, {note:null,solfege:'–',rest:true}, {note:null,solfege:'–',rest:true} ],
[ {note:'G4',solfege:'So'}, {note:'E4',solfege:'Mi'}, {note:'C4',solfege:'Do',dur:'half'}, {note:null,solfege:'–',rest:true} ],
],
},
D: {
name: 'D · Syncopated eighths (4/4)',
meter: 4,
bars: [
[ {note:'C4',solfege:'Do',motive:true,dur:'eighth'}, {note:'E4',solfege:'Mi',motive:true,dur:'eighth'}, {note:'G4',solfege:'So',motive:true,dur:'eighth'}, {note:'A4',solfege:'La',dur:'eighth'},
{note:'G4',solfege:'So',dur:'eighth'}, {note:'F4',solfege:'Fa',dur:'eighth'}, {note:'E4',solfege:'Mi'}, {note:'D4',solfege:'Re'} ],
[ {note:'C4',solfege:'Do',dur:'eighth'}, {note:'D4',solfege:'Re',dur:'eighth'}, {note:'E4',solfege:'Mi',dur:'eighth'}, {note:'F4',solfege:'Fa',dur:'eighth'},
{note:'G4',solfege:'So',dur:'eighth'}, {note:'A4',solfege:'La',dur:'eighth'}, {note:'B4',solfege:'Ti'}, {note:'C5',solfege:'Do'} ],
[ {note:'B4',solfege:'Ti',dur:'eighth'}, {note:'A4',solfege:'La',dur:'eighth'}, {note:'G4',solfege:'So',dur:'eighth'}, {note:'F4',solfege:'Fa',dur:'eighth'},
{note:'E4',solfege:'Mi',dur:'eighth'}, {note:'D4',solfege:'Re',dur:'eighth'}, {note:'C4',solfege:'Do'}, {note:'D4',solfege:'Re'} ],
[ {note:'E4',solfege:'Mi',dur:'eighth'}, {note:'F4',solfege:'Fa',dur:'eighth'}, {note:'G4',solfege:'So',dur:'eighth'}, {note:'A4',solfege:'La',dur:'eighth'},
{note:'G4',solfege:'So',dur:'eighth'}, {note:'F4',solfege:'Fa',dur:'eighth'}, {note:'E4',solfege:'Mi'}, {note:'D4',solfege:'Re'} ],
[ {note:'C4',solfege:'Do',motive:true,dur:'eighth'}, {note:'E4',solfege:'Mi',motive:true,dur:'eighth'}, {note:'G4',solfege:'So',motive:true,dur:'eighth'}, {note:'E4',solfege:'Mi',dur:'eighth'},
{note:'F4',solfege:'Fa',dur:'eighth'}, {note:'G4',solfege:'So',dur:'eighth'}, {note:'A4',solfege:'La'}, {note:'B4',solfege:'Ti'} ],
[ {note:'C5',solfege:'Do',dur:'eighth'}, {note:'B4',solfege:'Ti',dur:'eighth'}, {note:'A4',solfege:'La',dur:'eighth'}, {note:'G4',solfege:'So',dur:'eighth'},
{note:'F4',solfege:'Fa',dur:'eighth'}, {note:'E4',solfege:'Mi',dur:'eighth'}, {note:'D4',solfege:'Re'}, {note:'C4',solfege:'Do'} ],
[ {note:'G4',solfege:'So',motive:true,dur:'eighth'}, {note:'B4',solfege:'Ti',motive:true,dur:'eighth'}, {note:'D5',solfege:'Re',motive:true,dur:'eighth'}, {note:'C5',solfege:'Do',dur:'eighth'},
{note:'B4',solfege:'Ti',dur:'eighth'}, {note:'A4',solfege:'La',dur:'eighth'}, {note:'G4',solfege:'So'}, {note:'F4',solfege:'Fa'} ],
[ {note:'E4',solfege:'Mi',dur:'eighth'}, {note:'D4',solfege:'Re',dur:'eighth'}, {note:'C4',solfege:'Do',dur:'half'},
{note:null,solfege:'–',rest:true,dur:'quarter'} ],
],
},
E: {
name: 'E · Dotted long–short (4/4)',
meter: 4,
bars: [
[ {note:'C4',solfege:'Do',motive:true,dur:'dotted'}, {note:'E4',solfege:'Mi',motive:true,dur:'eighth'},
{note:'G4',solfege:'So',motive:true,dur:'dotted'}, {note:'A4',solfege:'La',dur:'eighth'} ],
[ {note:'G4',solfege:'So',dur:'dotted'}, {note:'F4',solfege:'Fa',dur:'eighth'},
{note:'E4',solfege:'Mi',dur:'dotted'}, {note:'D4',solfege:'Re',dur:'eighth'} ],
[ {note:'E4',solfege:'Mi',motive:true,dur:'dotted'}, {note:'G4',solfege:'So',motive:true,dur:'eighth'},
{note:'C5',solfege:'Do',motive:true,dur:'dotted'}, {note:'B4',solfege:'Ti',dur:'eighth'} ],
[ {note:'A4',solfege:'La',dur:'dotted'}, {note:'G4',solfege:'So',dur:'eighth'},
{note:'F4',solfege:'Fa',dur:'dotted'}, {note:'E4',solfege:'Mi',dur:'eighth'} ],
[ {note:'C4',solfege:'Do',motive:true,dur:'dotted'}, {note:'E4',solfege:'Mi',motive:true,dur:'eighth'},
{note:'G4',solfege:'So',motive:true,dur:'dotted'}, {note:'E4',solfege:'Mi',dur:'eighth'} ],
[ {note:'F4',solfege:'Fa',dur:'dotted'}, {note:'G4',solfege:'So',dur:'eighth'},
{note:'A4',solfege:'La',dur:'dotted'}, {note:'B4',solfege:'Ti',dur:'eighth'} ],
[ {note:'G4',solfege:'So',motive:true,dur:'dotted'}, {note:'B4',solfege:'Ti',motive:true,dur:'eighth'},
{note:'D5',solfege:'Re',motive:true,dur:'dotted'}, {note:'C5',solfege:'Do',dur:'eighth'} ],
[ {note:'G4',solfege:'So',dur:'dotted'}, {note:'E4',solfege:'Mi',dur:'eighth'},
{note:'C4',solfege:'Do',dur:'half'}, {note:null,solfege:'–',rest:true,dur:'quarter'} ],
],
},
F: {
name: 'F · Sixteenth flourish (4/4)',
meter: 4,
bars: [
[ {note:'C4',solfege:'Do',motive:true,dur:'sixteenth'}, {note:'E4',solfege:'Mi',motive:true,dur:'sixteenth'},
{note:'G4',solfege:'So',motive:true,dur:'sixteenth'}, {note:'A4',solfege:'La',dur:'sixteenth'},
{note:'G4',solfege:'So',dur:'eighth'}, {note:'F4',solfege:'Fa',dur:'eighth'},
{note:'E4',solfege:'Mi'}, {note:'D4',solfege:'Re'} ],
[ {note:'C4',solfege:'Do',dur:'eighth'}, {note:'D4',solfege:'Re',dur:'eighth'},
{note:'E4',solfege:'Mi',dur:'eighth'}, {note:'F4',solfege:'Fa',dur:'eighth'},
{note:'G4',solfege:'So'}, {note:'A4',solfege:'La'}, {note:'B4',solfege:'Ti'}, {note:'C5',solfege:'Do'} ],
[ {note:'E4',solfege:'Mi',motive:true,dur:'sixteenth'}, {note:'G4',solfege:'So',motive:true,dur:'sixteenth'},
{note:'C5',solfege:'Do',motive:true,dur:'sixteenth'}, {note:'B4',solfege:'Ti',dur:'sixteenth'},
{note:'A4',solfege:'La',dur:'eighth'}, {note:'G4',solfege:'So',dur:'eighth'},
{note:'F4',solfege:'Fa'}, {note:'E4',solfege:'Mi'} ],
[ {note:'D4',solfege:'Re',dur:'eighth'}, {note:'E4',solfege:'Mi',dur:'eighth'},
{note:'F4',solfege:'Fa',dur:'eighth'}, {note:'G4',solfege:'So',dur:'eighth'},
{note:'A4',solfege:'La'}, {note:'G4',solfege:'So'}, {note:'F4',solfege:'Fa'}, {note:'E4',solfege:'Mi'} ],
[ {note:'C4',solfege:'Do',motive:true,dur:'sixteenth'}, {note:'E4',solfege:'Mi',motive:true,dur:'sixteenth'},
{note:'G4',solfege:'So',motive:true,dur:'sixteenth'}, {note:'E4',solfege:'Mi',dur:'sixteenth'},
{note:'F4',solfege:'Fa',dur:'eighth'}, {note:'G4',solfege:'So',dur:'eighth'},
{note:'A4',solfege:'La'}, {note:'B4',solfege:'Ti'} ],
[ {note:'C5',solfege:'Do',dur:'eighth'}, {note:'B4',solfege:'Ti',dur:'eighth'},
{note:'A4',solfege:'La',dur:'eighth'}, {note:'G4',solfege:'So',dur:'eighth'},
{note:'F4',solfege:'Fa'}, {note:'E4',solfege:'Mi'}, {note:'D4',solfege:'Re'}, {note:'C4',solfege:'Do'} ],
[ {note:'G4',solfege:'So',motive:true,dur:'sixteenth'}, {note:'B4',solfege:'Ti',motive:true,dur:'sixteenth'},
{note:'D5',solfege:'Re',motive:true,dur:'sixteenth'}, {note:'C5',solfege:'Do',dur:'sixteenth'},
{note:'B4',solfege:'Ti',dur:'eighth'}, {note:'A4',solfege:'La',dur:'eighth'},
{note:'G4',solfege:'So'}, {note:'F4',solfege:'Fa'} ],
[ {note:'E4',solfege:'Mi',dur:'eighth'}, {note:'D4',solfege:'Re',dur:'eighth'},
{note:'C4',solfege:'Do',dur:'half'}, {note:null,solfege:'–',rest:true,dur:'quarter'} ],
],
},
G: {
name: 'G · Waltz 3/4',
meter: 3,
bars: [
[ {note:'C4',solfege:'Do',motive:true}, {note:'E4',solfege:'Mi',motive:true}, {note:'G4',solfege:'So',motive:true} ],
[ {note:'A4',solfege:'La'}, {note:'G4',solfege:'So'}, {note:'F4',solfege:'Fa'} ],
[ {note:'E4',solfege:'Mi',motive:true}, {note:'G4',solfege:'So',motive:true}, {note:'C5',solfege:'Do',motive:true} ],
[ {note:'B4',solfege:'Ti'}, {note:'A4',solfege:'La'}, {note:'G4',solfege:'So'} ],
[ {note:'C4',solfege:'Do',motive:true}, {note:'E4',solfege:'Mi',motive:true}, {note:'G4',solfege:'So',motive:true} ],
[ {note:'E4',solfege:'Mi'}, {note:'F4',solfege:'Fa'}, {note:'G4',solfege:'So'} ],
[ {note:'G4',solfege:'So',motive:true}, {note:'B4',solfege:'Ti',motive:true}, {note:'D5',solfege:'Re',motive:true} ],
[ {note:'C5',solfege:'Do'}, {note:'G4',solfege:'So'}, {note:'C4',solfege:'Do'} ],
],
},
H: {
name: 'H · Compound 6/8',
meter: 6,
bars: [
[ {note:'C4',solfege:'Do',motive:true}, {note:'E4',solfege:'Mi',motive:true}, {note:'G4',solfege:'So',motive:true},
{note:'A4',solfege:'La'}, {note:'G4',solfege:'So'}, {note:'F4',solfege:'Fa'} ],
[ {note:'E4',solfege:'Mi'}, {note:'D4',solfege:'Re'}, {note:'C4',solfege:'Do'},
{note:'D4',solfege:'Re'}, {note:'E4',solfege:'Mi'}, {note:'F4',solfege:'Fa'} ],
[ {note:'E4',solfege:'Mi',motive:true}, {note:'G4',solfege:'So',motive:true}, {note:'C5',solfege:'Do',motive:true},
{note:'B4',solfege:'Ti'}, {note:'A4',solfege:'La'}, {note:'G4',solfege:'So'} ],
[ {note:'F4',solfege:'Fa'}, {note:'E4',solfege:'Mi'}, {note:'D4',solfege:'Re'},
{note:'C4',solfege:'Do'}, {note:'D4',solfege:'Re'}, {note:'E4',solfege:'Mi'} ],
[ {note:'C4',solfege:'Do',motive:true}, {note:'E4',solfege:'Mi',motive:true}, {note:'G4',solfege:'So',motive:true},
{note:'E4',solfege:'Mi'}, {note:'F4',solfege:'Fa'}, {note:'G4',solfege:'So'} ],
[ {note:'A4',solfege:'La'}, {note:'B4',solfege:'Ti'}, {note:'C5',solfege:'Do'},
{note:'B4',solfege:'Ti'}, {note:'A4',solfege:'La'}, {note:'G4',solfege:'So'} ],
[ {note:'G4',solfege:'So',motive:true}, {note:'B4',solfege:'Ti',motive:true}, {note:'D5',solfege:'Re',motive:true},
{note:'C5',solfege:'Do'}, {note:'B4',solfege:'Ti'}, {note:'A4',solfege:'La'} ],
[ {note:'G4',solfege:'So'}, {note:'E4',solfege:'Mi'}, {note:'C4',solfege:'Do'},
{note:'C4',solfege:'Do',dur:'dotted-half'}, {note:null,solfege:'–',rest:true}, {note:null,solfege:'–',rest:true} ],
],
},
I: {
name: 'I · Riff & long answers (4/4)',
meter: 4,
bars: [
[ {note:'C4',solfege:'Do',motive:true,dur:'eighth'}, {note:'E4',solfege:'Mi',motive:true,dur:'eighth'}, {note:'G4',solfege:'So',motive:true,dur:'eighth'}, {note:'A4',solfege:'La',dur:'eighth'},
{note:'G4',solfege:'So',dur:'eighth'}, {note:'F4',solfege:'Fa',dur:'eighth'}, {note:'E4',solfege:'Mi',dur:'eighth'}, {note:'D4',solfege:'Re',dur:'eighth'} ],
[ {note:'E4',solfege:'Mi',dur:'half'}, {note:'C4',solfege:'Do',dur:'half'} ],
[ {note:'E4',solfege:'Mi',motive:true,dur:'eighth'}, {note:'G4',solfege:'So',motive:true,dur:'eighth'}, {note:'C5',solfege:'Do',motive:true,dur:'eighth'}, {note:'B4',solfege:'Ti',dur:'eighth'},
{note:'A4',solfege:'La',dur:'eighth'}, {note:'G4',solfege:'So',dur:'eighth'}, {note:'F4',solfege:'Fa',dur:'eighth'}, {note:'E4',solfege:'Mi',dur:'eighth'} ],
[ {note:'D4',solfege:'Re',dur:'half'}, {note:'C4',solfege:'Do',dur:'half'} ],
[ {note:'C4',solfege:'Do',motive:true,dur:'eighth'}, {note:'E4',solfege:'Mi',motive:true,dur:'eighth'}, {note:'G4',solfege:'So',motive:true,dur:'eighth'}, {note:'E4',solfege:'Mi',dur:'eighth'},
{note:'F4',solfege:'Fa',dur:'eighth'}, {note:'G4',solfege:'So',dur:'eighth'}, {note:'A4',solfege:'La',dur:'eighth'}, {note:'B4',solfege:'Ti',dur:'eighth'} ],
[ {note:'C5',solfege:'Do',dur:'half'}, {note:'B4',solfege:'Ti',dur:'half'} ],
[ {note:'G4',solfege:'So',motive:true,dur:'eighth'}, {note:'B4',solfege:'Ti',motive:true,dur:'eighth'}, {note:'D5',solfege:'Re',motive:true,dur:'eighth'}, {note:'C5',solfege:'Do',dur:'eighth'},
{note:'B4',solfege:'Ti',dur:'eighth'}, {note:'A4',solfege:'La',dur:'eighth'}, {note:'G4',solfege:'So',dur:'eighth'}, {note:'F4',solfege:'Fa',dur:'eighth'} ],
[ {note:'E4',solfege:'Mi'}, {note:'D4',solfege:'Re'},
{note:'C4',solfege:'Do',dur:'half'}, {note:null,solfege:'–',rest:true,dur:'quarter'} ],
],
},
};
/* =========================================================
4. PITCH → FREQUENCY
========================================================= */
const A4 = 440;
const NOTE_INDEX = { C:0,'C#':1,D:2,'D#':3,E:4,F:5,'F#':6,G:7,'G#':8,A:9,'A#':10,B:11 };
function freq(name) {
if (!name) return null;
const m = name.match(/^([A-G]#?)(-?\d)$/);
if (!m) return null;
const semi = NOTE_INDEX[m[1]] + (parseInt(m[2], 10) - 4) * 12 - 9;
return A4 * Math.pow(2, semi / 12);
}
/* =========================================================
5. FLATTEN a version into a linear event list (in beats)
========================================================= */
function flatten(version) {
const events = [];
let time = 0; // current position in beats
version.bars.forEach((bar, barIdx) => {
const barStart = time;
bar.forEach(n => {
const beats = n.beats ?? DUR[n.dur ?? 'quarter'];
events.push({ ...n, bar: barIdx, start: time, beats });
time += beats;
});
// pad or advance to next full bar (in version.meter beats)
const barLen = version.meter;
const elapsed = time - barStart;
if (elapsed < barLen) {
time = barStart + barLen;
} else {
time = barStart + Math.ceil(elapsed / barLen) * barLen;
}
});
return { events, totalBeats: time };
}
/* =========================================================
6. RENDER
========================================================= */
const staffEl = document.getElementById('staff');
let noteEls = []; // noteEls[bar][index]
let cursorEl = null;
function render(version) {
staffEl.innerHTML = '';
noteEls = [];
version.bars.forEach((bar, b) => {
const barDiv = document.createElement('div');
barDiv.className = 'bar';
barDiv.dataset.bar = b;
// bar label + chord
const label = document.createElement('div');
label.className = 'bar-label';
const h = HARMONY[b] || { label: '', roman: '' };
label.innerHTML = `<div class="chord">${h.label}</div><div class="num">${b + 1} · ${h.roman}</div>`;
barDiv.appendChild(label);
const row = [];
// Render by visual weight (not exact beats) for readability
bar.forEach((n, i) => {
const el = document.createElement('div');
const classes = ['note'];
if (n.rest) classes.push('rest');
if (n.motive) classes.push('motive');
if (n.tuplet) classes.push('tuplet');
el.className = classes.join(' ');
if (n.rest) {
el.innerHTML = `–<span class="solfege">rest</span>`;
} else {
el.innerHTML = `<div class="name">${n.note}</div><div class="solfege">${n.solfege}</div>`;
}
if (n.tuplet) el.dataset.tuplet = n.tuplet;
// visual flex weight based on duration (min 1)
const beats = n.beats ?? DUR[n.dur ?? 'quarter'];
el.style.flexGrow = String(Math.max(1, beats * 2));
barDiv.appendChild(el);
row.push(el);
});
noteEls.push(row);
// cursor line inside bar (for active bar)
const cur = document.createElement('div');
cur.className = 'cursor';
cur.style.display = 'none';
barDiv.appendChild(cur);
staffEl.appendChild(barDiv);
});
// Bass & chord rows
const bassRow = document.createElement('div');
bassRow.className = 'bass-row';
bassRow.innerHTML = '<div class="label">BASS</div>' +
HARMONY.map((h, i) => `<div class="cell" data-bass="${i}">${h.bass}</div>`).join('');
staffEl.appendChild(bassRow);
const chordRow = document.createElement('div');
chordRow.className = 'chord-row';
chordRow.innerHTML = '<div class="label">CHORD</div>' +
HARMONY.map((h, i) => `<div class="cell" data-chord="${i}">${h.chord.join(' ')}</div>`).join('');
staffEl.appendChild(chordRow);
// store row elements for highlighting
bassCells = bassRow.querySelectorAll('.cell');
chordCells = chordRow.querySelectorAll('.cell');
}
let bassCells = [], chordCells = [];
/* =========================================================
7. AUDIO ENGINE
========================================================= */
let audioCtx = null, masterGain = null;
let isPlaying = false, stopRequested = false;
let timeouts = [];
let currentBarEl = null, currentCursorEl = null;
function ensureAudio() {
if (!audioCtx) {
audioCtx = new (window.AudioContext || window.webkitAudioContext)();
masterGain = audioCtx.createGain();
masterGain.gain.value = 0.24;
masterGain.connect(audioCtx.destination);
}
if (audioCtx.state === 'suspended') audioCtx.resume();
}
function playTone(frequency, startTime, durationSec, opts = {}) {
const { gain = 1, type = 'triangle', detune = 4 } = opts;
if (!frequency || durationSec <= 0) return;
const osc1 = audioCtx.createOscillator();
const osc2 = audioCtx.createOscillator();
const gainNode = audioCtx.createGain();
const g2 = audioCtx.createGain();
g2.gain.value = 0.22;
osc1.type = type;
osc2.type = 'sine';
osc1.frequency.value = frequency;
osc2.frequency.value = frequency * 2.001;
osc2.detune.value = detune;
const t = startTime;
const attack = 0.012;
const release = Math.min(0.3, durationSec * 0.5);
const peak = 0.9 * gain;
gainNode.gain.setValueAtTime(0, t);
gainNode.gain.linearRampToValueAtTime(peak, t + attack);
gainNode.gain.setTargetAtTime(0.55 * peak, t + attack, 0.22);
gainNode.gain.setTargetAtTime(0, Math.max(t + attack, t + durationSec - release), release / 3);
osc1.connect(gainNode);
osc2.connect(g2).connect(gainNode);
gainNode.connect(masterGain);
osc1.start(t);
osc2.start(t);
const stopAt = t + durationSec + 0.06;
osc1.stop(stopAt);
osc2.stop(stopAt);
}
function playBass(frequency, startTime, durationSec) {
playTone(frequency, startTime, durationSec, { gain: 0.9, type: 'sine', detune: 0 });
}
function playChord(frequencies, startTime, durationSec) {
frequencies.forEach(f => playTone(f, startTime, durationSec, { gain: 0.35, type: 'triangle', detune: 3 }));
}
function clearTimeouts() {
timeouts.forEach(clearTimeout);
timeouts = [];
if (currentBarEl) currentBarEl.classList.remove('active');
currentBarEl = null;
if (currentCursorEl) currentCursorEl.style.display = 'none';
currentCursorEl = null;
noteEls.flat().forEach(el => el.classList.remove('playing'));
bassCells.forEach(c => c.classList.remove('playing'));
chordCells.forEach(c => c.classList.remove('playing'));
}
function setUIState(playing) {
document.getElementById('playBtn').disabled = playing;
document.getElementById('stopBtn').disabled = !playing;
document.getElementById('playBtn').textContent = playing ? '▶ Playing…' : '▶ Play';
}
/* =========================================================
8. PLAY
========================================================= */
async function play() {
if (isPlaying) return;
ensureAudio();
isPlaying = true;
stopRequested = false;
setUIState(true);
const versionKey = document.getElementById('version').value;
const version = VERSIONS[versionKey];
const bpm = parseInt(document.getElementById('tempo').value, 10);
const beatSec = 60 / bpm;
const useBass = document.getElementById('withBass').checked;
const useChords = document.getElementById('withChords').checked;
const loop = document.getElementById('loop').checked;
const { events, totalBeats } = flatten(version);
const barBeats = version.meter;
do {
clearTimeouts();
const startAt = audioCtx.currentTime + 0.09;
const barCount = version.bars.length;
// Schedule melody notes
events.forEach(ev => {
const beatStart = startAt + ev.start * beatSec;
const dur = ev.beats * beatSec * 0.94;
if (ev.note) {
playTone(freq(ev.note), beatStart, dur, { gain: 1 });
}
const delayMs = (beatStart - audioCtx.currentTime) * 1000;
const t = setTimeout(() => {
const el = noteEls[ev.bar]?.[ev.index ?? findNoteIndex(version, ev)];
// Use stored ref via a stable index — we tagged via flatten
}, 0);
clearTimeout(t);
});
// Re-schedule with correct element references by re-walking bars
let runningBeat = 0;
version.bars.forEach((bar, barIdx) => {
const barStart = startAt + runningBeat * beatSec;
// Bar highlight (on)
const onT = setTimeout(() => {
if (currentBarEl) currentBarEl.classList.remove('active');
const barEl = staffEl.children[barIdx];
if (barEl) {
barEl.classList.add('active');
currentBarEl = barEl;
currentCursorEl = barEl.querySelector('.cursor');
if (currentCursorEl) {
currentCursorEl.style.display = 'block';
currentCursorEl.style.left = '40px';
}
}
}, Math.max(0, (barStart - audioCtx.currentTime) * 1000));
timeouts.push(onT);
// Bass & chords per bar
if (useBass) {
playBass(freq(HARMONY[barIdx].bass), barStart, barBeats * beatSec * 0.95);
const bt = setTimeout(() => {
bassCells.forEach(c => c.classList.remove('playing'));
bassCells[barIdx]?.classList.add('playing');
}, Math.max(0, (barStart - audioCtx.currentTime) * 1000));
timeouts.push(bt);
}
if (useChords) {
playChord(HARMONY[barIdx].chord.map(freq), barStart, barBeats * beatSec * 0.9);
const ct = setTimeout(() => {
chordCells.forEach(c => c.classList.remove('playing'));
chordCells[barIdx]?.classList.add('playing');
}, Math.max(0, (barStart - audioCtx.currentTime) * 1000));
timeouts.push(ct);
}
// Note-by-note within bar
let localBeat = 0;
bar.forEach((n, noteIdx) => {
const beats = n.beats ?? DUR[n.dur ?? 'quarter'];
const noteStart = barStart + localBeat * beatSec;
const dur = beats * beatSec * 0.94;
const ht = setTimeout(() => {
noteEls[barIdx]?.forEach(el => el.classList.remove('playing'));
noteEls[barIdx]?.[noteIdx]?.classList.add('playing');
}, Math.max(0, (noteStart - audioCtx.currentTime) * 1000));
timeouts.push(ht);
localBeat += beats;
});
// Advance running beat to full bar length
const elapsed = bar.reduce((s, n) => s + (n.beats ?? DUR[n.dur ?? 'quarter']), 0);
runningBeat += Math.max(barBeats, Math.ceil(elapsed / barBeats) * barBeats);
});
const songMs = totalBeats * beatSec * 1000;
document.getElementById('status').textContent =
`Playing ${version.name} · ${bpm} BPM · ${totalBeats} beats · ~${(songMs/1000).toFixed(1)}s`;
await new Promise(res => {
const t = setTimeout(res, songMs + 250);
timeouts.push(t);
});
if (loop && !stopRequested) {
clearTimeouts();
await new Promise(res => {
const t = setTimeout(res, 350);
timeouts.push(t);
});
}
} while (loop && !stopRequested);
if (!stopRequested) document.getElementById('status').textContent = 'Done.';
isPlaying = false;
clearTimeouts();
setUIState(false);
}
// Helper (not actively used but kept for safety)
function findNoteIndex(version, ev) {
return 0;
}
function stop() {
stopRequested = true;
if (masterGain && audioCtx) {
try {
masterGain.gain.cancelScheduledValues(audioCtx.currentTime);
masterGain.gain.setValueAtTime(0, audioCtx.currentTime);
setTimeout(() => { if (masterGain) masterGain.gain.value = 0.24; }, 60);
} catch(e) {}
}
clearTimeouts();
isPlaying = false;
setUIState(false);
document.getElementById('status').textContent = 'Stopped.';
}
/* =========================================================
9. UI WIRING
========================================================= */
const versionSelect = document.getElementById('version');
Object.entries(VERSIONS).forEach(([k, v]) => {
const opt = document.createElement('option');
opt.value = k;
opt.textContent = v.name;
versionSelect.appendChild(opt);
});
function updateInfo() {
const v = VERSIONS[versionSelect.value];
const bpm = parseInt(document.getElementById('tempo').value, 10);
const beatSec = 60 / bpm;
const { totalBeats } = flatten(v);
document.getElementById('infoMeter').textContent = `${v.meter}/4`;
document.getElementById('infoBars').textContent = v.bars.length;
document.getElementById('infoVer').textContent = versionSelect.value;
document.getElementById('infoDur').textContent = `~${(totalBeats * beatSec).toFixed(1)}s`;
}
function loadVersion() {
const v = VERSIONS[versionSelect.value];
render(v);
updateInfo();
}
versionSelect.addEventListener('change', loadVersion);
const tempoSlider = document.getElementById('tempo');
const tempoVal = document.getElementById('tempoVal');
tempoSlider.addEventListener('input', () => {
tempoVal.textContent = tempoSlider.value;
updateInfo();
});
document.getElementById('withBass').addEventListener('change', e => {
document.querySelector('.bass-row').style.opacity = e.target.checked ? '1' : '.25';
});
document.getElementById('withChords').addEventListener('change', e => {
document.querySelector('.chord-row').style.opacity = e.target.checked ? '1' : '.25';
});
document.getElementById('playBtn').addEventListener('click', play);
document.getElementById('stopBtn').addEventListener('click', stop);
// Init
loadVersion();
</script>
</body>
</html>
What’s new vs. the previous version
| Feature | Before | Now |
|---|---|---|
| Rhythm versions | 1 (quarters) | 9 (A–I) with dropdown |
| Duration system | Fixed quarter notes | Full beat-value table (beats or dur per note) |
| Meter support | 4/4 only | 4/4, 3/4, 6/8 |
| Tuplets | None | Triplets rendered + scheduled |
| Swing | None | Long/short eighth timing |
| Bass line | None | Auto root notes (toggle) |
| Chord accompaniment | None | Auto block chords (toggle) |
| Bar labels | Bar number only | Bar + chord symbol + Roman numeral |
| Highlighting | Note only | Bar + note + bass cell + chord cell |
| Info badges | None | Meter, bars, duration, version |
| Scrolling staff | Fixed | Scrollable for long versions |
Version quick-reference
| Key | Name | Meter | Character |
|---|---|---|---|
| A | Plain quarters | 4/4 | Baseline |
| B | Swing eighths | 4/4 | Jazzy |
| C | Triplet arpeggio | 4/4 | Flowing |
| D | Syncopated eighths | 4/4 | Groovy |
| E | Dotted long–short | 4/4 | Lilting |
| F | Sixteenth flourish | 4/4 | Energetic |
| G | Waltz | 3/4 | Elegant |
| H | Compound | 6/8 | Rolling |
| I | Riff & answers | 4/4 | Call-response |
Things you can tweak
- Harmony per bar: edit the
HARMONYarray — swap chords, add 7ths, change bass octaves. - Add a new version: drop a new key into
VERSIONSfollowing the same shape. - Instrument change: modify
type:inplayTone('sine','square','sawtooth','triangle'). - Mix balance: adjust
masterGain.gain.value(currently0.24) or per-partgainvalues inplayTone.
Want me to add MIDI export, swing quantization toggle, or a reverb/delay effects bus next?