audiotoolbox

package module
v0.0.0-...-2492276 Latest Latest
Warning

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

Go to latest
Published: Sep 2, 2026 License: BSD-3-Clause Imports: 5 Imported by: 0

README

go-macos/audiotoolbox

Decode compressed audio and play PCM on macOS from pure Go — CGO_ENABLED=0, via purego. You bring the coded packets; this brings the decoder and the output.

cfg, _ := reader.TrackConfig(track.ID)        // go-avkit/avkit/container
codec, _ := audiotoolbox.CodecFor(cfg.Codec)  // "mp4a" -> AAC

dcfg := audiotoolbox.Config{
        Codec: codec, SampleRate: cfg.SampleRate, Channels: cfg.Channels,
        AudioObjectType: cfg.AudioObjectType, CodecConfig: cfg.CodecConfig,
}
dec, _ := audiotoolbox.NewDecoder(dcfg)
p, _ := audiotoolbox.NewPlayer(audiotoolbox.PlayerConfigFor(dcfg))
defer dec.Close()
defer p.Close()
p.Start()

for _, pkt := range packets {
        buf, _ := dec.Decode(audiotoolbox.Packet{Data: pkt.Data})
        p.Write(buf.PCM)        // blocks on the queue, which paces the loop
        video.SyncTo(p.Played()) // the master clock
}
tail, _ := dec.Flush()
p.Write(tail.PCM)
p.Drain()

Why this exists

It is the audio half of the hole go-macos/videotoolbox fills for pictures. AVFoundation plays an MP4 end to end and is the right tool when it works; it will not demux Matroska at all. So a player that wants sound out of an MKV has to demux the file itself and hand the coded packets to a decoder directly.

go-avkit/avkit/container does the demuxing — MP4, Matroska/WebM and MPEG-TS, in pure Go — and already reports audio tracks, their codec, channel count, sample rate and coded packets. This package is the other half: AudioConverter to turn those packets into PCM, and AudioQueue to put the PCM on the system output.

$ auprobe -for 5s "The.Addams.Family.2.2021.mkv"
  matroska, 5563.456 s, 3 tracks, 1 audio (read in 1.133s, demuxed in 435ms)
  audio track 2: ac-3 6 ch 48000 Hz, 5563.456 s, 173858 packets
  decoder: ac-3 -> 6 ch s16
  decoded 240864 frames = 5.018s of audio, in 18ms (276x real time)
  the output played 5.011s of the 5.018s written

What it covers

Codecs in AAC (mp4a, LC / HE / HE v2), AC-3 (ac-3), Enhanced AC-3 (ec-3), Opus
Input one coded packet at a time — an AAC access unit, an AC-3 sync frame
Output interleaved PCM, Int16 or Float32, at the coded rate
Downmix Config.OutputChannels asks the decoder for fewer, so 5.1 reaches two speakers
Playback AudioQueue on the system output, with a real clock
Files WAV writes the PCM where it can be looked at
Platforms darwin arm64 and amd64; every other platform builds and answers ErrUnsupported

Not here: encoding, resampling, mixing, and any format whose packets this does not name — a codec CodecFor does not know is refused up front, with a reason, rather than decoded into noise.

Audio is the master clock

Player.Played is how much audio has left the device, read from the AudioQueue's own timeline. It is not a count of what was written: the two differ by whatever is still queued — a quarter of a second by default — and that difference is the whole of the drift a naive player accumulates. The ear hears a drift the eye does not, so video follows this and never the other way round.

Two things were measured and are worth knowing.

AudioQueueStop does not wait. Asked to stop gently (inImmediate false) it returns in thirty microseconds and lets the queue finish in its own time, so a Drain that called it and returned would cut the tail off exactly as an immediate stop does. This one waits for the queue to hand every buffer back — which is when the device has actually finished with them — and only then stops. Before that fix a three-second decode reported 2.938 s played; after it, 3.001 s.

The timeline is the device's. A device that runs out of audio does not stop: it plays silence and the clock goes on advancing. That is right for a master clock, which must not stall, and Player.Queued is what says whether there is anything left to hear.

Why AudioQueue and not AudioUnit

Both were on the table; AudioQueue won on three counts, and none of them is "it was easier".

It calls back on a thread of its own. Asked for a nil run loop, AudioQueue runs its own; an output AudioUnit renders on a real-time thread whose callback must never allocate, never take a lock and never block — which a Go callback reached through purego cannot promise, because the Go runtime may preempt it or grow its stack. A render callback that misses its deadline is a click the listener hears.

It buffers, so Write can block. The queue holds a quarter of a second in front of the device, so a decode loop is paced by the output rather than by a time.Sleep on a guess, and a decode that stalls briefly is not heard. An AudioUnit pulls exactly what the device asks for, exactly when it asks, and the caller has to build that buffering itself — a ring buffer, a producer lock, and the same clock this gets for free.

It has the clock. AudioQueueGetCurrentTime reports the device's own timeline directly. An AudioUnit gives an AudioTimeStamp to its render callback and leaves the caller to keep the running total, which is one more thing to get wrong in the one number a player cannot afford to have wrong.

The cost is latency: a quarter of a second is far too much for a synthesiser or a game, and either would want the AudioUnit. For playing a film it is invisible, and PlayerConfig.BufferFrames and BufferCount shorten it for a caller who wants a faster response to a seek.

The format is stated, never sniffed

A Config says what the packets hold, the way videotoolbox takes its bitstream form as stated and for the same reason: a demuxer already knows, and a decoder that guesses is a decoder that is occasionally, silently wrong.

AAC in an MP4 or a Matroska file carries no in-band configuration — no ADTS header, nothing before the first packet — so the decoder is set up from an AudioSpecificConfig. When the demuxer states one it is used as it stands; when it does not, one is built from the sample rate, the channel count and the AAC profile the demuxer did state. Config.AudioSpecificConfig is exported so a caller can check it against a hex dump.

The wrapping is not decoration. Offered the bare AudioSpecificConfig, AudioConverterSetProperty answers kAudioCodecBadPropertySizeError ('!dat') and the configuration is not applied; wrapped in an MPEG-4 ES_Descriptor, the same two bytes are taken. A cookie that is refused is reported through Decoder.CodecConfigRefused rather than swallowed, because the alternative is a caller who believes the configuration took effect and finds out from a 5.1 track that comes back as noise.

Two callbacks, and what they cost

purego.NewCallback cannot describe a struct passed by value as a callback argument. Neither of this package's two callbacks takes one — five pointers for the converter's input proc, three for the queue's output callback — so both are expressible, and every C structure here is passed by pointer.

purego never frees a callback and allows a bounded number of them, so one per decoder would be a leak with a hard ceiling. There are exactly two for the process; each carries an integer key and looks its owner up in a registry. An integer, not a Go pointer: handing C a pointer into the Go heap and expecting it back later is what the cgo pointer rules forbid. The coded packet and the PCM the converter reads and writes are malloced for the same reason.

The input proc must say "not now", not "never"

Apple's documentation describes reporting zero packets with noErr when the input proc has nothing to hand over. Measured, that tells the converter the input is over: it decodes what it has, and every later FillComplexBuffer returns no frames at all. On a plain AAC MP4 the first packet decoded to 1024 frames and the next 12 090 decoded to nothing, silently, with noErr throughout. A distinctive status instead means "not now": FillComplexBuffer hands it straight back, keeps the frames it already wrote, and the next call works. End of stream is still reachable, and Decoder.Flush is what reaches it.

Buffers alias

Decoder.Decode returns PCM that lives in a scratch buffer the decoder owns, and the next call overwrites it. A player copies the samples into its own queue anyway, and allocating a fresh slice for forty-seven packets a second buys nothing. Buffer.Clone copies for a caller that wants to keep one.

Measured

M4 Max, macOS 26.6.2, Go 1.26.4, CGO_ENABLED=0, decoding through the public API of this package and of go-avkit/avkit/container:

file packets audio rate
4-minute presentation MP4, AAC-LC 2.0 48 kHz 12 091 / 12 091 4 m 17.941 s 192 ms, 1345× real time
feature film, 1 h 32 MKV, AC-3 5.1 48 kHz, 2.0 GB 173 858 / 173 858 1 h 32 m 43.45 s 17.9 s, 310× real time

The film's decoded length comes out 6 ms from the 5563.456 s the container states, over an hour and a half.

Against Apple's own decoder

A decoder nobody can check is a decoder nobody should trust, and a report of "it plays" is worth nothing to a reader who cannot hear it. So whole tracks were decoded here and by afconvert, and the samples compared:

samples compared identical largest difference
AC-3 5.1, MKV 27 646 272 100.0000 % 0
AAC-LC 2.0, MP4 24 760 320 99.986 % – 100 % 1 (of 32 767)
AAC-LC 1.0, MP4 24 000 100.0000 % 0

The AC-3 track is bit-for-bit Apple's output, on every run.

The AAC figure is a range on purpose, and the reason is worth writing down: AudioToolbox's own AAC decoder is not bit-reproducible between processes. Three runs of the same decode against the same afconvert output gave 99.9858 %, 100.0000 % and 99.9987 %; two runs compared against each other vary the same way. Every difference, in every direction, is exactly one least-significant bit of the final rounding to 16 bits. So the honest claim is not "identical" but "within one LSB of Apple on every one of 24.7 million samples" — and anyone quoting a single 100 % run of an AAC decode has measured it once.

Ours also runs 1024 frames — one AAC frame — longer than afconvert's, because afconvert trims the encoder's last frame to the duration the container states and this hands back everything the decoder produced. The mono comparison likewise allows for the 2112 frames of encoder priming, and is exact after it.

afconvert cannot open the MKV at all; what it was given was the AC-3 elementary stream this fleet's demuxer produced, which is the honest comparison.

Opus, and what has not been proved

macOS 26.6.2 builds an Opus converter, and the framing is confirmed with no Opus media anywhere: RFC 6716 says a packet that is nothing but its TOC byte carries one frame of length zero, which is legal, so 0xf8 — CELT-only, fullband, 20 ms — must decode to exactly 960 frames at 48 kHz, and does, mono and stereo, after the pre-skip the OpusHead states. Rate, framing and channel layout all confirmed at once.

What that does not prove is that a real Opus bitstream decodes to the right samples. Nothing on a Mac encodes Opus, so there was no file to check against — TestLiveDecode is where it would run, and it has not been run on Opus. Treat Opus here as wired up and framed correctly, not as measured.

cmd/auprobe

auprobe movie.mp4                       # decode and play the whole track
auprobe -for 10s movie.mkv              # play ten seconds and stop
auprobe -wav out.wav -play=false f.mp4  # decode as fast as possible to a WAV
auprobe -stereo film.mkv                # downmix a 5.1 track to the speakers
auprobe -track 1 film.mkv               # the second audio track
auprobe -volume 0.3 movie.mp4

It counts what it did — packets submitted, frames decoded, seconds of audio against the duration the container states — and with -wav it writes a file, checks its size against 44 + frames × channels × 2, and prints the command to play it. It reads the file whole, because the demuxer takes a byte slice; that is the tool's limit, not the package's.

Testing

The portable layer is at 100 % statement coverage behind platform seams, and CI gates on those files rather than on the total: the purego bindings answer OSStatus failure paths that cannot be reached without making a framework fail, and a total-coverage gate would either be a lie or force media into the repository.

The bindings are covered three ways. A real AudioConverter decodes a real AAC bitstream on every CI run — 732 bytes of a 1 kHz sine at −6 dBFS, committed to the test — and the decoded signal's frequency is then asserted with a Goertzel filter rather than described: 1 kHz comes back at 16 230 of full scale and 6.5 kHz at 5. A decoder that is silently wrong (wrong channel count, wrong rate, samples read at the wrong width) cannot pass that, and it needs no media on the runner. A real AudioQueue is opened, written to, clocked and drained, and skips itself on a machine with no output device. A real Opus converter decodes a bare TOC byte to the 960 frames RFC 6716 says it must. The end-to-end decode is opt-in:

AUDIOTOOLBOX_TEST_FILE=/path/to/movie.mkv go test -race ./...

Licence: BSD-3-Clause.

Documentation

Overview

Package audiotoolbox decodes compressed audio and plays PCM on macOS through AudioToolbox, with no cgo.

It exists because of a measured hole, and it is the audio half of the one github.com/go-macos/videotoolbox fills for pictures. AVFoundation plays an MP4 end to end and will not demux Matroska at all, so a player that wants sound out of an MKV has to demux the file itself and hand the coded packets to a decoder directly. github.com/go-avkit/avkit/container does the demuxing — it already reports audio tracks, their codec, channel count, sample rate and coded packets, for MP4 and for Matroska alike. This package is the other half: AudioConverter to turn those packets into PCM, and AudioQueue to put the PCM on the system output.

Everything goes through github.com/ebitengine/purego, so a consumer still builds with CGO_ENABLED=0.

Two halves, used together or apart

A Decoder turns one coded Packet into a Buffer of interleaved PCM. A Player takes interleaved PCM and plays it. Neither knows about the other: a caller that only wants samples never opens an output device, and a caller with PCM of its own never builds a decoder.

dec, _ := audiotoolbox.NewDecoder(cfg)
p, _ := audiotoolbox.NewPlayer(audiotoolbox.PlayerConfigFor(cfg))
p.Start()
for _, pkt := range packets {
        buf, _ := dec.Decode(audiotoolbox.Packet{Data: pkt.Data})
        p.Write(buf.PCM)
}
p.Drain()

The format is stated, never sniffed

A Config says what the packets hold: which codec, at what sample rate, with how many channels. It is taken as stated, the way github.com/go-macos/videotoolbox takes its bitstream form as stated, and for the same reason: a demuxer already knows, and a decoder that guesses is a decoder that is occasionally, silently wrong. container.TrackConfig hands back every field this needs.

AAC in an MP4 or a Matroska file carries no in-band configuration — no ADTS header, nothing before the first packet — so the decoder is set up from an AudioSpecificConfig. When the demuxer states one, in Config.CodecConfig, it is used as it stands; when it does not, one is built from the sample rate, the channel count and the AAC profile the demuxer did state. See Config.MagicCookie.

Buffers alias, and the reason is one memcpy

Decoder.Decode returns PCM that lives in a scratch buffer the decoder owns, and the next call overwrites it. That is deliberate: a player copies the samples into its own queue anyway, and allocating a fresh slice per packet — forty-seven of them a second at 48 kHz — buys nothing. Buffer.Clone copies for a caller that wants to keep one.

The clock

Player.Played is how many seconds of audio have left the device. It is not a count of what was written: a player that synchronises video against bytes handed to the output is a player that drifts, because the output consumes them at its own rate. Audio is the master clock of every serious player — the ear hears a drift the eye does not — so this reads the AudioQueue's own timeline rather than guessing at it.

Player.Drain is what waits for the last of it. AudioQueueStop does not: asked to stop gently it returns at once and finishes in its own time, so a drain that trusted it would cut the tail off. This one waits for the queue to hand every buffer back, which is when the device has finished with them.

Index

Constants

This section is empty.

Variables

View Source
var (
	// ErrUnsupported is returned by every entry point on non-darwin platforms.
	ErrUnsupported = errors.New("audiotoolbox: unsupported on this platform (darwin only)")
	// ErrClosed is returned when a Decoder or a Player is used after Close.
	ErrClosed = errors.New("audiotoolbox: closed")
	// ErrUnsupportedCodec is returned for a codec this package does not
	// describe to AudioToolbox.
	ErrUnsupportedCodec = errors.New("audiotoolbox: unsupported codec")
	// ErrConfig is returned for a configuration that does not describe a
	// stream: no sample rate, no channels, an impossible sample format.
	ErrConfig = errors.New("audiotoolbox: invalid configuration")
	// ErrPacket is returned when a packet cannot be submitted as given.
	ErrPacket = errors.New("audiotoolbox: invalid packet")
)

Errors reported by the package. They are stable and may be tested with errors.Is.

Functions

This section is empty.

Types

type Buffer

type Buffer struct {
	// PCM is the decoded samples, interleaved, Frames*Channels*Format.Size()
	// bytes of them.
	PCM []byte
	// Frames is how many sample frames PCM holds — samples per channel, not
	// samples.
	Frames int
	// Channels is how many channels are interleaved in PCM.
	Channels int
	// SampleRate is the rate PCM is at, in Hz.
	SampleRate int
	// Format is the layout of one sample.
	Format SampleFormat
	// PTS is the presentation timestamp of the packet this came from.
	PTS time.Duration
}

Buffer is the PCM one packet decoded into.

PCM is NOT copied out of the decoder: it aliases a scratch buffer the Decoder owns, and the next call to Decoder.Decode overwrites it. Use Buffer.Clone to keep one. A Player.Write consumes it before returning, so the common path never needs a copy.

func (Buffer) Clone

func (b Buffer) Clone() Buffer

Clone copies the samples out of the decoder's scratch buffer, so the result outlives the next Decoder.Decode.

func (Buffer) Duration

func (b Buffer) Duration() time.Duration

Duration is how long this buffer's audio lasts.

type Codec

type Codec uint8

Codec names the compressed form a decoder is set up for.

const (
	// AAC is MPEG-4 Advanced Audio Coding, the "mp4a" of an MP4 sample entry
	// and the A_AAC of a Matroska track. It is what the overwhelming
	// majority of MP4 files carry.
	AAC Codec = iota + 1
	// AC3 is Dolby Digital, the "ac-3" of an MP4 sample entry and the A_AC3
	// of a Matroska track. It is what a great many films in MKV carry, in
	// 5.1.
	AC3
	// EAC3 is Dolby Digital Plus, "ec-3".
	EAC3
	// Opus is the IETF codec, "Opus" in an MP4 sample entry and A_OPUS in
	// Matroska. macOS decodes it, but only on a recent enough system; a
	// session that cannot be built says so rather than falling back.
	Opus
)

The codecs this package describes to AudioToolbox. Every one of them is a codec github.com/go-avkit/avkit/container reports for a demuxed audio track, which is the whole point: what the demuxer produces is what this takes.

func CodecFor

func CodecFor(name string) (Codec, bool)

CodecFor maps the codec names container.TrackConfig reports onto a Codec. It reports false for anything else, which is how a caller finds out that a track it demuxed is not one this package decodes before it builds a decoder.

The names are the ones avkit normalises to: "mp4a" for AAC whether it came from an MP4 sample entry or from A_AAC/MPEG4/LC, "ac-3" for AC-3 whether from "ac-3" or A_AC3/BSID9, "ec-3", and "Opus" — with the capital, which is what the MP4 sample entry spells.

func (Codec) String

func (c Codec) String() string

String names the codec.

type Config

type Config struct {
	// Codec is the compressed form in the packets. It is taken as stated and
	// never inferred.
	Codec Codec
	// SampleRate is the coded sample rate in Hz, as the container states it.
	SampleRate int
	// Channels is the coded channel count.
	Channels int
	// AudioObjectType is the AAC profile from the track's AudioSpecificConfig:
	// 2 for AAC-LC, 5 for HE-AAC, 29 for HE-AAC v2. Zero means AAC-LC, which
	// is what a Matroska track that states no configuration at all is.
	// Ignored by every other codec.
	AudioObjectType byte
	// CodecConfig is the codec's own configuration record, as the demuxer
	// read it: an AudioSpecificConfig for AAC, an OpusHead for Opus. When it
	// is empty for AAC, one is built — see [Config.MagicCookie].
	CodecConfig []byte
	// OutputChannels is how many channels to decode into. Zero means
	// Channels, which is the usual case; setting 2 for a 5.1 track asks
	// AudioToolbox to downmix, which it will do for AC-3 and refuse for
	// codecs whose decoder has no matrix.
	OutputChannels int
	// Output is the PCM sample format to decode into. The zero value is
	// [Int16].
	Output SampleFormat
}

Config describes the track a decoder is built for, the way a demuxer states it.

container.TrackConfig hands back every field but Codec, which CodecFor maps from its Codec string.

func (Config) AudioSpecificConfig

func (c Config) AudioSpecificConfig() []byte

AudioSpecificConfig is the two-or-so bytes of ISO/IEC 14496-3 configuration an AAC decoder is set up from.

Config.CodecConfig is used as it stands when the demuxer stated one: it came out of the file and is more authoritative than anything derived. When there is none — which is what a Matroska track with no CodecPrivate gives, and what every MP4 track avkit reads gives, because avkit reports the AAC profile rather than the record it came from — one is built from the profile, the sample rate and the channel count the demuxer DID state.

It is exported because a caller that wants to know what its decoder was set up from should be able to look, and because it is the one piece of this package worth checking against a hex dump.

func (Config) MagicCookie

func (c Config) MagicCookie() []byte

MagicCookie is the codec configuration AudioToolbox is set up from, or nil for a codec that needs none.

For AAC it is an MPEG-4 ES_Descriptor — an "esds" — wrapping the track's AudioSpecificConfig. The wrapping is not decoration, it is measured: offered the bare AudioSpecificConfig, AudioConverterSetProperty answers kAudioCodecBadPropertySizeError ('!dat') and the configuration is not applied. Wrapped, the same two bytes are taken.

For every other codec it is Config.CodecConfig as the demuxer read it, or nil when there is none. AC-3 and Enhanced AC-3 carry everything in their packets — the sync frame states the rate, the bitrate and the channel mode — so a cookie would describe what the decoder is about to read anyway.

type Decoder

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

Decoder is one AudioConverter: a track's decoder, fed a coded packet at a time.

It is NOT safe for concurrent use.

func NewDecoder

func NewDecoder(cfg Config) (*Decoder, error)

NewDecoder builds a decoder for a track.

func (*Decoder) Close

func (d *Decoder) Close() error

Close tears the decoder down. It is safe to call more than once.

func (*Decoder) CodecConfigRefused

func (d *Decoder) CodecConfigRefused() error

CodecConfigRefused reports what AudioToolbox answered when the codec configuration was offered to it, or nil when it was taken — and nil, too, for a codec that has none to offer.

A refusal is not fatal and is not treated as one: measured on a plain AAC MP4, a decoder whose cookie was refused still decoded every packet bit for bit against afconvert. It is reported because the alternative is a caller who believes the configuration took effect when it did not, and finds out from a 5.1 track that comes back as noise.

func (*Decoder) Config

func (d *Decoder) Config() Config

Config returns the configuration the decoder was built from, with the defaults resolved.

func (*Decoder) Decode

func (d *Decoder) Decode(p Packet) (Buffer, error)

Decode turns one coded packet into PCM.

The buffer it returns may hold no frames at all, and that is not an error: an AAC decoder emits nothing for the encoder delay at the start of a track, and a converter is entitled to hold a packet back. The samples alias the decoder's scratch buffer and the next call overwrites them.

func (*Decoder) Decoded

func (d *Decoder) Decoded() time.Duration

Decoded is how long the audio produced so far lasts.

func (*Decoder) Flush

func (d *Decoder) Flush() (Buffer, error)

Flush returns whatever the decoder was still holding. A caller that has submitted its last packet must call it, or lose the tail of the track.

func (*Decoder) Frames

func (d *Decoder) Frames() int64

Frames is how many sample frames the decoder has produced since it was built. It is the honest count of what came out, which is not the count of what went in: an AAC decoder swallows the encoder's priming packets and emits nothing for them.

type Packet

type Packet struct {
	// Data is the coded packet, as the demuxer produced it.
	Data []byte
	// PTS is when this packet's audio starts, relative to the start of the
	// track. It travels through the decoder and comes back on the [Buffer];
	// nothing here depends on it.
	PTS time.Duration
}

Packet is one coded audio packet handed to the decoder — one AAC access unit, one AC-3 sync frame, one Opus packet. It is what container.Reader's Samples hands back for an audio track, one element at a time.

type Player

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

Player plays interleaved PCM on the system output.

Player.Write is the only entry point that blocks: it waits for a queue buffer to come free, which is what paces a decode loop to real time without the caller sleeping on a guess.

It is NOT safe for concurrent use, except that Player.Played may be read from another goroutine — which is the point of a clock.

func NewPlayer

func NewPlayer(cfg PlayerConfig) (*Player, error)

NewPlayer opens the system output. Nothing is heard until Player.Start.

func (*Player) Close

func (p *Player) Close() error

Close stops the output and gives it back. It is safe to call more than once, and it does NOT drain: call Player.Drain first to hear the end.

func (*Player) Config

func (p *Player) Config() PlayerConfig

Config returns the configuration the player was built from, with the defaults resolved.

func (*Player) Drain

func (p *Player) Drain() error

Drain waits for everything written to be played. A caller that has written its last buffer must call it, or Close cuts the tail off.

func (*Player) Played

func (p *Player) Played() time.Duration

Played is how much audio has left the device since Player.Start.

It is the clock a player synchronises video against. It is read from the output's own timeline, not counted from what was written: the two differ by whatever is still sitting in the queue, which is a quarter of a second by default and the whole of the drift a naive player accumulates.

One honest caveat, measured: the timeline is the DEVICE's, and a device that runs out of audio does not stop — it plays silence and the clock goes on advancing. So a player that stops writing sees Played run past what it wrote. That is the right behaviour for a master clock, which must not stall, and Player.Queued is what says whether there is anything left to hear.

func (*Player) Queued

func (p *Player) Queued() time.Duration

Queued is how much audio has been written but not yet played — the latency between a decode and the sound.

func (*Player) Start

func (p *Player) Start() error

Start begins playback. Writing before it is allowed and usual: filling the queue first is what stops the first buffers from being played half empty.

func (*Player) Stop

func (p *Player) Stop() error

Stop stops playback at once, dropping whatever is still queued.

func (*Player) Write

func (p *Player) Write(pcm []byte) (int, error)

Write hands interleaved PCM to the output, blocking until it has all been queued. It returns the number of bytes written, which is len(pcm) unless it failed part way.

A partial frame is refused rather than queued: half a sample frame shifts every channel that follows it by one, and the noise that makes is not obviously a bug.

func (*Player) Written

func (p *Player) Written() int64

Written is how many sample frames have been handed to the output.

type PlayerConfig

type PlayerConfig struct {
	// SampleRate is the rate of the PCM that will be written, in Hz.
	SampleRate int
	// Channels is how many channels are interleaved in it.
	Channels int
	// Format is the layout of one sample. The zero value is [Int16].
	Format SampleFormat
	// BufferFrames is how many frames one output buffer holds; zero means
	// 4096.
	BufferFrames int
	// BufferCount is how many of them the queue cycles through; zero means 3.
	// Two is the minimum that lets one play while another is filled.
	BufferCount int
	// Volume is the output gain, 0 to 1. Zero means 1 — silence is asked for
	// by not playing, not by a zero field nobody filled in.
	Volume float64
}

PlayerConfig describes the output.

func PlayerConfigFor

func PlayerConfigFor(cfg Config) PlayerConfig

PlayerConfigFor is the output that matches what a Decoder built from cfg produces, which is what a player feeding one decoder wants.

func (PlayerConfig) BytesPerFrame

func (c PlayerConfig) BytesPerFrame() int

BytesPerFrame is how many bytes one sample frame takes.

type SampleFormat

type SampleFormat uint8

SampleFormat is the layout of one PCM sample.

const (
	// Int16 is signed 16-bit, little-endian, interleaved. It is the zero
	// value because it is what an output device wants, what a WAV file
	// holds, and what a reader can look at with any tool it already has.
	Int16 SampleFormat = iota
	// Float32 is 32-bit float, native-endian, interleaved, nominally in
	// [-1, 1]. It is what a mixer or a resampler would rather have.
	Float32
)

func (SampleFormat) Size

func (f SampleFormat) Size() int

Size is how many bytes one sample of one channel takes.

func (SampleFormat) String

func (f SampleFormat) String() string

String names the sample format.

type StatusError

type StatusError struct {
	// Op is the C function that failed.
	Op string
	// Status is the OSStatus it returned.
	Status int32
}

StatusError carries an OSStatus from AudioToolbox, naming the call that returned it. AudioToolbox says everything through these codes, and half of them are four-character codes read as a signed integer — 1718449215 is 'fmt?', which is a great deal more informative.

func (*StatusError) Error

func (e *StatusError) Error() string

type WAV

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

WAV is a RIFF/WAVE file being written.

It is here because a decoder nobody can check is a decoder nobody should trust. "It plays" is not a result — the reader of a report cannot hear it, and neither can a CI runner. A WAV file is: it opens in anything, its size can be divided by the frame size and compared against the track duration, and its samples can be looked at.

The writer needs an io.WriteSeeker because RIFF states the size of the file and of the data chunk in their headers, before either is known. WAV.Close goes back and fills them in; a file closed without it is a file every reader refuses.

func NewWAV

func NewWAV(w io.WriteSeeker, cfg PlayerConfig) (*WAV, error)

NewWAV starts a WAV file for the PCM cfg describes. The header is written with placeholder sizes, which WAV.Close corrects.

func (*WAV) Close

func (f *WAV) Close() error

Close rewrites the header with the sizes now known. It does NOT close the underlying writer, which the caller opened and should close. Calling it twice is a no-op.

func (*WAV) Frames

func (f *WAV) Frames() int64

Frames is how many sample frames have been written.

func (*WAV) Write

func (f *WAV) Write(pcm []byte) (int, error)

Write appends interleaved PCM. A partial frame is refused, for the reason Player.Write gives.

Directories

Path Synopsis
cmd
auprobe command
Command auprobe demuxes a real file, decodes its audio through AudioToolbox and plays it on the system output.
Command auprobe demuxes a real file, decodes its audio through AudioToolbox and plays it on the system output.

Jump to

Keyboard shortcuts

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