AAC ADTS Decoder (Pull)

This article explains how you can use Transcoder.pull to decode an AAC ADTS elementary stream.

The code snippets in this article are from the dec_aac_adts_pull Python sample.

Source Audio

For source we use the Hydrate-Kenny_Beltrey.aac file from the AVBlocks Assets repository. After downloading and unzipping you will find Hydrate-Kenny_Beltrey.aac in the aud subdirectory.

Code

This code takes an AAC ADTS stream and decodes it to uncompressed LPCM samples. The AAC decoder Transcoder returns the decoded samples with Transcoder.pull, and a second Transcoder writes them to a WAV file using Transcoder.push.

Initialize AVBlocks

The first step in any AVBlocks application is to initialize the library. This must be done before using any other AVBlocks functionality. Call Library.shutdown() at the end of the program to clean up resources.

Library.initialize()

# Set license information. Without this AVBlocks runs in Demo mode.
# Library.set_license("<license-string>")

result = decode(input_file, output_file)

Library.shutdown()

sys.exit(0 if result else 2)

Configure Transcoder

The decoder Transcoder has the AAC file as input, and an output socket with a single LPCM pin and no file, because the decoded samples are pulled. The WAV writer is a second Transcoder that takes raw LPCM samples as input and writes a WAV file.

def create_decoder_output_socket() -> MediaSocket:
    """Create output socket for decoder (LPCM output)."""
    socket = MediaSocket()
    socket.stream_type = StreamType.LPCM

    pin = MediaPin()
    asi = AudioStreamInfo()
    asi.stream_type = StreamType.LPCM
    asi.channels = 2
    asi.sample_rate = 48000
    asi.bits_per_sample = 16

    pin.stream_info = asi
    socket.pins.add(pin)

    return socket


def create_wav_writer(output_file: str) -> Transcoder:
    """Create transcoder for writing WAV file."""
    # Input stream info, pin and socket
    infmt = AudioStreamInfo()
    infmt.stream_type = StreamType.LPCM
    infmt.channels = 2
    infmt.sample_rate = 48000
    infmt.bits_per_sample = 16

    in_pin = MediaPin()
    in_pin.stream_info = infmt

    in_socket = MediaSocket()
    in_socket.stream_type = StreamType.LPCM
    in_socket.pins.add(in_pin)

    # Output stream info, pin and socket
    outfmt = AudioStreamInfo()
    outfmt.stream_type = StreamType.LPCM
    outfmt.channels = 2
    outfmt.sample_rate = 48000
    outfmt.bits_per_sample = 16

    out_pin = MediaPin()
    out_pin.stream_info = outfmt

    out_socket = MediaSocket()
    out_socket.stream_type = StreamType.WAVE
    out_socket.pins.add(out_pin)
    out_socket.file = output_file

    # Create transcoder
    wav_writer = Transcoder()
    wav_writer.allow_demo_mode = True
    wav_writer.inputs.add(in_socket)
    wav_writer.outputs.add(out_socket)

    return wav_writer

Call Transcoder.pull

The decode function opens the decoder and the WAV writer, then pulls PCM samples from the decoder and pushes them to the WAV writer. Each call to Transcoder.pull returns one audio frame. At end of stream (CodecError.EOS) we push None to the WAV writer to signal end of stream.

def decode(input_file: str, output_file: str) -> bool:
    """Decode AAC ADTS file to WAV using pull mode."""
    # Transcoder will fail if output exists (by design)
    delete_file(output_file)

    # Create output directory if it doesn't exist
    output_dir = os.path.dirname(output_file)
    if output_dir and not os.path.exists(output_dir):
        os.makedirs(output_dir)

    # Create decoder transcoder
    with Transcoder() as decoder:
        decoder.allow_demo_mode = True

        input_socket = MediaSocket()
        input_socket.file = input_file
        decoder.inputs.add(input_socket)
        decoder.outputs.add(create_decoder_output_socket())

        if not decoder.open():
            print_error("Decoder open", decoder.error)
            return False

        # Create WAV writer transcoder
        wav_writer = create_wav_writer(output_file)

        if not wav_writer.open():
            print_error("WAV Writer open", wav_writer.error)
            decoder.close()
            return False

        # Pull-push decoding loop
        pcm_sample = MediaSample()
        decoder_eos = False

        while not decoder_eos:
            # Pull PCM sample from decoder
            res, _ = decoder.pull(pcm_sample)

            if res:
                # Push PCM sample to WAV writer
                if not wav_writer.push(0, pcm_sample):
                    print_error("WAV Writer push", wav_writer.error)
                    decoder.close()
                    wav_writer.close()
                    return False
                continue

            # No more PCM data from decoder
            error = decoder.error
            if error.facility == ErrorFacility.Codec and error.code == CodecError.EOS:
                # Push None to signal EOS to WAV writer
                wav_writer.push(0, None)
                decoder_eos = True
                continue

            # Decoder error
            print_error("Decoder pull", error)
            decoder.close()
            wav_writer.close()
            return False

        wav_writer.close()
        decoder.close()

    print("Decoding completed successfully.")
    return True

Complete Program

"""
Decode AAC ADTS file to WAV using pull mode.

This sample decodes an AAC ADTS audio file to a WAV file using the pull mode
(manually pulling decoded PCM samples and writing to output).
"""

import os
import sys
import click

from avblocks import (
    Library, Transcoder, MediaSocket, MediaPin,
    AudioStreamInfo, MediaSample, StreamType,
    ErrorFacility, CodecError
)


def delete_file(filename: str):
    """Delete a file if it exists."""
    try:
        if os.path.exists(filename):
            os.remove(filename)
    except OSError:
        pass


def print_error(action: str, error) -> None:
    """Print error information."""
    if action:
        print(f"{action}: ", end="")

    if error.facility == ErrorFacility.Success:
        print("Success")
        return

    print(f"{error.message or ''}, facility:{error.facility} code:{error.code} hint:{error.hint or ''}")


def create_decoder_output_socket() -> MediaSocket:
    """Create output socket for decoder (LPCM output)."""
    socket = MediaSocket()
    socket.stream_type = StreamType.LPCM

    pin = MediaPin()
    asi = AudioStreamInfo()
    asi.stream_type = StreamType.LPCM
    asi.channels = 2
    asi.sample_rate = 48000
    asi.bits_per_sample = 16

    pin.stream_info = asi
    socket.pins.add(pin)

    return socket


def create_wav_writer(output_file: str) -> Transcoder:
    """Create transcoder for writing WAV file."""
    # Input stream info, pin and socket
    infmt = AudioStreamInfo()
    infmt.stream_type = StreamType.LPCM
    infmt.channels = 2
    infmt.sample_rate = 48000
    infmt.bits_per_sample = 16

    in_pin = MediaPin()
    in_pin.stream_info = infmt

    in_socket = MediaSocket()
    in_socket.stream_type = StreamType.LPCM
    in_socket.pins.add(in_pin)

    # Output stream info, pin and socket
    outfmt = AudioStreamInfo()
    outfmt.stream_type = StreamType.LPCM
    outfmt.channels = 2
    outfmt.sample_rate = 48000
    outfmt.bits_per_sample = 16

    out_pin = MediaPin()
    out_pin.stream_info = outfmt

    out_socket = MediaSocket()
    out_socket.stream_type = StreamType.WAVE
    out_socket.pins.add(out_pin)
    out_socket.file = output_file

    # Create transcoder
    wav_writer = Transcoder()
    wav_writer.allow_demo_mode = True
    wav_writer.inputs.add(in_socket)
    wav_writer.outputs.add(out_socket)

    return wav_writer


def decode(input_file: str, output_file: str) -> bool:
    """Decode AAC ADTS file to WAV using pull mode."""
    # Transcoder will fail if output exists (by design)
    delete_file(output_file)

    # Create output directory if it doesn't exist
    output_dir = os.path.dirname(output_file)
    if output_dir and not os.path.exists(output_dir):
        os.makedirs(output_dir)

    # Create decoder transcoder
    with Transcoder() as decoder:
        decoder.allow_demo_mode = True

        input_socket = MediaSocket()
        input_socket.file = input_file
        decoder.inputs.add(input_socket)
        decoder.outputs.add(create_decoder_output_socket())

        if not decoder.open():
            print_error("Decoder open", decoder.error)
            return False

        # Create WAV writer transcoder
        wav_writer = create_wav_writer(output_file)

        if not wav_writer.open():
            print_error("WAV Writer open", wav_writer.error)
            decoder.close()
            return False

        # Pull-push decoding loop
        pcm_sample = MediaSample()
        decoder_eos = False

        while not decoder_eos:
            # Pull PCM sample from decoder
            res, _ = decoder.pull(pcm_sample)

            if res:
                # Push PCM sample to WAV writer
                if not wav_writer.push(0, pcm_sample):
                    print_error("WAV Writer push", wav_writer.error)
                    decoder.close()
                    wav_writer.close()
                    return False
                continue

            # No more PCM data from decoder
            error = decoder.error
            if error.facility == ErrorFacility.Codec and error.code == CodecError.EOS:
                # Push None to signal EOS to WAV writer
                wav_writer.push(0, None)
                decoder_eos = True
                continue

            # Decoder error
            print_error("Decoder pull", error)
            decoder.close()
            wav_writer.close()
            return False

        wav_writer.close()
        decoder.close()

    print("Decoding completed successfully.")
    return True


@click.command()
@click.option('-i', '--input', 'input_file',
              help='Input AAC file',
              type=click.Path(exists=True))
@click.option('-o', '--output', 'output_file',
              help='Output WAV file',
              type=click.Path())
def main(input_file: str, output_file: str):
    """Decode AAC ADTS file to WAV using pull mode."""

    # Set default options if not provided
    if not input_file or not output_file:
        script_dir = os.path.dirname(os.path.abspath(__file__))

        if not input_file:
            input_file = os.path.join(script_dir, "../../assets/aud/Hydrate-Kenny_Beltrey.adts.aac")
        if not output_file:
            output_file = os.path.join(script_dir, "../../output/dec_aac_adts_pull/Hydrate-Kenny_Beltrey.wav")

        print("Using default options:")
        print(f"  --input {input_file}")
        print(f"  --output {output_file}")
        print()

    # Validate options
    print(f"--input: {input_file}")
    print(f"--output: {output_file}")

    Library.initialize()

    # Set license information. Without this AVBlocks runs in Demo mode.
    # Library.set_license("<license-string>")

    result = decode(input_file, output_file)

    Library.shutdown()

    sys.exit(0 if result else 2)


if __name__ == '__main__':
    main()

How to run

See the Python sample for details. The samples are installed as command-line tools when you install the package from the repository root with uv pip install --editable . (see Installation).

Command Line

dec-aac-adts-pull --input <aac file> --output <wav file>

Examples

List options:

dec-aac-adts-pull --help
Usage: dec-aac-adts-pull [OPTIONS]

  Decode AAC ADTS file to WAV using pull mode.

Options:
  -i, --input PATH   Input AAC file
  -o, --output PATH  Output WAV file
  --help             Show this message and exit.

Decode the input file ./assets/aud/Hydrate-Kenny_Beltrey.adts.aac into output file ./output/dec_aac_adts_pull/Hydrate-Kenny_Beltrey.wav:

# Linux and macOS
mkdir -p ./output/dec_aac_adts_pull

dec-aac-adts-pull \
    --input ./assets/aud/Hydrate-Kenny_Beltrey.adts.aac \
    --output ./output/dec_aac_adts_pull/Hydrate-Kenny_Beltrey.wav
# Windows
New-Item -ItemType Directory -Force -Path ./output/dec_aac_adts_pull

dec-aac-adts-pull `
    --input ./assets/aud/Hydrate-Kenny_Beltrey.adts.aac `
    --output ./output/dec_aac_adts_pull/Hydrate-Kenny_Beltrey.wav