Bunyip a game engine in Go GitHub

Package github.com/matjam/bunyip/audio

audio

Package audio mixes sounds in Go and passes float32 stereo frames to the platform's output device. A Sound holds decoded samples at the mixer's rate. To play one, call Play, which returns a Voice that can be adjusted or stopped while it runs. Music streams from a decoder instead of being held in memory. Voices can be placed in the listener's world (with distance, panning or a binaural head model, Doppler and occlusion), filtered, sent to a reverb, faded, pitched, muted, soloed and prioritised, and grouped on a Bus to be turned down, muted or paused together. The mixer has one shared reverb, which a ReverbZone replaces while the listener is inside that zone, and a Bus can carry a reverb of its own. Mixing happens on the audio device's thread, so every method is safe to call from the game loop, and every change in gain ramps across a block so nothing clicks; stopping a voice ramps it to silence over about a millisecond first. A setter copies its value in under a short lock and the mixer applies it at the start of the next block, so setters do not wait for a whole block. While the mixer waits for that lock, setters yield to it, so a game calling setters in a tight loop does not hold a block back. Stream.Read runs without the settings lock but with the playback lock held; it may call setters or start voices, but must not call Voice.Seek. Voice.Seek waits for the block in flight, because it moves the playhead being read.

The decoders (Decode, DecodeWAV, DecodeOGG, DecodeMP3, DecodeFLAC) take the whole file as bytes. OpenMusic borrows a seekable reader; OpenMusicFile owns a file and releases it after Music.Close joins decoding.

OpenCapture records from a selected input into a ring the game drains with Read. It is separate from the mix: what it records is not played back unless the game plays it. Record retains bounded PCM; RecordPCM and RecordWAV stream to borrowed writers, and RecordWAVFile owns its output file. Stop joins capture and output processing. PCM and Sound expose WriteWAV and SaveWAV for export.

Playback advances only when an output device pulls mixed blocks. Headless runs, Config.NoAudio and unavailable output devices leave playheads stationary; do not use audio completion to drive essential game progress in those modes.

Index

Variables

var ErrCaptureDropped = errors.New("audio: capture dropped samples")

ErrCaptureDropped reports lost input samples. Recording stops rather than returning a silently incomplete recording as a success.

var ErrDeviceUnavailable = audioout.ErrUnavailable

ErrDeviceUnavailable means the requested endpoint could not be opened.

var ErrDeviceUnsupported = audioout.ErrUnsupported

ErrDeviceUnsupported means this operating system or installation has no backend.

var ErrNoDevice = errors.New("audio: this run has no audio device")

ErrNoDevice is returned by OpenCapture when the engine explicitly disabled devices for a headless run or Config.NoAudio. Failure to open an output device alone does not disable an independent capture.

var ErrNotSeekable = errors.New("audio: stream cannot seek")

ErrNotSeekable is returned by Voice.Seek for a stream that cannot seek.

Types

type Attenuation source

type Attenuation struct {
	Model   AttenuationModel
	Rolloff float32
}

Attenuation controls distance gain. Rolloff is nonnegative; zero means 1. All models except None are full volume within MinDistance and silent at MaxDistance. The zero value preserves the engine's original curve.

type AttenuationModel source

type AttenuationModel uint8

AttenuationModel selects distance gain between a voice's distance limits.

const (
	AttenuationDefault     AttenuationModel = iota // inverse distance times linear cutoff (legacy)
	AttenuationNone                                // distance does not affect gain
	AttenuationLinear                              // linear falloff
	AttenuationInverse                             // inverse-distance falloff
	AttenuationExponential                         // power-law falloff
)

type Bus source

type Bus struct {
	// contains filtered or unexported fields
}

Bus groups voices so they can be turned up, down or paused together. Every voice plays through one bus, or straight through the master when PlayOptions.Bus is nil; its gain is master × bus × voice. A mixer starts with three buses, Music, Effects and Dialogue, and games make more with NewBus. Bus methods are safe to call from the game loop.

Example
package main

import (
	"fmt"

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

func main() {
	// Voices play through a bus so a settings screen can turn music down
	// without touching effects, and a pause menu can hold them all.
	m := audio.NewMixer(48000)
	m.Music().SetVolume(0.4)
	footsteps := m.NewBus("footsteps") // buses beyond the three built in
	step, _ := m.NewSound(audio.Sine(120, 0.05, 48000))
	m.Play(step, audio.PlayOptions{Bus: footsteps})
	footsteps.SetVolume(0.6)
	m.SetPaused(true) // the pause menu: everything holds, nothing is lost
	fmt.Println(m.Playing(), footsteps.Volume())
}
Output
1 0.6

Muted source

func (b *Bus) Muted() bool

Muted reports whether the bus is muted.

Name source

func (b *Bus) Name() string

Name is the name the bus was made with.

Paused source

func (b *Bus) Paused() bool

Paused reports whether the bus is paused.

SetMute source

func (b *Bus) SetMute(mute bool)

SetMute silences every voice on the bus while they keep playing, so unmuting resumes wherever the sound has reached. The gain ramps over one block, so it never clicks.

SetPaused source

func (b *Bus) SetPaused(p bool)

SetPaused holds every voice on the bus in place, silent, until resumed; voices started while the bus is paused wait too. The pause fades out over the block it lands in, so it never clicks.

SetReverb source

func (b *Bus) SetReverb(s ReverbSettings)

SetReverb gives the bus a reverb of its own. Voices on the bus send to it instead of the mixer's shared reverb, so a cave's effects can ring while the music stays dry, or the reverse. The zero value removes it. The settings reach the reverb at the start of the next mixed block.

SetSolo source

func (b *Bus) SetSolo(solo bool)

SetSolo solos the bus. While any bus is soloed, every other bus, and every voice on no bus, is silent and keeps playing. Clearing the last solo makes the rest audible again.

SetVolume source

func (b *Bus) SetVolume(v float32)

SetVolume scales every voice on the bus; 1 is unity. The change ramps across the next mixed block, so it never clicks.

Soloed source

func (b *Bus) Soloed() bool

Soloed reports whether the bus is soloed.

Volume source

func (b *Bus) Volume() float32

Volume returns the bus gain.

type Capture source

type Capture struct {
	// contains filtered or unexported fields
}

Capture is a running recording from the input selected by CaptureOptions. An empty DeviceID uses the machine's default input. The device fills a ring buffer from its own thread and the game drains it with Read, which does not wait for samples to arrive. It briefly shares a mutex with the device's buffer writer. Samples that are not read in time are dropped, oldest first, so a game that stops reading falls behind by at most the buffer.

A Capture is not a Stream: to play what is recorded, feed the samples to a Stream of the game's own, or to a Sound built from them.

Buffered source

func (c *Capture) Buffered() int

Buffered is how many recorded samples are waiting to be read.

Channels source

func (c *Capture) Channels() int

Channels is how many channels each frame has; 1 is mono.

Close source

func (c *Capture) Close()

Close stops the stream and releases the device. Samples already buffered can still be read. Closing twice, or from two goroutines, does the work once; the second call waits for the first.

Dropped source

func (c *Capture) Dropped() int64

Dropped counts the samples the device recorded and overwrote because the game did not read them in time. A rising count means either a longer CaptureOptions.Buffer or more frequent reads.

Level source

func (c *Capture) Level() float32

Level is the root mean square of the samples the device delivered most recently, from 0 to 1, which is what a level meter draws. It moves at the device's own block rate, a few hundred times a second, and holds its last value when the stream is closed.

Rate source

func (c *Capture) Rate() int

Rate is the sample rate the stream records at.

Read source

func (c *Capture) Read(dst []float32) int

Read copies at most len(dst) recorded samples into dst and reports how many it copied, which is 0 when nothing has arrived since the last call. It does not wait for new samples, but may briefly wait for the buffer mutex. With more than one channel the samples are interleaved, so a caller that works in frames reads a multiple of Channels.

type CaptureOptions source

type CaptureOptions struct {
	DeviceID string  // empty selects the system default input; see InputDevices
	Rate     int     // samples per second; 0 is the mixer's rate
	Channels int     // 1..32; 0 is 1, mono
	Buffer   float32 // finite positive seconds held before dropping oldest samples; 0 is 0.5
}

CaptureOptions shape a capture stream. Zero values mean the defaults noted.

type Cone source

type Cone struct{ InnerAngle, OuterAngle, OuterGain float32 }

Cone directs a source's sound. Angles are full aperture angles in radians, 0 <= InnerAngle <= OuterAngle <= 2*pi. Gain interpolates from 1 inside the inner cone to OuterGain (0..1) outside the outer cone. The zero value is omnidirectional.

type DeviceInfo source

type DeviceInfo struct {
	ID      string // pass to SetOutputDevice or CaptureOptions.DeviceID
	Name    string // display name; not necessarily unique
	Default bool   // the default endpoint when enumerated
}

DeviceInfo identifies an output or input endpoint. IDs are opaque OS local selection identifiers, not list indices or user identities. They can become unavailable after device, driver or configuration changes. Linux endpoints include configured PCMs.

type Listener source

type Listener struct {
	Position lin.Vec3 // listener position in world units
	Forward  lin.Vec3 // facing direction; zero means (0, 0, -1) in SetListener
	Up       lin.Vec3 // head-up direction; zero means (0, 1, 0) in SetListener
	Velocity lin.Vec3 // world units per second for Doppler
}

Listener is where positional voices are heard from. Forward and Up orient it; the right ear lies along Forward × Up. Velocity, in world units per second, only matters once Doppler is on (see SetDoppler).

type Mixer source

type Mixer struct {
	// contains filtered or unexported fields
}

Mixer sums playing voices into the output stream.

Two locks divide the work. mu guards everything a game can change from its own goroutine, and is held only briefly: a setter takes it, and the mixer takes it twice a block, once to copy out what it is about to mix and once to write back where every voice reached. mixMu is held across the whole block and marks the playback state the mixer owns while it runs, so nothing else may move a voice through its sound mid-block. Streams are read with mu released and mixMu held. Stream.Read may call setters or start voices, but must not seek a voice because Seek also needs mixMu. Stream implementations must avoid lock cycles with callers on other goroutines.

Example
package main

import (
	"fmt"

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

func main() {
	// The engine creates the mixer and the output device pulls frames
	// from it; a game only starts and adjusts voices.
	m := audio.NewMixer(48000)
	beep, _ := m.NewSound(audio.Sine(440, 0.1, 48000))
	v := m.Play(beep, audio.PlayOptions{Volume: 0.5, Pan: -0.5})
	fmt.Println(m.Playing(), v.Playing())
	m.StopAll()
	fmt.Println(m.Playing())
}
Output
1 true
0

NewMixer source

func NewMixer(rate int) *Mixer

NewMixer makes a mixer for a positive output sample rate, with unity master gain and a 64-voice limit. It does not open an output device; the engine supplies and drives Context.Audio for normal games.

Bus source

func (m *Mixer) Bus(name string) *Bus

Bus looks a bus up by name, or returns nil when none has it.

Buses source

func (m *Mixer) Buses() []*Bus

Buses returns the mixer's buses in the order they were made, starting with music, effects and dialogue, for a mixing panel that shows them all.

CloseOutput source

func (m *Mixer) CloseOutput()

CloseOutput stops and releases this mixer's output. Voices remain available for a later SetOutputDevice call. Repeated and concurrent calls are safe. Do not call it from an audio callback; see SetOutputDevice.

Dialogue source

func (m *Mixer) Dialogue() *Bus

Dialogue is the bus for speech, named "dialogue". The name avoids confusion with Voice, which is any playing sound.

Effects source

func (m *Mixer) Effects() *Bus

Effects is the bus for sound effects, named "effects".

InputDevices source

func (m *Mixer) InputDevices() ([]DeviceInfo, error)

InputDevices lists capture endpoints without starting recording or requesting microphone access. Explicit NoAudio and headless runs return ErrNoDevice.

Listener source

func (m *Mixer) Listener() Listener

Listener returns the current listener.

Music source

func (m *Mixer) Music() *Bus

Music is the bus for soundtrack voices, named "music".

NewBus source

func (m *Mixer) NewBus(name string) *Bus

NewBus makes a named bus; the name is how Mixer.Bus finds it again. If a bus with that name already exists, it is returned instead.

NewSound source

func (m *Mixer) NewSound(p PCM) (*Sound, error)

NewSound converts decoded PCM to the mixer's format: stereo at the mixer rate, resampled linearly when the rates differ. It copies p.Samples, which must contain a whole number of mono or stereo frames. Unsupported channel counts or invalid PCM return an error.

OpenCapture source

func (m *Mixer) OpenCapture(opts CaptureOptions) (*Capture, error)

OpenCapture starts recording from DeviceID or the system default input. It returns an error when the run has no audio device, when the operating system has no capture backend, or when the device refuses to open, which is what happens on a machine with no microphone and on macOS when the player has not granted microphone access. Close the Capture when the game is done with it.

The stream is separate from the mixer: what it records is not played back unless the game plays it.

OpenMusic source

func (m *Mixer) OpenMusic(r io.ReadSeeker, loop bool) (*Music, error)

OpenMusic prepares a file for streaming; loop makes it start over at the end. It sniffs the format from the start of r and waits for the first decoded chunk or an error. The reader must support seeking and remain available until Music.Close returns. Music does not close r.

OpenMusicFile source

func (m *Mixer) OpenMusicFile(path string, loop bool) (*Music, error)

OpenMusicFile opens a WAV, Ogg Vorbis, MP3 or FLAC file for streaming. Music owns the file and closes it after the decoder stops. Failure closes the file before returning. Loop restarts playback at the end.

OutputDevice source

func (m *Mixer) OutputDevice() (DeviceInfo, bool)

OutputDevice reports the chosen endpoint and whether this mixer owns an output. Empty ID with Default true means system-default routing; it does not identify the physical destination, which the OS may change. The result is selection state, not a live device-health query.

OutputDevices source

func (m *Mixer) OutputDevices() ([]DeviceInfo, error)

OutputDevices lists available playback endpoints without opening a stream. Explicit NoAudio and headless runs return ErrNoDevice.

Paused source

func (m *Mixer) Paused() bool

Paused reports whether the whole mixer is paused.

Play source

func (m *Mixer) Play(s *Sound, opts PlayOptions) *Voice

Play starts a sound made for this mixer's rate and returns its voice. If all available voices have higher priority, the returned voice is already finished. A nil or empty sound ends when the mixer next reads it.

PlayStream source

func (m *Mixer) PlayStream(s Stream, opts PlayOptions) *Voice

PlayStream starts a stream as a voice. Pitch and Doppler resample its frames continuously across blocks. Loop has no effect: a looping stream loops itself. The voice buffers up to 513 source frames of lookahead. The mixer does not close the stream when the voice ends. The caller owns its lifetime and must not share a stateful stream between voices unless the stream explicitly supports that use.

Playing source

func (m *Mixer) Playing() int

Playing counts active voices. A voice that has been stopped is not counted while its last millisecond ramps out.

Rate source

func (m *Mixer) Rate() int

Rate is the sample rate sounds are stored and mixed at.

Record source

func (m *Mixer) Record(opts RecordOptions) (*Recorder, error)

Record starts an explicit bounded in-memory recording. Reaching MaxDuration is a normal stop; dropped capture samples are an error. The caller owns it.

RecordPCM source

func (m *Mixer) RecordPCM(w io.Writer, opts RecordOptions) (*Recording, error)

RecordPCM streams interleaved signed 16-bit little-endian PCM without a container header. Rate and channels come from opts; the writer is borrowed. The worker can block in Write; its owner must unblock a blocked writer before Stop/Close can finish. Do not call Stop/Close from the writer's callback.

RecordWAV source

func (m *Mixer) RecordWAV(w io.WriteSeeker, opts RecordOptions) (*Recording, error)

RecordWAV streams 16-bit PCM WAV at the borrowed writer's current offset. Stop finalizes the header and leaves the writer positioned after the WAV. The writer stays open. Its seek/write calls must not call Stop or Close. The configured maximum must fit RIFF's 32-bit size limit.

RecordWAVFile source

func (m *Mixer) RecordWAVFile(path string, opts RecordOptions) (*Recording, error)

RecordWAVFile creates or replaces path and records 16-bit PCM WAV to it. It owns the file, finalizing and closing it on stop, including error paths.

Reverb source

func (m *Mixer) Reverb() ReverbSettings

Reverb reports the settings the shared reverb is using now: those given to SetReverb, or the zone the listener is in blended by how far inside it stands. Wet is 0 when the reverb is off.

SetDoppler source

func (m *Mixer) SetDoppler(factor float32)

SetDoppler turns the Doppler effect on for positional sounds: a source closing on the listener plays sharp, one receding plays flat, by how fast each moves along the line between them. The factor scales the effect; 1 is physical, 0 (the default) is off. Speeds come from Listener.Velocity and Voice.SetVelocity, in world units per second, against the speed of sound (see SetSpeedOfSound). Streams have no pitch, so Doppler leaves them alone.

SetListener source

func (m *Mixer) SetListener(l Listener)

SetListener places the listener, usually at the camera each frame. Moving it also decides which ReverbZone, if any, the listener is in.

SetListener2D source

func (m *Mixer) SetListener2D(x, y float32)

SetListener2D places the listener for a 2D game whose y axis points down the screen: sounds to the right on screen come from the right.

Example
package main

import (
	"fmt"

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

func main() {
	// Positional voices pan and fade by where they are relative to the
	// listener; in a 2D game put the listener at the camera each frame.
	m := audio.NewMixer(48000)
	m.SetListener2D(400, 300)
	tone, _ := m.NewSound(audio.Sine(330, 1, 48000))
	v := m.Play(tone, audio.PlayOptions{Loop: true, Positional: true, Position: lin.V3(600, 300, 0), MaxDistance: 800})
	v.SetPosition(lin.V3(200, 300, 0)) // now to the left
	fmt.Println(v.Playing())
}
Output
true

SetMasterVolume source

func (m *Mixer) SetMasterVolume(v float32)

SetMasterVolume scales every voice; 1 is unity.

SetMaxVoices source

func (m *Mixer) SetMaxVoices(n int)

SetMaxVoices caps how many voices play at once (default 64). When the mixer is full, a new voice takes the place of the quietest voice of the lowest priority no higher than its own, or is refused. The stolen voice leaves the count at once and ramps out over the next millisecond, so the mixer may briefly render one more voice than the cap.

SetOutputDevice source

func (m *Mixer) SetOutputDevice(id string) error

SetOutputDevice opens id and switches this mixer to it. Empty selects the system default routing endpoint. Failure leaves the previous output active. A successful switch can briefly produce silence; voices keep their positions. NewMixer stays device-free until this is explicitly called. The engine opens the default itself, except in NoAudio/headless runs, which return ErrNoDevice. Calls serialize with CloseOutput. Do not call either from Stream.Read or an audio callback (including OnDone): switching waits for device callbacks to end.

SetPaused source

func (m *Mixer) SetPaused(p bool)

SetPaused holds every voice on every bus, for when the game loses focus or a pause menu opens; the engine calls it, so a game rarely has to. The block in which the pause lands fades out and the block after the resume fades back in, so neither clicks. Bus and voice pauses are kept separately, so resuming the mixer leaves them as they were.

SetReverb source

func (m *Mixer) SetReverb(s ReverbSettings)

SetReverb configures the mixer's shared reverb: the room the listener is in when no ReverbZone says otherwise. Voices with a Reverb send add their signal to it (unless their bus has a reverb of its own); the tail is mixed on top of the dry output. The zero value turns it off.

Example
package main

import (
	"fmt"

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

func main() {
	m := audio.NewMixer(48000)
	m.SetReverb(audio.ReverbSettings{RoomSize: 0.7, Wet: 0.3})
	hit, _ := m.NewSound(audio.Sine(200, 0.05, 48000))
	m.Play(hit, audio.PlayOptions{Reverb: 1, LowPass: 2000, Pitch: 0.9})
	fmt.Println(m.Playing())
}
Output
1

SetReverbZones source

func (m *Mixer) SetReverbZones(zones []ReverbZone)

SetReverbZones replaces the set of reverb zones. The mixer checks them against the listener whenever it moves, so a game sets them once per level and moves the listener each frame. Pass nil to clear them.

SetSpatial source

func (m *Mixer) SetSpatial(s SpatialSettings)

SetSpatial chooses how positional voices are placed between the ears. The zero value restores the constant-power pan law, which is the default. The change takes effect at the start of the next mixed block and every parameter is interpolated across it, so switching mode or moving the listener while sounds play does not click.

SetSpeedOfSound source

func (m *Mixer) SetSpeedOfSound(c float32)

SetSpeedOfSound sets the speed Doppler works against, in the game's world units per second. The default is 343, right for metres; a game in pixels or larger units raises it to keep the effect subtle.

Spatial source

func (m *Mixer) Spatial() SpatialSettings

Spatial reports the spatial settings, as given to SetSpatial.

StopAll source

func (m *Mixer) StopAll()

StopAll silences every voice. Each one frees its slot at once and ramps out over the next millisecond, so nothing clicks.

Voices source

func (m *Mixer) Voices() []VoiceInfo

Voices returns a snapshot of the voices retained by the mixer, in the order the mixer holds them, for a debug view or a test. The values are copies: changing them changes nothing, and a voice may end before the caller reads them. Stopped voices can remain here until the next blocks retire their ramps, so the length need not equal Playing. Settings are copied under the settings lock; positions are read afterward from the last mixed block and need not represent the exact same instant.

type Music source

type Music struct {
	// contains filtered or unexported fields
}

Music plays WAV, Ogg Vorbis, MP3 or FLAC through a two-second PCM buffer filled on a decoder goroutine. Ogg, MP3 and FLAC decode incrementally; WAV is decoded completely at open and retained in memory. Open one with Mixer.OpenMusicFile or Mixer.OpenMusic, start it with Mixer.PlayStream, and Close it when the game is done with it.

Buffered source

func (mu *Music) Buffered() float64

Buffered reports how many seconds are decoded and waiting to play.

Close source

func (mu *Music) Close()

Close stops and joins the decoder, then closes an owned file. Subsequent Read calls return zero; a voice ends when it next reads. Repeated and concurrent calls are safe. Readers supplied to OpenMusic remain borrowed and may be closed after this returns. Close waits for an in-flight Read or Seek, which a custom reader must unblock itself. Do not call Close from that reader or decoder, including its callbacks.

Duration source

func (mu *Music) Duration() float64

Duration is the track's length in seconds, or 0 when unknown. WAV always knows it. Ogg Vorbis reads it from the last page at open; MP3 scans the frame headers at open. Either reports 0 when its reader could not seek to find out. FLAC uses STREAMINFO's sample count, which may also be unknown (0).

Err source

func (mu *Music) Err() error

Err reports a decoding error, if one ended the music early.

LoopRange source

func (mu *Music) LoopRange() (start, end time.Duration)

LoopRange returns configured, source-frame-aligned boundaries; (0,0) means the whole track. Values round up to a nanosecond so passing the result back to SetLoopRange preserves source-frame boundaries.

Looping source

func (mu *Music) Looping() bool

Looping reports whether Music repeats. This is independent of whether any Voice is currently playing the music.

Read source

func (mu *Music) Read(out []float32) int

Read implements Stream for the mixer: it hands over buffered frames, pads a momentary shortfall with silence, and returns short only once the music has ended.

Seek source

func (mu *Music) Seek(seconds float64) error

Seek moves playback to seconds from the start. It returns at once and the decoder catches up on its goroutine, so a few milliseconds of silence can precede the new position; anything buffered from the old position is dropped. Seeking past the end ends the music, or starts it over when it loops. A decoder that fails to seek ends the music and reports through Err. WAV and Ogg Vorbis seek exactly; MP3 decodes from the previous frame boundary. FLAC seeks exactly by scanning frames, rewinding when necessary, with memory bounded to one decoded frame. Prefer Voice.Seek on a playing voice so its Position follows. Music whose voice has already ended can be sought and played again with PlayStream.

SetLoopRange source

func (mu *Music) SetLoopRange(start, end time.Duration) error

SetLoopRange selects [start,end) and restarts decoding at start, flushing prefetched audio. (0,0) restores the whole track. Nonzero ranges must contain at least one source frame and fit a known duration; unknown-length music accepts a finite range and also wraps at early EOF. Boundaries round down to source frames. The range repeats only while Looping; without looping playback continues from start to the file's end.

SetLooping source

func (mu *Music) SetLooping(loop bool)

SetLooping changes repetition and flushes prefetched samples at the next playback position, accounting for voice lookahead. Enabling it after all samples reach EOF restarts at the loop start; a Voice that already stopped must be played again. Closed or failed music is unchanged. The next read may briefly underrun.

type PCM source

type PCM struct {
	Samples  []float32 // interleaved channel samples, nominally -1..1
	Channels int       // samples per frame; NewSound accepts 1 or 2
	Rate     int       // frames per second; must be positive
}

PCM is decoded audio of any channel count and rate, interleaved.

Decode source

func Decode(data []byte) (PCM, error)

Decode picks a sampled-audio decoder (WAV, Ogg Vorbis, MP3, FLAC) from the data's magic bytes. Tracker modules are music, not clips; see PlayModule.

DecodeFLAC source

func DecodeFLAC(data []byte) (pcm PCM, err error)

DecodeFLAC decodes a native FLAC stream into interleaved float32 PCM. It supports 1..8 channels and 4..24-bit integer samples. Sound and Music playback accept mono or stereo. Frame CRCs are checked; the optional whole-stream MD5 is not verified. Ogg-encapsulated FLAC is not supported.

DecodeMP3 source

func DecodeMP3(data []byte) (pcm PCM, err error)

DecodeMP3 decodes a whole MP3 file into memory as stereo PCM.

Encode game audio at 44.1 or 48 kHz: the decoder reproduces MPEG-2 low-sample-rate files (22.05 kHz and below) with an uneven level.

DecodeOGG source

func DecodeOGG(data []byte) (pcm PCM, err error)

DecodeOGG decodes a whole Ogg Vorbis file into memory.

DecodeWAV source

func DecodeWAV(data []byte) (PCM, error)

DecodeWAV reads PCM (8/16/24/32-bit integer or 32-bit float) WAV data.

Sine source

func Sine(freq float64, seconds float64, rate int) PCM

Sine synthesises a tone, handy for tests and placeholder effects.

SaveWAV source

func (p PCM) SaveWAV(path string) (err error)

SaveWAV creates or replaces path with 16-bit PCM WAV and closes the file.

WriteWAV source

func (p PCM) WriteWAV(w io.Writer) error

WriteWAV writes interleaved signed 16-bit PCM WAV to a borrowed writer. Samples are rounded and clamped to [-1,1]; non-finite samples and invalid PCM are rejected before writing. RIFF files must fit the 32-bit size limit. The writer is never closed.

type PlayOptions source

type PlayOptions struct {
	Volume   float32 // 1
	Pan      float32 // -1 left .. +1 right; ignored when Positional
	Loop     bool    // repeat a Sound; streams control their own looping
	Pitch    float32 // playback rate for sounds and streams; default 1, clamped to 0.01..64
	Priority int     // higher survives when the mixer is full
	FadeIn   float32 // seconds to rise from silence
	Reverb   float32 // 0..1 send into the bus's reverb, or the mixer's (see SetReverb)
	LowPass  float32 // low-pass cutoff in Hz; 0 leaves the sound unfiltered
	Bus      *Bus    // bus the voice plays through; nil is the master alone

	// Occlusion is how much of the world is between the source and the
	// listener: 0 clear, 1 fully blocked, which drops the voice 20 dB and
	// muffles it to 400 Hz. A game sets it from a physics ray.
	Occlusion float32

	// Positional voices are heard from Position relative to the listener:
	// full volume within MinDistance (1), fading to silence at
	// MaxDistance (100), panned by direction. Velocity, in world units per
	// second, drives Doppler once SetDoppler turns it on.
	Positional         bool
	Position           lin.Vec3    // source position in listener world units
	Velocity           lin.Vec3    // source velocity in world units per second
	MinDistance        float32     // full-volume radius; zero means 1
	MaxDistance        float32     // silent at this distance; zero means 100
	RelativeToListener bool        // listener-local position/direction/velocity; also enables Positional
	Direction          lin.Vec3    // cone direction; nonzero enables Positional; zero or invalid means (0,0,-1)
	Cone               Cone        // nonzero enables Positional; zero is omnidirectional; invalid values use the default
	Attenuation        Attenuation // nonzero enables Positional; zero preserves the original curve; invalid values use the default
}

PlayOptions shape a voice at start. Zero values mean the defaults noted.

type PlaybackState source

type PlaybackState uint8

PlaybackState is a voice's effective playback state, including pauses applied by its bus or mixer. Muting does not pause playback.

const (
	PlaybackStopped PlaybackState = iota // ended or explicitly stopped
	PlaybackPlaying                      // advancing, even while muted or inaudible
	PlaybackPaused                       // held by the voice, its bus or the mixer
)

String source

func (s PlaybackState) String() string

String returns "stopped", "playing", "paused", or "unknown".

type RecordOptions source

type RecordOptions struct {
	Capture     CaptureOptions
	MaxDuration time.Duration // positive wall-clock and source-frame limit; zero means 30 seconds
}

RecordOptions bounds an explicit recording. Zero Capture values use the mixer's rate, mono, default input and a half-second capture ring.

type Recorder source

type Recorder struct {
	// contains filtered or unexported fields
}

Recorder owns a capture stream and worker retaining a bounded PCM recording. It stops automatically at MaxDuration. Stop or Close joins the worker.

Close source

func (r *Recorder) Close() error

Close joins the recording. PCM remains available from later Stop calls.

Done source

func (r *Recorder) Done() <-chan struct{}

Done closes after capture, output finalization and the worker have finished.

Err source

func (r *Recorder) Err() error

Err reports the terminal recording error after Done closes; before then nil.

Stop source

func (r *Recorder) Stop() (PCM, error)

Stop stops capture, drains accepted buffered frames up to MaxDuration, and joins the worker. It returns an independent PCM copy, including partial data on error. Repeated calls return equivalent independent copies.

type Recording source

type Recording struct {
	// contains filtered or unexported fields
}

Recording owns a capture stream and worker writing to an output. It retains only capture and conversion buffers, not the whole recording.

Close source

func (r *Recording) Close() error

Close is equivalent to Stop and satisfies io.Closer.

Done source

func (r *Recording) Done() <-chan struct{}

Done closes when capture and output processing have finished.

Err source

func (r *Recording) Err() error

Err reports the terminal recording error after Done closes; before then nil.

Stop source

func (r *Recording) Stop() error

Stop stops capture, drains accepted frames within the limit and joins the output worker, including WAV finalization and owned-file closure.

type ReverbSettings source

type ReverbSettings struct {
	RoomSize float32 // 0..1, how long the tail rings
	Damping  float32 // 0..1, how quickly highs die away
	Width    float32 // 0..1, stereo spread
	Wet      float32 // 0..1, level of the reverb in the mix
}

ReverbSettings describe a reverb, which voices feed through their Reverb send. Zero RoomSize, Damping and Width take the defaults 0.5, 0.5 and 1; a zero Wet turns the reverb off, so the zero value is no reverb. Every field runs from 0 to 1 and values outside that are clamped when the reverb uses them, because a room larger than 1 has feedback that never decays.

type ReverbZone source

type ReverbZone struct {
	Center   lin.Vec3       // sphere centre in listener world units
	Radius   float32        // sphere radius; nonpositive zones are ignored
	Fade     float32        // inward blend distance; nonpositive means Radius
	Settings ReverbSettings // reverb at full zone strength
}

ReverbZone is a region of the world with its own reverb: a cave, a hall, a tunnel. While the listener is inside the sphere the zone's Settings replace the mixer's shared reverb, blended in over Fade units from the edge so walking in never jumps. A zero Fade blends across the whole radius, so the zone is at full strength only at its centre; set Fade to a fraction of Radius for a room that sounds the same everywhere but its doorway. Where zones overlap, the one the listener is furthest inside wins.

type Seeker source

type Seeker interface {
	Stream
	// Seek moves playback to seconds from the start.
	Seek(seconds float64) error
}

Seeker is a Stream that can jump to a time, as Music can.

type Sound source

type Sound struct {
	// contains filtered or unexported fields
}

Sound is a PCM clip converted to the mixer's stereo rate.

Duration source

func (s *Sound) Duration() float64

Duration is the sound's length in seconds.

Frames source

func (s *Sound) Frames() int

Frames is the sound's length in frames.

SaveWAV source

func (s *Sound) SaveWAV(path string) error

SaveWAV creates or replaces path with this sound's stereo PCM WAV.

WriteWAV source

func (s *Sound) WriteWAV(w io.Writer) error

WriteWAV exports this sound at its mixer rate as stereo 16-bit PCM WAV. The writer remains borrowed; see PCM.WriteWAV.

type SpatialSettings source

type SpatialSettings struct {
	// Binaural renders each positional voice through a head model
	// instead: an interaural time difference, an interaural level
	// difference with head shadow, and an elevation cue. It suits
	// headphones; on speakers the two ears mix and the cues weaken.
	Binaural bool

	// HeadRadius is the modelled head's radius in metres, which sets how
	// far apart the ears are and so how large the time difference grows.
	// A zero radius means 0.0875, an average adult head; values above 0.3
	// are clamped.
	HeadRadius float32
}

SpatialSettings choose how a positional voice is placed between the listener's ears. The zero value pans by direction with a constant-power law, which is what a game gets without calling SetSpatial.

type Stream source

type Stream interface {
	// Read fills out with len(out)/2 frames and reports how many frames it
	// wrote, between zero and len(out)/2; fewer than asked means the stream
	// has ended. For a temporary underrun, fill the remainder with silence
	// and report the full frame count to keep the voice alive.
	Read(out []float32) int
}

Stream produces stereo float32 frames on demand at the mixer's rate; it is how music and synthesised sound play without being decoded up front. Read runs on the mixing thread and must return promptly. It may call mixer setters or Play, but must not call Voice.Seek. Do not retain out.

type Voice source

type Voice struct {
	// contains filtered or unexported fields
}

Voice is one playing sound or stream.

Attenuation source

func (v *Voice) Attenuation() Attenuation

Attenuation returns the effective distance model and rolloff.

Cone source

func (v *Voice) Cone() Cone

Cone returns the effective cone; the default has full-circle angles and unity gain.

Direction source

func (v *Voice) Direction() lin.Vec3

Direction returns the normalized cone direction in the selected coordinate space.

DistanceRange source

func (v *Voice) DistanceRange() (minDistance, maxDistance float32)

DistanceRange returns the full-volume and silence distances.

FadeOut source

func (v *Voice) FadeOut(seconds float32)

FadeOut fades to silence over seconds and then stops the voice. The stop ramps out whatever level the last block of the fade reached, so a fade shorter than one block does not click.

FadeTo source

func (v *Voice) FadeTo(vol, seconds float32)

FadeTo moves the volume to vol over seconds.

Muted source

func (v *Voice) Muted() bool

Muted reports whether the voice is muted.

Occlusion source

func (v *Voice) Occlusion() float32

Occlusion reports the voice's occlusion amount.

OnDone source

func (v *Voice) OnDone(fn func())

OnDone registers fn to run when the voice ends, whether it played out, was stopped, faded out, or lost its slot to a higher priority voice. A voice that played out or was stopped with Voice.Stop calls fn on the mixer's thread, usually the audio device's, after the mixer has released its lock; StopAll and a stolen slot call it on the calling goroutine, before the millisecond of ramp that follows is mixed. A voice that has already ended runs fn at once on the calling goroutine. fn may start another voice, but it must return quickly and must not block. Only the last fn registered runs.

Example
package main

import (
	"fmt"

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

func main() {
	m := audio.NewMixer(48000)
	beep, _ := m.NewSound(audio.Sine(440, 0.01, 48000))
	v := m.Play(beep, audio.PlayOptions{})
	v.OnDone(func() { fmt.Println("done") }) // when it plays out, or is stopped
	m.StopAll()
}
Output
done

Playing source

func (v *Voice) Playing() bool

Playing reports whether the voice is active, including while paused, muted or outside its audible range. A stopped voice reports false as soon as it is stopped, while its last millisecond ramps out.

Position source

func (v *Voice) Position() float64

Position is how far into the sound the voice is, in seconds. For a stream it counts source-rate time since the voice started or last sought, including pitch, Doppler and underrun silence, without wrapping at stream loop boundaries. Direct Music controls do not reset it. It reads the position the last finished block reached, so it takes no lock and never waits on the mixer.

RelativeToListener source

func (v *Voice) RelativeToListener() bool

RelativeToListener reports whether source coordinates follow the listener.

Seek source

func (v *Voice) Seek(seconds float64) error

Seek moves a sound voice to seconds from the start, clamped to the sound's length. A stream voice is moved when its stream is a Seeker; otherwise it stays put and ErrNotSeekable is returned. Seeking waits for the block being mixed to finish, because it moves the position the mixer is reading from, so it may block for the length of one block; a Seeker must not recursively call Voice.Seek. Do not call Seek from Stream.Read, which runs under the same playback lock.

SetAttenuation source

func (v *Voice) SetAttenuation(attenuation Attenuation) error

SetAttenuation enables positional audio and changes the distance model. Invalid values leave it unchanged.

SetCone source

func (v *Voice) SetCone(cone Cone) error

SetCone enables positional audio and changes directionality. Invalid values leave it unchanged.

SetDirection source

func (v *Voice) SetDirection(direction lin.Vec3) error

SetDirection enables positional audio and sets a nonzero finite direction. It is in world coordinates, or listener-local when relative mode is on.

SetDistanceRange source

func (v *Voice) SetDistanceRange(minDistance, maxDistance float32) error

SetDistanceRange enables positional audio with finite limits 0 < min < max.

SetLowPass source

func (v *Voice) SetLowPass(cutoff float32)

SetLowPass sets the low-pass cutoff in Hz; 0 removes the filter.

SetMute source

func (v *Voice) SetMute(mute bool)

SetMute silences the voice while it keeps playing, so unmuting resumes wherever the sound has reached. To stop the sound advancing, use SetPaused instead.

SetOcclusion source

func (v *Voice) SetOcclusion(o float32)

SetOcclusion sets how blocked the path from the source is: 0 clear, 1 fully blocked (20 dB down and muffled to 400 Hz), in between for a half-open door. A game sets it from a physics ray each frame; the gain ramps, so the change never clicks.

SetPan source

func (v *Voice) SetPan(pan float32)

SetPan moves the voice between -1 (left) and +1 (right).

SetPaused source

func (v *Voice) SetPaused(p bool)

SetPaused holds the voice in place, silent, until resumed. The block the pause lands in fades out, so it never clicks.

SetPitch source

func (v *Voice) SetPitch(p float32)

SetPitch changes playback rate; 2 plays an octave up at double speed. Values are clamped to 0.01..64; zero or nonfinite values restore 1.

SetPosition source

func (v *Voice) SetPosition(p lin.Vec3)

SetPosition moves a positional voice.

SetRelativeToListener source

func (v *Voice) SetRelativeToListener(relative bool)

SetRelativeToListener selects listener-local source coordinates: +X is right, +Y up, -Z forward. Position, direction and velocity use this basis; listener translation/velocity are added. This enables positional audio.

SetReverb source

func (v *Voice) SetReverb(send float32)

SetReverb changes the send level into the bus's reverb, or the mixer's.

SetSolo source

func (v *Voice) SetSolo(solo bool)

SetSolo solos the voice. While any voice is soloed, every voice that is not soloed is silent and keeps playing. Clearing the last solo makes them audible again. Bus solos are separate and combine with it.

SetVelocity source

func (v *Voice) SetVelocity(vel lin.Vec3)

SetVelocity sets a positional voice's velocity in world units per second, for Doppler. It only changes the pitch; the game moves the voice with SetPosition.

SetVolume source

func (v *Voice) SetVolume(vol float32)

SetVolume changes the voice's gain; 1 is unity.

Soloed source

func (v *Voice) Soloed() bool

Soloed reports whether the voice is soloed.

State source

func (v *Voice) State() PlaybackState

State reports the effective state. A pause is reported immediately, including the block that fades out; Stop reports stopped during its ramp.

Stop source

func (v *Voice) Stop()

Stop ends the voice. The gain ramps to silence over about a millisecond first, so stopping mid-cycle never clicks; the voice frees its slot at once and OnDone runs when the ramp finishes.

type VoiceInfo source

type VoiceInfo struct {
	// Bus is the name of the bus the voice plays through, empty when it
	// plays straight through the master.
	Bus string
	// Volume, Pan and Pitch are the voice's own settings, before the bus
	// and master gains.
	Volume, Pan, Pitch float32
	// Seconds is how far into the sound or stream the voice has played.
	Seconds float64
	// Stream is set for a voice playing a stream (music, a tracker
	// module) rather than a decoded sound.
	Stream bool
	// Positional voices are heard from Position in the listener's world;
	// the rest are panned.
	Positional bool
	Position   lin.Vec3 // source position in listener world units

	Loop      bool    // voice loop option; stream looping is controlled separately
	Paused    bool    // voice pause only, excluding bus and mixer pauses
	Muted     bool    // voice mute only, excluding bus mute and solo suppression
	Soloed    bool    // voice solo flag
	Priority  int     // priority used when choosing a voice to steal
	Reverb    float32 // send level into the bus or shared reverb
	Occlusion float32 // obstruction amount from 0 (clear) to 1 (blocked)
}

VoiceInfo is one playing voice as a debug view sees it: a snapshot, taken under the mixer's lock, of what the voice is doing now.

Source files

alloc_test.go audio_test.go bench_test.go binaural.go binaural_test.go bus.go bus_test.go capture.go capture_test.go decode.go denormal_test.go device.go device_test.go effects.go example_test.go flac.go flac_test.go fuzz_test.go golden_test.go hook.go inspect.go milestone5_test.go mixer.go mono_test.go mp3.go music.go music_close_test.go music_loop_test.go music_range_test.go perf_bench_test.go race_test.go record.go record_test.go reverb_test.go scene_test.go sound.go source_spatial.go source_spatial_test.go spatial.go stop_test.go stream.go stream_copy_test.go stream_pitch_test.go stream_rate.go voice_state.go wav.go