Skip to content
HY Devlog
Go back

It Recommends Snapshots, and Its Code Takes Them Away

I clipped this while continuing to look into AudioMixer after writing the earlier post. That post covered the log conversion, the dB range and the SetFloat / GetFloat contracts — but how to compose the mixer wasn’t part of it.

This 2023 post is that side. It walks the procedure for building a mixer in the editor screen by screen — adding groups, Expose, setting the Audio Source’s Output, wiring the sliders. If the earlier post was about units of value, this one is about procedure and structure. I’ll note where they overlap and move on, because there’s a separate set of snags.

The clipping gives three reasons to use a mixer. The third:

동적 오디오 변경: 스냅샷과 노출된 매개변수를 사용하여 다양한 Audio Mix 설정 간에 부드러운 전환을 생성하고 …

(Dynamic audio changes: using snapshots and exposed parameters you can create smooth transitions between different Audio Mix settings …)

And the code four sections later:

m_AudioMixer.SetFloat("Master", Mathf.Log10(volume) * 20);

There’s a line in the first paragraph of the SetFloat docs.

Once you call this function, mixer snapshots will no longer control the exposed parameter, and you can only modify the parameter using AudioMixer.SetFloat.

It recommends snapshots, then hands you code that takes those three parameters out of them.

Table of contents

Table of contents

It Recommends Snapshots, and Its Code Takes Them Away

The order matters. The docs’ sentence isn’t conditional — it’s “Once you call this function.” Call it once and that parameter doesn’t come back.

So following the clipping’s setup as-is gives this:

MomentWhat holds Master / BGM / SFX volume
Right after the scene startsThe mixer’s snapshots
After the slider moves onceSetFloat only

Moving a slider one notch ends the snapshot feature for those three values. If you’d built a duck-the-BGM-on-combat-entry effect as a snapshot, it stops working once the player has opened and closed the settings window. No error, no log.

ClearFloat on the same class is the route back. But its page writes only one line.

Resets an exposed parameter to its initial value.

That snapshot control returns isn’t on that page. The grounds are next door, in the GetFloat sentence quoted in the earlier post.

SetFloat이 호출되지 않았거나 ClearFloat이 쓰인 경우, 현재 스냅샷 또는 전환 중인 값을 반영한다.

(If SetFloat hasn’t been called, or ClearFloat has been used, it reflects the current snapshot or the transitioning value.)

If a value read after ClearFloat reflects “the current snapshot or the transitioning value,” then control has gone back to the snapshot — a conclusion that follows from joining two sentences, not something either page states directly. Which is why I’d recommend splitting the parameters over a design that leans on reverting.

Using both means drawing a boundary. Three ways:

ApproachParameters snapshots touchParameters sliders touch
Don’t mix—Master / BGM / SFX
Split the parametersA dedicated combat-ducking parameterMaster / BGM / SFX
Hand overOnly during the effectOnly otherwise (returned via ClearFloat)

The safest is the second. Expose the user-facing volume parameters and the effect parameters under different names from the start and there’s no place for SetFloat to collide with a snapshot. The snapshot transition itself is AudioMixerSnapshot.TransitionTo.

Performs an interpolated transition towards this snapshot over the time interval specified.

That’s the function behind the clipping’s “smooth transitions,” and timeToReach is that time. The feature the post introduces and the code the post gives are fighting over the same parameters — that’s the point of this section.

Master and BGM Both Apply

The clipping’s group layout:

[+] 버튼을 눌러서 그룹을 추가해 주면 됩니다. 이때 Master 자식으로 그룹이 분류되며 BGM그룹, SFX그룹으로 나누어 줍니다.

(Press the [+] button to add groups. They are classified as children of Master, and you split them into a BGM group and an SFX group.)

A correct description. And the manual states what that structure produces.

All sounds route into the Master group. The Master group contains categories for music, menu sounds, and all application sounds.

With the exception of sends and returns, the Audio Mixer contains groups that accept any number of input signals, mix those signals, and produce exactly one output.

Where attenuation applies is written down too.

The Attenuation (volume setting) is done here for an AudioGroup. The Attenuation can be applied anywhere in the effect stack.

Join the three sentences and the conclusion falls out. The signal that the BGM group’s attenuation was applied to enters Master, and Master’s attenuation is applied once more. Every group has attenuation and exactly one output, so a signal crossing the chain passes through two attenuations in sequence.

dB is a logarithmic unit, so the product reads as a sum.

MasterBGMAttenuation BGM actually getsAmplitude
0 dB0 dB0 dB×1
−6 dB0 dB−6 dBabout ×0.5
0 dB−6 dB−6 dBabout ×0.5
−6 dB−6 dB−12 dBabout ×0.25
−40 dB−40 dB−80 dBsilent

The last row is where this bites. Both sliders are pulled down to about the middle and nothing is audible. Each is “half” and the sum is the floor.

The docs don’t write “attenuation accumulates” as a sentence. The conclusion above comes out of the routing description, which is why I laid the grounds out as three quotes.

Three Sliders That Don’t Know About Each Other

The clipping’s three handlers are independent.

public void SetMasterVolume(float volume)
{
    m_AudioMixer.SetFloat("Master", Mathf.Log10(volume) * 20);
}

public void SetMusicVolume(float volume)
{
    m_AudioMixer.SetFloat("BGM", Mathf.Log10(volume) * 20);
}

Each writes only its own parameter. Structurally correct code, and the mixer does the summing. The problem is the expectation on the UI side. A user reads three sliders as three independent values, while what they hear is the sum of Master and the rest.

Using Master as an “overall multiplier” means the UI has to show that property. Two shapes used in practice:

Both amount to “don’t let the Master slider go down to 0.0001.”

Min Value 0.001 Puts the Floor at −60 dB

The last instruction about the slider setup:

슬라이드 세팅까지 다했으면 마지막으로 슬라이더 MinValue을 0.001로 해줍니다.

(Once the slider setup is done, finally set the slider’s MinValue to 0.001.)

The judgment that Min Value mustn’t be 0 is right. Mathf.Log10(0) is -Infinity. But the number is off by one digit.

Min ValueMathf.Log10(v) * 20In the mixer
0.001−60 dBfaintly audible
0.0001−80 dBsilent (the floor)

The earlier post dealt with the same number. There the problem was a mute button putting 0.001 in; here it’s the slider’s floor being 0.001. The symptom inverts — that one was “mute is louder than the bottom of the slider,” this one is “dragging the slider all the way down doesn’t turn it off.”

A 20 dB difference is ten times in amplitude. In a quiet scene it’s audible. A user who wants BGM fully off drags the slider to the bottom and the music is still there.

Set it to 0.0001 and the floor lines up with the mixer’s floor. The clipping’s conversion formula stays as-is — this is one digit to fix.

Assigning slider.value in Code Steps Into the Forbidden Window

The clipping’s wiring:

private void Awake()
{
    m_MusicMasterSlider.onValueChanged.AddListener(SetMasterVolume);
    m_MusicBGMSlider.onValueChanged.AddListener(SetMusicVolume);
    m_MusicSFXSlider.onValueChanged.AddListener(SetSFXVolume);
}

Doing AddListener in Awake isn’t itself a problem. What snags is the code usually added next — the line that restores the saved volume to the slider.

// The natural-looking next line
m_MusicBGMSlider.value = PlayerPrefs.GetFloat("BGM", 1f);

Slider has one method set apart.

SetValueWithoutNotify — Set the value of the slider without invoking onValueChanged callback.

That this method exists means assigning value does call the callback. So the line above calls SetMusicVolume, and inside it SetFloat is called. Inside Awake.

That’s the place the SetFloat docs forbid.

MonoBehaviour.Awake, MonoBehaviour.OnEnable, RuntimeInitializeLoadType.AfterSceneLoad

Instead, invoke this method in MonoBehaviour.Start or any event function Unity calls afterwards

And the docs put the outcome this way — “can result in unexpected behavior.”

The symptom is easy to pin. The setting saves, and the volume is back to default when you restart the game. SetFloat fails quietly and nobody looks at the return value. The earlier post flagged code that discards that return value; on this path that return value is the only clue.

There are two fixes.

FixEffect
Move initialization to StartLeaves the forbidden window
Use SetValueWithoutNotifyThe callback doesn’t run, so SetFloat isn’t called

Using both is right. In Start, set the slider with SetValueWithoutNotify and call SetFloat on the mixer separately, once. Trying to do both through the callback tangles the order.

The missing RemoveListener to pair with AddListener is worth noting too. If the settings panel lives as long as the scene it won’t bite in practice, but with a panel that toggles on and off, registrations accumulate. Why registration and teardown go in pairs is in the post on registering events with Action. UnityEvent has the same property, and the comparison with wiring it in the Inspector is in the post on InputField.

The Performance Claim Isn’t Backed by the Docs

The second reason to use a mixer:

효율적인 리소스 관리: 유사한 Audio Source를 Audio 그룹으로 그룹화하여 볼륨을 제어하고 효과를 적용하고 전체적으로 조정할 수 있습니다. Audio Source개별적으로 처리하는 성능 오버헤드를 줄입니다.

(Efficient resource management: grouping similar Audio Sources into an Audio group lets you control volume, apply effects and adjust them as a whole. It reduces the performance overhead of processing Audio Sources individually.)

The first sentence is right. Grouping to adjust as a whole is what a mixer is for. I couldn’t find grounds for the second sentence in the docs.

The mixer overview page has no sentence about CPU or AudioSource processing cost. If anything, the Audio Profiler docs put mixing itself on the cost side.

DSP CPU — the amount of CPU your project uses by mixing, audio effects, and decompression of non-streamed sounds …

Mixing is written down as an item that uses DSP CPU. Adding a group adds a node for the signal to pass through; it doesn’t reduce the number of AudioSources making sound.

Restated accurately: applying an effect once on a group is cheaper than applying it per source. Reverb on ten sources runs ten times; on the group it runs once. That’s the saving grouping gives, and it’s a different statement from the processing of the sources themselves going down.

ClaimHolds?
Groups let you adjust volume and effects at onceCorrect
An effect on a group is cheaper than one per sourceHolds. But it isn’t the sentence the clipping wrote
AudioSource per-source processing overhead goes downNo grounds in the docs

To actually see audio cost, read DSP CPU and Streaming CPU in the Profiler’s Audio module. Going through the optimization doc is in the post on Unity’s code optimization doc.

Items That Checked Out

The procedure description is almost entirely right.

The clipping’s claimVerified
Create via [Create] - [Audio Mixer]; only Master exists at firstCorrect
Groups added with [+] become children of MasterCorrect. The manual: “All sounds route into the Master group”
Right-click Attenuation’s Volume and Expose itCorrect
Rename the Exposed Parameters before using themCorrect. That name is SetFloat’s first argument
Set the group on the Audio Source’s OutputCorrect
The slider’s Min Value mustn’t be 0Correct. Log10(0) is -Infinity

Calling out the step of renaming the exposed parameter is good. The default name isn’t human-readable, and that string becomes SetFloat’s key verbatim. As the earlier post showed, one character off and SetFloat returns false and nothing happens.

Returns false if the exposed parameter was not found or snapshots are currently being edited.

That two failures are mixed into that return value is worth reading too. Name not found, and snapshots currently being edited. The latter can bite when you play with the mixer window left open in the editor.

Where and Why You’d Use It

One settings panel

The shape with all four points applied. Saving goes to PlayerPrefs, initialization to Start, and the slider is set without notifying.

using UnityEngine;
using UnityEngine.Audio;
using UnityEngine.UI;

public class AudioSettingsPanel : MonoBehaviour
{
    private const float MIN_LINEAR = 0.0001f;   // -80 dB, the mixer's floor
    private const float MAX_LINEAR = 1f;        //   0 dB
    private const float DEFAULT_LINEAR = 1f;

    private const string KEY_BGM = "volume.bgm";
    private const string KEY_SFX = "volume.sfx";

    [Header("Mixer")]
    [SerializeField, Tooltip("The mixer holding the exposed parameters")]
    private AudioMixer _mixer;

    [Header("Sliders")]
    [SerializeField, Tooltip("BGM slider. Min Value is 0.0001")]
    private Slider _bgmSlider;

    [SerializeField, Tooltip("SFX slider. Min Value is 0.0001")]
    private Slider _sfxSlider;

    // Start, not Awake. SetFloat is forbidden in Awake.
    private void Start()
    {
        float bgm = PlayerPrefs.GetFloat(KEY_BGM, DEFAULT_LINEAR);
        float sfx = PlayerPrefs.GetFloat(KEY_SFX, DEFAULT_LINEAR);

        // Set the sliders without notifying. A callback would call SetFloat twice.
        _bgmSlider.SetValueWithoutNotify(Mathf.Clamp(bgm, MIN_LINEAR, MAX_LINEAR));
        _sfxSlider.SetValueWithoutNotify(Mathf.Clamp(sfx, MIN_LINEAR, MAX_LINEAR));

        // Apply to the mixer separately, once.
        Apply("BGM", bgm);
        Apply("SFX", sfx);

        _bgmSlider.onValueChanged.AddListener(OnBgmChanged);
        _sfxSlider.onValueChanged.AddListener(OnSfxChanged);
    }

    private void OnDestroy()
    {
        // Unregister as many as were registered.
        if (_bgmSlider != null)
        {
            _bgmSlider.onValueChanged.RemoveListener(OnBgmChanged);
        }

        if (_sfxSlider != null)
        {
            _sfxSlider.onValueChanged.RemoveListener(OnSfxChanged);
        }
    }

    private void OnBgmChanged(float linear)
    {
        Apply("BGM", linear);
        PlayerPrefs.SetFloat(KEY_BGM, linear);
    }

    private void OnSfxChanged(float linear)
    {
        Apply("SFX", linear);
        PlayerPrefs.SetFloat(KEY_SFX, linear);
    }

    private void Apply(string parameterName, float linear)
    {
        float clamped = Mathf.Clamp(linear, MIN_LINEAR, MAX_LINEAR);
        float decibels = Mathf.Log10(clamped) * 20f;

        // Check the return. A wrong name shows up only here.
        if (!_mixer.SetFloat(parameterName, decibels))
        {
            Debug.LogWarning($"exposed parameter '{parameterName}' not found", this);
        }
    }
}

There’s no Master slider. Because of the summing above, Master stays at 0 dB in the mixer and isn’t exposed. What the user adjusts is BGM and SFX.

Mathf.Clamp runs again inside Apply because a saved value may fall outside the slider’s range — if you change the slider’s Min Value later, or a value saved by another build comes in. Where PlayerPrefs keeps values is in the post on its storage path.

Using it alongside snapshots

Expose a separate parameter for the effect. It doesn’t collide with user volume.

using UnityEngine;
using UnityEngine.Audio;

public class CombatAudioMood : MonoBehaviour
{
    private const float TRANSITION_SECONDS = 0.5f;

    [Header("Snapshots")]
    [SerializeField, Tooltip("Normal snapshot")]
    private AudioMixerSnapshot _normal;

    [SerializeField, Tooltip("In-combat snapshot. Ducks the BGM")]
    private AudioMixerSnapshot _combat;

    public void EnterCombat()
    {
        // Don't touch the user volume parameters. Snapshots move separate ones.
        if (_combat == null)
        {
            return;
        }

        _combat.TransitionTo(TRANSITION_SECONDS);
    }

    public void ExitCombat()
    {
        if (_normal == null)
        {
            return;
        }

        _normal.TransitionTo(TRANSITION_SECONDS);
    }
}

Separating the parameters snapshots move from the ones sliders move, from the start, is this code’s premise. Aim both at the same parameter and the snapshot lets go the moment SetFloat is called once.

Where Not to Use It

Wrapping Up


References

The starting point for this post was VR하는소년 — 유니티 Audio Mixer 사용방법 (2023-06-14). Units of value and the SetFloat / GetFloat contracts were covered in the earlier post; this one cross-checked procedure and group structure against both the current manual and the Scripting Reference. The check date is 2026-10-07.


Share this post:

Previous Post
await Awaitable Has No Value to Assign
Next Post
What Sits Between started and performed Is the Press Threshold