Skip to main content

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 starter_blip

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. 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). Point it at a project elsewhere with -C/--project:

shipwright                         # list available sounds
shipwright starter_blip            # render one sound
shipwright all                     # render every sound
shipwright all --flac --jobs 4     # parallel render with extra FLAC files
shipwright starter_blip --play     # render and audition
shipwright --watch starter_blip    # re-render on save
shipwright -C path/to/project all  # render a project without cd-ing into it

Useful render flags:

shipwright ui_blip --out build/blip.wav --duration 0.4 --gain -3
shipwright sea_bed --stems --lufs -18
shipwright sea_bed --sr 48000 --ogg --flac --mp3
shipwright all --seed 1234 --jobs 0
shipwright --watch all

--jobs 0 uses the available CPU count. MP3 support depends on the local libsndfile build used by soundfile.

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 midi extra.
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 the freq/gain/gate convention).
  • SoundFont: install the soundfont extra, create a soundfonts/ folder in your project, drop .sf2 files in it, and use instruments.soundfont("Piano") or instruments.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"

Two environment variables override discovery as escape hatches:

  • SHIPWRIGHT_ROOT — use this directory as the project root instead of walking up for a shipwright.toml.
  • SHIPWRIGHT_SOUNDS — load sounds from here (used to run the bundled examples/).

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 ui_blip
env SHIPWRIGHT_SOUNDS=examples uv run shipwright 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


Download files

Download the file for your platform. If you're not sure which to choose, learn more about installing packages.

Source Distribution

shipwright_audio-0.2.0.tar.gz (31.7 kB view details)

Uploaded Source

Built Distribution

If you're not sure about the file name format, learn more about wheel file names.

shipwright_audio-0.2.0-py3-none-any.whl (27.1 kB view details)

Uploaded Python 3

File details

Details for the file shipwright_audio-0.2.0.tar.gz.

File metadata

  • Download URL: shipwright_audio-0.2.0.tar.gz
  • Upload date:
  • Size: 31.7 kB
  • Tags: Source
  • Uploaded using Trusted Publishing? Yes
  • Uploaded via: twine/6.1.0 CPython/3.13.12

File hashes

Hashes for shipwright_audio-0.2.0.tar.gz
Algorithm Hash digest
SHA256 e5029d6dfc0698a76c8642944c50720bd942dfa254a0385bc5ec722a850f4c4c
MD5 df16f5a339540ba4cf5fae4e44bab46d
BLAKE2b-256 44a257379392d004e6be4ddaa838fa009f78a2c6dc82a39eaa4be32a025b748a

See more details on using hashes here.

Provenance

The following attestation bundles were made for shipwright_audio-0.2.0.tar.gz:

Publisher: publish.yml on dinger086/shipwright-audio

Attestations: Values shown here reflect the state when the release was signed and may no longer be current.

File details

Details for the file shipwright_audio-0.2.0-py3-none-any.whl.

File metadata

File hashes

Hashes for shipwright_audio-0.2.0-py3-none-any.whl
Algorithm Hash digest
SHA256 27ac290bb0274615ec9f1476ca7e1d66cd6caaa5e94376c05be0e6b6591c498e
MD5 ec3709a152522798cb2c37be05da3837
BLAKE2b-256 34a67ac4d3223c612b19d28f97453ade8e1cfa98b3197d75af76abfad4e1fd21

See more details on using hashes here.

Provenance

The following attestation bundles were made for shipwright_audio-0.2.0-py3-none-any.whl:

Publisher: publish.yml on dinger086/shipwright-audio

Attestations: Values shown here reflect the state when the release was signed and may no longer be current.

Supported by

AWS Cloud computing and Security Sponsor Datadog Monitoring Depot Continuous Integration Fastly CDN Google Download Analytics Pingdom Monitoring Sentry Error logging StatusPage Status page