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 &amp; 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 HARMONY array — swap chords, add 7ths, change bass octaves.
  • Add a new version: drop a new key into VERSIONS following the same shape.
  • Instrument change: modify type: in playTone ('sine', 'square', 'sawtooth', 'triangle').
  • Mix balance: adjust masterGain.gain.value (currently 0.24) or per-part gain values in playTone.

Want me to add MIDI export, swing quantization toggle, or a reverb/delay effects bus next?