A code-first audio studio: write -> render -> listen -> adjust.
Project description
shipwright-audio
A code-first audio studio for game and app sound. Define sounds in small Python functions, run one command, and get rendered audio files.
Requires Python 3.10 through 3.12.
Install
As a tool:
uv tool install shipwright-audio
# or
pipx install shipwright-audio
From a checkout:
uv sync
uv run shipwright --version
Quick Start
Create a project with one runnable starter sound:
shipwright init my_game_audio
cd my_game_audio
shipwright build
Generated layout:
my_game_audio/
shipwright.toml
.gitignore
sounds/
starter_blip.py
output/
.gitkeep
Use shipwright init . to initialize the current directory. Existing generated
files are not overwritten unless you pass --force.
CLI
shipwright.toml marks the project root and describes how the project builds.
shipwright walks up from the current directory to find it, loads the Python
files in sounds/, and writes renders to output/ (both relative to that root,
so you can run it from a subdirectory). The three commands are:
shipwright init NAME # scaffold a new project (use '.' for the current dir)
shipwright build # render the project per [build] in shipwright.toml
shipwright list # list the sounds available in the project
shipwright build is the main command. With no arguments it renders the targets
named in [build] (default: every sound) using each sound's configured formats.
Name specific sounds to build just those, and point at a project elsewhere with
-C/--project:
shipwright build # build everything per the toml
shipwright build starter_blip # build one sound
shipwright build a b c # build a subset
shipwright build --watch # re-render on every save
shipwright build -C path/to/project # build a project without cd-ing in
The [build] table is the source of truth; CLI flags are one-off overrides on
top of it:
shipwright build starter_blip --out dist/blip.wav --duration 0.4 --gain -3
shipwright build sea_bed --stems --lufs -18
shipwright build sea_bed --sr 48000 --ogg --flac --mp3
shipwright build --seed 1234 --jobs 0
shipwright build starter_blip --play
--jobs 0 uses the available CPU count. MP3 support depends on the local
libsndfile build used by soundfile; a format that can't be written is skipped
with a warning while the rest of the build continues (WAV always must succeed).
Write Sounds
Each sound is a function decorated with @sound("name"). It returns either:
Buffer: raw NumPy samples for direct SFX synthesis.RenderSpec: MIDI tracks, audio tracks, sends/returns, and master FX.
Synthesized SFX
from shipwright import Buffer, dsp, sound
@sound("zap")
def zap():
sig = dsp.saw(220, 0.25)
sig = dsp.ad_env(sig, attack=0.001, release=0.2)
sig = dsp.lowpass(sig, 1200)
sig = dsp.normalize(sig, 0.9)
return Buffer(dsp.to_stereo(sig))
Sample-Based Audio
Put WAV/AIFF/FLAC files in your project and place them with AudioClip.
Audio tracks use the same mixer controls as MIDI tracks: gain_db, pan,
Faust fx, sends/returns, sidechain, and stems.
from shipwright import AudioClip, AudioTrack, RenderSpec, ReturnBus, Send, sound
from shipwright import instruments
@sound("hit_with_space")
def hit_with_space():
hit = AudioTrack(
clips=[
AudioClip("assets/hit.wav", start=0.0),
AudioClip("assets/hit.wav", start=0.35, gain_db=-8),
],
gain_db=-3,
pan=-0.2,
sends=[Send("verb", -12)],
name="hit",
)
return RenderSpec(
tracks=[hit],
returns=[ReturnBus("verb", fx=[instruments.reverb(0.35)])],
)
AudioClip.start and AudioClip.dur use the spec's time_unit; offset is
always seconds into the source file. Relative paths resolve from the project
root.
MIDI / Instrument Tracks
from shipwright import RenderSpec, Track, compose, instruments, sound
@sound("loop")
def loop():
bpm = 96
chords = compose.progression(
["Dm9", "Bbadd9", "F/C", "C7sus4"],
bpm=bpm,
beats_per_chord=4,
timing="beats",
)
pad = Track(instruments.soft_pad(), chords, gain_db=-8, pan=-0.2)
return RenderSpec(
tracks=[pad],
tempo=bpm,
time_unit="beats",
master_fx=[instruments.reverb(0.3)],
)
See examples/music_sea_bed.py and
examples/sfx_ui_blip.py for complete examples.
Composition
shipwright.compose includes lightweight theory helpers:
- Chords:
dim,aug,6,9/11/13,add9, suspended chords, slash chords. - Scales and keys, including quantize-to-scale.
- Swing and humanization.
- Time signatures and beat/second conversion.
- Microtonal tuning (any n-EDO or just-intonation ratio set).
- Optional MIDI import/export through the
midiextra.
meter = (3, 4)
start = compose.bar_start(2, meter)
notes = compose.melody([60, 62, 63, 67], bpm=120, start_beat=start, timing="beats")
notes = compose.apply_groove(notes, swing=0.25, time_signature=meter, timing="beats")
notes = compose.read_midi("riff.mid")
compose.write_midi("out.mid", notes, bpm=120)
Microtonal
Note.pitch is a float (fractional MIDI = microtonal), and the numpy
@instrument path renders any frequency, so microtonality works end to end
without MIDI's integer limit. (Faust / SoundFont / VST instruments still go
through integer MIDI note-on, so use a numpy instrument for microtonal timbres.)
A tuning is a list of cents-above-the-root; edo() and just() build common
ones, tuned_scale() turns one into pitches, and quantize_tuning() snaps to it.
notes = compose.melody([60, 60.5, 61, 61.5], bpm=96, timing="beats") # quarter-tones
quarter = compose.edo(24) # 24 equal divisions of the octave
pitches = compose.tuned_scale(quarter, root=60, octaves=2)
snapped = compose.quantize_tuning_notes(notes, quarter, root=60)
ji = compose.just([1, 9/8, 5/4, 4/3, 3/2, 5/3, 15/8]) # just-intonation major
notes = compose.quantize_tuning_notes(notes, ji, root=60)
Instruments
Built-in Faust instruments need no external files:
instruments.pluck()
instruments.saw_lead()
instruments.soft_pad()
instruments.sub_bass()
Custom Instruments
The simplest way to write your own instrument is the @instrument decorator: a
per-note (freq, dur, vel) -> samples function built from the same dsp
vocabulary you use for SFX. It is called once per note and the voices are summed,
so polyphony, chords, and overlapping notes just work. The result drops onto a
Track like any built-in.
from shipwright import Track, RenderSpec, compose, dsp, instrument, sound
@instrument("organ")
def organ(freq, dur, vel):
sig = dsp.saw(freq, dur, amp=vel / 127)
sig = dsp.ad_env(sig, attack=0.01, release=0.25)
return dsp.lowpass(sig, 3000)
@sound("riff")
def riff():
notes = compose.melody([62, 65, 69, 65], bpm=96, timing="beats")
return RenderSpec([Track(organ, notes)], tempo=96, time_unit="beats")
Define instruments inline next to a sound, or in a shared module (e.g.
my_instruments.py) you import — the project root is on the import path.
For instruments backed by external files or DSP languages:
- Faust: build one with
Instrument.faust("name", dsp_string, voices)(see the built-ins for thefreq/gain/gateconvention). - SoundFont: install the
soundfontextra, create asoundfonts/folder in your project, drop.sf2files in it, and useinstruments.soundfont("Piano")orinstruments.soundfont("piano", "/path/to/file.sf2", preset=0). - VST/AU: use
instruments.plugin("name", "/path/to/plugin.vst3").
Mixing And Export
Tracks support gain_db, pan, per-track Faust fx, sends, sidechain ducking,
and stem export.
from shipwright import ReturnBus, Send, Track
pad = Track(
instruments.soft_pad(),
notes,
gain_db=-8,
pan=-0.2,
sends=[Send("verb", -14)],
)
return RenderSpec(
tracks=[pad],
returns=[ReturnBus("verb", fx=[instruments.reverb(0.4)], gain_db=-3)],
)
Exports are peak-limited, can normalize to a LUFS target with --lufs, and
dither 16-bit output by default.
Effects
Track fx, return-bus fx, and master_fx take Faust strings (streaming,
time-domain) — instruments.reverb() is one. For frequency-space / FFT work the
streaming graph can't do, write a numpy effect with @effect: an
(audio, sr) -> audio function that runs offline on the rendered stereo audio.
Drop it into a track's fx or master_fx next to Faust strings.
import numpy as np
from shipwright import Track, effect
@effect
def lowpass_blur(audio, sr):
spec = np.fft.rfft(audio, axis=0)
freqs = np.fft.rfftfreq(len(audio), 1 / sr)
spec[freqs > 2000] *= 0.2
return np.fft.irfft(spec, n=len(audio), axis=0)
pad = Track(organ, notes, fx=[lowpass_blur])
The dsp module ships ready-made frequency-space effects you can call from a
numpy @effect or directly in a Buffer SFX: dsp.spectral_gate,
dsp.spectral_filter, and dsp.convolve_reverb.
Because a numpy effect needs the whole signal, it runs after a track's Faust
chain (not interleaved per-sample), and it isn't supported on return buses — use
Faust there, or apply the effect on a track or in master_fx.
Project Config
A project is configured through its shipwright.toml:
[shipwright]
sr = 48000
block = 512
master_ceiling = 0.97
target_lufs = -18
export_subtype = "PCM_16"
dither = true
sounds_dir = "sounds"
soundfont_dir = "soundfonts"
output_dir = "output"
The [build] table describes what shipwright build produces. The keys under
[build] are the defaults for every sound; a [build.<name>] table overrides
them for one sound, and CLI flags override both:
[build]
targets = ["all"] # which sounds to build ("all" = every sound)
formats = ["wav"] # output formats: wav, ogg, flac, mp3
# lufs = -18 # normalize to integrated LUFS before export
# gain = 0.0 # post-render gain in dB
# stems = false # write per-track WAV stems (RenderSpec sounds)
# seed = 1234 # seed numpy and shipwright noise
# jobs = 0 # parallel jobs (0 = CPU count, 1 = serial)
[build.sea_bed] # per-sound overrides
formats = ["wav", "flac"]
duration = 30
stems = true
lufs = -14
Two environment variables override discovery as escape hatches:
SHIPWRIGHT_ROOT— use this directory as the project root instead of walking up for ashipwright.toml.SHIPWRIGHT_SOUNDS— load sounds from here (used to run the bundledexamples/).
Architecture
| Layer | File | Job |
|---|---|---|
| Composition | shipwright/compose.py |
chord symbols / pitch lists to Notes |
| Sound sources | shipwright/instruments.py |
Faust, SoundFont, and plugin instruments |
| SFX synthesis | shipwright/dsp.py |
NumPy oscillators, noise, envelopes, filters |
| Engine | shipwright/engine.py |
DawDreamer graph, mix, FX, offline render |
DawDreamer hosts plugins and renders deterministically offline. It does not compose and ships no instruments, so Shipwright keeps composition and sound source helpers above the engine.
Development
uv run --extra dev pytest
env SHIPWRIGHT_SOUNDS=examples uv run shipwright build ui_blip
env SHIPWRIGHT_SOUNDS=examples uv run shipwright build sea_bed
License
shipwright-audio is MIT licensed. Its DawDreamer dependency is GPLv3; review
that license before redistributing bundled applications or generated tooling.
Project details
Release history Release notifications | RSS feed
Download files
Download the file for your platform. If you're not sure which to choose, learn more about installing packages.
Source Distribution
Built Distribution
Filter files by name, interpreter, ABI, and platform.
If you're not sure about the file name format, learn more about wheel file names.
Copy a direct link to the current filters
File details
Details for the file shipwright_audio-0.2.1.tar.gz.
File metadata
- Download URL: shipwright_audio-0.2.1.tar.gz
- Upload date:
- Size: 35.7 kB
- Tags: Source
- Uploaded using Trusted Publishing? Yes
- Uploaded via: twine/6.1.0 CPython/3.13.12
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
0b8d9d1f0e0b00ee4f653911293c6e2b24b19f8f7e43152854f4ebbd9cdbceb9
|
|
| MD5 |
897928831a746e3633ce07d7e4554295
|
|
| BLAKE2b-256 |
abd027dc8d8f9377dbbc9d67b46645d9a0f1d46a2b5637f6b852a663124372a5
|
Provenance
The following attestation bundles were made for shipwright_audio-0.2.1.tar.gz:
Publisher:
publish.yml on dinger086/shipwright-audio
-
Statement:
-
Statement type:
https://in-toto.io/Statement/v1 -
Predicate type:
https://docs.pypi.org/attestations/publish/v1 -
Subject name:
shipwright_audio-0.2.1.tar.gz -
Subject digest:
0b8d9d1f0e0b00ee4f653911293c6e2b24b19f8f7e43152854f4ebbd9cdbceb9 - Sigstore transparency entry: 2002343044
- Sigstore integration time:
-
Permalink:
dinger086/shipwright-audio@9df79e58d3abc4eb19d234c4a4bb839dd742a1b8 -
Branch / Tag:
refs/tags/v0.2.1 - Owner: https://github.com/dinger086
-
Access:
public
-
Token Issuer:
https://token.actions.githubusercontent.com -
Runner Environment:
github-hosted -
Publication workflow:
publish.yml@9df79e58d3abc4eb19d234c4a4bb839dd742a1b8 -
Trigger Event:
release
-
Statement type:
File details
Details for the file shipwright_audio-0.2.1-py3-none-any.whl.
File metadata
- Download URL: shipwright_audio-0.2.1-py3-none-any.whl
- Upload date:
- Size: 29.4 kB
- Tags: Python 3
- Uploaded using Trusted Publishing? Yes
- Uploaded via: twine/6.1.0 CPython/3.13.12
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
aff4b9bb305f5c7c4085ce8376e3c99eb8be966c27d69e1ddf5ed0daa00c6caf
|
|
| MD5 |
a1bca93c78210861d231f88b9e5a48ed
|
|
| BLAKE2b-256 |
d9bf4554599447dc82ed535e98cc467110c8c11c9f508b9257fa654067953df5
|
Provenance
The following attestation bundles were made for shipwright_audio-0.2.1-py3-none-any.whl:
Publisher:
publish.yml on dinger086/shipwright-audio
-
Statement:
-
Statement type:
https://in-toto.io/Statement/v1 -
Predicate type:
https://docs.pypi.org/attestations/publish/v1 -
Subject name:
shipwright_audio-0.2.1-py3-none-any.whl -
Subject digest:
aff4b9bb305f5c7c4085ce8376e3c99eb8be966c27d69e1ddf5ed0daa00c6caf - Sigstore transparency entry: 2002343123
- Sigstore integration time:
-
Permalink:
dinger086/shipwright-audio@9df79e58d3abc4eb19d234c4a4bb839dd742a1b8 -
Branch / Tag:
refs/tags/v0.2.1 - Owner: https://github.com/dinger086
-
Access:
public
-
Token Issuer:
https://token.actions.githubusercontent.com -
Runner Environment:
github-hosted -
Publication workflow:
publish.yml@9df79e58d3abc4eb19d234c4a4bb839dd742a1b8 -
Trigger Event:
release
-
Statement type: