SFX Loom
Sound effectsBlogAPIPricingLicense
Log inStart free
SFX Loom

Sound effects for games, generated from a text prompt.

Product

ExamplesSound effectsBlogAPIPricingSound licenseCreate account

Sound effects

WeaponsMagic & spellsUI & menusFootsteps & foleyImpacts & explosionsSci-fi

Legal

Terms of servicePrivacy policyRefund policy

Contact

support@sfxloom.com

© 2026 SFX Loom

Payments are processed by Lemon Squeezy, our Merchant of Record.

Blog

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

  1. Open Tools → SFX Loom and paste your API key. It’s saved for this machine only.
  2. Write a prompt, set the duration and the number of takes.
  3. Click Generate. After 5–30 seconds the WAV files appear in Assets/Audio/Generated and 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 /generations sends the prompt and reserves the credits. If your balance is too low you get insufficient_credits in the status box and nothing is charged.
  • Poll: every 3 seconds the window asks for the generation until it’s completed or failed. 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 async continuations on the main thread, so calling Repaint() and the AssetDatabase from 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/models returns every model with its parameters and ranges.
  • Need a whole sound list at once? Use the batch generation script and point it at your Assets folder.
  • Using Godot instead? The same three requests work from an EditorPlugin with HTTPRequest; 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.

Generate game sound effects from a prompt

5 free credits · no card required

Start free

More guides

  • How to Generate Game Sound Effects with an API (Python and JavaScript)
  • Batch-Generate Every Sound Effect in Your Game with One Script
  • How to Write Prompts for AI Sound Effects (With Examples)