Repository navigation
Commands
sfx.py has 30 commands. Most print one JSON object. check, status and doc print text unless asked otherwise.
| Code | Means |
|---|---|
| 0 | The command ran. This includes flags and a BLOCKED answer. Read flags or answer; do not rely on the exit code to gate a build |
| 1 |
"ok": false: a missing file, a refused edit, an invalid list, a failed name --check
|
| 2 | Bad command line, from argparse: usage on stderr |
All output in the transcripts merges stdout and stderr.
| Command | Flags |
|---|---|
init LIST |
--project, --use {game,film,video,app,library,other}, --commercial {yes,no} (all required), --frame-rate, --style, --force
|
add LIST ID |
--event, --kind {one-shot,loop,ambience,sequence,stinger,ui}, --length (all required), --variations (1), --route (undecided), --format (wav), --category, --brief, --layers, --loudness, --sync-point, --at, --trigger, --priority, --max-instances, --spatial {2D,3D}, --gain (dB applied by place), --sync-class {tight,loose,bed}, --role {hard,background,foley,design,ui}
|
validate LIST, status LIST
|
none |
plan |
--axis name=count (repeat), --variations (3), --budget
|
plan compares three shapes before you commit to files:
$ python sfx.py plan --axis surface=6 --axis force=3 --variations 4 --budget 100
{"ok": true, "axes": {"surface": 6, "force": 3}, "variations_each": 4, "full_product_files": 72, "shared_layers_files": 36, "shared_layers_means": "one layer set per axis value, combined at play time", "parameter_files": 24, "parameter_means": "record only surface; drive the other axes with pitch, filter or volume", "ask": ["Would a player notice if this axis were removed?", "Is it heard often enough to pay for itself?", "Can a parameter do it instead of a file?"]}
$ echo $?
0
| Command | Flags |
|---|---|
synth RECIPE |
--out (required: a .wav file for one take, or a folder), --name, --takes (1), --seed (1), --sr (44100), --param name=value (repeat) |
synth list |
none; prints every recipe, its parameters and defaults, and the materials as JSON |
gen PROMPT |
--seconds (3), --takes (4), --seed (1), --out (takes), --name (take), --model, --host, --timeout (900), --workflow-out
|
library FILES |
--licence (required), --type {library,recording,generated,other}, --library, --title, --author, --url, --ai {yes,no}
|
register LIST ID FILES |
none |
gen with no server fails with exit 1 and, given --workflow-out, writes the workflow. A run against a real server is not shown here: these transcripts point SFX_COMFY_URLS at a dead port.
$ python sfx.py gen "heavy wooden crate dropped on concrete, dead room" --seconds 2 --takes 2 --out takes --name crate --workflow-out crate_workflow.json
{"ok": false, "error": "no reachable ComfyUI with an audio model", "fallback": "use `sfx.py synth` or a CC0 library, or run the workflow on any ComfyUI", "workflow": "crate_workflow.json"}
$ echo $?
1
The workflow asks for stable_audio_3_small_sfx.safetensors and adds the prompt wrapper TrackType: SFX, ... Length: 2 seconds.
| Command | Flags |
|---|---|
check [FILES] |
--list, --id, --seconds, --loop, --waive FLAGS, --json
|
set FILES |
none; at least two files |
timing FILE |
none |
degrade IN OUT |
--fault {click,dc,clip,cut-head,cut-tail,seam,noise,pad} (required), --amount, --at, --seed
|
Readings are explained on Checks and flags.
Every edit writes a new file and a new sidecar, and takes --override-licence.
| Command | Flags |
|---|---|
edit IN OUT |
--op {tidy,trim,fade,gain,peak,pitch,stretch,reverse,convolve,dc} (required), --value, --start, --end, --fade-in (2 ms), --fade-out (20 ms), --ir
|
layer FILES |
--out (required), --gains, --offsets, --align {peak,start}, --recipe {bigger,pitch,offset}
|
vary FILES |
--out (required), --name (var), --count (6), --pitch (1), --gain (1.5), --seed
|
sequence FILES |
--out (required), --count (8), --gap (0.55), --jitter (0.08), --limp (1.03), --gain (2), --breaks, --seed
|
loop IN OUT |
--crossfade (1) |
place LIST |
--out, --sr (48000) |
tone IN |
--out, --seconds, --min-seconds, --override-licence
|
pop IN |
--out, --fps, --level, --verify, --tolerance
|
record-sheet LIST |
--out, --all
|
tone finds a quiet stretch of a recording and can write a fill from it. pop adds or measures a 2 pop for sync. record-sheet writes a recording plan for sounds someone will record. layer --recipe stacks one file at several speeds or start points:
$ python sfx.py tone bed.wav --out fill.wav --seconds 10
{"ok": true, "start_s": 4.9, "seconds": 3.1, "rms_db": -19.1, "flags": ["TONE_SHORT"], "bands_low_mid_high_db": [-43.9, -27.5, -33.4], "out": "fill.wav", "fill_seconds": 10.0}
$ python sfx.py layer hit.wav --out bigger.wav --recipe bigger --gains 0,-3,-6
{"ok": true, "out": "bigger.wav", "layer_peaks_db": {"hit.wav #0": -3.1, "hit.wav #1": -3.1, "hit.wav #2": -3.1}, "flags": ["END_SILENCE"]}
$ python sfx.py record-sheet film.json --all
{"ok": true, "sounds": 1, "sheet": "# Recording sheet: p\n\nBefore the first take: stop the fridge, heating and fans, with your keys in the fridge so you restart it. Stand where the sound will be and listen. Clap, record and play back. Wear quiet clothes.\n\n48 kHz 24-bit; 96 kHz if it will be pitched far down. Watch peaks and leave at least 6 dB. Do not cut rumble now. Say the slate before each take. Vary each take: soft, hard, short, long. Record 30 s of the room with nobody moving.\n\n| Slate | Event | Layers | Length | Takes | Surface, material, distance | Done |\n|---|---|---|---|---|---|---|\n| door | slam | whole | 0.1 s | 3 | | |\n"}
The recipes are one engineer's ear each. In one blind listen of five sounds, bigger and generated parts each won once, the same as chance. Layering is an option, not a proven upgrade.
What each edit prints, on one synth creak:
$ python sfx.py edit creak.wav e_tidy.wav --op tidy
{"ok": true, "out": "e_tidy.wav", "route": "tidy", "flags": []}
$ echo $?
0
$ python sfx.py edit creak.wav e_trim.wav --op trim --start 0.1 --end 0.5
{"ok": true, "out": "e_trim.wav", "route": "trim", "flags": ["CUT_HEAD", "CUT_TAIL"]}
$ echo $?
0
$ python sfx.py edit creak.wav e_pitch.wav --op pitch --value -5
{"ok": true, "out": "e_pitch.wav", "route": "varispeed", "flags": []}
$ echo $?
0
$ python sfx.py edit creak.wav e_stretch.wav --op stretch --value 1.5
{"ok": true, "out": "e_stretch.wav", "route": "stretch", "warning": "stretch on an impact smears the attack; prefer pitch (varispeed)", "flags": ["CUT_TAIL"]}
$ echo $?
0
| Command | Flags |
|---|---|
finish IN OUT |
--dest (required), --ceiling, --format, --sr, --bits {16,24} (24), --keep-ends
|
name |
--check NAMES, --style {ucs,short}, --fx, --catid, --creator, --source, --user, --index
|
sheet FILES |
--out (required), --width (900): a waveform PNG |
review FILES |
--out (required), --gap (0.6), --match: one clip to listen to, with times |
doc LIST |
--kind {cue,impl,credits} (required), --format {md,csv}, --out
|
ship LIST |
--picked-only, --sell-as-pack
|
budget LIST |
none |
doctor |
--host |
$ python sfx.py name --fx "Crate Drop Heavy" --catid WOODImpt --creator ME --source GAME
{"ok": true, "name": "WOODImpt_Crate Drop Heavy_ME_GAME", "warning": null}
$ echo $?
0
$ python sfx.py name --fx "Crate Drop Heavy" --style short
{"ok": true, "name": "crate_drop_heavy", "warning": null}
$ echo $?
0
$ python sfx.py review s/hit_0[1-3].wav --out review.wav --match
{"ok": true, "out": "review.wav", "index": [{"at_s": 0.0, "file": "hit_01.wav"}, {"at_s": 1.0, "file": "hit_02.wav"}, {"at_s": 2.0, "file": "hit_03.wav"}]}
$ echo $?
0
Destinations for finish, with the caveat that they are file-level peak references and not delivery specs:
--dest |
Peak ceiling | Rate | Format |
|---|---|---|---|
| game-sfx, unity | -3 dB | 48 kHz | wav |
| game-foley | -12 dB | 48 kHz | wav |
| game-ambience | -14 dB | 48 kHz | ogg |
| roblox | -3 dB | 48 kHz | ogg |
| film | -1 dB | 48 kHz | wav |
| web | -3 dB | 44.1 kHz | ogg |
| library | -1 dB | 96 kHz | wav |
unity is from the repository's finish reference and was not run; the others were.
Start
Using it
- Commands
- Sound list format
- Checks and flags
- Routes and synth recipes
- Rights and shipping
- Workflows
- Errors and edge cases
The project
Elsewhere