Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion .claude-plugin/plugin.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "unity",
"version": "0.1.0-beta",
"version": "0.1.1-beta",
"description": "Unity's official plugin for Claude Code, with curated skills for game development, monetization, and performance optimization.",
"author": {
"name": "Unity Technologies",
Expand Down
127 changes: 127 additions & 0 deletions skills/audio-setup-mixers/SKILL.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,127 @@
---
name: audio-setup-mixers
description: Scans the scene and audio assets to appropriately route Audio Sources into existing Audio Mixer Groups, classifying each source by what it plays. Use when the user asks about cleaning up mixer assignments, routing audio through a mixer, or which group a sound belongs in. Creating mixers and groups, and setting volumes, are not automated — the skill inventories what exists and asks the user to add anything missing.
---
# Audio Mixer Setup

Routing an Audio Source to a mixer group is a scene edit that only a running Editor can
make, so this skill needs a live Editor it can execute C# in. Step 0 establishes that
before anything else.

**What this skill automates, and what it hands back to you.** Inspecting mixers and
routing Audio Sources into groups is entirely public Unity API, and that is the tedious
part — walking dozens of sources and classifying them by what they play. Creating a mixer
or a group has no public API; it exists only on types Unity does not commit to keeping
stable. So this skill will not create groups behind your back. It inventories what exists,
proposes the routing, asks you to add any missing group in the Audio Mixer window, and
then does all the routing itself.

That is a deliberate limit, not a gap to work around. Do not reach for reflection to
create groups, and do not hand-edit a `.mixer` file — mixer structure is not safely
authorable blind.

## Step 0: Confirm you can run C# in the Editor

Every C# step below runs inside a live Editor through the Unity CLI. **The `unity-cli` skill
owns getting you there** — installing the CLI, confirming a connected Editor, adding the
project's `com.unity.pipeline` package, telling a genuinely absent Editor apart from one
stuck in Safe Mode, and discovering the Editor's command catalog. Follow it first; don't
re-derive any of it here.

Two things it can't know for you:

- **You need `eval` in particular**, not just a reachable Editor. Confirm it appears in the
catalog. Its presence depends on the Pipeline package version, not on the CLI, so a
healthy install can still lack it — if it's missing, say so and stop.
- **Do not fall back to editing `.mixer` files by hand.** Mixer routing is not safely
authorable blind, so an unreachable Editor is a stop, not a cue to improvise.

Once `eval` is available, that is how each C# step below runs.

Run C# through the connected Editor with the `eval` command. Discover its parameter shape
from `unity command --format json` rather than assuming one — the inline form is
`unity command eval --code '<snippet>'`, and some Pipeline versions also register
`eval_file` for running a snippet from a file. **Check the catalog before reaching for
`eval_file`; it is frequently absent.** `unity command` defaults to a 30 second timeout.

### Passing C# to `eval`

`eval` compiles a **statement block, not a file**. Two consequences, both of which cause a
compile error rather than a warning:

- **No `using` directives.** The compiler reads `using UnityEngine;` as a resource-disposal
statement and rejects it (`CS0210`).
- **Types must be fully qualified.** A bare `AssetDatabase` or `Volume` does not resolve
(`CS0246` / `CS0103`), and a bare `Object` is ambiguous with `object` (`CS0104`).

Where a snippet below is written as a file — with usings, for readability, or because it is
meant to be saved into the project — qualify the types before passing it to `eval`.

## Step 1: Pre-flight
If the user hasn't explicitly asked for Audio Mixers, confirm that they want to proceed with setting them up.

Then inventory what already exists with the mixer-inventory snippet in
[references/api.md](references/api.md), run through the Editor as described in Step 0. That gives
you every mixer in the project and the group names in each.

It returns a flat list of groups, not the parent/child tree. That is enough to route into, and it
is all the public API exposes. If the hierarchy matters for the conversation, ask the user to look
at the Audio Mixer window and describe it — don't reach for the non-public tree API to find out.

**If the project has no mixer at all,** say so and stop rather than improvising one: creating a
mixer has no public API. Ask the user to create one (Window → Audio → Audio Mixer, then the **+**
next to Mixers), and pick up from here once it exists.

## Step 2: Find scene references
Find all Audio Source components, look at their assigned Generator asset names, and generalize a fitting class or category of the sound name, ideally something already existing.
Examples for Audio Clip asset names:
- "FootStep4_Sound" -> Foley
- "Dialogue_Female_Scene4" -> Vox/Voice/Dialogue
- "GunShot" -> SFX
- "Menu_Theme_Variation" -> Music

If the assigned asset isn't descriptive or non-existing, try to look at the GameObject name or potential adjacent MonoBehaviour names.
Ask to create an Uncategorized group if it seems hard or confidence is low in classifying how an Audio Source is being used.

## Step 3: Agree the group list, and get any missing groups created
Present the classification from Step 2 as a proposed routing — each Audio Source and the group you
intend to send it to — and revise it with the user.

**WAIT for the user to respond before proceeding.**

Prefer an existing group when it genuinely covers the category, even if you'd have named it
differently. But **don't collapse categories that a mixing engineer would keep apart** — Foley is a
subset of SFX, not another word for it, so a gunshot does not belong in a `Foley` group just because
one exists. When the existing groups only partly cover your categories, say which ones fit and which
need a new group, and let the user decide.

For categories with no matching group, you cannot create the group — there is no public API for it.
Hand it over precisely, naming the mixer and the exact group names, as shown at the end of
[references/api.md](references/api.md). Then **re-run the inventory snippet to confirm the groups
exist and check their spelling** before routing. Don't assume the user did it, and don't assume
they spelled it the way you asked.

## Step 4: Route the Audio Sources
With the group list settled and confirmed present, assign each Audio Source's output group using the
routing snippet in [references/api.md](references/api.md). It is public API throughout, and it wraps
the whole pass in a single undo step so the user can back all of it out at once.

**Key the mapping on the identifier you classified by.** Step 2 reads the clip asset name first and
only falls back to the GameObject name, so the mapping accepts either — the two are different
identifiers and keying on the wrong one drops sources.

Three things to report rather than assume:

- **Any `NO SUCH GROUP` entries the snippet returns.** That means a group you expected is not in the
mixer — usually a spelling difference. Resolve it with the user, don't silently skip the source.
- **Any `NOT IN THE MAPPING` entries.** Those are Audio Sources your classification missed. Reporting
a successful routing while sources were quietly left unrouted is the worst outcome here, because it
reads as success.
- **The scene was modified, not the mixer asset.** Routing lives on the Audio Source, so it only
persists once the scene is saved. Tell the user, and save only with their agreement.

**Volume, effects, and re-parenting are out of scope.** Those live on non-public API. If the user
asks for them, say the routing is done and point them at the Audio Mixer window for the mix itself.

## References
See [references/api.md](references/api.md)
162 changes: 162 additions & 0 deletions skills/audio-setup-mixers/references/api.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,162 @@
## What this skill does and does not automate

Every `AudioMixer` in the Editor is really an `AudioMixerController`, and every `AudioMixerGroup` is
an `AudioMixerGroupController`. Those two controller types are **not public**, and the authoring
calls — creating a mixer, creating a group, re-parenting a group, changing a group's volume — exist
only on them.

This skill deliberately does not use them. Unity makes no stability commitment for non-public API,
so a skill built on one can break silently between versions: the call fails at runtime rather than
at compile time, and the user cannot tell that from a Unity bug.

What matters is that the split is favorable. Each of those controllers derives from a public runtime
type — `UnityEngine.Audio.AudioMixer` and `UnityEngine.Audio.AudioMixerGroup` respectively — and
that public base is what every read and write below goes through. So **everything this skill needs
in order to inspect a mixer and route audio into it is public API**:

| Operation | Route |
|---|---|
| Find the project's mixers | public — `AssetDatabase` + `UnityEngine.Audio.AudioMixer` |
| List a mixer's groups | public — `AudioMixer.FindMatchingGroups` |
| Read an Audio Source's current group | public — `AudioSource.outputAudioMixerGroup` |
| Assign a group to an Audio Source | public — `AudioSource.outputAudioMixerGroup` |
| **Create a mixer or a group, change a group volume** | **not available** — the user does this in the Audio Mixer window |

So the division of labor is: the skill inventories what exists, proposes the routing, asks the user
to add any missing groups (two clicks in a window they already have open), and then does all the
routing itself. The tedious part — walking dozens of Audio Sources and classifying them — is the
part that was worth automating anyway.

All snippets below are written for `unity command eval --code '<snippet>'`: fully qualified, no
`using` directives, returning their result rather than logging it.

## Inventory the project's mixers and their groups

```csharp
// Scope the search to Assets. Unscoped, FindAssets also walks read-only packages, so the
// inventory fills up with mixers the user did not author and cannot edit. Measured on one
// project: t:Material returned 81 unscoped against 9 under Assets, t:Shader 204 against 22.
// The only overloads are (string) and (string, string[] searchInFolders) — there is no
// SearchMode parameter.
var guids = UnityEditor.AssetDatabase.FindAssets("t:AudioMixer", new[] { "Assets" });
if (guids.Length == 0) { return "no AudioMixer assets under Assets/"; }

var rows = new System.Collections.Generic.List<string>();
foreach (var guid in guids)
{
var path = UnityEditor.AssetDatabase.GUIDToAssetPath(guid);
var mixer = UnityEditor.AssetDatabase.LoadAssetAtPath<UnityEngine.Audio.AudioMixer>(path);
var groups = mixer.FindMatchingGroups("");
var names = System.Linq.Enumerable.Select(groups, g => g.name);
rows.Add($"{path} ({groups.Length} groups): {string.Join(", ", names)}");
}
return string.Join("\n", rows);
```

`FindMatchingGroups("")` returns every group in the mixer as the public `AudioMixerGroup` type,
which is what routing needs. It returns a flat list — it does not describe the parent/child shape.
If the hierarchy matters, read it off the Audio Mixer window with the user rather than reaching for
the non-public tree API.

## Read what the scene's Audio Sources are currently routed to

```csharp
var sources = UnityEngine.Object.FindObjectsByType<UnityEngine.AudioSource>(
UnityEngine.FindObjectsInactive.Include, UnityEngine.FindObjectsSortMode.None);

var rows = new System.Collections.Generic.List<string>();
foreach (var source in sources)
{
var group = source.outputAudioMixerGroup;
rows.Add($"{source.gameObject.name}: clip={(source.clip != null ? source.clip.name : "<none>")}, "
+ $"group={(group != null ? group.name : "<none — routes to Master>")}");
}
return rows.Count == 0 ? "no Audio Sources in the open scene" : string.Join("\n", rows);
```

Inactive objects are included on purpose: a disabled Audio Source still ships with the scene and
still needs routing.

## Assign groups to Audio Sources

This is the one write this skill performs. `AudioSource.outputAudioMixerGroup` is typed as the
public `AudioMixerGroup`, and the objects returned by `FindMatchingGroups` are assignable to it, so
there is no cast and no reflection.

**Key the mapping on whichever identifier you actually classified by.** Step 2 classifies from the
**clip asset name** first and falls back to the GameObject name, so the mapping has to accept either
— keying it on GameObject name alone silently drops every source you classified by its clip.

```csharp
var mixer = UnityEditor.AssetDatabase.LoadAssetAtPath<UnityEngine.Audio.AudioMixer>(
"Assets/Audio/TheMixer.mixer");

// Keys may be a clip asset name or a GameObject name — whichever you classified from.
var assignments = new System.Collections.Generic.Dictionary<string, string> {
{ "FootStep4_Sound", "Foley" }, // clip name
{ "MenuMusic", "Music" }, // GameObject name
};

var sources = UnityEngine.Object.FindObjectsByType<UnityEngine.AudioSource>(
UnityEngine.FindObjectsInactive.Include, UnityEngine.FindObjectsSortMode.None);

var done = new System.Collections.Generic.List<string>();
var noSuchGroup = new System.Collections.Generic.List<string>();
var unassigned = new System.Collections.Generic.List<string>();

UnityEditor.Undo.IncrementCurrentGroup();
UnityEditor.Undo.SetCurrentGroupName("Route Audio Sources to mixer groups");

foreach (var source in sources)
{
var clipName = source.clip != null ? source.clip.name : null;
string wanted = null;
if (clipName != null) { assignments.TryGetValue(clipName, out wanted); }
if (wanted == null) { assignments.TryGetValue(source.gameObject.name, out wanted); }

if (wanted == null)
{
// Never skip silently — an unmatched source is a result the user has to see.
unassigned.Add($"{source.gameObject.name} (clip={clipName ?? "<none>"})");
continue;
}

var group = System.Array.Find(mixer.FindMatchingGroups(""), g => g.name == wanted);
if (group == null) { noSuchGroup.Add($"{source.gameObject.name} -> {wanted}"); continue; }

UnityEditor.Undo.RegisterCompleteObjectUndo(source, "Route Audio Source");
source.outputAudioMixerGroup = group;
UnityEditor.EditorUtility.SetDirty(source);
done.Add($"{source.gameObject.name} -> {group.name}");
}

UnityEditor.Undo.FlushUndoRecordObjects();
UnityEditor.Undo.CollapseUndoOperations(UnityEditor.Undo.GetCurrentGroup());

var report = $"routed {done.Count}: {string.Join(", ", done)}";
if (noSuchGroup.Count > 0) { report += $"\nNO SUCH GROUP (create it first): {string.Join(", ", noSuchGroup)}"; }
if (unassigned.Count > 0) { report += $"\nNOT IN THE MAPPING (unrouted): {string.Join(", ", unassigned)}"; }
return report;
```

Three things to carry through to the user:

- **The scene changed, not the mixer asset.** `outputAudioMixerGroup` lives on the Audio Source, so
the routing only persists once the scene is saved
(`UnityEditor.SceneManagement.EditorSceneManager.SaveOpenScenes()`). Say so rather than assuming.
- **Report the `NO SUCH GROUP` list explicitly.** A group the user hasn't created yet is the normal
case in this flow, not an error to swallow. Show it and ask them to add those groups.
- **Report `NOT IN THE MAPPING` too.** Those are sources your classification missed. Reporting
"routed 4" while three sources were quietly skipped is the worst outcome available here, because it
reads as success.

## Asking the user to add a group

There is no supported programmatic route, so hand over precisely rather than vaguely:

> In the Audio Mixer window (Window → Audio → Audio Mixer), select **TheMixer**, then click the
> **+** next to Groups and name the new group **SFX**. Drag it under Master if it isn't already.
> Tell me when it's there and I'll route the sources.

Then re-run the inventory snippet to confirm the group exists before routing — don't assume the
user did it, and don't assume they spelled it the way you asked.
Loading