A timeline sound-effect workbench in plain Java. It began as SynthLab, a spellcasting-audio toy, and grew into a small DAW built for game sound: synth generators and recordings on six tracks, per-clip effects, a global musical key, spectral (sines + residual) re-pitching of recordings, and a "forge" that promotes a finished sound into a key-set of files for a Minecraft mod.
The design notes behind the tuning features are in AUDIO-MAGIC-NOTES.md.
- Java 21 (or newer). The authoring GUI is
SfxLab.java, run in source-file mode; the runtime it shares with the mod (engine, live mix,RegulatorCore) is inruntime/, which./sfxlabcompiles into.build/runtime.jarwhenever a source there changes (about a second), so there is still no build step to run by hand ffmpegon the PATH for anything that is not a plain wav: mp3/ogg/flac samples, video reference, ogg export- optional: Python 3 with numpy and scipy for
tools/partials.py, the standalone prototype of the partials analysis - more cores help: clips render in parallel (bit-identical to the single-threaded path); partials analyses are cached in
forge/.parts/
git clone https://github.com/BoobooBubberson/SfxLab.git
cd SfxLab
./sfxlabThe folder holding SfxLab.java is the workspace: samples/, projects/,
forge/, renders/ and lab.cfg live inside it. Set SFXLAB_DIR to use a
different folder; running from elsewhere without it falls back to ~/synthlab.
Press O and open projects/pyretic_synth.sfx to see a real project. Its
samples are in the repo. The three files in projects/examples/ are the recipes
referenced by the notes; they need library recordings that are not tracked (see
samples/README.md), so they load with silent clips until you supply those.
Headless, without the GUI:
# render a project to wav or ogg
./sfxlab --render projects/pyretic_synth.sfx renders/pyretic.ogg --mono --normalize [--key 7]
# promote a sound or project into a tuned key-set (residual, sines, one file per key)
./sfxlab --forge projects/pyretic_synth.sfx --name pyretic --root C2
# check the family files for format problems the lenient loader would skip (exit 1 if any)
./sfxlab --check [pyretic ...]
# bake what the mod ships for the families (default: all) into forge/baked/
./sfxlab --bake [pyretic gravitic ...] [--out dir] [--quality 5]The bake writes every partials analysis the families play as a compact pair (the tracks
quantised far below hearing and deflated, .ptk, beside the residual as a mono ogg,
.res.ogg), copies the recordings their sample and choir clips play, and lists what
each family needs in manifest.txt. The eight family files come to about 27 MB of partials
and 5 MB of recordings, from 390 MB of raw analyses; rendered from the bake they measure
within 0.01 to 0.07 dB per band of the exact renders (0.7 dB where grains time-stretch a
residual). tools/bench/BenchRender --baked forge/baked renders a family from a bake.
| path | what |
|---|---|
SfxLab.java |
the lab; the doc comment at the top is the full manual and key map |
projects/ |
named .sfx projects (S stamps here, O opens from here); examples/ are the notes' recipes |
regulator/ |
one file per regulator family, <family>.sfx: the palette that plays through the tuning, then a spell <id> section per spell (recipe, lock values, binds) |
docs/ |
the Harmonic Regulator design handover and its web prototype (the spec for the machine) |
samples/ |
recordings; the sets the regulator families play are tracked (pyretic/, gravitic/, luminal/, aqueous/, cryic/, resonant/), see samples/README.md |
library.sfx |
the clip library (B banks a clip, Q browses) |
combos.txt |
SynthLab-era parameter combos (, and . browse them, I inserts) |
tools/ |
padloop.py (a struck or bowed recording into a level, seamless loop), bench/ (headless bench renders with measurements; approach.py writes an approach script from any recipe), the partials.py prototype and the sfx_batch.sh batch cleaner for raw recordings |
legacy/ |
SynthLab.java, the original spellcasting playground, and its presets |
lab.cfg.example |
template for the git-ignored lab.cfg |
forge/, renders/, video-cache/, samples/.decoded/ |
outputs and caches, git-ignored. forge/.parts/ (analyses) and samples/.decoded/ (decoded copies) are rebuilt on demand and can be emptied at any time |
bench.sfx, regulator/.working/ |
the bench's autosaves (git-ignored): a scratch bench, and each family's unsaved edits |
The header comment in SfxLab.java documents every generator, parameter and
key. The short version:
- 1..9, 0 drop a palette clip at the playhead; W imports a recording; A docks the sample browser
- SPACE play, ENTER rewind, L loop, P solo-preview the selected clip
- drag clips to move or retrack, drag the right edge to resize, the left edge to trim, X splits at the playhead
- C cycles a sample clip through choir and partials; R pins a recording's pitch to the root, < > transpose the whole project
- K drops a marker; V attaches a reference video, M opens its monitor
- B / Q bank a clip to the library and browse it; , . browse combos
- S stamps the timeline into
projects/<name>.sfx(the workspace itself autosaves toproject.sfx); O opens one; N clears - E exports the mix (wav or ogg, mono, normalize, trim); F opens the forge
- ctrl+Z / ctrl+Y undo and redo; G toggles snap
- H switches to the bench, J docks the regulator panel (below)
The timeline crafts one-shots. The Harmonic Regulator (docs/HARMONIC-REGULATOR.md)
How the families were authored (notes from oscillators and recordings as texture, the
register plan, chord-tone voices, pacing, the measurement loop): docs/REGULATOR-FAMILIES.md.
needs something else: a palette of layers that all sound at once and are steered by
the machine's signals rather than by time. H swaps the timeline for that bench.
- Every layer is a clip with no position. Endless layers loop for ever while the bench plays (SPACE); a layer marked one-shot (right-click its row) fires on the lock or unlock event instead. Rows have mute / solo boxes and a level bar.
- Add layers with the palette keys, W import, the sample browser (A), or shift+H from a selected timeline clip. DEL, D, C, T, R work on layers.
- Right-click a slider to mark the range that sounded good, with a note, or to
bind a signal to it. A bind sweeps linearly unless its map says otherwise:
steps=3turns the sweep into a staircase,scale=penta(any chord name the cloud knows) snaps a pitch bind to the nearest degree, so a signal can step through a scale or through octaves instead of gliding. Marked ranges are the default bind ranges, so the listening notes become the machine's data. Bound params show a dot and a white tick at the live value. - J docks the regulator panel: a slider per signal (the scrubber), a score
slider, the family picker, lock / unlock buttons and the bind tables. Every
bind and every signal has an on box: off keeps the row but holds it still, so a
signal's effect is compared live instead of deleted and retyped (
offon the bind line). A filter box narrows the tables by signal, layer or param, or to the selected layer; column headers sort; copy / paste… (ctrl+C / ctrl+V) carry bind lines between tables, onto another layer, or into another family. The signal sliders and the spell rows fold so the tables get the height. The binds box switches every bind and every spell blend off so the whole bench plays as saved. While the machine window is open, powered and driving, the sliders follow it and are greyed out; P then solos a layer live through its binds. Cut the machine's power, close it, or untick drive and the signals go back to the panel's own values, and P plays the layer exactly as authored. - Sounds are organised by family, one file each:
regulator/<family>.sfxis the palette that plays through the tuning, followed by aspell <id>section per spell, the roster. Pick the family in the panel. Edits autosave to the family's working copy (regulator/.working/<family>.sfx), which reopens with it, so unsaved work survives switching families and restarts; S saves the family file, shift+O reverts to it (the header marks a family with unsaved edits*). Each spell is a signature, the same layers at their lock values plus its one-shots, and carries its recipe (recipe tier=1 X3p1r0.7 Y2p0: axis, integer ratio, phase in quarter turns, reach target;rtol=sets the reach tolerance), which is what the machine and, later, the mod score. Edit it with the spell'srecipe…button in the panel, or design the sigil on the machine's arms and press→ recipeto write it to the pinned spell; thenew spellbutton asks for an id and a recipe, prefilled from the machine. Both warn when a sigil retraces itself into an open line. - Every spell blends in by its own score signal (
score.<id>, a slider per spell in the panel, with lock and unlock buttons), above 0.55. The panel stacks a bind table per spell under the palette's: the palette's binds shape the searching mix, a spell's binds move its own target values, blended in with it, and a bind on the reserved layerspell(bind score.firebolt spell blend 0.55 1) sets when and how sharply the spell comes in. Two spells scoring at once share the blend, so overlapping recipes are heard while authoring rather than discovered in the game. Pinning a spell in the machine only changes the blueprint. - The bench view lists the palette's layers and then every spell of the family as a foldable group of its own layers: each row shows the spell's target level with a white tick where the blend sits right now, and selecting a row edits that spell's target values in the panel (undo covers it). A palette row's menu pushes the layer into a spell at its current values; a spell header's menu deletes the spell.
- A bench with no family is a scratch bench that autosaves to
bench.sfx; S saves it as a family. shift+S forks the loaded family under a new name. N starts a fresh scratch bench and leaves the family on disk.regulator/pyretic.sfxis the worked example. - U opens the machine: the regulator as the game plays it (
sfxlab.runtime.ConductedMachine, the same class the mod runs), worked on the stage with the mouse the way the game's three casts work it. Hold the left button for crescendo and the right for diminuendo on what the pointer is over when the button goes down; the middle button, or T, is the toggle. On the aiming arm's crystal the two verbs set the reach of the component in hand; on the composite figure they wind the aim (the ratio) round the ring of stations, and let go inside a station's window the crystal takes it (theon=acceptchime hook); on the component in hand they turn its phase. The toggle on the composite plants the component, on a planted one takes it back, and on the seated crystal passes the aim to the next arm; crescendo into the seated crystal while a sigil holds voices it. With "drive the bench" on and a crystal seated, the machine's signals replace the panel's sliders and every spell's match fires that spell's one-shots, so the puzzle is played and the palette answers. Auto-play conducts toward the pinned spell with the same casts on the game's own curves, with optional meanders and an "any spell" mode that keeps chaining locks: freeze it when the sound goes wrong, adjust the layers, resume. The period slider sets the tempo of the figure (3.5 s, as the game has it).
RegulatorCore is the machine with no Swing or Minecraft in it: crank physics, the
lever state machine, figure sampling, recipe matching with shape equivalence, the
setpoint and copy socket, and the signal contract. It lives in the shared runtime,
runtime/sfxlab/runtime/, beside the engine (Engine, Voice, Clip), the
partials model, the .sfx format (SfxFormat) and the live mix (BenchMixer: signals,
binds and the spell blend). None of it touches Swing, the file system or ffmpeg (hosts
install a Samples.loader and optionally a Partials.source), so the mod builds the
same sources. The web prototype in docs/ is the core's test oracle.
.sfx files are plain text, one clip per line, so they diff and merge like
code and clip lines can be copied between projects and the library by hand.
tests/run.sh # about 50 s; exits non-zero on any failureCoreTest:RegulatorCoreon its own: shape matching, all three crank models (brake & wells, the free crank, the classic catching crank), levers and coupling, events, signals, the pen and the blueprint.ConductedTest:ConductedMachineon its own: the two verbs' curves, what moves the aim and what does not, the ends of the arc, and every default recipe built by the casts alone.GuiCheck: the regulator panel (binds, mutes, filter, undo) and the machine window conducted to a lock and voicing the crystal, by hand and by its auto-player for every spell of the family, headless.golden.sh: 14 reference renders (every timeline project at key 0 and +7, a mono export, and a scripted machine approach per family) that must stay bit-identical. After a change meant to alter the sound, listen to the new renders in.build/test/renders/and accept them withtests/run.sh --bless. They are a guard for refactors and nothing more: a red result after a deliberate change is expected.
The suite renders frozen copies of the family and project files in tests/fixtures/, so authoring never turns it
red; tests/fixtures.sh refreshes them from the last commit when you want the references to follow. The hashes are
this machine's: they depend on the local ffmpeg and on samples/minecraft_sounds_assets/, which git does not track.
Recordings go in a subfolder of samples/ with a row in samples/CREDITS.md
naming the author, license and original title. Only files that are clear to
redistribute are committed; everything else stays local under the git-ignore
rules.
The code and the in-lab synthesis are MIT (see LICENSE). Downloaded
recordings keep their own terms, listed per file in samples/CREDITS.md.