Files
spacel/scripts/music_player.gd
T
alpacaman 98eaa77175
/ build (push) Failing after 2m12s
add ci build
2026-04-21 15:32:59 +02:00

764 lines
31 KiB
GDScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
extends Node
# ═══════════════════════════════════════════════════════════════════════════
# "STELLAR DRIFT" · Normal-OST (A-Moll, 140 BPM, Am→G→F→Em)
# "CRITICAL MASS" · Boss-OST #1 WRAITH (A-Moll, 175 BPM, Super Hexagon)
# "EVENT HORIZON" · Boss-OST #2 LEVIATHAN (E-Phrygisch, 140 BPM, Cosmic Dread)
#
# Voices in einem AudioStreamPlayer auf dem "Music"-Bus:
# Voice 0 · Melodie · Triangle (normal/boss2), Square (boss1)
# Voice 1 · Arpeggio · Square (normal/boss1), Triangle leise (boss2)
# Voice 2 · Bass · Sawtooth (boss2: +detuneter 2. Layer für Schwebung)
# Voice 3 · Schlagzeug · Hi-Hat+Kick+Snare (normal/boss1) ·
# Kick+Floor-Tom+Mid-Tom+Reverse-Whoosh (boss2)
# Voice 4 · BH-Bass · Sine Sub-Bass, proximity-driven, beat-pulsed
# ═══════════════════════════════════════════════════════════════════════════
@export var volume_db: float = -8.0
@export var loop: bool = true
@export var autoplay: bool = true
const SAMPLE_RATE := 44100
const BUFFER_SIZE := 0.1
# ── Normale Musik ─────────────────────────────────────────────────────────
const BPM := 140.0
const BEAT := 60.0 / BPM
# ── Boss-Musik #1 (WRAITH) ────────────────────────────────────────────────
const BOSS_BPM := 175.0
const BOSS_BEAT := 60.0 / BOSS_BPM
# ── Boss-Musik #2 (LEVIATHAN) ─────────────────────────────────────────────
const BOSS2_BPM := 140.0
const BOSS2_BEAT := 60.0 / BOSS2_BPM
# ── Frequenztabelle ───────────────────────────────────────────────────────
const FREQ := {
"R" : 0.0,
"D2" : 73.42, "E2" : 82.41, "F2" : 87.31, "A2" : 110.00,
"C3" : 130.81, "D3" : 146.83, "E3" : 164.81,
"F3" : 174.61, "G3" : 196.00, "A3" : 220.00, "B3" : 246.94,
"C4" : 261.63, "D4" : 293.66, "E4" : 329.63,
"F4" : 349.23, "G4" : 392.00, "A4" : 440.00, "B4" : 493.88,
"C5" : 523.25, "D5" : 587.33, "E5" : 659.25,
"F5" : 698.46, "G5" : 783.99, "A5" : 880.00, "B5" : 987.77,
"C6" :1046.50, "E6" :1318.51,
}
# ── Normal-Akkorde (chord: 0=Am 1=G 2=F 3=Em) ─────────────────────────
const ARP := {
0: ["A4","C5","E5","C5"],
1: ["G4","B4","D5","B4"],
2: ["F4","A4","C5","A4"],
3: ["E4","G4","B4","G4"],
}
const BASS := { 0:"A3", 1:"G3", 2:"F3", 3:"E3" }
# ── Boss-Akkorde (chord: 0=Am 1=Em) ─────────────────────────────────────
const BOSS_ARP := {
0: ["A4","E5","A5","E5"], # Am — Power-Arpeggio
1: ["E4","B4","E5","B4"], # Em — Moll-Arpeggio
}
const BOSS_BASS := { 0:"A2", 1:"E2" } # tiefer Sub-Bass
# ── Boss2-Akkorde (chord: 0=E 1=F 2=Dm) — E-Phrygisch ──────────────────
const BOSS2_ARP := {
0: ["E4","B4","E5","G4"], # E (phrygisch, mit kleiner Terz G)
1: ["F4","C5","F5","A4"], # F — der phrygische II-Akkord (F→E ist der Killer)
2: ["D4","A4","D5","F4"], # Dm — dunkler iv-Akkord
}
const BOSS2_BASS := { 0:"E2", 1:"F2", 2:"D2" } # Drone-Bass Grundtöne
# ═══════════════════════════════════════════════════════════════════════════
# NORMAL-SCORE — "STELLAR DRIFT"
# Format: [mel_note, beats, chord_idx, section]
# section 0=Intro 1=Build 2=Main
# ═══════════════════════════════════════════════════════════════════════════
const SCORE: Array = [
# ═══ INTRO ═══
["A4", 2.0, 0, 0], ["E5", 2.0, 0, 0],
["C5", 2.0, 0, 0], ["A4", 2.0, 0, 0],
["E5", 1.0, 0, 0], ["A5", 1.0, 0, 0], ["E5", 1.0, 0, 0], ["C5", 1.0, 0, 0],
["A4", 4.0, 0, 0],
# ═══ BUILD ═══
["A4",0.5,0,1],["C5",0.25,0,1],["E5",0.25,0,1],
["A5",0.5,0,1],["G5",0.25,0,1],["E5",0.25,0,1],
["D5",0.5,0,1],["C5",0.25,0,1],["A4",0.25,0,1],
["C5",0.5,0,1],["R", 0.5, 0,1],
["C5",0.5,0,1],["E5",0.25,0,1],["A5",0.25,0,1],
["C6",0.5,0,1],["A5",0.25,0,1],["E5",0.25,0,1],
["G5",0.5,0,1],["E5",0.25,0,1],["C5",0.25,0,1],["A4",1.0,0,1],
["G4",0.5,1,1],["B4",0.25,1,1],["D5",0.25,1,1],
["G5",0.5,1,1],["D5",0.25,1,1],["B4",0.25,1,1],
["G4",0.5,1,1],["R", 0.25,1,1],["B4",0.25,1,1],
["D5",0.5,1,1],["G5",0.5,1,1],
["F4",0.5,2,1],["A4",0.25,2,1],["C5",0.25,2,1],
["F5",0.5,2,1],["C5",0.25,2,1],["A4",0.25,2,1],
["F5",0.5,2,1],["A5",0.25,2,1],["C6",0.25,2,1],
["A5",0.5,2,1],["F5",0.5, 2,1],
["E5",0.5,3,1],["G5",0.25,3,1],["B5",0.25,3,1],
["E6",0.5,3,1],["B5",0.25,3,1],["G5",0.25,3,1],
["A5",0.5,3,1],["G5",0.25,3,1],["E5",0.25,3,1],["B4",1.0,3,1],
# ═══ MAIN ═══
["A4",0.5,0,2],["C5",0.25,0,2],["E5",0.25,0,2],
["A5",0.5,0,2],["G5",0.25,0,2],["E5",0.25,0,2],
["D5",0.5,0,2],["C5",0.25,0,2],["A4",0.25,0,2],
["C5",0.5,0,2],["R", 0.5, 0,2],
["C5",0.5,0,2],["E5",0.25,0,2],["A5",0.25,0,2],
["C6",0.5,0,2],["A5",0.25,0,2],["E5",0.25,0,2],
["G5",0.5,0,2],["E5",0.25,0,2],["C5",0.25,0,2],["A4",1.0,0,2],
["G4",0.5,1,2],["B4",0.25,1,2],["D5",0.25,1,2],
["G5",0.5,1,2],["D5",0.25,1,2],["B4",0.25,1,2],
["G4",0.5,1,2],["R", 0.25,1,2],["B4",0.25,1,2],
["D5",0.5,1,2],["G5",0.5,1,2],
["F4",0.5,2,2],["A4",0.25,2,2],["C5",0.25,2,2],
["F5",0.5,2,2],["C5",0.25,2,2],["A4",0.25,2,2],
["F5",0.5,2,2],["A5",0.25,2,2],["C6",0.25,2,2],
["A5",0.5,2,2],["F5",0.5, 2,2],
["E5",0.5,3,2],["G5",0.25,3,2],["B5",0.25,3,2],
["E6",0.5,3,2],["B5",0.25,3,2],["G5",0.25,3,2],
["A5",0.5,3,2],["G5",0.25,3,2],["E5",0.25,3,2],["B4",1.0,3,2],
]
const LOOP_START := 9 # Loop ab BUILD (Intro überspringen)
# ═══════════════════════════════════════════════════════════════════════════
# BOSS-SCORE — "CRITICAL MASS"
# Format: [mel_note, beats, chord_idx, section]
# chord 0=Am 1=Em · section=3 (volle Intensität immer)
# BPM=175, Stil: Super Hexagon — stakkato, getrieben, 4-Bar-Loop
# ═══════════════════════════════════════════════════════════════════════════
const BOSS_SCORE: Array = [
# ══ Bar 1 (Am) — Aufstiegs-Stabs ══
["E5", 0.25, 0, 3], ["R", 0.25, 0, 3], ["G5", 0.25, 0, 3], ["A5", 0.25, 0, 3],
["E5", 0.5, 0, 3], ["R", 0.25, 0, 3], ["C5", 0.25, 0, 3],
["E5", 0.25, 0, 3], ["R", 0.25, 0, 3], ["A4", 0.5, 0, 3],
["R", 1.0, 0, 3],
# ══ Bar 2 (Am) — Synkopierter Lauf ══
["G5", 0.25, 0, 3], ["A5", 0.25, 0, 3], ["G5", 0.25, 0, 3], ["E5", 0.25, 0, 3],
["C5", 0.25, 0, 3], ["E5", 0.25, 0, 3], ["G5", 0.5, 0, 3],
["A5", 0.25, 0, 3], ["R", 0.25, 0, 3], ["G5", 0.25, 0, 3], ["E5", 0.25, 0, 3],
["C5", 0.5, 0, 3], ["R", 0.5, 0, 3],
# ══ Bar 3 (Em) — Dunkle Dringlichkeit ══
["E5", 0.25, 1, 3], ["R", 0.25, 1, 3], ["B4", 0.25, 1, 3], ["R", 0.25, 1, 3],
["G5", 0.25, 1, 3], ["E5", 0.25, 1, 3], ["D5", 0.5, 1, 3],
["B4", 0.25, 1, 3], ["R", 0.25, 1, 3], ["G5", 0.5, 1, 3],
["E5", 0.25, 1, 3], ["D5", 0.25, 1, 3], ["B4", 0.5, 1, 3],
# ══ Bar 4 (Em) — Klimax ══
["G5", 0.25, 1, 3], ["B5", 0.25, 1, 3], ["A5", 0.25, 1, 3], ["G5", 0.25, 1, 3],
["E5", 0.5, 1, 3], ["R", 0.5, 1, 3],
["D5", 0.25, 1, 3], ["R", 0.25, 1, 3], ["B4", 0.25, 1, 3], ["R", 0.25, 1, 3],
["E5", 1.0, 1, 3],
]
const BOSS_LOOP_START := 0 # Voller Loop, kein Intro
# ═══════════════════════════════════════════════════════════════════════════
# BOSS2-SCORE — "EVENT HORIZON"
# Format: [mel_note, beats, chord_idx, section]
# chord 0=E 1=F 2=Dm · section=3 (Bass+Drums immer aktiv)
# BPM=140 · E-Phrygisch · 16-Bar-Struktur: 8 Bars Phase 1 + 8 Bars Phase 2
# Melodie: lange, sparsame Töne in Phase 1 — verdichtet sich in Phase 2
# ═══════════════════════════════════════════════════════════════════════════
const BOSS2_SCORE: Array = [
# ══ PHASE 1 — Bars 18 (Index 0–14) ═══════════════════════════════════
# Bar 1 (E) — langer Tonika-Drone
["E5", 3.0, 0, 3], ["R", 1.0, 0, 3],
# Bar 2 (E) — leichte Bewegung
["G5", 2.0, 0, 3], ["E5", 2.0, 0, 3],
# Bar 3 (F) — phrygische Spannung bricht ein
["F5", 3.0, 1, 3], ["C5", 1.0, 1, 3],
# Bar 4 (F) — hält die Spannung
["F5", 2.0, 1, 3], ["A5", 2.0, 1, 3],
# Bar 5 (Dm) — dunkler Abstieg
["D5", 2.0, 2, 3], ["F5", 2.0, 2, 3],
# Bar 6 (Dm) — Weite, Atem
["A5", 3.0, 2, 3], ["R", 1.0, 2, 3],
# Bar 7 (E) — Rückkehr
["B4", 2.0, 0, 3], ["E5", 2.0, 0, 3],
# Bar 8 (E) — ganze Note als Übergang
["E5", 4.0, 0, 3],
# ══ PHASE 2 — Bars 916 (Index 1547) ═════════════════════════════════
# Verdichtung: mehr Achtel, aufsteigende Linien, Klimax-Oktav
# Bar 9 (E) — Phrygisch-Lauf aufwärts
["E5", 1.0, 0, 3], ["G5", 1.0, 0, 3], ["B5", 1.0, 0, 3], ["G5", 1.0, 0, 3],
# Bar 10 (E) — Stakkato-Antwort
["E5", 0.5, 0, 3], ["R", 0.5, 0, 3], ["B4", 1.0, 0, 3], ["G5", 1.0, 0, 3], ["E5", 1.0, 0, 3],
# Bar 11 (F) — hochklettern, der F→E-Halbton beißt
["F5", 1.0, 1, 3], ["A5", 1.0, 1, 3], ["C6", 1.0, 1, 3], ["A5", 1.0, 1, 3],
# Bar 12 (F) — absteigende Variation
["F5", 0.5, 1, 3], ["R", 0.5, 1, 3], ["C5", 1.0, 1, 3], ["A5", 1.0, 1, 3], ["F5", 1.0, 1, 3],
# Bar 13 (Dm) — Dm-Arpeggio-Lick
["D5", 1.0, 2, 3], ["F5", 1.0, 2, 3], ["A5", 1.0, 2, 3], ["F5", 1.0, 2, 3],
# Bar 14 (Dm) — Dreiklangs-Dichte
["D5", 0.5, 2, 3], ["A4", 0.5, 2, 3], ["D5", 1.0, 2, 3], ["F5", 1.0, 2, 3], ["A5", 1.0, 2, 3],
# Bar 15 (E) — finaler Aufstieg
["B4", 1.0, 0, 3], ["E5", 1.0, 0, 3], ["G5", 1.0, 0, 3], ["B5", 1.0, 0, 3],
# Bar 16 (E) — Klimax-Oktavsprung E5→E4
["E5", 2.0, 0, 3], ["E4", 2.0, 0, 3],
]
const BOSS2_LOOP_START := 0 # bei Phase 1 loopt der ganze Track
const BOSS2_PHASE2_OFFSET := 15 # Index wo Bar 9 (Phase 2) beginnt
# ── Player-Zustand ────────────────────────────────────────────────────────
var _player: AudioStreamPlayer
var _generator: AudioStreamGenerator
var _playback: AudioStreamGeneratorPlayback
var _playing := false
# Modus: 0=normal, 1=WRAITH (CRITICAL MASS), 2=LEVIATHAN (EVENT HORIZON)
var _boss_mode: int = 0
var _cur_beat: float = BEAT
# Phase-2-Umschaltung (nur boss_mode=2)
var _phase2_active: bool = false
var _pending_phase2_jump: bool = false
# Melody
var _mel_idx := 0
var _mel_pos := 0
var _mel_samples := 0
var _mel_freq := 0.0
var _mel_phase := 0.0
# Arpeggio (16tel-Noten)
var _arp_step := 0
var _arp_pos := 0
var _arp_samples := 0
var _arp_freq := 0.0
var _arp_phase := 0.0
# Bass
var _bass_chord := -1
var _bass_freq := 0.0
var _bass_phase := 0.0
# Bass-Zweit-Oszillator (nur boss_mode=2, detunet für Schwebung)
var _bass2_phase := 0.0
# Voice 4: BH Proximity Sub-Bass
var _v4_freq: float = 0.0
var _v4_freq_cur: float = 0.0
var _v4_gain: float = 0.0
var _v4_phase: float = 0.0
var _v4_is_smbh: bool = false
# Music event stingers (short pitched overlays)
var _stingers: Array = []
# Globaler Zustand
var _section := 0
var _chord := 0
var _sample_g := 0
# ── Initialisierung ───────────────────────────────────────────────────────
func _ready() -> void:
var bus_idx := AudioServer.get_bus_index("Music")
if bus_idx == -1:
AudioServer.add_bus()
bus_idx = AudioServer.get_bus_count() - 1
AudioServer.set_bus_name(bus_idx, "Music")
AudioServer.set_bus_send(bus_idx, "Master")
AudioServer.set_bus_volume_db(bus_idx,
linear_to_db(Settings.music_volume) if Settings.music_volume > 0.0 else -80.0)
_generator = AudioStreamGenerator.new()
_generator.mix_rate = SAMPLE_RATE
_generator.buffer_length = BUFFER_SIZE
_player = AudioStreamPlayer.new()
_player.stream = _generator
_player.volume_db = volume_db
_player.bus = "Music"
add_child(_player)
if autoplay:
play()
func play() -> void:
_boss_mode = 0
_cur_beat = BEAT
_phase2_active = false; _pending_phase2_jump = false
_mel_idx = 0; _mel_pos = 0; _mel_phase = 0.0
_arp_step = 0; _arp_pos = 0; _arp_phase = 0.0
_bass_chord = -1; _bass_phase = 0.0; _bass2_phase = 0.0
_sample_g = 0; _section = 0; _chord = 0
_v4_gain = 0.0; _v4_freq = 0.0; _v4_freq_cur = 0.0; _v4_phase = 0.0; _v4_is_smbh = false; _stingers.clear()
_playing = true
_player.play()
_playback = _player.get_stream_playback()
_load_mel()
func play_boss() -> void:
_boss_mode = 1
_cur_beat = BOSS_BEAT
_phase2_active = false; _pending_phase2_jump = false
_mel_idx = 0; _mel_pos = 0; _mel_phase = 0.0
_arp_step = 0; _arp_pos = 0; _arp_phase = 0.0
_bass_chord = -1; _bass_phase = 0.0; _bass2_phase = 0.0
_sample_g = 0; _section = 3; _chord = 0
_v4_gain = 0.0; _v4_freq = 0.0; _v4_freq_cur = 0.0; _v4_phase = 0.0; _v4_is_smbh = false; _stingers.clear()
if not _playing:
_playing = true
_player.play()
_playback = _player.get_stream_playback()
_load_mel()
func play_boss_leviathan(phase: int = 1) -> void:
_boss_mode = 2
_cur_beat = BOSS2_BEAT
_phase2_active = (phase >= 2)
_pending_phase2_jump = false
_mel_idx = BOSS2_PHASE2_OFFSET if _phase2_active else 0
_mel_pos = 0; _mel_phase = 0.0
_arp_step = 0; _arp_pos = 0; _arp_phase = 0.0
_bass_chord = -1; _bass_phase = 0.0; _bass2_phase = 0.0
_sample_g = 0; _section = 3; _chord = 0
_v4_gain = 0.0; _v4_freq = 0.0; _v4_freq_cur = 0.0; _v4_phase = 0.0; _v4_is_smbh = false; _stingers.clear()
if not _playing:
_playing = true
_player.play()
_playback = _player.get_stream_playback()
_load_mel()
# Wird vom game_world.gd aufgerufen, wenn LEVIATHAN Phase 2 erreicht (< 50% HP).
# Setzt nur ein Flag — der Sprung geschieht beim nächsten Note-Wechsel,
# damit der Rhythmus nicht bricht.
func enter_phase2() -> void:
if _boss_mode == 2 and not _phase2_active:
_pending_phase2_jump = true
func play_normal() -> void:
if _boss_mode == 0:
return # already playing normal — don't restart
_boss_mode = 0
_cur_beat = BEAT
_phase2_active = false; _pending_phase2_jump = false
_mel_idx = LOOP_START; _mel_pos = 0; _mel_phase = 0.0
_arp_step = 0; _arp_pos = 0; _arp_phase = 0.0
_bass_chord = -1; _bass_phase = 0.0; _bass2_phase = 0.0
_sample_g = 0
_v4_gain = 0.0; _v4_freq = 0.0; _v4_freq_cur = 0.0; _v4_phase = 0.0; _v4_is_smbh = false; _stingers.clear()
_load_mel()
func stop_music() -> void:
_playing = false
_player.stop()
func set_music_volume(linear: float) -> void:
var idx := AudioServer.get_bus_index("Music")
if idx != -1:
AudioServer.set_bus_volume_db(idx,
linear_to_db(linear) if linear > 0.0 else -80.0)
func set_muted(muted: bool) -> void:
var idx := AudioServer.get_bus_index("Music")
if idx != -1:
AudioServer.set_bus_mute(idx, muted)
func set_bh_proximity(proximity: float, size_factor: float, is_smbh: bool) -> void:
_v4_is_smbh = is_smbh
var sf := minf(size_factor * 0.12, 0.55)
_v4_gain = clamp(proximity * sf, 0.0, 0.50)
_v4_freq = _get_v4_freq()
func _get_v4_freq() -> float:
var root_freq: float
match _boss_mode:
2: root_freq = FREQ.get(BOSS2_BASS.get(_chord, "E2"), FREQ["E2"])
1: root_freq = FREQ.get(BOSS_BASS.get(_chord, "A2"), FREQ["A2"])
_: root_freq = FREQ.get(BASS.get(_chord, "A3"), FREQ["A3"])
var base := maxf(30.0, root_freq * 0.25)
if _v4_is_smbh:
return maxf(22.0, base * 0.5)
return base
func play_music_event(event_name: String) -> void:
var root_hz: float = _bass_freq if _bass_freq > 0.0 else FREQ["A3"]
match event_name:
"bh_pulse":
_stingers.append({"type": "sine", "phase": 0.0,
"freq": root_hz * 0.25, "freq_end": root_hz * 0.125,
"gain": 0.20, "pos": 0, "total": int(1.2 * SAMPLE_RATE),
"attack": int(0.05 * SAMPLE_RATE)})
"neutron_beam":
_stingers.append({"type": "noise", "phase": 0.0,
"freq": 0.0, "freq_end": 0.0,
"gain": 0.14, "pos": 0, "total": int(0.07 * SAMPLE_RATE)})
_stingers.append({"type": "sine", "phase": 0.0,
"freq": root_hz * 8.0, "freq_end": root_hz * 4.0,
"gain": 0.10, "pos": 0, "total": int(0.10 * SAMPLE_RATE)})
"white_hole_eject":
var arp_table: Dictionary
match _boss_mode:
2: arp_table = BOSS2_ARP
1: arp_table = BOSS_ARP
_: arp_table = ARP
var chord_notes: Array = arp_table.get(_chord, ["A4", "C5", "E5"])
for i in 3:
var note_str: String = chord_notes[i] if i < chord_notes.size() else "A4"
var note_hz: float = FREQ.get(note_str, root_hz * 2.0)
_stingers.append({"type": "triangle", "phase": 0.0,
"freq": note_hz, "freq_end": note_hz,
"gain": 0.10 - i * 0.025,
"pos": -int(i * 0.09 * SAMPLE_RATE),
"total": int(0.28 * SAMPLE_RATE)})
"quasar_jet":
var base_hz := root_hz * 2.0
var scale_mult := [1.0, 1.122, 1.260, 1.498]
for i in 4:
var note_hz: float = base_hz * float(scale_mult[i])
_stingers.append({"type": "sine", "phase": 0.0,
"freq": note_hz, "freq_end": note_hz * 1.04,
"gain": 0.08 + i * 0.01,
"pos": -int(i * 0.11 * SAMPLE_RATE),
"total": int(0.18 * SAMPLE_RATE)})
"galaxy_consumed":
var swell_freqs: Array[float] = [root_hz * 0.5, root_hz * 0.75, root_hz]
for i in 3:
_stingers.append({"type": "sine", "phase": 0.0,
"freq": swell_freqs[i], "freq_end": swell_freqs[i],
"gain": 0.18 - i * 0.04, "pos": 0,
"total": int(1.6 * SAMPLE_RATE),
"attack": int(0.5 * SAMPLE_RATE)})
"planet_captured":
_stingers.append({"type": "noise", "phase": 0.0,
"freq": 0.0, "freq_end": 0.0,
"gain": 0.20, "pos": 0, "total": int(0.10 * SAMPLE_RATE)})
_stingers.append({"type": "sine", "phase": 0.0,
"freq": root_hz * 2.0, "freq_end": root_hz * 0.5,
"gain": 0.18, "pos": 0, "total": int(0.45 * SAMPLE_RATE)})
"comet_pass":
_stingers.append({"type": "sine", "phase": 0.0,
"freq": root_hz * 14.0, "freq_end": root_hz * 7.0,
"gain": 0.06, "pos": 0, "total": int(0.16 * SAMPLE_RATE)})
# ── Hauptschleife ─────────────────────────────────────────────────────────
func _process(_delta: float) -> void:
if _playing:
_fill_buffer()
func _fill_buffer() -> void:
var frames := _playback.get_frames_available()
for _i in frames:
# ── Melodie-Note weiterschalten ──────────────────────────────
if _mel_pos >= _mel_samples:
# Phase-2-Sprung bei LEVIATHAN: smooth am Note-Ende
if _pending_phase2_jump and _boss_mode == 2:
_mel_idx = BOSS2_PHASE2_OFFSET
_phase2_active = true
_pending_phase2_jump = false
else:
_mel_idx += 1
var score: Array
var loop_start: int
match _boss_mode:
2:
score = BOSS2_SCORE
# Phase-2 loopt nur die zweite Hälfte (endlose Eskalation)
loop_start = BOSS2_PHASE2_OFFSET if _phase2_active else BOSS2_LOOP_START
1:
score = BOSS_SCORE
loop_start = BOSS_LOOP_START
_:
score = SCORE
loop_start = LOOP_START
if _mel_idx >= score.size():
if loop:
_mel_idx = loop_start
else:
_playing = false
_player.stop()
return
_mel_pos = 0
_load_mel()
# ── Arp-Note weiterschalten (16tel) ──────────────────────────
if _arp_pos >= _arp_samples:
_arp_step = (_arp_step + 1) % 4
_arp_pos = 0
_load_arp()
# ── Samples mischen ──────────────────────────────────────────
var s := 0.0
# Voice 0: Melodie
# Normal: Triangle warm · Boss1 (WRAITH): Square scharf · Boss2 (LEVIATHAN): Triangle mit langem Attack
if _mel_freq > 0.0:
var attack: int
var release: int
match _boss_mode:
2: attack = 800; release = 1500
1: attack = 100; release = 500
_: attack = 200; release = 1200
var env := _env(_mel_pos, _mel_samples, attack, release)
var ph := fmod(_mel_phase, 1.0)
if _boss_mode == 1:
# Square-Wave: schärfer, beißender Klang
var sq := 1.0 if ph < 0.5 else -1.0
s += sq * env * 0.26
else:
# Triangle-Wave: warm, vibraphone-artig (normal & boss2)
var tri := 1.0 - absf(4.0 * ph - 2.0)
var mel_gain := 0.32 if _boss_mode == 2 else 0.38
s += tri * env * mel_gain
# Phase-2-Oktav-Layer: zusätzliche Triangle eine Oktave höher
if _boss_mode == 2 and _phase2_active:
var ph2 := fmod(_mel_phase * 2.0, 1.0)
var tri2 := 1.0 - absf(4.0 * ph2 - 2.0)
s += tri2 * env * 0.14
_mel_phase += _mel_freq / SAMPLE_RATE
# Voice 1: Arpeggio
# Normal/Boss1: Square 50% · Boss2: leise Triangle (Sternengeflimmer)
if _arp_freq > 0.0:
var env := _env(_arp_pos, _arp_samples, 40, 400)
var ph := fmod(_arp_phase, 1.0)
if _boss_mode == 2:
var tri := 1.0 - absf(4.0 * ph - 2.0)
var gain := 0.09 if _phase2_active else 0.06
s += tri * env * gain
else:
var sq := 1.0 if ph < 0.5 else -1.0
var gain: float
if _section == 0:
gain = 0.05
elif _boss_mode == 1:
gain = 0.20
else:
gain = 0.11
s += sq * env * gain
_arp_phase += _arp_freq / SAMPLE_RATE
# Voice 2: Bass — Sawtooth (ab section 1)
# Boss2: zusätzlicher 2. Saw-Oszillator, detunet für Drone-Schwebung
if _section >= 1 and _bass_freq > 0.0:
var ph := fmod(_bass_phase, 1.0)
var bass_gain: float
if _boss_mode == 2:
bass_gain = 0.22
elif _boss_mode == 1:
bass_gain = 0.30
elif _section == 1:
bass_gain = 0.15
else:
bass_gain = 0.25
s += (2.0 * ph - 1.0) * bass_gain
_bass_phase += _bass_freq / SAMPLE_RATE
if _boss_mode == 2:
# 2. Oszillator detunet (~3‰ = leichte Schwebung bei ~82 Hz: ~0.25 Hz)
var ph2 := fmod(_bass2_phase, 1.0)
s += (2.0 * ph2 - 1.0) * 0.22
_bass2_phase += (_bass_freq * 1.003) / SAMPLE_RATE
# Voice 3: Schlagzeug (ab section 2 oder Boss-Modus)
if _section >= 2:
if _boss_mode == 2:
# ── LEVIATHAN: Tom-lastig, keine Hi-Hats ─────────────────
# Kick auf Beat 1 jedes Bars (in Phase 2 zusätzlich auf Beat 3)
var bar_period := int(4.0 * _cur_beat * SAMPLE_RATE)
var bar_pos := _sample_g % bar_period
var kick_len := 900
if bar_pos < kick_len:
var kick_env := 1.0 - float(bar_pos) / float(kick_len)
var kick_f := 1.0 - float(bar_pos) / float(kick_len)
s += sin(kick_f * kick_f * 55.0) * kick_env * 0.26
if _phase2_active:
var beat3_offset := int(2.0 * _cur_beat * SAMPLE_RATE)
var kick2_pos := (_sample_g + bar_period - beat3_offset) % bar_period
if kick2_pos < kick_len:
var kick2_env := 1.0 - float(kick2_pos) / float(kick_len)
var kick2_f := 1.0 - float(kick2_pos) / float(kick_len)
s += sin(kick2_f * kick2_f * 55.0) * kick2_env * 0.22
# Floor-Tom auf Beats 1 & 3 (period = 2 beats, tiefer Sinus mit Pitch-Drop)
var tom_period := int(2.0 * _cur_beat * SAMPLE_RATE)
var tom_pos := _sample_g % tom_period
var tom_len := 2200
if tom_pos < tom_len:
var tom_t := float(tom_pos) / float(tom_len)
var tom_env := 1.0 - tom_t
# Pitch-Envelope: startet bei 130 Hz, fällt auf 70 Hz
var tom_freq := 130.0 - 60.0 * tom_t
var tom_ph := float(tom_pos) / float(SAMPLE_RATE) * tom_freq
s += sin(tom_ph * TAU) * tom_env * 0.18
# Mid-Tom-Fill: Beat 4 jedes 4. Bars (= Ende jeder 4-Bar-Phrase)
var phrase_period := int(16.0 * _cur_beat * SAMPLE_RATE)
var phrase_pos := _sample_g % phrase_period
var mid_tom_start := int(15.0 * _cur_beat * SAMPLE_RATE)
var mid_tom_rel := phrase_pos - mid_tom_start
if mid_tom_rel >= 0:
# 4 schnelle Mid-Tom-Hits auf dem letzten Beat
var hit_len: int = int(_cur_beat * SAMPLE_RATE * 0.25)
@warning_ignore("integer_division")
var hit_idx: int = mid_tom_rel / hit_len
if hit_idx < 4:
var hit_pos := mid_tom_rel % hit_len
if hit_pos < 1400:
var mt_t := float(hit_pos) / 1400.0
var mt_env := 1.0 - mt_t
var mt_freq := 220.0 - 80.0 * mt_t
var mt_ph := float(hit_pos) / float(SAMPLE_RATE) * mt_freq
s += sin(mt_ph * TAU) * mt_env * 0.14
# Reverse-Whoosh alle 8 Bars: aufsteigende Noise-Envelope als Übergang
var whoosh_period := int(32.0 * _cur_beat * SAMPLE_RATE)
var whoosh_pos := _sample_g % whoosh_period
var whoosh_start := whoosh_period - int(2.0 * _cur_beat * SAMPLE_RATE)
if whoosh_pos >= whoosh_start:
var w_t := float(whoosh_pos - whoosh_start) / float(whoosh_period - whoosh_start)
var w_env := w_t * w_t # aufsteigend
s += randf_range(-1.0, 1.0) * w_env * 0.09
else:
# ── Normal & WRAITH: Hi-Hats + Kick + (Boss1) Snare ──────
var hat_beat_frac := 0.25 if _boss_mode == 1 else 0.5
var hat_period := int(hat_beat_frac * _cur_beat * SAMPLE_RATE)
var hat_pos := _sample_g % hat_period
var hat_len := 200 if _boss_mode == 1 else 600
if hat_pos < hat_len:
var hat_env := 1.0 - float(hat_pos) / float(hat_len)
var hat_vol := 0.08 if _boss_mode == 1 else 0.07
s += randf_range(-1.0, 1.0) * hat_env * hat_vol
var kick_period := int(_cur_beat * SAMPLE_RATE)
var kick_pos := _sample_g % kick_period
var kick_len := 750 if _boss_mode == 1 else 800
if kick_pos < kick_len:
var kick_env := 1.0 - float(kick_pos) / float(kick_len)
var kick_f := 1.0 - float(kick_pos) / float(kick_len)
var kick_vol := 0.22 if _boss_mode == 1 else 0.18
s += sin(kick_f * kick_f * 60.0) * kick_env * kick_vol
if _boss_mode == 1:
var snare_period := int(2.0 * _cur_beat * SAMPLE_RATE)
var snare_offset := int(1.0 * _cur_beat * SAMPLE_RATE)
var snare_pos := (_sample_g + snare_offset) % snare_period
if snare_pos < 340:
var snare_env := 1.0 - float(snare_pos) / 340.0
s += randf_range(-1.0, 1.0) * snare_env * 0.13
# Voice 4: BH Proximity Sub-Bass — sine, beat-pulsed, frequency glide
var v4_glide := 0.9997
_v4_freq_cur = _v4_freq_cur * v4_glide + _v4_freq * (1.0 - v4_glide)
if _v4_gain > 0.004 and _v4_freq_cur > 0.0:
var beat_samples_v4 := int(_cur_beat * SAMPLE_RATE)
if beat_samples_v4 > 0:
var beat_pos_v4 := _sample_g % beat_samples_v4
var beat_t_v4 := float(beat_pos_v4) / float(beat_samples_v4)
var beat_pulse_v4: float
if beat_t_v4 < 0.06:
beat_pulse_v4 = beat_t_v4 / 0.06
else:
beat_pulse_v4 = 1.0 - (beat_t_v4 - 0.06) / 0.94 * 0.55
_v4_phase += _v4_freq_cur / SAMPLE_RATE
s += sin(_v4_phase * TAU) * _v4_gain * (0.45 + 0.55 * beat_pulse_v4)
# Stingers: short pitched musical overlays for celestial events
for st in _stingers:
st["pos"] = int(st["pos"]) + 1
var sp: int = int(st["pos"])
if sp <= 0:
continue
var stotal: int = int(st["total"])
if sp > stotal:
continue
var t_st := float(sp) / float(stotal)
var atk: int = int(st.get("attack", int(0.008 * SAMPLE_RATE)))
var env_st: float
if sp < atk:
env_st = float(sp) / float(atk)
else:
var remaining := stotal - atk
if remaining > 0:
env_st = maxf(0.0, 1.0 - float(sp - atk) / float(remaining))
else:
env_st = 0.0
var sf_st: float = lerp(float(st["freq"]), float(st["freq_end"]), t_st)
match st["type"]:
"sine":
st["phase"] = fmod(float(st["phase"]) + sf_st / SAMPLE_RATE, 1.0)
s += sin(float(st["phase"]) * TAU) * env_st * float(st["gain"])
"triangle":
st["phase"] = fmod(float(st["phase"]) + sf_st / SAMPLE_RATE, 1.0)
s += (1.0 - absf(4.0 * float(st["phase"]) - 2.0)) * env_st * float(st["gain"])
"noise":
s += randf_range(-1.0, 1.0) * env_st * float(st["gain"])
_stingers = _stingers.filter(func(st): return int(st["pos"]) <= int(st["total"]))
_playback.push_frame(Vector2(s, s))
_mel_pos += 1
_arp_pos += 1
_sample_g += 1
# ── Note laden ────────────────────────────────────────────────────────────
func _load_mel() -> void:
var score: Array
match _boss_mode:
2: score = BOSS2_SCORE
1: score = BOSS_SCORE
_: score = SCORE
var e: Array = score[_mel_idx]
_mel_freq = FREQ.get(e[0], 0.0)
_mel_samples = int(float(e[1]) * _cur_beat * SAMPLE_RATE)
_mel_phase = 0.0
_section = e[3]
if e[2] != _chord:
_chord = e[2]
_arp_step = 0
_arp_pos = 0
_load_arp()
if e[2] != _bass_chord:
_bass_chord = e[2]
var bass_dict: Dictionary
match _boss_mode:
2: bass_dict = BOSS2_BASS
1: bass_dict = BOSS_BASS
_: bass_dict = BASS
_bass_freq = FREQ.get(bass_dict[_bass_chord], 0.0)
_bass_phase = 0.0
_bass2_phase = 0.0
_v4_freq = _get_v4_freq()
func _load_arp() -> void:
var arp_dict: Dictionary
var arp_step_count: int = 4
match _boss_mode:
2:
arp_dict = BOSS2_ARP
# Arp-Geschwindigkeit: Phase 1 = 8tel, Phase 2 = 16tel (verdichtet)
var arp_frac := 0.25 if _phase2_active else 0.5
_arp_samples = int(arp_frac * _cur_beat * SAMPLE_RATE)
1:
arp_dict = BOSS_ARP
_arp_samples = int(0.25 * _cur_beat * SAMPLE_RATE)
_:
arp_dict = ARP
_arp_samples = int(0.25 * _cur_beat * SAMPLE_RATE)
_arp_freq = FREQ.get(arp_dict[_chord][_arp_step % arp_step_count], 0.0)
_arp_phase = 0.0
# ── Hüllkurve ─────────────────────────────────────────────────────────────
func _env(pos: int, total: int, attack: int, release: int) -> float:
if pos < attack:
return float(pos) / maxf(attack, 1.0)
elif pos > total - release:
return float(total - pos) / maxf(release, 1.0)
return 1.0