27.07.2015, 16:16, "Nicolas George" <geo...@nsup.org>:
> Le nonidi 9 thermidor, an CCXXIII, Vesselin Bontchev a écrit :
>>  This patch adds support for Audible AA files.
>>
>>  Audible samples can be obtained from,
>>
>>  https://gitlab.com/vesselin.bontchev/audible-samples/tree/master
>>  https://samples.ffmpeg.org/audible/
>
>>  Currently, this code generates corrupt audio output (in some places, and 
>> deterministically).
>
> Does this happen with the Go and Python implementations too?

There are no existing Go, or Python implementations actually.

>>  From ba73543efc3fdc4b5c61e9cb56f998d748716e00 Mon Sep 17 00:00:00 2001
>>  Subject: [PATCH] Add support for Audible AA files

>>  + avio_read(s->pb, src, trailing_bytes);
>>  + memcpy(buf + written, dst, trailing_bytes);
>
> Looks strange: you read to src but copy dst. May this be the cause of the
> glitches?

Good catch! This fixed the glitches entirely.

...

I am attaching a new revision (v2) of the patch. I still need to take care of 
memory leaks, and also harden input processing. This said, the code should 
already be looking much better (thanks for all the help!).

Vesselin
From e907a1c79b10f15a23caf62c85fffd9dcd2d4ec3 Mon Sep 17 00:00:00 2001
From: Vesselin Bontchev <vesselin.bontc...@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        |  10 ++
 doc/general.texi         |   2 +
 libavformat/Makefile     |   1 +
 libavformat/aadec.c      | 337 +++++++++++++++++++++++++++++++++++++++++++++++
 libavformat/allformats.c |   1 +
 5 files changed, 351 insertions(+)
 create mode 100644 libavformat/aadec.c

diff --git a/doc/demuxers.texi b/doc/demuxers.texi
index e45e1af..df95233 100644
--- a/doc/demuxers.texi
+++ b/doc/demuxers.texi
@@ -18,6 +18,16 @@ enabled demuxers.
 
 The description of some of the currently available demuxers follows.
 
+@section aa
+
+Audible Format 2, 3, and 4 demuxer.
+
+This demuxer is used to demux Audible Format 2, 3, and 4 (.aa) files.
+
+@example
+ffmpeg -v debug -i input.aa -c:a copy output.wav
+@end example
+
 @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.
+@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..5d71c74
--- /dev/null
+++ b/libavformat/aadec.c
@@ -0,0 +1,337 @@
+/*
+ * 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.
+ *
+ * This file is part of FFmpeg.
+ *
+ * FFmpeg is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 2.1 of the License, or (at your option) any later version.
+ *
+ * FFmpeg is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public
+ * License along with FFmpeg; if not, write to the Free Software
+ * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
+ */
+
+#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 TEA_BLOCK_SIZE 8
+
+typedef struct AADemuxContext {
+    uint8_t wtf[16];
+    void *aa_fixed_key;
+    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[512] = {0};
+    char val[512] = {0};
+    uint8_t output[24] = {0};
+    uint32_t v0, v1;
+    uint8_t dst[8];
+    uint8_t src[8];
+    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;
+    uint32_t TOC[MAX_TOC_ENTRIES][2];
+    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();
+    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][0] = avio_rb32(pb); // block offset
+        TOC[i][1] = avio_rb32(pb); // block size
+    }
+    avio_skip(pb, 24); // header termination block (ignored)
+    npairs = avio_rb32(pb); // read dictionary entries
+    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\0")) {
+            strncpy(codec_name, val, sizeof(codec_name) - 1);
+        }
+        if (!strcmp(key, "HeaderSeed\0")) {
+            header_seed = atoi(val);
+        }
+        if (!strcmp(key, "HeaderKey\0")) {
+            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) {
+        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);
+    if (c->codec_second_size == -1) {
+        return AVERROR_DECODER_NOT_FOUND;
+    }
+    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
+        v0 = header_seed;
+        v1 = header_seed + 1;
+        AV_WB32(src, v0);
+        AV_WB32(src + 4, v1);
+        header_seed = v1 + 1;
+        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;
+    } else {
+        return AVERROR_DECODER_NOT_FOUND;
+    }
+
+    /* determine, and jump to audio start offset */
+    for (i = 1; i < toc_size; i++) { // skip the first entry!
+        current_size = TOC[i][1];
+        if (current_size > largest_size) {
+            largest_idx = i;
+            largest_size = current_size;
+        }
+    }
+    start = TOC[largest_idx][0];
+    avio_seek(pb, start, SEEK_SET);
+    c->current_chapter_size = 0;
+
+    return 0;
+
+fail:
+    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 = 0;
+    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) {
+            ret = AVERROR_EOF;
+            goto end;
+        }
+        av_log(s, AV_LOG_DEBUG, "[aa] chapter %d (%ld bytes)\n", c->chapter_idx, c->current_chapter_size);
+        c->chapter_idx = c->chapter_idx + 1;
+        avio_rb32(s->pb); // 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;
+
+    av_new_packet(pkt, written);
+    memcpy(pkt->data, buf, written);
+
+end:
+    return ret;
+}
+
+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 / 4;
+}
+
+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
+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"},
+        .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

_______________________________________________
ffmpeg-devel mailing list
ffmpeg-devel@ffmpeg.org
http://ffmpeg.org/mailman/listinfo/ffmpeg-devel

Reply via email to