AAC ADTS Decoder (Pull)

This article explains how you can use TTranscoder::pull to decode an AAC (Advanced Audio Coding) ADTS (Audio Data Transport Stream) elementary stream.

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

Linux and Windows samples are also available:

Source Audio

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

Code

This code takes an AAC ADTS stream and decodes it to uncompressed LPCM samples.

Initialize AVBlocks

The first step in any AVBlocks application is to initialize the library. This must be done before using any other AVBlocks functionality. The TLibrary constructor initializes the library and loads the necessary codecs. The TLibrary destructor shuts the library down and releases any allocated memory, so the library is terminated when the TLibrary object goes out of scope at the end of main.

int main(int argc, char *argv[])
{
    Options opt;

    switch (prepareOptions(opt, argc, argv))
    {
    case Command:
        return 0;
    case Error:
        return 1;
    case Parsed:
        break;
    }

    TLibrary library;
    bool result = decode(opt);
    return result ? 0 : 1;
}

Configure Decoder Output Socket

The decoder output socket defines the format of the decoded audio data. In this case, we’re configuring it to output uncompressed LPCM audio with 2 channels at 48 kHz with 16-bit samples. The socket represents the output destination, while the pin represents the specific audio stream within that destination. The TAudioStreamInfo object specifies the audio format parameters.

TMediaSocket()
    .streamType(StreamType::LPCM)
    .addPin(
        TMediaPin()
            .streamInfo(TAudioStreamInfo()
                .streamType(StreamType::LPCM)
                .channels(2)
                .sampleRate(48000)
                .bitsPerSample(16)
            )
    )

Configure WAV Writer Socket

The WAV writer transcoder takes the decoded PCM samples and writes them to a WAV file. It creates an input socket matching the decoder output format and an output socket configured for WAV container format. The output file path is set on the WAV socket.

TTranscoder wavWriter;
wavWriter.allowDemoMode(true)
    .addInput(
        TMediaSocket()
            .streamType(StreamType::LPCM)
            .addPin(
                TMediaPin()
                    .streamInfo(TAudioStreamInfo()
                        .streamType(StreamType::LPCM)
                        .channels(2)
                        .sampleRate(48000)
                        .bitsPerSample(16)
                    )
            )
    )
    .addOutput(
        TMediaSocket()
            .file(opt.outputFile)
            .streamType(StreamType::WAVE)
            .addPin(
                TMediaPin()
                    .streamInfo(TAudioStreamInfo()
                        .streamType(StreamType::LPCM)
                        .channels(2)
                        .sampleRate(48000)
                        .bitsPerSample(16)
                    )
            )
    )
    .open();

Decode with Pull

This is the main decoding function that uses the pull method to retrieve decoded PCM samples from the AAC decoder and pushes them to a WAV writer transcoder. The pull loop continues until the decoder returns EOS (End of Stream). When EOS is reached, pushEos is called on the WAV writer to signal the end of the stream.

The function checks the error code to determine if the decoding completed successfully or if an actual error occurred.

bool decode(Options &opt)
{
    // transcoder will fail if output exists (by design)
    deleteFile(primo::ustring(opt.outputFile));

    try {
        // Create decoder transcoder - input from file, output PCM without file
        TTranscoder decoder;
        decoder.allowDemoMode(true)
            .addInput(
                TMediaSocket()
                    .file(opt.inputFile)
            )
            .addOutput(
                TMediaSocket()
                    .streamType(StreamType::LPCM)
                    .addPin(
                        TMediaPin()
                            .streamInfo(TAudioStreamInfo()
                                .streamType(StreamType::LPCM)
                                .channels(2)
                                .sampleRate(48000)
                                .bitsPerSample(16)
                            )
                    )
            )
            .open();

        // Create WAV writer transcoder - input PCM without file, output to file
        TTranscoder wavWriter;
        wavWriter.allowDemoMode(true)
            .addInput(
                TMediaSocket()
                    .streamType(StreamType::LPCM)
                    .addPin(
                        TMediaPin()
                            .streamInfo(TAudioStreamInfo()
                                .streamType(StreamType::LPCM)
                                .channels(2)
                                .sampleRate(48000)
                                .bitsPerSample(16)
                            )
                    )
            )
            .addOutput(
                TMediaSocket()
                    .file(opt.outputFile)
                    .streamType(StreamType::WAVE)
                    .addPin(
                        TMediaPin()
                            .streamInfo(TAudioStreamInfo()
                                .streamType(StreamType::LPCM)
                                .channels(2)
                                .sampleRate(48000)
                                .bitsPerSample(16)
                            )
                    )
            )
            .open();

        // Pull-push decoding loop
        int32_t decoderOutputIndex = 0;
        TMediaSample pcmSample;

        bool decoderEos = false;
        while (!decoderEos) {
            // Pull PCM sample from decoder
            if (decoder.pull(decoderOutputIndex, pcmSample)) {
                // Push PCM sample to WAV writer
                if (!wavWriter.push(0, pcmSample)) {
                    printError("WAV Writer push", wavWriter.error());
                    return false;
                }
                continue;
            }

            // No more PCM data from decoder
            const auto error = decoder.error();
            if (error.facility() == primo::error::ErrorFacility::Codec &&
                error.code()     == primo::codecs::CodecError::EOS) {
                // Signal EOS to WAV writer
                wavWriter.pushEos(0);
                decoderEos = true;
                continue;
            }

            printError("Decoder pull", error);
            return false;
        }

        decoder.close();
        wavWriter.close();

        return true;
    }
    catch (const TAVBlocksException& ex) {
        println(stderr, "AVBlocks error: {}", ex.what());
        return false;
    }
}

Complete C++ Code

Here’s the complete working example that demonstrates AAC ADTS decoding using AVBlocks. This code combines all the previous snippets into a functional program that can be compiled and run. The main function handles command-line argument parsing, initializes AVBlocks, performs the decoding operation using pull, and properly shuts down the library before exiting.

#include <primo/avblocks/avb++.h>

#include "util.h"
#include "options.h"

using namespace primo::codecs;
using namespace primo::avblocks::modern;
using namespace std;


bool decode(Options &opt)
{
    // transcoder will fail if output exists (by design)
    deleteFile(primo::ustring(opt.outputFile));

    try {
        // Create decoder transcoder - input from file, output PCM without file
        TTranscoder decoder;
        decoder.allowDemoMode(true)
            .addInput(
                TMediaSocket()
                    .file(opt.inputFile)
            )
            .addOutput(
                TMediaSocket()
                    .streamType(StreamType::LPCM)
                    .addPin(
                        TMediaPin()
                            .streamInfo(TAudioStreamInfo()
                                .streamType(StreamType::LPCM)
                                .channels(2)
                                .sampleRate(48000)
                                .bitsPerSample(16)
                            )
                    )
            )
            .open();

        // Create WAV writer transcoder - input PCM without file, output to file
        TTranscoder wavWriter;
        wavWriter.allowDemoMode(true)
            .addInput(
                TMediaSocket()
                    .streamType(StreamType::LPCM)
                    .addPin(
                        TMediaPin()
                            .streamInfo(TAudioStreamInfo()
                                .streamType(StreamType::LPCM)
                                .channels(2)
                                .sampleRate(48000)
                                .bitsPerSample(16)
                            )
                    )
            )
            .addOutput(
                TMediaSocket()
                    .file(opt.outputFile)
                    .streamType(StreamType::WAVE)
                    .addPin(
                        TMediaPin()
                            .streamInfo(TAudioStreamInfo()
                                .streamType(StreamType::LPCM)
                                .channels(2)
                                .sampleRate(48000)
                                .bitsPerSample(16)
                            )
                    )
            )
            .open();

        // Pull-push decoding loop
        int32_t decoderOutputIndex = 0;
        TMediaSample pcmSample;

        bool decoderEos = false;
        while (!decoderEos) {
            // Pull PCM sample from decoder
            if (decoder.pull(decoderOutputIndex, pcmSample)) {
                // Push PCM sample to WAV writer
                if (!wavWriter.push(0, pcmSample)) {
                    printError("WAV Writer push", wavWriter.error());
                    return false;
                }
                continue;
            }

            // No more PCM data from decoder
            const auto error = decoder.error();
            if (error.facility() == primo::error::ErrorFacility::Codec &&
                error.code()     == primo::codecs::CodecError::EOS) {
                // Signal EOS to WAV writer
                wavWriter.pushEos(0);
                decoderEos = true;
                continue;
            }

            printError("Decoder pull", error);
            return false;
        }

        decoder.close();
        wavWriter.close();

        return true;
    }
    catch (const TAVBlocksException& ex) {
        println(stderr, "AVBlocks error: {}", ex.what());
        return false;
    }
}

int main(int argc, char *argv[])
{
    Options opt;

    switch (prepareOptions(opt, argc, argv))
    {
    case Command:
        return 0;
    case Error:
        return 1;
    case Parsed:
        break;
    }

    TLibrary library;
    bool result = decode(opt);
    return result ? 0 : 1;
}

How to Run

See the build instructions for macOS and the dec_aac_adts_pull example for details.

Command Line

./dec_aac_adts_pull --input <aac file> --output <wav file>

Examples

List options:

./bin/x64/dec_aac_adts_pull --help

dec_aac_adts_pull --input <aac file> --output <wav file>
  -h,    --help
  -i,    --input    input AAC file
  -o,    --output   output WAV file

The following example decodes input file Hydrate-Kenny_Beltrey.adts.aac into output file Hydrate-Kenny_Beltrey.wav:

mkdir -p ./output/dec_aac_adts_pull

./bin/x64/dec_aac_adts_pull \
  --input ./assets/aud/Hydrate-Kenny_Beltrey.adts.aac \
  --output ./output/dec_aac_adts_pull/Hydrate-Kenny_Beltrey.wav

Copy the Hydrate-Kenny_Beltrey.aac file from the assets archive to x64/Debug under the project’s directory.

Run the application in Visual Studio.