Die Algorithmen im Detail
Vorne einfach, hier ehrlich: Was genau passiert „unter der Haube". Alle Größen sind transpositionsinvariant — sie hängen nur von Tonhöhen-verhältnissen und Dauern ab, nicht von der notierten Tonart.
1. Parsing: von Noten zu Zahlen
Aus jeder Kern-Datei werden die MIDI-Tonhöhen und Notendauern extrahiert. Aus den Tonhöhen entstehen Intervalle (Differenzen aufeinanderfolgender Töne), aus den Dauern Dauer-Verhältnisklassen — letztere tempo- und notationsrobust:
iv = [seq[i+1] - seq[i] for i in range(len(seq)-1)] # Intervalle (Halbtöne) rr = [round(log2(d[i+1]/d[i])) for i in range(len(d)-1)] # Dauer-Verhältnisklassen
2. Fingerabdrücke: 3-Gramme + tf-idf
Aus Intervallen bzw. Rhythmusklassen werden überlappende 3-Gramme gezählt — kurze melodische bzw. rhythmische Wendungen. Jede Wendung wird per tf-idf gewichtet: Allerwelts-Floskeln, die im ganzen Korpus vorkommen, zählen wenig; seltene, charakteristische Wendungen zählen viel.
png = Counter(tuple(iv[i:i+3]) for i in range(len(iv)-2)) # Pitch-3-Gramme
idf = {k: log(N / df[k]) for k in df} # Inverse Dokumentfrequenz
gewicht = png[k] * idf[k] # tf-idf je 3-Gramm
3. Motiv-Entropie (steuert den Winkel)
Der Winkel im Bogen kommt aus der Shannon-Entropie — einem Grundbegriff der Informationstheorie (Claude Shannon, 1948), der hier eine sehr anschauliche Bedeutung hat: Wie überraschend ist der weitere Verlauf einer Melodie?
Die Idee: Eine Melodie ist eine Kette kurzer Drei-Ton-Wendungen (die 3-Gramme von oben). Wir zählen, welche Wendung wie oft vorkommt. Besteht ein Lied fast nur aus ein, zwei ständig wiederholten Wendungen, ist der nächste Schritt sehr vorhersagbar — die Entropie ist niedrig. Verteilt sich das Lied gleichmäßig über viele verschiedene Wendungen, ist jeder nächste Schritt eine kleine Überraschung — die Entropie ist hoch.
Gemessen wird in Bit: die mittlere Zahl von Ja/Nein-Fragen, die man bräuchte, um die jeweils nächste Wendung zu erraten. Faustregel: 2 Bit ≈ ein Lied, das mit ~4 Wendungen auskommt (2² = 4); 5 Bit ≈ ~32 gleichberechtigte Wendungen. Im Korpus reicht die Spanne von 2,1 Bit („Wie wollen ein bisschen spazieren gehn" — sehr schlicht) über den Median 4,7 bis 6,4 Bit („Wie wir so fest zusammen stehn" — sehr variantenreich).
png = Counter(tuple(iv[i:i+3]) for i in range(len(iv)-2)) # Häufigkeit jeder Wendung tot = sum(png.values()) # Gesamtzahl der Wendungen H = -sum((c/tot) * log2(c/tot) for c in png.values()) # Shannon-Entropie in Bit
Die Entropie bestimmt nur den Winkel — sie fließt bewusst nicht in die Ähnlichkeit ein (Disjunktheit: Anzeige-Achse und Ähnlichkeits-Maß bleiben getrennt, sonst wäre die Karte redundant).
4. Kombinierte Ähnlichkeit
Die Ähnlichkeit zweier Lieder ist eine gewichtete Summe aus drei Kanälen: Tonhöhen-Wendungen (Kosinus), Rhythmus-Wendungen (Kosinus) und einem Zusammenfassungs-Vektor (Schritt-/Sprunganteil, Konturrichtung/-varianz), der als Korpus-Perzentil normiert ist:
sim(a,b) = 0.5 · cos(pitch_tfidf)
+ 0.3 · cos(rhythm_tfidf)
+ 0.2 · (1 − euklid_distanz_im_perzentilraum)
Im Browser ist genau diese Funktion implementiert; bei jedem Re-Zentrieren wird die Ähnlichkeit des neuen Fokus zu allen ~5.300 Liedern neu berechnet (wenige Millisekunden) — daher braucht es keinen Server:
function sim(a,b){
const sp = cos(a,b,'pitch_ng','pitch_norm');
const sr = cos(a,b,'rhythm_ng','rhythm_norm');
let d=0; for(let i=0;i<a.summ_pct.length;i++){const e=a.summ_pct[i]-b.summ_pct[i]; d+=e*e;}
d = Math.sqrt(d)/Math.sqrt(a.summ_pct.length);
return 0.5*sp + 0.3*sr + 0.2*(1-d);
}
5. Die Bogen-Kodierung
- Radius = Unähnlichkeit zum Fokus (je näher, desto verwandter).
- Winkel = Motiv-Entropie (links wenig, rechts viel).
- Farbe = Metrum (kategorial, farbenblind-sichere Palette).
- Form = Tongeschlecht (leer = Dur, ⊖ = Moll).
Eine Feinheit für die Lesbarkeit: Sowohl Ähnlichkeiten als auch Entropien ballen sich in der Mitte ihrer Skala (die berühmte Glockenkurve). Roh aufgetragen ergäbe das einen unlesbaren Klumpen. Beide Achsen werden deshalb über ihre Perzentile entzerrt (Histogramm-Ausgleich): Nicht der Rohwert, sondern der Rangplatz bestimmt die Position — die Mitte wird gestreckt, die Ränder gerafft, und der Radius zusätzlich flächentreu verteilt. Die Ringe und die Winkelskala sind mit den echten Werten an der jeweiligen Stelle beschriftet, damit nichts vorgetäuscht wird.
Referenz-Implementierung: build/04_features.py
(Features & Fingerabdrücke), build/05_assemble.py (Montage),
Browser-Ähnlichkeit in dist/app.js.