Muziektheorie voor programmeurs

Het probleem was nooit het oefenen. Het was dat elke uitleg over muziektheorie leek te passen op het negeren van de fundamentele redenen voor hoe en waarom dingen zijn zoals ze zijn. "Hier is een notenbalk. Hier staan de noten op. Dit is een majeurtoonladder, memoriseer het patroon." Waarom die noten? Waarom dat patroon? "Omdat dat de conventie is."

Dat is een vreemde manier om een systeem te onderwijzen dat in essentie voortkomt uit natuurkunde en rekenkunde. Er zijn twaalf noten met een reden. De majeurtoonladder heeft de vorm die hij heeft met een reden. Akkoorden die goed klinken, klinken goed met een reden, en je kunt die redenen berekenen.

Ik wilde daarom vanaf nul beginnen en muziek leren vanuit basisprincipes (first principles), en ik begon die reis door code te schrijven.

Dit artikel is het resultaat. Het begint met een enkel getal dat in de loop van de tijd verandert. Als je meevolgt, zul je de twaalf noten afleiden, toonladders en akkoorden bouwen uit arrays, en een akkoordenschema schrijven dat klinkt als echte muziek. Er is geen instrument nodig, en er is niets wat je op basis van vertrouwen moet accepteren. Notatie komt wel aan de orde, maar pas aan het einde, zodra er iets is waar notatie voor bedoeld is.

Geluid is een getal dat in de loop van de tijd verandert

Geluid is simpelweg trillende luchtdruk. Een luidspreker maakt geluid door zijn conus in en uit te duwen. Alles wat je computer met audio doet, komt neer op het produceren van een lijst met getallen die beschrijven waar die conus moet zijn, veertigduizend keer per seconde.

Een audiobestand is die lijst in geschreven vorm. Een synthesizer verzint de lijst terwijl hij bezig is, en de browser doet dat deel voor je als je aangeeft welke vorm je wilt. De eenvoudigste vorm is een sinusgolf; hier is er een die 440 keer per seconde herhaalt:

Een sinusgolf op 440Hz

const osc = ctx.createOscillator();
osc.frequency.value = 440;
osc.connect(out);
osc.start();
osc.stop(ctx.currentTime + 1);

ctx en out zijn in dit voorbeeld eigen variabelen in plaats van die van de browser. Alles overig is de Web Audio API zoals deze geleverd wordt. Om dit snippet elders uit te voeren, begin je met:

const ctx = new AudioContext();
const out = ctx.destination;

Het getal 440 is het enige dat muzikale betekenis heeft, en zelfs dat is arbitrair. Het is de frequentie die men heeft afgesproken "A" te noemen, en het is de stemvork waaraan de rest van het systeem is vastgepind. Verander het naar 300 en voer het opnieuw uit. Het werkt nog steeds, maar het speelt een andere toonhoogte. De oscillator heeft geen idee wat een noot is; hij weet alleen hoe vaak per seconde hij moet zwaaien.

Frequentie is toonhoogte: een hoger getal betekent een hogere noot. Niets anders in de muziek zal zo eenvoudig zijn.

Waarom die noot klikte

Je hebt mogelijk een kleine klik aan het einde van dat geluid gehoord. De browser deed precies wat hem verteld was. De klik is natuurkunde.

De oscillator was halverwege een golf toen hij stopte, waardoor de luidsprekerconus zich ergens aan de rand van zijn beweging bevond en vervolgens direct terugsprong. Een plotselinge sprong in druk is wat een klik is.

De oplossing is een tweede getal dat in de loop van de tijd verandert, waarbij dit getal het volume regelt in plaats van de toonhoogte. Musici noemen de vorm hiervan een envelope:

Dezelfde noot met een envelope

const osc = ctx.createOscillator();
osc.frequency.value = 440;
const env = ctx.createGain();
const t = ctx.currentTime;
env.gain.setValueAtTime(0, t);
env.gain.linearRampToValueAtTime(0.3, t + 0.01);
env.gain.exponentialRampToValueAtTime(0.001, t + 1);
osc.connect(env).connect(out);
osc.start(t);
osc.stop(t + 1);

Een envelope is de volumecurve van een enkele noot, van stilte terug naar stilte. De stijging aan het begin is de attack en de daling daarna is de decay. Tien milliseconden om in te faden, gevolgd door een langzame decay naar bijna niets. Dat is het verschil tussen een testtoon en iets waar je bereid zou zijn twee keer naar te luisteren.

Als je de attack uitrekt naar een halve seconde, stopt de noot met "arriveren" en begint hij te zwellen. Het is niet langer iets dat wordt aangeslagen, maar iets dat wordt gestreken, terwijl de toonhoogte geen enkele hertz is verschoven. Het grootste deel van wat je hoort als het karakter van een noot, komt voort uit de envelope.

Dit is een vereenvoudigde envelope. De volledige versie is ADSR: attack, decay, sustain en release, waarbij sustain het niveau is dat een noot vasthoudt terwijl een toets is ingedrukt, en release hoe de noot wegsterft zodra je de toets loslaat.

De onderstaande functie is een helper die in elk volgend voorbeeld wordt gebruikt:

function note(freq, start = 0, length = 0.5, type = "sine") {
  const t = ctx.currentTime + start;
  const osc = ctx.createOscillator();
  const env = ctx.createGain();
  osc.type = type;
  osc.frequency.value = freq;
  env.gain.setValueAtTime(0, t);
  env.gain.linearRampToValueAtTime(0.3, t + 0.01);
  env.gain.exponentialRampToValueAtTime(0.001, t + length);
  osc.connect(env).connect(out);
  osc.start(t);
  osc.stop(t + length);
}

Timbre zijn de frequenties waar je niet om vroeg

Een sinusgolf is een enkele frequentie en niets anders, wat verklaart waarom het klinkt als een gehoortest en niet als een instrument. Pluk een gitaarsnaar gestemd op 440Hz en je krijgt inderdaad een golf die 440 keer per seconde herhaalt, maar de snaar trilt tegelijkertijd in helften, derden en kwarten. Dat zijn extra frequenties op 880, 1320, 1760 enzovoort, die allemaal bovenop de frequentie liggen waar je om vroeg.

Die stapel wordt de harmonische reeks genoemd. De noot waar je om vroeg is de grondtoon (fundamental), en de reeks is die frequentie vermenigvuldigd met 1, 2, 3, 4, 5 enzovoort:

  • 1x 220Hz
  • 2x 440Hz
  • 3x 660Hz
  • 4x 880Hz
  • 5x 1100Hz
  • 6x 1320Hz
  • 7x 1540Hz
  • 8x 1760Hz

Wat belangrijk is, is dat deze frequenties als een pakket arriveren. De "receptuur" van hoe hard elke frequentie is ten opzichte van de andere, is wat een viool als een viool laat klinken en niet als een trompet. Musici noemen dat timbre (klankkleur), en de noot blijft in beide gevallen dezelfde.

De browser levert vier van deze recepten kant-en-klaar:

["sine", "triangle", "square", "sawtooth"].forEach((type, i) => {
  note(220, i * 0.7, 0.6, type);
});

Hetzelfde is 220Hz, maar met vier zeer verschillende karakters. Een blokgolf (square wave) bevat alleen de oneven harmonieken, waardoor hij hol en enigszins elektronisch klinkt. Een zaagtandgolf (sawtooth) bevat ze allemaal en klinkt scherp en zoemend.

Onthoud de harmonische reeks, want deze verklaart het grootste deel van wat volgt. Elke noot die je speelt, sleept een stapel zachte extra noten met zich mee, en welke noten dat zijn, is niet aan ons. Het is rekenkunde, vastgelegd door de natuurkunde van trillende snaren en luchtkolommen.

Het verdubbelen van de frequentie geeft dezelfde noot

Hier zijn vijf noten. Elke noot heeft een frequentie die precies het dubbele is van de vorige:

[110, 220, 440, 880, 1760].forEach((freq, i) =>
  note(freq, i * 0.45, 0.4),
);

Het zijn verschillende toonhoogtes, en toch klinken ze als dezelfde noot. Niet alleen vergelijkbaar, maar identiek. Culturen die geen contact met elkaar hadden, zijn hier onafhankelijk van tot gekomen: verdubbel de frequentie en je krijgt iets dat zo sterk lijkt dat het dezelfde naam verdient. In de westerse notatie worden deze frequenties in het bovenstaande voorbeeld allemaal "A" genoemd, en de afstand tussen hen is het oktaaf.

De naamgeving is niet arbitrair; de harmonische reeks verklaart waarom. Elke harmoniek van 440 bevindt zich al in de harmonische reeks van 220, omdat de reeks van 220 bestaat uit 220, 440, 660, 880, 1100, en die van 440 uit 440, 880, 1320, 1760. De hogere noot voegt geen frequentie toe die de lagere noot niet al produceerde, dus je oor hoort dezelfde kleur op een andere helderheid.

Die ene observatie geeft ons twee regels:

  1. Toonhoogte is multiplicatief, niet additief. Een octaaf omhoog gaan betekent vermenigvuldigen met twee, niet iets optellen. De sprong van 110 naar 220 is 110Hz en de sprong van 880 naar 1760 is 880Hz, maar ze klinken als exact dezelfde afstand. De frequentieruimte is logaritmisch, en elk interval in de muziek is een ratio.
  2. We hoeven maar één octaaf op te lossen. Omdat verdubbeling je terugbrengt naar dezelfde noot, wordt het probleem van "welke toonhoogtes zouden moeten bestaan" gereduceerd tot "hoe moeten we de ruimte tussen een frequentie en twee keer die frequentie verdelen". Los het één keer op en het antwoord herhaalt zich over het gehele hoorbare bereik.

Eenvoudige ratio's klinken goed, en dit is waarom

Het naïeve antwoord is om het octaaf gelijkmatig te verdelen. Dat doet niemand, omdat we niet alle paren frequenties op dezelfde manier ervaren. Sommige combinaties klinken rustig en sommige klinken als een fout.

const ratios = [
  ["2/1   octaaf", 2],
  ["3/2   kwint", 3 / 2],
  ["4/3   kwart", 4 / 3],
  ["5/4   grote terts", 5 / 4],
  ["16/15 halve toon", 16 / 15],
  ["√2    de ongemakkelijke", Math.SQRT2],
];
ratios.forEach(([label, ratio], i) => {
  note(220, i * 1.4, 1.2);
  note(220 * ratio, i * 1.4, 1.2);
  console.log(label, "->", (220 * ratio).toFixed(2) + "Hz");
});

De eerste vier klinken als akkoorden. De 16/15 klinkt als twee noten die ruzie maken. De laatste klinkt als een autalarm. Het patroon is duidelijk: hoe eenvoudiger de breuk, hoe beter het klinkt. 2/1 het octaaf, dan 3/2 de kwint, dan 4/3 de kwart, dan 5/4 de grote terts, en tegen de tijd dat je bij 16/15 komt, valt het geheel volledig uit elkaar.

Er zijn twee fysieke redenen voor dit rekenkundige resultaat:

Ten eerste: de harmonische reeks. Speel 220 en 330 samen (een 3:2 ratio). De eerste noot produceert 220, 440, 660, 880, 1100, 1320. De tweede produceert 330, 660, 990, 1320. Ze delen 660 en 1320 exact. Twee noten die een kwint uit elkaar liggen, zijn niet echt twee aparte geluiden; het zijn twee zwaar overlappende stapels die elkaar versterken. Probeer nu 220 en 311 (dicht bij √2). Er lijnigt niets uit op enig harmonisch niveau.

Ten tweede: ruwheid (roughness). Wanneer twee frequenties dicht bij elkaar liggen maar niet identiek zijn, drijven ze in en uit fase en hoor je het volume pulseren. Dat is beating (zweving), en dat is wat een vals gestemde noot vals laat klinken.

[220, 226, 223, 221, 220.5, 220].forEach((freq, i) => {
  note(220, i * 1.3, 1.2);
  note(freq, i * 1.3, 1.2);
});

Wanneer breuken ingewikkeld zijn, zitten de twee stapels harmonieken vol met paren die slechts een paar hertz uit elkaar liggen, en elk van die paren veroorzaakt zwevingen. Dat is wat dissonantie is.

Consonantie is simpelweg je oor dat snel een herhalend patroon vindt.

Vijfden stapelen en de bug die je niet kunt oplossen

We weten dat eenvoudige ratio's goed zijn, en na het octaaf zelf is de schoonste ratio in de fysica 3/2, de kwint. Wat gebeurt er als we een muzikaal alfabet bouwen met alleen octaven en kwinten?

De logische stap is om steeds een kwint omhoog te gaan en te halveren zodra je buiten het octaaf treedt. Dit is ongeveer wat Pythagoras deed:

let freq = 220;
const notes = [220];
for (let i = 0; i < 12; i++) {
  freq = (freq * 3) / 2;
  while (freq >= 440) freq = freq / 2;
  notes.push(freq);
  console.log(`kwint ${i + 1}: ${freq.toFixed(3)}Hz`);
}
notes.sort((a, b) => a - b).forEach((f, i) => note(f, i * 0.22, 0.3));

Kijk naar de eerste en de laatste noot. We begonnen bij 220, pasten twaalf kwinten toe, en landden op 222,99. Niet op 220. Het is dichtbij genoeg om hoorbaar als dezelfde noot te willen zijn, maar ver genoeg ernaast om onbruikbaar te zijn.

De fout is niet een afrondingsfout, maar iets structureels:

const twelveFifths = (3 / 2) ** 12;
const sevenOctaves = 2 ** 7;
console.log("twaalf kwinten:", twelveFifths.toFixed(6));
console.log("zeven octaven:", sevenOctaves.toFixed(6));
console.log("ratio:", (twelveFifths / sevenOctaves).toFixed(6));
console.log("in cents:", (1200 * Math.log2(twelveFifths / sevenOctaves)).toFixed(2));

Twaalf perfecte kwinten schieten zeven perfecte octaven over met een factor van 1,0136. Dat gat wordt de Pythagoreïsche komma genoemd. Cents zijn de maatstaf voor toonafstand, waarbij er 1200 in een octaaf zitten. Een verschil van 23 cents is ongeveer een kwart van de afstand tussen twee aangrenzende pianotoetsen.

Dit kan niet worden opgelost om een reden die een programmeur zal herkennen. Het stapelen van kwinten betekent vermenigvuldigen met 3/2, dus na n kwinten ben je op $3^n / 2^n$. Het stapelen van octaven betekent $2^m$. Om deze ooit te laten samenvallen, zou een macht van drie gelijk moeten zijn aan een macht van twee ($3^n = 2^{n+m}$). Omdat drie en twee beide priemgetallen zijn, gebeurt dit nooit.

Elk stemsysteem in de geschiedenis is een andere keuze over waar deze fout "gedumpt" wordt.

Gelijke temperatuur (Equal Temperament) is het compromis

Het moderne antwoord is om pure ratio's volledig op te geven. Neem het octaaf, verdeel het in twaalf gelijke multiplicatieve stappen, en accepteer dat niets behalve het octaaf ooit weer exact correct zal zijn.

Eén stap is de twaalfde machtswortel van twee:

const semitone = 2 ** (1 / 12);
console.log("één halve toon =", semitone);
let freq = 220;
for (let i = 0; i < 13; i++) {
  note(freq, i * 0.2, 0.28);
  freq = freq * semitone;
}

Twaalf stappen landen exact op 440. De vraag is: waarom twaalf? Je kunt dit zelf ontdekken door het octaaf te verdelen in n gelijke stappen voor elke plausibele n, en te controleren hoe dicht de beste beschikbare stap bij een echte 3/2 kwint komt:

function bestFifth(n) {
  let error = Infinity;
  let step = 0;
  for (let candidate = 1; candidate < n; candidate++) {
    const off = Math.abs(2 ** (candidate / n) - 1.5);
    if (off < error) {
      error = off;
      step = candidate;
    }
  }
  return { step, error };
}
for (let n = 5; n <= 25; n++) {
  const { step, error } = bestFifth(n);
  const pct = (error / 1.5) * 100;
  console.log(`${n} stappen: beste kwint is stap ${step}, afwijking ${pct.toFixed(3)}%`);
}

Twaalf is de eerste verdeling die de kwint correct krijgt tot ongeveer een tiende van een procent, en het is meer dan drie keer beter dan alles daaronder. Het is het goedkoopste aantal noten dat je een overtuigende kwint oplevert.

Het resultaat is dat we nu met gehele getallen kunnen werken. Kies een willekeurige noot als nummer 0, en elke andere noot is een geheel aantal halve tonen daarvandaan. De conventie is MIDI-nummering, waarbij 69 onze 440Hz A is:

const midiToFreq = (n) => 440 * 2 ** ((n - 69) / 12);

Waarom twaalf noten en niet allemaal?

Het hebben van twaalf noten betekent niet dat je ze allemaal gebruikt. Alle twaalf in volgorde spelen klinkt onmuzikaal: het klinkt als een geluidseffect. Er is geen focus, niets klinkt als "thuis" en er is geen manier om te bepalen waar je bent.

Muziek kiest een subset. Bijna altijd zeven van de twaalf, gekozen zodat de gaten ertussen ongelijk zijn. Dit is precies wat het mogelijk maakt om de ene noot van de andere te onderscheiden. Deze subset is een toonladder (scale), en een toonladder wordt het best beschreven als de stappen tussen de noten:

const midiToFreq = (n) => 440 * 2 ** ((n - 69) / 12);
const major = [2, 2, 1, 2, 2, 2, 1];
function buildScale(root, pattern) {
  return pattern.reduce(
    (notes, step) => [...notes, notes.at(-1) + step],
    [root],
  );
}
const cMajor = buildScale(60, major);
cMajor.forEach((n, i) => note(midiToFreq(n), i * 0.25, 0.4));

De stappen 2, 2, 1, 2, 2, 2, 1 tellen op tot 12, zodat het patroon het octaaf exact sluit. Dat is de majeurtoonladder. Verander één getal en de stemming verandert volledig:

const patterns = {
  major: [2, 2, 1, 2, 2, 2, 1],
  naturalMinor: [2, 1, 2, 2, 1, 2, 2],
  majorPenta: [2, 2, 3, 2, 3],
  minorPenta: [3, 2, 2, 3, 2],
  blues: [3, 2, 1, 1, 3, 2],
};

De modi (modes), die vaak worden gepresenteerd als exotische Griekse namen om te memoriseren, blijken simpelweg rotaties van dezelfde array te zijn. Haal de eerste stap van de voorkant en zet hem achteraan, en je hebt de volgende modus.

Akkoorden zijn noten gestapeld in tertsen

Een akkoord is meer dan één noot tegelijk. De nuttige vraag is: welke combinaties klinken niet vreselijk? Het antwoord ligt bij de ratio's: noten waarvan de harmonieken overlappen. In een toonladder zijn dit de noten die twee toonladderstappen uit elkaar liggen, een afstand die musici een terts noemen.

const midiToFreq = (n) => 440 * 2 ** ((n - 69) / 12);
const build = (root, p) => p.reduce((n, s) => [...n, n.at(-1) + s], [root]);
const cMajor = build(60, [2, 2, 1, 2, 2, 2, 1]);
const triad = [0, 2, 4].map((i) => cMajor[i]);
triad.forEach((n) => note(midiToFreq(n), 0, 2));

Dit is een C-majeurakkoord. Drie noten op 0, 4 en 7 halve tonen boven de grondtoon (root). In frequentie is dit ongeveer 4:5:6.

Een terts is ofwel 4 halve tonen (grote terts) of 3 halve tonen (kleine terts). Door twee tertsen te stapelen krijg je een majeur- of mineurakkoord:

const shapes = {
  major: [0, 4, 7],
  minor: [0, 3, 7],
  diminished: [0, 3, 6],
  augmented: [0, 4, 8],
};

Majeur naar mineur is slechts één noot die één halve toon verschuift. Dat is alles wat de twee emotionele polen van de westerse muziek scheidt: [0, 4, 7] versus [0, 3, 7].

Voeg een vierde noot toe (weer een terts omhoog) en je krijgt septiemakkoorden (seventh chords), wat begint te klinken als echte muziek in plaats van een hymne.

Een toonsoort geeft je zeven akkoorden

Er is niets speciaals aan het beginnen op de eerste trap van een toonladder. Doe dit vanaf elke trap, en je krijgt zeven akkoorden, allemaal gebouwd uit de zeven noten van de toonladder.

Niemand heeft deze kwaliteiten "gekozen". Drie blijken majeur te zijn, drie mineur en de laatste is verminderd (diminished). Dit patroon wordt geforceerd door de ongelijkmatige gaten in de toonladder.

Musici schrijven deze zeven akkoorden als Romeinse cijfers: hoofdletters voor majeur, kleine letters voor mineur, en een klein cirkeltje voor het verminderde akkoord: I ii iii IV V vi vii°

Deze notatie is nuttig omdat hij akkoorden beschrijft op basis van hun positie in de toonladder, niet op basis van hun naam. Een V-akkoord is "het akkoord gebouwd op de vijfde trap", ongeacht in welke toonsoort je zit. Dit is in feite relative addressing.

Spanning en resolutie

Muziek is wat er gebeurt als je akkoorden in een bepaalde volgorde zet. De sterkste trek in het systeem is van V terug naar I.

Er zijn twee concrete redenen hiervoor:

  1. De noot B in het V-akkoord zit één halve toon onder de grondtoon van I. Dit klinkt alsof de noot tegen de deur leunt. Musici noemen dit de leidtoon (leading tone).
  2. Een dominant-septiemakkoord bevat zowel B als F, die zes halve tonen uit elkaar liggen. Zes halve tonen is de tritonus, exact de helft van een octaaf ($\sqrt{2}$). Dit is het meest onstabiele interval; beide noten lossen op door één halve toon in tegenovergestelde richtingen te bewegen wanneer je naar I gaat.

De taal van spanning en ontspanning is gebouwd op dit mechanisme. Denk aan veelvoorkomende progressies zoals I V vi IV (popmuziek) of ii V I (jazz).

Notatie is een serialisatieformaat

Niets van wat tot nu toe besproken is, vereiste een notenbalk. Alles is een array van integers en een functie die deze omzet in frequenties.

Maar notatie bestaat als een formaat waarin de literatuur van de westerse muziek wordt bewaard. Het is een serialisatieformaat, geoptimaliseerd voor een mens die het in real-time leest terwijl de handen bezig zijn.

  • De verticale as is toonhoogte, maar niet lineair. De as is diatonisch in plaats van chromatisch: hij stapt door de toonladder in plaats van door alle twaalf noten.
  • De sleutel (clef) bepaalt de oorsprong. De vioelsleutel (G-sleutel) en de bassleutel (F-sleutel) markeren waar de oorsprong ligt op de balk.
  • Alteraties (accidentals) dienen als "patch" voor de lossy encoding. De kruis- (#), mol- (b) en natuur-tekens verschuiven de noot van de positie die hij anders zou hebben.
  • De toonsoortteken (key signature) is een vorm van DRY (Don't Repeat Yourself). In plaats van bij elke noot een kruis te zetten, declareer je dit één keer aan het begin van de regel.
  • Duur wordt weergegeven in machten van twee: hele noot, halve, kwart, achtste, zestiende. Dit is een unaire codering van een binaire exponent.
  • Beats vs. Seconden: Notatie slaat relatieve beats op. De vertaler (de muzikant) levert de klok via het tempo (BPM).

Samenvattend

Als we alles combineren — een toonsoort, diatonische akkoorden, een progressie en een melodie die zich aan de toonladder houdt — ontstaat er muziek.

Elk getal in zo'n systeem heeft een afgeleide betekenis. De keuze voor 12 noten komt voort uit de poging om machten van twee met machten van drie te verzoenen en genoegen te nemen met een afrondingsfout. Een toonladder is een subset van die noten met ongelijkmatige gaten zodat je weet waar je bent. Een akkoord is een set noten waarvan de harmonieken overlappen. Een toonsoort is een manier om akkoorden relatief te benoemen.

Muziektheorie is geen verzameling triviale feitjes om te memoriseren, maar een reeks engineering-beslissingen die lang geleden zijn genomen onder specifieke beperkingen.