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]
- Inspect — Confirm sample rate (48 kHz), bit depth (16-bit), and channel count (mono).
- Trim — Cut to the target duration using SoX. Apply a short fade-in and fade-out to avoid clicks.
- Normalize — Run FFmpeg's two-pass
loudnormfilter targeting EBU R128 integrated loudness. This ensures consistent perceived volume across all SFX in the game. - Re-inspect — Verify the output file properties after processing.
- 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"]
- Inspect — Same initial check as non-loop cues.
- Trim — Cut to the intended loop boundary. Fade-out is typically omitted so the tail can blend seamlessly into the head.
- Loop check —
grit_audio_loop_checkcompares the RMS energy of the first and last N samples. A large delta indicates an audible seam. - Ship raw — If the loop passes, ingest without normalization. Normalization is intentionally skipped because FFmpeg's
loudnormfilter 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:
- 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 |
Recommended MetaSounds Patch Structure¶
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:
| 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 termsgenerator—elevenlabs-sfxprompt— The exact text prompt used for generationissue— 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¶
- Grit Tooling — Audio SFX Pipeline — MCP tool parameter reference
- Asset Production Pipelines — Architecture overview and shared infrastructure
- SFX Production Workflow — Manual DAW-based SFX workflow (Studio One Pro → UE5)