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