Skip to main content

Wavnote

A Python CLI for making multitrack waveform illustrations as SVG or PNG. Describe speech, timed text, sound effects, and background noise in a YAML or JSON file; Wavnote draws synchronized channels in a dark or light DAW interface, or a minimal layout. Useful for printed evidence, fictional recordings, and props.

Runs offline. No DAW, audio device, browser, font download, or external speech service is required.

Proudly sponsored by CaseClosed

CaseClosed

Proudly sponsored by CaseClosed in Norway and Caseclosed Mysteries internationally, creators of printed murder mystery games.

Get started

You'll need Python 3.11 or newer and uv. Run it without a permanent installation:

uvx wavnote init -o session.yaml
uvx wavnote render session.yaml -o recording.svg
uvx --from 'wavnote[png]' wavnote render session.yaml -o recording.png --dpi 300

uvx installs the tool's dependencies in its own isolated environment. For PNG exports, use --from 'wavnote[png]' to include the rasterizer. For a permanent installation with both formats, use uv tool install 'wavnote[png]'.

To run from a source checkout, use uvx --from . wavnote ... for SVG, or uvx --from '.[png]' wavnote ... for PNG. No uv sync step is needed to run the CLI this way. Pin a published version with uvx --from 'wavnote[png]==0.1.0' wavnote ....

init writes a starter file and refuses to overwrite an existing file. Edit that file to change the channels, speech, effects, volumes, and timing.

A few examples

These are actual generator outputs. The three themes use the same session.yaml; only the presentation changes. SVGs are included alongside the PNG previews so you can inspect or print them.

DAW dark

A dark audio-editor interface with transport controls, channel panels, a shared clock ruler, and optional timed annotations.

Open SVG

Dark DAW waveform example

DAW light

The same audio-editor interface in a light theme.

Open SVG

Light DAW waveform example

Minimal

Waveforms, channel labels, ruler, and annotations, suitable for placing inside an existing document.

Open SVG

Minimal waveform example

Recording gap

Unavailable data is visibly different from recorded silence. It keeps its place on the common timeline.

Session · SVG

Recording gap example

Wrapped timeline

Each channel wraps into two consecutive lines on the same page: K1, K1, K2, K2, K3, K3. Matching lines retain identical time ranges across every channel. This portrait example also uses a custom application title.

Session · SVG

Wrapped multitrack example

Recreate the previews:

uvx --from '.[png]' --with PyYAML python examples/render_examples.py

The landscape previews are 1400 pixels wide; the wrapped portrait preview is 1100 pixels wide. SVGs retain their A4 physical sizes; default landscape PNG exports at 300 DPI are 3508 × 2480 pixels. No fonts are bundled. These checked-in examples use the installed Menlo interface font; your local system fallback may look different.

Session files

YAML and JSON use the same versioned schema. Here is a small complete session:

schema_version: 1
session:
  title: recording_01
  start: "2026-01-01T22:32:00+01:00"
  duration: 15
  sample_rate: 48000
  seed: 42
render:
  mode: daw_dark
  title: Wavnote
  page: a4-landscape
  timestamps: clock
  annotations: words
voices:
  voice_a:
    delivery: quiet
    gain_db: -2
    pitch_hz: 180
    breathiness: 0.2
channels:
  - id: K1
    ambience: {preset: room-tone, level_dbfs: -45}
    events:
      - id: phrase_01
        type: speech
        voice: voice_a
        at: 2
        duration: 4
        words:
          - {text: "Wait", start: 2, end: 2.5}
          - {text: "here.", start: 3, end: 3.8}
  - id: K2
    events:
      - id: knock_01
        type: sfx
        preset: hard-knock
        at: 9.5
        duration: 0.3
        gain_db: -6

All at, start, and end values are elapsed seconds from the session start, including words. The datetime needs a timezone offset. Clock labels are computed from that start and work across midnight. Source recordings and fonts in a session file resolve relative to that file.

Give events stable IDs: their variation is seeded by the session seed and event ID. Changing a style, page size, or crop does not regenerate the signal. If you omit IDs, Wavnote creates them from the channel and event position; inserting an earlier event then changes those automatically assigned IDs.

Unknown keys, invalid intervals, unknown voices/effects, duplicate source event IDs, and events overlapping recording gaps are rejected with validation errors. Events may overlap each other, including simultaneous speech.

Timed text below the waveform

There is no forced alignment or speech recognition. Annotations are authored text positioned immediately below their channel's waveform.

For precise control, supply words with explicit start/end values. A word entry can also contain a short phrase. Its anchor is the supplied start and a small line shows its duration. The speech-shaped waveform follows those intervals, preserving your pauses.

For quick authoring, use text instead of words:

- id: phrase_02
  type: speech
  at: 5
  duration: 6
  text: "Did you hear that? Stay here."

Wavnote distributes the text over that duration using token lengths and punctuation. These positions are visual authoring conveniences, not measured speech timings. Explicit timings override this convenience by using words instead of text.

You can omit text entirely, or hide it using --annotations none. Sound effects have no automatic explanatory labels. Voices identify signal profiles in the input; the export shows channel names and your supplied annotations.

When words would collide, additional annotation rows preserve their time anchors. If the annotations cannot fit legibly, Wavnote reports an error. Shorten the window, enlarge the page, or export multiple pages; it does not silently shrink or drop words. Labels at crop boundaries retain their anchors and produce a diagnostic warning.

Sounds and volume

Speech is synthesized into a nonverbal speech-shaped signal. This tool makes illustrations, not playable spoken dialogue. Harmonics, noisy consonant sections, syllable envelopes, irregular modulation, and pauses create the waveform. Use a real WAV source if you need the illustration to represent actual recorded audio.

List the procedural sound library with uvx wavnote presets:

  • Background: room-tone, ventilation, electrical-hum, tape-hiss.
  • Soft sounds: breath, fabric-rustle, soft-slide.
  • Impacts: dull-thud, hard-knock, object-drop, distant-impact.
  • Doors and panels: door-latch, door-close, hinge-creak, metal-click, panel-rattle.
  • Movement: scrape, footsteps.

Effects differ in attack, decay, texture, and repetition; the drawing comes from signal peaks rather than arbitrary decorative bars. Overlapping events are mixed before extracting peaks, and short transients survive zooming out.

- id: steps_01
  type: sfx
  preset: footsteps
  at: 10
  duration: 3
  count: 5
  material: concrete
  gain_db: -4
  decay: 1.2

material accepts wood, metal, fabric, or concrete, changing the resonance of footsteps, knocks, and drops. count controls footsteps and panel rattles; decay scales the decay of impact recipes. They are preset-specific controls, not universal effects on every sound.

Set gain_db independently on an event, voice, or channel. Gains add in dB; -6 dB roughly halves amplitude. A voice's delivery is quiet, normal, whisper, or raised. pitch_hz and breathiness affect its signal profile, not typography. Channel ambience has a separate level_dbfs control.

Every channel shares the same amplitude scale. Channels are not individually normalized: quiet sounds stay quiet. Signals over 0 dBFS produce warnings and are clipped visually at the lane bounds. render.display_gain_db adjusts the shared display scale without changing the signal.

Channel acoustics accepts dry, room, behind-door, corridor, and shaft. These add simple attenuation, filtering, and reflection tails. They are illustrative approximations rather than measurements of a real room.

Sound bleed across microphones

Reference an existing event to hear the same source through another channel's acoustics, with independent gain and delay:

- id: bleed_01
  ref: phrase_01
  type: speech
  at: 2
  duration: 4
  gain_db: -12
  delay: 0.012

The referenced event provides the signal, voice, and source audio; at is its new start and delay is an additional propagation delay. Its duration must match the source. References point to original events, not other references. No transcript is copied to the receiving channel automatically. You can add explicit words there if desired. An SFX reference should repeat the source's type: sfx and preset fields.

Import a WAV recording

- id: actual_audio
  type: audio
  source: ./audio/channel1.wav
  source_offset: 12
  at: 3
  duration: 8
  gain_db: -4
  words:
    - {text: "Wait.", start: 3.2, end: 3.7}

Imports support uncompressed 8/16/24/32-bit integer PCM WAV. Multichannel sources are averaged to mono and resampled to the session rate. The source must contain the requested duration. Float WAV and compressed audio need conversion first. Optional annotations remain manually supplied session times.

Recording gaps

gaps:
  - {start: 12, end: 15}

Add gaps at channel level. They draw a hatched missing-data region while preserving elapsed time. Recorded quiet still includes its configured ambience; a gap has no samples. Events cannot occupy a gap. Use separate crop windows for separate excerpts.

Fonts

Choose one interface font for the whole layout, not separate fonts for voices. By default Wavnote looks for an installed monospace interface font, then a sans-serif fallback. Nothing is bundled or downloaded.

uvx wavnote render session.yaml --font ./fonts/interface.ttf -o custom.svg
uvx wavnote render session.yaml --font-family sans-serif -o sans.svg

TTF, OTF, and the first face of a TTC collection are supported. A session file can set render.font; a CLI font path resolves from your working directory. If there is no suitable installed font, provide one with --font.

SVG text is outlined by default, so readers do not need the font installed and PNG exports preserve its placement. Use --editable-text for actual SVG text; that output depends on the recipient having the font installed. The selected font must contain the characters you use. Complex-script shaping, emoji, and variable-font axis selection are not supported.

Sizes, windows, and exports

All modes default to A4 landscape (297 × 210 mm). Other --page choices: a4-portrait, a3-landscape, and letter (US Letter portrait). Custom --width and --height accept mm, cm, in, pt, or px; a bare number means px. SVG dimensions remain physical; DPI applies only to PNG.

uvx wavnote render session.yaml --mode daw_light -o light.svg
uvx wavnote render session.yaml --title "Studio / session 03" -o custom-title.svg
uvx wavnote render session.yaml --width 24cm --height 16cm -o custom.svg
uvx wavnote render session.yaml --window 10:25 -o excerpt.svg
uvx wavnote render session.yaml --annotations none --timestamps none -o plain.svg
uvx --from 'wavnote[png]' wavnote render session.yaml -o print.png --dpi 300
uvx --from 'wavnote[png]' wavnote render session.yaml -o preview.png --pixel-width 1400
uvx wavnote render session.yaml --paginate 15 --output-dir pages
uvx wavnote render session.yaml -o - > recording.svg

PNG resolution can be controlled by DPI or explicit pixel width/height. One pixel dimension preserves the page aspect ratio; supplying both must preserve it too. PNG export requires the png extra (uvx --from 'wavnote[png]' wavnote ...), which uses resvg and does not need a browser. PNG pixel dimensions determine print resolution; SVG retains the explicit physical size. Print at actual size / 100%.

The export modes are daw_dark (default), daw_light, and minimal. Both DAW modes show the same controls; only the palette changes. Their header reads 48 kHz MULTITRACK at the default sample rate. There is no redundant time-range or channel-count footer.

Use --title "Studio / session 03" or render.title to replace the default Wavnote application header. session.title remains the recording name shown beneath it. Minimal mode omits the application header.

--timestamps accepts clock, elapsed, and none. --annotations accepts words and none. render.playhead adds a cursor at an elapsed second.

A window uses elapsed seconds, such as --window 10:25, not clock text. --paginate 15 writes recording-001.svg, recording-002.svg, etc. Combine it with --window to paginate only an excerpt. Change the -o suffix or use --format png for PNG pages.

Wrap a timeline on the same page

uvx wavnote render session.yaml --rows 2 --page a4-portrait -o wrapped.svg
uvx wavnote render session.yaml --wrap 15 --page a4-portrait -o wrapped.svg

--rows 2 divides the selected recording window into two equal segments. --wrap 15 starts a new segment every 15 seconds; the last segment can be shorter. All segments keep the same horizontal time scale; a shorter final segment leaves unused space instead of stretching its sounds. Set render.rows or render.wrap in YAML/JSON for the same behavior. Use one or the other. All segments of K1 appear consecutively, followed by all segments of K2, and so on. Each channel uses the same split points and time scale. Times and sound positions remain session-relative throughout.

Wrapping keeps everything in one SVG/PNG. --paginate creates separate files; you can combine pagination and wrapping, for example --paginate 60 --rows 2 for two 30-second segments per page. Each segment has its own ruler. Annotations and gaps crossing a wrap boundary are clipped to each segment without changing their original times.

More segments need more vertical space. For three channels split into two segments, the included example uses A4 portrait. Increase --height, choose a larger page, or paginate if the waveforms and annotations cannot fit legibly.

Limits: 32 channels, a 24-hour session, sample rates 8–96 kHz, at most 32 timeline segments and 120 seconds per segment, 40–1500 mm page dimensions, and PNGs up to 64 megapixels (64–16384 pixels per edge). Longer sessions need wrapping, cropping, or pagination. Dense speech benefits from shorter windows.

uvx wavnote validate session.yaml
uvx wavnote render --help

Validation checks schema, source recordings, font glyphs, clipping, and the selected layout. Errors and progress go to stderr; SVG stdout stays clean. Internal voice IDs, effect names, acoustic settings, and asset paths are not embedded in the exported illustration. Only your public title, labels, application title, and annotations appear.

Using it from Python

from pathlib import Path
from wavnote import load_session, render_session, png_bytes

path = Path("session.yaml")
session = load_session(path)
result = render_session(session, base_dir=path.parent)
Path("recording.svg").write_text(result.svg, encoding="utf-8")
Path("recording.png").write_bytes(png_bytes(result, dpi=300))

The CLI and Python API share the same schema, signal engine, and renderer. For reproducible exports, use explicit event IDs, a fixed seed, the same Wavnote/dependency versions, and an explicitly supplied font.

Development and releases

uvx --with '.[png]' --with pillow pytest
uvx ruff check src tests examples
uvx ruff format --check src tests examples
uv build --no-sources

Development commands above run from a checkout. The --with '.[png]' option installs the project and its runtime dependencies; --with pillow adds image checks to the isolated pytest environment. The pytest configuration points imports at the local src package. Ruff only needs its own tool environment. uv build builds distributions rather than running a tool.

The package uses uv_build and a console entry point. Tests cover timing anchors, crop stability, relative gain, brief transients, gaps, WAV import, font selection, pagination, synchronized wrapping, header customization, privacy of exported metadata, and PNG dimensions. GitHub Actions checks Linux, macOS, and Windows, plus installed-wheel execution.

To release, update the version in pyproject.toml and src/wavnote/__init__.py, configure this repository as a PyPI Trusted Publisher for the release.yml workflow and pypi environment, then push a matching v0.1.0 tag. The workflow checks that the tag matches the package version before building and publishing.

The code is MIT licensed. User-supplied fonts and audio retain their own licenses; no font or audio recording is redistributed by this package. The CaseClosed logo in the documentation remains CaseClosed branding.

Metadata

Release files for wavnote 0.1.0

For a detailed explanation of source distributions (sdists) and built distributions (wheels), please see the package formats documentation.

Source distribution (sdist)

Source distribution for wavnote 0.1.0
File Size Uploaded
wavnote-0.1.0.tar.gz 830.7 kB Details

Built distribution (wheel)

Table of built distributions (wheels) for wavnote 0.1.0
File Interpreter ABI Platform
wavnote-0.1.0-py3-none-any.whl Python 3 none any Details

Total release size: 858.5 kB

Release files / wavnote-0.1.0.tar.gz

Download URL wavnote-0.1.0.tar.gz
Size 830.7 kB
Tags Source
SHA-256 checksum
How to use checksums
d9d0fa4678624c7c796853e5176cdf0482f3b79965d27f7fa34a8136ecdcdfb4
BLAKE2b-256 checksum
How to use checksums
67eb9dccfec38a863855f57fdfcd6615e1d60f534b2ea46a3b4c8607e55a8bba
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
Yes
Uploaded via uv/0.13.0 {"installer":{"name":"uv","version":"0.13.0","subcommand":["publish"]},"python":null,"implementation":{"name":null,"version":null},"distro":{"name":"Ubuntu","version":"24.04","id":"noble","libc":null},"system":{"name":null,"release":null},"cpu":null,"openssl_version":null,"setuptools_version":null,"rustc_version":null,"ci":true}

Release files / wavnote-0.1.0-py3-none-any.whl

Download URL wavnote-0.1.0-py3-none-any.whl
Size 27.8 kB
Tags Python 3
SHA-256 checksum
How to use checksums
fa6c6460a485ee9e46576c4875c0f936287d129e6357c9ba2d7299376a29d90a
BLAKE2b-256 checksum
How to use checksums
34f11341255c9750b35188d0fc035751967ddf658d6867c514faaee090f862db
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
Yes
Uploaded via uv/0.13.0 {"installer":{"name":"uv","version":"0.13.0","subcommand":["publish"]},"python":null,"implementation":{"name":null,"version":null},"distro":{"name":"Ubuntu","version":"24.04","id":"noble","libc":null},"system":{"name":null,"release":null},"cpu":null,"openssl_version":null,"setuptools_version":null,"rustc_version":null,"ci":true}

Release history Release notifications | RSS feed

This release

0.1.0 This release

2 release files

Anthropic, PBC Visionary sponsor Bloomberg Visionary sponsor Hudson River Trading Visionary sponsor Meta Visionary sponsor NVIDIA Visionary sponsor Microsoft Sustainability sponsor Depot Continuous Integration AWS Cloud computing and Security Sponsor Datadog Monitoring Fastly CDN Google Download Analytics Sentry Error logging StatusPage Status page