# game-audio

Procedural game audio library. Generates tones, noise, pattern-based music, and full multi-track songs from a text format — without bundled audio files.

## Features

- **Tone** – sine wave with configurable frequency, frequency slide, and amplitude decay
- **Noise** – PRNG-based noise with decay
- **Sequence** – multi-segment frequency sequences for one-shot SFX
- **Track format** – layered songs in a `.track` text format with synthesized notes (custom timbres, envelopes, vibrato/tremolo/slide, per-note glide and volume ramps), audio file clips (WAV/MP3/OGG/FLAC), per-layer volume/pan/mute/solo/loop
- **Motifs** – named, reusable melodic figures placed on tracks with a chosen sound and transposition; edit the definition and every instance updates
- **Tunings** – custom scales (just intonation, equal temperaments, non-octave periods) as frequency ratios; pitches become scale-degree indices
- **Song graphs** – chain songs across files with `@jump`, optionally conditional on host-set parameters, a play counter, or randomness, for adaptive/interactive music
- **Imports** – pull sounds, motifs, and tunings from other `.track` files, local or by URL (downloaded and cached)
- **SoundSystem** – rodio wrapper for one-shot, delayed, and ambient sounds

## Quick example

```rust
use std::time::Duration;
use game_audio::{Tone, Noise, Sequence, SoundSystem};

let system = SoundSystem::new();

system.play(Tone::new(Duration::from_millis(200), 440.0, 0.0, 0.2, 0.999));
system.play(Noise::new(Duration::from_millis(500), 0.5, 0.99995));
system.play(Sequence::new(&[(800.0, 0.04), (1100.0, 0.06)]));
```

## Track format

Songs live in `.track` files: a small text format with synthesizable sounds, multiple tracks, and clips placed on a beat timeline.

```
@tempo 120

# Sounds

bell:   sine 1, octave 0.4, decay 0.5
guitar: sine 0.6, triangle 0.3, attack 0.01, decay 0.2

# Layer Drums
@volume 0.9

kick.wav 0
snare.wav 1

# Layer Melody
@volume 0.6

bell 0 0 2
bell 2 7 2
```

- Sound definitions list harmonics (`sine`, `triangle`, `soft_square`, `square`, `saw`, `hill` with optional `@multiplier`) plus `distortion`, `volume`, `sustain`, `noise`, `vibrato`/`tremolo <rate> <depth> [wave]`, `slide <semitones> <sec>`, and `attack`/`decay <sec> [curve]`. Curves are `linear`, `ease-in`, `ease-out`, or `ease` (both); omitting the curve token defaults to `ease` (a smooth S-curve).
- Layers (`# Layer <name>` sections) mix simultaneously and have `@volume`, `@pan`, `@mute`, `@solo`, `@loop <length> [skip <n>]` directives.
- Note clips: `<sound> <start_beat> <pitch_semitones> <duration_beats>`, with optional `glide <semitones> [curve]`, `volume <v>`, and `vol-ramp <delta> [curve]`. Pitch is in semitones relative to A4 (0 = 440 Hz); glide and vol-ramp are relative (added over the note, 0 = steady).
- Audio clips: `<file.wav> <start_beat>`. Paths are relative to the song file.
- `@import other.track` pulls in another file's sounds and motifs. The path may
  be a URL (`@import https://example.com/instruments.track`); URL imports are
  downloaded and cached (native, default `online` feature). A cached URL is
  reused until forced to re-fetch: add `refresh` to one import
  (`@import <url> refresh`), or pass `--refresh` to `track-play` to re-download
  all of them. Ready-made instrument banks ship in [`sounds/`](sounds)
  (`orchestral`, `chiptune`, `drums`, and `all` which re-exports them); import a
  category or `sounds/all.track`. External projects can import them straight
  from this repository:

  ```
  @import https://gitlab.com/porky11/game-audio/-/raw/master/sounds/all.track
  ```

  This tracks `master`; because imports are cached, pull updates with `refresh`
  (or `track-play --refresh`). Pin to a release tag instead of `master` for a
  frozen, reproducible version.

Motifs are named, sound-less note figures defined in a `# Motif <name>` section
(`<start> <pitch> <duration>` per line, with the same optional glide/volume
tokens as notes). Place one on a track with `motif <name> <start> <sound>
[transpose <n>] [tuning <name>]` — it references the definition rather than
copying it, so editing the motif updates every placement. An optional `tuning`
overrides the layer's tuning for that placement, so the same motif can recur in
major and minor (a full leitmotif shift: pitch, instrument, and scale per
instance).

A placement can also tweak individual notes without editing the shared
definition, via indented lines under the `motif` line. `note <index> <field>
<value>…` overrides fields (`pitch`, `start`, `duration`, `glide`, `volume`) of
the note at `index` (0-based, in play order); overridden values still get the
instance's transpose and start offset, and an index with no such note is
ignored. `trim <from> <count>` keeps only that run of notes for the placement
(dropping leading/trailing ones). To add or duplicate notes, define a separate
variant motif instead.

```
# Motif hook
0 0 1
1 7 1

# Layer Lead
motif hook 0 bell
motif hook 8 bell transpose 12
  note 1 pitch 14 volume 0.8
  trim 0 1
```

### Song structure

Each `.track` file is one song. `@length <beats>` sets its length explicitly;
without it the length is the end of the latest note across all layers.
`@signature <beats_per_bar> [beat_unit]` sets the time signature (default 4/4)
for consumers that group beats into bars; it is metadata only, playback counts
beats regardless. `@jump <file> [skip_beats]` continues to another song when
this one ends — a self-reference loops, and any cycle is allowed. The optional
skip drops that many beats at the target's start (e.g. re-enter a loop past its
intro).

```
@length 8
@jump loop.track 2
# Layer Melody
lead 0 0 8
```

A song may have several `@jump` lines, each optionally conditional; the first
whose condition holds is taken when the song ends (an unconditional jump is the
fallback). A condition is `if <source> <op> <value>`, where `source` is `plays`
(how many times this song has played), `random` (a fresh `0..1` draw), or a
parameter name; `op` is one of `< <= > >= == !=`. Parameters are declared with
`@param <name> [default]` and set at playtime with `SongPlayer::set_parameter`,
so the host can steer the music (combat, location, mood). Conditions are
read-only comparisons — the graph stays a finite-state machine, not a program.

```
@param combat 0
@jump action.track if combat > 0
@jump verse.track if plays >= 4
@jump chorus.track if random < 0.3
@jump loop.track
```

### Tunings

By default a pitch is a semitone in 12-tone equal temperament. A `# Tuning
<name>` section defines a custom scale as frequency ratios (fractions like
`3/2` or decimals, normalized so the first is 1.0), or `equal <n>` for `n`
equal divisions; `period <ratio>` sets the repeat interval (default `2/1`, the
octave). A layer selects one with `@tuning <name>` and sets the frequency of
pitch 0 with `@root <hz>` (default 440). Pitches are then scale-degree indices;
fractional pitches interpolate. Tunings are merged by `@import` like sounds and
motifs, and a small library ships in [`scales/scales.track`](scales/scales.track).

```
@import scales/scales.track

# Layer Melody
@tuning major
@root 261.6
lead 0 0 1
lead 1 1 1
lead 2 2 1
```

With `@root 261.6` (C) and `major`, pitch 0 is C, pitch 1 the second scale
degree (D), pitch 2 the third (E), and so on. There are no mid-line comments,
but a whole `#` line with an unrecognized header starts an "unknown" section
that is preserved and ignored for playback, so such lines work as comments;
`@` directives after them still apply.

Play a single song:

```rust
use game_audio::{SoundSystem, track::SongPlayer};

let system = SoundSystem::new();
let mut player = SongPlayer::new();
player.play_path(&system, "song.track").unwrap();
```

Play a song and follow its `@jump` graph across files (call `update`
periodically to advance jumps):

```rust
use game_audio::{SoundSystem, track::SongPlayer};

let system = SoundSystem::new();
let mut player = SongPlayer::new();
player.play_graph_path(&system, "intro.track").unwrap();
loop {
    player.update(&system).unwrap();
    std::thread::sleep(std::time::Duration::from_millis(50));
}
```

The `track` module also exposes `parse`, `resolve_imports`, `SongGraph`, `Song`, `Sound`, `Harmonic`, `Note`, etc., for direct use without the convenience player. `Song` implements `Display` and `FromStr`, so `song.to_string()` writes canonical `.track` text and `text.parse::<Song>()` reads it back; a canonically-formatted file survives a parse → serialize round trip unchanged.

## Tools

Feature-gated binaries ship with the crate:

- `track-play` (feature `play`) — plays a `.track` file, following its `@jump` graph, with seek/speed/volume keys and `--refresh` to re-fetch cached URL imports.
- `track-export` (feature `export`) — renders a `.track` file to WAV or MP3.
- `midi-import` (feature `midi`) — converts a Standard MIDI File to `.track`: one layer per track, GM programs and the channel-10 drum map to bank sound names (`@import sounds/all.track`), note pitch/timing/velocity carried over. The first tempo is used (tempo changes are dropped with a warning); pitch bend and controllers are ignored. `midi-import <input.mid> [output.track]`.

## Editor

Companion crate [`multitrack`](https://crates.io/crates/multitrack) provides a graphical editor for `.track` files: timeline, mixer, sound designer, file open/save, live source view.

## Notes

This project is designed for use with AI coding assistants: consistent types, no implicit defaults, all parameters explicit. The `deny(missing_docs)` lint ensures complete documentation.
