Skip to content

Pi-Dev/SoundFlow

Folders and files

NameName
Last commit message
Last commit date

Latest commit

 

History

3 Commits
 
 
 
 
 
 
 
 

Repository files navigation

Pi-Dev SoundFlow

A modern, explicit Unity audio runtime with global weight, duck, and mute logic.

SoundFlow is a modular audio management system for Unity that lets you layer, sequence, and adapt audio playback in real time.

It's designed for precision control with adaptive music in mind. Supports live mixing, song blending and smooth transitions. Runs on top of Unity's AudioMixer.

SoundFlow is not a general-purpose runtime sound system like fmod or wwise, it does not support one-shot SFX and no events. It's is dedicated to music playback, optimized for music transitions and mixing stems.

SoundFlow cooperates very well with PiDevUtils SoundBankSet for SFX, also available here on GitHub. In fact, SoundFlow builds on top and replaces PiDevUtils' MediaPlayer, but can ve used standalone.

Features

  • Global Audio Logic

    • Weight-based volume balancing
    • Automatic ducking by priority
    • Per track mute control
  • Multiple Track Types

    • BasicFlowTrack – loops a single AudioClip
    • AdaptiveFlowTrack – multi-stem adaptive mixing based on single intensity value
    • LayeredFlowTrack – mixes multiple stems with per stem weight and pan.
  • Runtime Track Management

    • Start, stop, and swap between music tracks on the fly
    • Smoothly fade between tracks during gameplay by weight and priority
    • Schedule precise playback times using DSP timing

Installation

  1. Add the PiDev.SoundFlow scripts to your Unity project.
  2. (Optional but recommended) Install Pi-Dev Utilities — this provides the Singleton<T> base used by SoundFlowPlayer. Without it, you must implement your own singleton pattern or adjust SoundFlowPlayer accordingly.
  3. Place a SoundFlowPlayer GameObject in your scene. With Pi-Dev Utilities, SoundFlowPlayer.instance will be globally available for direct access from scripts.
  4. Attach and configure FlowTrackBase-derived components (Basic, Adaptive, Layered, Sequence) on GameObjects to set up your music playback, layering, or adaptive mixing.
  5. you can also start BasicFlowTrack tracks by using the Play() method that accepts AudioClip.

Quick Start

var player = GetComponent<SoundFlowPlayer>();

// Play a basic sound
var track = player.Play(myClip, volumeScale: 1f, name: "SFX");

// Stop by name with fade
player.StopWithName("SFX", fadeTime: 0.5f);

// Adaptive track example
// You are encouraged to design AdaptiveFlowTrack inside Unity Editor
var adaptive = gameObject.AddComponent<AdaptiveFlowTrack>();
adaptive.stems.Add(new AdaptiveFlowTrack.Stem { clip = myMusicStem });
adaptive.intensity = 0.8f;
SoundFlowPlayer.instance.Play(adaptive);

Track Types

BasicFlowTrack

Simple playback of a single AudioClip with pitch, looping, and volume scale.

AdaptiveFlowTrack

Multi-stem adaptive mixing driven by single intensity parameter with per-stem curves and fade settings.

LayeredFlowTrack

Layered multi-track playback with Manual or Weighted mixing modes and per-layer panning.

SequenceFlowTrack

Plays a list of AudioClips in order, with optional looping and delay between clips.


Debugging

Add a SoundFlowDebugger UI Text element to see all active tracks, their volumes, weights, and priorities in real time.


Developer documentation

Below is developer-facing documentation for SoundFlowPlayer and the core track system.

Core objects

  • SoundFlowPlayer — central mixer/controller. Holds all active tracks and runs the global weight/duck/mute algorithm every frame.
  • FlowTrackBase — abstract base for all tracks. Implements common state, fading and settings.

FlowTrackBase API

Fields

public SoundFlowPlayer player;
public string trackName;
public Fading fading;     // start/stop/pause/resume times (seconds)
public State state;       // runtime envelope & flags
public Settings settings; // mixing directives
public bool registerOnStart = true;

On Start(), if registerOnStart && player != null, the track auto-registers. Override if you need manual control.

Settings (per track)

  • mute — force the track to silence (ramped using fading.pauseTime).
  • priority — higher wins; lower priorities duck to 0 while a higher one is active.
  • weight — relative share within the top-priority audible group when manualVolumeControl=false.
  • manualVolumeControl — if true, the engine will not normalize by weight; it uses your settings.targetVolume as the per-track target. Cleanup-on-silence is also skipped while manual control is true.
  • targetVolume — explicit target when manualVolumeControl=true. Ignored otherwise; the engine computes a normalized target from weights.
  • exclusiveMode — excluded from the global mix/normalization pass (the engine skips them in candidate set and in UpdateFlowTrack). Useful for tracks you control entirely yourself.

Fading

  • startTime, stopTime — default fade-in/out durations for engine starts/stops.
  • pauseTime, resumeTime — used for ducking/muting behaviors.

State (read-mostly outside tracks)

  • currentVolume — the engine-computed envelope (0..1).
  • activeFadeTime — the time constant currently used to ramp toward rampTarget.
  • removeIfSilent — when true and manualVolumeControl=false, the engine will auto-remove when the track reaches silence.
  • hasError — set by SafeInvoke on exceptions.
  • isPlaying — engine considers the track in the active set.
  • scheduledDsp — absolute DSP time for next start; NaN means “now”.
  • rampTarget — internal per-frame target after priority/weight logic.

Hooks to override

  • OnPlay(SoundFlowPlayer engine) — create/configure AudioSource(s), schedule start if needed.
  • OnStop(SoundFlowPlayer engine) — mark local intent to stop; typically used to set “ramp down” flags.
  • UpdateFlowTrack(SoundFlowPlayer engine) — called every frame while the track participates in the mix; apply state.currentVolume and other per-track logic here.
  • OnCleanup(SoundFlowPlayer engine) — dispose AudioSource(s) and transient objects.
  • OnErrored(Exception e) — optional error hook.

Built-in tracks

BasicFlowTrack

  • Single AudioSource playback for one AudioClip.
  • Supports startOffset, loop, pitch, and a per-track volumeScale (multiplied by state.currentVolume).
  • Starts immediately or at state.scheduledDsp; applies source.time if startOffset is set and clip is known.

AdaptiveFlowTrack

  • Multi-stem adaptive music: each Stem has an AudioClip and an intensityCurve.
  • Global intensity drives per-stem targetVolume = intensityCurve(intensity) * state.currentVolume, with independent in/out fade times (mixFade).
  • Optional self-governed mixing:
    • setOwnTargetVolume ⇒ controls track's own target volume and assigns manual volume control.
    • setOwnWeight ⇒ controls track's own weight value based on the intensity value.
    • setOwnPriority ⇒ controls track's priority value based on the intensity value.
  • OnPlay creates/schedules one looping AudioSource per stem at a shared DSP start for tight sync and OnCleanup destroys created sources.

LayeredFlowTrack

  • Multiple stems with either ManualVolume or Weighted mixing:
    • ManualVolume — per-layer magnitude from weight (mono) or leftWeight/rightWeight (stereo). No normalization.
    • Weighted — per-channel normalization: compute each stem's left/right contribution, normalize within L/R sums, and use the dominant share as the stem's normalized magnitude.
  • Per-stem panning: stereoMode uses explicit L/R weights; mono uses pan derived from those shares.
  • Independent per-stem fades and pan ramps via mixFade.
  • Convenience setters SetLayerWeight(...) for name or index.

SequenceFlowTrack

This track type is currently not fully implemented.

  • Plays a list of SequenceClip items in order, each with an optional delayAfter.
  • Uses shared _source (non-looping). Schedules the next clip when AudioSettings.dspTime approaches _nextStartDsp.
  • loopSequence optionally wraps to the first clip. Volume follows state.currentVolume.

SoundFlowPlayer lifecycle

Registration

  • Register(FlowTrackBase track) — attaches the track to the player (ignores tracks with state.hasError). Sets track.player, clears isPlaying, and adds to the internal list.
  • Unregister(FlowTrackBase track) — removes the track and clears its player reference.

Starting playback

To start playback, you can:

  • Start a one-off clip (creates a BasicFlowTrack under the player):
    var t = SoundFlowPlayer.instance.Play(myClip, volumeScale: 1f, name: "Music", dspTime: null, startOffset: 0f, loop: true, pitch: 1f);
    If an equivalent non-errored BasicFlowTrack (same clip + name) already exists, the player reuses it.
  • Start an existing track instance:
    var layered = gameObject.AddComponent<LayeredFlowTrack>();
    SoundFlowPlayer.instance.Play(layered);   // schedules and sets isPlaying=true
    Play(track) sets state.scheduledDsp (if provided), flips isPlaying=true, and invokes track.OnPlay.

Stopping playback

  • Stop(track, fadeTime = NaN, unload = true) — computes a final fade (defaults to track.fading.stopTime or falls back to startTime), then ramps to zero and optionally unloads/destroys when silent. Calls track.OnStop immediately; OnCleanup later when removed.
  • StopWithName(name, fadeTime = 0, unload = true) — stop all tracks with a given trackName.
  • StopAll(fadeTime = 0, unload = true) — stop every track (per-track fallbacks applied).

Scheduling

  • A track can be scheduled to start at an absolute DSP time via state.scheduledDsp. The player checks each frame and starts tracks whose scheduledDsp <= AudioSettings.dspTime; if NaN, they're considered ready immediately.

  • You can schedule a BasicFlowTrack by passing the appropriate parameter to Play

The global mixing algorithm (per-frame)

Executed in Update() on SoundFlowPlayer:

  1. Start due schedules — any non-errored, registered track with !isPlaying and scheduledDsp <= now gets activeFadeTime = fading.startTime and OnPlay() invoked.
  2. Candidate set — considers tracks that are: not errored, not exclusiveMode, and isPlaying.
  3. Priority scan — finds the max settings.priority among unmuted candidates. Muted tracks are ignored for this decision.
  4. Weight sum — sums settings.weight across the audible group (top-priority, unmuted).
  5. Target assignment & ducking
    • Muted ⇒ ramp target 0 using fading.pauseTime.
    • Lower priority than max ⇒ duck to 0 using fading.pauseTime.
    • Top priority & unmuted:
      • If manualVolumeControl=false, per-track target = weight / weightSum (or 0 if sum is 0).
      • If manualVolumeControl=true, per-track target stays at settings.targetVolume.
        This per-track “ramp target” is stored in state.rampTarget.
  6. Ramp envelopestate.currentVolume moves toward state.rampTarget at a rate derived from state.activeFadeTime (falls back to fading.startTime if needed).
  7. Per-track update — calls track.UpdateFlowTrack(this) on every audible, non-exclusive track so the track can apply state.currentVolume to its internal AudioSource(s).
  8. Cleanup — if state.removeIfSilent and manualVolumeControl=false and currentVolume≈0, the track is stopped, removed, OnCleanup() is called, and the GameObject is destroyed (only if it's a child of the player).

SafeInvoke wraps all callbacks; any exception marks the track hasError=true, calls OnErrored(e), and logs in the editor.

Debugging

  • SoundFlowDebugger (attach to a UnityEngine.UI.Text) prints one line per track with: name, clip info, current → target volume, weight, priority, and flags (M mute, U manualVolumeControl, X exclusiveMode, R removeIfSilent, ERROR). Useful to verify the priority/weight logic at runtime.

Practical usage patterns

1) Weighted crossfades between songs

  • Give both tracks the same priority, set different weights, and let the engine normalize.
  • Animate weight A from 1→0 and B from 0→1; the engine keeps the sum normalized within the audible group.

2) Duck lower-priority ambience under music

  • Set ambience priority lower than music. When music is active, ambience ramps to 0 using fading.pauseTime; when music stops, ambience resumes.

3) Manual programmatic control

  • For a track you want to drive directly, set settings.manualVolumeControl=true and adjust settings.targetVolume yourself. The engine won't normalize it by weights and won't auto-remove while manual control is on.

4) Synchronized adaptive stems

  • Use AdaptiveFlowTrack; set state.scheduledDsp before Play() so all stems start sample-aligned. Drive intensity to morph the mix.

Error handling

Any exception in OnPlay/OnStop/UpdateFlowTrack/OnCleanup marks the track hasError=true, attempts OnErrored(e), and logs (Editor). Errored tracks are skipped by the engine.


Minimal example

// Create a layered music controller at runtime
var go = new GameObject("Music");
var layered = go.AddComponent<LayeredFlowTrack>();
layered.trackName = "MainMusic";
layered.stems.Add(new LayeredFlowTrack.Stem { name = "Drums", clip = drums });
layered.stems.Add(new LayeredFlowTrack.Stem { name = "Bass",  clip = bass  });
layered.stems.Add(new LayeredFlowTrack.Stem { name = "Pads",  clip = pads  });

// Start at equal share
layered.mixingMode = LayeredFlowTrack.MixingMode.Weighted;
layered.settings.priority = 10;

SoundFlowPlayer.instance.Play(layered);

// During gameplay: bring Pads forward
layered.SetLayerWeight("Pads", 1.0f);
layered.SetLayerWeight("Drums", 0.6f);
layered.SetLayerWeight("Bass",  0.8f);

// Stop gracefully
SoundFlowPlayer.instance.Stop(layered, fadeTime: 1.5f);

(Behavior: the player normalizes by per-channel shares at top priority and ramps each stem's volume/pan with mixFade.)

If you need this split into separate README sections (Overview, Usage, API Reference) or formatted for doc tooling, say and I'll output it with the structure you prefer.


License

MIT License – free to use, modify, and distribute.

About

SoundFlow is a C# music mixing and playback framework for Unity, aiming for adaptive music.

Resources

License

Stars

Watchers

Forks

Releases

No releases published

Packages

No packages published

Languages