Help us shape the future of Go! Take the 2026 Go Developer Survey

audio

package
v1.0.0 Latest Latest
Warning

This package is not in the latest version of its module.

Go to latest
Published: Oct 14, 2026 License: MIT Imports: 10 Imported by: 0

Documentation

Overview

Package audio is the acoustic I/O seam for murmur.

Everything above this package deals in exactly one thing: mono float32 PCM at a fixed sample rate, samples in [-1, 1]. Nothing above this package knows whether the bytes came from a sound server, a WAV file, or a simulator.

The acoustic channel is half-duplex: the link layer never calls Play and Capture concurrently. Backends are nonetheless encouraged to keep both directions open for the whole session, because re-opening a stream costs tens of milliseconds and restarts the sample clock, which shows up as an unexplained timing step in the middle of a link.

Index

Constants

View Source
const (
	// DefaultSampleRate is the rate murmur prefers end to end. Laptop hardware
	// is overwhelmingly 48 kHz native; asking for anything else invites the
	// sound server to resample, which both costs latency and makes the two ends'
	// clocks disagree for reasons that are invisible from inside the process.
	DefaultSampleRate = 48000

	// DefaultChannels is mono. Stereo buys nothing acoustically for a single
	// speaker and doubles every buffer for no gain.
	DefaultChannels = 1
)
View Source
const (
	// KindNull is a device that hears nothing and says nothing. It exists so the
	// protocol can be exercised without hardware.
	KindNull = "null"
	// KindWav is a file-backed device: playback is written to a WAV, capture reads
	// one back. It is how a transfer is inspected after the fact.
	KindWav = "wav"
	// KindLoopback is a pair of cross-wired channels that exist only inside this
	// process: what one end plays, the other end captures, immediately and
	// perfectly.
	//
	// Consecutive Opens hand out the two ends of one pair - the first is the
	// receiver's, the second the sender's - and the third starts a new pair. That
	// is the whole mechanism, and it is what lets a self test run a sender and a
	// receiver in one process with no sound server, and then run the other protocol
	// on a fresh pair instead of on the wreckage of the first.
	//
	// It never crosses a process boundary and carries no sound at all, so it proves
	// the software and says nothing about a room. A self test that is supposed to
	// say something about the room has to use a real recorder and speaker.
	KindLoopback = "loopback"
)

The device kinds, as spelled on the command line and in the library.

They are named here rather than in the CLI because both the command line and a caller embedding murmur need the same vocabulary, and a device kind that only one of them understood would be a bug waiting to happen.

None of them is a sound server, and that is deliberate. murmur's acoustic seam is PCM: a program that wants a real microphone wires a recorder to Decode, and one that wants a speaker wires a player to Encode. The backends here exist so that the protocol can be exercised without either - in memory or from a file - which is also why the test suite needs no hardware.

View Source
const CaptureHighPassHz = 300

CaptureHighPassHz is the cutoff of the high-pass murmur applies to everything it captures from a microphone.

The lowest tone any murmur profile uses is 1200 Hz, so nothing it wants lives below this, and a great deal of what a laptop microphone hears does: fan and disk whir, handling noise, desk thumps, HVAC, and the sub-100 Hz rumble that dominates a raw ALSA capture. That rumble is not a cosmetic problem, it is the difference between a link that works and one that does not, because the sync detector normalises its correlation by the energy of the 8 ms window it is looking in: low-frequency energy inside that window adds to the denominator and nothing to the numerator, and it scales with the square of the amplitude, so a rumble 13 dB louder than the signal costs far more than 13 dB of score.

Measured on the machine this was written on, over a whisper-level (-30 dBFS) playback recorded through the built-in microphone:

reference chirp 1400-4800 Hz   file only 0.963   room raw 0.325   room high-passed 0.643
reference chirp 1200-2400 Hz   file only 0.750   room raw 0.574   room high-passed 0.669

0.325 is below SyncThreshold 0.35, which is exactly the failure that was observed: nothing decoded, on a channel whose 1.4-4.8 kHz band carried the signal at about 20 dB above its own floor. 0.643 is not marginal.

The cutoff is deliberately far below the working band: a second-order Butterworth at 300 Hz attenuates 1200 Hz by well under a decibel, so this cannot cost anything the link wants. It also makes the level and clipping measurements honest, since a peak of 1.0 caused by rumble is not a clipped microphone.

Variables

View Source
var (
	// ErrClosed is returned by any operation on a closed Device.
	ErrClosed = errors.New("audio: device closed")
	// ErrUnsupported is returned when a backend cannot provide a capability,
	// for example Discard on a file-backed device.
	ErrUnsupported = errors.New("audio: unsupported by backend")
	// ErrNotImplemented marks scaffolded backends that are declared but not yet
	// written. Callers should surface it to the user verbatim.
	ErrNotImplemented = errors.New("audio: not implemented")
)
View Source
var ErrNotSeekable = errors.New("audio: this destination cannot hold a WAV header")

ErrNotSeekable means a WAV header cannot be written to this destination.

A RIFF file states the length of its data in its first 44 bytes, so writing one to a pipe means either lying about the length up front or rewinding to fix it afterwards. murmur refuses to guess: the honest options are a file (where the length is known once it is written) or raw samples, which need no header because the player was told the format.

View Source
var ErrNotWAV = errors.New("audio: not a WAV file")

ErrNotWAV is returned when a file is not a RIFF/WAVE container.

View Source
var ErrUnknownDevice = errors.New("audio: unknown device")

ErrUnknownDevice means a device kind this build does not have.

Functions

func EncodeWAV

func EncodeWAV(w io.Writer, f Format, pcm []float32) error

EncodeWAV writes pcm as a canonical 16-bit PCM RIFF/WAVE stream. 16-bit is the format every consumer understands and the same depth murmur's PCM seam is defined in, so a captured WAV round-trips unchanged.

func FromInt16

func FromInt16(dst []float32, src []byte) int

FromInt16 converts little-endian signed 16-bit PCM into dst, returning the number of samples written.

func Identity

func Identity(src []float32) []float32

Identity is a Transformer that does nothing.

func Int16Bytes

func Int16Bytes(n int) int

Int16Bytes is the buffer size needed to hold n samples as int16 PCM.

func NewLoopback

func NewLoopback(f Format, leftXform, rightXform Transformer) (*Loopback, *Loopback)

NewLoopback returns two cross-wired Devices. left.Play feeds right.Capture and vice versa. Either xform may be nil, meaning audio.Transformer Identity.

func ToInt16

func ToInt16(dst []byte, src []float32)

ToInt16 writes src as little-endian signed 16-bit PCM into dst. len(dst) must be at least 2*len(src). Values are clamped to [-1, 1] rather than wrapped.

Both directions use the same 32767 scale factor. The common asymmetric variant (32768 for negative values, 32767 for positive) is one code more precise but introduces a level-dependent round-trip error of up to s/32768, which is a systematic gain error on every sample - harmless for audio, and exactly the kind of thing that biases a correlation-based synchroniser.

func WriteWAVFile

func WriteWAVFile(path string, f Format, pcm []float32) error

WriteWAVFile encodes pcm to path atomically (temp file + rename), so a recording is either complete or absent, never half-written.

Types

type Device

type Device interface {
	Format() Format
	Play(ctx context.Context, pcm []float32) error
	Capture(ctx context.Context, dst []float32) (int, error)
	Drain(ctx context.Context) error
	Discard(ctx context.Context) error
	Latency() Latency
	Close() error
}

Device is a half-duplex-capable mono PCM endpoint. Contract:

  • Play blocks until the backend has accepted all of pcm. It does not imply the audio has left the speaker; call Drain for that.
  • Capture blocks until dst is full or ctx is done, returning the number of samples actually read. A short read with a nil error means the stream ended.
  • Drain blocks until every sample previously passed to Play has physically left the speaker. The link layer waits for Drain before it starts listening, otherwise the receiver decodes its own transmission.
  • Discard throws away any samples waiting to be read. It is how the link layer clears the reverb tail and the sound-server buffer out of the capture path after a turnaround.
  • Close releases resources and is idempotent.

func Open

func Open(kind string) (Device, error)

Open returns the device for a kind name. An empty name is the default, which is the in-process loopback: murmur has no acoustic backend of its own, so the default is the one that needs no hardware.

type Format

type Format struct {
	SampleRate int // Hz, e.g. 48000
	Channels   int // 1 = mono
}

Format describes the PCM stream layout a Device consumes and produces.

func DefaultFormat

func DefaultFormat() Format

DefaultFormat is the format murmur asks every backend for.

func (Format) Duration

func (f Format) Duration(samples int) time.Duration

Duration is the sample count expressed as wall-clock time.

func (Format) SampleCount

func (f Format) SampleCount(d time.Duration) int

SampleCount is the sample count corresponding to a wall-clock duration, rounded to the nearest whole sample.

func (Format) String

func (f Format) String() string

func (Format) Validate

func (f Format) Validate() error

Validate reports whether the format is one murmur can work with.

type HighPass

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

HighPass is the capture high-pass as a stream: one filter section with state that carries across blocks.

It is exported because the filter is a property of capture, not of any one recorder. A receiver that gets its samples from anywhere - a raw PCM pipe, a saved recording - must apply the same filter, or the sync detector sees sub-300 Hz rumble that a filtered capture never would. See CaptureHighPassHz for why that is the difference between a link that works and one that does not.

func NewHighPass

func NewHighPass(sampleRate int) *HighPass

NewHighPass returns a high-pass for a sample rate.

func (*HighPass) Process

func (h *HighPass) Process(pcm []float32)

Process filters pcm in place, carrying state across calls.

type Latency

type Latency struct {
	Play    time.Duration
	Capture time.Duration
}

Latency reports the buffering a backend adds in each direction. These numbers dominate half-duplex turnaround cost, so they are surfaced rather than hidden: the link layer measures turnaround at runtime and sizes its windows from that measurement, never from a constant.

type LevelMeter

type LevelMeter struct {
	Peak    float32
	SumSq   float64
	Samples int64
}

LevelMeter accumulates simple statistics about a float32 stream. It is the seam that has to exist before anything can enforce the amplitude ceiling or report a perceived loudness, both of which are policy, not transport.

func (*LevelMeter) Observe

func (m *LevelMeter) Observe(pcm []float32)

Observe folds a block of samples into the meter.

func (*LevelMeter) RMS

func (m *LevelMeter) RMS() float32

RMS returns the root-mean-square level of everything observed so far.

func (*LevelMeter) Reset

func (m *LevelMeter) Reset()

Reset zeroes the meter.

type Loopback

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

Loopback is one end of a cross-wired pair of Devices: whatever is played on one side is captured by the other.

This is the workhorse of hardware-free testing. A test can build a pair, hang a pkg/sim channel off either direction to model reverb and noise, and then run the entire stack — modulation, framing, ARQ, file transfer — with no speaker, no microphone and no sound server. The only thing that is not real is physics.

func (*Loopback) Capture

func (d *Loopback) Capture(ctx context.Context, dst []float32) (int, error)

func (*Loopback) Close

func (d *Loopback) Close() error

func (*Loopback) Discard

func (d *Loopback) Discard(_ context.Context) error

Discard drops samples already handed over by the peer. This models the fact that a real receiver's capture buffer holds its own reverb tail after a turnaround, which is exactly the condition the settle timer exists for.

func (*Loopback) Drain

func (d *Loopback) Drain(ctx context.Context) error

Drain is a no-op: cross-wiring has no speaker to wait for.

func (*Loopback) Format

func (d *Loopback) Format() Format

func (*Loopback) Latency

func (d *Loopback) Latency() Latency

func (*Loopback) Pending

func (d *Loopback) Pending() int

Pending reports how many samples from the peer are waiting to be captured.

func (*Loopback) Play

func (d *Loopback) Play(ctx context.Context, pcm []float32) error

func (*Loopback) Played

func (d *Loopback) Played() []float32

Played returns a copy of everything this side has transmitted.

func (*Loopback) SetTransformer

func (d *Loopback) SetTransformer(t Transformer)

SetTransformer swaps the in-flight transform. Tests use it to change channel conditions mid-session without rebuilding the pair.

type Null

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

Null is a Device that discards everything it plays, records it for inspection, and captures from a caller-supplied buffer.

It is not a testing afterthought. puts the whole reliability core (framing, FEC, ARQ, transfer) behind this backend so that the protocol can be proven before acoustics make debugging hard, and so that CI can run without a sound server.

func NewNull

func NewNull() *Null

NewNull returns a Null device using the default format.

func NewNullFormat

func NewNullFormat(f Format) *Null

NewNullFormat returns a Null device with an explicit format.

func (*Null) Capture

func (d *Null) Capture(ctx context.Context, dst []float32) (int, error)

func (*Null) Close

func (d *Null) Close() error

func (*Null) Discard

func (d *Null) Discard(_ context.Context) error

func (*Null) Drain

func (d *Null) Drain(ctx context.Context) error

func (*Null) Feed

func (d *Null) Feed(pcm []float32)

Feed appends samples that subsequent Capture calls will return.

func (*Null) Format

func (d *Null) Format() Format

func (*Null) Latency

func (d *Null) Latency() Latency

func (*Null) Play

func (d *Null) Play(ctx context.Context, pcm []float32) error

func (*Null) Played

func (d *Null) Played() []float32

Played returns a copy of everything passed to Play so far.

type Transformer

type Transformer func(src []float32) []float32

Transformer rewrites samples in flight. It exists so that the simulator can be spliced into a live signal path without any layer above audio knowing. Implementations must not retain or modify src.

type WAV

type WAV struct {
	Format Format
	PCM    []float32
}

WAV is a decoded mono PCM buffer plus its format.

func DecodeWAV

func DecodeWAV(r io.Reader) (*WAV, error)

func ReadWAVFile

func ReadWAVFile(path string) (*WAV, error)

ReadWAVFile decodes a WAV file from disk.

type WAVStream

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

WAVStream writes a 16-bit PCM WAV file incrementally.

It exists because the alternative - buffer the whole recording, then encode it - is fine for a ten-second capture and absurd for a ten-minute one. The header goes out first with a zero length and is patched on Close, which is why the destination has to be seekable.

func NewWAVStream

func NewWAVStream(w io.WriteSeeker, f Format) (*WAVStream, error)

NewWAVStream writes a placeholder header and returns a stream that appends samples to it.

func (*WAVStream) Close

func (s *WAVStream) Close() error

Close patches the header's two length fields and leaves the destination positioned at the end.

The 32-bit RIFF length is a real limit rather than a formality: at 48 kHz a header can describe four and a half hours of mono 16-bit audio, and a stream longer than that is refused instead of being written with a wrapped length that every player would misread.

func (*WAVStream) Samples

func (s *WAVStream) Samples() int

Samples is how many samples have been written.

func (*WAVStream) WriteSamples

func (s *WAVStream) WriteSamples(pcm []float32) error

WriteSamples appends PCM to the stream.

type Wav

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

Wav is a Device backed by memory, with optional persistence. Play accumulates into an in-memory transmit record; Capture replays whatever was passed to Feed. This is how a test plays a signal, captures it elsewhere, and compares the two without involving a sound card.

func NewWav

func NewWav() *Wav

NewWav returns a memory-backed Device using the default format.

func NewWavFormat

func NewWavFormat(f Format) *Wav

NewWavFormat returns a memory-backed Device with an explicit format.

func (*Wav) Capture

func (d *Wav) Capture(ctx context.Context, dst []float32) (int, error)

func (*Wav) Close

func (d *Wav) Close() error

Close persists the transmit record when a destination path was configured.

func (*Wav) Discard

func (d *Wav) Discard(_ context.Context) error

func (*Wav) Drain

func (d *Wav) Drain(ctx context.Context) error

func (*Wav) Feed

func (d *Wav) Feed(pcm []float32)

Feed queues samples for subsequent Capture calls.

func (*Wav) Format

func (d *Wav) Format() Format

func (*Wav) Latency

func (d *Wav) Latency() Latency

func (*Wav) Play

func (d *Wav) Play(ctx context.Context, pcm []float32) error

func (*Wav) PlayedTo

func (d *Wav) PlayedTo() []float32

PlayedTo returns a copy of everything transmitted through this device.

func (*Wav) SetCaptureFile

func (d *Wav) SetCaptureFile(path string)

SetCaptureFile makes Close persist the transmit record to path.

Jump to

Keyboard shortcuts

? : This menu
/ : Search site
f or F : Jump to
y or Y : Canonical URL