764 lines
31 KiB
GDScript
764 lines
31 KiB
GDScript
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 1–8 (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 9–16 (Index 15–47) ═════════════════════════════════
|
||
# 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
|