m4a

package module
v0.3.0 Latest Latest
Warning

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

Go to latest
Published: Jul 21, 2026 License: MIT Imports: 7 Imported by: 0

README

go-m4a

CI Go Reference Go Report Card Go Version Sponsor

Pure-Go MP4/M4A muxer and demuxer for AAC-LC, Opus, and FLAC audio. No cgo and no external binaries. It is the container half that codecs like go-aac deliberately leave to an external muxer: go-aac's pcm package notes that sample-accurate trimming of the encoder priming "requires a container with an edit list (MP4)", and points at an external muxer as the escape hatch. go-m4a is that muxer, plus the matching demuxer.

Write an .m4a/.mp4 from AAC-LC, Opus, or FLAC access units, with an edit list that trims the encoder priming so playback is gapless and sample-accurate. Read one back into access units that decode with go-aac, go-opus, go-flac, ffmpeg, QuickTime, or any compatible decoder. The core m4a package is codec-agnostic and stdlib-only; optional aacm4a, opusm4a, and flacm4a subpackages bridge the matching codec libraries, so a consumer that only muxes or demuxes pulls no codec.

Note on interop: Opus-in-MP4 is a niche encapsulation (browsers and hardware players prefer Ogg Opus or WebM), and FLAC-in-MP4 buys nothing over native .flac. Their value here is a pure-Go alternative to ffmpeg -c:a libopus/flac and API symmetry with the AAC path, not broad playback compatibility.

Status

The writer and reader dispatch on the codec, so all three share the same ftyp | mdat | moov plumbing, sample tables, and edit list; only the sample entry and its codec-specific box differ.

  • AAC-LC (mp4a + esds): the original path. Output is validated with ffprobe and decodes byte-for-byte back to the source through ffmpeg (a go-aac-encoded chirp round-trips at cross-correlation 1.0 at lag 0). The reader tolerates the extra boxes real encoders emit (free, udta, meta, sgpd, sbgp), expands arbitrary multi-chunk stsc tables, and reads ffmpeg and Apple afconvert files.
  • Opus (Opus + dOps): each MP4 sample is one Opus packet; the dOps pre-skip drives the same edit-list priming trim as AAC. The emitted dOps is byte-identical to ffmpeg's, ffmpeg reads the container, and an encode/decode round-trip through go-opus preserves the signal.
  • FLAC (fLaC + dfLa): each MP4 sample is one native FLAC frame, with the 34-byte STREAMINFO in dfLa; no edit list (FLAC has no priming). Reads ffmpeg-produced files and round-trips bit-exact (lossless) through go-flac.

Parsing is bounds-checked throughout and never panics on malformed input, and the accumulating decodes are bounded too. The decoded size of an audio file is not proportional to the file: a FLAC constant subframe encodes a whole 65535-sample block in a handful of bytes, so a crafted file of tens of kilobytes decodes to something on the order of a gigabyte, and a two-byte Opus packet of zero-length DTX frames decodes to 120 ms. Sixty seconds of ordinary digital silence encodes to about 14 kB and decodes back to 11.5 MB, a ratio of 831:1, so this is not only an adversarial concern.

flacm4a.DecodeInterleaved and opusm4a.DecodeInterleaved therefore stop at m4a.DefaultMaxDecodedBytes (1 GiB, about 101 minutes of CD-quality stereo) and return an error wrapping m4a.ErrDecodeLimit; see that constant for how the value was chosen. DecodeInterleavedLimit takes the ceiling as an argument (zero or less for none), and DecodeStream hands each frame's PCM to a callback without accumulating at all, so it decodes a stream of any length in memory proportional to a single frame. What those decodes reserve up front is bounded separately, by what the container corroborates, by a fixed ceiling and by the limit itself, so a file that declares an implausible length cannot make the decoder allocate for the claim before decoding anything.

aacm4a needs none of this and gets none of it: it has no accumulating decode, because NewDecoder returns a streaming decoder. A caller that accumulates that decoder's output itself owns the bound, and should size it against the audio rather than against the file.

There is also a fragmented (CMAF) writer for live HLS or DASH: InitSegment builds the ftyp/moov initialization segment and a FragmentWriter appends styp/moof/mdat media segments. It never seeks, so it can write straight into a byte slice or a socket, and it reuses its buffers, so a steady-state segment allocates nothing. It is codec-generic like the rest of the writer. See Fragmented output for HLS.

Scope: a single audio track, mono or stereo. The sample rate depends on the codec, because each one constrains it differently: AAC-LC takes the eleven MPEG-4 sampling-frequency table rates that the 16-bit sample-entry field can hold (7350, 8000, 11025, 12000, 16000, 22050, 24000, 32000, 44100, 48000, 64000 Hz; 88.2 and 96 kHz are in the table but are rejected rather than written wrong); Opus is always 48 kHz, which its encapsulation fixes as the container timescale; FLAC has no rate table, so any rate up to 65535 Hz. 44.1 and 48 kHz are the best-trodden paths; every AAC rate above is covered by a round trip rather than only by the validator letting it past.

Two caveats on that. The aacm4a convenience bridge is narrower than the core writer, because go-aac's encoder accepts only 44100 and 48000. And for FLAC the 65535 Hz ceiling is this package's limit rather than the format's: the Xiph encapsulation defines a fallback for higher rates that go-m4a does not implement (see #4). Fragmented MP4 is write-only: the reader is for plain files and returns a typed ErrUnsupported for fragmented input. Also out of scope (again ErrUnsupported, never a crash): video or multiple audio tracks, other codecs, HE-AAC, surround, and writing metadata tags. See the open issues for the tracked extensions (non-48 kHz Opus input, high sample rates, more than two channels).

Install

go get github.com/tphakala/go-m4a

The core m4a package is stdlib-only. The optional aacm4a, opusm4a, and flacm4a subpackages wire the container to go-aac, go-opus, and go-flac; each pulls in its codec module, so import only the ones you use.

Usage

m4a: the container

WriterConfig.Codec selects the codec (the zero value is CodecAACLC, so existing AAC callers are unchanged). AAC-LC access units are a fixed 1024 samples each, so WriteFrame suffices; Opus packets and FLAC frames vary in length, so those use WriteFrameDuration(au, sampleDuration). The reader reports Info.Codec and a generic Info.CodecConfig (the ASC, the dOps body, or the STREAMINFO).

Write AAC-LC access units (from any source) into an .m4a:

import "github.com/tphakala/go-m4a"

w, err := m4a.NewWriter(f, m4a.WriterConfig{
    SampleRate:  48000,
    Channels:    1,
    ASC:         asc,   // 2-byte AudioSpecificConfig, e.g. aac.Encoder.AudioSpecificConfig()
    MediaLength: nSamples, // source samples per channel, for a sample-accurate edit list
})
for _, au := range accessUnits {
    if err := w.WriteFrame(au); err != nil { /* ... */ }
}
err = w.Close() // patches the mdat size and writes moov

NewWriter needs an io.WriteSeeker because the streamed mdat size is patched once at Close. The edit list trims WriterConfig.EncoderDelay priming samples (default 1024, go-aac's measured value); set EncoderDelay: m4a.NoEdit to omit the edit list.

Read an .m4a back into access units:

r, err := m4a.NewReader(f) // io.ReadSeeker
info := r.Info()           // SampleRate, Channels, ASC, FrameCount, EncoderDelay, Duration
for {
    au, err := r.ReadFrame()
    if err == io.EOF { break }
    if err != nil { /* ... */ }
    // hand au to your AAC decoder
}

Reader.RawStream() returns an io.Reader that frames each access unit exactly as go-aac's pcm.WithRawStream expects, so the two libraries plug together with no glue, allocation-free per frame. For callers that want the bytes directly without the io.Reader framing, Reader.ReadFrameInto(dst) fills a reused buffer instead of allocating one per frame like ReadFrame does (it returns the required length with io.ErrShortBuffer if dst is too small).

aacm4a: the go-aac bridge

The aacm4a subpackage is the one-call path for callers that have interleaved integer PCM and want an .m4a, or have an .m4a and want PCM:

import (
    aacpcm "github.com/tphakala/go-aac/pcm"
    "github.com/tphakala/go-m4a/aacm4a"
)

// Encode interleaved little-endian PCM to a gapless AAC-LC .m4a.
err := aacm4a.EncodeInterleaved(f, aacpcm.Config{
    SampleRate: 48000, BitDepth: 16, Channels: 1, Bitrate: 96000,
}, pcmBytes)

// Decode an .m4a to interleaved S16 PCM with go-aac.
dec, info, err := aacm4a.NewDecoder(f) // *aacpcm.Decoder, m4a.Info, error
_, err = io.Copy(pcmOut, dec)

The go-aac decoder is not edit-list aware: NewDecoder emits every decoded sample, including both the leading priming and the trailing final-frame padding. For sample-accurate output, skip info.EncoderDelay leading samples per channel, then keep only info.Duration-worth of samples (Duration * SampleRate per channel) and discard the rest.

opusm4a and flacm4a: the go-opus and go-flac bridges

The same one-call shape wraps Opus (48 kHz interleaved S16) and FLAC (interleaved 16- or 24-bit):

import (
    "github.com/tphakala/go-m4a/opusm4a"
    "github.com/tphakala/go-m4a/flacm4a"
)

// Opus: gapless (the pre-skip and trailing padding are trimmed by the edit list).
err := opusm4a.EncodeInterleaved(f, opusm4a.Config{SampleRate: 48000, Channels: 2, Bitrate: 96000}, pcm)
pcmOut, info, err := opusm4a.DecodeInterleaved(r) // []byte, m4a.Info, error

// FLAC: lossless, so the decode is bit-identical to the input.
err = flacm4a.EncodeInterleaved(f, flacm4a.Config{SampleRate: 44100, Channels: 1, BitDepth: 16, CompressionLevel: 5}, pcm)
pcmOut, info, err = flacm4a.DecodeInterleaved(r)

As with aacm4a, opusm4a.DecodeInterleaved returns every decoded sample, including the Opus pre-skip priming and the trailing padding, so trim info.EncoderDelay leading samples then keep info.Duration-worth for sample-accurate output. flacm4a needs no trimming (FLAC has no priming).

Both DecodeInterleaved calls stop at m4a.DefaultMaxDecodedBytes. For a file the caller did not produce, or one longer than that ceiling, decode it a frame at a time instead and let nothing accumulate. Both bridges expose the same pair, so the flacm4a calls below have opusm4a counterparts that behave the same way:

info, err := flacm4a.DecodeStream(r, func(pcm []byte) error {
    // pcm aliases a reused buffer: copy whatever outlives this call.
    _, werr := w.Write(pcm)
    return werr
})

// Or keep the one-call shape with a ceiling of your own:
pcmOut, info, err := flacm4a.DecodeInterleavedLimit(r, 8<<20)
if errors.Is(err, m4a.ErrDecodeLimit) {
    // The stream decodes to more than 8 MiB.
}
Fragmented output for HLS

Writer produces ftyp | mdat | moov and needs an io.WriteSeeker, because the mdat size is patched at Close. A live HLS or DASH stream cannot seek, so it uses the fragmented path instead: one initialization segment, then a media segment every couple of seconds, each appended to a buffer the caller owns.

cfg := m4a.WriterConfig{SampleRate: 48000, Channels: 1, ASC: asc}

// Built once, served as the playlist's EXT-X-MAP target.
initSeg, err := m4a.InitSegment(cfg)
if err != nil {
    return err
}
publishInit(initSeg)

fw, err := m4a.NewFragmentWriter(cfg)
if err != nil {
    return err
}

var segment []byte // reused across segments
for _, au := range accessUnits {
    if err := fw.WriteFrame(au); err != nil { // copies au, so reuse it freely
        return err
    }
    // Cut on an access-unit boundary once the target duration is reached.
    if fw.PendingDuration() >= 2*48000 {
        extinf := float64(fw.PendingDuration()) / 48000 // seconds, for the playlist
        segment, err = fw.AppendSegment(segment[:0])
        if err != nil {
            return err
        }
        publish(segment, extinf)
    }
}

WriteFrame copies each access unit into an internal buffer, so an encoder may keep reusing one scratch slice. Both that buffer and the segment assembly are retained across segments: once they have grown, a segment allocates nothing at all. Opus and FLAC use WriteFrameDuration here too, and a segment whose frames do not share a duration automatically carries per-sample durations.

The segments use 64-bit tfdt decode times, because a 32-bit one at 48 kHz wraps after about 24.8 hours of continuous streaming. Reset rebinds a writer to a new stream while keeping its buffers, for pooling across sessions.

Gapless playback and the edit list

A lossy encoder emits priming before the first real sample and pads the final frame: AAC-LC primes 1024 samples (go-aac), Opus 312 at 48 kHz (dOps pre-skip). go-m4a writes an elst edit list whose media_time skips the priming and whose segment_duration (set from WriterConfig.MediaLength) excludes the trailing padding, so a compliant player presents exactly the original audio. The reader surfaces the edit list as Info.EncoderDelay and Info.Duration. FLAC is lossless and has no priming, so no edit list is written.

The fragmented writer trims the priming the same way, with segment_duration 0, the open-ended form used by fragmented-MP4 packagers for a stream whose length is not yet known (MediaLength has no meaning there and is rejected). A codec with no priming gets no edit list on the fragmented path, so FLAC never carries one there. The non-fragmented Writer still writes one unless the caller passes NoEdit, which is what flacm4a does.

This was verified rather than assumed. TestFragmentedEditListTrimsPriming writes the same access units with and without the edit list and decodes both with ffmpeg, asserting the difference is exactly the 1024 priming samples; the same comparison in hls.js on Chromium reports media durations 0.021332 s apart, which is that same frame. Worth knowing, because ffmpeg's HLS fMP4 packager emits an edit list that trims nothing (media_time 0) and its -movflags +empty_moov path emits none at all, so players differ in how much they exercise this. Set EncoderDelay: m4a.NoEdit to opt out and accept the priming as a constant offset of about 21 ms.

License

MIT. See LICENSE. go-m4a is clean-room container code and does not include any code ported from FFmpeg, so it is not bound by go-aac's LGPL.

Documentation

Overview

Package m4a muxes AAC-LC, Opus, or FLAC access units into an MP4/M4A container and demuxes them back out. It is the container half that codecs like go-aac deliberately leave to an external muxer: an edit list (elst) trims the encoder priming so the written file is sample-accurate and gapless.

There are two writers. Writer produces a plain ftyp|mdat|moov file and needs an io.WriteSeeker, because the mdat size is patched at Close. InitSegment and FragmentWriter produce fragmented (CMAF) output for live HLS or DASH: they only ever append, so they can write to a byte slice or a socket. Reading is non-fragmented only, so the package writes a shape Reader deliberately rejects with ErrUnsupported.

The public surface is stdlib-only; the codec bridges (aacm4a, opusm4a, flacm4a) are optional subpackages. The ISO-BMFF byte mechanics live in the internal/box package.

Index

Constants

View Source
const DefaultEncoderDelay = 1024

DefaultEncoderDelay is the number of leading priming samples an AAC-LC encoder emits before the first real sample. It is go-aac's measured low-level encoder priming (one 1024-sample frame) and is the value used when WriterConfig leaves EncoderDelay at zero.

View Source
const DefaultMaxDecodedBytes = 1 << 30

DefaultMaxDecodedBytes is the ceiling the codec bridges apply to a decode that accumulates its output into one slice: flacm4a.DecodeInterleaved and opusm4a.DecodeInterleaved stop and return ErrDecodeLimit rather than hand back more than this. The DecodeInterleavedLimit variants take an explicit limit instead, and DecodeStream is bounded by construction because it never accumulates.

A limit is needed because the decoded size is not proportional to the file. The container's own parsing is bounded by the real file length, so the number of access units an attacker gets is bounded by what they pay for, but what an access unit decodes to is not: a FLAC constant subframe encodes a whole 65535-sample block in a handful of bytes, and a two-byte Opus packet with zero-length DTX frames decodes to 120 ms. FLAC amplifies by four to five orders of magnitude, so a crafted file of tens of kilobytes decodes to something on the order of a gigabyte; Opus is less exposed at three to four orders, because a packet decodes to at most 5760 samples per channel and each one costs a sample-table entry as well, but the loop shape is identical and the bound is worth having in both. Nor is this only an adversarial concern: sixty seconds of ordinary digital silence encodes to about 14 kB of FLAC and decodes back to 11.5 MB, a ratio of 831:1, so a quiet field recording lands in the same place as a crafted one.

1 GiB is deliberately generous, because this is a backstop against amplification rather than a policy on how long a clip may be. It has to sit above the largest honest input, and honest inputs run long: 1 GiB is about 101 minutes of CD-quality 44.1 kHz stereo 16-bit, which covers essentially every album, podcast and lecture recording, about 93 minutes of the same at 48 kHz, about 31 minutes of 24-bit 96 kHz stereo, and about 15 minutes of 48 kHz 8-channel 24-bit. Being generous costs little, because what an attacker has to supply to reach the ceiling stays small either way: the cheapest maximal FLAC access unit is on the order of fifty bytes and decodes to about two megabytes, so tens of kilobytes of crafted input reach 1 GiB just as they reached a quarter of it.

It is not a memory guarantee, and a caller that needs one wants DecodeStream. An accumulating decode grows geometrically (about 1.25x once the buffer is large, not 2x), so while the old array is still live and the new one is being filled the transient peak is close to twice the length reached, and the total allocated over the whole decode, most of it garbage, is around five times it.

View Source
const DefaultOpusPreSkip = 312

DefaultOpusPreSkip is the pre-skip an Opus encoder emits before the first real sample, in samples at the 48 kHz Opus timescale. It is go-opus's measured value (Encoder.PreSkip) and the value used when a CodecOpus WriterConfig leaves OpusPreSkip at zero.

View Source
const NoEdit = -1

NoEdit is the WriterConfig.EncoderDelay sentinel that suppresses the edit list entirely: the writer emits no edts/elst and presents every decoded sample.

Variables

View Source
var (
	// ErrCorrupt indicates a malformed container: a truncated box, a size field
	// that overflows the stream, a missing required box (moov, stbl, esds), or
	// an inconsistent sample table.
	ErrCorrupt = errors.New("go-m4a: corrupt container")

	// ErrUnsupported indicates a well-formed MP4 that falls outside the reader's
	// scope: fragmented input, no audio track, a codec other than AAC-LC, Opus or
	// FLAC, or an esds object type that is not AAC. Fragmented input is the
	// asymmetric case: this package writes it (InitSegment, FragmentWriter) but
	// deliberately does not read it back.
	//
	// The codec bridges return it for the narrower scope they accept, which is a
	// subset of the reader's: a track whose codec is not the one that bridge
	// handles, or a channel layout it does not support. Such a file may be
	// perfectly readable through this package and simply belong to another bridge.
	ErrUnsupported = errors.New("go-m4a: unsupported container")

	// ErrClosed is returned by WriteFrame and Close once the Writer has been
	// closed. A second Close, or any WriteFrame after Close, reports this
	// instead of panicking.
	ErrClosed = errors.New("go-m4a: writer is closed")

	// ErrDecodeLimit indicates that a decode was stopped because its output would
	// have exceeded the caller's byte limit: output landing exactly on the limit
	// is a fit, not an excess. It is returned by the codec bridges
	// (flacm4a, opusm4a) rather than by the container code here, which decodes
	// nothing; it lives in this package so both bridges report the same sentinel
	// and a caller handling a mix of codecs matches one error. See
	// DefaultMaxDecodedBytes for why the limit exists.
	//
	// It is a resource limit, not a verdict on the file. A stream that trips it
	// may be perfectly well-formed and merely longer than the caller allowed, so
	// the remedy is a larger limit or a streaming decode, not treating the input
	// as corrupt.
	ErrDecodeLimit = errors.New("go-m4a: decoded size limit exceeded")
)

Package-wide sentinel errors. They are returned wrapped (via fmt.Errorf with %w) so callers can match with errors.Is while still getting a descriptive message. The Reader shares these with the Writer, so they live here rather than beside a single implementation; ErrDecodeLimit is here for the same reason, shared by the codec bridges rather than by the container code.

Functions

func InitSegment added in v0.3.0

func InitSegment(cfg WriterConfig) ([]byte, error)

InitSegment builds the CMAF initialization segment for cfg: the ftyp box and a moov that describes the track but contains no samples, with an mvex/trex declaring the movie fragmented. It is the payload an HLS playlist references with EXT-X-MAP, and it is constant for a given config, so a caller builds it once per stream.

cfg is the same WriterConfig the non-fragmented Writer takes, so the codec selection, AudioSpecificConfig, STREAMINFO, Opus pre-skip and EncoderDelay all carry over unchanged. Two fields behave differently here. MediaLength has no meaning, because a live stream has no known total length, so a non-zero value is rejected rather than silently ignored. Brand still overrides the ftyp major brand, but the fragmented default is "cmfc" rather than "M4A ", so overriding it moves the CMAF declaration out of the major-brand position (it stays in the compatible-brand list, which is fixed); reusing a config written for the non-fragmented writer is the likely way to do that by accident. Brand never applies to the styp of a media segment, whose brands are fixed by the format.

The encoder priming is trimmed with an edit list, as in the non-fragmented writer: EncoderDelay zero selects the codec's default priming, a positive value trims exactly that many samples, and NoEdit omits the edit list entirely. A codec with no priming at all (FLAC) also gets no edit list, since there would be nothing to trim. The entry uses segment_duration 0, the open-ended form used by fragmented-MP4 packagers for a track whose duration is not yet known; it is a de-facto convention rather than a normative rule, and ffmpeg's HLS fMP4 packager emits exactly the same (0, 0) shape.

Note that ffmpeg's HLS fMP4 packager writes an edit list that trims nothing (media_time 0), and its plain fragmented path (-movflags +empty_moov) writes no edit list at all, so players vary in how much they exercise this. A caller that hits a player which mishandles it can set NoEdit and accept the codec's priming as a constant offset (about 21 ms for AAC-LC at 48 kHz).

Types

type Codec added in v0.2.0

type Codec uint8

Codec identifies the audio codec of a track. It is the discriminator the reader reports in Info.Codec and the writer selects with WriterConfig.Codec. The zero value is CodecAACLC, so existing writers that set only ASC keep muxing AAC-LC.

const (
	// CodecAACLC is MPEG-4 AAC-LC, carried in an mp4a sample entry with an esds
	// box holding the AudioSpecificConfig. It is the zero value and the default.
	CodecAACLC Codec = iota
	// CodecOpus is Opus, carried in an Opus sample entry with a dOps
	// OpusSpecificBox, per the Encapsulation of Opus in ISOBMFF.
	CodecOpus
	// CodecFLAC is FLAC, carried in a fLaC sample entry with a dfLa FLACSpecificBox
	// holding the STREAMINFO metadata block, per the Encapsulation of FLAC in
	// ISOBMFF.
	CodecFLAC
)

func (Codec) String added in v0.2.0

func (c Codec) String() string

String returns the codec's short name.

type FragmentWriter added in v0.3.0

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

FragmentWriter emits the media segments of a fragmented MP4 (CMAF) stream: the styp, moof and mdat boxes that follow the InitSegment. It exists because the non-fragmented Writer needs an io.WriteSeeker to patch the mdat size at Close, which a live HLS stream cannot provide; a FragmentWriter only ever appends.

Access units are buffered with WriteFrame or WriteFrameDuration and flushed as one segment by AppendSegment. The buffers are reused across segments, so a steady-state stream allocates nothing per segment once they have grown.

A FragmentWriter is not safe for concurrent use. One live stream needs one FragmentWriter, which is also what keeps the sequence number and decode time monotonic.

func NewFragmentWriter added in v0.3.0

func NewFragmentWriter(cfg WriterConfig) (*FragmentWriter, error)

NewFragmentWriter creates a FragmentWriter for cfg, validated exactly as InitSegment validates it. The caller is responsible for building the matching init segment; the two must come from the same config.

func (*FragmentWriter) AppendSegment added in v0.3.0

func (f *FragmentWriter) AppendSegment(dst []byte) ([]byte, error)

AppendSegment appends one complete media segment (styp, moof and mdat) for the buffered access units to dst and returns the extended slice, in the manner of the strconv Append functions. Passing a retained buffer as dst[:0] reuses its capacity, which is the point: a live stream reuses one buffer per segment.

It advances the sequence number and the base media decode time, and empties the sample buffers ready for the next segment. It returns an error if no access units are buffered. On any error dst is returned truncated to the length it came in at, so a failed call neither emits a partial segment nor loses the caller's buffer, and the writer's own state is left untouched.

func (*FragmentWriter) BaseMediaDecodeTime added in v0.3.0

func (f *FragmentWriter) BaseMediaDecodeTime() uint64

BaseMediaDecodeTime is the decode time the next segment will start at, in the media timescale: the sum of every duration flushed so far. It is the stream position a caller correlates with wall clock for EXT-X-PROGRAM-DATE-TIME.

func (*FragmentWriter) PendingDuration added in v0.3.0

func (f *FragmentWriter) PendingDuration() uint64

PendingDuration is the total decode duration of the buffered access units, in the media timescale, which is what a caller cuts segments on and what the playlist's EXTINF reports. Read it before AppendSegment, which resets it to zero. Dividing by the sample rate gives seconds.

func (*FragmentWriter) PendingSamples added in v0.3.0

func (f *FragmentWriter) PendingSamples() int

PendingSamples is the number of buffered access units.

func (*FragmentWriter) Reset added in v0.3.0

func (f *FragmentWriter) Reset(cfg WriterConfig) error

Reset rebinds the writer to cfg and returns it to its initial state: sequence number 1, decode time 0, and no pending samples. The sample buffers keep their capacity, so a writer pooled across streams stops allocating after the first. Any samples buffered but not yet flushed are discarded.

func (*FragmentWriter) WriteFrame added in v0.3.0

func (f *FragmentWriter) WriteFrame(au []byte) error

WriteFrame buffers one access unit using the codec's fixed per-sample duration, which exists only for AAC-LC (1024 samples). Opus and FLAC frames vary in duration, so for those it returns an error directing the caller to WriteFrameDuration.

func (*FragmentWriter) WriteFrameDuration added in v0.3.0

func (f *FragmentWriter) WriteFrameDuration(au []byte, sampleDuration uint32) error

WriteFrameDuration buffers one access unit and records sampleDuration, the number of samples per channel it decodes to in the media timescale: 1024 for an AAC-LC access unit, the packet's 48 kHz sample count for Opus, or the block size for FLAC. The access unit is copied, so the caller may reuse its buffer immediately. It rejects an empty access unit and a zero duration.

type Info

type Info struct {
	// SampleRate is the audio sample rate in Hz, derived from the ASC and, when
	// the ASC does not carry an explicit rate, the mp4a AudioSampleEntry.
	SampleRate int

	// Channels is the channel count, normally 1 (mono) or 2 (stereo).
	Channels int

	// Codec identifies the audio codec of the selected track: CodecAACLC,
	// CodecOpus, or CodecFLAC.
	Codec Codec

	// ASC is the MPEG-4 AudioSpecificConfig from the esds DecoderSpecificInfo,
	// suitable for go-aac's pcm.WithRawStream. It is set only for AAC-LC; it is nil
	// for Opus and FLAC. Info returns a fresh copy.
	ASC []byte

	// CodecConfig is the codec-specific configuration recovered from the sample
	// entry: the AudioSpecificConfig for AAC-LC (same bytes as ASC), the
	// OpusSpecificBox (dOps) body for Opus, or the FLAC STREAMINFO metadata block
	// for FLAC. It is what a codec bridge passes to its decoder. Info returns a
	// fresh copy.
	CodecConfig []byte

	// FrameCount is the number of access units (MP4 "samples") in the track.
	FrameCount int

	// EncoderDelay is the leading priming sample count taken verbatim from the
	// edit list media_time. It is 0 when there is no edit list or the first edit
	// is an empty edit (media_time -1).
	EncoderDelay int64

	// Duration is the track presentation duration after the edit list.
	Duration time.Duration

	// Brand is the ftyp major brand.
	Brand string
}

Info summarizes an MP4/M4A file's single audio track (AAC-LC, Opus, or FLAC), populated by NewReader from the moov metadata. The fields come straight from the file's own sample tables, the codec-specific box (esds, dOps, or dfLa), and the edit list; Codec reports which codec, and CodecConfig carries its configuration.

type Reader

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

Reader demuxes a non-fragmented MP4/M4A file (AAC-LC, Opus, or FLAC) into its access units. NewReader parses the moov metadata up front; ReadFrame then returns each access unit in order by seeking to its computed (offset, size). The reader is not safe for concurrent use, and ReadFrame, RawStream, and their shared cursor advance together.

func NewReader

func NewReader(r io.ReadSeeker) (*Reader, error)

NewReader reads and parses the moov metadata from r and returns a Reader positioned at the first access unit. It locates moov whether it precedes or follows mdat, selects the first "soun" track carrying a supported sample entry (mp4a AAC-LC, Opus, or fLaC), and builds the sample geometry from the stsc/stsz/stco tables. It returns a wrapped ErrCorrupt for malformed input and a wrapped ErrUnsupported for well-formed input outside scope (fragmented files, no supported audio track, an unsupported codec, or an AAC object type that is not MPEG-4 Audio).

func (*Reader) ASC

func (rd *Reader) ASC() []byte

ASC returns a fresh copy of the AudioSpecificConfig, suitable for passing to go-aac's pcm.WithRawStream.

func (*Reader) Info

func (rd *Reader) Info() Info

Info returns a copy of the parsed track summary, including a fresh copy of the ASC so the caller cannot mutate the Reader's state.

func (*Reader) RawStream

func (rd *Reader) RawStream() io.Reader

RawStream returns an io.Reader that emits each access unit framed as a 2-byte big-endian length prefix followed by the access-unit bytes, exactly the framing go-aac's pcm.WithRawStream consumes. It shares the Reader's cursor with ReadFrame and reports ErrUnsupported if any access unit exceeds 65535 bytes (impossible for AAC-LC, guarded regardless).

func (*Reader) ReadFrame

func (rd *Reader) ReadFrame() ([]byte, error)

ReadFrame returns the next access unit in decode order and advances the cursor. It returns io.EOF after the last access unit. A frame whose computed extent falls outside the stream, or a short read, is a wrapped ErrCorrupt.

func (*Reader) ReadFrameInto added in v0.1.1

func (rd *Reader) ReadFrameInto(dst []byte) (int, error)

ReadFrameInto reads the next access unit into dst and returns its length, advancing the cursor. It is the zero-allocation form of ReadFrame for callers that reuse a buffer across frames. If dst is too small to hold the frame, ReadFrameInto reads nothing, does not advance, and returns the required length with io.ErrShortBuffer, so the caller can grow dst and call again. It returns io.EOF after the last access unit, and a wrapped ErrCorrupt for a frame whose extent falls outside the stream or a short read.

type Writer

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

Writer streams AAC-LC, Opus, or FLAC access units into an MP4/M4A file (selected by WriterConfig.Codec, defaulting to AAC-LC). The on-disk layout is ftyp | mdat | moov: ftyp and the mdat header are written up front, each WriteFrame (or WriteFrameDuration) appends one access unit to the mdat payload, and Close patches the mdat size and writes the moov metadata. It requires an io.WriteSeeker because the mdat size is a placeholder patched once at Close.

func NewWriter

func NewWriter(w io.WriteSeeker, cfg WriterConfig) (*Writer, error)

NewWriter validates cfg for its codec (the AAC-LC ASC against SampleRate and Channels, the Opus rate, or the FLAC STREAMINFO), then writes the ftyp box and the placeholder mdat header to w. It returns an error, prefixed "go-m4a: ", when the writer is nil, the codec configuration is malformed or disagrees with SampleRate or Channels, the sample rate is unsupported, or an initial write fails.

func (*Writer) Close

func (w *Writer) Close() error

Close finalizes the file: it patches the streamed mdat largesize, seeks past the payload, and writes the moov metadata (mvhd, trak with tkhd, optional edts/elst, and mdia down to the sample tables). It reports an error if no frames were written or a write fails. After a successful Close a second call returns ErrClosed. A Close that fails on a transient Seek or Write may be retried (WriteFrame stays rejected in between); a Close after a failed WriteFrame returns that latched error and writes nothing.

func (*Writer) WriteFrame

func (w *Writer) WriteFrame(au []byte) error

WriteFrame appends one access unit to the mdat payload using the codec's fixed per-sample duration. It works for AAC-LC, whose every AU is 1024 samples. For Opus and FLAC, whose frames vary in duration, it returns an error directing the caller to WriteFrameDuration. It rejects a nil or empty access unit, and any call after Close.

func (*Writer) WriteFrameDuration added in v0.2.0

func (w *Writer) WriteFrameDuration(au []byte, sampleDuration uint32) error

WriteFrameDuration appends one access unit to the mdat payload and records its size for the stsz table and sampleDuration (in the media timescale) for the stts table. sampleDuration is the number of samples per channel the access unit decodes to: 1024 for an AAC-LC AU, the packet's 48 kHz sample count for Opus, or the block size for FLAC. It rejects a nil or empty access unit, a zero duration, and any call after Close.

type WriterConfig

type WriterConfig struct {
	// Codec selects the audio codec. The zero value is CodecAACLC, so a config
	// that sets only ASC keeps muxing AAC-LC. For CodecOpus set OpusPreSkip and
	// OpusInputSampleRate (and SampleRate must be 48000); for CodecFLAC set
	// STREAMINFO.
	Codec Codec

	// SampleRate is the audio sample rate in Hz (for example 48000). Required, and
	// bounded for every codec by what the sample entry can represent, so at most
	// maxAudioSampleEntryRate (65535). Within that the accepted set is per codec:
	// for AAC-LC it must be one of the eleven MPEG-4 sampling-frequency table
	// rates that fit, 7350 to 64000, and must match the rate encoded in ASC; for
	// Opus it must be 48000, the fixed Opus container timescale; for FLAC any
	// positive rate is accepted, because FLAC has no rate table.
	//
	// For FLAC it should also agree with the rate inside STREAMINFO. That is the
	// encapsulation's requirement, not one this package enforces: NewWriter does
	// not compare them and will not reject a mismatch. Keeping them in step is the
	// caller's job, and it matters because the two fields are read by different
	// consumers. This package's Reader reports the sample entry, while a
	// conforming decoder takes the authoritative rate from STREAMINFO (Xiph,
	// Encapsulation of FLAC in ISOBMFF, section 3.3.1), so a config where they
	// disagree produces a file that go-m4a and ffmpeg read differently, with no
	// error from either.
	SampleRate int

	// Channels is the channel count, 1 (mono) or 2 (stereo). Required, and for
	// AAC-LC it must match the channel configuration encoded in ASC.
	Channels int

	// ASC is the MPEG-4 AudioSpecificConfig (two bytes for AAC-LC). Required for
	// CodecAACLC; ignored otherwise. The writer copies the bytes verbatim into the
	// esds DecoderSpecificInfo.
	ASC []byte

	// OpusPreSkip is the Opus pre-skip in samples at 48 kHz (go-opus's
	// Encoder.PreSkip). It fills the dOps PreSkip field and the edit-list media
	// time. Used only for CodecOpus; zero selects DefaultOpusPreSkip.
	OpusPreSkip int

	// OpusInputSampleRate is the original source sample rate recorded in the dOps
	// InputSampleRate field (informational; Opus always decodes at 48 kHz). Used
	// only for CodecOpus; zero selects SampleRate.
	OpusInputSampleRate int

	// STREAMINFO is the 34-byte FLAC STREAMINFO metadata block, the payload of the
	// dfLa box (from go-flac's pcm.FrameEncoder.StreamInfoBytes). Required for
	// CodecFLAC; ignored otherwise.
	STREAMINFO []byte

	// EncoderDelay is the number of leading priming samples to trim with an edit
	// list. Zero uses the codec's default priming (DefaultEncoderDelay, 1024, for
	// AAC-LC; the resolved OpusPreSkip, default 312, for Opus; 0 for FLAC); NoEdit
	// writes no edit list at all; a positive value trims exactly that many samples.
	EncoderDelay int

	// MediaLength, when greater than zero, is the number of PCM samples per
	// channel the source contained. It sets the edit-list segment duration
	// exactly, so trailing final-frame padding is also excluded. Zero presents
	// every decoded sample after the priming. A live fragmented stream has no
	// known total length, so InitSegment and NewFragmentWriter reject any non-zero
	// value rather than ignore it.
	MediaLength int64

	// Brand overrides the ftyp major brand. When set it must be exactly four bytes
	// (space-padded, for example "mp42"); the constructors reject any other
	// length. NewWriter defaults it to "M4A " and always lists "M4A ", "mp42" and
	// "isom" as compatible brands. InitSegment defaults it to "cmfc" and lists
	// "cmfc", "iso6" and "isom" instead, so overriding it there moves the CMAF
	// declaration out of the major-brand position, though it stays in the
	// compatible-brand list; it never affects a media segment's styp.
	Brand string
}

WriterConfig configures a Writer, and also the fragmented writers InitSegment and NewFragmentWriter. SampleRate and Channels must agree with ASC; the constructors validate them against it and refuse a mismatch. Two fields mean something different on the fragmented path, noted on the fields themselves: MediaLength is rejected there, and Brand has a different default.

Directories

Path Synopsis
Package aacm4a is an optional convenience bridge that couples go-aac's AAC-LC codec to the go-m4a container.
Package aacm4a is an optional convenience bridge that couples go-aac's AAC-LC codec to the go-m4a container.
Package flacm4a is an optional convenience bridge that couples go-flac's FLAC codec to the go-m4a container.
Package flacm4a is an optional convenience bridge that couples go-flac's FLAC codec to the go-m4a container.
internal
box
Package box implements the low-level ISO Base Media File Format (ISO/IEC 14496-12) byte primitives and typed marshalers that the go-m4a writer emits.
Package box implements the low-level ISO Base Media File Format (ISO/IEC 14496-12) byte primitives and typed marshalers that the go-m4a writer emits.
Package opusm4a is an optional convenience bridge that couples go-opus's Opus codec to the go-m4a container.
Package opusm4a is an optional convenience bridge that couples go-opus's Opus codec to the go-m4a container.

Jump to

Keyboard shortcuts

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