Skip to content

kids.kapish.audio

unity

Technical reference for the kids.kapish.audio Unity package -- audio management for Carrot Unity projects.

Package Identity

  • Package name: kids.kapish.audio
  • Display name: Carrot.Audio
  • Version: 0.1.0
  • Minimum Unity: 6000.0
  • License: MIT
  • Dependencies: kids.kapish 0.1.0, kids.kapish.addressables 0.1.0

Assembly Structure

Carrot.Audio (Runtime)

  • Path: Runtime/Carrot.Audio.asmdef
  • Root namespace: Carrot.Audio
  • References: Carrot, Carrot.Precompiled, Carrot.Addressables, Unity.Addressables, Unity.ResourceManager
  • Engine references: Yes
  • Platforms: All

The main runtime assembly. Contains AudioManager, AudioEvent, AudioHandle, AudioGroup, AudioGroupSettings, AudioSourcePool, and MusicPlayer.

Carrot.Audio.Editor (Editor)

  • Path: Editor/Carrot.Audio.Editor.asmdef
  • Root namespace: Carrot.Audio.Editor
  • References: Carrot, Carrot.Audio, Carrot.Editor
  • Platforms: Editor only

Placeholder for custom inspectors and editor tooling. Currently contains no scripts.

Architecture

Singleton Manager

AudioManager extends MonoSingleton<AudioManager> from kids.kapish. It owns the source pool, group settings array, and the MusicPlayer instance. All playback flows through the manager.

The manager is lazily initialised on first use (EnsureInitialized). Group settings are dynamically sized to match the AudioGroup enum, preserving any existing inspector values when the enum grows.

Playback Flow

  1. Caller invokes AudioManager.Instance.Play(audioEvent) or the positional overload
  2. Manager checks AudioEvent.CanPlay() (cooldown gate) and marks the event as played
  3. For direct clips: GetDirectClip() selects a clip (random or sequential), a pooled AudioSource is acquired, configured (volume, pitch, loop, spatial blend), and started
  4. For Addressable clips: a loading placeholder is registered in activeSounds, then AssetLoader.LoadAsync<AudioClip> runs. On completion, a source is acquired and playback begins. If the sound is stopped before loading finishes, the load result is discarded
  5. An AudioHandle is returned to the caller with a unique ID

Active Sound Tracking

All playing sounds are tracked in Dictionary<uint, ActiveSound>. The Update loop:

  • Ticks fade progress for any sounds in FadeState.FadingOut
  • Marks completed fades and naturally-finished non-looping sounds for cleanup
  • Returns completed sources to the pool
  • Ticks the MusicPlayer

Fade System

AudioHandle.FadeOut(timeMs) sets the FadeState on the ActiveSound struct. Each frame, the volume is lerped from its base level to zero. When the fade completes, the sound is stopped and its source returned to the pool. Fade timing is in milliseconds for consistency with the MusicPlayer.

AudioEvent Design

AudioEvent is a CarrotObject (ScriptableObject with grouped inspector support) configured entirely in the inspector:

  • Clip selection: Supports both direct AudioClip[] references and string[] Addressable keys. Direct clips are checked first, then Addressables.
  • Randomisation: When randomise is true, clips are selected randomly. When false, they cycle sequentially.
  • Variance: volumeVariance and pitchVariance add random offsets each play. Volume is clamped to 0-1, pitch to 0.1-3.0.
  • Cooldown: Minimum time between plays (in seconds). Uses Time.time for tracking.
  • Spatial blend: Optional<float> override. If unset, the manager defaults to 1.0 for positional plays and 0.0 for non-positional.

Source Pooling

AudioSourcePool wraps ComponentPool<AudioSource> from kids.kapish. On construction, it creates a template GameObject with a pre-configured AudioSource (playOnAwake = false). The pool instantiates clones from this template.

On return, sources are stopped and reset (clip cleared, loop/volume/pitch reset to defaults) before being recycled.

The pool grows on demand. The initial size is configurable on the AudioManager inspector (default 16).

Music System

MusicPlayer is a separate class (not a MonoBehaviour) owned by AudioManager. It uses two dedicated AudioSource components (not from the pool) for crossfading:

  • current -- the active music source
  • outgoing -- the previous track, fading out

Crossfade flow:

  1. PlayAsync is called with a new AudioEvent or AssetAddress
  2. Any pending Addressable load is cancelled
  3. The current source becomes outgoing
  4. A new source is created via AudioManager.AcquireMusicSource()
  5. The new track starts at volume 0 (if fading) and ramps up over fadeMs
  6. The outgoing source fades to 0 and is destroyed when complete

Music sources are added directly to the manager's GameObject and destroyed when no longer needed (not pooled, since there are at most two at any time).

The Tick(deltaTime) method is called from AudioManager.Update() to drive crossfade progress.

Group Mixing

AudioGroupSettings holds per-group Volume (0-1) and Muted state. EffectiveVolume returns 0 when muted, otherwise the raw volume.

The final volume applied to any source is: event.GetVolume() * groupSettings.EffectiveVolume * masterVolume.

Group settings are stored as an array indexed by (int)AudioGroup. The array is auto-resized in EnsureGroupSettings() whenever the enum grows, preserving existing values.

Handle System

AudioHandle is a readonly struct containing only a uint Id and a reference to the AudioManager. Methods delegate to the manager. Default handles (IsValid == false) are safe to call -- all methods no-op via null checks.

Handle IDs are monotonically increasing (++nextHandleId). There is no reuse or wrap-around handling, which is fine for the ~4 billion plays available from a uint.

Key Design Decisions

  1. Pooled sources for SFX, dedicated sources for music. SFX sources churn rapidly and benefit from pooling. Music uses at most two sources and needs to persist across fades, so dedicated components are simpler and more robust.

  2. Addressable loading is transparent. AudioEvent treats direct and Addressable clips uniformly. The manager handles the async load internally and presents the same AudioHandle API to callers.

  3. Value-type handles. AudioHandle is a readonly struct to avoid allocations on every Play call. Default values are safe (all methods no-op).

  4. Fade timing in milliseconds. Matches common audio middleware conventions and avoids confusion with per-frame deltaTime.

  5. ScriptableObject events. AudioEvent assets decouple sound design from gameplay code. Designers configure clips, variance, and groups in the inspector; code just calls Play.

  6. MusicPlayer as a plain class. Avoids the overhead and lifecycle complexity of another MonoBehaviour. The manager ticks it explicitly, keeping the update order deterministic.


Usage Guide

Practical guide to the kids.kapish.audio package -- audio management for Carrot Unity projects.

Installation

Add kids.kapish.audio as a dependency in your package's package.json or install it via the Unity Package Manager from the Carrot registry.

json
{
  "dependencies": {
    "kids.kapish.audio": "0.1.0"
  }
}

Reference the Carrot.Audio assembly in your .asmdef:

json
{
  "references": ["Carrot.Audio"]
}

Setup

The AudioManager is a singleton. Either add it to a scene manually or let it auto-create on first access. You can configure these settings in the inspector:

  • Master Volume -- global volume multiplier
  • Group Settings -- per-group volume and mute (auto-sized to match the AudioGroup enum)
  • Source Pool Size -- initial number of pooled AudioSource components (default 16)

Creating Audio Events

Create an AudioEvent asset via Create > Carrot > Audio > Audio Event in the Project window.

Inspector fields:

FieldDescription
GroupMixing group (SFX, Music, Ambient, UI, Voice)
Direct ClipsArray of AudioClip references for immediate playback
Addressable KeysArray of Addressable address strings for runtime loading
Volume / Volume VarianceBase volume with optional random offset per play
Pitch / Pitch VarianceBase pitch with optional random offset per play
CooldownMinimum seconds between plays (0 = no limit)
RandomisePick clips randomly (true) or cycle sequentially (false)
LoopWhether the sound loops
Spatial BlendOptional override for 3D spatialization (0 = 2D, 1 = 3D)

Playing Sounds

Basic 2D Playback

csharp
using Carrot.Audio;
using UnityEngine;

public class UIButtonSound : MonoBehaviour
{
    [SerializeField] private AudioEvent clickSound;

    public void OnClick()
    {
        AudioManager.Instance.Play(clickSound);
    }
}

3D Positional Playback

csharp
using Carrot.Audio;
using UnityEngine;

public class Explosion : MonoBehaviour
{
    [SerializeField] private AudioEvent explosionSound;

    void Explode()
    {
        AudioManager.Instance.Play(explosionSound, transform.position);
    }
}

Using the AudioHandle

Every Play call returns an AudioHandle you can use to control the sound later.

csharp
using Carrot.Audio;
using UnityEngine;

public class AlarmSystem : MonoBehaviour
{
    [SerializeField] private AudioEvent alarmLoop;

    private AudioHandle alarmHandle;

    public void TriggerAlarm()
    {
        alarmHandle = AudioManager.Instance.Play(alarmLoop);
    }

    public void StopAlarm()
    {
        // Immediate stop
        alarmHandle.Stop();
    }

    public void FadeAlarm()
    {
        // Fade to silence over 2 seconds
        alarmHandle.FadeOut(2000f);
    }

    void Update()
    {
        if (alarmHandle.IsPlaying)
        {
            // Alarm is still running
        }
    }
}

Default (uninitialized) handles are safe -- Stop(), FadeOut(), and IsPlaying all no-op gracefully.


Music

Access the music player through AudioManager.Instance.Music.

Play a Track from an AudioEvent

csharp
using Carrot.Audio;
using UnityEngine;

public class LevelMusic : MonoBehaviour
{
    [SerializeField] private AudioEvent levelTheme;

    async void Start()
    {
        // Crossfade to the level theme over 1.5 seconds
        await AudioManager.Instance.Music.PlayAsync(levelTheme, fadeMs: 1500f);
    }
}

Play from an Addressable Address

csharp
using Carrot.Addressables;
using Carrot.Audio;

async void PlayBossMusic()
{
    var address = new AssetAddress("Audio/Music/BossTheme");
    await AudioManager.Instance.Music.PlayAsync(address, fadeMs: 2000f);
}

Pause, Resume, and Stop

csharp
var music = AudioManager.Instance.Music;

// Pause during menus
music.Pause();

// Resume gameplay
music.Resume();

// Stop with fade
music.Stop(fadeOutMs: 1000f);

// Stop immediately
music.Stop(fadeOutMs: 0f);

Group Mixing

Control volume and mute state per group at runtime -- ideal for settings screens.

csharp
using Carrot.Audio;

void ApplyAudioSettings(float sfxVolume, float musicVolume, bool muteVoice)
{
    var mgr = AudioManager.Instance;

    mgr.SetGroupVolume(AudioGroup.SFX, sfxVolume);
    mgr.SetGroupVolume(AudioGroup.Music, musicVolume);
    mgr.SetGroupMuted(AudioGroup.Voice, muteVoice);

    // Master volume affects everything
    mgr.MasterVolume = 0.8f;
}

Reading current values:

csharp
float sfxVol = AudioManager.Instance.GetGroupVolume(AudioGroup.SFX);
float effectiveVol = AudioManager.Instance.GetEffectiveVolume(AudioGroup.SFX);
// effectiveVol = sfxVol * masterVolume (0 if group is muted)

Addressable Clips

AudioEvents support both direct clip references and Addressable string keys. You can mix both on the same event -- direct clips are checked first, falling back to Addressables if none are assigned.

This is useful for keeping initial scene load times low: assign frequently-used clips directly, and let rarer clips load on demand via Addressables.

AudioEvent "FootstepDirt"
  Direct Clips: [footstep_dirt_01, footstep_dirt_02, footstep_dirt_03]
  Addressable Keys: []                     <-- all direct, instant play

AudioEvent "VoiceLine_Boss"
  Direct Clips: []
  Addressable Keys: ["VO/Boss/Intro_01"]   <-- loaded on demand

When an Addressable clip is loading, the sound is tracked as "loading" in the manager. If you stop the handle before loading completes, the load is discarded cleanly.


Cooldowns and Variance

Cooldowns

Set Cooldown on an AudioEvent to prevent the same sound from firing too rapidly. Useful for rapid-fire weapons, footsteps, and UI feedback.

csharp
// Even if called every frame, the event will only play once per cooldown period.
AudioManager.Instance.Play(footstepEvent);

Pitch and Volume Variance

Add subtle variation to repeated sounds to avoid the "machine gun" effect:

  • Volume Variance 0.1 -- volume will randomly vary by +/-0.1 from the base
  • Pitch Variance 0.15 -- pitch will randomly vary by +/-0.15 from the base

Combined with randomised clip selection, this gives natural-sounding repetition with zero code.


Spatial Blend

By default, Play(event) is 2D (spatial blend 0) and Play(event, position) is 3D (spatial blend 1). To override this, set the Spatial Blend optional field on the AudioEvent:

  • Set to 0 to force 2D even when played with a position
  • Set to 0.5 for a half-spatial mix
  • Leave unset for the automatic 2D/3D behaviour

Sequential vs Random Clips

When Randomise is on (default), clips are picked randomly each play. When off, clips cycle through the array in order, wrapping back to the start. Sequential mode is useful for:

  • Dialogue lines that should play in order
  • Musical phrases or stingers
  • Footstep patterns that alternate left/right

Carrot