From 0071c97128f5e7a615807ef1d9b4b5c418589310 Mon Sep 17 00:00:00 2001 From: Carlos Rafael Giani Date: Fri, 3 Mar 2023 12:10:38 +0100 Subject: [PATCH] qtdemux: Add audio clipping meta when playing gapless m4a content Part-of: --- .../gst-plugins-good/gst/isomp4/qtdemux.c | 224 +++++++++- .../gst-plugins-good/gst/isomp4/qtdemux.h | 33 ++ .../gst/isomp4/qtdemux_tags.c | 103 ++++- .../tests/check/elements/qtdemux.c | 418 ++++++++++++++++++ ...Hzrate-mono-s32le-200000samples-itunes.m4a | Bin 0 -> 38639 bytes ...s32le-200000samples-nero-with-itunsmpb.m4a | Bin 0 -> 19032 bytes ...le-200000samples-nero-without-itunsmpb.m4a | Bin 0 -> 18530 bytes 7 files changed, 775 insertions(+), 3 deletions(-) create mode 100644 subprojects/gst-plugins-good/tests/files/sine-1kHztone-48kHzrate-mono-s32le-200000samples-itunes.m4a create mode 100644 subprojects/gst-plugins-good/tests/files/sine-1kHztone-48kHzrate-mono-s32le-200000samples-nero-with-itunsmpb.m4a create mode 100644 subprojects/gst-plugins-good/tests/files/sine-1kHztone-48kHzrate-mono-s32le-200000samples-nero-without-itunsmpb.m4a diff --git a/subprojects/gst-plugins-good/gst/isomp4/qtdemux.c b/subprojects/gst-plugins-good/gst/isomp4/qtdemux.c index 3d668a9c8e..a872a69090 100644 --- a/subprojects/gst-plugins-good/gst/isomp4/qtdemux.c +++ b/subprojects/gst-plugins-good/gst/isomp4/qtdemux.c @@ -386,6 +386,8 @@ static gboolean gst_qtdemux_stream_update_segment (GstQTDemux * qtdemux, static void gst_qtdemux_send_gap_for_segment (GstQTDemux * demux, QtDemuxStream * stream, gint segment_index, GstClockTime pos); +static void qtdemux_check_if_is_gapless_audio (GstQTDemux * qtdemux); + static gboolean qtdemux_pull_mfro_mfra (GstQTDemux * qtdemux); static void check_update_duration (GstQTDemux * qtdemux, GstClockTime duration); @@ -659,7 +661,12 @@ gst_qtdemux_get_duration (GstQTDemux * qtdemux, GstClockTime * duration) if (qtdemux->duration != 0 && qtdemux->duration != G_MAXINT64 && qtdemux->timescale != 0) { - *duration = QTTIME_TO_GSTTIME (qtdemux, qtdemux->duration); + /* If this is single-stream audio media with gapless data, + * report the duration of the valid subset of the overall data. */ + if (qtdemux->gapless_audio_info.type != GAPLESS_AUDIO_INFO_TYPE_NONE) + *duration = qtdemux->gapless_audio_info.valid_duration; + else + *duration = QTTIME_TO_GSTTIME (qtdemux, qtdemux->duration); res = TRUE; } else { *duration = GST_CLOCK_TIME_NONE; @@ -2048,6 +2055,11 @@ gst_qtdemux_reset (GstQTDemux * qtdemux, gboolean hard) qtdemux->have_group_id = FALSE; qtdemux->group_id = G_MAXUINT; + qtdemux->gapless_audio_info.type = GAPLESS_AUDIO_INFO_TYPE_NONE; + qtdemux->gapless_audio_info.num_start_padding_pcm_frames = 0; + qtdemux->gapless_audio_info.num_end_padding_pcm_frames = 0; + qtdemux->gapless_audio_info.num_valid_pcm_frames = 0; + g_queue_clear_full (&qtdemux->protection_event_queue, (GDestroyNotify) gst_event_unref); @@ -5507,6 +5519,14 @@ gst_qtdemux_stream_update_segment (GstQTDemux * qtdemux, QtDemuxStream * stream, stream->segment.time = time; stream->segment.position = stream->segment.start; + /* Gapless audio requires adjustments to the segment + * to reflect the actual playtime length. In + * particular, this must exclude padding data. */ + if (qtdemux->gapless_audio_info.type != GAPLESS_AUDIO_INFO_TYPE_NONE) { + stream->segment.stop = stream->segment.start + + qtdemux->gapless_audio_info.valid_duration; + } + GST_DEBUG_OBJECT (stream->pad, "New segment: %" GST_SEGMENT_FORMAT, &stream->segment); @@ -6414,6 +6434,83 @@ gst_qtdemux_push_buffer (GstQTDemux * qtdemux, QtDemuxStream * stream, GST_ERROR_OBJECT (qtdemux, "failed to attach aavd metadata to buffer"); } + if (qtdemux->gapless_audio_info.type != GAPLESS_AUDIO_INFO_TYPE_NONE) { + guint64 num_start_padding_pcm_frames; + guint64 audio_sample_offset; + guint64 audio_sample_offset_end; + guint64 start_of_trailing_padding; + guint64 start_clip = 0, end_clip = 0; + guint64 total_num_clipped_samples; + GstClockTime timestamp_decrement; + + /* Attach GstAudioClippingMeta to exclude padding data. */ + + num_start_padding_pcm_frames = + qtdemux->gapless_audio_info.num_start_padding_pcm_frames; + + audio_sample_offset = stream->sample_index * stream->stts_duration; + audio_sample_offset_end = audio_sample_offset + stream->stts_duration; + start_of_trailing_padding = num_start_padding_pcm_frames + + qtdemux->gapless_audio_info.num_valid_pcm_frames; + + if (audio_sample_offset < num_start_padding_pcm_frames) { + guint64 num_padding_audio_samples = + num_start_padding_pcm_frames - audio_sample_offset; + start_clip = MIN (num_padding_audio_samples, stream->stts_duration); + } + + timestamp_decrement = qtdemux->gapless_audio_info.start_padding_duration; + + if (audio_sample_offset >= start_of_trailing_padding) { + /* This case happens when the buffer is located fully past + * the beginning of the padding area at the end of the stream. + * Add the end padding to the decrement amount to ensure + * continuous timestamps when transitioning from gapless + * media to gapless media. */ + end_clip = stream->stts_duration; + timestamp_decrement += qtdemux->gapless_audio_info.end_padding_duration; + } else if (audio_sample_offset_end >= start_of_trailing_padding) { + /* This case happens when the beginning of the padding area that + * is located at the end of the stream intersects the buffer. */ + end_clip = audio_sample_offset_end - start_of_trailing_padding; + } + + total_num_clipped_samples = start_clip + end_clip; + + if (total_num_clipped_samples != 0) { + GST_DEBUG_OBJECT (qtdemux, "adding audio clipping meta: start / " + "end clip: %" G_GUINT64_FORMAT " / %" G_GUINT64_FORMAT, + start_clip, end_clip); + gst_buffer_add_audio_clipping_meta (buf, GST_FORMAT_DEFAULT, + start_clip, end_clip); + + if (total_num_clipped_samples >= stream->stts_duration) { + GST_BUFFER_DURATION (buf) = 0; + GST_BUFFER_FLAG_SET (buf, GST_BUFFER_FLAG_DECODE_ONLY); + GST_BUFFER_FLAG_SET (buf, GST_BUFFER_FLAG_DROPPABLE); + } else { + guint64 num_valid_samples = + stream->stts_duration - total_num_clipped_samples; + GST_BUFFER_DURATION (buf) = + QTSTREAMTIME_TO_GSTTIME (stream, num_valid_samples); + } + } + + /* The timestamps need to be shifted to factor in the skipped padding data. */ + + if (GST_BUFFER_PTS_IS_VALID (buf)) { + GstClockTime ts = GST_BUFFER_PTS (buf); + GST_BUFFER_PTS (buf) = + (ts >= timestamp_decrement) ? (ts - timestamp_decrement) : 0; + } + + if (GST_BUFFER_DTS_IS_VALID (buf)) { + GstClockTime ts = GST_BUFFER_DTS (buf); + GST_BUFFER_DTS (buf) = + (ts >= timestamp_decrement) ? (ts - timestamp_decrement) : 0; + } + } + if (stream->protected && (stream->protection_scheme_type == FOURCC_cenc || stream->protection_scheme_type == FOURCC_cbcs)) { GstStructure *crypto_info; @@ -7565,6 +7662,129 @@ gst_qtdemux_send_gap_for_segment (GstQTDemux * demux, } } +static void +qtdemux_check_if_is_gapless_audio (GstQTDemux * qtdemux) +{ + QtDemuxStream *stream; + + if (QTDEMUX_N_STREAMS (qtdemux) != 1) + goto incompatible_stream; + + stream = QTDEMUX_NTH_STREAM (qtdemux, 0); + + if (stream->subtype != FOURCC_soun || stream->n_segments != 1) + goto incompatible_stream; + + /* Gapless audio info from revdns tags (most notably iTunSMPB) is + * detected in the main udta node. If it isn't present, try as + * fallback to recognize the encoder name, and apply known priming + * and padding quantities specific to the encoder. */ + if (qtdemux->gapless_audio_info.type == GAPLESS_AUDIO_INFO_TYPE_NONE) { + const gchar *orig_encoder_name = NULL; + + if (gst_tag_list_peek_string_index (qtdemux->tag_list, GST_TAG_ENCODER, 0, + &orig_encoder_name) && orig_encoder_name != NULL) { + gchar *lowercase_encoder_name = g_ascii_strdown (orig_encoder_name, -1); + + if (strstr (lowercase_encoder_name, "nero") != NULL) + qtdemux->gapless_audio_info.type = GAPLESS_AUDIO_INFO_TYPE_NERO; + + g_free (lowercase_encoder_name); + + switch (qtdemux->gapless_audio_info.type) { + case GAPLESS_AUDIO_INFO_TYPE_NERO:{ + guint64 total_length; + guint64 valid_length; + guint64 start_padding; + + /* The Nero AAC encoder always uses a lead-in of 1600 PCM frames. + * Also, in Nero AAC's case, stream->duration contains the number + * of PCM frames with start padding but without end padding. + * The decoder delay equals 1 frame length, which is covered by + * factoring stream->stts_duration into the start padding. */ + start_padding = 1600 + stream->stts_duration; + + if (G_UNLIKELY (stream->duration < start_padding)) { + GST_ERROR_OBJECT (qtdemux, "stream duration is %" G_GUINT64_FORMAT + " but start_padding is %" G_GUINT64_FORMAT, stream->duration, + start_padding); + goto invalid_gapless_audio_info; + } + valid_length = stream->duration - start_padding; + + qtdemux->gapless_audio_info.num_start_padding_pcm_frames = + start_padding; + qtdemux->gapless_audio_info.num_valid_pcm_frames = valid_length; + + total_length = stream->n_samples * stream->stts_duration; + + if (G_LIKELY (total_length >= valid_length)) { + guint64 total_padding = total_length - valid_length; + if (G_UNLIKELY (total_padding < start_padding)) { + GST_ERROR_OBJECT (qtdemux, "total_padding is %" G_GUINT64_FORMAT + " but start_padding is %" G_GUINT64_FORMAT, total_padding, + start_padding); + goto invalid_gapless_audio_info; + } + + qtdemux->gapless_audio_info.num_end_padding_pcm_frames = + total_padding - start_padding; + } else { + qtdemux->gapless_audio_info.num_end_padding_pcm_frames = 0; + } + + GST_DEBUG_OBJECT (qtdemux, "media was encoded with Nero AAC encoder; " + "using encoder specific lead-in and padding figures"); + } + + default: + break; + } + } + } + + if (qtdemux->gapless_audio_info.type != GAPLESS_AUDIO_INFO_TYPE_NONE) { + qtdemux->gapless_audio_info.start_padding_duration = + QTSTREAMTIME_TO_GSTTIME (stream, + qtdemux->gapless_audio_info.num_start_padding_pcm_frames); + qtdemux->gapless_audio_info.end_padding_duration = + QTSTREAMTIME_TO_GSTTIME (stream, + qtdemux->gapless_audio_info.num_end_padding_pcm_frames); + qtdemux->gapless_audio_info.valid_duration = + QTSTREAMTIME_TO_GSTTIME (stream, + qtdemux->gapless_audio_info.num_valid_pcm_frames); + } + + GST_DEBUG_OBJECT (qtdemux, "found valid gapless audio info: num start / end " + "PCM padding frames: %" G_GUINT64_FORMAT " / %" G_GUINT64_FORMAT "; " + "start / end padding durations: %" GST_TIME_FORMAT " / %" GST_TIME_FORMAT + "; num valid PCM frames: %" G_GUINT64_FORMAT "; valid duration: %" + GST_TIME_FORMAT, qtdemux->gapless_audio_info.num_start_padding_pcm_frames, + qtdemux->gapless_audio_info.num_end_padding_pcm_frames, + GST_TIME_ARGS (qtdemux->gapless_audio_info.start_padding_duration), + GST_TIME_ARGS (qtdemux->gapless_audio_info.end_padding_duration), + qtdemux->gapless_audio_info.num_valid_pcm_frames, + GST_TIME_ARGS (qtdemux->gapless_audio_info.valid_duration)); + + return; + +incompatible_stream: + if (G_UNLIKELY (qtdemux->gapless_audio_info.type != + GAPLESS_AUDIO_INFO_TYPE_NONE)) { + GST_WARNING_OBJECT (qtdemux, + "media contains gapless audio info, but it is not suitable for " + "gapless audio playback (media must be audio-only, single-stream, " + "single-segment; ignoring unusable gapless info"); + qtdemux->gapless_audio_info.type = GAPLESS_AUDIO_INFO_TYPE_NONE; + } + return; + +invalid_gapless_audio_info: + GST_WARNING_OBJECT (qtdemux, + "media contains invalid/unusable gapless audio info"); + return; +} + static GstFlowReturn gst_qtdemux_chain (GstPad * sinkpad, GstObject * parent, GstBuffer * inbuf) { @@ -14009,6 +14229,8 @@ qtdemux_prepare_streams (GstQTDemux * qtdemux) } } + qtdemux_check_if_is_gapless_audio (qtdemux); + return ret; } diff --git a/subprojects/gst-plugins-good/gst/isomp4/qtdemux.h b/subprojects/gst-plugins-good/gst/isomp4/qtdemux.h index 830ed2fda5..6e7f64b91c 100644 --- a/subprojects/gst-plugins-good/gst/isomp4/qtdemux.h +++ b/subprojects/gst-plugins-good/gst/isomp4/qtdemux.h @@ -54,6 +54,7 @@ typedef struct _QtDemuxSample QtDemuxSample; typedef struct _QtDemuxSegment QtDemuxSegment; typedef struct _QtDemuxRandomAccessEntry QtDemuxRandomAccessEntry; typedef struct _QtDemuxStreamStsdEntry QtDemuxStreamStsdEntry; +typedef struct _QtDemuxGaplessAudioInfo QtDemuxGaplessAudioInfo; typedef GstBuffer * (*QtDemuxProcessFunc)(GstQTDemux * qtdemux, QtDemuxStream * stream, GstBuffer * buf); @@ -78,6 +79,36 @@ typedef enum { VARIANT_MSS_FRAGMENTED, } Variant; +typedef enum { + /* No valid gapless audio info present. Types other than this one + * are used only if all of these apply: + * + * 1. There is embedded gapless audio information available + * 2. Only one stream exists + * 3. Said stream has only one segment + * 4. Said stream is an audio stream + */ + GAPLESS_AUDIO_INFO_TYPE_NONE, + /* Using information from the iTunes iTunSMPB revdns tag. */ + GAPLESS_AUDIO_INFO_TYPE_ITUNES, + /* Using known Nero encoder delay information. */ + GAPLESS_AUDIO_INFO_TYPE_NERO +} QtDemuxGaplessAudioInfoType; + +/* Gapless audio information, only used for single-stream audio-only media. */ +struct _QtDemuxGaplessAudioInfo { + QtDemuxGaplessAudioInfoType type; + + guint64 num_start_padding_pcm_frames; + guint64 num_end_padding_pcm_frames; + guint64 num_valid_pcm_frames; + + /* PCM frame amounts converted to nanoseconds. */ + GstClockTime start_padding_duration; + GstClockTime end_padding_duration; + GstClockTime valid_duration; +}; + struct _GstQTDemux { GstElement element; @@ -177,6 +208,8 @@ struct _GstQTDemux { gint64 chapters_track_id; + QtDemuxGaplessAudioInfo gapless_audio_info; + /* protection support */ GPtrArray *protection_system_ids; /* Holds identifiers of all content protection systems for all tracks */ GQueue protection_event_queue; /* holds copy of upstream protection events */ diff --git a/subprojects/gst-plugins-good/gst/isomp4/qtdemux_tags.c b/subprojects/gst-plugins-good/gst/isomp4/qtdemux_tags.c index 0531dcba59..0ec9cb99cd 100644 --- a/subprojects/gst-plugins-good/gst/isomp4/qtdemux_tags.c +++ b/subprojects/gst-plugins-good/gst/isomp4/qtdemux_tags.c @@ -747,12 +747,111 @@ qtdemux_tag_add_revdns (GstQTDemux * demux, GstTagList * taglist, break; } } - if (i == G_N_ELEMENTS (tags)) - goto unknown_tag; + + /* Some tags might not actually be used for metadata about the media, + * but for other purposes. One such tag is iTunSMPB, which contains + * padding information for gapless playback. Scan these separately. */ + if (i == G_N_ELEMENTS (tags)) { + if (!g_ascii_strncasecmp ("iTunSMPB", namestr, 8)) { + /* iTunSMPB tag format goes as follows: + * + * " 00000000 xxxxxxxx yyyyyyyy zzzzzzzzzzzzzzzz 00000000 00000000 00000000 00000000 00000000 00000000 00000000 00000000" + * + * The data is actually an ASCII string containing these hex fields. + * The description above is _not_ a description of a binary format! + * These need to be parsed with g_ascii_strtoull() and base 16. + * + * (The quotes are not part of it; they just emphasize the + * whitespace at the beginning of the string). + * + * Only the fields marked with x/y/z are of interest here. + * + * The x field is the priming, in samples. + * These are the padding samples at the beginning of the stream. + * + * The y field is the remainder, in samples. + * These are the padding samples at the end of the stream. + * + * The z field is the number of valid PCM frames, excluding the + * priming and remainder. (In other words, the number of PCM + * frames that make up the actual audio, without the padding.) + * + * The data starts at offset 16. All access to it must therefore skip + * the first 16 bytes. + */ + + const gsize start_offset = 16; + const gsize priming_offset = start_offset + 10; + const gsize remainder_offset = start_offset + 19; + const gsize num_valid_pcm_frames_offset = start_offset + 28; + const gsize total_length = 44; + const gchar *str; + guint64 priming; + guint64 remainder; + guint64 num_valid_pcm_frames; + /* Temporary buffer for g_ascii_strtoull() calls. + * Add extra +1 space for nullbyte. */ + gchar tmp[16 + 1]; + + /* Use the iTunSMPB info if no other info has been found yet. */ + if (demux->gapless_audio_info.type != GAPLESS_AUDIO_INFO_TYPE_NONE) { + GST_DEBUG_OBJECT (demux, "iTunSMPB information found, " + "but other gapless audio info was already read"); + goto finish; + } + + if (G_UNLIKELY (datasize < (start_offset + total_length))) { + GST_WARNING_OBJECT (demux, + "iTunSMPB tag data size too small - not parsing"); + goto finish; + } + + str = (gchar *) ((guint8 *) data->data); + +#define PARSE_ITUNSMPB_FIELD(FIELD_NAME, NUM_DIGITS) \ + G_STMT_START \ + { \ + gint str_idx; \ +\ + for (str_idx = 0; str_idx < (NUM_DIGITS); ++str_idx) { \ + gchar ch = str[FIELD_NAME ## _offset + str_idx]; \ + if (!g_ascii_isxdigit (ch)) { \ + GST_WARNING_OBJECT (demux, #FIELD_NAME " field in iTunSMPB " \ + "tag data has invalid character '%c'", ch); \ + goto finish; \ + } \ + tmp[str_idx] = ch; \ + } \ + tmp[NUM_DIGITS] = 0; \ +\ + FIELD_NAME = g_ascii_strtoull (tmp, NULL, 16); \ + } \ + G_STMT_END + + PARSE_ITUNSMPB_FIELD (priming, 8); + PARSE_ITUNSMPB_FIELD (remainder, 8); + PARSE_ITUNSMPB_FIELD (num_valid_pcm_frames, 16); + +#undef PARSE_ITUNSMPB_FIELD + + GST_DEBUG_OBJECT (demux, "iTunSMPB information: priming %" + G_GUINT64_FORMAT " remainder %" G_GUINT64_FORMAT + " num valid PCM frames %" G_GUINT64_FORMAT, priming, remainder, + num_valid_pcm_frames); + + demux->gapless_audio_info.type = GAPLESS_AUDIO_INFO_TYPE_ITUNES; + demux->gapless_audio_info.num_start_padding_pcm_frames = priming; + demux->gapless_audio_info.num_end_padding_pcm_frames = remainder; + demux->gapless_audio_info.num_valid_pcm_frames = num_valid_pcm_frames; + } else { + goto unknown_tag; + } + } } else { goto unknown_tag; } +finish: return; /* errors */ diff --git a/subprojects/gst-plugins-good/tests/check/elements/qtdemux.c b/subprojects/gst-plugins-good/tests/check/elements/qtdemux.c index 04092afff1..c98a0d655e 100644 --- a/subprojects/gst-plugins-good/tests/check/elements/qtdemux.c +++ b/subprojects/gst-plugins-good/tests/check/elements/qtdemux.c @@ -27,6 +27,8 @@ #include #include +#include +#include #define TEST_FILE_PREFIX GST_TEST_FILES_PATH G_DIR_SEPARATOR_S @@ -1200,6 +1202,419 @@ GST_START_TEST (test_qtdemux_mss_fragment) GST_END_TEST; +typedef struct +{ + const gchar *filename; + /* Total number of AAC frames, including any and all dummy/empty/padding frames. */ + guint num_aac_frames; + /* In AAC, this is 1024 in the vast majority of the cases. + * AAC can also use 960 samples per frame, but this is rare. */ + guint num_samples_per_frame; + /* How many padding samples to expect at the beginning and the end. + * The amount of padding samples can exceed the size of a frame. + * This means that the first and last N frame(s) can actually be + * fully made of padding samples and thus need to be thrown away. */ + guint num_start_padding_samples; + guint num_end_padding_samples; + guint sample_rate; + /* Some encoders produce data whose last frame uses a different + * (smaller) stts value to handle the padding at the end. Data + * produced by such encoders will not get a clipmeta added at the + * end. When using test data produced by such an encoder, this + * must be set to FALSE, otherwise it must be set to TRUE. + * Notably, anything that produces an iTunSMPB tag (iTunes itself + * as well as newer Nero encoders for example) will cause such + * a clipmeta to be added. */ + gboolean expect_clipmeta_at_end; + + /* Total number of samples available, with / without padding + * samples factored in. */ + guint64 num_samples_with_padding; + guint64 num_samples_without_padding; + + /* The index of the first / last frame that contains valid samples. + * Indices start with 0. Valid range is [0 , (num_aac_frames-1)]. + * In virtually all cases, when the AAC data was encoded with iTunes, + * the first and last valid frames will be partially clipped. */ + guint first_frame_with_valid_samples; + guint last_frame_with_valid_samples; + + guint64 num_samples_in_first_valid_frame; + guint64 num_samples_in_last_valid_frame; + + GstClockTime total_duration_without_padding; + + GstElement *appsink; +} GaplessTestInfo; + +static void +precalculate_gapless_test_factors (GaplessTestInfo * info) +{ + info->num_samples_with_padding = info->num_aac_frames * + info->num_samples_per_frame; + info->num_samples_without_padding = info->num_samples_with_padding - + info->num_start_padding_samples - info->num_end_padding_samples; + + info->first_frame_with_valid_samples = info->num_start_padding_samples / + info->num_samples_per_frame; + info->last_frame_with_valid_samples = (info->num_samples_with_padding - + info->num_end_padding_samples) / info->num_samples_per_frame; + + info->num_samples_in_first_valid_frame = + (info->first_frame_with_valid_samples + 1) * info->num_samples_per_frame - + info->num_start_padding_samples; + info->num_samples_in_last_valid_frame = + (info->num_samples_with_padding - info->num_end_padding_samples) - + info->last_frame_with_valid_samples * info->num_samples_per_frame; + + /* The total actual playtime duration. */ + info->total_duration_without_padding = + gst_util_uint64_scale_int (info->num_samples_without_padding, GST_SECOND, + info->sample_rate); + + GST_DEBUG ("num_samples_with_padding %" G_GUINT64_FORMAT + " num_samples_without_padding %" G_GUINT64_FORMAT + " first_frame_with_valid_samples %u" + " last_frame_with_valid_samples %u" + " num_samples_in_first_valid_frame %" G_GUINT64_FORMAT + " num_samples_in_last_valid_frame %" G_GUINT64_FORMAT + " total_duration_without_padding %" G_GUINT64_FORMAT, + info->num_samples_with_padding, info->num_samples_without_padding, + info->first_frame_with_valid_samples, info->last_frame_with_valid_samples, + info->num_samples_in_first_valid_frame, + info->num_samples_in_last_valid_frame, + info->total_duration_without_padding); +} + +static void +setup_gapless_itunes_test_info (GaplessTestInfo * info) +{ + info->filename = + "sine-1kHztone-48kHzrate-mono-s32le-200000samples-itunes.m4a"; + info->num_aac_frames = 198; + info->num_samples_per_frame = 1024; + info->sample_rate = 48000; + info->expect_clipmeta_at_end = TRUE; + + info->num_start_padding_samples = 2112; + info->num_end_padding_samples = 640; + + precalculate_gapless_test_factors (info); +} + +static void +setup_gapless_nero_with_itunsmpb_test_info (GaplessTestInfo * info) +{ + info->filename = + "sine-1kHztone-48kHzrate-mono-s32le-200000samples-nero-with-itunsmpb.m4a"; + info->num_aac_frames = 198; + info->num_samples_per_frame = 1024; + info->sample_rate = 48000; + info->expect_clipmeta_at_end = TRUE; + + info->num_start_padding_samples = 2624; + info->num_end_padding_samples = 128; + + precalculate_gapless_test_factors (info); +} + +static void +setup_gapless_nero_without_itunsmpb_test_info (GaplessTestInfo * info) +{ + info->filename = + "sine-1kHztone-48kHzrate-mono-s32le-200000samples-nero-without-itunsmpb.m4a"; + info->num_aac_frames = 198; + info->num_samples_per_frame = 1024; + info->sample_rate = 48000; + /* Older Nero AAC encoders produce a different stts value for the + * last frame to skip padding data. In this file, all frames except + * the last one use an stts value of 1024, while the last value + * uses an stts value of 896. Consequently, the logic inside qtdemux + * won't deem it necessary to add an audioclipmeta - there are no + * padding samples to clip. */ + info->expect_clipmeta_at_end = FALSE; + + info->num_start_padding_samples = 2624; + info->num_end_padding_samples = 128; + + precalculate_gapless_test_factors (info); +} + +static void +check_parsed_aac_frame (GaplessTestInfo * info, guint frame_num) +{ + GstClockTime expected_pts = GST_CLOCK_TIME_NONE; + GstClockTime expected_duration = GST_CLOCK_TIME_NONE; + GstClockTimeDiff ts_delta; + guint64 expected_sample_offset; + guint64 expected_num_samples; + gboolean expect_audioclipmeta = FALSE; + guint64 expected_audioclipmeta_start = 0; + guint64 expected_audioclipmeta_end = 0; + GstSample *sample; + GstBuffer *buffer; + GstAudioClippingMeta *audioclip_meta; + + if (frame_num < info->first_frame_with_valid_samples) { + /* Frame is at the beginning and is fully clipped. */ + expected_sample_offset = 0; + expected_num_samples = 0; + + expected_audioclipmeta_start = info->num_samples_per_frame; + expected_audioclipmeta_end = 0; + } else if (frame_num == info->first_frame_with_valid_samples) { + /* Frame is at the beginning and is partially clipped. */ + + expected_sample_offset = 0; + expected_num_samples = info->num_samples_in_first_valid_frame; + + expected_audioclipmeta_start = info->num_samples_per_frame - + info->num_samples_in_first_valid_frame; + expected_audioclipmeta_end = 0; + } else if (frame_num < info->last_frame_with_valid_samples) { + /* Regular, unclipped frame. */ + + expected_sample_offset = info->num_samples_in_first_valid_frame + + info->num_samples_per_frame * (frame_num - + info->first_frame_with_valid_samples - 1); + expected_num_samples = info->num_samples_per_frame; + } else if (frame_num == info->last_frame_with_valid_samples) { + /* The first frame at the end with padding samples. This one will have + * the last few valid samples, followed by the first padding samples. */ + + expected_sample_offset = info->num_samples_in_first_valid_frame + + info->num_samples_per_frame * (frame_num - + info->first_frame_with_valid_samples - 1); + expected_num_samples = info->num_samples_in_last_valid_frame; + + if (info->expect_clipmeta_at_end) { + expect_audioclipmeta = TRUE; + expected_audioclipmeta_start = 0; + expected_audioclipmeta_end = + info->num_samples_per_frame - expected_num_samples; + } + } else { + /* A fully clipped frame at the end of the stream. */ + + expected_sample_offset = info->num_samples_in_first_valid_frame + + info->num_samples_without_padding; + expected_num_samples = 0; + + if (info->expect_clipmeta_at_end) { + expect_audioclipmeta = TRUE; + expected_audioclipmeta_start = 0; + expected_audioclipmeta_end = info->num_samples_per_frame; + } + } + + /* Pull the frame from appsink so we can check it. */ + + sample = gst_app_sink_pull_sample (GST_APP_SINK (info->appsink)); + fail_if (sample == NULL); + fail_unless (GST_IS_SAMPLE (sample)); + + expected_pts = gst_util_uint64_scale_int (expected_sample_offset, + GST_SECOND, info->sample_rate); + expected_duration = gst_util_uint64_scale_int (expected_num_samples, + GST_SECOND, info->sample_rate); + + buffer = gst_sample_get_buffer (sample); + fail_if (buffer == NULL); + + /* Verify the sample's PTS and duration. Allow for 1 nanosecond difference + * to account for rounding errors in sample <-> timestamp conversions. */ + ts_delta = GST_CLOCK_DIFF (GST_BUFFER_PTS (buffer), expected_pts); + fail_unless (ABS (ts_delta) <= 1); + ts_delta = GST_CLOCK_DIFF (GST_BUFFER_DURATION (buffer), expected_duration); + fail_unless (ABS (ts_delta) <= 1); + /* Check if there's audio clip metadata, and verify it if it exists. */ + if (expect_audioclipmeta) { + audioclip_meta = gst_buffer_get_audio_clipping_meta (buffer); + fail_if (audioclip_meta == NULL); + fail_unless_equals_uint64 (audioclip_meta->start, + expected_audioclipmeta_start); + fail_unless_equals_uint64 (audioclip_meta->end, expected_audioclipmeta_end); + } + + gst_sample_unref (sample); +} + +static void +qtdemux_pad_added_cb_for_gapless (GstElement * demux, GstPad * pad, + GaplessTestInfo * info) +{ + GstPad *appsink_pad; + GstPadLinkReturn ret; + + appsink_pad = gst_element_get_static_pad (info->appsink, "sink"); + + if (gst_pad_is_linked (appsink_pad)) + goto finish; + + ret = gst_pad_link (pad, appsink_pad); + if (GST_PAD_LINK_FAILED (ret)) { + GST_ERROR ("Could not link qtdemux and appsink: %s", + gst_pad_link_get_name (ret)); + } + +finish: + gst_object_unref (GST_OBJECT (appsink_pad)); +} + +static void +perform_gapless_test (GaplessTestInfo * info) +{ + GstElement *source, *demux, *appsink, *pipeline; + GstStateChangeReturn state_ret; + guint frame_num; + + pipeline = gst_pipeline_new (NULL); + source = gst_element_factory_make ("filesrc", NULL); + demux = gst_element_factory_make ("qtdemux", NULL); + appsink = gst_element_factory_make ("appsink", NULL); + + info->appsink = appsink; + + g_signal_connect (demux, "pad-added", (GCallback) + qtdemux_pad_added_cb_for_gapless, info); + + gst_bin_add_many (GST_BIN (pipeline), source, demux, appsink, NULL); + gst_element_link (source, demux); + + { + char *full_filename = + g_build_filename (GST_TEST_FILES_PATH, info->filename, NULL); + g_object_set (G_OBJECT (source), "location", full_filename, NULL); + g_free (full_filename); + } + + g_object_set (G_OBJECT (appsink), "async", FALSE, "sync", FALSE, + "max-buffers", 1, "enable-last-sample", FALSE, "processing-deadline", + G_MAXUINT64, NULL); + + state_ret = gst_element_set_state (pipeline, GST_STATE_PLAYING); + + fail_unless (state_ret != GST_STATE_CHANGE_FAILURE); + + if (state_ret == GST_STATE_CHANGE_ASYNC) { + GST_LOG ("waiting for pipeline to reach PAUSED state"); + state_ret = gst_element_get_state (pipeline, NULL, NULL, -1); + fail_unless_equals_int (state_ret, GST_STATE_CHANGE_SUCCESS); + } + + /* Verify all frames from the test signal. */ + for (frame_num = 0; frame_num < info->num_aac_frames; ++frame_num) + check_parsed_aac_frame (info, frame_num); + + /* Check what duration is returned by a query. This duration must exclude + * the padding samples. */ + { + GstQuery *query; + gint64 duration; + GstFormat format; + + query = gst_query_new_duration (GST_FORMAT_TIME); + fail_unless (gst_element_query (pipeline, query)); + + gst_query_parse_duration (query, &format, &duration); + fail_unless_equals_int (format, GST_FORMAT_TIME); + fail_unless_equals_uint64 ((guint64) duration, + info->total_duration_without_padding); + + gst_query_unref (query); + } + + /* Seek tests: Here we seek to a certain position that corresponds to a + * certain frame. Then we check if we indeed got that frame. */ + + /* Seek back to the first frame. This will _not_ be the first valid frame. + * Instead, it will be a frame that gets only decoded and has duration + * zero. Other zero-duration frames may follow, until the first frame + * with valid data is encountered. This means that when the user seeks + * to position 0, downstream will subsequently get a number of buffers + * with PTS 0, and all of those buffers except the last will have a + * duration of 0. */ + { + fail_unless_equals_int (gst_element_set_state (pipeline, GST_STATE_PAUSED), + GST_STATE_CHANGE_SUCCESS); + gst_element_seek_simple (pipeline, GST_FORMAT_TIME, GST_SEEK_FLAG_FLUSH, 0); + fail_unless_equals_int (gst_element_set_state (pipeline, GST_STATE_PLAYING), + GST_STATE_CHANGE_SUCCESS); + + check_parsed_aac_frame (info, 0); + } + + /* Now move to the frame past the very first one that contained valid samples. + * This very first frame will usually be clipped, and be output as the last + * buffer at PTS 0 (see above). */ + { + GstClockTime position; + + position = + gst_util_uint64_scale_int (info->num_samples_in_first_valid_frame, + GST_SECOND, info->sample_rate); + + fail_unless_equals_int (gst_element_set_state (pipeline, GST_STATE_PAUSED), + GST_STATE_CHANGE_SUCCESS); + gst_element_seek_simple (pipeline, GST_FORMAT_TIME, GST_SEEK_FLAG_FLUSH, + position); + fail_unless_equals_int (gst_element_set_state (pipeline, GST_STATE_PLAYING), + GST_STATE_CHANGE_SUCCESS); + + check_parsed_aac_frame (info, info->first_frame_with_valid_samples + 1); + } + + /* Seek to the last frame with valid samples (= the first frame with padding + * samples at the end of the stream). */ + { + GstClockTime position; + + position = + gst_util_uint64_scale_int (info->num_samples_in_first_valid_frame + + info->num_samples_without_padding - info->num_samples_per_frame, + GST_SECOND, info->sample_rate); + + fail_unless_equals_int (gst_element_set_state (pipeline, GST_STATE_PAUSED), + GST_STATE_CHANGE_SUCCESS); + gst_element_seek_simple (pipeline, GST_FORMAT_TIME, GST_SEEK_FLAG_FLUSH, + position); + fail_unless_equals_int (gst_element_set_state (pipeline, GST_STATE_PLAYING), + GST_STATE_CHANGE_SUCCESS); + + check_parsed_aac_frame (info, info->last_frame_with_valid_samples); + } + + gst_element_set_state (pipeline, GST_STATE_NULL); + gst_object_unref (pipeline); +} + +GST_START_TEST (test_qtdemux_gapless_itunes_data) +{ + GaplessTestInfo info; + setup_gapless_itunes_test_info (&info); + perform_gapless_test (&info); +} + +GST_END_TEST; + +GST_START_TEST (test_qtdemux_gapless_nero_data_with_itunsmpb) +{ + GaplessTestInfo info; + setup_gapless_nero_with_itunsmpb_test_info (&info); + perform_gapless_test (&info); +} + +GST_END_TEST; + +GST_START_TEST (test_qtdemux_gapless_nero_data_without_itunsmpb) +{ + GaplessTestInfo info; + setup_gapless_nero_without_itunsmpb_test_info (&info); + perform_gapless_test (&info); +} + +GST_END_TEST; + static Suite * qtdemux_suite (void) { @@ -1215,6 +1630,9 @@ qtdemux_suite (void) tcase_add_test (tc_chain, test_qtdemux_pad_names); tcase_add_test (tc_chain, test_qtdemux_compensate_data_offset); tcase_add_test (tc_chain, test_qtdemux_mss_fragment); + tcase_add_test (tc_chain, test_qtdemux_gapless_itunes_data); + tcase_add_test (tc_chain, test_qtdemux_gapless_nero_data_with_itunsmpb); + tcase_add_test (tc_chain, test_qtdemux_gapless_nero_data_without_itunsmpb); return s; } diff --git a/subprojects/gst-plugins-good/tests/files/sine-1kHztone-48kHzrate-mono-s32le-200000samples-itunes.m4a b/subprojects/gst-plugins-good/tests/files/sine-1kHztone-48kHzrate-mono-s32le-200000samples-itunes.m4a new file mode 100644 index 0000000000000000000000000000000000000000..ddea727aa5b02733e5c83f6649967ad21e01b9bb GIT binary patch literal 38639 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 I+UmLg0l|hyOaK4? literal 0 HcmV?d00001 diff --git a/subprojects/gst-plugins-good/tests/files/sine-1kHztone-48kHzrate-mono-s32le-200000samples-nero-with-itunsmpb.m4a b/subprojects/gst-plugins-good/tests/files/sine-1kHztone-48kHzrate-mono-s32le-200000samples-nero-with-itunsmpb.m4a new file mode 100644 index 0000000000000000000000000000000000000000..963e9cf2d3bbc282c02cde7ff3fd71010f3f9659 GIT binary patch literal 19032 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