Skip to content

jizhang495/wavemusic

Repository files navigation

WaveMusic

Writing music audio files using sine, square, triangle, and saw waves.

Quick Start

Prerequisites

  • Linux/WSL
  • Python 3.12+
  • uv
  • Node.js/npm
  • g++
  • pybind11 build deps
sudo apt install python3-dev g++ cmake pybind11-dev

Run The App

uv sync
uv run setup.py build_ext --inplace
uv run main.py

Open http://127.0.0.1:8000.

Music File Format

WaveMusic stores projects as JSON files in sheets/:

{
  "title": "summer's end",
  "key": "e major",
  "time_signature": "4/4",
  "bpm": 100,
  "sample_rate": 44100,
  "transpose": 0,
  "parts": [
    {
      "name": "lead",
      "timbre": "square",
      "score": ["2c4 g e f | 4g 4r"]
    }
  ]
}

The score field keeps the compact note syntax. The JSON wrapper stores project metadata and per-part settings. transpose is a whole-number semitone offset applied at render time.

timbre can be a simple preset name such as sine, square, triangle, saw, soft organ, bright organ, reed organ, mellow organ, string organ, warm synth organ, baroque violin, viola da gamba, recorder, lute, or harpsichord. Some presets expand to a full render recipe with partials, filters, and note shaping, while the JSON stays compact. For custom timbre, use either a mix of the four base waves or additive partials:

{
  "name": "lead",
  "timbre": {
    "preset": "custom",
    "mix": {
      "sine": 0.4,
      "square": 0.15,
      "triangle": 0.3,
      "saw": 0.15
    }
  },
  "score": ["2c4 d e f | 4g r"]
}
{
  "name": "lead",
  "timbre": {
    "preset": "custom",
    "partials": [1.0, 0.55, 0.35, 0.25],
    "filter": {
      "highpass": 120,
      "lowpass": 4500
    },
    "noise": 0.02,
    "envelope": {
      "attack_ms": 25,
      "decay_ms": 60,
      "sustain": 0.9,
      "release_ms": 120
    },
    "vibrato": {
      "depth": 0
    }
  },
  "score": ["2c4 d e f | 4g r"]
}

See docs/json-score-format.md for the full JSON schema, note syntax, and barline convention. See docs/ai-generation-prompt.md for the AI prompt, and docs/timbre.md for presets, mix and partials timbre format, filter visualization, noise, envelope, and vibrato.

JSON is the project format because it is explicit, easy to validate, and maps directly to the web UI. It is also a better target for AI-generated music: future prompt-based generation can ask a model for one JSON object with title, key, time_signature, bpm, sample_rate, transpose, and parts, while the model only needs to learn WaveMusic's compact note syntax inside each score field.

Before rendering, Python converts the JSON project into an internal .score stream for the C++ engine. The .score format contains only render data, such as mix sections and notes; metadata like title, key, and time_signature stays in JSON unless it later affects sound generation. This keeps the C++ code focused on the heavy work: parsing a strict render stream, synthesizing audio, mixing parts, filtering, and writing WAV output. Because .score is generated by Python rather than edited directly by users, the Python conversion layer should validate messy user or AI input and emit a strict, predictable .score stream.

See docs/score-format.md for the internal .score syntax contract.

Architecture

  • C++ audio engine
  • Python web/API layer
  • TypeScript web frontend

Architecture of audio engine




                                  main()
                             ┌──────────────────────────┐
                             │                          │
                             │   Write WAV              │
                             │   header                 │
               sigen.cpp     │                          │
                             │     │                    │
┌──────────────────────────┐ │     ▼                    │
│                          │ │                          │
│ play()       Generate    │ │   Parse        parse()   │
│              signals   ◄─┼─┼── score                  │
│                          │ │                          │
│                 │        │ │     │                    │
│                 ▼        │ │     │                    │
│                          │ │     │                    │
│ envelope_    ADSR-style  │ │     │                    │
│ gain()       envelope    │ │     │                    │
│                          │ │     │                    │
│                 │        │ │     │                    │
│                 ▼        │ │     ▼                    │
│                          │ │                          │
│ highpass/    Optional  ──┼─┼─► Write                  │
│ lowpass      filters     │ │   notes                  │
│                          │ │                          │
└──────────────────────────┘ │     │                    │
                             │     ▼                    │
                             │                          │
                             │   Insert data size       │
                             │   in WAV header          │
                             │                          │
                             │     │                    │
                             │     ▼                    │
                             │                          │
                             │   Playback               │
                             │   with                   │
                             │   system Call            │
                             │                          │
                             └──────────────────────────┘

Development

C++ Audio Engine

To build and run directly:

./simple tests/fixtures/score/basic_triangle.score

for debug mode:

make refresh DEBUG=1
./simple tests/fixtures/score/basic_triangle.score --no-play

Full flag example:

./simple \
  --score=tests/fixtures/score/basic_triangle.score \
  --out=/tmp/basic_triangle.wav \
  --sample-rate=44100 \
  --bpm=100 \
  --transpose=0 \
  --no-play

Flags:

  • --score=<path>: input .score file. A positional score path also works.
  • --out=<path>: output WAV path. If omitted, writes m.wav.
  • --sample-rate=<hz>: output sample rate. Defaults to 44100.
  • --bpm=<beats>: tempo. Defaults to 100.
  • --transpose=<semitones>: whole-number pitch shift in semitones. Defaults to 0.
  • --no-play or --silent: render the WAV without starting playback.

The C++ executable renders the internal score stream. Normal project files are JSON and should be rendered through main.py or the web app. The .score fixtures under tests/fixtures/score/ should strictly follow docs/score-format.md, because .score is the narrow format parsed by the C++ engine. BPM, sample rate, and transpose are runtime flags, not .score syntax.

When rendering a JSON project through main.py, Python converts it to a temporary .score file before calling the C++ engine. The CLI uses a unique path like /tmp/wavemusic-*/main.score while rendering and then removes it, so two renders cannot overwrite the same intermediate file. The web API keeps generated intermediates under /tmp/wavemusic/ by default, or under WAVEMUSIC_GENERATED_DIR if that environment variable is set.

To inspect or reuse a stable intermediate .score file, write one explicitly:

uv run python - <<'PY' > /tmp/wavemusic-main.score
from pathlib import Path
from scripts.project import compose_score, load_project

project = load_project(Path("sheets/pokemoncentre.json"))
print(compose_score(project["parts"]))
PY

./simple /tmp/wavemusic-main.score --out=/tmp/pokemoncentre.wav --no-play

Python API Layer

Uses FastAPI, pybind.

The wave module in the Python standard library provides a convenient interface to the WAV sound format. References: https://docs.python.org/3/library/wave.html https://www.tutorialspoint.com/read-and-write-wav-files-using-python-wave

To run the python script:

uv run main.py

To use command-line interface:

uv run main.py cli

To generate WAV from a JSON project:

uv run main.py sheets/<title>.json

Web frontend

Run the app:

uv run main.py

Then open http://127.0.0.1:8000.

This starts the Python API and serves the built TypeScript frontend from the same local server. The first run builds the frontend automatically if webapp/dist does not exist.

For frontend development, run the API in one terminal and Vite in another:

uv run uvicorn scripts.web_api:app --host 127.0.0.1 --port 8000 --reload
cd webapp && npm run dev

Then open http://127.0.0.1:5173.

Deployment

Recommended public demo setup:

  • Backend: deploy the FastAPI/C++ engine container to Google Cloud Run.
  • Frontend: deploy the static TypeScript build to GitHub Pages.
  • Local mode: keep using uv run main.py; it serves the API and frontend from one local server.

Deployment guides:

Roadmap

TODO list:

  • time different implementations python vs c++. Additive partials take much more time to compute. Use wavetable/cache?
  • write tests
  • add docs
  • use () to pass frequecies or chords
  • add loudness: [ff], [f], [fp], [p], [pp]
  • add dynamics: cresc, dim
  • functional REPL: modernize uv run main.py cli as terminal interactive score preview

About

Writing music audio files using sine, square, triangular and sawtooth waves

Resources

License

Stars

3 stars

Watchers

2 watching

Forks

Releases

No releases published

Packages

 
 
 

Contributors