{"slug": "building-a-chord-progression-generator-in-the-browser-music-theory-in-js-sound", "title": "Building a Chord Progression Generator in the Browser — Music Theory in JS, Sound via Web Audio API", "summary": "This article describes a browser-based chord progression generator built with approximately 300 lines of vanilla JavaScript and the Web Audio API, with no external dependencies. The tool generates progressions across 12 keys, 2 scales, and 7 genre presets by deriving chord tones from scale intervals and dynamically naming chords based on interval patterns rather than hardcoded tables. The audio engine uses a simple 30-line `ChordPlayer` class that schedules oscillator-based triangle wave playback through a lowpass filter.", "body_md": "Pop songs, jazz turnarounds, Pachelbel's canon. Chord progressions you've heard a thousand times all reduce to\n\nscale degrees stacked on a diatonic scale. This tool generates progressions in 12 keys × 2 scales × 7 genre presets and plays them through the Web Audio API. ~300 lines of vanilla JS, zero dependencies.\n\n🌐 **Demo**: [https://sen.ltd/portfolio/chord-progression-gen/](https://sen.ltd/portfolio/chord-progression-gen/)\n\n📦 **GitHub**: [https://github.com/sen-ltd/chord-progression-gen](https://github.com/sen-ltd/chord-progression-gen)\n\n## The minimum music theory you need\n\nIf \"music theory\" sounds heavy, the slice you actually need to write code is surprisingly small.\n\n### 1. A scale is just 7 notes\n\nThe C major scale is C, D, E, F, G, A, B. In semitones from the root: ** [0, 2, 4, 5, 7, 9, 11]**. That's it.\n\n``` js\nconst MAJOR_SCALE = [0, 2, 4, 5, 7, 9, 11];\nconst NATURAL_MINOR = [0, 2, 3, 5, 7, 8, 10];\n```\n\nNatural minor (C, D, E♭, F, G, A♭, B♭) is `[0, 2, 3, 5, 7, 8, 10]`\n\n.\n\n### 2. A chord stacks the 1st, 3rd, and 5th degrees (diatonic triad)\n\nTake notes 1, 3, 5 of the scale: C, E, G → **C major triad**. Start from note 2 instead: D, F, A → **D minor**. The starting position is the \"scale degree\" and we label it with a roman numeral:\n\n| Degree | C major | Quality |\n|---|---|---|\n| I | C | Major |\n| ii | Dm | minor |\n| iii | Em | minor |\n| IV | F | Major |\n| V | G | Major |\n| vi | Am | minor |\n| vii° | B° | diminished |\n\nUppercase = major, lowercase = minor, `°`\n\n= diminished. **Every chord progression in this app is a sequence of these seven**.\n\n### 3. A progression is just an ordered list of degrees\n\n-\n**Pop**: I → V → vi → IV (in C: C → G → Am → F — the \"Axis of Awesome\" progression) -\n**Pachelbel's Canon**: I → V → vi → iii → IV → I → IV → V -\n**Jazz turnaround**: ii → V → I → vi\n\n## Building the chord\n\nThe triad-building function:\n\n```\nexport function chordTones(tonic, scaleName, degree, seventh = false) {\n  const scale = SCALES[scaleName];\n  const positions = seventh ? [0, 2, 4, 6] : [0, 2, 4];\n  return positions.map((p) => {\n    const idx = degree + p;\n    const octaveShift = Math.floor(idx / 7) * 12;\n    return tonic + octaveShift + scale.intervals[idx % 7];\n  });\n}\n```\n\n`tonic`\n\nis the MIDI number of the root (C4 = 60). When `degree + p`\n\nwalks past the seventh scale note we need to wrap to the next octave, hence `Math.floor(idx / 7) * 12`\n\n. Building vi (Am) in C major: `chordTones(60, \"major\", 5)`\n\nreturns `[69, 72, 76]`\n\n— A4, C5, E5.\n\nFor seventh chords, change `positions`\n\nto `[0, 2, 4, 6]`\n\n. The chord **quality** (maj7 / 7 / m7 / m7♭5) we derive from the interval pattern afterwards.\n\n## Naming chords without a lookup table\n\nWe could hardcode \"Am\" → \"minor on A\", \"Bm7♭5\" → \"half-diminished on B\" and so on. But that table has 12 keys × 7 degrees × 4 qualities × {triad, seventh} entries, and it doesn't extend when you add modes (Dorian, Phrygian, harmonic minor).\n\nBetter: **derive the name from the interval pattern** of the constructed chord:\n\n``` js\nexport function chordName(tonic, scaleName, degree, seventh = false) {\n  const tones = chordTones(tonic, scaleName, degree, seventh);\n  const rootPc = ((tones[0] % 12) + 12) % 12;\n  const third = tones[1] - tones[0];\n  const fifth = tones[2] - tones[0];\n  let quality = \"\";\n  if (third === 4 && fifth === 7) quality = \"\";      // Major\n  else if (third === 3 && fifth === 7) quality = \"m\"; // minor\n  else if (third === 3 && fifth === 6) quality = \"°\"; // dim\n  else if (third === 4 && fifth === 8) quality = \"+\"; // aug\n  // ... seventh logic continues\n  return NOTE_NAMES[rootPc] + quality;\n}\n```\n\nMajor third (4 semis) + perfect fifth (7 semis) → Major. Minor third (3) + perfect fifth (7) → minor. Minor third (3) + diminished fifth (6) → diminished. **Names fall out of the intervals**, so adding Dorian or harmonic minor later doesn't require touching the naming logic at all.\n\n## Playing it through Web Audio\n\nThe audio engine is the kind of thing that *looks* complicated and turns out to be 30 lines.\n\n``` js\nimport { midiToHz } from \"./theory.js\";\n\nclass ChordPlayer {\n  constructor() {\n    this.ctx = null;\n  }\n\n  ensureContext() {\n    if (this.ctx) return;\n    this.ctx = new AudioContext();\n    this.master = this.ctx.createGain();\n    this.master.gain.value = 0.18;\n    const filter = this.ctx.createBiquadFilter();\n    filter.type = \"lowpass\";\n    filter.frequency.value = 2400;\n    this.master.connect(filter);\n    filter.connect(this.ctx.destination);\n  }\n\n  scheduleChord(tones, startTime, duration) {\n    for (const midi of tones) {\n      const osc = this.ctx.createOscillator();\n      osc.type = \"triangle\";\n      osc.frequency.value = midiToHz(midi);\n      const gain = this.ctx.createGain();\n      gain.gain.setValueAtTime(0, startTime);\n      gain.gain.linearRampToValueAtTime(1 / tones.length, startTime + 0.02);\n      gain.gain.linearRampToValueAtTime(0, startTime + duration);\n      osc.connect(gain);\n      gain.connect(this.master);\n      osc.start(startTime);\n      osc.stop(startTime + duration + 0.05);\n    }\n  }\n}\n```\n\nFour small choices matter:\n\n### 1. Use `triangle`\n\noscillators\n\n`sawtooth`\n\nis too harsh, `sine`\n\nis too thin, `square`\n\nsounds like a Game Boy. `triangle`\n\nlands in the middle and reads as \"acoustic-ish\" without effort.\n\n### 2. ADSR can be trivial\n\nReal pianos and strings have complex envelopes. For chord progressions you don't need them. **A 20 ms attack and a short linear release is enough** to avoid clicks and sound natural:\n\n```\nvolume\n 1.0 │   /‾‾‾‾‾‾‾‾‾‾‾\\\n     │  /              \\\n 0.0 │_/                \\____\n     ├──┼──────────────┼─┼──→ time\n        20ms          release start\n```\n\n### 3. Divide gain by chord size\n\nStacking 3 or 4 oscillators at full volume sums to clipping. Multiply each oscillator's gain by `1 / tones.length`\n\nand the master stays well-behaved no matter how many voices you add.\n\n### 4. Resume the context on user gesture\n\nChrome's autoplay policy refuses to let `AudioContext`\n\nmake sound until the user has interacted with the page. If you call `osc.start()`\n\nbefore `ctx.resume()`\n\n, the first chord goes silently into the void:\n\n```\nasync resume() {\n  this.ensureContext();\n  if (this.ctx.state === \"suspended\") await this.ctx.resume();\n}\n```\n\nSkip this and you get the classic \"works locally, silent in production\" trap.\n\n## Architecture\n\n```\ntheory.js   ← Music theory (pure, zero DOM/Audio dependencies)\naudio.js    ← Web Audio scheduler (consumes MIDI numbers from theory.js)\napp.js      ← UI glue (DOM events → theory → audio)\n```\n\nNothing in `theory.js`\n\nknows about the DOM or about `AudioContext`\n\n. That means Node's built-in test runner can verify all 25 cases of the music logic without a browser:\n\n``` python\nimport { test } from \"node:test\";\nimport assert from \"node:assert/strict\";\nimport { buildProgression, tonicMidi, PRESETS } from \"../theory.js\";\n\ntest(\"Pop in C major yields C G Am F\", () => {\n  const out = buildProgression({\n    tonic: tonicMidi(\"C\", 4),\n    scaleName: \"major\",\n    degrees: PRESETS.pop.degrees,\n  });\n  assert.deepEqual(out.map((c) => c.name), [\"C\", \"G\", \"Am\", \"F\"]);\n});\n```\n\n`audio.js`\n\nconsumes the **output of theory.js** (MIDI number arrays) and has no opinion about scales or roman numerals. The dependency arrow only points one direction. When you eventually add Dorian, harmonic minor, secondary dominants, or jazz substitutions, the audio layer doesn't change at all —\n\n`theory.js`\n\nkeeps emitting MIDI, the rest keeps playing it.`app.js`\n\nis a tiny coordinator: UI event → mutate `state`\n\n→ rebuild progression via `theory`\n\n→ optionally play through `audio`\n\n. No React, no Vue, no signals. The whole state is a plain object.\n\n## Try it\n\n-\n**Demo**:[https://sen.ltd/portfolio/chord-progression-gen/](https://sen.ltd/portfolio/chord-progression-gen/)(try the Pachelbel preset in C major with 7ths on) -\n**GitHub**:[https://github.com/sen-ltd/chord-progression-gen](https://github.com/sen-ltd/chord-progression-gen)\n\n## Takeaways\n\n- Chord progressions reduce to\n**7 notes × scale degrees × roman numerals**— small enough to fit in your head - Deriving chord names from\n**interval relationships** instead of a lookup table makes the music-theory module trivially extensible - The Web Audio API plays chord progressions cleanly with\n**triangle waves + minimal ADSR + autoplay-policy handling**— that's the whole synthesis layer - Keeping music theory pure (no DOM, no audio) means\n`node --test`\n\ncan verify all the harmony rules without spinning up a browser\n\nThis is OSS portfolio #240 from SEN LLC (Tokyo). We ship small, sharp tools continuously: [https://sen.ltd/portfolio/](https://sen.ltd/portfolio/)", "url": "https://wpnews.pro/news/building-a-chord-progression-generator-in-the-browser-music-theory-in-js-sound", "canonical_source": "https://dev.to/sendotltd/building-a-chord-progression-generator-in-the-browser-music-theory-in-js-sound-via-web-audio-api-519j", "published_at": "2026-05-22 22:53:30+00:00", "updated_at": "2026-05-22 23:34:26.496655+00:00", "lang": "en", "topics": ["developer-tools", "open-source", "products"], "entities": ["Web Audio API", "GitHub"], "alternates": {"html": "https://wpnews.pro/news/building-a-chord-progression-generator-in-the-browser-music-theory-in-js-sound", "markdown": "https://wpnews.pro/news/building-a-chord-progression-generator-in-the-browser-music-theory-in-js-sound.md", "text": "https://wpnews.pro/news/building-a-chord-progression-generator-in-the-browser-music-theory-in-js-sound.txt", "jsonld": "https://wpnews.pro/news/building-a-chord-progression-generator-in-the-browser-music-theory-in-js-sound.jsonld"}}