m4a

package module
v0.1.1 Latest Latest
Warning

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

Go to latest
Published: Jul 18, 2026 License: MIT Imports: 6 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 audio. No cgo and no external binaries. It is the container half that go-aac deliberately leaves 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 from AAC-LC access units, with an edit list that trims the encoder priming so playback is gapless and sample-accurate. Read an .m4a back into access units that decode with go-aac, ffmpeg, QuickTime, or any AAC decoder.

Status

  • Writer: complete for AAC-LC. Streams ftyp | mdat | moov to an io.WriteSeeker, with a proper esds (including the mandatory SLConfigDescriptor), a single-track sample table, and an edts/elst edit list. Output is validated with ffprobe and decodes byte-for-byte back to the source through ffmpeg: a go-aac-encoded chirp muxed by go-m4a and decoded by ffmpeg reproduces the input with a cross-correlation peak of 1.0 at lag 0.
  • Reader: complete for AAC-LC. Locates moov whether it precedes or follows mdat, tolerates the extra boxes real encoders emit (free, udta, meta, sgpd, sbgp), expands arbitrary multi-chunk stsc tables, and extracts the ASC from esds. It reads files produced by ffmpeg and Apple's afconvert (mono and stereo, 44.1 and 48 kHz, single and multi chunk, edit list or iTunSMPB), yielding access units that decode identically to the reference extraction. Parsing is bounds-checked throughout and never panics on malformed input.

Scope in v1: non-fragmented MP4, a single AAC-LC audio track, mono or stereo, 44.1 or 48 kHz, matching what go-aac encodes and decodes. Out of scope (the reader returns a typed ErrUnsupported, never crashes): fragmented MP4, video or multiple audio tracks, codecs other than AAC-LC, HE-AAC, and writing metadata tags. Apple iTunSMPB gapless tags are not parsed; the reader reports the edit list, so an Apple file with no elst reports an encoder delay of 0.

Install

go get github.com/tphakala/go-m4a

The core m4a package is stdlib-only. The optional aacm4a subpackage wires the container to go-aac and pulls that module in; import it only if you use it.

Usage

m4a: the container

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.

Gapless playback and the edit list

An AAC-LC encoder emits a frame of priming (1024 samples for go-aac) before the first real sample, and pads the final frame. ADTS cannot signal either, so raw AAC streams decode with roughly 1024 extra leading samples. 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.

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 access units into an MP4/M4A container and demuxes them back out. It is the container half that go-aac deliberately leaves to an external muxer: an edit list (elst) trims the encoder priming so the written file is sample-accurate and gapless. The public surface is stdlib-only; the ISO-BMFF byte mechanics live in the internal/box package, whose layout is fixed by docs/box-layout.md.

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 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 v1
	// scope: fragmented input, no AAC-LC audio track, a non-mp4a codec, or an
	// object type other than AAC-LC.
	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")
)

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.

Functions

This section is empty.

Types

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

	// ASC is the MPEG-4 AudioSpecificConfig from the esds DecoderSpecificInfo,
	// suitable for go-aac's pcm.WithRawStream. Info returns a fresh copy.
	ASC []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 AAC-LC audio track, populated by NewReader from the moov metadata. It is codec-agnostic beyond AAC: the fields come straight from the file's own sample tables, esds, and edit list.

type Reader

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

Reader demuxes a non-fragmented MP4/M4A file into its AAC-LC 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 an mp4a AAC-LC sample entry, 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 the v1 scope (fragmented files, no AAC-LC audio track, a non-mp4a codec, or a non-AAC object type).

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 access units into an MP4/M4A file. The on-disk layout is ftyp | mdat | moov: ftyp and the mdat header are written up front, each WriteFrame 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 against its ASC, 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 ASC 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 raw AAC-LC access unit to the mdat payload and records its size for the stsz table. It rejects a nil or empty access unit, and any call after Close, with an error.

type WriterConfig

type WriterConfig struct {
	// SampleRate is the audio sample rate in Hz (for example 48000). Required,
	// and it must match the rate encoded in ASC.
	SampleRate int

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

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

	// EncoderDelay is the number of leading priming samples to trim with an edit
	// list. Zero uses DefaultEncoderDelay (1024); 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.
	MediaLength int64

	// Brand overrides the ftyp major brand (default "M4A "). When set it must be
	// exactly four bytes (space-padded, for example "mp42"); NewWriter rejects
	// any other length. The compatible brands always include "M4A ", "mp42", and
	// "isom".
	Brand string
}

WriterConfig configures a Writer. SampleRate and Channels must agree with ASC; NewWriter validates them against it and refuses a mismatch.

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.
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.

Jump to

Keyboard shortcuts

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