Skip to content

Audio SFX Pipeline

Production pipeline for generating, post-processing, and delivering game-ready sound effects via grit-mcp.


End-to-End Flow

flowchart LR
    subgraph Generate["1. Generate"]
        P[Text Prompt] --> EL[ElevenLabs API]
        EL --> RAW[Raw PCM]
        RAW --> WAV[48kHz 16-bit Mono WAV]
    end

    subgraph PostProd["2. Post-Process"]
        WAV --> DEC{Loop cue?}
        DEC -->|No| TRIM[SoX Trim]
        TRIM --> NORM_NL[FFmpeg Normalize]
        NORM_NL --> DONE_NL[Game-Ready WAV]
        DEC -->|Yes| LOOP[SoX Trim to loop point]
        LOOP --> LC[Loop Check]
        LC --> DONE_L[Game-Ready Loop WAV]
    end

    subgraph Deliver["3. Deliver"]
        DONE_NL --> S3[S3 Upload + .meta.json]
        DONE_L --> S3
        S3 --> SLACK[Slack Review]
        SLACK -->|Approved| UE[UE5 MetaSounds]
    end

MCP Tool Reference

Seven MCP tools form the audio SFX pipeline:

Tool Depends On Description
grit_generate_sfx ElevenLabs API Text-to-SFX generation; returns 48 kHz / 16-bit / mono WAV.
grit_audio_inspect stdlib wave Read WAV properties (duration, sample rate, channels, bit depth).
grit_audio_trim SoX (Homebrew) Exact duration cut with configurable fade-in / fade-out.
grit_audio_normalize FFmpeg (Homebrew) EBU R128 two-pass LUFS normalization.
grit_audio_loop_check stdlib wave, struct Head/tail RMS comparison for seamless-loop verification.
grit_audio_post_process SoX + FFmpeg Chains trim + normalize + loop check in one call.
grit_ingest_audio boto3 (S3) Upload with naming convention + provenance .meta.json sidecar.

See Grit Tooling — Audio SFX Pipeline for full parameter details.

Prerequisites

Dependency Install Notes
SoX brew install sox Used by grit_audio_trim and grit_audio_post_process
FFmpeg brew install ffmpeg Used by grit_audio_normalize (EBU R128 loudnorm filter)
ElevenLabs API key Environment variable Required by grit_generate_sfx
boto3 / S3 credentials Environment variable Required by grit_ingest_audio

Post-Processing Flows

Audio post-processing follows two distinct paths depending on whether the cue is intended to loop.

Non-Loop Cues (LAUNCH, IMPACT, CANCEL)

flowchart TD
    RAW[Raw WAV from ElevenLabs] --> INSPECT[grit_audio_inspect]
    INSPECT --> TRIM["grit_audio_trim<br/>(target duration + fade in/out)"]
    TRIM --> NORM["grit_audio_normalize<br/>(EBU R128 LUFS)"]
    NORM --> VERIFY[grit_audio_inspect]
    VERIFY --> INGEST[grit_ingest_audio]
  1. Inspect — Confirm sample rate (48 kHz), bit depth (16-bit), and channel count (mono).
  2. Trim — Cut to the target duration using SoX. Apply a short fade-in and fade-out to avoid clicks.
  3. Normalize — Run FFmpeg's two-pass loudnorm filter targeting EBU R128 integrated loudness. This ensures consistent perceived volume across all SFX in the game.
  4. Re-inspect — Verify the output file properties after processing.
  5. Ingest — Upload the final WAV to S3 with provenance metadata.

One-call shortcut

grit_audio_post_process chains trim → normalize → loop check in a single call, ideal for batch workflows.

Loop Cues (AMBIENT, SUSTAIN)

flowchart TD
    RAW[Raw WAV from ElevenLabs] --> INSPECT[grit_audio_inspect]
    INSPECT --> TRIM["grit_audio_trim<br/>(trim to loop boundary)"]
    TRIM --> LC["grit_audio_loop_check<br/>(head/tail RMS delta)"]
    LC -->|Pass| INGEST[grit_ingest_audio]
    LC -->|Fail| REGEN["Adjust prompt & regenerate"]
  1. Inspect — Same initial check as non-loop cues.
  2. Trim — Cut to the intended loop boundary. Fade-out is typically omitted so the tail can blend seamlessly into the head.
  3. Loop checkgrit_audio_loop_check compares the RMS energy of the first and last N samples. A large delta indicates an audible seam.
  4. Ship raw — If the loop passes, ingest without normalization. Normalization is intentionally skipped because FFmpeg's loudnorm filter can break loop points. Runtime gain is handled by MetaSounds (see below).

Loop normalization

Do not run grit_audio_normalize on loop cues. The two-pass loudnorm filter resamples the waveform, which destroys seamless loop alignment. Ship the raw loop and control gain in MetaSounds at runtime.


Post-Processing Decision Tree

Use this tree when deciding how to process a new SFX asset:

Is this cue intended to loop?
├── NO (one-shot: LAUNCH, IMPACT, CANCEL, UI)
│   ├── Trim to target duration (grit_audio_trim)
│   ├── Normalize to EBU R128 (grit_audio_normalize)
│   └── Ingest to S3 (grit_ingest_audio)
└── YES (looping: AMBIENT, SUSTAIN)
    ├── Trim to loop boundary (grit_audio_trim, no fade-out)
    ├── Run loop check (grit_audio_loop_check)
    ├── Pass? → Ingest to S3 (skip normalize)
    └── Fail? → Adjust prompt, regenerate, re-trim

MetaSounds Integration

Once SFX assets are delivered to S3 and imported into Unreal Engine 5, they are played back through MetaSounds — UE5's node-graph audio system.

Runtime Gain Control

Loop cues are shipped without normalization. MetaSounds patches apply gain at runtime:

[Wave Player] → [Gain Node (dB)] → [Output]
  • One-shot cues arrive pre-normalized (EBU R128), so MetaSounds plays them at unity gain (0 dB) by default.
  • Loop cues arrive at raw generation levels. A Gain node in the MetaSounds patch adjusts perceived loudness to match the rest of the mix.

Cue Type Mapping

Cue Type MetaSounds Pattern Gain Strategy
LAUNCH / IMPACT / CANCEL Fire-and-forget Play Sound 2D Pre-normalized; unity gain
AMBIENT Looping Wave Player Runtime gain node; crossfade on stop
SUSTAIN Looping Wave Player with envelope Runtime gain node; release envelope on stop
UI Fire-and-forget Play Sound 2D Pre-normalized; separate UI bus attenuation
flowchart LR
    TRIG[Trigger] --> WP[Wave Player]
    WP --> GAIN[Gain dB]
    GAIN --> ATTEN[Attenuation]
    ATTEN --> OUT[Audio Output]

    LOOP_TRIG[Loop Trigger] --> LWP[Looping Wave Player]
    LWP --> LGAIN[Gain dB — compensate raw level]
    LGAIN --> XFADE[Crossfade Envelope]
    XFADE --> LATTEN[Attenuation]
    LATTEN --> OUT

S3 Naming Convention

Audio SFX assets are stored under a consistent S3 path:

grit/audio/sfx/{group}/{element}/SFX_{GROUP}_{ELEMENT}_{PHASE}.wav
Segment Example Description
{group} musical_cantrip Gameplay system or feature area
{element} fire_bolt Specific sound element
{PHASE} LAUNCH, IMPACT, LOOP Cue phase within the element

Each uploaded WAV is accompanied by a provenance sidecar (.meta.json) containing:

  • license — Rights / usage terms
  • generatorelevenlabs-sfx
  • prompt — The exact text prompt used for generation
  • issue — GitLab issue reference for traceability

Known Limitations

Limitation Impact Workaround
ElevenLabs output length Generated SFX can vary in duration; very short prompts sometimes produce silence or noise tails. Inspect output before trimming; regenerate with a more descriptive prompt if needed.
Mono output only grit_generate_sfx returns mono WAV. Stereo or spatial audio must be handled downstream. Use UE5 spatialization and attenuation settings in MetaSounds for positional audio.
Loudnorm breaks loops FFmpeg's two-pass loudnorm filter resamples audio, destroying seamless loop alignment. Skip normalization for loop cues; apply gain in MetaSounds at runtime.
SoX fade granularity SoX fade curves are limited to linear, logarithmic, and quarter-sine shapes. For complex envelopes, post-process in a DAW (Studio One Pro) before ingest.
No batch generation grit_generate_sfx processes one prompt at a time; no parallel batch API. Script sequential calls; use grit_audio_post_process to streamline per-file post-processing.
Sample rate fixed at 48 kHz ElevenLabs returns 48 kHz. Some legacy UE5 projects expect 44.1 kHz. Resample in a DAW if needed, or configure UE5 project to accept 48 kHz (recommended).

Typical Operator Workflow

A step-by-step example for producing a "fire bolt launch" SFX:

# 1. Generate
grit_generate_sfx(prompt="short punchy fire bolt launch whoosh, fantasy magic")
# → /tmp/sfx_abc123.wav

# 2. Inspect raw output
grit_audio_inspect(file_path="/tmp/sfx_abc123.wav")
# → duration: 2.4s, 48kHz, 16-bit, mono

# 3. Trim to 1.5s with 50ms fade-in, 100ms fade-out
grit_audio_trim(file_path="/tmp/sfx_abc123.wav",
                output_path="/tmp/sfx_abc123_trimmed.wav",
                duration=1.5, fade_in=0.05, fade_out=0.1)

# 4. Normalize
grit_audio_normalize(file_path="/tmp/sfx_abc123_trimmed.wav",
                     output_path="/tmp/sfx_abc123_final.wav")

# 5. Final inspect
grit_audio_inspect(file_path="/tmp/sfx_abc123_final.wav")
# → duration: 1.5s, 48kHz, 16-bit, mono, LUFS: -16.0

# 6. Ingest to S3
grit_ingest_audio(file_path="/tmp/sfx_abc123_final.wav",
                  group="musical_cantrip", element="fire_bolt", phase="LAUNCH")
# → s3://<asset-bucket>/audio/sfx/musical_cantrip/fire_bolt/SFX_MUSICAL_CANTRIP_FIRE_BOLT_LAUNCH.wav

See Also