Split Main audio into partial class

This commit is contained in:
gamer147
2026-08-02 17:52:54 -04:00
parent f395b77a93
commit 123794cace
4 changed files with 320 additions and 307 deletions

View File

@@ -135,9 +135,10 @@ The Godot deliverable includes `Himegari.sln` because Godot's .NET exporter requ
the project do not enter its import or export scan. `tools/export-linux-x64.ps1` produces and validates the the project do not enter its import or export scan. `tools/export-linux-x64.ps1` produces and validates the
complete disposable artifact under `build/export/linux-x64/`. complete disposable artifact under `build/export/linux-x64/`.
`godot/Main.cs` retains the front-end's startup and runtime coordination. Its synthetic threaded/headless `godot/Main.cs` retains the front-end's startup and runtime coordination. Behavior-neutral partial-class
regression harness lives in the behavior-neutral partial-class companion `godot/Main.SelfTest.cs`, keeping companions keep cohesive surfaces independently navigable without changing the Godot node type or invocation
the validation surface independently navigable without changing the Godot node type or invocation path. paths: `godot/Main.SelfTest.cs` owns the synthetic threaded/headless regression harness, while
`godot/Main.Audio.cs` owns BGM, voice, sound-effect, mixer-routing/persistence, and audio-bus control.
The disposable `build/page-map-<SCENE>.jsonl` files are produced by editor/development Godot runs and map The disposable `build/page-map-<SCENE>.jsonl` files are produced by editor/development Godot runs and map
runtime ADV page ordinals to their authoritative script offsets for `tools/locate_page.py`. Packaged exports runtime ADV page ordinals to their authoritative script offsets for `tools/locate_page.py`. Packaged exports

View File

@@ -559,9 +559,11 @@ do not mix mechanical moves with semantic changes.
stage, battle, card, routine, gallery, and general table implementations plus tests into importable stage, battle, card, routine, gallery, and general table implementations plus tests into importable
modules. modules.
**Progress (2026-08-02):** the first bounded split moved `Main`'s synthetic scene builder and threaded **Progress (2026-08-02):** the first two bounded splits moved `Main`'s synthetic scene builder/threaded
self-test harness into `godot/Main.SelfTest.cs`. The partial class retains the same node type and private self-test harness into `godot/Main.SelfTest.cs`, then its BGM, voice, sound-effect, mixer, and bus-control
calls; runtime validation, including all 590 engine tests and the Godot threaded self-test, remains green. surface into `godot/Main.Audio.cs`. The partial class retains the same node type, fields, signatures, and
call sites; runtime validation, including all 590 engine tests and the Godot threaded self-test, remains
green after each move.
**Gate:** no externally visible behavior or command changes; generated artifacts are byte-identical where **Gate:** no externally visible behavior or command changes; generated artifacts are byte-identical where
deterministic, and the corresponding engine, Python, Godot, and corpus validations remain green after deterministic, and the corresponding engine, Python, Godot, and corpus validations remain green after
@@ -933,8 +935,8 @@ layer's rendering diverges from ADV; save layout.
## 8. Immediate next step ## 8. Immediate next step
Continue step 2 of the **codebase consolidation** maintenance slice: behavior-neutral physical splits backed Continue step 2 of the **codebase consolidation** maintenance slice: behavior-neutral physical splits backed
by the tracked launcher and layered validation driver. With the embedded `Main` self-test surface isolated, by the tracked launcher and layered validation driver. With the embedded `Main` self-test and audio surfaces
move the `Main` audio control surface next, then one existing domain at a time while preserving public types, isolated, move the `Main` movie playback surface next, then one existing domain at a time while preserving
commands, and generated output. public types, commands, and generated output.
Concrete playthrough blockers may still preempt this bounded maintenance work; the consolidation effort does Concrete playthrough blockers may still preempt this bounded maintenance work; the consolidation effort does
not replace Phase B gameplay validation or the open cross-platform gates. not replace Phase B gameplay validation or the open cross-platform gates.

308
godot/Main.Audio.cs Normal file
View File

@@ -0,0 +1,308 @@
using System;
using System.IO;
using Godot;
using Age.Engine.Model;
using Age.Engine.Persistence;
public partial class Main
{
private AudioStreamPlayer _bgm = null!; // looping background music
private Tween? _bgmFadeTween;
private AudioStreamPlayer _voice = null!; // interrupt-on-new voice
private int _voiceQueuedGeneration;
private int _voiceStartedGeneration;
private int _voiceCompletedGeneration;
private bool _voiceBgmDuckActive;
private int _voiceBgmDuckGeneration;
private float _voiceBgmDuckRestoreDb;
private readonly AudioStreamPlayer[] _sfx = new AudioStreamPlayer[10]; // SC0000 channels 0..9
private readonly int[] _sfxGenerations = new int[10];
private AudioMixerSettings _audioMixerSettings = null!;
private Sys4RegIniStore? _sys4RegIniStore;
// Decode VFS-owned bytes in Godot. Ordinary BGM loops; forced starts retain AGE's
// loop/one-shot mode. Voice plays once, cutting off any prior line.
public void PlayBgm(byte[] oggBytes, string assetName)
=> StartBgm(oggBytes, assetName, 1);
public void RestartBgm(byte[] oggBytes, string assetName, int startMode)
=> StartBgm(oggBytes, assetName, startMode);
private void StartBgm(byte[] oggBytes, string assetName, int startMode)
{
var stream = AudioStreamOggVorbis.LoadFromBuffer(oggBytes);
if (stream == null) { GD.Print($"OGG load failed {assetName}"); return; }
stream.Loop = startMode != 0;
// FadeBgm is marshalled from the VM thread and starts on the next Godot frame, while the VM's
// blocking deadline begins immediately. Scene startup can consequently request the replacement
// track just before the old fade tween reaches zero. Do not let that orphaned tween mute the new
// stream after this method restores its normal gain.
CancelBgmFade();
_bgm.VolumeDb = 0;
_bgm.Stream = stream;
_bgm.Play();
}
public void StopBgm()
{
RestoreVoiceBgmDuck();
CancelBgmFade();
_bgm.Stop();
_bgm.Stream = null;
_bgm.VolumeDb = 0;
}
private void PersistAudioMixerSettings(AudioMixerSettingsSnapshot snapshot)
{
try
{
_sys4RegIniStore?.Save(snapshot);
}
catch (Exception error) when (
error is IOException or UnauthorizedAccessException or InvalidDataException)
{
System.Console.Error.WriteLine(
$"[settings] audio mixer save failed; runtime setting remains active: {error.Message}");
}
}
private void ApplyAudioMixerSnapshot(AudioMixerSettingsSnapshot snapshot)
{
for (int category = 0; category < AudioMixerSettings.CategoryCount; category++)
ApplyAudioVolume(category, snapshot.Volumes[category]);
for (int category = (int)AudioMixerCategory.Music;
category < AudioMixerSettings.CategoryCount;
category++)
SetAudioBusMuted(category, !snapshot.Routes[category]);
}
public void ApplyAudioVolume(int category, int basisPoints)
{
string? busName = AudioBusName(category);
if (busName == null) return;
int bus = AudioServer.GetBusIndex(busName);
if (bus < 0) return;
int effectiveBasisPoints = basisPoints < 0
? AudioMixerSettings.MaximumVolume
: System.Math.Clamp(basisPoints, 0, AudioMixerSettings.MaximumVolume);
float linear = effectiveBasisPoints / (float)AudioMixerSettings.MaximumVolume;
AudioServer.SetBusVolumeDb(bus, linear <= 0 ? -80.0f : Mathf.LinearToDb(linear));
}
public void ApplyAudioRouteEnabled(int category, bool enabled)
{
if (category == (int)AudioMixerCategory.Music)
{
RestoreVoiceBgmDuck();
CancelBgmFade();
if (enabled)
{
if (_bgm.Stream != null) _bgm.Play();
}
else
_bgm.Stop();
}
else if (!enabled && category == (int)AudioMixerCategory.SoundEffect)
{
CancelScheduledSoundEffectStarts();
foreach (AudioStreamPlayer player in _sfx) player.Stop();
}
else if (!enabled && category == (int)AudioMixerCategory.Voice)
StopVoiceForMessageSkip();
SetAudioBusMuted(category, !enabled);
}
private static void SetAudioBusMuted(int category, bool muted)
{
string? busName = AudioBusName(category);
if (busName == null) return;
int bus = AudioServer.GetBusIndex(busName);
if (bus >= 0) AudioServer.SetBusMute(bus, muted);
}
private static string? AudioBusName(int category) => category switch
{
(int)AudioMixerCategory.Master => "Master",
(int)AudioMixerCategory.Music => "Music",
(int)AudioMixerCategory.SoundEffect => "SFX",
(int)AudioMixerCategory.Voice => "Voice",
(int)AudioMixerCategory.Movie => "Movie",
_ => null,
};
public int QueueVoicePlayback()
=> System.Threading.Interlocked.Increment(ref _voiceQueuedGeneration);
public bool IsVoicePlaybackActive
=> System.Threading.Volatile.Read(ref _voiceCompletedGeneration)
< System.Threading.Volatile.Read(ref _voiceQueuedGeneration);
public void PlayVoice(byte[] oggBytes, string assetName, int generation, bool duckBgm, int duckTargetPercent)
{
var stream = AudioStreamOggVorbis.LoadFromBuffer(oggBytes);
if (stream == null)
{
GD.Print($"OGG load failed {assetName}");
CompleteVoiceGeneration(generation);
return;
}
if (duckBgm)
BeginVoiceBgmDuck(generation, duckTargetPercent);
else
RestoreVoiceBgmDuck();
stream.Loop = false;
_voice.Stream = stream;
System.Threading.Volatile.Write(ref _voiceStartedGeneration, generation);
_voice.Play();
}
public void StopVoiceForMessageSkip()
{
_voice.Stop();
CompleteVoiceGeneration(System.Threading.Volatile.Read(ref _voiceQueuedGeneration));
}
private void UpdateVoicePlaybackState()
{
int started = System.Threading.Volatile.Read(ref _voiceStartedGeneration);
if (started > System.Threading.Volatile.Read(ref _voiceCompletedGeneration) && !_voice.Playing)
CompleteVoiceGeneration(started);
}
private void CompleteVoiceGeneration(int generation)
{
int current;
do
{
current = System.Threading.Volatile.Read(ref _voiceCompletedGeneration);
if (current >= generation) return;
}
while (System.Threading.Interlocked.CompareExchange(
ref _voiceCompletedGeneration, generation, current) != current);
if (_voiceBgmDuckActive && generation >= _voiceBgmDuckGeneration)
RestoreVoiceBgmDuck();
}
private void BeginVoiceBgmDuck(int generation, int targetPercent)
{
// Native voice ducking is suppressed while an explicit 0xc2 BGM fade owns the envelope.
if (_bgmFadeTween != null) return;
if (!_voiceBgmDuckActive)
_voiceBgmDuckRestoreDb = _bgm.VolumeDb;
_voiceBgmDuckActive = true;
_voiceBgmDuckGeneration = generation;
float linear = System.Math.Clamp(targetPercent / 100.0f, 0.0f, 1.0f);
_bgm.VolumeDb = linear <= 0 ? -80.0f : Mathf.LinearToDb(linear);
}
private void RestoreVoiceBgmDuck()
{
if (!_voiceBgmDuckActive) return;
_bgm.VolumeDb = _voiceBgmDuckRestoreDb;
_voiceBgmDuckActive = false;
_voiceBgmDuckGeneration = 0;
}
public void LoadSoundEffect(byte[] wavBytes, string assetName, int channel)
{
if ((uint)channel >= (uint)_sfx.Length) return;
_sfxGenerations[channel]++;
_sfx[channel].Stop();
_sfx[channel].Stream = null;
// This is intentionally a Godot-only compatibility boundary. The VFS and engine retain the
// original WAV bytes; Godot sees an AGE-compatible first-RIFF copy without CP932 INFO metadata.
byte[] godotWav = RiffWaveSanitizer.PrepareForGodot(wavBytes);
var stream = AudioStreamWav.LoadFromBuffer(godotWav);
if (stream == null) { GD.Print($"WAV load failed {assetName}"); return; }
stream.LoopMode = AudioStreamWav.LoopModeEnum.Disabled;
_sfx[channel].VolumeDb = 0;
_sfx[channel].Stream = stream;
}
public void StartSoundEffect(int channel) => StartSoundEffect(channel, 0);
public void StartSoundEffect(int channel, int startMode)
{
if ((uint)channel >= (uint)_sfx.Length || _sfx[channel].Stream == null) return;
if (_sfx[channel].Stream is AudioStreamWav wav)
wav.LoopMode = startMode == 0
? AudioStreamWav.LoopModeEnum.Disabled
: AudioStreamWav.LoopModeEnum.Forward;
_sfx[channel].Play();
}
public void ScheduleSoundEffectStart(int channel, int startMode, double realDelaySeconds)
{
if ((uint)channel >= (uint)_sfx.Length || _sfx[channel].Stream == null) return;
int generation = _sfxGenerations[channel];
void StartIfCurrent()
{
if (_sfxGenerations[channel] != generation || _sfx[channel].Stream == null) return;
if (_sfx[channel].Stream is AudioStreamWav wav)
wav.LoopMode = startMode == 0
? AudioStreamWav.LoopModeEnum.Disabled
: AudioStreamWav.LoopModeEnum.Forward;
_sfx[channel].Play();
}
if (realDelaySeconds <= 0)
{
StartIfCurrent();
return;
}
GetTree().CreateTimer(realDelaySeconds).Timeout += StartIfCurrent;
}
public void CancelScheduledSoundEffectStarts()
{
// Native scene_context_init_reset calls sfx_clear_scheduled_starts. Generation invalidation
// cancels the timer callbacks without stopping active sounds or unloading their channel streams.
for (int channel = 0; channel < _sfxGenerations.Length; channel++)
_sfxGenerations[channel]++;
}
public void ReleaseSoundEffect(int channel)
{
if ((uint)channel >= (uint)_sfx.Length) return;
_sfxGenerations[channel]++;
_sfx[channel].Stop();
_sfx[channel].Stream = null;
}
public void FadeBgm(int targetPercent, double realDurationSeconds)
{
float linear = System.Math.Clamp(targetPercent / 100.0f, 0.0f, 1.0f);
float targetDb = linear <= 0 ? -80.0f : Mathf.LinearToDb(linear);
RestoreVoiceBgmDuck();
CancelBgmFade();
if (realDurationSeconds <= 0) { _bgm.VolumeDb = targetDb; return; }
var tween = CreateTween();
_bgmFadeTween = tween;
tween.Finished += () =>
{
if (ReferenceEquals(_bgmFadeTween, tween))
_bgmFadeTween = null;
};
tween.TweenProperty(_bgm, "volume_db", targetDb, realDurationSeconds);
}
private void CancelBgmFade()
{
var tween = _bgmFadeTween;
_bgmFadeTween = null;
if (tween?.IsValid() == true)
tween.Kill();
}
private static void EnsureAudioBuses()
{
foreach (string name in new[] { "Music", "SFX", "Voice", "Movie" })
{
if (AudioServer.GetBusIndex(name) >= 0) continue;
AudioServer.AddBus();
AudioServer.SetBusName(AudioServer.BusCount - 1, name);
}
}
}

View File

@@ -36,19 +36,6 @@ public partial class Main : Godot.Control
private byte[] _screenPixels = []; private byte[] _screenPixels = [];
private Label _status = null!; private Label _status = null!;
private Label _locatorHud = null!; private Label _locatorHud = null!;
private AudioStreamPlayer _bgm = null!; // looping background music
private Tween? _bgmFadeTween;
private AudioStreamPlayer _voice = null!; // interrupt-on-new voice
private int _voiceQueuedGeneration;
private int _voiceStartedGeneration;
private int _voiceCompletedGeneration;
private bool _voiceBgmDuckActive;
private int _voiceBgmDuckGeneration;
private float _voiceBgmDuckRestoreDb;
private readonly AudioStreamPlayer[] _sfx = new AudioStreamPlayer[10]; // SC0000 channels 0..9
private readonly int[] _sfxGenerations = new int[10];
private AudioMixerSettings _audioMixerSettings = null!;
private Sys4RegIniStore? _sys4RegIniStore;
private VirtualMachine _vm = null!; private VirtualMachine _vm = null!;
private GodotAdvHost _host = null!; private GodotAdvHost _host = null!;
private IDisposable? _glyphRasterizerOwner; private IDisposable? _glyphRasterizerOwner;
@@ -1621,281 +1608,6 @@ public partial class Main : Godot.Control
px[i + 3] = 0; px[i + 3] = 0;
} }
// Decode VFS-owned bytes in Godot. Ordinary BGM loops; forced starts retain AGE's
// loop/one-shot mode. Voice plays once, cutting off any prior line.
public void PlayBgm(byte[] oggBytes, string assetName)
=> StartBgm(oggBytes, assetName, 1);
public void RestartBgm(byte[] oggBytes, string assetName, int startMode)
=> StartBgm(oggBytes, assetName, startMode);
private void StartBgm(byte[] oggBytes, string assetName, int startMode)
{
var stream = AudioStreamOggVorbis.LoadFromBuffer(oggBytes);
if (stream == null) { GD.Print($"OGG load failed {assetName}"); return; }
stream.Loop = startMode != 0;
// FadeBgm is marshalled from the VM thread and starts on the next Godot frame, while the VM's
// blocking deadline begins immediately. Scene startup can consequently request the replacement
// track just before the old fade tween reaches zero. Do not let that orphaned tween mute the new
// stream after this method restores its normal gain.
CancelBgmFade();
_bgm.VolumeDb = 0;
_bgm.Stream = stream;
_bgm.Play();
}
public void StopBgm()
{
RestoreVoiceBgmDuck();
CancelBgmFade();
_bgm.Stop();
_bgm.Stream = null;
_bgm.VolumeDb = 0;
}
private void PersistAudioMixerSettings(AudioMixerSettingsSnapshot snapshot)
{
try
{
_sys4RegIniStore?.Save(snapshot);
}
catch (Exception error) when (
error is IOException or UnauthorizedAccessException or InvalidDataException)
{
System.Console.Error.WriteLine(
$"[settings] audio mixer save failed; runtime setting remains active: {error.Message}");
}
}
private void ApplyAudioMixerSnapshot(AudioMixerSettingsSnapshot snapshot)
{
for (int category = 0; category < AudioMixerSettings.CategoryCount; category++)
ApplyAudioVolume(category, snapshot.Volumes[category]);
for (int category = (int)AudioMixerCategory.Music;
category < AudioMixerSettings.CategoryCount;
category++)
SetAudioBusMuted(category, !snapshot.Routes[category]);
}
public void ApplyAudioVolume(int category, int basisPoints)
{
string? busName = AudioBusName(category);
if (busName == null) return;
int bus = AudioServer.GetBusIndex(busName);
if (bus < 0) return;
int effectiveBasisPoints = basisPoints < 0
? AudioMixerSettings.MaximumVolume
: System.Math.Clamp(basisPoints, 0, AudioMixerSettings.MaximumVolume);
float linear = effectiveBasisPoints / (float)AudioMixerSettings.MaximumVolume;
AudioServer.SetBusVolumeDb(bus, linear <= 0 ? -80.0f : Mathf.LinearToDb(linear));
}
public void ApplyAudioRouteEnabled(int category, bool enabled)
{
if (category == (int)AudioMixerCategory.Music)
{
RestoreVoiceBgmDuck();
CancelBgmFade();
if (enabled)
{
if (_bgm.Stream != null) _bgm.Play();
}
else
_bgm.Stop();
}
else if (!enabled && category == (int)AudioMixerCategory.SoundEffect)
{
CancelScheduledSoundEffectStarts();
foreach (AudioStreamPlayer player in _sfx) player.Stop();
}
else if (!enabled && category == (int)AudioMixerCategory.Voice)
StopVoiceForMessageSkip();
SetAudioBusMuted(category, !enabled);
}
private static void SetAudioBusMuted(int category, bool muted)
{
string? busName = AudioBusName(category);
if (busName == null) return;
int bus = AudioServer.GetBusIndex(busName);
if (bus >= 0) AudioServer.SetBusMute(bus, muted);
}
private static string? AudioBusName(int category) => category switch
{
(int)AudioMixerCategory.Master => "Master",
(int)AudioMixerCategory.Music => "Music",
(int)AudioMixerCategory.SoundEffect => "SFX",
(int)AudioMixerCategory.Voice => "Voice",
(int)AudioMixerCategory.Movie => "Movie",
_ => null,
};
public int QueueVoicePlayback()
=> System.Threading.Interlocked.Increment(ref _voiceQueuedGeneration);
public bool IsVoicePlaybackActive
=> System.Threading.Volatile.Read(ref _voiceCompletedGeneration)
< System.Threading.Volatile.Read(ref _voiceQueuedGeneration);
public void PlayVoice(byte[] oggBytes, string assetName, int generation, bool duckBgm, int duckTargetPercent)
{
var stream = AudioStreamOggVorbis.LoadFromBuffer(oggBytes);
if (stream == null)
{
GD.Print($"OGG load failed {assetName}");
CompleteVoiceGeneration(generation);
return;
}
if (duckBgm)
BeginVoiceBgmDuck(generation, duckTargetPercent);
else
RestoreVoiceBgmDuck();
stream.Loop = false;
_voice.Stream = stream;
System.Threading.Volatile.Write(ref _voiceStartedGeneration, generation);
_voice.Play();
}
public void StopVoiceForMessageSkip()
{
_voice.Stop();
CompleteVoiceGeneration(System.Threading.Volatile.Read(ref _voiceQueuedGeneration));
}
private void UpdateVoicePlaybackState()
{
int started = System.Threading.Volatile.Read(ref _voiceStartedGeneration);
if (started > System.Threading.Volatile.Read(ref _voiceCompletedGeneration) && !_voice.Playing)
CompleteVoiceGeneration(started);
}
private void CompleteVoiceGeneration(int generation)
{
int current;
do
{
current = System.Threading.Volatile.Read(ref _voiceCompletedGeneration);
if (current >= generation) return;
}
while (System.Threading.Interlocked.CompareExchange(
ref _voiceCompletedGeneration, generation, current) != current);
if (_voiceBgmDuckActive && generation >= _voiceBgmDuckGeneration)
RestoreVoiceBgmDuck();
}
private void BeginVoiceBgmDuck(int generation, int targetPercent)
{
// Native voice ducking is suppressed while an explicit 0xc2 BGM fade owns the envelope.
if (_bgmFadeTween != null) return;
if (!_voiceBgmDuckActive)
_voiceBgmDuckRestoreDb = _bgm.VolumeDb;
_voiceBgmDuckActive = true;
_voiceBgmDuckGeneration = generation;
float linear = System.Math.Clamp(targetPercent / 100.0f, 0.0f, 1.0f);
_bgm.VolumeDb = linear <= 0 ? -80.0f : Mathf.LinearToDb(linear);
}
private void RestoreVoiceBgmDuck()
{
if (!_voiceBgmDuckActive) return;
_bgm.VolumeDb = _voiceBgmDuckRestoreDb;
_voiceBgmDuckActive = false;
_voiceBgmDuckGeneration = 0;
}
public void LoadSoundEffect(byte[] wavBytes, string assetName, int channel)
{
if ((uint)channel >= (uint)_sfx.Length) return;
_sfxGenerations[channel]++;
_sfx[channel].Stop();
_sfx[channel].Stream = null;
// This is intentionally a Godot-only compatibility boundary. The VFS and engine retain the
// original WAV bytes; Godot sees an AGE-compatible first-RIFF copy without CP932 INFO metadata.
byte[] godotWav = RiffWaveSanitizer.PrepareForGodot(wavBytes);
var stream = AudioStreamWav.LoadFromBuffer(godotWav);
if (stream == null) { GD.Print($"WAV load failed {assetName}"); return; }
stream.LoopMode = AudioStreamWav.LoopModeEnum.Disabled;
_sfx[channel].VolumeDb = 0;
_sfx[channel].Stream = stream;
}
public void StartSoundEffect(int channel) => StartSoundEffect(channel, 0);
public void StartSoundEffect(int channel, int startMode)
{
if ((uint)channel >= (uint)_sfx.Length || _sfx[channel].Stream == null) return;
if (_sfx[channel].Stream is AudioStreamWav wav)
wav.LoopMode = startMode == 0
? AudioStreamWav.LoopModeEnum.Disabled
: AudioStreamWav.LoopModeEnum.Forward;
_sfx[channel].Play();
}
public void ScheduleSoundEffectStart(int channel, int startMode, double realDelaySeconds)
{
if ((uint)channel >= (uint)_sfx.Length || _sfx[channel].Stream == null) return;
int generation = _sfxGenerations[channel];
void StartIfCurrent()
{
if (_sfxGenerations[channel] != generation || _sfx[channel].Stream == null) return;
if (_sfx[channel].Stream is AudioStreamWav wav)
wav.LoopMode = startMode == 0
? AudioStreamWav.LoopModeEnum.Disabled
: AudioStreamWav.LoopModeEnum.Forward;
_sfx[channel].Play();
}
if (realDelaySeconds <= 0)
{
StartIfCurrent();
return;
}
GetTree().CreateTimer(realDelaySeconds).Timeout += StartIfCurrent;
}
public void CancelScheduledSoundEffectStarts()
{
// Native scene_context_init_reset calls sfx_clear_scheduled_starts. Generation invalidation
// cancels the timer callbacks without stopping active sounds or unloading their channel streams.
for (int channel = 0; channel < _sfxGenerations.Length; channel++)
_sfxGenerations[channel]++;
}
public void ReleaseSoundEffect(int channel)
{
if ((uint)channel >= (uint)_sfx.Length) return;
_sfxGenerations[channel]++;
_sfx[channel].Stop();
_sfx[channel].Stream = null;
}
public void FadeBgm(int targetPercent, double realDurationSeconds)
{
float linear = System.Math.Clamp(targetPercent / 100.0f, 0.0f, 1.0f);
float targetDb = linear <= 0 ? -80.0f : Mathf.LinearToDb(linear);
RestoreVoiceBgmDuck();
CancelBgmFade();
if (realDurationSeconds <= 0) { _bgm.VolumeDb = targetDb; return; }
var tween = CreateTween();
_bgmFadeTween = tween;
tween.Finished += () =>
{
if (ReferenceEquals(_bgmFadeTween, tween))
_bgmFadeTween = null;
};
tween.TweenProperty(_bgm, "volume_db", targetDb, realDurationSeconds);
}
private void CancelBgmFade()
{
var tween = _bgmFadeTween;
_bgmFadeTween = null;
if (tween?.IsValid() == true)
tween.Kill();
}
public bool TryPlayMovie(byte[] mpegBytes, string assetName, long playbackId, public bool TryPlayMovie(byte[] mpegBytes, string assetName, long playbackId,
long resourceId, int assetId, long movieFlags, long resourceId, int assetId, long movieFlags,
long initialPositionMs, long startDelayMs, long initialPositionMs, long startDelayMs,
@@ -2004,16 +1716,6 @@ public partial class Main : Godot.Control
return MovieAudioRoute.Movie; return MovieAudioRoute.Movie;
} }
private static void EnsureAudioBuses()
{
foreach (string name in new[] { "Music", "SFX", "Voice", "Movie" })
{
if (AudioServer.GetBusIndex(name) >= 0) continue;
AudioServer.AddBus();
AudioServer.SetBusName(AudioServer.BusCount - 1, name);
}
}
public void PageBreak() public void PageBreak()
{ {
_pageCount++; _pageCount++;