MP3 Encoder (Pull)

This article explains how to use TTranscoder::pull to encode a WAV file to an MP3 audio stream.

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

Linux and Windows samples are also available:

Source Audio

For source we use the equinox-48KHz.wav file from the AVBlocks Assets repository. After downloading and unzipping you will find equinox-48KHz.wav in the aud subdirectory.

Code

This code takes a WAV file and encodes it to MP3 using the pull method. The encoded samples are pulled from the transcoder and written to the output file by the application.

Initialize AVBlocks

Initialize the AVBlocks library before creating the transcoder, and shut it down after encoding is complete.

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;
    return encode(opt) ? 0 : 1;
}

Configure Output Socket

The output socket describes the MP3 stream. The stream info defines the codec.

TMediaSocket()
    .streamType(StreamType::MPEG_Audio)
    .streamSubType(StreamSubType::MPEG_Audio_Layer3)
    .addPin(
        TMediaPin()
            .streamInfo(TAudioStreamInfo()
                .streamType(StreamType::MPEG_Audio)
                .streamSubType(StreamSubType::MPEG_Audio_Layer3)
            )
    )

Configure Transcoder and Pull Encoded Samples

The sample creates the input socket from the WAV file name only, because AVBlocks detects the audio format automatically. It also creates the output socket, then creates a transcoder, enables demo mode, opens it, pulls encoded samples, writes each sample buffer to the output file, and checks for end-of-stream. The output socket has no file set because the application writes the pulled samples itself.

bool encode(const Options& opt)
{
    deleteFile(opt.outputFile.c_str());

    std::ofstream outfile(opt.outputFile, std::ios_base::binary);
    if (!outfile.is_open())
    {
        std::println(stderr, "Could not open file: {}", opt.outputFile);
        return false;
    }

    try {
        TTranscoder transcoder;
        transcoder
            .allowDemoMode(true)
            .addInput(
                TMediaSocket()
                    .file(opt.inputFile)
            )
            .addOutput(
                // No file on the output socket — we pull samples manually
                TMediaSocket()
                    .streamType(StreamType::MPEG_Audio)
                    .streamSubType(StreamSubType::MPEG_Audio_Layer3)
                    .addPin(
                        TMediaPin()
                            .streamInfo(TAudioStreamInfo()
                                .streamType(StreamType::MPEG_Audio)
                                .streamSubType(StreamSubType::MPEG_Audio_Layer3)
                            )
                    )
            )
            .open();

        int32_t outputIndex = 0;
        TMediaSample sample;

        while (transcoder.pull(outputIndex, sample))
        {
            auto buf = sample.buffer();
            outfile.write(reinterpret_cast<const char*>(buf.data()), buf.dataSize());
        }

        const auto error = transcoder.error();
        bool success = (error.facility() == primo::error::ErrorFacility::Codec &&
                        error.code()     == primo::codecs::CodecError::EOS);

        if (!success)
            printError("Transcoder pull", error);

        transcoder.close();

        if (success)
            std::println("Output: {}", opt.outputFile);

        return success;

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

Complete C++ Code

Here’s the complete working example from enc_mp3_pull.cpp:

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

#include <print>
#include <fstream>

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

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

bool encode(const Options& opt)
{
    deleteFile(opt.outputFile.c_str());

    std::ofstream outfile(opt.outputFile, std::ios_base::binary);
    if (!outfile.is_open())
    {
        std::println(stderr, "Could not open file: {}", opt.outputFile);
        return false;
    }

    try {
        TTranscoder transcoder;
        transcoder
            .allowDemoMode(true)
            .addInput(
                TMediaSocket()
                    .file(opt.inputFile)
            )
            .addOutput(
                // No file on the output socket — we pull samples manually
                TMediaSocket()
                    .streamType(StreamType::MPEG_Audio)
                    .streamSubType(StreamSubType::MPEG_Audio_Layer3)
                    .addPin(
                        TMediaPin()
                            .streamInfo(TAudioStreamInfo()
                                .streamType(StreamType::MPEG_Audio)
                                .streamSubType(StreamSubType::MPEG_Audio_Layer3)
                            )
                    )
            )
            .open();

        int32_t outputIndex = 0;
        TMediaSample sample;

        while (transcoder.pull(outputIndex, sample))
        {
            auto buf = sample.buffer();
            outfile.write(reinterpret_cast<const char*>(buf.data()), buf.dataSize());
        }

        const auto error = transcoder.error();
        bool success = (error.facility() == primo::error::ErrorFacility::Codec &&
                        error.code()     == primo::codecs::CodecError::EOS);

        if (!success)
            printError("Transcoder pull", error);

        transcoder.close();

        if (success)
            std::println("Output: {}", opt.outputFile);

        return success;

    } catch (const TAVBlocksException& ex) {
        std::println(stderr, "AVBlocks error: {}", ex.what());
        return false;
    } catch (const std::exception& ex) {
        std::println(stderr, "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;
    return encode(opt) ? 0 : 1;
}

How to Run

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

Command Line

./enc_mp3_pull --input <wav file> --output <mp3 file>

Examples

List options:

./bin/x64/enc_mp3_pull --help
Usage: enc_mp3_pull --input <wav file> --output <mp3 file>
  -h,    --help
  -i,    --input    input WAV file
  -o,    --output   output MP3 file

The following example encodes input file ./assets/aud/equinox-48KHz.wav into output file equinox-48KHz.mp3:

mkdir -p ./output/enc_mp3_pull

./bin/x64/enc_mp3_pull \
    --input ./assets/aud/equinox-48KHz.wav \
    --output ./output/enc_mp3_pull/equinox-48KHz.mp3