Lizenzfreie Spielmusik: jeden Sound im Code erzeugen
10 Min. Lesezeit

Nichts, was Sie in Kalecik hören, stammt aus einer Aufnahme. Das weiche „Tock“, wenn sich ein Stein auf den anderen setzt, die fernen Vögel, die Grillen in der Nacht, das Wasser am Teichufer, das Blöken der Schafe und jede Note der Hintergrundmusik wurden in einem Python-Skript aus Zahlen erzeugt. Keine Soundpakete, keine Samples, keine Lizenzen. Kalecik ist ein ruhiges Burg- und Dorfbauspiel, das ich für iPhone und iPad entwickle, und dieser Beitrag zeigt, wie sein Klang entstanden ist.
Zuerst das Ehrliche: Den Code haben Claude-Code-Agenten geschrieben. Ich habe auf dem Telefon zugehört, gesagt, was mich stört, und entschieden, was bleibt. Das Kuriose daran: Der Agent, der die Klänge schrieb, konnte keinen einzigen davon hören. Ein Teil dieses Beitrags handelt deshalb davon, Klang ohne Ohren zu bauen. Das Projekt als Ganzes beschreibe ich im Hauptbeitrag dieser Serie.
Warum Synthese statt Aufnahmen?
Aufnahmen werfen zwei Fragen auf: Wie steht es um die Lizenz, und passt der Klang zur Stimmung des Spiels? Mit Synthese erledigen sich beide. Die erste Zeile von tools/make_audio.py fasst es zusammen: „Synthesise every sound the game uses (no samples, no licences).“ Das Skript braucht nur numpy und ruft für die MP3-Kodierung lame auf. Alles läuft mit 22.050 Hz in Mono und besteht aus additiver und modaler Synthese, FFT-Filtern und einem Faltungshall per FFT.

Abend: Auch die Musik kommt aus dem Stapel dieser Tageszeit.
Am Steinklang sieht man modale Synthese am leichtesten. Ein angeschlagener Gegenstand schwingt auf einigen wenigen Frequenzen, von denen jede in ihrem eigenen Tempo abklingt. Wer diese Frequenzen und Abklingzeiten wählt, wählt den Gegenstand:
def modal(freqs, decays, amps, seconds):
"""Angeschlagener Gegenstand: einige exponentiell abklingende Sinusmoden."""
t = tt(seconds)
y = np.zeros(len(t))
for f, d, a in zip(freqs, decays, amps):
if f < 0.45 * SR:
y += a * np.sin(2 * np.pi * f * t) * np.exp(-t / d)
return y
def stone_place(seed):
"""Ein Stein setzt sich auf einen anderen: ein weiches 'Tock' mit etwas Sand."""
r = np.random.default_rng(seed)
s = r.uniform(0.85, 1.15) # jede Variante ist ein etwas anderer Stein
y = modal([95 * s, 230 * s, 520 * s, 880 * s, 1400 * s], [0.07, 0.06, 0.035, 0.02, 0.012],
[0.8, 1.0, 0.55, 0.3, 0.15], 0.4)
y += click(0.4, 300, 3000, 0.005) * 0.25 # der Moment des Aufpralls
y += fft_filter(rng.normal(0, 1, len(y)), lo=600, hi=3500) * np.exp(-tt(0.4) / 0.06) * 0.08 # Sand
return outdoor(fft_filter(y, hi=4500), 0.12)Die meisten Klänge nehmen einen Seed entgegen. Ändert sich der Seed, ändert sich die Größe des Steins, und Varianten wie thud, thud_2 und thud_3 gibt es gratis dazu. Das Spiel wählt jedes Mal eine zufällige Variante, aber nie zweimal hintereinander dieselbe.
Vögel, Grillen und Wasser
Die Naturgeräusche entstehen aus denselben kleinen Bausteinen. Ein Vogelruf ist ein Sinuston, dessen Frequenz schnell gleitet (ein Chirp); reiht man einige steigende Chirps aneinander, entsteht ein Zwitschern, paarweise fallende ergeben eine andere Art. Eine Grille ist die gleichmäßige Wiederholung von 14 ms langen Pulsen bei etwa 4 kHz. Das Plätschern am Ufer ist bandpassgefiltertes Rauschen plus einige Bläschen, deren Tonhöhe nach oben gleitet. Am Ende läuft alles durch outdoor(): eine Faltung mit einer kurzen Impulsantwort aus exponentiell abklingendem Rauschen, die den Klang ins Freie und ein Stück in die Ferne rückt.
Das Schaf, zweiter Versuch: Formanten
Das erste Schaf klang schlecht. Meine Nachricht nach dem Anhören auf dem Telefon lautete sinngemäß übersetzt: „baa.wav und baa_2.wav klingen furchtbar, nichts davon erinnert an ein Schaf. Bitte neu vertonen, sodass es nicht nervt.“
Beim zweiten Versuch griff der Agent auf das Quelle-Filter-Modell der menschlichen Stimme zurück. Der Kehlkopf erzeugt regelmäßige Pulse, deren Obertöne durch die Resonanzen von Mund- und Nasenraum laufen, die sogenannten Formanten. Wo diese Resonanzen liegen, bestimmt, welchen Vokal wir hören. Für das „Mäh“ des Schafs nutzte er drei Formanten bei etwa 700, 2.100 und 2.900 Hz, ein nasales „m“ am Anfang und ein Zittern von 7 bis 9 Hz (gekürzt):
def baa(seed):
"""Ein Schaf ein Feld weiter: ein nasales 'm', das sich in ein zitterndes 'äh' öffnet."""
r = np.random.default_rng(seed)
d = r.uniform(0.7, 0.95)
t = tt(d)
rate = r.uniform(7.0, 9.0) # Zittern mit 7-9 Hz
f0 = r.uniform(215, 260) * (1 + 0.035 * np.sin(2 * np.pi * rate * t))
ph = 2 * np.pi * np.cumsum(f0) / SR
src = np.zeros(len(t))
k = 1
while k * 280 < 5000: # Kehlkopfquelle: Obertonreihe
src += np.sin(k * ph) / k ** 1.3
k += 1
vowel = (resonator(src, r.uniform(650, 800), 5) * 1.0 # F1 ~700 Hz
+ resonator(src, r.uniform(1900, 2300), 8) * 0.45 # F2 ~2100 Hz
+ resonator(src, r.uniform(2700, 3000), 10) * 0.15) # F3 ~2900 Hz
nasal = resonator(src, 260, 3) * 0.8 # das 'm' am Anfang
open_ = np.clip((t - 0.05) / 0.08, 0, 1) # Übergang m -> äh
trem = 1 - 0.45 * (0.5 + 0.5 * np.sin(2 * np.pi * rate * t + np.pi))
y = (nasal * (1 - open_) + vowel * open_) * trem
# ... der echte Code ergänzt Atem, Hüllkurve und Bandpass
return outdoor(y, 0.35)Heute blökt tagsüber alle 70 bis 160 Sekunden ein Schaf, leise und in der Ferne. Ob es gut genug klingt, habe ich entschieden, nicht der Agent; ihm standen nur Zahlen zur Verfügung.
Nichts läuft in Schleife
In der ersten Version war der Wind eine Schleife. Meine Rückmeldung vom Telefon: „Mit den Hintergrundgeräuschen stimmt etwas nicht. Da ist ein Meeresrauschen, das ständig hängen bleibt. Gibt es im Spiel etwa ein Meer?“ Ein durchgehendes, synthetisches Windrauschen klang wie Brandung, und die Stelle, an der die Schleife von vorn begann, sorgte vermutlich für das Gefühl des Hängens.
Die Lösung wurde zur Regel: Im Spiel läuft nichts in Schleife. Der Kommentar oben in audio.gd sagt es klar: „Nothing loops, so there is no loop seam to hear.“ Die Atmosphäre besteht aus zufällig getakteten Einzelklängen, und jeder Klang hat je Stimmung seinen eigenen Pausenbereich:
## Atmosphäre je Stimmung: [Klang, min. Pause s, max. Pause s, dB, Tonhöhenstreuung]
const AMBIENCE := [
[["bird", 5.0, 14.0, -21.0, 0.06], ["leaves", 14.0, 30.0, -25.0, 0.15], ["baa", 70.0, 160.0, -28.0, 0.05]], # Tag
[["bird", 12.0, 28.0, -24.0, 0.06], ["cricket", 6.0, 12.0, -30.0, 0.04], ["baa", 120.0, 240.0, -29.0, 0.05]], # Abend
[["cricket", 2.0, 4.0, -27.0, 0.05], ["owl", 45.0, 100.0, -24.0, 0.04]], # Nacht
[["bird", 30.0, 60.0, -27.0, 0.05]], # Winter
]Bei jedem Abspielen wird die Tonhöhe leicht verschoben und der nächste Zeitpunkt neu aus dem Bereich gezogen. Das Blätterrascheln hat bewusst keine tiefen Frequenzen; im Code steht dazu „so it never reads as surf“. Schaut die Kamera auf einen Teich, kommt Wasserplätschern hinzu, im Winter verstummt es, weil die Teiche zufrieren. Selbst Regen ist keine Schleife: 7 Sekunden lange Schichten mit weichen Blenden an beiden Enden starten alle 5,5 Sekunden und überlappen sich. Bei Regen schweigen außerdem die Vögel und Schafe meistens.
Die Musik folgt derselben Logik. Für jede Stimmung (Tag, Abend, Nacht, Winter) werden die Stücke aus einem gemischten Stapel ausgeteilt; ein Stück kommt erst wieder, wenn alle anderen gespielt wurden:
func _deal() -> String:
var deck: Array = _decks[_mood]
if deck.is_empty():
deck.append_array(_pieces[_mood])
deck.shuffle()
if deck.size() > 1 and deck.back() == _last_piece: # nie zweimal dasselbe Stück hintereinander
deck.push_front(deck.pop_back())
return deck.pop_back() if not deck.is_empty() else ""Zwischen zwei Stücken bleibt Stille: tagsüber 30 bis 75 Sekunden, nachts und im Winter 45 bis 110 Sekunden. Der Klang läuft über drei getrennte Busse, Music, SFX und Ambience; die Schalter für Musik und Effekte in den Einstellungen schalten diese Busse stumm.
Ein generativer Komponist: 37 Stücke, rund 65 Minuten
Der erste Soundtrack bestand aus sechs von Hand geschriebenen Stücken: Morgen, Wiese, Walzer, Sonnenuntergang, Nacht und Schnee. Dann gab ich dem Agenten ein Ziel vor, sinngemäß: „Mach Musik, die man zwei Stunden hören kann, ohne dass sie langweilt oder erdrückt.“

Winterstimmung: ruhige Stücke mit Glocken und Harfe.
Seine Antwort war tools/music_gen.py, ein regelbasierter Komponist. Jedes Stück entsteht aus einem Seed, und der Seed wählt Tonart, Modus, Tempo, Takt, Akkordfolge, Instrumente und eine motivische Melodie:
class Composer:
def __init__(self, mood, seed):
self.cfg = MOODS[mood]
self.r = np.random.default_rng(seed) # jede Entscheidung im Stück stammt aus diesem Seed
r, c = self.r, self.cfg
self.mode = str(r.choice(c["modes"])) # Dur, lydisch, dorisch...
self.tonic = 60 + int(r.choice(c["keys"]))
if self.tonic > 60:
self.tonic -= 12
self.meter = int(r.choice(c["meters"])) # 3/4 oder 4/4
self.bpm = float(r.uniform(*c["bpm"]))
leads = list(c["leads"])
r.shuffle(leads)
self.lead_a, self.lead_b = leads[0], leads[1] # bei der Wiederholung wechselt die Melodie das Instrument
# ... Akkordfolgen und Begleitung stammen aus demselben SeedDie Melodien folgen strengen Regeln, damit sie wie Volkslieder singbar bleiben. Die Grundeinheit ist eine achttaktige Periode: Die ersten vier Takte steigen wie eine Frage an und ruhen auf einem Halbschluss, die letzten vier greifen das Motiv wieder auf und schließen auf der Tonika. Die Töne bleiben immer in der Tonleiter, betonte Zählzeiten landen auf Akkordtönen, die Melodie bewegt sich überwiegend schrittweise, nach einem Sprung folgt ein Schritt zurück, und die hohe Lage bleibt tabu. Die Form hängt von der Stimmung ab; ein Tagesstück besteht aus Einleitung, A, verziertem A', B, einer Atempause, A'' und Schluss.
| Stimmung | Modus | Tempo | Führende Instrumente |
|---|---|---|---|
| Tag | Dur, lydisch, mixolydisch | 70-90 BPM | Flöte, Klavier, Harfe |
| Abend | Dur, lydisch (Septakkorde) | 58-70 BPM | Klavier, Flöte |
| Nacht | dorisch, äolisch | 50-60 BPM | Celesta, Spieluhr, Klavier |
| Winter | Dur, lydisch | 54-64 BPM | Glockenspiel, Celesta, Flöte |
Auch die Instrumente sind synthetisch. Die Flöte ist ein fast reiner Ton mit verzögertem Vibrato und bandbegrenztem Atem; das Klavier besteht aus zwei leicht gegeneinander verstimmten Saiten; Harfe und Gitarre sind gezupfte Saiten, deren obere Obertöne schneller abklingen. Das Ergebnis: 37 Stücke, 6 von Hand und 31 vom Komponisten (14 Tag, 9 Abend, 7 Nacht, 7 Winter), insgesamt rund 65 Minuten und 19 MB MP3. Zwei Stunden einzigartige Musik gibt es also nicht. Durch die langen Pausen zwischen den Stücken und die in jeder Runde neu gemischte Reihenfolge wiederholt sich aber auch in einer langen Sitzung keine Abfolge.
Warum die Musik abbrach: ein Token und ein Wächter
In derselben Nachricht hatte ich einen Fehler gemeldet: „Nach einer Weile bricht die Musik plötzlich ab.“ Der Agent fand die Ursache beim Lesen des Codes. Bei einem Stimmungswechsel wurde das laufende Stück über 3 Sekunden ausgeblendet, und am Ende der Blende wurde stop() aufgerufen. Endete das Stück in diesen 3 Sekunden von selbst, startete ein neues, und das stop() der alten Blende würgte das neue Stück mittendrin ab. Die Lösung war ein Token: Jedes neue Stück erhöht einen Zähler, und eine veraltete Blende lässt alles in Ruhe, wenn sich das Token geändert hat:
var token := _token
_fade = create_tween()
_fade.tween_property(_music, "volume_db", -50.0, 3.0)
_fade.tween_callback(func():
if token == _token: # inzwischen läuft vielleicht ein neues Stück: nicht anfassen
_music.stop())Als zweites Sicherheitsnetz kam ein Wächter hinzu. Unter iOS können ein Anruf, Siri oder der Wechsel der App in den Hintergrund die Wiedergabe anhalten. Der Wächter prüft in jedem Frame: Scheint das Stück zu laufen, seine Position bewegt sich aber seit 2 Sekunden nicht, oder hat es vor dem Ende angehalten, setzt er es an derselben Stelle mit einer kurzen Einblendung fort:
if _music.playing:
var pos := _music.get_playback_position()
_stall = _stall + delta if absf(pos - _last_pos) < 0.0005 else 0.0
_last_pos = pos
if _stall > 2.0: # spielt, aber die Uhr steht: anstoßen
_stall = 0.0
_resume()
elif _last_pos < _piece_len - 1.5:
_resume() # angehalten, obwohl das Stück nicht zu Ende warDaraus lässt sich auch etwas über parallel arbeitende Agenten lernen. Ein externer GPT-Review, den ich eingefügt hatte, bemerkte, dass Musikdateien und Code zwischenzeitlich nicht zusammenpassten: Beim Erzeugen der neuen Stücke waren die alten MP3s gelöscht worden, bevor der Code, der nach ihnen suchte, angepasst war. Danach galt immer dieselbe Reihenfolge: zuerst die neuen Dateien, dann der Code, erst zuletzt das Alte löschen.
Klang ohne Ohren: LUFS und Spektrogramme
Das war die seltsamste Einschränkung des Projekts: Der Agent, der die Klänge schrieb, konnte sie nicht hören. Zur Kontrolle stützte er sich auf zwei Werkzeuge. Das erste war die Lautheitsmessung. LUFS, eine Lautheitseinheit, die an die Empfindlichkeit des menschlichen Gehörs angepasst ist, wurde für jedes Stück mit dem ebur128-Filter von ffmpeg gemessen, der zugleich den Spitzenpegel liefert. Das zweite waren Spektrogramme: ffmpeg zeichnete für jedes Stück ein Frequenz-Zeit-Bild als PNG, und der Agent beurteilte anhand dieser Bilder, wo Höhen und Rauschen lagen:
cd audio/music
for f in *.mp3; do
printf "%-14s " "$f"
ffmpeg -hide_banner -nostats -i "$f" -af ebur128=peak=true -f null - 2>&1 \
| grep -E "^\s+(I|LRA|Peak):" | tr -s ' ' | tr '\n' ' '
echo
done
ffmpeg -loglevel error -y -i gunduz_01.mp3 -lavfi "showspectrumpic=s=1200x400:legend=0:scale=log" spec.pngDer Komponist bringt jedes Stück auf einen Ziel-RMS-Pegel für seine Stimmung und schickt es dann durch einen sanften tanh-Begrenzer. Die handgeschriebenen Stücke fielen etwas lauter aus und wurden mit einer eigenen Verstärkung je Stimmung angeglichen. Das gemessene Ergebnis: tagsüber etwa -22,5 LUFS, abends -23,3, nachts und im Winter -24. In der ersten Messrunde wurde außerdem das Atemrauschen der Flöte auf ein schmaleres Band begrenzt und leiser gemacht.
Das letzte Wort haben trotzdem die Ohren. Die Zahlen sagen, dass die Stücke zueinander passen, nicht, ob sie gut sind. Deshalb habe ich jede Klangänderung auf dem Telefon angehört. Eine Randnotiz: Als mich der Ton aus dem Mac während der Testläufe bei anderer Arbeit störte, liefen alle automatischen Tests fortan stumm mit Godots Option --audio-driver Dummy.
Stummmodus
Das letzte Thema war keine Technik-, sondern eine Grundsatzfrage. Im Stummmodus des Telefons war das Spiel komplett still. Ich wollte das Gegenteil und fragte sinngemäß: „Können wir den Ton auch im Stummmodus anlassen?“ Unter iOS entscheidet darüber die Kategorie der Audiositzung, in Godot eine Einstellung aus zwei Zeilen in project.godot:
[audio]
general/ios/session_category=3
general/ios/mix_with_others=true3 steht für die Kategorie „Playback“, die auch im Stummmodus Ton ausgibt. mix_with_others sorgt dafür, dass sich das Spiel mit dem Ton einer anderen App mischt, statt ihn zu unterbrechen. Diese Entscheidung ist diskutabel, denn viele Spiele respektieren den Stummmodus. In Kalecik liegt das Gegengewicht in den Einstellungen: Musik und Soundeffekte lassen sich getrennt abschalten.
Kalecik entsteht für iPhone und iPad. Version 1.0 wird für die Prüfung im App Store vorbereitet, eine Android-Version für Google Play ist unterwegs. Das Spiel wird einmalig gekauft, kommt ohne Werbung aus und funktioniert komplett offline; die Store-Links stehen auf der Kalecik-Seite, sobald sie live sind. Wie derselbe Ansatz, alles aus Code zu erzeugen, auf der visuellen Seite funktioniert, zeige ich im Beitrag über prozedurale Steinmauern, Tore und Brücken.
Häufige Fragen
Braucht man Musiktheorie, um Spielklänge im Code zu erzeugen?
Für Soundeffekte reicht grundlegende Akustik: Frequenz, Abklingzeit, Filter. Bei Musik machen Begriffe wie Tonleiter, Akkord und Kadenz einen großen Unterschied; der Komponist von Kalecik ist im Grunde die Übersetzung dieser Regeln in Code. Wenn Sie die Regeln festlegen, sorgt der Zufall nur innerhalb dieser Regeln für Abwechslung.
Ist im Code erzeugte Musik lizenzfrei?
Kalecik verwendet keine Samples, Soundpakete oder fremden Aufnahmen; jeder Klang ist die Ausgabe einer Python-Funktion. Im Spiel gibt es daher keinen Klang, der eine Lizenz von Dritten benötigt.
Was ist LUFS, und warum reicht der Spitzenpegel nicht?
LUFS misst die durchschnittliche Lautheit, gewichtet nach der Empfindlichkeit des menschlichen Gehörs. Der Spitzenpegel zeigt nur den lautesten Moment; zwei Stücke mit gleichem Spitzenpegel können sehr unterschiedlich laut wirken. Um Stücke aneinander anzugleichen, ist LUFS das bessere Maß.
Lässt sich die Musik in Kalecik abschalten?
Ja. Musik und Soundeffekte lassen sich in den Einstellungen getrennt ein- und ausschalten. Die Musik läuft auf einem eigenen Bus, Effekte und Atmosphäre auf separaten Bussen, daher beeinflusst das Abschalten der Musik die Effekte nicht.
Wann erscheint Kalecik, und auf welchen Plattformen?
Kalecik ist für iPhone und iPad gedacht. Die App-Store-Version kommt bald, eine Google-Play-Version ist in Arbeit. Das Spiel wird einmalig gekauft, ohne Werbung und Abo, und läuft offline. Den aktuellen Stand finden Sie auf der Kalecik-Seite.
Verwandte Artikel
Prozedurale Steinmauern, Tore und Brücken in Godot
Wie in Kalecik aus einem Fingerstrich eine Steinmauer, ein Torbogen, eine Brücke oder ein Haus wird: Glättung, geseedetes Mauerwerk und Mesh-Chunks.
Ein Mobile-Spiel mit KI-Agenten entwickeln: Kalecik
Vom leeren Ordner aufs iPhone: wie ich Kalecik mit fünf parallelen Claude-Code-Sitzungen gebaut habe, was ich entschied und wo es hakte.
App Store Connect API: Veröffentlichung automatisieren
So läuft die App-Store-Vorbereitung von Kalecik über die API: JWT, Preisplan, Store-Texte in 50 Sprachen, Screenshots aus der Engine, Upload per Befehl.