using System; using AcDream.Core.Audio; using DatReaderWriter.DBObjs; using DRWSound = DatReaderWriter.Enums.Sound; namespace AcDream.App.Audio; /// /// Retail's interface sound bus — SoundManager::PlaySoundFromCenter @ /// 0x00550950 over ClientUISystem::GetUISoundTable @ /// 0x00563FB0. /// /// /// "From centre" means pan 0 and GetAttenuation(0.0f, vol, &out, 0) — /// distance zero, so the flat branch of the curve, and the effect volume /// knob rather than the ambient one. Retail's separate /// interface_sound_volume preference is registered and never read, so /// there is deliberately no interface volume here either (AP-174). /// /// /// /// The bank's DID is resolved from the dats by /// rather than hard-coded, and the table itself is fetched lazily on first use /// exactly as retail's GetUISoundTable caches it behind a null check. /// /// public sealed class UiSoundController { private readonly OpenAlAudioEngine _engine; private readonly DatSoundCache _cache; private readonly ISoundRandom _rng; private readonly uint _tableDid; private SoundTable? _table; private bool _tableMissing; public UiSoundController( OpenAlAudioEngine engine, DatSoundCache cache, uint tableDid, ISoundRandom? rng = null) { _engine = engine ?? throw new ArgumentNullException(nameof(engine)); _cache = cache ?? throw new ArgumentNullException(nameof(cache)); _tableDid = tableDid; _rng = rng ?? new SoundRandom(); } /// The resolved bank DID, or 0 when the dats carry no chain. public uint TableDid => _tableDid; /// /// Play one interface slot. Returns false when the bank is absent, the slot /// is unauthored, or the entry's probability gate says silence. /// public bool Play(SoundId sound) { if (!_engine.IsAvailable || _tableDid == 0 || _tableMissing) return false; if (_table is null) { _table = _cache.GetSoundTable(_tableDid); if (_table is null) { _tableMissing = true; return false; } } var entry = SoundCookbook.Select(_table, (DRWSound)sound, _rng); if (entry is null) return false; uint waveId = (uint)entry.Id; if (waveId == 0) return false; WaveData? wave = _cache.GetWave(waveId); if (wave is null) return false; // PlaySoundFromCenter takes no volume argument, so the authored entry // volume is the one that reaches the mixer. return _engine.PlayUiWave(waveId, wave, entry.Volume); } /// /// Play the interface stinger for an AdminEnvirons change type — the /// server-driven dungeon atmosphere (chanting, drums, whispers, thunder). /// Codes retail has no case for, including 0x73/0x74 inside the /// run, play nothing. /// public bool PlayEnvironCue(uint changeType) => EnvironSoundCueMap.TryGetSound(changeType, out SoundId sound) && Play(sound); }