mirror of
https://gitlab.freedesktop.org/gstreamer/gstreamer.git
synced 2024-09-09 05:38:58 +00:00
Gapless playback is handled by adjusting buffer timestamps & durations and by adding GstAudioClippingMeta. Support for "Frankenstein" streams (= poorly stitched together streams) is also added, so that gapless playback support doesn't prevent those from being properly played. Co-authored-by: Sebastian Dröge <sebastian@centricular.com> Part-of: <https://gitlab.freedesktop.org/gstreamer/gstreamer/-/merge_requests/1028>
446 lines
15 KiB
C
446 lines
15 KiB
C
/*
|
|
* GStreamer
|
|
*
|
|
* unit test for aacparse
|
|
*
|
|
* Copyright (C) 2008 Nokia Corporation. All rights reserved.
|
|
*
|
|
* Contact: Stefan Kost <stefan.kost@nokia.com>
|
|
*
|
|
* This library is free software; you can redistribute it and/or
|
|
* modify it under the terms of the GNU Library General Public
|
|
* License as published by the Free Software Foundation; either
|
|
* version 2 of the License, or (at your option) any later version.
|
|
*
|
|
* This library 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
|
|
* Library General Public License for more details.
|
|
*
|
|
* You should have received a copy of the GNU Library General Public
|
|
* License along with this library; if not, write to the
|
|
* Free Software Foundation, Inc., 51 Franklin St, Fifth Floor,
|
|
* Boston, MA 02110-1301, USA.
|
|
*/
|
|
|
|
#include <gst/check/gstcheck.h>
|
|
#include <gst/app/gstappsink.h>
|
|
#include <gst/audio/audio.h>
|
|
#include "parser.h"
|
|
|
|
#define SRC_CAPS_TMPL "audio/mpeg, parsed=(boolean)false, mpegversion=(int)1"
|
|
#define SINK_CAPS_TMPL "audio/mpeg, parsed=(boolean)true, mpegversion=(int)1"
|
|
|
|
GstStaticPadTemplate sinktemplate = GST_STATIC_PAD_TEMPLATE ("sink",
|
|
GST_PAD_SINK,
|
|
GST_PAD_ALWAYS,
|
|
GST_STATIC_CAPS (SINK_CAPS_TMPL)
|
|
);
|
|
|
|
GstStaticPadTemplate srctemplate = GST_STATIC_PAD_TEMPLATE ("src",
|
|
GST_PAD_SRC,
|
|
GST_PAD_ALWAYS,
|
|
GST_STATIC_CAPS (SRC_CAPS_TMPL)
|
|
);
|
|
|
|
/* some data */
|
|
static guint8 mp3_frame[384] = {
|
|
0xff, 0xfb, 0x94, 0xc4, 0xff, 0x83, 0xc0, 0x00,
|
|
0x01, 0xa4, 0x00, 0x00, 0x00, 0x20, 0x00, 0x00,
|
|
0x34, 0x80, 0x00, 0x00, 0x04, 0x00,
|
|
};
|
|
|
|
static guint8 garbage_frame[] = {
|
|
0xff, 0xff, 0xff, 0xff, 0xff
|
|
};
|
|
|
|
|
|
GST_START_TEST (test_parse_normal)
|
|
{
|
|
gst_parser_test_normal (mp3_frame, sizeof (mp3_frame));
|
|
}
|
|
|
|
GST_END_TEST;
|
|
|
|
|
|
GST_START_TEST (test_parse_drain_single)
|
|
{
|
|
gst_parser_test_drain_single (mp3_frame, sizeof (mp3_frame));
|
|
}
|
|
|
|
GST_END_TEST;
|
|
|
|
|
|
GST_START_TEST (test_parse_drain_garbage)
|
|
{
|
|
gst_parser_test_drain_garbage (mp3_frame, sizeof (mp3_frame),
|
|
garbage_frame, sizeof (garbage_frame));
|
|
}
|
|
|
|
GST_END_TEST;
|
|
|
|
|
|
GST_START_TEST (test_parse_split)
|
|
{
|
|
gst_parser_test_split (mp3_frame, sizeof (mp3_frame));
|
|
}
|
|
|
|
GST_END_TEST;
|
|
|
|
|
|
GST_START_TEST (test_parse_skip_garbage)
|
|
{
|
|
gst_parser_test_skip_garbage (mp3_frame, sizeof (mp3_frame),
|
|
garbage_frame, sizeof (garbage_frame));
|
|
}
|
|
|
|
GST_END_TEST;
|
|
|
|
|
|
#define structure_get_int(s,f) \
|
|
(g_value_get_int(gst_structure_get_value(s,f)))
|
|
#define fail_unless_structure_field_int_equals(s,field,num) \
|
|
fail_unless_equals_int (structure_get_int(s,field), num)
|
|
|
|
GST_START_TEST (test_parse_detect_stream)
|
|
{
|
|
GstStructure *s;
|
|
GstCaps *caps;
|
|
|
|
caps = gst_parser_test_get_output_caps (mp3_frame, sizeof (mp3_frame), NULL);
|
|
|
|
fail_unless (caps != NULL);
|
|
|
|
GST_LOG ("mpegaudio output caps: %" GST_PTR_FORMAT, caps);
|
|
s = gst_caps_get_structure (caps, 0);
|
|
fail_unless (gst_structure_has_name (s, "audio/mpeg"));
|
|
fail_unless_structure_field_int_equals (s, "mpegversion", 1);
|
|
fail_unless_structure_field_int_equals (s, "layer", 3);
|
|
fail_unless_structure_field_int_equals (s, "channels", 1);
|
|
fail_unless_structure_field_int_equals (s, "rate", 48000);
|
|
|
|
gst_caps_unref (caps);
|
|
}
|
|
|
|
GST_END_TEST;
|
|
|
|
|
|
/* Gapless tests are performed using a test signal that contains 30 MPEG
|
|
* frames, has padding samples at the beginning and at the end, a LAME
|
|
* tag to inform about said padding samples, and a sample rate of 32 kHz
|
|
* and 1 channel. The test signal is 1009ms long. setup_gapless_test_info()
|
|
* fills the GaplessTestInfo struct with details about this test signal. */
|
|
|
|
typedef struct
|
|
{
|
|
const gchar *filename;
|
|
guint num_mpeg_frames;
|
|
guint num_samples_per_frame;
|
|
guint num_start_padding_samples;
|
|
guint num_end_padding_samples;
|
|
guint sample_rate;
|
|
|
|
guint first_padded_end_frame;
|
|
guint64 num_samples_with_padding;
|
|
guint64 num_samples_without_padding;
|
|
|
|
GstClockTime first_frame_duration;
|
|
GstClockTime regular_frame_duration;
|
|
GstClockTime total_duration_without_padding;
|
|
|
|
GstElement *appsink;
|
|
GstElement *parser;
|
|
} GaplessTestInfo;
|
|
|
|
static void
|
|
setup_gapless_test_info (GaplessTestInfo * info)
|
|
{
|
|
info->filename = "sine-1009ms-1ch-32000hz-gapless-with-lame-tag.mp3";
|
|
info->num_mpeg_frames = 31;
|
|
info->num_samples_per_frame = 1152; /* standard for MP3s */
|
|
info->sample_rate = 32000;
|
|
|
|
/* Note that these start and end padding figures are not exactly like
|
|
* those that we get from the LAME tag. That's because that tag only
|
|
* contains the _encoder_ delay & padding. In the figures below, the
|
|
* _decoder_ delay is also factored in (529 samples). mpegaudioparse
|
|
* does the same, so we have to apply it here. */
|
|
info->num_start_padding_samples = 1105;
|
|
info->num_end_padding_samples = 1167;
|
|
|
|
/* In MP3s with LAME tags, the first frame is a frame made of Xing/LAME
|
|
* metadata and dummy nullsamples (this is for backwards compatibility).
|
|
* num_start_padding_samples defines how many padding samples are there
|
|
* (this does not include the nullsamples from the first dummy frame).
|
|
* Likewise, num_end_padding_samples defines how many padding samples
|
|
* are there at the end of the MP3 stream.
|
|
* There may be more padding samples than the size of one frame, meaning
|
|
* that there may be frames that are made entirely of padding samples.
|
|
* Such frames are output by mpegaudioparse, but their duration is set
|
|
* to 0, and their PTS corresponds to the last valid PTS in the stream
|
|
* (= the last PTS that is within the actual media data).
|
|
* For this reason, we cannot just assume that the last frame is the
|
|
* one containing padding - there may be more. So, calculate the number
|
|
* of the first frame that contains padding sames from the _end_ of
|
|
* the stream. We'll need that later for buffer PTS and duration checks. */
|
|
info->first_padded_end_frame = (info->num_mpeg_frames - 1 -
|
|
info->num_end_padding_samples / info->num_samples_per_frame);
|
|
info->num_samples_with_padding = (info->num_mpeg_frames - 1) *
|
|
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;
|
|
|
|
/* The first frame (excluding the dummy frame at the beginning) will be
|
|
* clipped due to the padding samples at the start of the stream, so we
|
|
* have to calculate this separately. */
|
|
info->first_frame_duration =
|
|
gst_util_uint64_scale_int (info->num_samples_per_frame -
|
|
info->num_start_padding_samples, GST_SECOND, info->sample_rate);
|
|
/* Regular, unclipped MPEG frame duration. */
|
|
info->regular_frame_duration =
|
|
gst_util_uint64_scale_int (info->num_samples_per_frame, GST_SECOND,
|
|
info->sample_rate);
|
|
/* 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);
|
|
}
|
|
|
|
static void
|
|
check_parsed_mpeg_frame (GaplessTestInfo * info, guint frame_num)
|
|
{
|
|
GstClockTime expected_pts = GST_CLOCK_TIME_NONE;
|
|
GstClockTime expected_duration = GST_CLOCK_TIME_NONE;
|
|
gboolean expect_audioclipmeta = FALSE;
|
|
guint64 expected_audioclipmeta_start = 0;
|
|
guint64 expected_audioclipmeta_end = 0;
|
|
GstSample *sample;
|
|
GstBuffer *buffer;
|
|
GstAudioClippingMeta *audioclip_meta;
|
|
|
|
GST_DEBUG ("checking frame %u", frame_num);
|
|
|
|
/* This is called after the frame with the given number has been output by
|
|
* mpegaudioparse. We can then pull that frame from appsink, and check its
|
|
* PTS, duration, and audioclipmeta (if we expect it to be there). */
|
|
|
|
if (frame_num == 0) {
|
|
expected_pts = 0;
|
|
expected_duration = 0;
|
|
expect_audioclipmeta = FALSE;
|
|
} else if (frame_num == 1) {
|
|
/* First frame (excluding the dummy metadata frame at the beginning of
|
|
* the MPEG stream that mpegaudioparse internally drops). This one will be
|
|
* clipped due to the padding samples at the beginning, so we expect a
|
|
* clipping meta to be there. Also, its duration will be smaller than that
|
|
* of regular, unclipped frames. */
|
|
|
|
expected_pts = 0;
|
|
expected_duration = info->first_frame_duration;
|
|
|
|
expect_audioclipmeta = TRUE;
|
|
expected_audioclipmeta_start = info->num_start_padding_samples;
|
|
expected_audioclipmeta_end = 0;
|
|
} else if (frame_num > 1 && frame_num < info->first_padded_end_frame) {
|
|
/* Regular, unclipped frame. */
|
|
|
|
expected_pts = info->first_frame_duration + (frame_num - 2) *
|
|
info->regular_frame_duration;
|
|
expected_duration = info->regular_frame_duration;
|
|
} else if (frame_num == info->first_padded_end_frame) {
|
|
/* The first frame at the end with padding samples. This one will have
|
|
* the last few valid samples, followed by the first padding samples. */
|
|
|
|
guint64 num_valid_samples = (info->num_samples_with_padding -
|
|
info->num_end_padding_samples) - (frame_num - 1) *
|
|
info->num_samples_per_frame;
|
|
guint64 num_padding_samples = info->num_samples_per_frame -
|
|
num_valid_samples;
|
|
|
|
expected_pts = info->first_frame_duration + (frame_num - 2) *
|
|
info->regular_frame_duration;
|
|
expected_duration = gst_util_uint64_scale_int (num_valid_samples,
|
|
GST_SECOND, info->sample_rate);
|
|
|
|
expect_audioclipmeta = TRUE;
|
|
expected_audioclipmeta_start = 0;
|
|
expected_audioclipmeta_end = num_padding_samples;
|
|
} else {
|
|
/* A fully clipped frame at the end of the stream. */
|
|
|
|
expected_pts = info->total_duration_without_padding;
|
|
expected_duration = 0;
|
|
|
|
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));
|
|
|
|
buffer = gst_sample_get_buffer (sample);
|
|
fail_if (buffer == NULL);
|
|
|
|
/* Verify the sample's PTS and duration. */
|
|
fail_unless_equals_uint64 (GST_BUFFER_PTS (buffer), expected_pts);
|
|
fail_unless_equals_uint64 (GST_BUFFER_DURATION (buffer), expected_duration);
|
|
/* 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);
|
|
}
|
|
|
|
GST_START_TEST (test_parse_gapless_and_skip_padding_samples)
|
|
{
|
|
GstElement *source, *parser, *appsink, *pipeline;
|
|
GstStateChangeReturn state_ret;
|
|
guint frame_num;
|
|
GaplessTestInfo info;
|
|
|
|
setup_gapless_test_info (&info);
|
|
|
|
pipeline = gst_pipeline_new (NULL);
|
|
source = gst_element_factory_make ("filesrc", NULL);
|
|
parser = gst_element_factory_make ("mpegaudioparse", NULL);
|
|
appsink = gst_element_factory_make ("appsink", NULL);
|
|
|
|
info.appsink = appsink;
|
|
info.parser = parser;
|
|
|
|
gst_bin_add_many (GST_BIN (pipeline), source, parser, appsink, NULL);
|
|
gst_element_link_many (source, parser, appsink, NULL);
|
|
|
|
{
|
|
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_mpeg_frames; ++frame_num)
|
|
check_parsed_mpeg_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. */
|
|
{
|
|
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 |
|
|
GST_SEEK_FLAG_KEY_UNIT, 0);
|
|
fail_unless_equals_int (gst_element_set_state (pipeline, GST_STATE_PLAYING),
|
|
GST_STATE_CHANGE_SUCCESS);
|
|
|
|
check_parsed_mpeg_frame (&info, 1);
|
|
}
|
|
|
|
/* Seek to the second frame. */
|
|
{
|
|
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 |
|
|
GST_SEEK_FLAG_KEY_UNIT, info.first_frame_duration);
|
|
fail_unless_equals_int (gst_element_set_state (pipeline, GST_STATE_PLAYING),
|
|
GST_STATE_CHANGE_SUCCESS);
|
|
|
|
check_parsed_mpeg_frame (&info, 2);
|
|
}
|
|
|
|
/* Seek to the last frame with valid samples (= the first frame with padding
|
|
* samples at the end of the stream). */
|
|
{
|
|
GstClockTime pts = info.first_frame_duration +
|
|
(info.first_padded_end_frame - 2) * info.regular_frame_duration;
|
|
|
|
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 |
|
|
GST_SEEK_FLAG_KEY_UNIT, pts);
|
|
fail_unless_equals_int (gst_element_set_state (pipeline, GST_STATE_PLAYING),
|
|
GST_STATE_CHANGE_SUCCESS);
|
|
|
|
check_parsed_mpeg_frame (&info, info.first_padded_end_frame);
|
|
}
|
|
|
|
gst_element_set_state (pipeline, GST_STATE_NULL);
|
|
gst_object_unref (pipeline);
|
|
}
|
|
|
|
GST_END_TEST;
|
|
|
|
|
|
static Suite *
|
|
mpegaudioparse_suite (void)
|
|
{
|
|
Suite *s = suite_create ("mpegaudioparse");
|
|
TCase *tc_chain = tcase_create ("general");
|
|
|
|
|
|
/* init test context */
|
|
ctx_factory = "mpegaudioparse";
|
|
ctx_sink_template = &sinktemplate;
|
|
ctx_src_template = &srctemplate;
|
|
|
|
suite_add_tcase (s, tc_chain);
|
|
tcase_add_test (tc_chain, test_parse_normal);
|
|
tcase_add_test (tc_chain, test_parse_drain_single);
|
|
tcase_add_test (tc_chain, test_parse_drain_garbage);
|
|
tcase_add_test (tc_chain, test_parse_split);
|
|
tcase_add_test (tc_chain, test_parse_skip_garbage);
|
|
tcase_add_test (tc_chain, test_parse_detect_stream);
|
|
tcase_add_test (tc_chain, test_parse_gapless_and_skip_padding_samples);
|
|
|
|
return s;
|
|
}
|
|
|
|
|
|
/*
|
|
* TODO:
|
|
* - Both push- and pull-modes need to be tested
|
|
* * Pull-mode & EOS
|
|
*/
|
|
GST_CHECK_MAIN (mpegaudioparse);
|