/* GStreamer ReplayGain volume adjustment
 *
 * Copyright (C) 2007 Rene Stadler <mail@renestadler.de>
 *
 * rgvolume.c: Unit test for the rgvolume element
 *
 * This library 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.
 * 
 * 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
 * Lesser General Public License for more details.
 * 
 * You should have received a copy of the GNU Lesser General Public
 * License along with this library; if not, write to the Free Software
 * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA
 * 02110-1301 USA
 */

#include <gst/check/gstcheck.h>
#include <gst/audio/audio.h>

#include <math.h>

static GList *events = NULL;

/* For ease of programming we use globals to keep refs for our floating src and
 * sink pads we create; otherwise we always have to do get_pad, get_peer, and
 * then remove references in every test function */
static GstPad *mysrcpad, *mysinkpad;

#define RG_VOLUME_CAPS_TEMPLATE_STRING        \
  "audio/x-raw, "                             \
  "format = (string) "GST_AUDIO_NE (F32) ", " \
  "layout = (string) interleaved, "           \
  "channels = (int) [ 1, MAX ], "             \
  "rate = (int) [ 1, MAX ]"

static GstStaticPadTemplate sinktemplate = GST_STATIC_PAD_TEMPLATE ("sink",
    GST_PAD_SINK,
    GST_PAD_ALWAYS,
    GST_STATIC_CAPS (RG_VOLUME_CAPS_TEMPLATE_STRING)
    );
static GstStaticPadTemplate srctemplate = GST_STATIC_PAD_TEMPLATE ("src",
    GST_PAD_SRC,
    GST_PAD_ALWAYS,
    GST_STATIC_CAPS (RG_VOLUME_CAPS_TEMPLATE_STRING)
    );

static GstBuffer *test_buffer_new (gfloat value);

/* gstcheck sets up a chain function that appends buffers to a global list.
 * This is our equivalent of that for event handling. */
static gboolean
event_func (GstPad * pad, GstObject * parent, GstEvent * event)
{
  GST_DEBUG ("received event %p (%s)", event, GST_EVENT_TYPE_NAME (event));
  events = g_list_append (events, event);

  return TRUE;
}

static GstElement *
setup_rgvolume (void)
{
  GstElement *element;

  GST_DEBUG ("setup_rgvolume");
  element = gst_check_setup_element ("rgvolume");
  mysrcpad = gst_check_setup_src_pad (element, &srctemplate);
  mysinkpad = gst_check_setup_sink_pad (element, &sinktemplate);

  /* Capture events, to test tag filtering behavior: */
  gst_pad_set_event_function (mysinkpad, event_func);

  gst_pad_set_active (mysrcpad, TRUE);
  gst_pad_set_active (mysinkpad, TRUE);

  return element;
}

static void
send_empty_buffer (void)
{
  GstBuffer *buf;

  buf = test_buffer_new (0.0);
  gst_buffer_resize (buf, 0, 0);
  GST_BUFFER_DURATION (buf) = 0;
  GST_BUFFER_OFFSET_END (buf) = GST_BUFFER_OFFSET (buf);
  fail_unless (gst_pad_push (mysrcpad, buf) == GST_FLOW_OK);

  fail_unless (g_list_length (buffers) == 1);
  fail_unless (buffers->data == buf);
  gst_mini_object_unref ((GstMiniObject *) buffers->data);
  buffers = g_list_remove (buffers, buf);
}

static void
cleanup_rgvolume (GstElement * element)
{
  GST_DEBUG ("cleanup_rgvolume");

  g_list_foreach (buffers, (GFunc) gst_mini_object_unref, NULL);
  g_list_free (buffers);
  buffers = NULL;

  g_list_foreach (events, (GFunc) gst_mini_object_unref, NULL);
  g_list_free (events);
  events = NULL;

  gst_pad_set_active (mysrcpad, FALSE);
  gst_pad_set_active (mysinkpad, FALSE);
  gst_check_teardown_src_pad (element);
  gst_check_teardown_sink_pad (element);
  gst_check_teardown_element (element);
}

static void
set_playing_state (GstElement * element)
{
  fail_unless (gst_element_set_state (element,
          GST_STATE_PLAYING) == GST_STATE_CHANGE_SUCCESS,
      "Could not set state to PLAYING");
}

static void
set_null_state (GstElement * element)
{
  fail_unless (gst_element_set_state (element,
          GST_STATE_NULL) == GST_STATE_CHANGE_SUCCESS,
      "Could not set state to NULL");
}

static void
send_flush_events (GstElement * element)
{
  gboolean res;

  res = gst_pad_push_event (mysrcpad, gst_event_new_flush_start ());
  fail_unless (res, "flush-start even not handled");

  res = gst_pad_push_event (mysrcpad, gst_event_new_flush_stop (TRUE));
  fail_unless (res, "flush-stop event not handled");
}

static void
send_stream_start_event (GstElement * element)
{
  gboolean res;

  res = gst_pad_push_event (mysrcpad, gst_event_new_stream_start ("test"));
  fail_unless (res, "STREAM_START event not handled");
}

static void
send_caps_event (GstElement * element)
{
  GstCaps *caps;
  gboolean res;

  caps = gst_caps_from_string ("audio/x-raw, format = " GST_AUDIO_NE (F32) ", "
      "layout = interleaved, rate = 8000, channels = 1");
  res = gst_pad_push_event (mysrcpad, gst_event_new_caps (caps));
  fail_unless (res, "CAPS event not handled");
  gst_caps_unref (caps);
}

static void
send_segment_event (GstElement * element)
{
  GstSegment segment;
  gboolean res;

  gst_segment_init (&segment, GST_FORMAT_TIME);
  res = gst_pad_push_event (mysrcpad, gst_event_new_segment (&segment));
  fail_unless (res, "SEGMENT event not handled");
}

static void
send_eos_event (GstElement * element)
{
  GstEvent *event = gst_event_new_eos ();

  fail_unless (gst_pad_push_event (mysrcpad, event),
      "Pushing EOS event failed");
}

static GstEvent *
send_tag_event (GstElement * element, GstEvent * event)
{
  GList *l;
  GstTagList *tag_list;
  gdouble dummy;

  g_return_val_if_fail (event->type == GST_EVENT_TAG, NULL);

  fail_unless (gst_pad_push_event (mysrcpad, event),
      "Pushing tag event failed");

  event = NULL;

  for (l = g_list_last (events); l; l = l->prev) {
    if (GST_EVENT_TYPE (l->data) == GST_EVENT_TAG) {
      event = l->data;
      events = g_list_delete_link (events, l);
      break;
    }
  }

  /* Event got filtered out */
  if (event == NULL)
    return NULL;

  fail_unless (event->type == GST_EVENT_TAG);
  gst_event_parse_tag (event, &tag_list);

  /* The element is supposed to filter out ReplayGain related tags. */
  fail_if (gst_tag_list_get_double (tag_list, GST_TAG_TRACK_GAIN, &dummy),
      "tag event still contains track gain tag");
  fail_if (gst_tag_list_get_double (tag_list, GST_TAG_TRACK_PEAK, &dummy),
      "tag event still contains track peak tag");
  fail_if (gst_tag_list_get_double (tag_list, GST_TAG_ALBUM_GAIN, &dummy),
      "tag event still contains album gain tag");
  fail_if (gst_tag_list_get_double (tag_list, GST_TAG_ALBUM_PEAK, &dummy),
      "tag event still contains album peak tag");

  return event;
}

static GstBuffer *
test_buffer_new (gfloat value)
{
  GstBuffer *buf;
  GstMapInfo map;
  gfloat *data;
  gint i;

  buf = gst_buffer_new_and_alloc (8 * sizeof (gfloat));
  gst_buffer_map (buf, &map, GST_MAP_WRITE);
  data = (gfloat *) map.data;
  for (i = 0; i < 8; i++)
    data[i] = value;
  gst_buffer_unmap (buf, &map);


  ASSERT_BUFFER_REFCOUNT (buf, "buf", 1);

  return buf;
}

#define MATCH_GAIN(g1, g2) ((g1 < g2 + 1e-6) && (g2 < g1 + 1e-6))

static void
fail_unless_target_gain (GstElement * element, gdouble expected_gain)
{
  gdouble prop_gain;

  g_object_get (element, "target-gain", &prop_gain, NULL);

  fail_unless (MATCH_GAIN (prop_gain, expected_gain),
      "Target gain is %.2f dB, expected %.2f dB", prop_gain, expected_gain);
}

static void
fail_unless_result_gain (GstElement * element, gdouble expected_gain)
{
  GstBuffer *input_buf, *output_buf;
  gfloat *data;
  gfloat input_sample, output_sample;
  gdouble gain, prop_gain;
  gboolean is_passthrough, expect_passthrough;
  gint i;
  GstMapInfo map;

  fail_unless (g_list_length (buffers) == 0);

  input_sample = 1.0;
  input_buf = test_buffer_new (input_sample);

  /* We keep an extra reference to detect passthrough mode. */
  gst_buffer_ref (input_buf);
  /* Pushing steals a reference. */
  fail_unless (gst_pad_push (mysrcpad, input_buf) == GST_FLOW_OK);
  gst_buffer_unref (input_buf);

  /* The output buffer ends up on the global buffer list. */
  fail_unless (g_list_length (buffers) == 1);
  output_buf = buffers->data;
  fail_if (output_buf == NULL);

  buffers = g_list_remove (buffers, output_buf);
  ASSERT_BUFFER_REFCOUNT (output_buf, "output_buf", 1);

  fail_unless_equals_int (gst_buffer_get_size (output_buf),
      8 * sizeof (gfloat));

  gst_buffer_map (output_buf, &map, GST_MAP_READ);
  data = (gfloat *) map.data;

  output_sample = *data;
  fail_if (output_sample == 0.0, "First output sample is zero");
  for (i = 1; i < 8; i++) {
    fail_unless (output_sample == data[i], "Output samples not uniform");
  };
  gst_buffer_unmap (output_buf, &map);

  gain = 20. * log10 (output_sample / input_sample);
  fail_unless (MATCH_GAIN (gain, expected_gain),
      "Applied gain is %.2f dB, expected %.2f dB", gain, expected_gain);
  g_object_get (element, "result-gain", &prop_gain, NULL);
  fail_unless (MATCH_GAIN (prop_gain, expected_gain),
      "Result gain is %.2f dB, expected %.2f dB", prop_gain, expected_gain);

  is_passthrough = (output_buf == input_buf);
  expect_passthrough = MATCH_GAIN (expected_gain, +0.00);
  fail_unless (is_passthrough == expect_passthrough,
      expect_passthrough
      ? "Expected operation in passthrough mode"
      : "Incorrect passthrough behaviour");

  gst_buffer_unref (output_buf);
}

static void
fail_unless_gain (GstElement * element, gdouble expected_gain)
{
  fail_unless_target_gain (element, expected_gain);
  fail_unless_result_gain (element, expected_gain);
}

/* Start of tests. */

GST_START_TEST (test_no_buffer)
{
  GstElement *element = setup_rgvolume ();

  set_playing_state (element);
  set_null_state (element);
  set_playing_state (element);
  send_eos_event (element);

  cleanup_rgvolume (element);
}

GST_END_TEST;

GST_START_TEST (test_events)
{
  GstElement *element = setup_rgvolume ();
  GstEvent *event;
  GstEvent *new_event;
  GstTagList *tag_list;
  gchar *artist;

  set_playing_state (element);
  send_stream_start_event (element);
  send_caps_event (element);
  send_segment_event (element);

  send_empty_buffer ();

  tag_list = gst_tag_list_new_empty ();
  gst_tag_list_add (tag_list, GST_TAG_MERGE_REPLACE,
      GST_TAG_TRACK_GAIN, +4.95, GST_TAG_TRACK_PEAK, 0.59463,
      GST_TAG_ALBUM_GAIN, -1.54, GST_TAG_ALBUM_PEAK, 0.693415,
      GST_TAG_ARTIST, "Foobar", NULL);
  event = gst_event_new_tag (tag_list);
  new_event = send_tag_event (element, event);
  gst_event_parse_tag (new_event, &tag_list);
  fail_unless (gst_tag_list_get_string (tag_list, GST_TAG_ARTIST, &artist));
  fail_unless (g_str_equal (artist, "Foobar"));
  g_free (artist);
  gst_event_unref (new_event);

  /* Same as above, but with a non-writable event. */

  tag_list = gst_tag_list_new_empty ();
  gst_tag_list_add (tag_list, GST_TAG_MERGE_REPLACE,
      GST_TAG_TRACK_GAIN, +4.95, GST_TAG_TRACK_PEAK, 0.59463,
      GST_TAG_ALBUM_GAIN, -1.54, GST_TAG_ALBUM_PEAK, 0.693415,
      GST_TAG_ARTIST, "Foobar", NULL);
  gst_tag_list_ref (tag_list);
  event = gst_event_new_tag (tag_list);
  new_event = send_tag_event (element, event);

  /* Make sure our tags weren't modified in place while we still got a ref */
  fail_unless_equals_int (5, gst_tag_list_n_tags (tag_list));
  gst_tag_list_unref (tag_list);

  gst_event_parse_tag (new_event, &tag_list);
  fail_unless (gst_tag_list_get_string (tag_list, GST_TAG_ARTIST, &artist));
  fail_unless (g_str_equal (artist, "Foobar"));
  g_free (artist);
  gst_event_unref (new_event);

  cleanup_rgvolume (element);
}

GST_END_TEST;

GST_START_TEST (test_simple)
{
  GstElement *element = setup_rgvolume ();
  GstTagList *tag_list;

  g_object_set (element, "album-mode", FALSE, "headroom", +0.00,
      "pre-amp", -6.00, "fallback-gain", +1.23, NULL);
  set_playing_state (element);
  send_stream_start_event (element);
  send_caps_event (element);
  send_segment_event (element);

  send_empty_buffer ();

  tag_list = gst_tag_list_new_empty ();
  gst_tag_list_add (tag_list, GST_TAG_MERGE_REPLACE,
      GST_TAG_TRACK_GAIN, -3.45, GST_TAG_TRACK_PEAK, 1.0,
      GST_TAG_ALBUM_GAIN, +2.09, GST_TAG_ALBUM_PEAK, 1.0, NULL);
  fail_unless (send_tag_event (element, gst_event_new_tag (tag_list)) == NULL);
  fail_unless_gain (element, -9.45);    /* pre-amp + track gain */
  send_eos_event (element);

  g_object_set (element, "album-mode", TRUE, NULL);

  send_flush_events (element);
  send_segment_event (element);

  tag_list = gst_tag_list_new_empty ();
  gst_tag_list_add (tag_list, GST_TAG_MERGE_REPLACE,
      GST_TAG_TRACK_GAIN, -3.45, GST_TAG_TRACK_PEAK, 1.0,
      GST_TAG_ALBUM_GAIN, +2.09, GST_TAG_ALBUM_PEAK, 1.0, NULL);
  fail_unless (send_tag_event (element, gst_event_new_tag (tag_list)) == NULL);
  fail_unless_gain (element, -3.91);    /* pre-amp + album gain */

  /* Switching back to track mode in the middle of a stream: */
  g_object_set (element, "album-mode", FALSE, NULL);
  fail_unless_gain (element, -9.45);    /* pre-amp + track gain */
  send_eos_event (element);

  cleanup_rgvolume (element);
}

GST_END_TEST;

/* If there are no gain tags at all, the fallback gain is used. */

GST_START_TEST (test_fallback_gain)
{
  GstElement *element = setup_rgvolume ();
  GstTagList *tag_list;

  /* First some track where fallback does _not_ apply. */

  g_object_set (element, "album-mode", FALSE, "headroom", 10.00,
      "pre-amp", -6.00, "fallback-gain", -3.00, NULL);
  set_playing_state (element);
  send_stream_start_event (element);
  send_caps_event (element);
  send_segment_event (element);

  send_empty_buffer ();

  tag_list = gst_tag_list_new_empty ();
  gst_tag_list_add (tag_list, GST_TAG_MERGE_REPLACE,
      GST_TAG_TRACK_GAIN, +3.5, GST_TAG_TRACK_PEAK, 1.0,
      GST_TAG_ALBUM_GAIN, -0.5, GST_TAG_ALBUM_PEAK, 1.0, NULL);
  fail_unless (send_tag_event (element, gst_event_new_tag (tag_list)) == NULL);
  fail_unless_gain (element, -2.50);    /* pre-amp + track gain */
  send_eos_event (element);

  /* Now a track completely missing tags. */
  send_flush_events (element);
  send_segment_event (element);

  fail_unless_gain (element, -9.00);    /* pre-amp + fallback-gain */

  /* Changing the fallback gain in the middle of a stream, going to pass-through
   * mode: */
  g_object_set (element, "fallback-gain", +6.00, NULL);
  fail_unless_gain (element, +0.00);    /* pre-amp + fallback-gain */
  send_eos_event (element);

  /* Verify that result gain is set to +0.00 with pre-amp + fallback-gain >
   * +0.00 and no headroom. */
  send_flush_events (element);
  send_segment_event (element);

  g_object_set (element, "fallback-gain", +12.00, "headroom", +0.00, NULL);
  fail_unless_target_gain (element, +6.00);     /* pre-amp + fallback-gain */
  fail_unless_result_gain (element, +0.00);
  send_eos_event (element);

  cleanup_rgvolume (element);
}

GST_END_TEST;

/* If album gain is to be preferred but not available, the track gain is to be
 * taken instead. */

GST_START_TEST (test_fallback_track)
{
  GstElement *element = setup_rgvolume ();
  GstTagList *tag_list;

  g_object_set (element, "album-mode", TRUE, "headroom", +0.00,
      "pre-amp", -6.00, "fallback-gain", +1.23, NULL);
  set_playing_state (element);
  send_stream_start_event (element);
  send_caps_event (element);
  send_segment_event (element);

  send_empty_buffer ();

  tag_list = gst_tag_list_new_empty ();
  gst_tag_list_add (tag_list, GST_TAG_MERGE_REPLACE,
      GST_TAG_TRACK_GAIN, +2.11, GST_TAG_TRACK_PEAK, 1.0, NULL);
  fail_unless (send_tag_event (element, gst_event_new_tag (tag_list)) == NULL);
  fail_unless_gain (element, -3.89);    /* pre-amp + track gain */

  send_eos_event (element);

  cleanup_rgvolume (element);
}

GST_END_TEST;

/* If track gain is to be preferred but not available, the album gain is to be
 * taken instead. */

GST_START_TEST (test_fallback_album)
{
  GstElement *element = setup_rgvolume ();
  GstTagList *tag_list;

  g_object_set (element, "album-mode", FALSE, "headroom", +0.00,
      "pre-amp", -6.00, "fallback-gain", +1.23, NULL);
  set_playing_state (element);
  send_stream_start_event (element);
  send_caps_event (element);
  send_segment_event (element);

  send_empty_buffer ();

  tag_list = gst_tag_list_new_empty ();
  gst_tag_list_add (tag_list, GST_TAG_MERGE_REPLACE,
      GST_TAG_ALBUM_GAIN, +3.73, GST_TAG_ALBUM_PEAK, 1.0, NULL);
  fail_unless (send_tag_event (element, gst_event_new_tag (tag_list)) == NULL);
  fail_unless_gain (element, -2.27);    /* pre-amp + album gain */

  send_eos_event (element);

  cleanup_rgvolume (element);
}

GST_END_TEST;

GST_START_TEST (test_headroom)
{
  GstElement *element = setup_rgvolume ();
  GstTagList *tag_list;

  g_object_set (element, "album-mode", FALSE, "headroom", +0.00,
      "pre-amp", +0.00, "fallback-gain", +1.23, NULL);
  set_playing_state (element);
  send_stream_start_event (element);
  send_caps_event (element);
  send_segment_event (element);

  send_empty_buffer ();

  tag_list = gst_tag_list_new_empty ();
  gst_tag_list_add (tag_list, GST_TAG_MERGE_REPLACE,
      GST_TAG_TRACK_GAIN, +3.50, GST_TAG_TRACK_PEAK, 1.0, NULL);
  fail_unless (send_tag_event (element, gst_event_new_tag (tag_list)) == NULL);
  fail_unless_target_gain (element, +3.50);     /* pre-amp + track gain */
  fail_unless_result_gain (element, +0.00);
  send_eos_event (element);

  send_flush_events (element);
  send_segment_event (element);

  g_object_set (element, "headroom", +2.00, NULL);
  tag_list = gst_tag_list_new_empty ();
  gst_tag_list_add (tag_list, GST_TAG_MERGE_REPLACE,
      GST_TAG_TRACK_GAIN, +9.18, GST_TAG_TRACK_PEAK, 0.687149, NULL);
  fail_unless (send_tag_event (element, gst_event_new_tag (tag_list)) == NULL);
  fail_unless_target_gain (element, +9.18);     /* pre-amp + track gain */
  /* Result is 20. * log10 (1. / peak) + headroom. */
  fail_unless_result_gain (element, 5.2589816238303335);
  send_eos_event (element);

  send_flush_events (element);
  send_segment_event (element);

  g_object_set (element, "album-mode", TRUE, NULL);
  tag_list = gst_tag_list_new_empty ();
  gst_tag_list_add (tag_list, GST_TAG_MERGE_REPLACE,
      GST_TAG_ALBUM_GAIN, +5.50, GST_TAG_ALBUM_PEAK, 1.0, NULL);
  fail_unless (send_tag_event (element, gst_event_new_tag (tag_list)) == NULL);
  fail_unless_target_gain (element, +5.50);     /* pre-amp + album gain */
  fail_unless_result_gain (element, +2.00);     /* headroom */
  send_eos_event (element);

  cleanup_rgvolume (element);
}

GST_END_TEST;

GST_START_TEST (test_reference_level)
{
  GstElement *element = setup_rgvolume ();
  GstTagList *tag_list;

  g_object_set (element,
      "album-mode", FALSE,
      "headroom", +0.00, "pre-amp", +0.00, "fallback-gain", +1.23, NULL);
  set_playing_state (element);

  send_stream_start_event (element);
  send_caps_event (element);
  send_segment_event (element);

  send_empty_buffer ();

  tag_list = gst_tag_list_new_empty ();
  gst_tag_list_add (tag_list, GST_TAG_MERGE_REPLACE,
      GST_TAG_TRACK_GAIN, 0.00, GST_TAG_TRACK_PEAK, 0.2,
      GST_TAG_REFERENCE_LEVEL, 83., NULL);
  fail_unless (send_tag_event (element, gst_event_new_tag (tag_list)) == NULL);
  /* Because our authorative reference is 89 dB, we bump it up by +6 dB. */
  fail_unless_gain (element, +6.00);    /* pre-amp + track gain */
  send_eos_event (element);

  g_object_set (element, "album-mode", TRUE, NULL);

  /* Same as above, but with album gain. */
  send_flush_events (element);
  send_segment_event (element);

  tag_list = gst_tag_list_new_empty ();
  gst_tag_list_add (tag_list, GST_TAG_MERGE_REPLACE,
      GST_TAG_TRACK_GAIN, 1.23, GST_TAG_TRACK_PEAK, 0.1,
      GST_TAG_ALBUM_GAIN, 0.00, GST_TAG_ALBUM_PEAK, 0.2,
      GST_TAG_REFERENCE_LEVEL, 83., NULL);
  fail_unless (send_tag_event (element, gst_event_new_tag (tag_list)) == NULL);
  fail_unless_gain (element, +6.00);    /* pre-amp + album gain */

  cleanup_rgvolume (element);
}

GST_END_TEST;

static Suite *
rgvolume_suite (void)
{
  Suite *s = suite_create ("rgvolume");
  TCase *tc_chain = tcase_create ("general");

  suite_add_tcase (s, tc_chain);

  tcase_add_test (tc_chain, test_no_buffer);
  tcase_add_test (tc_chain, test_events);
  tcase_add_test (tc_chain, test_simple);
  tcase_add_test (tc_chain, test_fallback_gain);
  tcase_add_test (tc_chain, test_fallback_track);
  tcase_add_test (tc_chain, test_fallback_album);
  tcase_add_test (tc_chain, test_headroom);
  tcase_add_test (tc_chain, test_reference_level);

  return s;
}

GST_CHECK_MAIN (rgvolume);
