[FFmpeg-devel] [PATCH v6] Add support for Audible AA files

James Almer jamrial at gmail.com
Sun Aug 2 06:42:58 CEST 2015


On 30/07/15 7:46 AM, Vesselin Bontchev wrote:
> From 06b0c0013404a67c72ea14a3c90730c0c4bd5b9a Mon Sep 17 00:00:00 2001
> From: Vesselin Bontchev <vesselin.bontchev at yandex.com>
> Date: Sun, 19 Jul 2015 23:16:36 +0200
> Subject: [PATCH] Add support for Audible AA files
> 
> https://en.wikipedia.org/wiki/Audible.com#Quality
> ---
>  doc/demuxers.texi        |   6 +
>  doc/general.texi         |   2 +
>  libavformat/Makefile     |   1 +
>  libavformat/aadec.c      | 325 +++++++++++++++++++++++++++++++++++++++++++++++
>  libavformat/allformats.c |   1 +
>  5 files changed, 335 insertions(+)
>  create mode 100644 libavformat/aadec.c
> 
> diff --git a/doc/demuxers.texi b/doc/demuxers.texi
> index e45e1af..c86e8a4 100644
> --- a/doc/demuxers.texi
> +++ b/doc/demuxers.texi
> @@ -18,6 +18,12 @@ enabled demuxers.
>  
>  The description of some of the currently available demuxers follows.
>  
> + at section aa
> +
> +Audible Format 2, 3, and 4 demuxer.
> +
> +This demuxer is used to demux Audible Format 2, 3, and 4 (.aa) files.
> +
>  @section applehttp
>  
>  Apple HTTP Live Streaming demuxer.
> diff --git a/doc/general.texi b/doc/general.texi
> index a260e79..2b782e0 100644
> --- a/doc/general.texi
> +++ b/doc/general.texi
> @@ -228,6 +228,8 @@ library:
>  @item 8088flex TMV              @tab   @tab X
>  @item AAX                       @tab   @tab X
>      @tab Audible Enhanced Audio format, used in audiobooks.
> + at item AA                        @tab   @tab X
> +    @tab Audible Format 2, 3, and 4, used in audiobooks.
>  @item ACT Voice                 @tab   @tab X
>      @tab contains G.729 audio
>  @item Adobe Filmstrip           @tab X @tab X
> diff --git a/libavformat/Makefile b/libavformat/Makefile
> index cc73fd8..466da51 100644
> --- a/libavformat/Makefile
> +++ b/libavformat/Makefile
> @@ -59,6 +59,7 @@ OBJS-$(CONFIG_SHARED)                    += log2_tab.o golomb_tab.o
>  
>  # muxers/demuxers
>  OBJS-$(CONFIG_A64_MUXER)                 += a64.o rawenc.o
> +OBJS-$(CONFIG_AA_DEMUXER)                += aadec.o
>  OBJS-$(CONFIG_AAC_DEMUXER)               += aacdec.o apetag.o img2.o rawdec.o
>  OBJS-$(CONFIG_AC3_DEMUXER)               += ac3dec.o rawdec.o
>  OBJS-$(CONFIG_AC3_MUXER)                 += rawenc.o
> diff --git a/libavformat/aadec.c b/libavformat/aadec.c
> new file mode 100644
> index 0000000..771bcfd
> --- /dev/null
> +++ b/libavformat/aadec.c
> @@ -0,0 +1,325 @@
> +/*
> + * Audible AA demuxer
> + * Copyright (c) 2015 Vesselin Bontchev
> + *
> + * Header parsing is borrowed from https://github.com/jteeuwen/audible project.
> + * Copyright (c) 2001-2014, Jim Teeuwen
> + *
> + * Redistribution and use in source and binary forms, with or without modification,
> + * are permitted provided that the following conditions are met:
> + *
> + * 1. Redistributions of source code must retain the above copyright notice, this
> + *    list of conditions and the following disclaimer.
> + *
> + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND
> + * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
> + * WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
> + * DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR
> + * ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
> + * (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
> + * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON
> + * ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
> + * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
> + * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
> + */
> +
> +#include "avformat.h"
> +#include "internal.h"
> +#include "libavutil/intreadwrite.h"
> +#include "libavutil/tea.h"
> +#include "libavutil/opt.h"
> +
> +#define AA_MAGIC 1469084982 /* this identifies an audible .aa file */
> +#define MAX_CODEC_SECOND_SIZE 3982
> +#define MAX_TOC_ENTRIES 16
> +#define MAX_DICTIONARY_ENTRIES 128
> +#define TEA_BLOCK_SIZE 8
> +
> +typedef struct AADemuxContext {
> +    AVClass *class;
> +    void *aa_fixed_key;

char

> +    int aa_fixed_key_size;
> +    int32_t codec_second_size;
> +    struct AVTEA *tea_ctx;
> +    uint8_t file_key[16];
> +    int64_t current_chapter_size;
> +    int64_t current_codec_second_size;
> +    int chapter_idx;
> +} AADemuxContext;
> +
> +static int32_t get_second_size(char *codec_name)
> +{
> +    int32_t result = -1;
> +
> +    if (!strcmp(codec_name, "mp332")) {
> +        result = 3982;
> +    } else if (!strcmp(codec_name, "acelp16")) {
> +        result = 2000;
> +    } else if (!strcmp(codec_name, "acelp85")) {
> +        result = 1045;
> +    }
> +
> +    return result;
> +}
> +
> +static int aa_read_header(AVFormatContext *s)
> +{
> +    int i, j, idx;
> +    uint32_t nkey;
> +    uint32_t nval;
> +    char key[128];
> +    char val[128];
> +    uint8_t output[24];
> +    uint8_t dst[8];
> +    uint8_t src[8];

Any reason some are char and others uint8_t? Also, try to put more than one variable
of the same type per line.

> +    int largest_idx = -1;
> +    int64_t largest_size = -1;
> +    int64_t current_size = -1;
> +    uint32_t start;
> +    char codec_name[64] = {0};
> +    int ret = 0;
> +    struct toc_entry {
> +        uint32_t offset;
> +        uint32_t size;
> +    } TOC[MAX_TOC_ENTRIES];
> +    uint32_t toc_size;
> +    uint32_t npairs;
> +    uint32_t header_seed;
> +    union {
> +        uint8_t key[16];
> +        uint32_t part[4];
> +    } header_key;
> +    AADemuxContext *c = s->priv_data;
> +    AVIOContext *pb = s->pb;
> +    AVStream *st = avformat_new_stream(s, NULL);
> +    if (!st)
> +        return AVERROR(ENOMEM);
> +    c->tea_ctx = av_tea_alloc();

You could move this below right before tea_init and replace all of the gotos below
with simple returns.

> +    if (!c->tea_ctx) {
> +        ret =  AVERROR(ENOMEM);
> +        goto fail;
> +    }
> +
> +    avio_skip(pb, 4); // file size
> +    avio_skip(pb, 4); // magic string
> +    toc_size = avio_rb32(pb); // TOC size
> +    avio_skip(pb, 4); // unidentified integer
> +    if (toc_size > MAX_TOC_ENTRIES) {
> +        ret = AVERROR_INVALIDDATA;
> +        goto fail;
> +    }
> +    for (i = 0; i < toc_size; i++) { // read TOC
> +        avio_skip(pb, 4); // TOC entry index
> +        TOC[i].offset = avio_rb32(pb); // block offset
> +        TOC[i].size = avio_rb32(pb); // block size
> +    }
> +    avio_skip(pb, 24); // header termination block (ignored)
> +    npairs = avio_rb32(pb); // read dictionary entries
> +    if (npairs > MAX_DICTIONARY_ENTRIES) {
> +        ret = AVERROR_INVALIDDATA;
> +        goto fail;
> +    }
> +    for (i = 0; i < npairs; i++) {
> +        memset(val, 0, sizeof(val));
> +        memset(key, 0, sizeof(key));
> +        avio_skip(pb, 1); // unidentified integer
> +        nkey = avio_rb32(pb); // key string length
> +        nval = avio_rb32(pb); // value string length
> +        if (nkey > sizeof(key)) {
> +            avio_skip(pb, nkey);
> +        } else {
> +            avio_read(pb, key, nkey); // key string
> +        }
> +        if (nval > sizeof(val)) {
> +            avio_skip(pb, nval);
> +        } else {
> +            avio_read(pb, val, nval); // value string
> +        }
> +        if (!strcmp(key, "codec")) {
> +            strncpy(codec_name, val, sizeof(codec_name) - 1);
> +        }
> +        if (!strcmp(key, "HeaderSeed")) {
> +            header_seed = atoi(val);
> +        }
> +        if (!strcmp(key, "HeaderKey")) {
> +            sscanf(val, "%d%d%d%d", &header_key.part[0], &header_key.part[1], &header_key.part[2], &header_key.part[3]);
> +            for (idx = 0; idx < 4; idx++) {
> +                header_key.part[idx] = AV_RB32(&header_key.part[idx]); // convert to BE!
> +            }
> +        }
> +    }
> +
> +    /* verify fixed key */
> +    if (c->aa_fixed_key_size != 16) {

This is zeroed during init but apparently never touched after that.
You should probably check the length of the AVOption aa_fixed_key instead.

> +        av_log(s, AV_LOG_FATAL, "[aa] aa_fixed_key value needs to be 16 bytes!\n");
> +        ret = AVERROR(EINVAL);
> +        goto fail;
> +    }
> +
> +    /* decryption key derivation */
> +    av_tea_init(c->tea_ctx, c->aa_fixed_key, 16);
> +    c->codec_second_size = get_second_size(codec_name);

You check for the codec name below again. Maybe you could combine both?

> +    if (c->codec_second_size == -1) {
> +        av_log(s, AV_LOG_FATAL, "[aa] unknown codec <%s>!\n", codec_name);

AV_LOG_ERROR is enough, and there's no need to add "[aa]" since you're passing the
AVFormatContext as argument.

> +        return AVERROR_INVALIDDATA;

goto fail, or move the tea alloc and init functions right below this.

> +    }
> +    output[0] = output[1] = 0; // purely for padding purposes
> +    memcpy(output + 2, &header_key, 16);
> +    idx = 0;
> +    for (i = 0; i < 3; i++) { // TEA CBC with weird mixed endianness
> +        AV_WB32(src, header_seed);
> +        AV_WB32(src + 4, header_seed + 1);
> +        header_seed += 2;
> +        av_tea_crypt(c->tea_ctx, dst, src, 1, NULL, 0); // TEA ECB encrypt
> +        for (j = 0; j < TEA_BLOCK_SIZE && idx < 18; j+=1, idx+=1) {
> +            output[idx] = output[idx] ^ dst[j];
> +        }
> +    }
> +    memcpy(c->file_key, output + 2, 16); // skip first 2 bytes of output
> +
> +    /* decoder setup */
> +    st->codec->codec_type = AVMEDIA_TYPE_AUDIO;
> +    if (!strcmp(codec_name, "mp332")) {
> +        st->codec->codec_id = AV_CODEC_ID_MP3;
> +        st->codec->sample_rate = 22050;
> +        st->need_parsing = AVSTREAM_PARSE_FULL_RAW;
> +        st->start_time = 0;
> +    } else if (!strcmp(codec_name, "acelp85")) {
> +        st->codec->codec_id = AV_CODEC_ID_SIPR;
> +        st->codec->block_align = 19;
> +        st->codec->channels = 1;
> +        st->codec->sample_rate = 8500;
> +    } else if (!strcmp(codec_name, "acelp16")) {
> +        st->codec->codec_id = AV_CODEC_ID_SIPR;
> +        st->codec->block_align = 20;
> +        st->codec->channels = 1;
> +        st->codec->sample_rate = 16000;
> +    }
> +
> +    /* determine, and jump to audio start offset */
> +    for (i = 1; i < toc_size; i++) { // skip the first entry!
> +        current_size = TOC[i].size;
> +        if (current_size > largest_size) {
> +            largest_idx = i;
> +            largest_size = current_size;
> +        }
> +    }
> +    start = TOC[largest_idx].offset;
> +    avio_seek(pb, start, SEEK_SET);
> +    c->current_chapter_size = 0;
> +
> +    return 0;
> +
> +fail:

If you move the tea_alloc() and tea_init() functions down this will not be needed
anymore.

> +    av_freep(&c->tea_ctx);
> +    av_free(st);
> +    return ret;
> +}
> +
> +static int aa_read_packet(AVFormatContext *s, AVPacket *pkt)
> +{
> +    uint8_t dst[TEA_BLOCK_SIZE];
> +    uint8_t src[TEA_BLOCK_SIZE];
> +    int i;
> +    int trailing_bytes;
> +    int blocks;
> +    uint8_t buf[MAX_CODEC_SECOND_SIZE * 2];
> +    int written = 0;
> +    int ret;
> +    AADemuxContext *c = s->priv_data;
> +
> +    // are we at the start of a chapter?
> +    if (c->current_chapter_size == 0) {
> +        c->current_chapter_size = avio_rb32(s->pb);
> +        if (c->current_chapter_size == 0) {
> +            return AVERROR_EOF;
> +        }
> +        av_log(s, AV_LOG_DEBUG, "[aa] chapter %d (%" PRId64 " bytes)\n", c->chapter_idx, c->current_chapter_size);

Same, no need for [aa].

> +        c->chapter_idx = c->chapter_idx + 1;
> +        avio_skip(s->pb, 4); // data start offset
> +        c->current_codec_second_size = c->codec_second_size;
> +    }
> +
> +    // is this the last block in this chapter?
> +    if (c->current_chapter_size / c->current_codec_second_size == 0) {
> +        c->current_codec_second_size = c->current_chapter_size % c->current_codec_second_size;
> +    }
> +
> +    // decrypt c->current_codec_second_size bytes
> +    blocks = c->current_codec_second_size / TEA_BLOCK_SIZE;
> +    for (i = 0; i < blocks; i++) {
> +        avio_read(s->pb, src, TEA_BLOCK_SIZE);
> +        av_tea_init(c->tea_ctx, c->file_key, 16);
> +        av_tea_crypt(c->tea_ctx, dst, src, 1, NULL, 1);
> +        memcpy(buf + written, dst, TEA_BLOCK_SIZE);
> +        written = written + TEA_BLOCK_SIZE;
> +    }
> +    trailing_bytes = c->current_codec_second_size % TEA_BLOCK_SIZE;
> +    if (trailing_bytes != 0) { // trailing bytes are left unencrypted!
> +        avio_read(s->pb, src, trailing_bytes);
> +        memcpy(buf + written, src, trailing_bytes);
> +        written = written + trailing_bytes;
> +    }
> +
> +    // update state
> +    c->current_chapter_size = c->current_chapter_size - c->current_codec_second_size;
> +    if (c->current_chapter_size <= 0)
> +        c->current_chapter_size = 0;
> +
> +    ret = av_new_packet(pkt, written);
> +    if (ret < 0)
> +        return ret;
> +    memcpy(pkt->data, buf, written);
> +
> +    return 0;
> +}
> +
> +static int aa_probe(AVProbeData *p)
> +{
> +    uint8_t *buf = p->buf;
> +
> +    // first 4 bytes are file size, next 4 bytes are the magic
> +    if (AV_RB32(buf+4) != AA_MAGIC)
> +        return 0;
> +
> +    return AVPROBE_SCORE_MAX / 2;
> +}
> +
> +static int aa_read_close(AVFormatContext *s)
> +{
> +    AADemuxContext *c = s->priv_data;
> +
> +    av_freep(&c->tea_ctx);
> +
> +    return 0;
> +}
> +
> +#define OFFSET(x) offsetof(AADemuxContext, x)
> +#define FLAGS AV_OPT_FLAG_DECODING_PARAM

You're defining this but not using it.

> +static const AVOption aa_options[] = {
> +    { "aa_fixed_key", // extracted from libAAX_SDK.so and AAXSDKWin.dll files!
> +        "Fixed key used for handling Audible AA files", OFFSET(aa_fixed_key),
> +        AV_OPT_TYPE_BINARY, {.str="77214d4b196a87cd520045fd2a51d673"},

This should probably be AV_OPT_TYPE_STRING.

> +        .flags = AV_OPT_FLAG_DECODING_PARAM },
> +    { NULL },
> +};
> +
> +static const AVClass aa_class = {
> +    .class_name = "aa",
> +    .item_name  = av_default_item_name,
> +    .option     = aa_options,
> +    .version    = LIBAVUTIL_VERSION_INT,
> +};
> +
> +AVInputFormat ff_aa_demuxer = {
> +    .name           = "aa",
> +    .long_name      = NULL_IF_CONFIG_SMALL("Audible AA format files"),
> +    .priv_class     = &aa_class,
> +    .priv_data_size = sizeof(AADemuxContext),
> +    .extensions     = "aa",
> +    .read_probe     = aa_probe,
> +    .read_header    = aa_read_header,
> +    .read_packet    = aa_read_packet,
> +    .read_close     = aa_read_close,
> +    .flags          = AVFMT_GENERIC_INDEX,
> +};
> diff --git a/libavformat/allformats.c b/libavformat/allformats.c
> index 181cb9e..0a24ac7 100644
> --- a/libavformat/allformats.c
> +++ b/libavformat/allformats.c
> @@ -60,6 +60,7 @@ void av_register_all(void)
>  
>      /* (de)muxers */
>      REGISTER_MUXER   (A64,              a64);
> +    REGISTER_DEMUXER (AA,               aa);
>      REGISTER_DEMUXER (AAC,              aac);
>      REGISTER_MUXDEMUX(AC3,              ac3);
>      REGISTER_DEMUXER (ACT,              act);
> -- 2.1.4

I'd say wait for more comments from other people before sending the patch again,
i probably missed some things.



More information about the ffmpeg-devel mailing list