Bunyip a game engine in Go GitHub

Package github.com/matjam/bunyip/audio/tracker

audio/tracker

Package tracker loads and plays tracker music: ProTracker MOD (4 to 32 channels), ScreamTracker 3 S3M, FastTracker 2 XM and Impulse Tracker IT. Player implements audio.Stream, so a module plays through the mixer as any other voice does. While it plays, call Seek to move to a song position and row, read the Position, and Mute or Solo pattern channels. The four formats share one Module model. Module.Format and the flags the loaders set select the format-specific behaviour (period tables, slide units, effect semantics).

To load a module from bytes, call Load, which detects the format; LoadMOD, LoadS3M, LoadXM and LoadIT each read one format. A Module carries the song's name, orders, patterns, samples and instruments, so a game can show what is playing or drive visuals from the pattern data. NewPlayer renders it at the mixer's rate. The player reports its position (order and row) for syncing gameplay to the music, and it accepts seeks and per-channel mute and solo. Playback is deterministic, so a module renders the same bytes on every platform.

Index

Constants

const (
	NoteNone = -1
	NoteOff  = 1000 // key off: release envelopes, then fade
	NoteCut  = 1001 // stop immediately
	NoteFade = 1002 // start fading without releasing sustain
)

Special note values.

Types

type AutoVibrato source

type AutoVibrato struct {
	Type  int // 0 sine, 1 ramp, 2 square, 3 random
	Sweep int // ticks to reach full depth
	Depth int // depth in the source format's units
	Rate  int // oscillator increment per tick in source-format units
}

AutoVibrato is instrument-level vibrato (XM instruments, IT samples).

type Cell source

type Cell struct {
	Note       int    // NoteNone, NoteOff, NoteCut, NoteFade, or a note index
	Instrument int    // 0 none
	VolCmd     volCmd // decoded volume-column command; internal command vocabulary
	VolParam   int    // volume-column command argument
	Effect     effect // decoded effect command; internal command vocabulary
	Param      byte   // effect command argument
}

Cell is one channel's entry in a row.

type EnvPoint source

type EnvPoint struct {
	Tick  int     // time in tracker ticks
	Value float32 // envelope value in the ranges described by EnvPoint
}

EnvPoint is one node: Tick and a value in 0..64 for volume, -32..32 for panning and pitch (IT stores signed values; XM pan is 0..64 shifted).

type Envelope source

type Envelope struct {
	Points       []EnvPoint // nodes in increasing tick order
	Enabled      bool       // apply the envelope during playback
	Sustain      bool       // hold or loop sustain points before key off
	Loop         bool       // loop between LoopStart and LoopEnd
	SustainStart int        // point indices
	SustainEnd   int        // last sustain point index
	LoopStart    int        // first loop point index
	LoopEnd      int        // last loop point index
}

Envelope is a volume, panning or pitch envelope.

type Format source

type Format int

Format selects the period and effect semantics.

const (
	FormatMOD Format = iota
	FormatS3M
	FormatXM
	FormatIT
)

Supported source formats and their playback semantics.

String source

func (f Format) String() string

String names the format, as in "MOD" or "IT".

type Instrument source

type Instrument struct {
	Name            string   // instrument display name
	SampleMap       [120]int // note -> sample index, -1 none
	NoteMap         [120]int // note -> note actually played (IT); identity elsewhere
	VolEnv          Envelope // volume envelope
	PanEnv          Envelope // panning envelope
	PitchEnv        Envelope // pitch or filter envelope
	PitchIsFilter   bool     // IT: the pitch envelope drives the filter cutoff instead
	Fadeout         int      // subtracted from a 65536-scale fade each tick after key off
	NNA             NNA      // action for the previous note when a new note begins
	DCT             int      // duplicate check type: 0 off, 1 note, 2 sample, 3 instrument
	DCA             int      // duplicate check action: 0 cut, 1 note off, 2 fade
	GlobalVolume    int      // 0..128 (IT); 128 elsewhere
	Pan             float32  // default pan
	HasPan          bool     // apply Pan when this instrument starts a note
	FilterCutoff    int      // IT: 0..127, -1 unset
	FilterResonance int      // IT: 0..127, -1 unset
}

Instrument maps notes to samples and shapes them with envelopes.

type LoopType source

type LoopType uint8

LoopType says how a sample repeats.

const (
	LoopNone LoopType = iota
	LoopForward
	LoopPingPong
)

Sample loop modes: no repetition, forward wrap, or alternating direction.

type Module source

type Module struct {
	Title       string       // display title from the file
	Channels    int          // pattern channels, excluding NNA background voices
	Samples     []Sample     // decoded samples referenced by instruments and cells
	Instruments []Instrument // empty for sample-only formats (MOD, S3M)
	Patterns    []Pattern    // pattern data indexed by Orders
	Orders      []int        // pattern index per song position
	Restart     int          // song position to loop back to
	Speed       int          // initial ticks per row
	Tempo       int          // initial BPM
	Pan         []float32    // initial pan per channel, -1 left to +1 right
	ChannelVol  []int        // 0..64 per channel; nil means 64
	Format      Format       // selects the source format's playback semantics

	GlobalVolume int  // 0..128
	MixVolume    int  // 0..128: the file's master/mixing volume; per-channel gain follows it
	LinearSlides bool // XM/IT: slides move pitch by fractions of a semitone
	OldEffects   bool // IT "old effects" flag
	CompatGxx    bool // IT: Gxx shares memory with Exx/Fxx

}

Module is a song in memory, in a form shared by every loader.

Load source

func Load(data []byte) (*Module, error)

Load detects the format and parses a module.

LoadIT source

func LoadIT(data []byte) (*Module, error)

LoadIT parses an Impulse Tracker module, including compressed samples.

LoadMOD source

func LoadMOD(data []byte) (*Module, error)

LoadMOD parses a ProTracker-family module.

LoadS3M source

func LoadS3M(data []byte) (*Module, error)

LoadS3M parses a ScreamTracker 3 module. Adlib instruments are ignored.

LoadXM source

func LoadXM(data []byte) (*Module, error)

LoadXM parses a FastTracker 2 module.

type NNA source

type NNA uint8

NNA is Impulse Tracker's new-note action.

const (
	NNACut NNA = iota
	NNAContinue
	NNAOff
	NNAFade
)

New-note actions for the old voice: cut, continue unchanged, release its envelopes, or begin fading it out.

type Pattern source

type Pattern struct {
	Rows [][]Cell // row index, then channel index
}

Pattern is rows of cells, one cell per channel.

type Player source

type Player struct {
	Loop bool // restart at Module.Restart on reaching the order list's end
	// AmigaFilter enables the ProTracker LED low-pass filter (E00/E01).
	// Off by default: most players and listeners expect the unfiltered mix.
	AmigaFilter bool
	// Cubic switches sample interpolation from linear to four-point
	// Hermite, which rounds off the aliasing that makes chip samples sound
	// harsher than in other players. Off by default to match the classic
	// sound.
	Cubic bool
	// contains filtered or unexported fields
}

Player renders a Module to stereo float32 at a fixed rate. It implements audio.Stream. Read runs on the mixer's thread once the player is playing; Seek, Position, Mute and Solo take the same lock, so the game loop can call them while it plays. Set Loop, AmigaFilter and Cubic before playing. Keep the Module and its referenced slices immutable while a player uses them. The player retains them rather than copying the song.

Example

Play a module through the engine's mixer. Player is an audio.Stream, so it plays like any other music.

package main

import (
	"os"

	"github.com/matjam/bunyip/audio"
	"github.com/matjam/bunyip/audio/tracker"
)

func main() {
	data, err := os.ReadFile("song.xm")
	if err != nil {
		return
	}
	mod, err := tracker.Load(data) // MOD, S3M, XM or IT, told apart by content
	if err != nil {
		return
	}
	mixer := audio.NewMixer(48000)
	player := tracker.NewPlayer(mod, mixer.Rate())
	player.Loop = true
	mixer.PlayStream(player, audio.PlayOptions{Volume: 0.8})
}

NewPlayer source

func NewPlayer(m *Module, rate int) *Player

NewPlayer prepares a module for playback at rate Hz. Use a positive rate and a non-nil module returned by a loader. For a hand-built module, Pan must contain Channels entries, and ChannelVol must either be nil or contain Channels entries; pattern and sample indices must follow the Module model. Construction does not validate every field or return an error.

Channels source

func (p *Player) Channels() int

Channels is the number of pattern channels, the range Mute and Solo accept.

Finished source

func (p *Player) Finished() bool

Finished reports whether a non-looping song has ended.

Length source

func (p *Player) Length() int

Length is the number of song positions in the order list, the range Seek accepts.

Mute source

func (p *Player) Mute(channel int, mute bool)

Mute silences a pattern channel while the song plays on, as a tracker's channel mute does; notes it starts still run their course silently, so unmuting is seamless. Channels outside the module are ignored.

Muted source

func (p *Player) Muted(channel int) bool

Muted reports whether a channel is muted.

Position source

func (p *Player) Position() (order, row int)

Position reports the current song position (an index into the order list) and the row within its pattern.

Read source

func (p *Player) Read(out []float32) int

Read fills out and returns the frames written (audio.Stream).

Rows source

func (p *Player) Rows(order int) int

Rows is the number of rows in the pattern at a song position, or 0 for a position outside the song or one that names no pattern.

Seek source

func (p *Player) Seek(order, row int)

Seek jumps to a song position and row and plays on from there, cutting whatever was sounding. Order is clamped to the song and row to the pattern; a position that names no pattern moves on to the next that does. Speed, tempo and global volume stay as they are, so a song that changes them along the way plays from the new place at whatever was in force. Seeking a finished song starts it again.

Solo source

func (p *Player) Solo(channel int, solo bool)

Solo auditions a channel: while any channel is soloed, only soloed channels are heard. Clearing the last solo brings the rest back.

Soloed source

func (p *Player) Soloed(channel int) bool

Soloed reports whether a channel is soloed.

type Sample source

type Sample struct {
	Name         string      // sample display name
	Data         []float32   // mono sample frames, nominally -1..1
	LoopStart    int         // first frame of the normal loop
	LoopEnd      int         // exclusive end frame of the normal loop
	Loop         LoopType    // normal loop behavior
	SusLoopStart int         // IT sustain loop, active until key off
	SusLoopEnd   int         // exclusive end frame of the sustain loop
	SusLoop      LoopType    // sustain loop behavior before key off
	Volume       int         // 0..64
	GlobalVolume int         // 0..64 (IT); 64 elsewhere
	Finetune     float32     // semitones added to every note
	C4Speed      int         // playback rate of the reference note (S3M/IT)
	RelativeNote int         // XM: semitones added to every note
	Pan          float32     // default pan, -1..1
	HasPan       bool        // whether Pan applies on note start
	Vibrato      AutoVibrato // sample auto-vibrato settings
}

Sample is one instrument's PCM data, mono, -1..1.

Source files

bench_test.go denormal_test.go effects.go envelope.go example_test.go fuzz_test.go golden_test.go it.go itdecompress.go mod.go module.go perf_bench_test.go player.go render.go review_test.go row.go roweffects.go s3m.go tables.go tick.go tracker_test.go xm.go