Files
train-game/scripts/sfx.gd
2026-09-16 17:55:38 +02:00

238 lines
9.9 KiB
GDScript

extends RefCounted
## Alle Klänge des Spiels, einmal beim Start berechnet.
const Synth = preload("res://scripts/synth.gd")
const BPM := 72.0
const BEAT := 60.0 / BPM
const BAR := BEAT * 4.0
const BARS := 8
## Eine Stufe der Harmoniefolge. Alle Akkorde liegen in derselben Lage
## (F3..G4), damit kein Takt aus dem Klangbild springt. "melody" enthält
## nur Töne, die zum Akkord passen. Geprüft wird auf kleine Sekunde und
## kleine None gegen jede Stimme - genau die reiben hörbar. Über G6 fehlt
## deshalb das C, über Cmaj7 ebenfalls.
const PROGRESSION := [
{"name": "Am7", "pad": [220.00, 261.63, 329.63, 392.00], "bass": 110.00,
"melody": [440.00, 493.88, 523.25, 587.33, 659.26, 783.99]},
{"name": "Fmaj7", "pad": [174.61, 220.00, 261.63, 329.63], "bass": 87.31,
"melody": [440.00, 523.25, 587.33, 659.26, 783.99, 880.00]},
{"name": "Cmaj7", "pad": [196.00, 246.94, 329.63, 392.00], "bass": 130.81,
"melody": [493.88, 587.33, 659.26, 783.99, 880.00]},
{"name": "G6", "pad": [196.00, 246.94, 293.66, 329.63], "bass": 98.00,
"melody": [440.00, 493.88, 587.33, 659.26, 783.99]},
{"name": "Am7", "pad": [220.00, 261.63, 329.63, 392.00], "bass": 110.00,
"melody": [440.00, 493.88, 523.25, 587.33, 659.26, 783.99]},
{"name": "Fmaj7", "pad": [174.61, 220.00, 261.63, 329.63], "bass": 87.31,
"melody": [440.00, 523.25, 587.33, 659.26, 783.99, 880.00]},
{"name": "Dm7", "pad": [220.00, 261.63, 293.66, 349.23], "bass": 146.83,
"melody": [440.00, 523.25, 587.33, 659.26, 698.46, 783.99]},
{"name": "G6", "pad": [196.00, 246.94, 293.66, 329.63], "bass": 98.00,
"melody": [440.00, 493.88, 587.33, 659.26, 783.99]},
]
## Zweiter Teil: geht über Dm7 und Em7, kommt über G6 zurück.
const PROGRESSION_B := [
{"name": "Dm7", "pad": [220.00, 261.63, 293.66, 349.23], "bass": 146.83,
"melody": [440.00, 523.25, 587.33, 659.26, 698.46, 783.99]},
{"name": "Em7", "pad": [196.00, 246.94, 293.66, 329.63], "bass": 164.81,
"melody": [493.88, 587.33, 659.26, 783.99, 880.00]},
{"name": "Fmaj7", "pad": [174.61, 220.00, 261.63, 329.63], "bass": 87.31,
"melody": [440.00, 523.25, 587.33, 659.26, 783.99, 880.00]},
{"name": "G6", "pad": [196.00, 246.94, 293.66, 329.63], "bass": 98.00,
"melody": [440.00, 493.88, 587.33, 659.26, 783.99]},
{"name": "Am7", "pad": [220.00, 261.63, 329.63, 392.00], "bass": 110.00,
"melody": [440.00, 493.88, 523.25, 587.33, 659.26, 783.99]},
{"name": "Em7", "pad": [196.00, 246.94, 293.66, 329.63], "bass": 164.81,
"melody": [493.88, 587.33, 659.26, 783.99, 880.00]},
{"name": "Fmaj7", "pad": [174.61, 220.00, 261.63, 329.63], "bass": 87.31,
"melody": [440.00, 523.25, 587.33, 659.26, 783.99, 880.00]},
{"name": "G6", "pad": [196.00, 246.94, 293.66, 329.63], "bass": 98.00,
"melody": [440.00, 493.88, 587.33, 659.26, 783.99]},
]
## Reihenfolge der Teile: A, B, A mit anderer Melodie.
const FORM := [0, 1, 0]
const SECTION_SEEDS := [7411, 9187, 3323]
static func build_all() -> Dictionary:
var rng := RandomNumberGenerator.new()
rng.seed = 20260828
return {
"clack": _clack(rng),
"clack2": _clack2(rng),
"rumble": _rumble(rng),
"squeal": _squeal(rng),
"whistle": _whistle(rng),
"hiss": _hiss(rng),
"bell": _bell(),
"coin": _coin(),
"pour": _pour(rng),
"clink": _clink(),
"thud": _thud(rng),
"step": _step(rng),
"click": _click(),
"deny": _deny(),
}
static func build_music() -> PackedByteArray:
var section_len := BAR * BARS
var loop_len := section_len * FORM.size()
var buf := Synth.buffer(loop_len + 3.0)
for sec in FORM.size():
var prog: Array = PROGRESSION if FORM[sec] == 0 else PROGRESSION_B
var rng := RandomNumberGenerator.new()
rng.seed = int(SECTION_SEEDS[sec])
_render_section(buf, sec * section_len, prog, rng)
return Synth.to_pcm(Synth.seamless(buf, loop_len))
static func _render_section(buf: PackedFloat32Array, offset: float, prog: Array,
rng: RandomNumberGenerator) -> void:
for b in BARS:
var t0 := offset + b * BAR
var stage: Dictionary = prog[b]
var pad: Array = stage["pad"]
var bass: float = float(stage["bass"])
var melody: Array = stage["melody"]
for i in pad.size():
var amp: float = 0.19 / (1.0 + i * 0.4)
Synth.add_pad(buf, t0 - 0.45, float(pad[i]), BAR + 1.0, amp, 1.2)
Synth.add_tone(buf, t0, bass, 2.3, 0.36, 1.4, [1.0, 0.38, 0.12], 0.01)
Synth.add_tone(buf, t0 + BEAT * 2.0, bass * 1.5, 1.7, 0.20, 1.8, [1.0, 0.28], 0.01)
var beats := [0.0, 1.5, 2.5] if b % 2 == 0 else [0.5, 2.0, 3.0]
for k in beats.size():
if rng.randf() < 0.3:
continue
var f: float = float(melody[rng.randi() % melody.size()])
Synth.add_tone(buf, t0 + float(beats[k]) * BEAT, f, 2.4,
0.11, 2.2, [1.0, 0.45, 0.18, 0.07], 0.006)
for beat in 4:
Synth.add_noise(buf, t0 + beat * BEAT, 0.12,
0.032 if beat % 2 == 1 else 0.020, 34.0, 0.62, rng)
# --- Einzelklänge ---------------------------------------------------------
## Ein Radstoß: tiefer Schlag, mittleres Klopfen, kurzer Metallanteil.
static func _impact(b: PackedFloat32Array, t: float, low: float, mid: float,
amp: float, rng: RandomNumberGenerator) -> void:
Synth.add_tone(b, t, low, 0.30, 0.85 * amp, 13.0, [1.0, 0.30, 0.10], 0.002)
Synth.add_tone(b, t, mid, 0.22, 0.42 * amp, 26.0, [1.0, 0.35], 0.002)
Synth.add_noise(b, t, 0.16, 0.30 * amp, 30.0, 0.14, rng, 0.001)
Synth.add_noise(b, t, 0.05, 0.20 * amp, 90.0, 0.55, rng, 0.001)
Synth.add_resonant_noise(b, t, 0.14, 1450.0, 0.985, 0.16 * amp, rng, 26.0)
## Doppelschlag der beiden Achsen eines Drehgestells.
static func _clack(rng: RandomNumberGenerator) -> PackedByteArray:
var b := Synth.buffer(0.62)
_impact(b, 0.0, 61.0, 196.0, 1.0, rng)
_impact(b, 0.155, 58.0, 183.0, 0.72, rng)
return Synth.to_pcm(b)
static func _clack2(rng: RandomNumberGenerator) -> PackedByteArray:
var b := Synth.buffer(0.62)
_impact(b, 0.0, 68.0, 213.0, 0.94, rng)
_impact(b, 0.168, 64.0, 172.0, 0.66, rng)
return Synth.to_pcm(b)
## Grundrauschen der Fahrt, als Schleife.
static func _rumble(rng: RandomNumberGenerator) -> PackedByteArray:
var loop := 1.5
var b := Synth.buffer(loop + 0.4)
var y := 0.0
var y2 := 0.0
for i in b.size():
y += (rng.randf_range(-1.0, 1.0) - y) * 0.06
y2 += (y - y2) * 0.09
b[i] = y2 * 3.4
for k in 3:
Synth.add_tone(b, 0.0, 46.0 + k * 23.0, loop + 0.4, 0.1 / (k + 1), 0.0, [1.0], 0.05)
return Synth.to_pcm(Synth.seamless(b, loop))
## Bremsenquietschen. Der tonale Teil ist exakt periodisch zur Schleifen-
## länge, der Rauschanteil wird übergeblendet - so klickt die Naht nicht.
static func _squeal(rng: RandomNumberGenerator) -> PackedByteArray:
var loop := 1.2
var unit := 1.0 / loop ## alles Vielfache davon
var noise := Synth.buffer(loop + 0.35)
Synth.add_resonant_noise(noise, 0.0, loop + 0.35, 1740.0, 0.9955, 0.55, rng)
Synth.add_resonant_noise(noise, 0.0, loop + 0.35, 2610.0, 0.9945, 0.28, rng)
Synth.add_noise(noise, 0.0, loop + 0.35, 0.05, 0.0, 0.5, rng, 0.02)
var b := Synth.seamless(noise, loop)
Synth.add_wobble(b, 0.0, unit * 1740.0, loop, 0.26, 0.010, unit * 4.0, 0.12)
Synth.add_wobble(b, 0.0, unit * 2610.0, loop, 0.13, 0.008, unit * 7.0)
return Synth.to_pcm(b)
static func _whistle(rng: RandomNumberGenerator) -> PackedByteArray:
var b := Synth.buffer(1.6)
for f in [523.0, 659.0, 784.0]:
Synth.add_tone(b, 0.0, float(f), 1.45, 0.30, 1.1,
[1.0, 0.45, 0.2, 0.08], 0.09, 0.012, 5.5)
Synth.add_tone(b, 0.0, float(f) * 1.004, 1.45, 0.22, 1.1, [1.0, 0.3], 0.11)
Synth.add_noise(b, 0.0, 1.4, 0.12, 1.4, 0.55, rng, 0.1)
return Synth.to_pcm(b)
static func _hiss(rng: RandomNumberGenerator) -> PackedByteArray:
var b := Synth.buffer(1.3)
Synth.add_noise(b, 0.0, 1.2, 0.7, 3.2, 0.75, rng, 0.03)
Synth.add_noise(b, 0.05, 1.0, 0.3, 2.4, 0.35, rng, 0.05)
return Synth.to_pcm(b)
static func _bell() -> PackedByteArray:
var b := Synth.buffer(1.9)
for k in 2:
var t := k * 0.34
Synth.add_tone(b, t, 987.77, 1.5, 0.34, 2.6, [1.0, 0.42, 0.18, 0.09], 0.004)
Synth.add_tone(b, t, 1318.51, 1.4, 0.20, 3.0, [1.0, 0.3], 0.004)
return Synth.to_pcm(b)
static func _coin() -> PackedByteArray:
var b := Synth.buffer(0.5)
Synth.add_tone(b, 0.0, 1174.66, 0.3, 0.4, 11.0, [1.0, 0.4, 0.15], 0.003)
Synth.add_tone(b, 0.07, 1567.98, 0.42, 0.4, 8.0, [1.0, 0.35, 0.12], 0.003)
Synth.add_tone(b, 0.07, 2349.32, 0.35, 0.15, 10.0, [1.0], 0.003)
return Synth.to_pcm(b)
static func _pour(rng: RandomNumberGenerator) -> PackedByteArray:
var b := Synth.buffer(0.95)
Synth.add_noise(b, 0.0, 0.9, 0.4, 1.1, 0.42, rng, 0.08)
for i in 9:
var t := 0.1 + i * 0.085
Synth.add_tone(b, t, rng.randf_range(700.0, 1500.0), 0.09, 0.1, 30.0, [1.0], 0.004)
return Synth.to_pcm(b)
static func _clink() -> PackedByteArray:
var b := Synth.buffer(0.6)
Synth.add_tone(b, 0.0, 2637.02, 0.5, 0.3, 7.0, [1.0, 0.3], 0.002)
Synth.add_tone(b, 0.0, 3520.0, 0.4, 0.16, 9.0, [1.0], 0.002)
Synth.add_tone(b, 0.11, 2793.83, 0.4, 0.2, 8.0, [1.0], 0.002)
return Synth.to_pcm(b)
static func _thud(rng: RandomNumberGenerator) -> PackedByteArray:
var b := Synth.buffer(0.4)
Synth.add_tone(b, 0.0, 68.0, 0.3, 0.65, 13.0, [1.0, 0.3], 0.003)
Synth.add_noise(b, 0.0, 0.14, 0.3, 34.0, 0.28, rng)
return Synth.to_pcm(b)
## Schritt: Ferse zuerst, dann das Abrollen. Ohne den tiefen Anteil
## bleibt nur ein heller Tick übrig.
static func _step(rng: RandomNumberGenerator) -> PackedByteArray:
var b := Synth.buffer(0.34)
Synth.add_tone(b, 0.0, 84.0, 0.26, 0.55, 15.0, [1.0, 0.28, 0.08], 0.003)
Synth.add_tone(b, 0.0, 137.0, 0.16, 0.22, 26.0, [1.0], 0.003)
Synth.add_noise(b, 0.0, 0.20, 0.26, 21.0, 0.12, rng, 0.003)
Synth.add_noise(b, 0.035, 0.12, 0.10, 34.0, 0.42, rng, 0.012)
return Synth.to_pcm(b)
static func _click() -> PackedByteArray:
var b := Synth.buffer(0.12)
Synth.add_tone(b, 0.0, 1720.0, 0.06, 0.28, 60.0, [1.0, 0.4], 0.001)
Synth.add_tone(b, 0.02, 1180.0, 0.07, 0.2, 45.0, [1.0], 0.001)
return Synth.to_pcm(b)
static func _deny() -> PackedByteArray:
var b := Synth.buffer(0.35)
Synth.add_tone(b, 0.0, 320.0, 0.14, 0.3, 16.0, [1.0, 0.4], 0.004)
Synth.add_tone(b, 0.12, 240.0, 0.2, 0.3, 13.0, [1.0, 0.4], 0.004)
return Synth.to_pcm(b)