After an evaluation, GNOME has moved from Bugzilla to GitLab. Learn more about GitLab.
No new issues can be reported in GNOME Bugzilla anymore.
To report an issue in a GNOME project, go to GNOME GitLab.
Do not go to GNOME Gitlab for: Bluefish, Doxygen, GnuCash, GStreamer, java-gnome, LDTP, NetworkManager, Tomboy.
Bug 738538 - Add demuxer/parser for Apple's Core Audio Format (CAF)
Add demuxer/parser for Apple's Core Audio Format (CAF)
Status: RESOLVED OBSOLETE
Product: GStreamer
Classification: Platform
Component: gst-plugins-bad
git master
Other Linux
: Normal enhancement
: git master
Assigned To: GStreamer Maintainers
GStreamer Maintainers
Depends on:
Blocks:
 
 
Reported: 2014-10-14 16:37 UTC by Peter G. Baum
Modified: 2018-11-03 13:27 UTC
See Also:
GNOME target: ---
GNOME version: ---


Attachments
Patch to implement a CAF parser (16.21 KB, patch)
2014-10-14 16:37 UTC, Peter G. Baum
none Details | Review
Patch to implement a CAF parser (16.15 KB, patch)
2014-10-14 19:53 UTC, Peter G. Baum
needs-work Details | Review
Demuxer for CAF (19.13 KB, patch)
2014-10-19 14:55 UTC, Peter G. Baum
none Details | Review
cafdemux: new element to demux Core Audio Format (19.70 KB, patch)
2014-10-26 08:21 UTC, Peter G. Baum
needs-work Details | Review
Patch to implement a CAF parser (21.29 KB, patch)
2014-12-22 17:18 UTC, Peter G. Baum
none Details | Review

Description Peter G. Baum 2014-10-14 16:37:56 UTC
Created attachment 288536 [details] [review]
Patch to implement a CAF parser

CAF is Apple's core audio format.
https://developer.apple.com/library/mac/#documentation/MusicAudio/Reference/CAFSpec/CAF_overview/CAF_overview.html#//apple_ref/doc/uid/TP40001862-CH209-TPXREF101

It is of course very common in the Apple world.
It can be seen as an enhanced WAV format.
Unlike WAV it has for example no limitation to the file length.
It is therefor also useful to use it on other platforms.
Comment 1 Peter G. Baum 2014-10-14 16:42:14 UTC
The parser still fails in my tests on some files, but it would be nice to get already a code review.

I'm also unsure about the line:

   *skipsize = pushSize;
    // return gst_base_parse_finish_frame (parse, frame, pushSize);
    return gst_base_parse_push_frame (parse, frame);

which seems to be the reason of some failures in my test:

[gst-master] [peter@core2 tests]$ gst-launch-1.0 filesrc location=./files/s1.caf ! cafparse ! audioconvert dithering=0 ! wavenc ! filesink location=./files/s2.wav
Setting pipeline to PAUSED ...
Pipeline is PREROLLING ...

** (gst-launch-1.0:14581): WARNING **: audioconvert0: size 65536 is not a multiple of unit size 12
0:00:00.036802853 14581      0x16f9590 ERROR          basetransform gstbasetransform.c:1669:default_prepare_output_buffer:<audioconvert0> unknown output size
ERROR: from element /GstPipeline:pipeline0/GstCafParse:cafparse0: GStreamer encountered a general stream error.
Comment 2 Peter G. Baum 2014-10-14 19:53:54 UTC
Created attachment 288549 [details] [review]
Patch to implement a CAF parser

Fixed a length issue.
My PCM test files are now parsed OK.
Comment 3 Olivier Crête 2014-10-14 23:07:25 UTC
Review of attachment 288549 [details] [review]:

General stylistic comment, in GStreamer, we normally don't use CamelCase, but it's not a big deal. Also, this not really being a parser (but a demuxer), I'm not sure if GstBaseParse is really the right base class, but we have no demuxer base class, so if it works, I'd leave it as-is.

If you have one a typefind function for the typefind plugin would also be appreciated, so it can "just work".

::: gst/audioparsers/gstcafparse.c
@@ +54,3 @@
+    "channels = (int) [ 1, MAX ]," \
+    "layout = interleaved, " \
+    "channel-mask = (bitmask) 0"

Leave that channel mask out I think. 0 sounds wrong.

@@ +95,3 @@
+
+  gst_element_class_set_static_metadata (element_class, "CAF audio parser",
+      "Codec/Parser/Audio", "Parses a Core Audio (CAF) audio stream",

In GStreamer nomenclature, it's really a Demuxer, not a parser (so it wavparse).

@@ +109,3 @@
+{
+  gst_caf_parse_reset (caf);
+  GST_PAD_SET_ACCEPT_INTERSECT (GST_BASE_PARSE_SINK_PAD (caf));

This is not needed, the sink caps have no properties.

@@ +222,3 @@
+      "rate", G_TYPE_INT, rate, "channels", G_TYPE_INT, channelsPerFrame,
+      "layout", G_TYPE_STRING, "interleaved",
+      "channel-mask", GST_TYPE_BITMASK, G_GUINT64_CONSTANT (0), NULL);

To create the audio caps, its safer to do gst_audio_info_init(); gst_audio_info_set_format(); gst_audio_info_to_caps();

@@ +223,3 @@
+      "layout", G_TYPE_STRING, "interleaved",
+      "channel-mask", GST_TYPE_BITMASK, G_GUINT64_CONSTANT (0), NULL);
+  gst_pad_set_caps (GST_BASE_PARSE_SRC_PAD (GST_BASE_PARSE (caf)), caps);

This gst_pad_set_caps() is redundant. In GStreamer 1.x, it's just a compat macro to gst_pad_push_event(gst_event_new_caps()).

@@ +263,3 @@
+    case GST_MAKE_FOURCC ('a', 'l', 'a', 'c'):
+      GST_DEBUG_OBJECT (caf, "format '%" GST_FOURCC_FORMAT
+          "' not yet implemented", GST_FOURCC_ARGS (formatID));

If you return a GST_FLOW_ERROR, you must post an error message with GST_ELEMENT_ERROR()

@@ +267,3 @@
+    default:
+      GST_DEBUG_OBJECT (caf, "unknown format '%" GST_FOURCC_FORMAT "'",
+          GST_FOURCC_ARGS (formatID));

This one too.
Comment 4 Peter G. Baum 2014-10-19 14:55:21 UTC
Created attachment 288851 [details] [review]
Demuxer for CAF

Incorporated all comments from Olivier (BTW: many thanks for the review).

Implemented
- Reading of "free", "chan", "desc" and "data" chunk
- Handling of uncompressed PCM data
- Basic channel mask settings

Still missing
- Handling of compressed data
- More complete channel mask setting (but CAF is here more
  flexible than gstreamer)
- Reading of chunks with more metadata (time code etc.)
Comment 5 Peter G. Baum 2014-10-26 04:14:22 UTC
Any comments?
Comment 6 Peter G. Baum 2014-10-26 08:21:13 UTC
Created attachment 289335 [details] [review]
cafdemux: new element to demux Core Audio Format

Renamed to cafdemux, since it is a demuxer
Moved to own directory.
Comment 7 Thiago Sousa Santos 2014-11-12 07:01:27 UTC
The code looks good but I'm still bothered by the fact that you inherited from baseparse. Was there any reason or benefit for doing this?

++ on having a typefind function to allow this element being autoplugged with playbin to work directly on CAF files.
Comment 8 Tim-Philipp Müller 2014-11-12 10:36:44 UTC
Could you convert the CamelCase in functions and variable names to GStreamer style please, and also make the patch against gst-plugins-bad instead of -good? (-bad is where new elements land usually).

Does seeking work already? Is the format really syncable?
Comment 9 Peter G. Baum 2014-11-12 21:25:28 UTC
Typefind is already committed: https://bugzilla.gnome.org/show_bug.cgi?id=739840

On inherting from baseparse, well, at that time I thought it was a good idea and it seems to work.
But I don't claim to understand gstreamer internals, so I'm open for suggestions.
Comment 10 Olivier Crête 2014-11-12 21:51:31 UTC
And I guess using BaseParse gives you seekability almost for free, and we're still lacking a BaseDemux...
Comment 11 Thiago Sousa Santos 2014-11-21 22:40:02 UTC
Took a quick look at the CAF spec. It is full audio file inside another file structure. Also has the usual: HEADER | DATA | INDEX (data and index can be in any order, there is also some metadata entries but they are optional).

One advantage of inheriting from GstElement is being able to go and parse the index when it is at the end just like qtdemux does when moov if after mdat. One can also seek without the index using estimatives for the audio frame sizes when VBR is used.
Comment 12 Reynaldo H. Verdejo Pinochet 2014-11-24 18:31:11 UTC
Comment on attachment 289335 [details] [review]
cafdemux: new element to demux Core Audio Format

Hi

> [..]
>+static gboolean
>+checkCAFFileHeader (GstCafDemux * caf, GstMapInfo * map)
>+{
>+  guint32 fileType = GST_READ_UINT32_LE (map->data);
>+  guint16 version = GST_READ_UINT16_BE (map->data + 4);
>+  guint16 flags = GST_READ_UINT16_BE (map->data + 6);
>+
>+  if (fileType != GST_MAKE_FOURCC ('c', 'a', 'f', 'f')) {
>+    GST_ELEMENT_ERROR (caf, STREAM, TYPE_NOT_FOUND, (NULL),
>+        ("Invalid CAF header: %" GST_FOURCC_FORMAT,
>+            GST_FOURCC_ARGS (fileType)));
>+    return FALSE;
>+  }
>+  GST_DEBUG_OBJECT (caf, "CAF version %d flags 0x%x", (gint) version,
>+      (guint) flags);
>+
>+  return TRUE;
>+}

Might make sense to at least issue a warning if version != 1.0?
I understand this is what we are absolutely* sure this code supports.

> [..]
>+static gboolean
>+initLPCM (GstCafDemux * caf, gdouble sampleRate, guint32 formatFlags,
>+    guint32 bytesPerPacket, guint32 framesPerPacket, guint32 channelsPerFrame,
>+    guint32 bitsPerChannel)
>+{
>+  const gboolean isFloat = (formatFlags & 0x1) != 0;
>+  const gboolean isLittleEndian = (formatFlags & 0x2) != 0;
>+  const guint rate = (gint) sampleRate;
>+  GstAudioFormat format;
>+  GstCaps *caps;
>+
>+  if (sampleRate <= .0 || rate < 0) {
>+    GST_DEBUG_OBJECT (caf, "Sample rate must be > 0 (not %lf -> %d)",
>+        sampleRate, rate);
>+    return FALSE;
>+  }

Please see bellow

>+
>+  if (framesPerPacket != 1) {
>+    GST_DEBUG_OBJECT (caf, "framesPerPacket must be 1 (not %d)",
>+        framesPerPacket);
>+    return FALSE;
>+  }
>+

Ditto

>+  if (channelsPerFrame <= 0) {
>+    GST_DEBUG_OBJECT (caf, "channelsPerFrame must be > 1 (not %d)",
>+        channelsPerFrame);
>+    return FALSE;
>+  }
>+

Ditto

>+  if (isFloat) {
>+    switch (bitsPerChannel) {
>+      case 64:
>+        format = isLittleEndian ? GST_AUDIO_FORMAT_F64LE :
>+            GST_AUDIO_FORMAT_F64BE;
>+        break;
>+      case 32:
>+        format = isLittleEndian ? GST_AUDIO_FORMAT_F32LE :
>+            GST_AUDIO_FORMAT_F32BE;
>+        break;
>+      default:
>+        GST_DEBUG_OBJECT (caf,
>+            "Only float samples with 32 or 64 bits are supported (not %d)",
>+            bitsPerChannel);
>+        return FALSE;

Ditto

>+    }
>+  } else {
>+    switch (bitsPerChannel) {
>+      case 16:
>+        format = isLittleEndian ? GST_AUDIO_FORMAT_S16LE :
>+            GST_AUDIO_FORMAT_S16BE;
>+        break;
>+      case 24:
>+        format = isLittleEndian ? GST_AUDIO_FORMAT_S24LE :
>+            GST_AUDIO_FORMAT_S24BE;
>+        break;
>+      case 32:
>+        format = isLittleEndian ? GST_AUDIO_FORMAT_S32LE :
>+            GST_AUDIO_FORMAT_S32BE;
>+        break;
>+      default:
>+        GST_DEBUG_OBJECT (caf,
>+            "Only int samples with 16, 24 or 32 bits are supported (not %d)",
>+            bitsPerChannel);
>+        return FALSE;

Ditto

> [..]
>+  /* TODO check bytesPerPacket */
>+  caf->frameSize = bytesPerPacket;
>+
>+  if (caf->channelMask == -1) {
>+    GST_DEBUG_OBJECT (caf, "Setting default channel-mask");
>+    if (channelsPerFrame == 2) {
>+      caf->channelMask = (1 << GST_AUDIO_CHANNEL_POSITION_FRONT_LEFT) |
>+          (1 << GST_AUDIO_CHANNEL_POSITION_FRONT_RIGHT);
>+    } else {
>+      caf->channelMask = 0;
>+    }
>+  }
>+
>+  gst_audio_info_set_format (&caf->audioInfo, format, rate, channelsPerFrame,
>+      NULL);
>+  if (caf->channelMask == 0
>+      || !gst_audio_channel_positions_from_mask (channelsPerFrame,
>+          caf->channelMask, caf->audioInfo.position)) {
>+    int k;
>+    for (k = 0; k < 64; ++k) {
>+      GST_AUDIO_INFO_POSITION (&caf->audioInfo, k) =
>+          GST_AUDIO_CHANNEL_POSITION_NONE;
>+    }
>+    GST_INFO_OBJECT (caf, "Setting channel-mask to 0 (from 0x%"
>+        G_GINT64_MODIFIER "x)", caf->channelMask);
>+  }
>+
>+  caps = gst_audio_info_to_caps (&caf->audioInfo);
>+  GST_INFO_OBJECT (caf, "caps changed to %" GST_PTR_FORMAT, caps);
>+  gst_pad_push_event (GST_BASE_PARSE_SRC_PAD (caf), gst_event_new_caps (caps));
>+
>+  gst_caps_unref (caps);
>+
>+  return TRUE;
>+}

Looking at the error out path seems like pretty all the above
GST_DEBUG_OBJECT() messages should rather be WARNINGs. Unless
they catch known unfinished details on this new element. In
which case you should go with GST_FIXME_OBJECT() instead.

>+
>+static void
>+handleChannelBitmap (GstCafDemux * caf, guint32 channelBitmap)
>+{
>+  /* up to POSITION_REAR_CENTER the positions are in CAF the same  as in gst */

Drop the extra space before "as"

>+  if (channelBitmap < (1 << (GST_AUDIO_CHANNEL_POSITION_REAR_CENTER + 1)))
>+    caf->channelMask = channelBitmap;
>+  else {
>+    GST_WARNING_OBJECT (caf, "Channel bitmask %x not yet implemented",
>+        channelBitmap);

GST_FIXME_OBJECT()

> [..]
>+static gboolean
>+handleDescChunk (GstCafDemux * caf, const guint8 * buf)
>+{
>+  gdouble sampleRate = GST_READ_DOUBLE_BE (buf);
>+  guint32 formatID = GST_READ_UINT32_LE (buf + 8);
>+  guint32 formatFlags = GST_READ_UINT32_BE (buf + 12);
>+  guint32 bytesPerPacket = GST_READ_UINT32_BE (buf + 16);
>+  guint32 framesPerPacket = GST_READ_UINT32_BE (buf + 20);
>+  guint32 channelsPerFrame = GST_READ_UINT32_BE (buf + 24);
>+  guint32 bitsPerChannel = GST_READ_UINT32_BE (buf + 28);
>+
>+  GST_DEBUG_OBJECT (caf, "'desc': rate %lf, flags 0x%x, bytesPerPacket %d, "
>+      "framesPerPacket %d, channels %d, bitsPerChannel %d", sampleRate,
>+      formatFlags, bytesPerPacket, framesPerPacket, channelsPerFrame,
>+      bitsPerChannel);
>+  switch (formatID) {
>+    case GST_MAKE_FOURCC ('l', 'p', 'c', 'm'):
>+      return initLPCM (caf, sampleRate, formatFlags, bytesPerPacket,
>+          framesPerPacket, channelsPerFrame, bitsPerChannel);
>+    case GST_MAKE_FOURCC ('i', 'm', 'a', '4'):
>+    case GST_MAKE_FOURCC ('a', 'a', 'c', ' '):
>+    case GST_MAKE_FOURCC ('M', 'A', 'C', '3'):
>+    case GST_MAKE_FOURCC ('M', 'A', 'C', '6'):
>+    case GST_MAKE_FOURCC ('u', 'l', 'a', 'w'):
>+    case GST_MAKE_FOURCC ('a', 'l', 'a', 'w'):
>+    case GST_MAKE_FOURCC ('.', 'm', 'p', '1'):
>+    case GST_MAKE_FOURCC ('.', 'm', 'p', '2'):
>+    case GST_MAKE_FOURCC ('.', 'm', 'p', '3'):
>+    case GST_MAKE_FOURCC ('a', 'l', 'a', 'c'):
>+      GST_ELEMENT_ERROR (caf, STREAM, NOT_IMPLEMENTED, (NULL),
>+          ("format '%" GST_FOURCC_FORMAT "' not yet implemented",
>+              GST_FOURCC_ARGS (formatID)));
>+      return FALSE;

GST_FIXME_OBJECT() too.

> [..]
>+static GstFlowReturn
>+gst_caf_demux_handle_frame (GstBaseParse * demux, GstBaseParseFrame * frame,
>+    gint * skipsize)
>+{
>+  GstMapInfo map;
>+  GstFlowReturn ret = GST_FLOW_ERROR;
>+  GstCafDemux *caf = GST_CAF_DEMUX (demux);
>+
>+  if (G_LIKELY (caf->state == GST_CAF_STATE_GOT_DATA)) {
>+    gsize bufSize = gst_buffer_get_size (frame->buffer);
>+    gsize pushSize = MIN (caf->dataBytesLeft,
>+        (bufSize / caf->frameSize) * caf->frameSize);
>+
>+    GST_DEBUG_OBJECT (caf, "pushing data, bytes left=%" G_GUINT64_FORMAT
>+        " avail=%" G_GUINT64_FORMAT " frameSize:%d size:%" G_GUINT64_FORMAT,
>+        caf->dataBytesLeft, bufSize, caf->frameSize, pushSize);
>+    if (caf->dataBytesLeft == 0) {
>+      GST_DEBUG_OBJECT (caf, "Data chunk done");
>+      return GST_FLOW_OK;
>+    }
>+    caf->dataBytesLeft -= pushSize;
>+    return gst_base_parse_finish_frame (demux, frame, pushSize);
>+  }
>+
>+  gst_buffer_map (frame->buffer, &map, GST_MAP_READ);
>+
>+  g_assert (map.size >= 12);
>+
>+  if (G_UNLIKELY (caf->state == GST_CAF_STATE_NONE)) {
>+    if (!checkCAFFileHeader (caf, &map))
>+      goto fail;
>+    caf->state = GST_CAF_STATE_INITIALIZED;
>+    *skipsize = 8;
>+  } else {
>+    guint32 chunkType = GST_READ_UINT32_LE (map.data);
>+    gint64 chunkSize = GST_READ_UINT64_BE (map.data + 4);
>+
>+    g_assert (caf->state == GST_CAF_STATE_INITIALIZED);
>+
>+    switch (chunkType) {
>+      case GST_MAKE_FOURCC ('f', 'r', 'e', 'e'):
>+        *skipsize = chunkSize + 12;
>+        GST_DEBUG_OBJECT (caf, "Skipping 'free' chunk of size %"
>+            G_GINT64_FORMAT, chunkSize);
>+        break;
>+      case GST_MAKE_FOURCC ('c', 'h', 'a', 'n'):
>+        if (!fullChunkMapped (demux, chunkSize, &map, skipsize))
>+          break;
>+        handleChanChunk (caf, map.data + 12);
>+        *skipsize = chunkSize + 12;
>+        break;
>+      case GST_MAKE_FOURCC ('d', 'e', 's', 'c'):
>+        if (!fullChunkMapped (demux, chunkSize, &map, skipsize))
>+          break;
>+        if (!handleDescChunk (caf, map.data + 12))
>+          goto fail;
>+        *skipsize = chunkSize + 12;
>+        break;
>+      case GST_MAKE_FOURCC ('d', 'a', 't', 'a'):
>+        if (map.size < 16) {
>+          /* try until we have enough data for header */
>+          gst_base_parse_set_min_frame_size (demux, 16);
>+          *skipsize = 0;
>+          break;
>+        }
>+        {
>+          const guint32 editCount = GST_READ_UINT32_BE (map.data + 12);
>+          GST_DEBUG_OBJECT (caf, "editCount=%d", editCount);
>+          *skipsize = 16;
>+          caf->state = GST_CAF_STATE_GOT_DATA;
>+          caf->dataBytesLeft = chunkSize;
>+        }
>+        break;
>+      case GST_MAKE_FOURCC ('k', 'u', 'k', 'i'):
>+      case GST_MAKE_FOURCC ('i', 'n', 's', 't'):
>+      case GST_MAKE_FOURCC ('u', 'm', 'i', 'd'):
>+      case GST_MAKE_FOURCC ('u', 'u', 'i', 'd'):
>+        *skipsize = chunkSize + 12;
>+        GST_DEBUG_OBJECT (caf, "Chunk '%" GST_FOURCC_FORMAT
>+            "' not yet impelemented, skipping size %" G_GINT64_FORMAT,
>+            GST_FOURCC_ARGS (chunkType), chunkSize);

As above. Should be a GST_FIXME_OBJECT() instead.

>+        break;
>+      default:
>+        *skipsize = chunkSize + 12;
>+        GST_DEBUG_OBJECT (caf, "Skipping unknown chunk '%" GST_FOURCC_FORMAT
>+            "' size %" G_GINT64_FORMAT, GST_FOURCC_ARGS (chunkType), chunkSize);

_WARNING_OBJECT()

Also, please add the bug number in the commit message.
Comment 13 Peter G. Baum 2014-12-22 17:18:31 UTC
Created attachment 293196 [details] [review]
Patch to implement a CAF parser

Implemented reviewers comments
- Moved to gst-plugins-bad
- GST_FIXME_OBJECT
Comment 14 Stefan Sauer (gstreamer, gtkdoc dev) 2015-01-09 21:48:38 UTC
Review of attachment 293196 [details] [review]:

FYI: other similar elements wavparse, aifparse also subclass from GstElement.

::: gst/cafdemux/gstcafdemux.c
@@ +24,3 @@
+/**
+ * SECTION:element-cafdemux
+ * @see_also:

remove the empty @see_also:

@@ +143,3 @@
+  guint32 fileType = GST_READ_UINT32_LE (map->data);
+  guint16 version = GST_READ_UINT16_BE (map->data + 4);
+  guint16 flags = GST_READ_UINT16_BE (map->data + 6);

this will probably emit a "assigned, but unused variable warning" if debug is disabled. The spec states
"""
For CAF v1 files, must be set to 0. You should ignore any value of this field you don’t understand.
"""
So I'd add a
if (flags != 0) {
  GST_WARNING_OBJECT (caf, "Unhandled flags 0x%x", (guint) flags);
}

@@ +163,3 @@
+  const gboolean isFloat = (formatFlags & 0x1) != 0;
+  const gboolean isLittleEndian = (formatFlags & 0x2) != 0;
+  const guint rate = (gint) sampleRate;

(guint) sampleRate

@@ +167,3 @@
+  GstCaps *caps;
+
+  if (sampleRate <= .0 || rate < 0) {

rate is guint and can't be < 0

@@ +179,3 @@
+  }
+
+  if (channelsPerFrame <= 0) {

channelsPerFrame is unsigned, only check for != 0

@@ +202,3 @@
+    }
+  } else {
+    switch (bitsPerChannel) {

shouldn't we support 8: also as it is simple?
Comment 15 Edward Hervey 2018-05-10 12:35:29 UTC
Peter, Stefan : any hopes of getting this mergeable in current -bad ?
Comment 16 GStreamer system administrator 2018-11-03 13:27:44 UTC
-- GitLab Migration Automatic Message --

This bug has been migrated to freedesktop.org's GitLab instance and has been closed from further activity.

You can subscribe and participate further through the new bug through this link to our GitLab instance: https://gitlab.freedesktop.org/gstreamer/gst-plugins-bad/issues/182.