
Створення Генератора Прогресій Акордів у Браузері — Теорія Музики на JS, Звук через Web Audio API
https://ift.tt/YpI5DNh
«Поп-пісні, джазові розв’язки, канон Пахельбеля. Прогресії акордів, які ви чули тисячу разів, зводяться до складання по діатонічній шкалі. Цей інструмент генерує прогресії у 12 ключах × 2 шкали × 7 жанрових пресетів і програє їх через Web Audio API. Близько 300 рядків чистого JavaScript, ніяких залежностей.»
🌐 Демо: https://ift.tt/90S1Lkn
📦 GitHub: https://ift.tt/azhYuGW
The minimum music theory you need
Якщо «музична теорія» звучить важко, то частина, яку дійсно потрібно знати для написання коду, на диво невелика.
1. Скала — це всього 7 нот
C-мінорна скала: C, D, E, F, G, A, B. У півтонах від корня: [0, 2, 4, 5, 7, 9, 11]. Ось і все.
const MAJOR_SCALE = [0, 2, 4, 5, 7, 9, 11];
const NATURAL_MINOR = [0, 2, 3, 5, 7, 8, 10];
Natural minor (C, D, E♭, F, G, A♭, B♭) is [0, 2, 3, 5, 7, 8, 10].
2. Акорд складає 1-й, 3-й та 5-й ступені (діатонічний триад)
Візьмемо ноти 1, 3, 5 зі шкали: C, E, G → C major triad. Починаючи з ноти 2: D, F, A → D minor. Початкова позиція називається «ступінь шкали» і позначається римською цифрою:
Діапазон таблиці:
Degree | C major | Quality
I | C | Major
ii | Dm | minor
iii | Em | minor
IV | F | Major
V | G | Major
vi | Am | minor
vii° | B° | diminished
Верхній регістр означає мажор, нижній — мінор, ° — зменшений. Будь-яка прогресія в цьому додатку є послідовністю цих семи.
3. Прогресія — це просто впорядкований список ступенів
– Поп: I → V → vi → IV (у C: C → G → Am → F — прогресія «Axis of Awesome»)
– Пachelbel’s Canon: I → V → vi → iii → IV → I → IV → V
– Джазовий розв’язок: ii → V → I → vi
Building the chord
Функція побудови триади:
export function chordTones(tonic, scaleName, degree, seventh = false) {
const scale = SCALES[scaleName];
const positions = seventh ? [0, 2, 4, 6] : [0, 2, 4];
return positions.map((p) => {
const idx = degree + p;
const octaveShift = Math.floor(idx / 7) * 12;
return tonic + octaveShift + scale.intervals[idx % 7];
});
}
tonic — MIDI-номер через якийсь корінь (C4 = 60). Коли degree + p заходитиме за сім нот скали, потрібно повернутися на наступну октаву: Math.floor(idx / 7) * 12. Побудова vi (Am) у C-дорі: chordTones(60, “major”, 5) повертає [69, 72, 76] — A4, C5, E5.
Для септакорд змініть positions на [0, 2, 4, 6]. Якість акорду (maj7 / 7 / m7 / m7♭5) отримується пізніше з шаблону інтервалів.
Naming chords without a lookup table
Можна було зашити «Am» → «мінор на A», «Bm7♭5» → «напівзменшений на B» тощо. Але така таблиця має 12 ключів × 7 ступенів × 4 якості × {триада, септакорд} записів, і не розширюється коли додаються моди (Dorian, Phrygian, гармонійна мінорна).
Краще: виводити назву з інтервалів побудованого акорду:
export function chordName(tonic, scaleName, degree, seventh = false) {
const tones = chordTones(tonic, scaleName, degree, seventh);
const rootPc = ((tones[0] % 12) + 12) % 12;
const third = tones[1] – tones[0];
const fifth = tones[2] – tones[0];
let quality = “”;
if (third === 4 && fifth === 7) quality = “”; // Major
else if (third === 3 && fifth === 7) quality = “m”; // minor
else if (third === 3 && fifth === 6) quality = “°”; // diminished
else if (third === 4 && fifth === 8) quality = “+”; // aug
// … логіка септакорда продовжується
return NOTE_NAMES[rootPc] + quality;
}
Major third (4 semitones) + perfect fifth (7 semitones) → Major. Minor third (3) + perfect fifth (7) → minor. Minor third (3) + diminished fifth (6) → diminished. Назви виходять з інтервалів, тому додавання Dorian чи гармонійної мінорності пізніше не потребує зміни логіки називання.
Playing it through Web Audio
Аудіоядро — це те, що виглядає складним, але на практиці воно близько 30 рядків.
import { midiToHz } from “./theory.js”;
class ChordPlayer {
constructor() {
this.ctx = null;
}
ensureContext() {
if (this.ctx) return;
this.ctx = new AudioContext();
this.master = this.ctx.createGain();
this.master.gain.value = 0.18;
const filter = this.ctx.createBiquadFilter();
filter.type = “lowpass”;
filter.frequency.value = 2400;
this.master.connect(filter);
filter.connect(this.ctx.destination);
}
scheduleChord(tones, startTime, duration) {
for (const midi of tones) {
const osc = this.ctx.createOscillator();
osc.type = “triangle”;
osc.frequency.value = midiToHz(midi);
const gain = this.ctx.createGain();
gain.gain.setValueAtTime(0, startTime);
gain.gain.linearRampToValueAtTime(1 / tones.length, startTime + 0.02);
gain.gain.linearRampToValueAtTime(0, startTime + duration);
osc.connect(gain);
gain.connect(this.master);
osc.connect(gain);
osc.start(startTime);
osc.stop(startTime + duration + 0.05);
}
}
}
Four small choices matter:
1. Use triangle oscillators
sawtooth terlalu різкий, sine занадто тонкий, square звучить як Game Boy. triangle розміщується посередині і звучить як наближено «акустично».
2. ADSR може бути тривіальним
Реальні піаніно та струни мають складні огиначі. Для прогресій акордів їх не потрібно. 20 мс атаки і коротке лінійне розпускання достатні щоб уникнути кликів і звучати природно:
volume
1.0 │ /‾‾‾‾‾‾‾‾‾‾‾\
│ / \
0.0 │_/ \____
├──┼──────────────┼─┼──→ time
20ms release start
3. ДілиGain по розміру акорду
Stacking 3 або 4 осцилятори на повну гучність призводить до клиппінгу. Помножте кожному осцилятору gain на 1 / tones.length, і мастер залишатиметься адекватним незалежно від кількості голосів.
4. Відновлювати контекст після взаємодії з користувачем
Політика Chrome щодо автозапуску не дозволяє AudioContext видавати звук, доки користувач не взаємодіє зі сторінкою. Якщо викликати osc.start() перед ctx.resume(), перший акорд безмовно зникне:
async resume() {
this.ensureContext();
if (this.ctx.state === “suspended”) await this.ctx.resume();
}
Пропустіть це і отримаєте класичну пастку «робить локально — безшумно у продакшн».
Architecture
theory.js — Теорія музики (чиста, без DOM/Audio залежностей)
audio.js — Планувальник Web Audio (обробляє MIDI номери з theory.js)
app.js — Зв’язок UI (події DOM → теорія → аудіо)
Ніщо в theory.js не знає про DOM або AudioContext. Це означає, що вбудований у вузлі тест-раннер може перевірити всі 25 випадків музичної логіки без браузера.
import { test } from “node:test”;
import assert from “node:assert/strict”;
import { buildProgression, tonicMidi, PRESETS } from “../theory.js”;
test(“Pop in C major yields C G Am F”, () => {
const out = buildProgression({
tonic: tonicMidi(“C”, 4),
scaleName: “major”,
degrees: PRESETS.pop.degrees,
});
assert.deepEqual(out.map((c) => c.name), [“C”, “G”, “Am”, “F”]);
});
audio.js consumes 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 — theory.js keeps emitting MIDI, the rest keeps playing it.
app.js is a tiny coordinator: UI event → mutate state → rebuild progression via theory → optionally play through audio. No React, no Vue, no signals. The whole state is a plain object.
Try it
Demo: https://ift.tt/90S1Lkn (try the Pachelbel preset in C major with 7ths on)
GitHub: https://ift.tt/azhYuGW
Takeaways
– Прогресії акордів зводяться до 7 нот × ступені скали × римські цифри — досить, щоб тримати в голові
– Виводити назви акордів з відносин інтервалів замість таблиці назв робить музичну теорію модулю extensible
– Web Audio API відтворює прогресії чисто з трикутними хвилями + мінімальний ADSR + обробка autoplay — це увесь синтез
– Тримати музичну теорію чистою (без DOM, без аудіо) означає, що node --test може перевіряти всі правила гармонії без запуску браузера
Це OSS-портфоліо №240 від SEN LLC (Токіо). Ми постійно випускаємо маленькі, різкі інструменти: https://ift.tt/8KBO6uI
HI-FI News
via DEV Community: javascript https://ift.tt/S6rhg7U
23 травня 2026 о 01:06
May 23, 2026 at 01:06AM

Залишити відповідь