/*
 * GStreamer
 * @author Tiago Katcipis <tiagokatcipis@gmail.com>
 * @author Paulo Pizarro  <paulo.pizarro@gmail.com>
 * 
 * Permission is hereby granted, free of charge, to any person obtaining a
 * copy of this software and associated documentation files (the "Software"),
 * to deal in the Software without restriction, including without limitation
 * the rights to use, copy, modify, merge, publish, distribute, sublicense,
 * and/or sell copies of the Software, and to permit persons to whom the
 * Software is furnished to do so, subject to the following conditions:
 *
 * The above copyright notice and this permission notice shall be included in
 * all copies or substantial portions of the Software.
 *
 * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
 * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
 * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
 * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
 * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
 * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
 * DEALINGS IN THE SOFTWARE.
 *
 * Alternatively, the contents of this file may be used under the
 * GNU Lesser General Public License Version 2.1 (the "LGPL"), in
 * which case the following provisions apply instead of the ones
 * mentioned above:
 *
 * 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., 59 Temple Place - Suite 330,
 * Boston, MA 02111-1307, USA.
 */

/**
 * SECTION:element-removesilence
 *
 * Removes all silence periods from an audio stream, fixing timestamp discontinuities.
 *
 * <refsect2>
 * <title>Example launch line</title>
 * |[
 * gst-launch -v -m filesrc location="audiofile" ! decodebin ! removesilence ! autoaudiosink
 * ]|
 * </refsect2>
 */
#ifdef HAVE_CONFIG_H
#  include "config.h"
#endif

#include <gst/gst.h>
#include "gstremovesilence.h"
#include "vad_private.h"

GST_DEBUG_CATEGORY_STATIC (gst_remove_silence_debug);
#define GST_CAT_DEFAULT gst_remove_silence_debug

struct _GstRemoveSilence
{
  GstElement element;
  GstPad *sinkpad, *srcpad;
  VADFilter* vad;
  gboolean remove;
  gint64 segment_end;
  gint samplerate;
  GstClockTime last_timestamp;
};

struct _GstRemoveSilenceClass 
{
  GstElementClass parent_class;
};

/* Filter signals and args */
enum
{
  /* FILL ME */
  LAST_SIGNAL
};

enum
{
  PROP_0,
  PROP_REMOVE
};


static GstStaticPadTemplate sink_factory = GST_STATIC_PAD_TEMPLATE ("sink",
    GST_PAD_SINK,
    GST_PAD_ALWAYS,
    GST_STATIC_CAPS ("audio/x-raw-int, rate=[1, 2147483647], channels=1, endianness=1234, width=16, depth=16, signed=true")
    );

static GstStaticPadTemplate src_factory = GST_STATIC_PAD_TEMPLATE ("src",
    GST_PAD_SRC,
    GST_PAD_ALWAYS,
    GST_STATIC_CAPS ("audio/x-raw-int, rate=[1, 2147483647], channels=1, endianness=1234, width=16, depth=16, signed=true")
    );

GST_BOILERPLATE (GstRemoveSilence, gst_remove_silence, GstElement, GST_TYPE_ELEMENT);

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

static gboolean gst_remove_silence_set_caps (GstPad * pad, GstCaps * caps);
static GstFlowReturn gst_remove_silence_chain (GstPad * pad, GstBuffer * buf);
static gboolean gst_remove_silence_event (GstPad * pad, GstEvent * event);

/* GObject vmethod implementations */

static void
gst_remove_silence_base_init (gpointer gclass)
{
  GstElementClass *element_class = GST_ELEMENT_CLASS (gclass);

  gst_element_class_set_details_simple(element_class,
    "RemoveSilence",
    "Filter/Effect/Audio",
    "Removes all the silence periods from the stream.",
    "Tiago Katcipis <tiagokatcipis@gmail.com>\n \
     Paulo Pizarro  <paulo.pizarro@gmail.com>");

  gst_element_class_add_pad_template (element_class,
      gst_static_pad_template_get (&src_factory));
  gst_element_class_add_pad_template (element_class,
      gst_static_pad_template_get (&sink_factory));
}

/* initialize the removesilence's class */
static void
gst_remove_silence_class_init (GstRemoveSilenceClass * klass)
{
  GObjectClass *gobject_class;
  GstElementClass *gstelement_class;

  gobject_class = (GObjectClass *) klass;
  gstelement_class = (GstElementClass *) klass;

  gobject_class->set_property = gst_remove_silence_set_property;
  gobject_class->get_property = gst_remove_silence_get_property;

  g_object_class_install_property (gobject_class, PROP_REMOVE,
      g_param_spec_boolean ("remove", "Remove", "Set to true to remove silence from the stream, false otherwhise",
          FALSE, G_PARAM_READWRITE));
}

/* initialize the new element
 * instantiate pads and add them to element
 * set pad calback functions
 * initialize instance structure
 */
static void
gst_remove_silence_init (GstRemoveSilence * filter,
    GstRemoveSilenceClass * gclass)
{
  filter->sinkpad = gst_pad_new_from_static_template (&sink_factory, "sink");
  gst_pad_set_setcaps_function (filter->sinkpad,
                                GST_DEBUG_FUNCPTR(gst_remove_silence_set_caps));
  gst_pad_set_getcaps_function (filter->sinkpad,
                                GST_DEBUG_FUNCPTR(gst_pad_proxy_getcaps));
  gst_pad_set_chain_function (filter->sinkpad,
                              GST_DEBUG_FUNCPTR(gst_remove_silence_chain));

  filter->srcpad = gst_pad_new_from_static_template (&src_factory, "src");
  gst_pad_set_getcaps_function (filter->srcpad,
                                GST_DEBUG_FUNCPTR(gst_pad_proxy_getcaps));
  
  gst_pad_set_event_function (filter->sinkpad, gst_remove_silence_event);

  gst_element_add_pad (GST_ELEMENT (filter), filter->sinkpad);
  gst_element_add_pad (GST_ELEMENT (filter), filter->srcpad);

  filter->remove = FALSE;
  filter->last_timestamp = 0;
  filter->vad = vad_new();
  if(!filter->vad){
      GST_DEBUG("Error initializing VAD !!");
      return;
  }
}

static void
gst_remove_silence_set_property (GObject * object, guint prop_id,
    const GValue * value, GParamSpec * pspec)
{
  GstRemoveSilence *filter = GST_REMOVESILENCE (object);

  switch (prop_id) {
    case PROP_REMOVE:
      filter->remove = g_value_get_boolean (value);
      break;
    default:
      G_OBJECT_WARN_INVALID_PROPERTY_ID (object, prop_id, pspec);
      break;
  }
}

static void
gst_remove_silence_get_property (GObject * object, guint prop_id,
    GValue * value, GParamSpec * pspec)
{
  GstRemoveSilence *filter = GST_REMOVESILENCE (object);

  switch (prop_id) {
    case PROP_REMOVE:
      g_value_set_boolean (value, filter->remove);
      break;
    default:
      G_OBJECT_WARN_INVALID_PROPERTY_ID (object, prop_id, pspec);
      break;
  }
}

/* this function handles the link with other elements */
static gboolean
gst_remove_silence_set_caps (GstPad * pad, GstCaps * caps)
{
  GstRemoveSilence *filter;
  GstPad *otherpad;
  GstStructure *structure = gst_caps_get_structure(caps, 0);

  filter = GST_REMOVESILENCE (gst_pad_get_parent (pad));
  otherpad = (pad == filter->srcpad) ? filter->sinkpad : filter->srcpad;

  if(!gst_pad_set_caps (otherpad, caps))
    return FALSE;
  
  gst_structure_get_int (structure, "rate", &filter->samplerate);
  return TRUE; 
}

static GstFlowReturn
gst_remove_silence_chain (GstPad * pad, GstBuffer * inbuf)
{
  GstRemoveSilence *filter = NULL;
  int frame_type;
  filter = GST_REMOVESILENCE (GST_OBJECT_PARENT (pad));
  
  /*
    Validate timestamp, some elements does not control the segment being played
    using timestamp, if you are looping on a segment with silence a skew can be generated
    because the stream will emit segment end based on the audio played not the timestamp. 
  */
  if(filter->remove){
      if(GST_BUFFER_CAST(inbuf)->timestamp > filter->segment_end){
          //Reached the end of the segment
          GST_DEBUG("EOS reached!");
          GST_DEBUG("buffer timestamp[%lli], segment_end[%lli]", GST_BUFFER_CAST(inbuf)->timestamp, filter->segment_end);
          gst_buffer_unref(inbuf);
          return GST_FLOW_UNEXPECTED;
      }
  }
  frame_type = vad_update(filter->vad, (short *) GST_BUFFER_DATA(inbuf), GST_BUFFER_SIZE(inbuf) / sizeof(short));
  if (frame_type == VAD_SILENCE) {
      GST_DEBUG("Silence detected!");
      if(filter->remove){
          GST_DEBUG("Removing silence!");
          gst_buffer_unref(inbuf);
          return GST_FLOW_OK;    
      }
  } 

  GST_DEBUG("Voice!");      
  GST_BUFFER_CAST(inbuf)->timestamp = filter->last_timestamp;
  filter->last_timestamp += GST_BUFFER_DURATION(inbuf);
  
  return gst_pad_push (filter->srcpad, inbuf);
}


static gboolean gst_remove_handle_new_segment_event(GstRemoveSilence* filter, GstEvent* event)
{
    gdouble rate = 0.0;
    gboolean update = FALSE;
    gint64 start,stop,position;
    GstFormat format;

    gst_event_parse_new_segment(event, &update, &rate, &format,  
                                &start, &stop, &position);

    GST_DEBUG("Updating segment: update[%d], rate[%f], format[%d], start[%lli], stop[%lli], position[%lli]",
              update, rate, format, start, stop, position);

    if(format == GST_FORMAT_TIME){
        GST_DEBUG("Getting new segment in time");
        filter->segment_end = stop;
        filter->last_timestamp = start;
        return TRUE;
    }

    if(format == GST_FORMAT_BYTES){
        GST_DEBUG("Getting new segment in bytes");
        filter->segment_end = (stop / sizeof(short)) / (filter->samplerate / GST_SECOND);
        filter->last_timestamp = (start / sizeof(short)) / (filter->samplerate / GST_SECOND);
        return TRUE;
    }

    GST_DEBUG("Unable to get the segment being played");
    return FALSE;
}

static gboolean gst_remove_silence_event (GstPad * pad, GstEvent * event)
{
    GstRemoveSilence* filter = GST_REMOVESILENCE (GST_OBJECT_PARENT (pad));
    gboolean ret = FALSE;

    switch (GST_EVENT_TYPE (event)) {
    case GST_EVENT_NEWSEGMENT:
      if(!gst_remove_handle_new_segment_event(filter, event)){
          return FALSE;
      }
      ret = gst_pad_push_event (filter->srcpad, event);
      break;
    default:
        ret = gst_pad_event_default(pad, event);
        break;
    }
    return ret;
}

/*Plugin init functions*/
static gboolean
plugin_init (GstPlugin * plugin)
{
  return gst_element_register (plugin, "removesilence", GST_RANK_NONE, gst_remove_silence_get_type());
}

GST_PLUGIN_DEFINE (GST_VERSION_MAJOR,
    GST_VERSION_MINOR,
    "removesilence",
    "Removes silence from an audio stream",
    plugin_init, VERSION, "LGPL", GST_PACKAGE_NAME, GST_PACKAGE_ORIGIN);

