# Audio

The [audio](../pkg/audio.md) package mixes in Go on the output device's
own thread. A game gets a `Mixer` from `ctx.Audio`; the engine opens and
owns the default output. Optional device enumeration and selection let
the game choose another endpoint through that mixer.

The engine still supplies `ctx.Audio` in headless mode, with `NoAudio`,
or when output initialization fails. Without output callbacks, sound
and stream playheads do not advance and natural-completion callbacks
do not run. Keep essential game progress independent of audio completion.

Every method is safe to call from the game loop. A setter copies its
value in under a short lock and the mixer picks it up at the start of the
next block, so setters do not wait for a whole block to finish.
The mixer takes the same lock twice a block. While it waits for the lock,
setters yield to it instead of queueing, so a game that calls setters
in a tight loop does not delay a block. To keep the lock short, call
setters once per frame for each voice that changed rather than
repeatedly for the same value.
`Stream.Read` runs without the settings lock, but retains the playback
lock. It may call setters or start voices, but must return promptly and
must not call `Voice.Seek`: seeking needs that same playback lock.
`Voice.Seek` waits for the block in flight when called from the game loop.

## Sounds and voices

`NewSound` converts decoded PCM to the mixer's format; `Decode` reads
WAV, Ogg Vorbis, MP3 and native FLAC from bytes, and `Sine` synthesises a tone for
tests and placeholders. `Play` starts a `Voice` with options: volume,
pan, loop, pitch, a fade-in, a low-pass cutoff, a reverb send, occlusion
and a priority. The voice can be adjusted while it runs: `SetVolume`,
`FadeTo`, `FadeOut`, `SetPitch`, `SetPaused`, `SetLowPass`,
`SetPosition`, `SetOcclusion`, `SetMute`, `SetSolo`.

Gains ramp across each block, so changes never click. `Stop`, `StopAll`
and a voice that loses its slot ramp to silence over about a
millisecond, so cutting a sound off mid-cycle is inaudible; the voice
leaves `Playing` at once and the mixer spends that millisecond fading
what is left. `Voice.Position` and `Seek` read and move the playhead,
`Sound.Duration` is its length, and `Voice.OnDone` runs a callback when
the voice ends, for chaining clips.

`Volume: 0` and `Pitch: 0` in `PlayOptions` both mean 1. Use
`SetVolume(0)` after starting a voice to silence it. `Playing` includes
muted, paused and out-of-range voices; it does not measure audibility.
Use `Voice.State()` to distinguish `PlaybackPlaying`, `PlaybackPaused`
and `PlaybackStopped`. It includes pauses applied by the bus or mixer;
mute leaves the state playing. Stop reports stopped immediately during
its final gain ramp. States have readable `String()` values for a UI or log.
Pitch applies to sounds and streams, defaults to 1 and is clamped to
0.01 through 64. Zero and non-finite pitch values select 1.
An `OnDone` callback normally runs on the mixing thread after its locks
are released. `StopAll` and voice stealing run it on their caller before
the final ramp, and registration on an already finished voice calls it
immediately. Keep callbacks short and send game-state changes back to
the game loop.

```go
pcm, err := audio.Decode(data) // WAV, Ogg Vorbis, MP3 or FLAC bytes
if err != nil {
	return err
}
hit, err := ctx.Audio.NewSound(pcm)
if err != nil {
	return err
}
v := ctx.Audio.Play(hit, audio.PlayOptions{Volume: 0.8, Pan: -0.3, Pitch: 1.1, FadeIn: 0.05})
v.OnDone(func() { ctx.Audio.Play(g.ricochet, audio.PlayOptions{}) })

// Later, while it plays.
v.SetVolume(0.4)
v.SetLowPass(1200)
v.FadeOut(0.5)
```

`audio.Sine(440, 0.3, ctx.Audio.Rate())` makes a `PCM` without a file.
The examples and tests use it to get something to play.

## Buses

Voices play through a `Bus`. A settings screen binds its sliders to
buses rather than to every voice. `Music`, `Effects` and `Dialogue`
already exist; `NewBus` makes more. A bus has its own volume, pause,
mute, solo and reverb, applied with the same ramps. To choose a bus, set
`PlayOptions.Bus`.

```go
m := ctx.Audio
m.Music().SetVolume(0.4) // the settings screen's music slider
m.Effects().SetVolume(0.9)

steps := m.NewBus("footsteps")
m.Play(g.step, audio.PlayOptions{Bus: steps, Volume: 0.6})
m.Play(g.line, audio.PlayOptions{Bus: m.Dialogue(), Priority: 100})
steps.SetVolume(0.3) // the whole group at once, ramped
```

`Mixer.Bus(name)` finds a bus made earlier, so the settings screen does
not have to be handed one.

## Pausing

To pause every voice at once, call `Mixer.SetPaused`. A pause menu calls
it, and the engine applies automatic pausing when every active window
requests it through its focus/minimized pause settings. In a single
window, `Config.PauseUnfocused` pauses audio when that window loses focus.
`Bus.SetPaused` pauses one bus and
`Voice.SetPaused` one voice. A pause fades out over the block it lands
in and the resume fades back in, so neither clicks. Each level is kept
separately, so resuming the mixer leaves a paused bus paused.

```go
if ctx.Input.KeyPressed(input.KeyEscape) {
	g.menu = !g.menu
	ctx.Audio.SetPaused(g.menu) // everything holds where it is
}
ctx.Audio.Effects().SetPaused(true) // one bus, so effects stop and music plays on
g.engine.SetPaused(true)            // one voice
```

## Mute and solo

To mute a voice or bus, call `Voice.SetMute` or `Bus.SetMute`. A muted
voice or bus keeps playing silently, so unmuting resumes wherever the
sound has reached; to stop the sound advancing, use `SetPaused` instead.
To solo, call `Voice.SetSolo` or `Bus.SetSolo`. While any voice is
soloed, only soloed voices are heard. While any bus is soloed, only
soloed buses are heard, and a voice on no bus is silent. Clearing the
last solo brings everything back.

```go
music := ctx.Audio.Music()
music.SetMute(!music.Muted()) // the mute button; the music keeps playing silently

// A mixing screen plays one bus at a time; g.audition is "" for none.
for _, b := range []*audio.Bus{ctx.Audio.Music(), ctx.Audio.Effects(), ctx.Audio.Dialogue()} {
	b.SetSolo(b.Name() == g.audition)
}
```

## Music

`OpenMusicFile` plays WAV, Ogg, MP3 or native FLAC through a two-second buffer filled by
a decoder goroutine. Ogg, MP3 and FLAC decode incrementally; WAV is decoded
fully at open and held in memory. `PlayStream` plays it, and `Close`
joins the decoder before closing the owned file.
`Music.Duration` and `Music.Seek` work for all four formats. FLAC duration
comes from STREAMINFO (0 when unknown). FLAC seeks scan forward, rewinding
when necessary, and retain at most one decoded FLAC frame. A distant seek
can take time, but opening does not decode the whole track or build an index.
FLAC decoding accepts 1..8 channels and 4..24-bit samples; playback accepts
mono/stereo. Frame CRCs are checked; whole-stream MD5 is not. 32-bit FLAC
and Ogg-encapsulated FLAC are not supported.
Looping retains resampling history across the track boundary, including
tracks as short as one source frame. An explicit seek resets that history;
nonlooping playback includes the final frame's interval.
Anything implementing `Stream` plays the same way. `Read` returns a
frame count, not a sample count: each frame has two interleaved samples.
A short return ends the voice. For a temporary underrun, fill with
silence and report a full buffer. Procedural music and the tracker
player use this contract.
Stream pitch conversion retains interpolation and lookahead across mixer
blocks without allocating during steady playback. A voice can read ahead
up to 513 source frames; `Voice.Seek` clears that lookahead. For streams,
`Voice.Position` reports source-rate elapsed time, including pitch,
Doppler and underrun silence, without wrapping at loop boundaries.

```go
if g.music, err = ctx.Audio.OpenMusicFile("music/theme.ogg", true); err != nil { // true loops
	return err
}
ctx.Cleanup(g.music.Close)
g.theme = ctx.Audio.PlayStream(g.music, audio.PlayOptions{Bus: ctx.Audio.Music(), Volume: 0.5})
...
g.theme.Seek(30)  // move playback and the voice's reported position
```

`Music.SetLooping(bool)` changes repetition at runtime; `Looping()` reads
it. `SetLoopRange(start, end time.Duration)` selects an exclusive end
boundary and restarts at start. `(0, 0)` restores the whole track. A range
must contain at least one source frame and fit the duration when known.
Boundaries round down to source frames; `LoopRange()` returns the aligned
boundaries rounded up to nanoseconds so they can be passed back unchanged.
Seeking outside an enabled explicit range returns to its start. With
looping disabled, playback continues from start through the file's end.

Loop changes flush prefetched audio and can briefly produce silence while
the decoder refills. Toggling repetition resumes at the next source frame,
rounded down, accounting for unread samples but excluding underrun padding.
A mixer block already in progress can finish with its previous settings.
Changing a range intentionally restarts it. Enabling looping after EOF
restarts the decoder; an already stopped voice needs a new `PlayStream`
call. Direct music controls do not reset `Voice.Position`.

`asset.Music(ctx.Audio, fs, "music/theme.ogg", true)` does the same
through the asset sources, so a packed or embedded track opens the same
way as a loose one. It reads the encoded file into memory. Use
`OpenMusicFile` for incremental file I/O without separate file cleanup,
or `OpenMusic` for a borrowed `io.ReadSeeker`. A borrowed reader stays
open and can be closed safely after `Music.Close` returns. Close waits
for an in-flight read or seek; a custom reader must unblock those itself.
Never call Music.Close from that reader or its callbacks. Repeated and
concurrent Close calls are safe. Close every Music even after playback
ends, since its decoder parks for a future seek.

## Positional audio

Set `Positional` and a `Position` on a voice, and put the listener where
the camera is each frame with `SetListener` (or `SetListener2D` for a 2D
game). Volume falls with distance between `MinDistance` and
`MaxDistance`, and the voice pans by direction.

```go
// 2D: the listener goes where the camera is looking.
ctx.Audio.SetListener2D(g.camX, g.camY)

// 3D: position and orientation.
ctx.Audio.SetListener(audio.Listener{Position: g.eye, Forward: g.dir, Up: lin.V3(0, 1, 0)})

torch := ctx.Audio.Play(g.fire, audio.PlayOptions{
	Loop: true, Positional: true,
	Position: lin.V3(4, 1, -8), MinDistance: 2, MaxDistance: 40,
})
torch.SetPosition(lin.V3(6, 1, -8)) // when the source moves
```

`RelativeToListener: true` interprets position, direction and velocity in
the listener's local basis: +X right, +Y up, -Z forward. Listener motion
is added to local velocity, so a stationary attached source has no
relative Doppler shift. Set it later with `SetRelativeToListener`.

For a directional source, set `Direction` and `Cone` in `PlayOptions`,
or use `SetDirection` and `SetCone`. Cone angles are full apertures in
radians: gain is 1 inside `InnerAngle`, interpolates to `OuterGain` at
`OuterAngle`, and stays there outside. The zero cone is omnidirectional.
Angles must satisfy `0 <= inner <= outer <= 2*pi`, and outer gain is 0..1.
Directions must be finite and nonzero; they are normalized.

`Attenuation` selects `AttenuationDefault` (the existing inverse-distance
curve multiplied by a linear cutoff), `AttenuationLinear`,
`AttenuationInverse`, `AttenuationExponential`, or `AttenuationNone`.
Rolloff defaults to 1; a larger value makes distance loss steeper.
All distance models reach silence at MaxDistance except None, which
ignores distance. `SetAttenuation` and `SetDistanceRange` validate finite
values; the distance range requires `0 < min < max`. Direction, cone,
attenuation, distance-range and relative-mode setters enable positional
audio. Matching getters expose the current settings for editing interfaces.

### Binaural rendering

By default a positional voice is panned between the two channels by a
constant-power law. `SetSpatial(audio.SpatialSettings{Binaural: true})`
renders it through a head model instead, which is worth having when the
player wears headphones. Each voice then gets:

- an interaural time difference, from Woodworth's formula for the path
  around a sphere, so the far ear hears it up to about 0.66 ms later;
- an interaural level difference, the far ear down by up to 6 dB;
- a head shadow, a one-pole low-pass on each ear whose cutoff falls from
  22 kHz to 1.5 kHz as the source moves to the far side;
- an elevation cue, a shelf at 4 kHz that lifts the high band for a
  source above the listener and drops it for one below.

This is a parametric head model, not a measured head-related transfer
function: it has no ear shape, so it does not tell front from back, and
it is the same head for every player. `HeadRadius` sets that head's size
in metres; the default of 0.0875 is an average adult, and larger heads
give a wider time difference. Everything is interpolated across each
block, so moving sources and a moving listener glide. The zero
`SpatialSettings` restore panning, and voices that are not positional
are unaffected either way.

```go
// A settings screen's headphones switch.
ctx.Audio.SetSpatial(audio.SpatialSettings{Binaural: g.headphones})
```

The cost is a delay line and three one-pole filters per positional
voice, and the signal collapses to mono before the ears, so a stereo
clip loses its own width when it is spatialised.

### Doppler

To turn the Doppler effect on, call `SetDoppler(1)`. A positional sound
closing on the listener then plays sharp, and one receding plays flat,
by how fast each moves along the line between them. The mixer does not
integrate motion, so the game gives it velocities: `Listener.Velocity`
and `Voice.SetVelocity` (or `PlayOptions.Velocity`), in world units per
second. They are measured against the speed of sound, 343 by default,
which suits metres; a game in pixels sets `SetSpeedOfSound` higher to
keep the effect subtle. The factor scales the shift, so 0.5 halves it and
0 (the default) is off. Doppler applies to both sound and stream voices,
combining with their configured pitch within the 0.01 through 64 rate range.

```go
ctx.Audio.SetDoppler(1)
ctx.Audio.SetSpeedOfSound(3000) // pixels, not metres, so the shift stays subtle

l := ctx.Audio.Listener()
l.Position, l.Velocity = g.ship.Pos, g.ship.Vel
ctx.Audio.SetListener(l)
train.SetVelocity(lin.V3(0, 0, -40)) // world units per second
```

### Occlusion

A sound behind a wall is quieter and duller than one in the open.
Occlusion applies that. `PlayOptions.Occlusion` and
`Voice.SetOcclusion` take 0 (clear) to 1 (fully blocked, 20 dB down and
low-passed to 400 Hz), with the amounts in between on a decibel scale.
The mixer has no scene, so the game supplies the amount. Cast a physics
ray from the listener to the source each frame and set the occlusion
from what it hits, or fade it as a door opens.

```go
// g.wallBetween is the game's own ray against the level.
for _, s := range g.sources {
	occ := float32(0)
	if g.wallBetween(g.eye, s.pos) {
		occ = 0.8
	}
	s.voice.SetOcclusion(occ)
}
```

## Reverb

`SetReverb` configures the mixer's shared reverb, a Freeverb-style comb
and all-pass network; voices feed it through their `Reverb` send, and the
tail is mixed on top of the dry output. `ReverbSettings` has a room size,
damping, stereo width and wet level, and its zero value is no reverb.
Every field runs from 0 to 1; a larger room size is clamped, because
above about 1.07 the comb feedback reaches one and the tail grows
without end instead of dying away.

```go
ctx.Audio.SetReverb(audio.ReverbSettings{RoomSize: 0.7, Damping: 0.4, Wet: 0.3})

// The send decides how much of each voice reaches it.
ctx.Audio.Play(g.shot, audio.PlayOptions{Reverb: 1})
ctx.Audio.Play(g.click, audio.PlayOptions{Bus: g.menu}) // no send: stays dry
g.pad.SetReverb(0.5)                                    // change it while it plays
```

### Reverb zones

To give an area its own reverb, so a cave does not sound like the field
outside it, call `SetReverbZones`. It takes a list of `ReverbZone`s,
each a sphere with its own settings, and the mixer checks them whenever
the listener moves. Inside a zone its settings replace the shared
reverb, blended in over `Fade` units from the edge so walking through
the doorway never jumps. A zero `Fade` blends across the whole radius;
set it to a fraction of the radius for a room that sounds the same
everywhere but its threshold. Where zones overlap, the one the listener
is furthest inside wins. `Mixer.Reverb` reports what is in effect, for a
debug overlay.

```go
ctx.Audio.SetReverbZones([]audio.ReverbZone{
	{
		Center: lin.V3(0, 0, -40), Radius: 20, Fade: 5, // the cave
		Settings: audio.ReverbSettings{RoomSize: 0.95, Damping: 0.2, Wet: 0.8},
	},
	{
		Center: lin.V3(30, 0, 0), Radius: 8, // the stairwell
		Settings: audio.ReverbSettings{RoomSize: 0.6, Wet: 0.5},
	},
})
here := ctx.Audio.Reverb() // what the listener is hearing now
```

### Bus reverb

`Bus.SetReverb` gives a bus a reverb of its own, and voices on that bus
send to it instead of the shared one. That keeps the music dry while the
cave's effects ring, or gives dialogue a small room while the world has a
large one.

```go
// The cave rings; the music stays dry because it is on another bus.
ctx.Audio.Effects().SetReverb(audio.ReverbSettings{RoomSize: 0.95, Damping: 0.2, Wet: 0.6})
ctx.Audio.Dialogue().SetReverb(audio.ReverbSettings{RoomSize: 0.3, Wet: 0.2})
ctx.Audio.Play(g.step, audio.PlayOptions{Bus: ctx.Audio.Effects(), Reverb: 1})
```

## Voice limits

`SetMaxVoices` caps the number of voices playing at once. When the cap
is reached, a new voice chooses the lowest eligible priority, then the
quietest voice at that priority. Voices with higher priority are
ineligible, so a footstep never steals from the dialogue. If none is
eligible, `Play` returns an already finished voice. The default cap is 64.

```go
ctx.Audio.SetMaxVoices(32)

// Dialogue outranks the world, which outranks incidental noise.
ctx.Audio.Play(g.line, audio.PlayOptions{Bus: ctx.Audio.Dialogue(), Priority: 100})
ctx.Audio.Play(g.explosion, audio.PlayOptions{Priority: 50})
ctx.Audio.Play(g.step, audio.PlayOptions{Priority: 0, Volume: 0.3})
n := ctx.Audio.Playing() // for the debug overlay
```

## Microphone input

`OpenCapture` records from the default input or `CaptureOptions.DeviceID`.
It hands back a `Capture` whose `Read` copies
whatever is buffered without waiting for new samples, so the game loop
can call it every update. The read briefly shares a mutex with the
device's buffer writer. `Level` is the root mean square of the block that arrived most
recently, which is what a meter draws, and `Close` releases the device.

`CaptureOptions` take a rate and a channel count, defaulting to the
mixer's rate and mono, and `Buffer` sets how many seconds the ring holds
before the oldest samples are dropped; the default is half a second.
`Dropped` counts what was lost that way, so a rising count means the
game is not reading often enough.

```go
if g.mic, err = ctx.Audio.OpenCapture(audio.CaptureOptions{}); err != nil {
	return err // no device, no permission, or a headless run
}
...
// In Update, every frame.
for {
	n := g.mic.Read(g.buf)
	if n == 0 {
		break
	}
	g.pushToVoiceChat(g.buf[:n])
}
g.meter = g.mic.Level()
```

Capture is separate from the mixer: nothing recorded is played back
unless the game plays it, which avoids a feedback loop by default. A
headless run and `Config.NoAudio` have no device at all, so `OpenCapture`
returns `ErrNoDevice` there rather than reaching the hardware behind the
game's back. On macOS the operating system asks the player for
microphone access the first time a game records, and a sandboxed
application needs the audio-input entitlement. `go run ./examples/audio
-mic` records and draws a level meter.

## Audio devices

Normal games open the system-default output automatically. `NewMixer`
alone opens no hardware. `OutputDevices()` and `InputDevices()` enumerate
endpoints without starting playback or recording. Each `DeviceInfo` has
an opaque local selection `ID`, a display `Name`, and a `Default` flag.
Names need not be unique; save and pass the ID when selecting an endpoint.
IDs can become unavailable after hardware, driver or configuration changes.
Linux lists configured ALSA PCMs, including virtual routing endpoints;
macOS uses device UIDs, and Windows uses WASAPI endpoint IDs.

```go
outputs, err := ctx.Audio.OutputDevices()
if err != nil {
	return err
}
// Present outputs[i].Name, retaining outputs[i].ID for the selected row.
if len(outputs) > 0 {
	if err := ctx.Audio.SetOutputDevice(outputs[0].ID); err != nil {
		return err // the previous output remains active
	}
}
```

`SetOutputDevice("")` selects system-default routing. A replacement opens
with a silent callback before it takes over, so two devices never advance
the mixer together. Failure preserves the old output; success can briefly
produce silence while preserving voice positions. The old device is
closed after handoff. `OutputDevice()` reports the chosen endpoint and
whether the mixer owns an output. Empty ID with Default true represents
the default routing choice, not a physical destination or live health
query; the OS may change its destination. There is no device-change
notification or automatic recovery after an endpoint disconnects.

The engine closes its current output at shutdown. For a standalone mixer,
call `CloseOutput()` when finished; a later `SetOutputDevice` can reopen it.
Selection and close calls are safe from game goroutines and serialize with
one another. Do not invoke either from audio callbacks, including
`Stream.Read` and `OnDone`, because device shutdown waits for callbacks.
`ErrDeviceUnavailable` identifies unavailable endpoints and
`ErrDeviceUnsupported` identifies missing backends. A headless run or
`Config.NoAudio` returns `ErrNoDevice` for enumeration, selection and capture.

To select a microphone, pass an ID from `InputDevices()` in
`CaptureOptions.DeviceID`. Empty selects the system default. Capture
remains explicit and caller-owned: opening an output or enumerating inputs
does not record, persist or transmit audio. Close each Capture when done.

## Recording and WAV export

`Record` owns a Capture and a worker that retains PCM. `RecordOptions`
contains `CaptureOptions` and `MaxDuration`; zero duration means 30 seconds.
The duration bounds both elapsed capture time and the number of complete
source frames. Negative durations, sub-frame limits and overflowing sample
counts are rejected. Reaching the limit is a normal stop.

```go
rec, err := ctx.Audio.Record(audio.RecordOptions{MaxDuration: 10 * time.Second})
if err != nil {
	return err
}
ctx.Cleanup(func() { rec.Close() })
...
pcm, err := rec.Stop() // stop input, drain accepted frames and join
if err != nil {
	return err
}
clip, err := ctx.Audio.NewSound(pcm)
```

`Recorder.Stop` returns an independent PCM copy, including partial data on
error. Repeated calls return equivalent copies. `Close` joins and retains
the data for a later Stop. `Done()` closes after capture and worker cleanup;
`Err()` reports the terminal error after Done closes. Neither creating a
mixer nor enumerating devices starts a recording.

For longer recordings without retaining all samples, `RecordPCM` streams
raw interleaved signed 16-bit little-endian samples to a borrowed
`io.Writer`. `RecordWAV` writes PCM16 WAV to a borrowed `io.WriteSeeker`,
starting at its current offset and finalizing the header when stopped;
it leaves the writer positioned after the WAV and open. `RecordWAVFile`
creates or replaces a file and owns its closure. These return `Recording`,
whose `Stop()` and `Close()` return errors, plus the same Done and Err
methods. Capture and conversion buffers remain bounded independently of
recording length. A requested WAV maximum must fit RIFF's 32-bit file-size
limit; choose an explicit duration for recordings longer than 30 seconds.

Stop closes capture promptly even if output is blocked, then waits for
the writer to finish. A borrowed writer must unblock its own Write/Seek
calls. Do not call Stop or Close from those callbacks. Capture ring drops
stop the recording with `ErrCaptureDropped`; write, header-finalization
and owned-file close errors are returned together. Data written before an
error may be partial. Recording APIs perform no automatic transmission.

`PCM.WriteWAV(w)` and `Sound.WriteWAV(w)` export PCM16 WAV to a borrowed
writer; `SaveWAV(path)` owns the created file. Samples are rounded and
clamped to [-1,1]; non-finite samples, incomplete frames and invalid
rate/channel/RIFF sizes are rejected before writing. Sound exports stereo
at its mixer rate.

Normal tests use synthetic samples and never open microphones or outputs.
Hardware integration tests are explicit opt-ins, for example
`BUNYIP_TEST_AUDIO_HARDWARE=1 CGO_ENABLED=0 go test ./audio -run '^TestCaptureDevice$'`
records briefly from the default microphone. On macOS, the same environment
flag enables `go test ./internal/audioout -run '^TestOpenPullsFrames$'`.

## Looking inside the mixer

`Voices` returns a snapshot of what is playing, one `VoiceInfo` per
voice with its bus, gain, pan, pitch, playhead and position, and `Buses`
returns every bus in the order it was made. Both are for a mixing panel
or a test rather than for the game's own logic; the [debug
console](console.md) shows them.

## Tracker music

[audio/tracker](../pkg/audio/tracker.md) loads and plays MOD, S3M, XM
and IT modules with one engine: envelopes, new-note actions, loops, the
IT filter and ProTracker's quirks. The player is a `Stream`, so
`PlayStream` plays it. `bunyip-play` plays any supported file from the
command line and can dump what the device received for comparison.

```go
mod, err := tracker.Load(data) // the format is sniffed from the bytes
if err != nil {
	return err
}
g.player = tracker.NewPlayer(mod, ctx.Audio.Rate())
g.player.Loop = true
g.song = ctx.Audio.PlayStream(g.player, audio.PlayOptions{Bus: ctx.Audio.Music(), Volume: 0.6})
```

`asset.Tracker(fs, "music/level1.xm")` loads a module through the asset
sources. `Module.Title`, `Channels` and `Patterns` are there for a
player screen or for driving visuals from the pattern data.

### Tracker control

Set the player's `Loop`, `Cubic` and `AmigaFilter` fields before playback,
and keep its `Module` and sample data unchanged while a player uses them.
The player can be driven while it plays; its methods take the same lock
as `Read`, so the game loop calls them freely. `Position` reports the
song position (an index into the order list) and row, `Length` counts the
positions and `Rows` the rows in the pattern at one, and `Seek(order,
row)` jumps there, cutting whatever was sounding, so a level with several
sections in one module can seek between them. `Mute(channel, true)`
silences a pattern channel while the song plays on, `Solo` plays one
channel alone, and `Channels` says how many there are. To drop the drums
while the player hides, mute their channel and unmute it later; the song
does not break.

```go
p := g.player
order, row := p.Position()
g.hud = fmt.Sprintf("%d/%d row %d of %d", order, p.Length(), row, p.Rows(order))
if g.enteredBossRoom {
	p.Seek(3, 0) // the boss section starts at song position 3
}
p.Mute(g.drums, g.hiding) // the drums drop out while the player hides
for ch := range p.Channels() {
	g.lit[ch] = !p.Muted(ch) // a channel strip for the overlay
}
```
