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
- Variables
- func EncodeWAV(w io.Writer, f Format, pcm []float32) error
- func FromInt16(dst []float32, src []byte) int
- func Identity(src []float32) []float32
- func Int16Bytes(n int) int
- func NewLoopback(f Format, leftXform, rightXform Transformer) (*Loopback, *Loopback)
- func ToInt16(dst []byte, src []float32)
- func WriteWAVFile(path string, f Format, pcm []float32) error
- type Device
- type Format
- type HighPass
- type Latency
- type LevelMeter
- type Loopback
- func (d *Loopback) Capture(ctx context.Context, dst []float32) (int, error)
- func (d *Loopback) Close() error
- func (d *Loopback) Discard(_ context.Context) error
- func (d *Loopback) Drain(ctx context.Context) error
- func (d *Loopback) Format() Format
- func (d *Loopback) Latency() Latency
- func (d *Loopback) Pending() int
- func (d *Loopback) Play(ctx context.Context, pcm []float32) error
- func (d *Loopback) Played() []float32
- func (d *Loopback) SetTransformer(t Transformer)
- type Null
- func (d *Null) Capture(ctx context.Context, dst []float32) (int, error)
- func (d *Null) Close() error
- func (d *Null) Discard(_ context.Context) error
- func (d *Null) Drain(ctx context.Context) error
- func (d *Null) Feed(pcm []float32)
- func (d *Null) Format() Format
- func (d *Null) Latency() Latency
- func (d *Null) Play(ctx context.Context, pcm []float32) error
- func (d *Null) Played() []float32
- type Transformer
- type WAV
- type WAVStream
- type Wav
- func (d *Wav) Capture(ctx context.Context, dst []float32) (int, error)
- func (d *Wav) Close() error
- func (d *Wav) Discard(_ context.Context) error
- func (d *Wav) Drain(ctx context.Context) error
- func (d *Wav) Feed(pcm []float32)
- func (d *Wav) Format() Format
- func (d *Wav) Latency() Latency
- func (d *Wav) Play(ctx context.Context, pcm []float32) error
- func (d *Wav) PlayedTo() []float32
- func (d *Wav) SetCaptureFile(path string)
Constants ¶
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 )
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.
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 ¶
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") )
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.
var ErrNotWAV = errors.New("audio: not a WAV file")
ErrNotWAV is returned when a file is not a RIFF/WAVE container.
var ErrUnknownDevice = errors.New("audio: unknown device")
ErrUnknownDevice means a device kind this build does not have.
Functions ¶
func EncodeWAV ¶
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 ¶
FromInt16 converts little-endian signed 16-bit PCM into dst, returning the number of samples written.
func Int16Bytes ¶
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 ¶
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.
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.
type Format ¶
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) SampleCount ¶
SampleCount is the sample count corresponding to a wall-clock duration, rounded to the nearest whole sample.
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 ¶
NewHighPass returns a high-pass for a sample rate.
type Latency ¶
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 ¶
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.
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) Discard ¶
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) Pending ¶
Pending reports how many samples from the peer are waiting to be captured.
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 NewNullFormat ¶
NewNullFormat returns a Null device with an explicit format.
type Transformer ¶
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 ¶
WAV is a decoded mono PCM buffer plus its format.
func ReadWAVFile ¶
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 ¶
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) WriteSamples ¶
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 NewWavFormat ¶
NewWavFormat returns a memory-backed Device with an explicit format.
func (*Wav) SetCaptureFile ¶
SetCaptureFile makes Close persist the transmit record to path.