/* GStreamer
 * Copyright (C) <2017> Carlos Rafael Giani <dv at pseudoterminal dot org>
 *
 * 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.
 */


/**
 * SECTION:element-openmptdec
 * @see_also: #GstOpenMptDec
 *
 * openmpdec decodes module music formats, such as S3M, MOD, XM, IT.
 * It uses the [OpenMPT library](https://lib.openmpt.org) for this purpose.
 * It can be autoplugged and therefore works with decodebin.
 *
 * ## Example launch line
 *
 * |[
 * gst-launch-1.0 filesrc location=media/example.it ! openmptdec ! audioconvert ! audioresample ! autoaudiosink
 * ]|
 */


#ifdef HAVE_CONFIG_H
#include <config.h>
#endif

#include <gst/gst.h>

#include "gstopenmptdec.h"

#ifndef OPENMPT_API_VERSION_AT_LEAST
#define OPENMPT_API_VERSION_AT_LEAST(x, y, z) (FALSE)
#endif

GST_DEBUG_CATEGORY_STATIC (openmptdec_debug);
#define GST_CAT_DEFAULT openmptdec_debug


enum
{
  PROP_0,
  PROP_MASTER_GAIN,
  PROP_STEREO_SEPARATION,
  PROP_FILTER_LENGTH,
  PROP_VOLUME_RAMPING,
  PROP_OUTPUT_BUFFER_SIZE
};


#define DEFAULT_MASTER_GAIN 0
#define DEFAULT_STEREO_SEPARATION 100
#define DEFAULT_FILTER_LENGTH 0
#define DEFAULT_VOLUME_RAMPING -1
#define DEFAULT_OUTPUT_BUFFER_SIZE 1024

#define DEFAULT_SAMPLE_FORMAT GST_AUDIO_FORMAT_F32
#define DEFAULT_SAMPLE_RATE 48000
#define DEFAULT_NUM_CHANNELS 2



static GstStaticPadTemplate sink_template = GST_STATIC_PAD_TEMPLATE ("sink",
    GST_PAD_SINK,
    GST_PAD_ALWAYS,
    GST_STATIC_CAPS ("audio/x-mod, "
        "type = (string) { 669, asylum-amf, dsmi-amf, extreme-ams, velvet-ams, "
        "dbm, digi, dmf, dsm, far, gdm, imf, it, j2b, mdl, med, mod, mt2, mtm, "
        "okt, psm, ptm, s3m, stm, ult, xm }")
    );

static GstStaticPadTemplate src_template = GST_STATIC_PAD_TEMPLATE ("src",
    GST_PAD_SRC,
    GST_PAD_ALWAYS,
    GST_STATIC_CAPS ("audio/x-raw, "
        "format = (string) { " GST_AUDIO_NE (S16) ", " GST_AUDIO_NE (F32) " }, "
        "layout = (string) interleaved, "
        "rate = (int) [ 1, 192000 ], " "channels = (int) { 1, 2, 4 } ")
    );



G_DEFINE_TYPE (GstOpenMptDec, gst_openmpt_dec,
    GST_TYPE_NONSTREAM_AUDIO_DECODER);



static void gst_openmpt_dec_finalize (GObject * object);

static void gst_openmpt_dec_set_property (GObject * object, guint prop_id,
    const GValue * value, GParamSpec * pspec);
static void gst_openmpt_dec_get_property (GObject * object, guint prop_id,
    GValue * value, GParamSpec * pspec);

static gboolean gst_openmpt_dec_seek (GstNonstreamAudioDecoder * dec,
    GstClockTime * new_position);
static GstClockTime gst_openmpt_dec_tell (GstNonstreamAudioDecoder * dec);

static void gst_openmpt_dec_log_func (char const *message, void *user);
static void gst_openmpt_dec_add_metadata_to_tag_list (GstOpenMptDec *
    openmpt_dec, GstTagList * tags, char const *key, gchar const *tag);
static gboolean gst_openmpt_dec_load_from_buffer (GstNonstreamAudioDecoder *
    dec, GstBuffer * source_data, guint initial_subsong,
    GstNonstreamAudioSubsongMode initial_subsong_mode,
    GstClockTime * initial_position,
    GstNonstreamAudioOutputMode * initial_output_mode,
    gint * initial_num_loops);

static GstTagList *gst_openmpt_dec_get_main_tags (GstNonstreamAudioDecoder *
    dec);

static gboolean gst_openmpt_dec_set_current_subsong (GstNonstreamAudioDecoder *
    dec, guint subsong, GstClockTime * initial_position);
static guint gst_openmpt_dec_get_current_subsong (GstNonstreamAudioDecoder *
    dec);

static guint gst_openmpt_dec_get_num_subsongs (GstNonstreamAudioDecoder * dec);
static GstClockTime
gst_openmpt_dec_get_subsong_duration (GstNonstreamAudioDecoder * dec,
    guint subsong);
static GstTagList *gst_openmpt_dec_get_subsong_tags (GstNonstreamAudioDecoder *
    dec, guint subsong);
static gboolean gst_openmpt_dec_set_subsong_mode (GstNonstreamAudioDecoder *
    dec, GstNonstreamAudioSubsongMode mode, GstClockTime * initial_position);

static gboolean gst_openmpt_dec_set_num_loops (GstNonstreamAudioDecoder * dec,
    gint num_loops);
static gint gst_openmpt_dec_get_num_loops (GstNonstreamAudioDecoder * dec);

static guint
gst_openmpt_dec_get_supported_output_modes (GstNonstreamAudioDecoder * dec);
static gboolean gst_openmpt_dec_decode (GstNonstreamAudioDecoder * dec,
    GstBuffer ** buffer, guint * num_samples);

static gboolean gst_openmpt_dec_select_subsong (GstOpenMptDec *
    openmpt_dec, GstNonstreamAudioSubsongMode subsong_mode,
    gint openmpt_subsong);


void
gst_openmpt_dec_class_init (GstOpenMptDecClass * klass)
{
  GObjectClass *object_class;
  GstElementClass *element_class;
  GstNonstreamAudioDecoderClass *dec_class;

  GST_DEBUG_CATEGORY_INIT (openmptdec_debug, "openmptdec", 0,
      "OpenMPT-based module music decoder");

  object_class = G_OBJECT_CLASS (klass);
  element_class = GST_ELEMENT_CLASS (klass);
  dec_class = GST_NONSTREAM_AUDIO_DECODER_CLASS (klass);

  gst_element_class_add_pad_template (element_class,
      gst_static_pad_template_get (&sink_template));
  gst_element_class_add_pad_template (element_class,
      gst_static_pad_template_get (&src_template));

  object_class->finalize = GST_DEBUG_FUNCPTR (gst_openmpt_dec_finalize);
  object_class->set_property = GST_DEBUG_FUNCPTR (gst_openmpt_dec_set_property);
  object_class->get_property = GST_DEBUG_FUNCPTR (gst_openmpt_dec_get_property);

  dec_class->seek = GST_DEBUG_FUNCPTR (gst_openmpt_dec_seek);
  dec_class->tell = GST_DEBUG_FUNCPTR (gst_openmpt_dec_tell);
  dec_class->load_from_buffer =
      GST_DEBUG_FUNCPTR (gst_openmpt_dec_load_from_buffer);
  dec_class->get_main_tags = GST_DEBUG_FUNCPTR (gst_openmpt_dec_get_main_tags);
  dec_class->set_num_loops = GST_DEBUG_FUNCPTR (gst_openmpt_dec_set_num_loops);
  dec_class->get_num_loops = GST_DEBUG_FUNCPTR (gst_openmpt_dec_get_num_loops);
  dec_class->get_supported_output_modes =
      GST_DEBUG_FUNCPTR (gst_openmpt_dec_get_supported_output_modes);
  dec_class->decode = GST_DEBUG_FUNCPTR (gst_openmpt_dec_decode);
  dec_class->set_current_subsong =
      GST_DEBUG_FUNCPTR (gst_openmpt_dec_set_current_subsong);
  dec_class->get_current_subsong =
      GST_DEBUG_FUNCPTR (gst_openmpt_dec_get_current_subsong);
  dec_class->get_num_subsongs =
      GST_DEBUG_FUNCPTR (gst_openmpt_dec_get_num_subsongs);
  dec_class->get_subsong_duration =
      GST_DEBUG_FUNCPTR (gst_openmpt_dec_get_subsong_duration);
  dec_class->get_subsong_tags =
      GST_DEBUG_FUNCPTR (gst_openmpt_dec_get_subsong_tags);
  dec_class->set_subsong_mode =
      GST_DEBUG_FUNCPTR (gst_openmpt_dec_set_subsong_mode);

  gst_element_class_set_static_metadata (element_class,
      "OpenMPT-based module music decoder",
      "Codec/Decoder/Audio",
      "Decoders module files (MOD/S3M/XM/IT/MTM/...) using OpenMPT",
      "Carlos Rafael Giani <dv@pseudoterminal.org>");

  g_object_class_install_property (object_class,
      PROP_MASTER_GAIN,
      g_param_spec_int ("master-gain",
          "Master gain",
          "Gain to apply to the playback, in millibel",
          -G_MAXINT, G_MAXINT,
          DEFAULT_MASTER_GAIN, G_PARAM_READWRITE | G_PARAM_STATIC_STRINGS)
      );
  g_object_class_install_property (object_class,
      PROP_STEREO_SEPARATION,
      g_param_spec_int ("stereo-separation",
          "Stereo separation",
          "Degree of separation for stereo channels, in percent",
          0, 400,
          DEFAULT_STEREO_SEPARATION, G_PARAM_READWRITE | G_PARAM_STATIC_STRINGS)
      );
  g_object_class_install_property (object_class,
      PROP_FILTER_LENGTH,
      g_param_spec_int ("filter-length",
          "Filter length",
          "Length of interpolation filter to use for the samples (0 = internal default)",
          0, 8,
          DEFAULT_FILTER_LENGTH, G_PARAM_READWRITE | G_PARAM_STATIC_STRINGS)
      );
  g_object_class_install_property (object_class,
      PROP_VOLUME_RAMPING,
      g_param_spec_int ("volume-ramping",
          "Volume ramping",
          "Volume ramping strength; higher value -> slower ramping (-1 = internal default)",
          -1, 10,
          DEFAULT_VOLUME_RAMPING, G_PARAM_READWRITE | G_PARAM_STATIC_STRINGS)
      );
  /* 4*4 => quad output with F32 samples; this ensures that no overflow can happen */
  g_object_class_install_property (object_class,
      PROP_OUTPUT_BUFFER_SIZE,
      g_param_spec_uint ("output-buffer-size",
          "Output buffer size",
          "Size of each output buffer, in samples (actual size can be smaller "
          "than this during flush or EOS)",
          1, G_MAXUINT / (4 * 4),
          DEFAULT_OUTPUT_BUFFER_SIZE,
          G_PARAM_READWRITE | G_PARAM_STATIC_STRINGS)
      );
}


void
gst_openmpt_dec_init (GstOpenMptDec * openmpt_dec)
{
  openmpt_dec->mod = NULL;

  openmpt_dec->cur_subsong = 0;
  openmpt_dec->num_subsongs = 0;
  openmpt_dec->subsong_durations = NULL;

  openmpt_dec->num_loops = 0;

  openmpt_dec->master_gain = DEFAULT_MASTER_GAIN;
  openmpt_dec->stereo_separation = DEFAULT_STEREO_SEPARATION;
  openmpt_dec->filter_length = DEFAULT_FILTER_LENGTH;
  openmpt_dec->volume_ramping = DEFAULT_VOLUME_RAMPING;

  openmpt_dec->output_buffer_size = DEFAULT_OUTPUT_BUFFER_SIZE;

  openmpt_dec->main_tags = NULL;

  openmpt_dec->sample_format = DEFAULT_SAMPLE_FORMAT;
  openmpt_dec->sample_rate = DEFAULT_SAMPLE_RATE;
  openmpt_dec->num_channels = DEFAULT_NUM_CHANNELS;
}


static void
gst_openmpt_dec_finalize (GObject * object)
{
  GstOpenMptDec *openmpt_dec;

  g_return_if_fail (GST_IS_OPENMPT_DEC (object));
  openmpt_dec = GST_OPENMPT_DEC (object);

  if (openmpt_dec->main_tags != NULL)
    gst_tag_list_unref (openmpt_dec->main_tags);

  if (openmpt_dec->mod != NULL)
    openmpt_module_destroy (openmpt_dec->mod);

  g_free (openmpt_dec->subsong_durations);

  G_OBJECT_CLASS (gst_openmpt_dec_parent_class)->finalize (object);
}


static void
gst_openmpt_dec_set_property (GObject * object, guint prop_id,
    const GValue * value, GParamSpec * pspec)
{
  GstNonstreamAudioDecoder *dec;
  GstOpenMptDec *openmpt_dec;

  dec = GST_NONSTREAM_AUDIO_DECODER (object);
  openmpt_dec = GST_OPENMPT_DEC (object);

  switch (prop_id) {
    case PROP_MASTER_GAIN:
    {
      GST_NONSTREAM_AUDIO_DECODER_LOCK_MUTEX (dec);
      openmpt_dec->master_gain = g_value_get_int (value);
      if (openmpt_dec->mod != NULL)
        openmpt_module_set_render_param (openmpt_dec->mod,
            OPENMPT_MODULE_RENDER_MASTERGAIN_MILLIBEL,
            openmpt_dec->master_gain);
      GST_NONSTREAM_AUDIO_DECODER_UNLOCK_MUTEX (dec);
      break;
    }

    case PROP_STEREO_SEPARATION:
    {
      GST_NONSTREAM_AUDIO_DECODER_LOCK_MUTEX (dec);
      openmpt_dec->stereo_separation = g_value_get_int (value);
      if (openmpt_dec->mod != NULL)
        openmpt_module_set_render_param (openmpt_dec->mod,
            OPENMPT_MODULE_RENDER_STEREOSEPARATION_PERCENT,
            openmpt_dec->stereo_separation);
      GST_NONSTREAM_AUDIO_DECODER_UNLOCK_MUTEX (dec);
      break;
    }

    case PROP_FILTER_LENGTH:
    {
      GST_NONSTREAM_AUDIO_DECODER_LOCK_MUTEX (dec);
      openmpt_dec->filter_length = g_value_get_int (value);
      if (openmpt_dec->mod != NULL)
        openmpt_module_set_render_param (openmpt_dec->mod,
            OPENMPT_MODULE_RENDER_INTERPOLATIONFILTER_LENGTH,
            openmpt_dec->filter_length);
      GST_NONSTREAM_AUDIO_DECODER_UNLOCK_MUTEX (dec);
      break;
    }

    case PROP_VOLUME_RAMPING:
    {
      GST_NONSTREAM_AUDIO_DECODER_LOCK_MUTEX (dec);
      openmpt_dec->volume_ramping = g_value_get_int (value);
      if (openmpt_dec->mod != NULL)
        openmpt_module_set_render_param (openmpt_dec->mod,
            OPENMPT_MODULE_RENDER_VOLUMERAMPING_STRENGTH,
            openmpt_dec->volume_ramping);
      GST_NONSTREAM_AUDIO_DECODER_UNLOCK_MUTEX (dec);
      break;
    }

    case PROP_OUTPUT_BUFFER_SIZE:
    {
      GST_NONSTREAM_AUDIO_DECODER_LOCK_MUTEX (dec);
      openmpt_dec->output_buffer_size = g_value_get_uint (value);
      GST_NONSTREAM_AUDIO_DECODER_UNLOCK_MUTEX (dec);
      break;
    }

    default:
      G_OBJECT_WARN_INVALID_PROPERTY_ID (object, prop_id, pspec);
      break;
  }
}


static void
gst_openmpt_dec_get_property (GObject * object, guint prop_id, GValue * value,
    GParamSpec * pspec)
{
  GstOpenMptDec *openmpt_dec = GST_OPENMPT_DEC (object);

  switch (prop_id) {
    case PROP_MASTER_GAIN:
    {
      GST_NONSTREAM_AUDIO_DECODER_LOCK_MUTEX (object);
      g_value_set_int (value, openmpt_dec->master_gain);
      GST_NONSTREAM_AUDIO_DECODER_UNLOCK_MUTEX (object);
      break;
    }

    case PROP_STEREO_SEPARATION:
    {
      GST_NONSTREAM_AUDIO_DECODER_LOCK_MUTEX (object);
      g_value_set_int (value, openmpt_dec->stereo_separation);
      GST_NONSTREAM_AUDIO_DECODER_UNLOCK_MUTEX (object);
      break;
    }

    case PROP_FILTER_LENGTH:
    {
      GST_NONSTREAM_AUDIO_DECODER_LOCK_MUTEX (object);
      g_value_set_int (value, openmpt_dec->filter_length);
      GST_NONSTREAM_AUDIO_DECODER_UNLOCK_MUTEX (object);
      break;
    }

    case PROP_VOLUME_RAMPING:
    {
      GST_NONSTREAM_AUDIO_DECODER_LOCK_MUTEX (object);
      g_value_set_int (value, openmpt_dec->volume_ramping);
      GST_NONSTREAM_AUDIO_DECODER_UNLOCK_MUTEX (object);
      break;
    }

    case PROP_OUTPUT_BUFFER_SIZE:
    {
      GST_NONSTREAM_AUDIO_DECODER_LOCK_MUTEX (object);
      g_value_set_uint (value, openmpt_dec->output_buffer_size);
      GST_NONSTREAM_AUDIO_DECODER_UNLOCK_MUTEX (object);
      break;
    }

    default:
      G_OBJECT_WARN_INVALID_PROPERTY_ID (object, prop_id, pspec);
      break;
  }
}


static gboolean
gst_openmpt_dec_seek (GstNonstreamAudioDecoder * dec,
    GstClockTime * new_position)
{
  GstOpenMptDec *openmpt_dec = GST_OPENMPT_DEC (dec);
  g_return_val_if_fail (openmpt_dec->mod != NULL, FALSE);

  openmpt_module_set_position_seconds (openmpt_dec->mod,
      (double) (*new_position) / GST_SECOND);
  *new_position = gst_openmpt_dec_tell (dec);

  return TRUE;
}


static GstClockTime
gst_openmpt_dec_tell (GstNonstreamAudioDecoder * dec)
{
  GstOpenMptDec *openmpt_dec = GST_OPENMPT_DEC (dec);
  g_return_val_if_fail (openmpt_dec->mod != NULL, GST_CLOCK_TIME_NONE);

  return (GstClockTime) (openmpt_module_get_position_seconds (openmpt_dec->mod)
      * GST_SECOND);
}


static void
gst_openmpt_dec_log_func (char const *message, void *user)
{
  GST_LOG_OBJECT (GST_OBJECT (user), "%s", message);
}


static void
gst_openmpt_dec_add_metadata_to_tag_list (GstOpenMptDec * openmpt_dec,
    GstTagList * tags, char const *key, gchar const *tag)
{
  char const *metadata = openmpt_module_get_metadata (openmpt_dec->mod, key);

  if (metadata && *metadata) {
    GST_DEBUG_OBJECT (openmpt_dec,
        "adding metadata \"%s\" with key \"%s\" to tag list as tag \"%s\"",
        metadata, key, tag);

    if (g_strcmp0 (tag, GST_TAG_DATE_TIME) == 0) {
      /* Special handling for date-time tags - interpret the
       * metadata string as an iso8601 string and convert it
       * to a GstDateTime value, since this is the data type
       * that GST_TAG_DATE_TIME expects. */

      GstDateTime *date_time = gst_date_time_new_from_iso8601_string (metadata);
      if (date_time) {
        GST_DEBUG_OBJECT (openmpt_dec,
            "successfully created date-time object out of iso8601 string");
        gst_tag_list_add (tags, GST_TAG_MERGE_REPLACE, tag, date_time, NULL);
        gst_date_time_unref (date_time);
      } else
        GST_WARNING_OBJECT (openmpt_dec,
            "could not create date-time object out of iso8601 string - not adding metadata to tags");
    } else {
      /* Default handling - just insert the metadata string as-is */
      gst_tag_list_add (tags, GST_TAG_MERGE_REPLACE, tag, metadata, NULL);
    }
  } else
    GST_DEBUG_OBJECT (openmpt_dec,
        "attempted to add metadata with key \"%s\" to tag list as tag \"%s\", but none exists",
        key, tag);

  if (metadata)
    openmpt_free_string (metadata);
}


static gboolean
gst_openmpt_dec_load_from_buffer (GstNonstreamAudioDecoder * dec,
    GstBuffer * source_data, guint initial_subsong,
    GstNonstreamAudioSubsongMode initial_subsong_mode,
    GstClockTime * initial_position,
    GstNonstreamAudioOutputMode * initial_output_mode, gint * initial_num_loops)
{
  GstMapInfo map;
  GstOpenMptDec *openmpt_dec;

  openmpt_dec = GST_OPENMPT_DEC (dec);

  /* First, determine the sample rate, channel count, and sample format to use */
  openmpt_dec->sample_format = DEFAULT_SAMPLE_FORMAT;
  openmpt_dec->sample_rate = DEFAULT_SAMPLE_RATE;
  openmpt_dec->num_channels = DEFAULT_NUM_CHANNELS;
  gst_nonstream_audio_decoder_get_downstream_info (dec,
      &(openmpt_dec->sample_format), &(openmpt_dec->sample_rate),
      &(openmpt_dec->num_channels));

  /* Set output format */
  if (!gst_nonstream_audio_decoder_set_output_format_simple (dec,
          openmpt_dec->sample_rate,
          openmpt_dec->sample_format, openmpt_dec->num_channels))
    return FALSE;

  /* Pass the module data to OpenMPT for loading */
  gst_buffer_map (source_data, &map, GST_MAP_READ);
#if OPENMPT_API_VERSION_AT_LEAST(0,3,0)
  openmpt_dec->mod =
      openmpt_module_create_from_memory2 (map.data, map.size,
      gst_openmpt_dec_log_func, dec, NULL, NULL, NULL, NULL, NULL);
#else
  openmpt_dec->mod =
      openmpt_module_create_from_memory (map.data, map.size,
      gst_openmpt_dec_log_func, dec, NULL);
#endif
  gst_buffer_unmap (source_data, &map);

  if (openmpt_dec->mod == NULL) {
    GST_ERROR_OBJECT (dec, "loading module failed");
    return FALSE;
  }

  /* Copy subsong states */
  openmpt_dec->cur_subsong = initial_subsong;
  openmpt_dec->cur_subsong_mode = initial_subsong_mode;

  /* Query the number of subsongs available for logging and for checking
   * the initial subsong index */
  openmpt_dec->num_subsongs =
      openmpt_module_get_num_subsongs (openmpt_dec->mod);
  if (G_UNLIKELY (initial_subsong >= openmpt_dec->num_subsongs)) {
    GST_WARNING_OBJECT (openmpt_dec,
        "initial subsong %u out of bounds (there are %u subsongs) - setting it to 0",
        initial_subsong, openmpt_dec->num_subsongs);
    initial_subsong = 0;
  }
  GST_INFO_OBJECT (openmpt_dec, "%d subsong(s) available",
      openmpt_dec->num_subsongs);

  /* Query the OpenMPT default subsong (can be -1)
   * The default subsong is the one that is initially selected, so we
   * need to query it here, *before* any openmpt_module_select_subsong()
   * calls are done */
  {
    gchar const *subsong_cstr =
        openmpt_module_ctl_get (openmpt_dec->mod, "subsong");
    gchar *endptr;

    if (subsong_cstr != NULL) {
      openmpt_dec->default_openmpt_subsong =
          g_ascii_strtoll (subsong_cstr, &endptr, 10);
      if (subsong_cstr == endptr) {
        GST_WARNING_OBJECT (openmpt_dec,
            "could not convert ctl string \"%s\" to subsong index - using default OpenMPT index -1 instead",
            subsong_cstr);
        openmpt_dec->default_openmpt_subsong = -1;
      } else
        GST_DEBUG_OBJECT (openmpt_dec, "default OpenMPT subsong index is %d",
            openmpt_dec->default_openmpt_subsong);

      openmpt_free_string (subsong_cstr);
    } else {
      GST_INFO_OBJECT (openmpt_dec,
          "could not get subsong ctl string - using default OpenMPT index -1 instead");
      openmpt_dec->default_openmpt_subsong = -1;
    }
  }

  /* Seek to initial position */
  if (*initial_position != 0) {
    openmpt_module_set_position_seconds (openmpt_dec->mod,
        (double) (*initial_position) / GST_SECOND);
    *initial_position =
        (GstClockTime) (openmpt_module_get_position_seconds (openmpt_dec->mod) *
        GST_SECOND);
  }

  /* LOOPING output mode is not supported */
  *initial_output_mode = GST_NONSTREAM_AUDIO_OUTPUT_MODE_STEADY;

  /* Query the durations of each subsong (if any exist) */
  if (openmpt_dec->num_subsongs > 0) {
    guint i;

    openmpt_dec->subsong_durations =
        g_try_malloc (openmpt_dec->num_subsongs * sizeof (double));
    if (openmpt_dec->subsong_durations == NULL) {
      GST_NONSTREAM_AUDIO_DECODER_UNLOCK_MUTEX (dec);
      GST_ELEMENT_ERROR (openmpt_dec, RESOURCE, NO_SPACE_LEFT,
          ("could not allocate memory for subsong duration array"), (NULL));
      GST_NONSTREAM_AUDIO_DECODER_LOCK_MUTEX (dec);
      return FALSE;
    }

    for (i = 0; i < openmpt_dec->num_subsongs; ++i) {
      openmpt_module_select_subsong (openmpt_dec->mod, i);
      openmpt_dec->subsong_durations[i] =
          openmpt_module_get_duration_seconds (openmpt_dec->mod);
    }
  }

  /* Select the initial subsong */
  gst_openmpt_dec_select_subsong (openmpt_dec, initial_subsong_mode,
      initial_subsong);

  /* Set the number of loops, and query the actual number
   * that was chosen by OpenMPT */
  {
    int32_t actual_repeat_count;
    openmpt_module_set_repeat_count (openmpt_dec->mod, *initial_num_loops);
    actual_repeat_count = openmpt_module_get_repeat_count (openmpt_dec->mod);

    if (actual_repeat_count != *initial_num_loops) {
      GST_DEBUG_OBJECT (openmpt_dec,
          "requested num-loops value %d differs from actual value %d",
          *initial_num_loops, actual_repeat_count);
      *initial_num_loops = actual_repeat_count;
    }
  }

  /* Set render parameters (adjustable via properties) */
  openmpt_module_set_render_param (openmpt_dec->mod,
      OPENMPT_MODULE_RENDER_MASTERGAIN_MILLIBEL, openmpt_dec->master_gain);
  openmpt_module_set_render_param (openmpt_dec->mod,
      OPENMPT_MODULE_RENDER_STEREOSEPARATION_PERCENT,
      openmpt_dec->stereo_separation);
  openmpt_module_set_render_param (openmpt_dec->mod,
      OPENMPT_MODULE_RENDER_INTERPOLATIONFILTER_LENGTH,
      openmpt_dec->filter_length);
  openmpt_module_set_render_param (openmpt_dec->mod,
      OPENMPT_MODULE_RENDER_VOLUMERAMPING_STRENGTH,
      openmpt_dec->volume_ramping);

  /* Log the available metadata keys, and produce a
   * tag list if any keys are available */
  {
    char const *metadata_keys =
        openmpt_module_get_metadata_keys (openmpt_dec->mod);
    if (metadata_keys != NULL) {
      GstTagList *tags = gst_tag_list_new_empty ();

      GST_DEBUG_OBJECT (dec, "metadata keys: [%s]", metadata_keys);
      openmpt_free_string (metadata_keys);

      gst_openmpt_dec_add_metadata_to_tag_list (openmpt_dec, tags, "title",
          GST_TAG_TITLE);
      gst_openmpt_dec_add_metadata_to_tag_list (openmpt_dec, tags, "artist",
          GST_TAG_ARTIST);
      gst_openmpt_dec_add_metadata_to_tag_list (openmpt_dec, tags, "message",
          GST_TAG_COMMENT);
      gst_openmpt_dec_add_metadata_to_tag_list (openmpt_dec, tags, "tracker",
          GST_TAG_APPLICATION_NAME);
      gst_openmpt_dec_add_metadata_to_tag_list (openmpt_dec, tags, "type_long",
          GST_TAG_CODEC);
      gst_openmpt_dec_add_metadata_to_tag_list (openmpt_dec, tags, "date",
          GST_TAG_DATE_TIME);
      gst_openmpt_dec_add_metadata_to_tag_list (openmpt_dec, tags,
          "container_long", GST_TAG_CONTAINER_FORMAT);

      openmpt_dec->main_tags = tags;
    } else {
      GST_DEBUG_OBJECT (dec,
          "no metadata keys found - not producing a tag list");
    }
  }

  /* Log any warnings that were produced by OpenMPT while loading */
  {
    char const *warnings =
        openmpt_module_get_metadata (openmpt_dec->mod, "warnings");
    if (warnings) {
      if (*warnings)
        GST_WARNING_OBJECT (openmpt_dec, "reported warnings during loading: %s",
            warnings);
      openmpt_free_string (warnings);
    }
  }

  return TRUE;
}


static GstTagList *
gst_openmpt_dec_get_main_tags (GstNonstreamAudioDecoder * dec)
{
  GstOpenMptDec *openmpt_dec = GST_OPENMPT_DEC (dec);
  return gst_tag_list_ref (openmpt_dec->main_tags);
}


static gboolean
gst_openmpt_dec_set_current_subsong (GstNonstreamAudioDecoder * dec,
    guint subsong, GstClockTime * initial_position)
{
  GstOpenMptDec *openmpt_dec = GST_OPENMPT_DEC (dec);
  g_return_val_if_fail (openmpt_dec->mod != NULL, FALSE);

  if (gst_openmpt_dec_select_subsong (openmpt_dec,
          openmpt_dec->cur_subsong_mode, subsong)) {
    GST_DEBUG_OBJECT (openmpt_dec,
        "selected subsong %u and switching subsong mode to SINGLE", subsong);
    openmpt_dec->cur_subsong_mode = GST_NONSTREAM_AUDIO_SUBSONG_MODE_SINGLE;
    openmpt_dec->cur_subsong = subsong;
    *initial_position = 0;
    return TRUE;
  } else {
    GST_ERROR_OBJECT (openmpt_dec, "could not select subsong %u", subsong);
    return FALSE;
  }
}


static guint
gst_openmpt_dec_get_current_subsong (GstNonstreamAudioDecoder * dec)
{
  GstOpenMptDec *openmpt_dec = GST_OPENMPT_DEC (dec);
  return openmpt_dec->cur_subsong;
}


static guint
gst_openmpt_dec_get_num_subsongs (GstNonstreamAudioDecoder * dec)
{
  GstOpenMptDec *openmpt_dec = GST_OPENMPT_DEC (dec);
  return openmpt_dec->num_subsongs;
}


static GstClockTime
gst_openmpt_dec_get_subsong_duration (GstNonstreamAudioDecoder * dec,
    guint subsong)
{
  GstOpenMptDec *openmpt_dec = GST_OPENMPT_DEC (dec);
  return (GstClockTime) (openmpt_dec->subsong_durations[subsong] * GST_SECOND);
}


static GstTagList *
gst_openmpt_dec_get_subsong_tags (GstNonstreamAudioDecoder * dec, guint subsong)
{
  GstOpenMptDec *openmpt_dec;
  char const *name;

  openmpt_dec = GST_OPENMPT_DEC (dec);

  name = openmpt_module_get_subsong_name (openmpt_dec->mod, subsong);
  if (name != NULL) {
    GstTagList *tags = NULL;

    if (*name) {
      tags = gst_tag_list_new_empty ();
      gst_tag_list_add (tags, GST_TAG_MERGE_REPLACE, "title", name, NULL);
    }

    openmpt_free_string (name);

    return tags;
  } else
    return NULL;
}


static gboolean
gst_openmpt_dec_set_subsong_mode (GstNonstreamAudioDecoder * dec,
    GstNonstreamAudioSubsongMode mode, GstClockTime * initial_position)
{
  GstOpenMptDec *openmpt_dec = GST_OPENMPT_DEC (dec);
  g_return_val_if_fail (openmpt_dec->mod != NULL, FALSE);

  if (gst_openmpt_dec_select_subsong (openmpt_dec, mode,
          openmpt_dec->cur_subsong)) {
    GST_DEBUG_OBJECT (openmpt_dec, "set subsong mode");
    openmpt_dec->cur_subsong_mode = mode;
    *initial_position = 0;
    return TRUE;
  } else {
    GST_ERROR_OBJECT (openmpt_dec, "could not set subsong mode");
    return FALSE;
  }
}


static gboolean
gst_openmpt_dec_set_num_loops (GstNonstreamAudioDecoder * dec, gint num_loops)
{
  GstOpenMptDec *openmpt_dec = GST_OPENMPT_DEC (dec);
  openmpt_dec->num_loops = num_loops;

  if (openmpt_dec->mod != NULL) {
    if (openmpt_module_set_repeat_count (openmpt_dec->mod, num_loops)) {
      GST_DEBUG_OBJECT (openmpt_dec, "successfully set repeat count %d",
          num_loops);
      return TRUE;
    } else {
      GST_ERROR_OBJECT (openmpt_dec, "could not set repeat count %d",
          num_loops);
      return FALSE;
    }
  } else
    return TRUE;
}


static gint
gst_openmpt_dec_get_num_loops (GstNonstreamAudioDecoder * dec)
{
  GstOpenMptDec *openmpt_dec = GST_OPENMPT_DEC (dec);
  return openmpt_dec->num_loops;
}


static guint
gst_openmpt_dec_get_supported_output_modes (G_GNUC_UNUSED
    GstNonstreamAudioDecoder * dec)
{
  return 1u << GST_NONSTREAM_AUDIO_OUTPUT_MODE_STEADY;
}


static gboolean
gst_openmpt_dec_decode (GstNonstreamAudioDecoder * dec, GstBuffer ** buffer,
    guint * num_samples)
{
  GstOpenMptDec *openmpt_dec;
  GstBuffer *outbuf;
  GstMapInfo map;
  size_t num_read_samples;
  gsize outbuf_size;
  GstAudioFormatInfo const *fmt_info;

  openmpt_dec = GST_OPENMPT_DEC (dec);

  fmt_info = gst_audio_format_get_info (openmpt_dec->sample_format);

  /* Allocate output buffer */
  outbuf_size =
      openmpt_dec->output_buffer_size * (fmt_info->width / 8) *
      openmpt_dec->num_channels;
  outbuf =
      gst_nonstream_audio_decoder_allocate_output_buffer (dec, outbuf_size);
  if (G_UNLIKELY (outbuf == NULL))
    return FALSE;

  /* Write samples into the output buffer */

  gst_buffer_map (outbuf, &map, GST_MAP_WRITE);

  switch (openmpt_dec->sample_format) {
    case GST_AUDIO_FORMAT_S16:
    {
      int16_t *out_samples = (int16_t *) (map.data);
      switch (openmpt_dec->num_channels) {
        case 1:
          num_read_samples =
              openmpt_module_read_mono (openmpt_dec->mod,
              openmpt_dec->sample_rate, openmpt_dec->output_buffer_size,
              out_samples);
          break;
        case 2:
          num_read_samples =
              openmpt_module_read_interleaved_stereo (openmpt_dec->mod,
              openmpt_dec->sample_rate, openmpt_dec->output_buffer_size,
              out_samples);
          break;
        case 4:
          num_read_samples =
              openmpt_module_read_interleaved_quad (openmpt_dec->mod,
              openmpt_dec->sample_rate, openmpt_dec->output_buffer_size,
              out_samples);
          break;
        default:
          g_assert_not_reached ();
      }
      break;
    }
    case GST_AUDIO_FORMAT_F32:
    {
      float *out_samples = (float *) (map.data);
      switch (openmpt_dec->num_channels) {
        case 1:
          num_read_samples =
              openmpt_module_read_float_mono (openmpt_dec->mod,
              openmpt_dec->sample_rate, openmpt_dec->output_buffer_size,
              out_samples);
          break;
        case 2:
          num_read_samples =
              openmpt_module_read_interleaved_float_stereo (openmpt_dec->mod,
              openmpt_dec->sample_rate, openmpt_dec->output_buffer_size,
              out_samples);
          break;
        case 4:
          num_read_samples =
              openmpt_module_read_interleaved_float_quad (openmpt_dec->mod,
              openmpt_dec->sample_rate, openmpt_dec->output_buffer_size,
              out_samples);
          break;
        default:
          g_assert_not_reached ();
      }
      break;
    }
    default:
    {
      GST_ERROR_OBJECT (dec, "using unsupported sample format %s",
          fmt_info->name);
      g_assert_not_reached ();
    }
  }

  gst_buffer_unmap (outbuf, &map);

  if (num_read_samples == 0)
    return FALSE;

  *buffer = outbuf;
  *num_samples = num_read_samples;

  return TRUE;
}


static gboolean
gst_openmpt_dec_select_subsong (GstOpenMptDec * openmpt_dec,
    GstNonstreamAudioSubsongMode subsong_mode, gint openmpt_subsong)
{
  switch (subsong_mode) {
    case GST_NONSTREAM_AUDIO_SUBSONG_MODE_SINGLE:
      GST_DEBUG_OBJECT (openmpt_dec, "setting subsong mode to SINGLE");
      return openmpt_module_select_subsong (openmpt_dec->mod, openmpt_subsong);

    case GST_NONSTREAM_AUDIO_SUBSONG_MODE_ALL:
      GST_DEBUG_OBJECT (openmpt_dec, "setting subsong mode to ALL");
      return openmpt_module_select_subsong (openmpt_dec->mod, -1);

    case GST_NONSTREAM_AUDIO_SUBSONG_MODE_DECODER_DEFAULT:
      /* NOTE: The OpenMPT documentation recommends to not bother
       * calling openmpt_module_select_subsong() if the decoder
       * default shall be used. However, the user might have switched
       * the subsong mode from SINGLE or ALL to DECODER_DEFAULT,
       * in which case we *do* have to set the default subsong index.
       * So, just set the default index here. */
      GST_DEBUG_OBJECT (openmpt_dec, "setting subsong mode to DECODER_DEFAULT");
      return openmpt_module_select_subsong (openmpt_dec->mod,
          openmpt_dec->default_openmpt_subsong);

    default:
      g_assert_not_reached ();
      return TRUE;
  }
}
