Generate Sound Effects Inside the Unity Editor with AI
November 4, 2026 · 7 min read
Switching between the browser, your downloads folder and the Unity Project window breaks your flow every time you need a new sound. In this guide we build a small editor window: type a prompt, pick a duration, click Generate, and the WAV files appear in your project, already imported. It’s one C# file, about 130 lines, using the SFX Loom API.
Before you start
- Unity 2021.3 LTS or newer (any render pipeline).
- An API key: sign in to SFX Loom, open API and click Create key. New accounts get free credits to try it.
Keep the key out of your game. The script below lives in an Editor folder, so Unity never includes it in a build, and the key is stored in EditorPrefs on your machine, not in the project. Don’t commit keys to version control and never call the API from runtime code: anyone can read strings out of a shipped game.
1. Create the script
Create Assets/Editor/SfxLoomWindow.cs (make the Editor folder if you don’t have one) and paste:
// Assets/Editor/SfxLoomWindow.cs: editor-only, never included in a build.
using System;
using System.IO;
using System.Text;
using System.Text.RegularExpressions;
using System.Threading.Tasks;
using UnityEditor;
using UnityEngine;
using UnityEngine.Networking;
public class SfxLoomWindow : EditorWindow
{
const string Api = "https://sfxloom.com/api/v1";
const string Model = "elevenlabs/sound-effects-v2";
const string KeyPref = "SfxLoom.ApiKey";
const string OutDir = "Assets/Audio/Generated";
string prompt = "Soft UI button click, short and crisp, modern mobile game menu, no reverb, no music";
float duration = 1f;
int variations = 2;
string status = "";
bool busy;
[Serializable] class Params { public float duration; }
[Serializable] class CreateBody { public string model; public string prompt; public Params @params; public int variations; }
[Serializable] class Take { public string status; public string sound_id; }
[Serializable] class Generation { public string id; public string status; public Take[] variations; }
[Serializable] class ErrorDetail { public string code; public string message; }
[Serializable] class ErrorBody { public ErrorDetail error; }
[MenuItem("Tools/SFX Loom")]
static void Open() => GetWindow<SfxLoomWindow>("SFX Loom");
void OnGUI()
{
var key = EditorPrefs.GetString(KeyPref, "");
var newKey = EditorGUILayout.PasswordField("API key", key);
if (newKey != key) EditorPrefs.SetString(KeyPref, newKey);
EditorGUILayout.LabelField("Prompt");
prompt = EditorGUILayout.TextArea(prompt, GUILayout.MinHeight(60));
// The API only accepts durations on the model's 0.5 s grid.
duration = Mathf.Round(EditorGUILayout.Slider("Duration (s)", duration, 0.5f, 22f) * 2f) / 2f;
variations = EditorGUILayout.IntSlider("Variations", variations, 1, 4);
using (new EditorGUI.DisabledScope(busy || string.IsNullOrEmpty(newKey)))
{
if (GUILayout.Button(busy ? "Generating..." : "Generate", GUILayout.Height(28)))
_ = Generate(newKey);
}
if (!string.IsNullOrEmpty(status)) EditorGUILayout.HelpBox(status, MessageType.None);
}
async Task Generate(string key)
{
busy = true;
try
{
SetStatus("Starting...");
var body = JsonUtility.ToJson(new CreateBody
{
model = Model,
prompt = prompt,
@params = new Params { duration = duration },
variations = variations,
});
Generation gen;
using (var create = new UnityWebRequest(Api + "/generations", "POST"))
{
create.uploadHandler = new UploadHandlerRaw(Encoding.UTF8.GetBytes(body));
create.downloadHandler = new DownloadHandlerBuffer();
create.SetRequestHeader("Content-Type", "application/json");
await Send(create, key);
gen = JsonUtility.FromJson<Generation>(create.downloadHandler.text);
}
while (gen.status == "queued" || gen.status == "processing")
{
SetStatus("Generating...");
await Task.Delay(3000);
using var poll = UnityWebRequest.Get($"{Api}/generations/{gen.id}");
await Send(poll, key);
gen = JsonUtility.FromJson<Generation>(poll.downloadHandler.text);
}
Directory.CreateDirectory(OutDir);
var saved = 0;
foreach (var take in gen.variations)
{
if (take.status != "completed") continue;
// This model stores MP3, so ?format=wav is converted on the server and streamed back.
using var download = UnityWebRequest.Get($"{Api}/sounds/{take.sound_id}/download?format=wav");
await Send(download, key);
var path = $"{OutDir}/{FileName(prompt)}_{take.sound_id.Substring(0, 6)}.wav";
File.WriteAllBytes(path, download.downloadHandler.data);
AssetDatabase.ImportAsset(path);
saved++;
}
SetStatus(saved > 0 ? $"Saved {saved} sound(s) to {OutDir}" : "Generation failed. Credits were refunded.");
if (saved > 0) EditorGUIUtility.PingObject(AssetDatabase.LoadAssetAtPath<UnityEngine.Object>(OutDir));
}
catch (Exception e)
{
SetStatus(e.Message);
}
finally
{
busy = false;
Repaint();
}
}
static async Task Send(UnityWebRequest req, string key)
{
req.SetRequestHeader("Authorization", "Bearer " + key);
var op = req.SendWebRequest();
while (!op.isDone) await Task.Delay(50);
if (req.result == UnityWebRequest.Result.Success) return;
var message = req.error;
try
{
var err = JsonUtility.FromJson<ErrorBody>(req.downloadHandler.text);
if (!string.IsNullOrEmpty(err?.error?.code)) message = $"{err.error.code}: {err.error.message}";
}
catch { /* not a JSON error body */ }
throw new Exception(message);
}
static string FileName(string text)
{
var words = Regex.Replace(text.ToLowerInvariant(), "[^a-z0-9]+", " ").Trim().Split(' ');
return string.Join("_", words, 0, Math.Min(4, words.Length));
}
void SetStatus(string text)
{
status = text;
Repaint();
}
}2. Use it
- Open Tools → SFX Loom and paste your API key. It’s saved for this machine only.
- Write a prompt, set the duration and the number of takes.
- Click Generate. After 5–30 seconds the WAV files appear in
Assets/Audio/Generatedand the folder is highlighted in the Project window.
Files are named after the first words of the prompt plus a short id, for example soft_ui_button_click_b7Rf2k.wav. Rename them to fit your naming scheme and move them where they belong. Every sound also lands in your SFX Loom library, so you can trim a take on the waveform later and download it again.
How it works
- Start:
POST /generationssends the prompt and reserves the credits. If your balance is too low you getinsufficient_creditsin the status box and nothing is charged. - Poll: every 3 seconds the window asks for the generation until it’s
completedorfailed. Failed takes are refunded automatically. - Download: each finished take is fetched as 16-bit WAV, written into the project and imported with
AssetDatabase.ImportAsset. - Async without coroutines: editor code runs
asynccontinuations on the main thread, so callingRepaint()and theAssetDatabasefrom them is safe.
Make it yours
- Set import settings on the new clips: Decompress On Load for short effects, Streaming for long ambience. The Unity import guide covers each setting.
- Add a model dropdown.
GET /api/v1/modelsreturns every model with its parameters and ranges. - Need a whole sound list at once? Use the batch generation script and point it at your
Assetsfolder. - Using Godot instead? The same three requests work from an
EditorPluginwithHTTPRequest; see the API tutorial for the flow.
Better prompts mean fewer retries: the prompt writing guide shows what to describe for every type of sound.
More guides