164 lines
5.3 KiB
GDScript
164 lines
5.3 KiB
GDScript
extends RefCounted
|
|
## Kleiner Synthesizer. Erzeugt PCM-Puffer und daraus AudioStreamWAV.
|
|
## Alles wird beim Start berechnet, zur Laufzeit kostet es nichts mehr.
|
|
|
|
const RATE := 22050
|
|
const TABLE_BITS := 11
|
|
const TABLE_SIZE := 1 << TABLE_BITS
|
|
const TABLE_MASK := TABLE_SIZE - 1
|
|
|
|
static var _sine: PackedFloat32Array
|
|
|
|
static func _table() -> PackedFloat32Array:
|
|
if _sine.is_empty():
|
|
_sine.resize(TABLE_SIZE)
|
|
for i in TABLE_SIZE:
|
|
_sine[i] = sin(TAU * float(i) / float(TABLE_SIZE))
|
|
return _sine
|
|
|
|
## Sinus über eine Phase in Umdrehungen (0..1). Schneller als sin().
|
|
static func osc(phase: float) -> float:
|
|
var t := _table()
|
|
return t[int(phase * TABLE_SIZE) & TABLE_MASK]
|
|
|
|
static func buffer(seconds: float) -> PackedFloat32Array:
|
|
var b := PackedFloat32Array()
|
|
b.resize(int(seconds * RATE))
|
|
return b
|
|
|
|
## Ton mit Obertönen und exponentiellem Abfall.
|
|
static func add_tone(buf: PackedFloat32Array, start: float, freq: float, dur: float,
|
|
amp: float, decay: float, harmonics: Array = [1.0], attack := 0.005,
|
|
vibrato := 0.0, vib_hz := 5.0) -> void:
|
|
var i0 := int(start * RATE)
|
|
var n := int(dur * RATE)
|
|
var step := freq / float(RATE)
|
|
for i in n:
|
|
var idx := i0 + i
|
|
if idx < 0 or idx >= buf.size():
|
|
continue
|
|
var t := float(i) / float(RATE)
|
|
var env := exp(-t * decay)
|
|
if t < attack:
|
|
env *= t / attack
|
|
var ph := float(i) * step
|
|
if vibrato > 0.0:
|
|
# Integrierte Phasenauslenkung. Die Phase selbst zu skalieren
|
|
# würde die Tonhöhe über die Zeit davonlaufen lassen.
|
|
ph += vibrato * freq / (TAU * vib_hz) * (1.0 + osc(t * vib_hz - 0.25))
|
|
var v := 0.0
|
|
var h := 1.0
|
|
for a in harmonics:
|
|
v += float(a) * osc(ph * h)
|
|
h += 1.0
|
|
buf[idx] += v * env * amp
|
|
|
|
## Weiches Pad: langsam an, langsam aus.
|
|
static func add_pad(buf: PackedFloat32Array, start: float, freq: float, dur: float,
|
|
amp: float, fade: float) -> void:
|
|
var i0 := int(start * RATE)
|
|
var n := int(dur * RATE)
|
|
var step := freq / float(RATE)
|
|
for i in n:
|
|
var idx := i0 + i
|
|
if idx < 0 or idx >= buf.size():
|
|
continue
|
|
var t := float(i) / float(RATE)
|
|
var env: float = clampf(t / fade, 0.0, 1.0) * clampf((dur - t) / fade, 0.0, 1.0)
|
|
env = env * env * (3.0 - 2.0 * env)
|
|
var p := float(i) * step
|
|
var v := osc(p) + 0.30 * osc(p * 2.0) + 0.09 * osc(p * 3.0)
|
|
# Leichtes Schweben durch minimale Verstimmung
|
|
v += 0.26 * osc(p * 1.003)
|
|
buf[idx] += v * env * amp * 0.5
|
|
|
|
## Gefiltertes Rauschen mit Hüllkurve.
|
|
static func add_noise(buf: PackedFloat32Array, start: float, dur: float, amp: float,
|
|
decay: float, lp: float, rng: RandomNumberGenerator, attack := 0.002) -> void:
|
|
var i0 := int(start * RATE)
|
|
var n := int(dur * RATE)
|
|
var y := 0.0
|
|
for i in n:
|
|
var idx := i0 + i
|
|
if idx < 0 or idx >= buf.size():
|
|
continue
|
|
var t := float(i) / float(RATE)
|
|
var env := exp(-t * decay)
|
|
if t < attack:
|
|
env *= t / attack
|
|
y += (rng.randf_range(-1.0, 1.0) - y) * lp
|
|
buf[idx] += y * env * amp
|
|
|
|
## Ton mit echtem Frequenzwobbeln. Die Phase wird aufsummiert, nicht
|
|
## skaliert - nur so bleibt die Tonhöhe da, wo sie hingehört.
|
|
static func add_wobble(buf: PackedFloat32Array, start: float, freq: float, dur: float,
|
|
amp: float, depth: float, rate: float, harm2 := 0.0) -> void:
|
|
var i0 := int(start * RATE)
|
|
var n := int(dur * RATE)
|
|
var ph := 0.0
|
|
for i in n:
|
|
var idx := i0 + i
|
|
if idx < 0 or idx >= buf.size():
|
|
continue
|
|
var t := float(i) / float(RATE)
|
|
ph += freq * (1.0 + depth * osc(t * rate)) / float(RATE)
|
|
buf[idx] += (osc(ph) + harm2 * osc(ph * 2.0)) * amp
|
|
|
|
## Rauschen durch einen schmalen Resonanzfilter - klingt metallisch,
|
|
## ohne die Härte eines reinen Sinus.
|
|
static func add_resonant_noise(buf: PackedFloat32Array, start: float, dur: float,
|
|
freq: float, r: float, amp: float, rng: RandomNumberGenerator,
|
|
decay := 0.0) -> void:
|
|
var i0 := int(start * RATE)
|
|
var n := int(dur * RATE)
|
|
var b1 := 2.0 * r * cos(TAU * freq / float(RATE))
|
|
var b2 := -r * r
|
|
var y1 := 0.0
|
|
var y2 := 0.0
|
|
var gain := (1.0 - r) * 1.6
|
|
for i in n:
|
|
var idx := i0 + i
|
|
if idx < 0 or idx >= buf.size():
|
|
continue
|
|
var y := gain * rng.randf_range(-1.0, 1.0) + b1 * y1 + b2 * y2
|
|
y2 = y1
|
|
y1 = y
|
|
var env := 1.0 if decay <= 0.0 else exp(-float(i) / float(RATE) * decay)
|
|
buf[idx] += y * amp * env
|
|
|
|
## Legt den Ausklang auf den Anfang, damit die Schleife nahtlos wird.
|
|
static func seamless(buf: PackedFloat32Array, loop_len: float) -> PackedFloat32Array:
|
|
var n := int(loop_len * RATE)
|
|
var tail: int = mini(buf.size() - n, n)
|
|
var out := buf.slice(0, n)
|
|
for i in tail:
|
|
var w := 1.0 - float(i) / float(tail)
|
|
out[i] += buf[n + i] * w
|
|
return out
|
|
|
|
## Begrenzt sanft und wandelt in rohes PCM. Läuft auch in einem Thread.
|
|
static func to_pcm(buf: PackedFloat32Array) -> PackedByteArray:
|
|
var peak := 0.0
|
|
for v in buf:
|
|
peak = maxf(peak, absf(v))
|
|
var gain: float = 0.92 / peak if peak > 0.001 else 1.0
|
|
var data := PackedByteArray()
|
|
data.resize(buf.size() * 2)
|
|
for i in buf.size():
|
|
var v: float = clampf(buf[i] * gain, -1.0, 1.0)
|
|
data.encode_s16(i * 2, int(v * 32767.0))
|
|
return data
|
|
|
|
## Baut den abspielbaren Stream. Nur im Hauptthread aufrufen.
|
|
static func to_stream(pcm: PackedByteArray, loop := false) -> AudioStreamWAV:
|
|
var w := AudioStreamWAV.new()
|
|
w.format = AudioStreamWAV.FORMAT_16_BITS
|
|
w.mix_rate = RATE
|
|
w.stereo = false
|
|
w.data = pcm
|
|
if loop:
|
|
w.loop_mode = AudioStreamWAV.LOOP_FORWARD
|
|
w.loop_begin = 0
|
|
w.loop_end = pcm.size() / 2
|
|
return w
|