using UnityEngine; using UnityEngine.Audio; /// Global master audio volume (0 = mute .. 1 = full), applied to the "MasterVolume" exposed /// parameter of the AudioMixer (loaded from Resources). Persisted via GameSettings (Settings.json). /// Applied once at startup via a runtime-init hook so the saved level takes effect before the /// first scene's audio plays, and again on every change. public static class MasterVolume { public const float Min = 0f; public const float Max = 1f; /// The default is the level currently authored on the mixer (its exposed-parameter value /// converted back to linear), so a fresh install starts at the mixer's own setting instead of /// an arbitrary constant. Falls back to 0.5 if the mixer/param is unavailable. public static float Default => TryReadMixerLinear(out float linear) ? linear : .5f; // Must match the exposed parameter name authored on the AudioMixer asset. private const string ExposedParam = "MasterVolume"; // AudioMixer.mixer lives under a Resources/ folder -> load by name without extension. private const string MixerResourcePath = "AudioMixer"; // The exposed param is the master group's attenuation in dB; below this the log curve dives // to -inf, so -80 dB is the practical silence floor (matches the mixer's own lower bound). private const float SilenceDb = -80f; private static AudioMixer _mixer; private static AudioMixer Mixer => _mixer ? _mixer : _mixer = Resources.Load(MixerResourcePath); public static float Volume { get => Mathf.Clamp(GameSettings.MasterVolume, Min, Max); set { float v = Mathf.Clamp(value, Min, Max); GameSettings.MasterVolume = v; Apply(v); // set unconditionally: keeps the live mixer in sync even if the stored value was unchanged } } // A perceptual volume slider is linear 0..1; the mixer wants dB. 20*log10 maps 1 -> 0 dB and // fades smoothly, with 0 pinned to the silence floor (log10(0) is -inf). private static float LinearToDb(float linear) => linear <= Min ? SilenceDb : Mathf.Max(SilenceDb, Mathf.Log10(linear) * 20f); // Inverse of LinearToDb: read the mixer's authored dB back into the slider's 0..1 range. private static float DbToLinear(float db) => db <= SilenceDb ? Min : Mathf.Clamp(Mathf.Pow(10f, db / 20f), Min, Max); // Reads the mixer's current exposed-parameter level (from the active snapshot) as linear 0..1. // Returns false if the mixer isn't loaded or the parameter isn't exposed, so callers can fall back. private static bool TryReadMixerLinear(out float linear) { var mixer = Mixer; if (!mixer || !mixer.GetFloat(ExposedParam, out float db)) { linear = .5f; return false; } linear = DbToLinear(db); return true; } private static void Apply(float linear) { var mixer = Mixer; if (!mixer) { Debug.LogWarning($"[MasterVolume] AudioMixer '{MixerResourcePath}' not found under a Resources/ folder."); return; } if (!mixer.SetFloat(ExposedParam, LinearToDb(linear))) Debug.LogWarning($"[MasterVolume] Exposed parameter '{ExposedParam}' not found on the AudioMixer."); } // Push the saved level before any scene audio starts. SetFloat overrides the exposed parameter // permanently (until ClearFloat), so the start snapshot's authored value is superseded. [RuntimeInitializeOnLoadMethod(RuntimeInitializeLoadType.BeforeSceneLoad)] private static void ApplyOnStartup() { Apply(Volume); } }