/*
 * GStreamer
 *
 * Copyright (C) 2005 Wim Taymans <wim@fluendo.com>
 * Copyright (C) 2006 Tim-Philipp Müller <tim centricular net>
 * Copyright (C) 2009-2010 Chris Robinson <chris.kcat@gmail.com>
 * Copyright (C) 2013 Juan Manuel Borges Caño <juanmabcmail@gmail.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.
 */

/**
 * SECTION:element-openalsink
 * @see_also: openalsrc
 * @short_description: capture raw audio samples through OpenAL
 *
 * This element plays raw audio samples through OpenAL.
 *
 * Unfortunately the capture API doesn't have a format enumeration/check. all you can do is try opening it and see if it works.
 *
 * <refsect2>
 * <title>Example pipelines</title>
 * |[
 * gst-launch-1.0 audiotestsrc ! audioconvert ! volume volume=0.5 ! openalsink
 * ]| will play a sine wave (continuous beep sound) through OpenAL.
 * |[
 * gst-launch-1.0 filesrc location=stream.wav ! decodebin ! audioconvert ! openalsink
 * ]| will play a wav audio file through OpenAL.
 * |[
 * gst-launch-1.0 openalsrc ! "audio/x-raw,format=S16LE,rate=44100" ! audioconvert ! volume volume=0.25 ! openalsink
 * ]| will capture and play audio through OpenAL.
 * </refsect2>
 */

/*
 * DEV:
 * To get better timing/delay information you may also be interested in this:
 *  http://kcat.strangesoft.net/openal-extensions/SOFT_source_latency.txt
 */

#ifdef HAVE_CONFIG_H
#include "config.h"
#endif

#include <gst/gst.h>
#include <gst/gsterror.h>

GST_DEBUG_CATEGORY_EXTERN (openal_debug);
#define GST_CAT_DEFAULT openal_debug

#include "gstopenalsink.h"

static void gst_openal_sink_dispose (GObject * object);
static void gst_openal_sink_finalize (GObject * object);

static void gst_openal_sink_get_property (GObject * object, guint prop_id,
    GValue * value, GParamSpec * pspec);
static void gst_openal_sink_set_property (GObject * object, guint prop_id,
    const GValue * value, GParamSpec * pspec);
static GstCaps *gst_openal_sink_getcaps (GstBaseSink * basesink,
    GstCaps * filter);
static gboolean gst_openal_sink_open (GstAudioSink * audiosink);
static gboolean gst_openal_sink_close (GstAudioSink * audiosink);
static gboolean gst_openal_sink_prepare (GstAudioSink * audiosink,
    GstAudioRingBufferSpec * spec);
static gboolean gst_openal_sink_unprepare (GstAudioSink * audiosink);
static gint gst_openal_sink_write (GstAudioSink * audiosink, gpointer data,
    guint length);
static guint gst_openal_sink_delay (GstAudioSink * audiosink);
static void gst_openal_sink_reset (GstAudioSink * audiosink);

#define OPENAL_DEFAULT_DEVICE NULL

#define OPENAL_MIN_RATE 8000
#define OPENAL_MAX_RATE 192000

enum
{
  PROP_0,

  PROP_DEVICE,
  PROP_DEVICE_NAME,

  PROP_USER_DEVICE,
  PROP_USER_CONTEXT,
  PROP_USER_SOURCE
};

static GstStaticPadTemplate openalsink_factory =
    GST_STATIC_PAD_TEMPLATE ("sink",
    GST_PAD_SINK,
    GST_PAD_ALWAYS,
    GST_STATIC_CAPS ("audio/x-raw, " "format = (string) " GST_AUDIO_NE (F64)
        ", " "rate = (int) [ 1, MAX ], " "channels = (int) [ 1, 2 ]; "
        "audio/x-raw, " "format = (string) " GST_AUDIO_NE (F32) ", "
        "rate = (int) [ 1, MAX ], " "channels = (int) [ 1, MAX ]; "
        "audio/x-raw, " "format = (string) " GST_AUDIO_NE (S16) ", "
        "rate = (int) [ 1, MAX ], " "channels = (int) [ 1, MAX ]; "
        "audio/x-raw, " "format = (string) " G_STRINGIFY (U8) ", "
        "rate = (int) [ 1, MAX ], " "channels = (int) [ 1, MAX ]; "
        /* These caps do not work on my card */
        // "audio/x-adpcm, " "layout = (string) ima, "
        // "rate = (int) [ 1, MAX ], " "channels = (int) [ 1, 2 ]; "
        // "audio/x-alaw, " "rate = (int) [ 1, MAX ], "
        // "channels = (int) [ 1, 2 ]; "
        // "audio/x-mulaw, " "rate = (int) [ 1, MAX ], "
        // "channels = (int) [ 1, MAX ]"
    )
    );

static PFNALCSETTHREADCONTEXTPROC palcSetThreadContext;
static PFNALCGETTHREADCONTEXTPROC palcGetThreadContext;

static inline ALCcontext *
pushContext (ALCcontext * context)
{
  ALCcontext *old;
  if (!palcGetThreadContext || !palcSetThreadContext)
    return NULL;

  old = palcGetThreadContext ();
  if (old != context)
    palcSetThreadContext (context);
  return old;
}

static inline void
popContext (ALCcontext * old, ALCcontext * context)
{
  if (!palcGetThreadContext || !palcSetThreadContext)
    return;

  if (old != context)
    palcSetThreadContext (old);
}

static inline ALenum
checkALError (const char *fname, unsigned int fline)
{
  ALenum err = alGetError ();
  if (err != AL_NO_ERROR)
    g_warning ("%s:%u: context error: %s", fname, fline, alGetString (err));
  return err;
}

#define checkALError() checkALError(__FILE__, __LINE__)

G_DEFINE_TYPE (GstOpenALSink, gst_openal_sink, GST_TYPE_AUDIO_SINK);

static void
gst_openal_sink_dispose (GObject * object)
{
  GstOpenALSink *sink = GST_OPENAL_SINK (object);

  if (sink->probed_caps)
    gst_caps_unref (sink->probed_caps);
  sink->probed_caps = NULL;

  G_OBJECT_CLASS (gst_openal_sink_parent_class)->dispose (object);
}

static void
gst_openal_sink_class_init (GstOpenALSinkClass * klass)
{
  GObjectClass *gobject_class = (GObjectClass *) klass;
  GstElementClass *gstelement_class = (GstElementClass *) klass;
  GstBaseSinkClass *gstbasesink_class = (GstBaseSinkClass *) klass;
  GstAudioSinkClass *gstaudiosink_class = (GstAudioSinkClass *) klass;

  if (alcIsExtensionPresent (NULL, "ALC_EXT_thread_local_context")) {
    palcSetThreadContext = alcGetProcAddress (NULL, "alcSetThreadContext");
    palcGetThreadContext = alcGetProcAddress (NULL, "alcGetThreadContext");
  }

  gobject_class->dispose = GST_DEBUG_FUNCPTR (gst_openal_sink_dispose);
  gobject_class->finalize = GST_DEBUG_FUNCPTR (gst_openal_sink_finalize);
  gobject_class->set_property =
      GST_DEBUG_FUNCPTR (gst_openal_sink_set_property);
  gobject_class->get_property =
      GST_DEBUG_FUNCPTR (gst_openal_sink_get_property);

  gst_openal_sink_parent_class = g_type_class_peek_parent (klass);

  gstbasesink_class->get_caps = GST_DEBUG_FUNCPTR (gst_openal_sink_getcaps);

  gstaudiosink_class->open = GST_DEBUG_FUNCPTR (gst_openal_sink_open);
  gstaudiosink_class->close = GST_DEBUG_FUNCPTR (gst_openal_sink_close);
  gstaudiosink_class->prepare = GST_DEBUG_FUNCPTR (gst_openal_sink_prepare);
  gstaudiosink_class->unprepare = GST_DEBUG_FUNCPTR (gst_openal_sink_unprepare);
  gstaudiosink_class->write = GST_DEBUG_FUNCPTR (gst_openal_sink_write);
  gstaudiosink_class->delay = GST_DEBUG_FUNCPTR (gst_openal_sink_delay);
  gstaudiosink_class->reset = GST_DEBUG_FUNCPTR (gst_openal_sink_reset);

  g_object_class_install_property (gobject_class, PROP_DEVICE_NAME,
      g_param_spec_string ("device-name", "Device name",
          "Human-readable name of the opened device", "", G_PARAM_READABLE));

  g_object_class_install_property (gobject_class, PROP_DEVICE,
      g_param_spec_string ("device", "Device",
          "Human-readable name of the device", OPENAL_DEFAULT_DEVICE,
          G_PARAM_READWRITE));

  g_object_class_install_property (gobject_class, PROP_USER_DEVICE,
      g_param_spec_pointer ("user-device", "ALCdevice", "User device",
          G_PARAM_READWRITE | G_PARAM_STATIC_STRINGS));

  g_object_class_install_property (gobject_class, PROP_USER_CONTEXT,
      g_param_spec_pointer ("user-context", "ALCcontext", "User context",
          G_PARAM_READWRITE | G_PARAM_STATIC_STRINGS));

  g_object_class_install_property (gobject_class, PROP_USER_SOURCE,
      g_param_spec_uint ("user-source", "ALsource", "User source", 0, UINT_MAX,
          0, G_PARAM_READWRITE));

  gst_element_class_set_static_metadata (gstelement_class, "OpenAL Audio Sink",
      "Sink/Audio", "Output audio through OpenAL",
      "Juan Manuel Borges Caño <juanmabcmail@gmail.com>");

  gst_element_class_add_pad_template (gstelement_class,
      gst_static_pad_template_get (&openalsink_factory));

}

static void
gst_openal_sink_init (GstOpenALSink * sink)
{
  GST_DEBUG_OBJECT (sink, "initializing");

  sink->device_name = g_strdup (OPENAL_DEFAULT_DEVICE);

  sink->user_device = NULL;
  sink->user_context = NULL;
  sink->user_source = 0;

  sink->default_device = NULL;
  sink->default_context = NULL;
  sink->default_source = 0;

  sink->buffer_idx = 0;
  sink->buffer_count = 0;
  sink->buffers = NULL;
  sink->buffer_length = 0;

  sink->write_reset = AL_FALSE;
  sink->probed_caps = NULL;

  g_mutex_init (&sink->openal_lock);
}

static void
gst_openal_sink_finalize (GObject * object)
{
  GstOpenALSink *sink = GST_OPENAL_SINK (object);

  g_free (sink->device_name);
  sink->device_name = NULL;
  g_mutex_clear (&sink->openal_lock);

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

static void
gst_openal_sink_set_property (GObject * object, guint prop_id,
    const GValue * value, GParamSpec * pspec)
{
  GstOpenALSink *sink = GST_OPENAL_SINK (object);

  switch (prop_id) {
    case PROP_DEVICE:
      g_free (sink->device_name);
      sink->device_name = g_value_dup_string (value);
      if (sink->probed_caps)
        gst_caps_unref (sink->probed_caps);
      sink->probed_caps = NULL;
      break;
    case PROP_USER_DEVICE:
      if (!sink->default_device)
        sink->user_device = g_value_get_pointer (value);
      break;
    case PROP_USER_CONTEXT:
      if (!sink->default_device)
        sink->user_context = g_value_get_pointer (value);
      break;
    case PROP_USER_SOURCE:
      if (!sink->default_device)
        sink->user_source = g_value_get_uint (value);
      break;

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

static void
gst_openal_sink_get_property (GObject * object, guint prop_id, GValue * value,
    GParamSpec * pspec)
{
  GstOpenALSink *sink = GST_OPENAL_SINK (object);
  const ALCchar *device_name = sink->device_name;
  ALCdevice *device = sink->default_device;
  ALCcontext *context = sink->default_context;
  ALuint source = sink->default_source;

  switch (prop_id) {
    case PROP_DEVICE_NAME:
      device_name = "";
      if (device)
        device_name = alcGetString (device, ALC_DEVICE_SPECIFIER);
      /* fall-through */
    case PROP_DEVICE:
      g_value_set_string (value, device_name);
      break;
    case PROP_USER_DEVICE:
      if (!device)
        device = sink->user_device;
      g_value_set_pointer (value, device);
      break;
    case PROP_USER_CONTEXT:
      if (!context)
        context = sink->user_context;
      g_value_set_pointer (value, context);
      break;
    case PROP_USER_SOURCE:
      if (!source)
        source = sink->user_source;
      g_value_set_uint (value, source);
      break;

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

static GstCaps *
gst_openal_helper_probe_caps (ALCcontext * context)
{
  static const struct
  {
    gint count;
    GstAudioChannelPosition positions[8];
  } chans[] = {
    {
      1, {
      GST_AUDIO_CHANNEL_POSITION_MONO}
    }, {
      2, {
      GST_AUDIO_CHANNEL_POSITION_FRONT_LEFT,
            GST_AUDIO_CHANNEL_POSITION_FRONT_RIGHT}
    }, {
      4, {
      GST_AUDIO_CHANNEL_POSITION_FRONT_LEFT,
            GST_AUDIO_CHANNEL_POSITION_FRONT_RIGHT,
            GST_AUDIO_CHANNEL_POSITION_REAR_LEFT,
            GST_AUDIO_CHANNEL_POSITION_REAR_RIGHT}
    }, {
      6, {
      GST_AUDIO_CHANNEL_POSITION_FRONT_LEFT,
            GST_AUDIO_CHANNEL_POSITION_FRONT_RIGHT,
            GST_AUDIO_CHANNEL_POSITION_FRONT_CENTER,
            GST_AUDIO_CHANNEL_POSITION_LFE1,
            GST_AUDIO_CHANNEL_POSITION_REAR_LEFT,
            GST_AUDIO_CHANNEL_POSITION_REAR_RIGHT}
    }, {
      7, {
      GST_AUDIO_CHANNEL_POSITION_FRONT_LEFT,
            GST_AUDIO_CHANNEL_POSITION_FRONT_RIGHT,
            GST_AUDIO_CHANNEL_POSITION_FRONT_CENTER,
            GST_AUDIO_CHANNEL_POSITION_LFE1,
            GST_AUDIO_CHANNEL_POSITION_REAR_CENTER,
            GST_AUDIO_CHANNEL_POSITION_SIDE_LEFT,
            GST_AUDIO_CHANNEL_POSITION_SIDE_RIGHT}
    }, {
      8, {
      GST_AUDIO_CHANNEL_POSITION_FRONT_LEFT,
            GST_AUDIO_CHANNEL_POSITION_FRONT_RIGHT,
            GST_AUDIO_CHANNEL_POSITION_FRONT_CENTER,
            GST_AUDIO_CHANNEL_POSITION_LFE1,
            GST_AUDIO_CHANNEL_POSITION_REAR_LEFT,
            GST_AUDIO_CHANNEL_POSITION_REAR_RIGHT,
            GST_AUDIO_CHANNEL_POSITION_SIDE_LEFT,
            GST_AUDIO_CHANNEL_POSITION_SIDE_RIGHT}
  },};
  GstStructure *structure;
  guint64 channel_mask;
  GstCaps *caps;
  ALCcontext *old;

  old = pushContext (context);

  caps = gst_caps_new_empty ();

  if (alIsExtensionPresent ("AL_EXT_MCFORMATS")) {
    const char *fmt32[] = {
      "AL_FORMAT_MONO_FLOAT32",
      "AL_FORMAT_STEREO_FLOAT32",
      "AL_FORMAT_QUAD32",
      "AL_FORMAT_51CHN32",
      "AL_FORMAT_61CHN32",
      "AL_FORMAT_71CHN32",
      NULL
    }, *fmt16[] = {
    "AL_FORMAT_MONO16",
          "AL_FORMAT_STEREO16",
          "AL_FORMAT_QUAD16",
          "AL_FORMAT_51CHN16",
          "AL_FORMAT_61CHN16", "AL_FORMAT_71CHN16", NULL}, *fmt8[] = {
    "AL_FORMAT_MONO8",
          "AL_FORMAT_STEREO8",
          "AL_FORMAT_QUAD8",
          "AL_FORMAT_51CHN8", "AL_FORMAT_61CHN8", "AL_FORMAT_71CHN8", NULL};
    int i;

    if (alIsExtensionPresent ("AL_EXT_FLOAT32")) {
      for (i = 0; fmt32[i]; i++) {
        ALenum value = alGetEnumValue (fmt32[i]);
        if (checkALError () != AL_NO_ERROR || value == 0 || value == -1)
          continue;

        structure =
            gst_structure_new ("audio/x-raw", "format", G_TYPE_STRING,
            GST_AUDIO_NE (F32), "rate", GST_TYPE_INT_RANGE, OPENAL_MIN_RATE,
            OPENAL_MAX_RATE, "channels", G_TYPE_INT, chans[i].count, NULL);
        if (chans[i].count > 2) {
          gst_audio_channel_positions_to_mask (chans[i].positions,
              chans[i].count, FALSE, &channel_mask);
          gst_structure_set (structure, "channel-mask", GST_TYPE_BITMASK,
              channel_mask, NULL);
        }
        gst_caps_append_structure (caps, structure);
      }
    }

    for (i = 0; fmt16[i]; i++) {
      ALenum value = alGetEnumValue (fmt16[i]);
      if (checkALError () != AL_NO_ERROR || value == 0 || value == -1)
        continue;

      structure =
          gst_structure_new ("audio/x-raw", "format", G_TYPE_STRING,
          GST_AUDIO_NE (S16), "rate", GST_TYPE_INT_RANGE, OPENAL_MIN_RATE,
          OPENAL_MAX_RATE, "channels", G_TYPE_INT, chans[i].count, NULL);
      if (chans[i].count > 2) {
        gst_audio_channel_positions_to_mask (chans[i].positions, chans[i].count,
            FALSE, &channel_mask);
        gst_structure_set (structure, "channel-mask", GST_TYPE_BITMASK,
            channel_mask, NULL);
      }
      gst_caps_append_structure (caps, structure);
    }
    for (i = 0; fmt8[i]; i++) {
      ALenum value = alGetEnumValue (fmt8[i]);
      if (checkALError () != AL_NO_ERROR || value == 0 || value == -1)
        continue;

      structure =
          gst_structure_new ("audio/x-raw", "format", G_TYPE_STRING,
          G_STRINGIFY (U8), "rate", GST_TYPE_INT_RANGE, OPENAL_MIN_RATE,
          OPENAL_MAX_RATE, "channels", G_TYPE_INT, chans[i].count, NULL);
      if (chans[i].count > 2) {
        gst_audio_channel_positions_to_mask (chans[i].positions, chans[i].count,
            FALSE, &channel_mask);
        gst_structure_set (structure, "channel-mask", GST_TYPE_BITMASK,
            channel_mask, NULL);
      }
      gst_caps_append_structure (caps, structure);
    }
  } else {
    if (alIsExtensionPresent ("AL_EXT_FLOAT32")) {
      structure =
          gst_structure_new ("audio/x-raw", "format", G_TYPE_STRING,
          GST_AUDIO_NE (F32), "rate", GST_TYPE_INT_RANGE, OPENAL_MIN_RATE,
          OPENAL_MAX_RATE, "channels", GST_TYPE_INT_RANGE, 1, 2, NULL);
      gst_caps_append_structure (caps, structure);
    }

    structure =
        gst_structure_new ("audio/x-raw", "format", G_TYPE_STRING,
        GST_AUDIO_NE (S16), "rate", GST_TYPE_INT_RANGE, OPENAL_MIN_RATE,
        OPENAL_MAX_RATE, "channels", GST_TYPE_INT_RANGE, 1, 2, NULL);
    gst_caps_append_structure (caps, structure);

    structure =
        gst_structure_new ("audio/x-raw", "format", G_TYPE_STRING,
        G_STRINGIFY (U8), "rate", GST_TYPE_INT_RANGE, OPENAL_MIN_RATE,
        OPENAL_MAX_RATE, "channels", GST_TYPE_INT_RANGE, 1, 2, NULL);
    gst_caps_append_structure (caps, structure);
  }

  if (alIsExtensionPresent ("AL_EXT_double")) {
    structure =
        gst_structure_new ("audio/x-raw", "format", G_TYPE_STRING,
        GST_AUDIO_NE (F64), "rate", GST_TYPE_INT_RANGE, OPENAL_MIN_RATE,
        OPENAL_MAX_RATE, "channels", GST_TYPE_INT_RANGE, 1, 2, NULL);
    gst_caps_append_structure (caps, structure);
  }

  if (alIsExtensionPresent ("AL_EXT_IMA4")) {
    structure =
        gst_structure_new ("audio/x-adpcm", "layout", G_TYPE_STRING, "ima",
        "rate", GST_TYPE_INT_RANGE, OPENAL_MIN_RATE, OPENAL_MAX_RATE,
        "channels", GST_TYPE_INT_RANGE, 1, 2, NULL);
    gst_caps_append_structure (caps, structure);
  }

  if (alIsExtensionPresent ("AL_EXT_ALAW")) {
    structure =
        gst_structure_new ("audio/x-alaw", "rate", GST_TYPE_INT_RANGE,
        OPENAL_MIN_RATE, OPENAL_MAX_RATE, "channels", GST_TYPE_INT_RANGE, 1, 2,
        NULL);
    gst_caps_append_structure (caps, structure);
  }

  if (alIsExtensionPresent ("AL_EXT_MULAW_MCFORMATS")) {
    const char *fmtmulaw[] = {
      "AL_FORMAT_MONO_MULAW",
      "AL_FORMAT_STEREO_MULAW",
      "AL_FORMAT_QUAD_MULAW",
      "AL_FORMAT_51CHN_MULAW",
      "AL_FORMAT_61CHN_MULAW",
      "AL_FORMAT_71CHN_MULAW",
      NULL
    };
    int i;

    for (i = 0; fmtmulaw[i]; i++) {
      ALenum value = alGetEnumValue (fmtmulaw[i]);
      if (checkALError () != AL_NO_ERROR || value == 0 || value == -1)
        continue;

      structure =
          gst_structure_new ("audio/x-mulaw", "rate", GST_TYPE_INT_RANGE,
          OPENAL_MIN_RATE, OPENAL_MAX_RATE, "channels", G_TYPE_INT,
          chans[i].count, NULL);
      if (chans[i].count > 2) {
        gst_audio_channel_positions_to_mask (chans[i].positions, chans[i].count,
            FALSE, &channel_mask);
        gst_structure_set (structure, "channel-mask", GST_TYPE_BITMASK,
            channel_mask, NULL);
      }
      gst_caps_append_structure (caps, structure);
    }
  } else if (alIsExtensionPresent ("AL_EXT_MULAW")) {
    structure =
        gst_structure_new ("audio/x-mulaw", "rate", GST_TYPE_INT_RANGE,
        OPENAL_MIN_RATE, OPENAL_MAX_RATE, "channels", GST_TYPE_INT_RANGE, 1, 2,
        NULL);
    gst_caps_append_structure (caps, structure);
  }

  popContext (old, context);

  return caps;
}

static GstCaps *
gst_openal_sink_getcaps (GstBaseSink * basesink, GstCaps * filter)
{
  GstOpenALSink *sink = GST_OPENAL_SINK (basesink);
  GstCaps *caps;

  if (sink->default_device == NULL) {
    GstPad *pad = GST_BASE_SINK_PAD (basesink);
    GstCaps *tcaps = gst_pad_get_pad_template_caps (pad);
    caps = gst_caps_copy (tcaps);
    gst_caps_unref (tcaps);
  } else if (sink->probed_caps)
    caps = gst_caps_copy (sink->probed_caps);
  else {
    if (sink->default_context)
      caps = gst_openal_helper_probe_caps (sink->default_context);
    else if (sink->user_context)
      caps = gst_openal_helper_probe_caps (sink->user_context);
    else {
      ALCcontext *context = alcCreateContext (sink->default_device, NULL);
      if (context) {
        caps = gst_openal_helper_probe_caps (context);
        alcDestroyContext (context);
      } else {
        GST_ELEMENT_WARNING (sink, RESOURCE, FAILED,
            ("Could not create temporary context."),
            GST_ALC_ERROR (sink->default_device));
        caps = NULL;
      }
    }

    if (caps && !gst_caps_is_empty (caps))
      sink->probed_caps = gst_caps_copy (caps);
  }

  if (filter) {
    GstCaps *intersection;

    intersection =
        gst_caps_intersect_full (filter, caps, GST_CAPS_INTERSECT_FIRST);
    return intersection;
  } else {
    return caps;
  }
}

static gboolean
gst_openal_sink_open (GstAudioSink * audiosink)
{
  GstOpenALSink *sink = GST_OPENAL_SINK (audiosink);

  if (sink->user_device) {
    ALCint value = -1;
    alcGetIntegerv (sink->user_device, ALC_ATTRIBUTES_SIZE, 1, &value);
    if (value > 0) {
      if (!sink->user_context
          || alcGetContextsDevice (sink->user_context) == sink->user_device)
        sink->default_device = sink->user_device;
    }
  } else if (sink->user_context)
    sink->default_device = alcGetContextsDevice (sink->user_context);
  else
    sink->default_device = alcOpenDevice (sink->device_name);
  if (!sink->default_device) {
    GST_ELEMENT_ERROR (sink, RESOURCE, OPEN_WRITE,
        ("Could not open device."), GST_ALC_ERROR (sink->default_device));
    return FALSE;
  }

  return TRUE;
}

static gboolean
gst_openal_sink_close (GstAudioSink * audiosink)
{
  GstOpenALSink *sink = GST_OPENAL_SINK (audiosink);

  if (!sink->user_device && !sink->user_context) {
    if (alcCloseDevice (sink->default_device) == ALC_FALSE) {
      GST_ELEMENT_ERROR (sink, RESOURCE, CLOSE,
          ("Could not close device."), GST_ALC_ERROR (sink->default_device));
      return FALSE;
    }
  }
  sink->default_device = NULL;

  if (sink->probed_caps)
    gst_caps_unref (sink->probed_caps);
  sink->probed_caps = NULL;

  return TRUE;
}

static void
gst_openal_sink_parse_spec (GstOpenALSink * sink,
    const GstAudioRingBufferSpec * spec)
{
  ALuint format = AL_NONE;

  GST_DEBUG_OBJECT (sink,
      "looking up format for type %d, gst-format %d, and %d channels",
      spec->type, GST_AUDIO_INFO_FORMAT (&spec->info),
      GST_AUDIO_INFO_CHANNELS (&spec->info));

  /* Don't need to verify supported formats, since the probed caps will only
   * report what was detected and we shouldn't get anything different */
  switch (spec->type) {
    case GST_AUDIO_RING_BUFFER_FORMAT_TYPE_RAW:
      switch (GST_AUDIO_INFO_FORMAT (&spec->info)) {
        case GST_AUDIO_FORMAT_U8:
          switch (GST_AUDIO_INFO_CHANNELS (&spec->info)) {
            case 1:
              format = AL_FORMAT_MONO8;
              break;
            case 2:
              format = AL_FORMAT_STEREO8;
              break;
            case 4:
              format = AL_FORMAT_QUAD8;
              break;
            case 6:
              format = AL_FORMAT_51CHN8;
              break;
            case 7:
              format = AL_FORMAT_61CHN8;
              break;
            case 8:
              format = AL_FORMAT_71CHN8;
              break;
            default:
              break;
          }
          break;

        case GST_AUDIO_FORMAT_S16:
          switch (GST_AUDIO_INFO_CHANNELS (&spec->info)) {
            case 1:
              format = AL_FORMAT_MONO16;
              break;
            case 2:
              format = AL_FORMAT_STEREO16;
              break;
            case 4:
              format = AL_FORMAT_QUAD16;
              break;
            case 6:
              format = AL_FORMAT_51CHN16;
              break;
            case 7:
              format = AL_FORMAT_61CHN16;
              break;
            case 8:
              format = AL_FORMAT_71CHN16;
              break;
            default:
              break;
          }
          break;

        case GST_AUDIO_FORMAT_F32:
          switch (GST_AUDIO_INFO_CHANNELS (&spec->info)) {
            case 1:
              format = AL_FORMAT_MONO_FLOAT32;
              break;
            case 2:
              format = AL_FORMAT_STEREO_FLOAT32;
              break;
            case 4:
              format = AL_FORMAT_QUAD32;
              break;
            case 6:
              format = AL_FORMAT_51CHN32;
              break;
            case 7:
              format = AL_FORMAT_61CHN32;
              break;
            case 8:
              format = AL_FORMAT_71CHN32;
              break;
            default:
              break;
          }
          break;

        case GST_AUDIO_FORMAT_F64:
          switch (GST_AUDIO_INFO_CHANNELS (&spec->info)) {
            case 1:
              format = AL_FORMAT_MONO_DOUBLE_EXT;
              break;
            case 2:
              format = AL_FORMAT_STEREO_DOUBLE_EXT;
              break;
            default:
              break;
          }
          break;
        default:
          break;
      }
      break;

    case GST_AUDIO_RING_BUFFER_FORMAT_TYPE_IMA_ADPCM:
      switch (GST_AUDIO_INFO_CHANNELS (&spec->info)) {
        case 1:
          format = AL_FORMAT_MONO_IMA4;
          break;
        case 2:
          format = AL_FORMAT_STEREO_IMA4;
          break;
        default:
          break;
      }
      break;

    case GST_AUDIO_RING_BUFFER_FORMAT_TYPE_A_LAW:
      switch (GST_AUDIO_INFO_CHANNELS (&spec->info)) {
        case 1:
          format = AL_FORMAT_MONO_ALAW_EXT;
          break;
        case 2:
          format = AL_FORMAT_STEREO_ALAW_EXT;
          break;
        default:
          break;
      }
      break;

    case GST_AUDIO_RING_BUFFER_FORMAT_TYPE_MU_LAW:
      switch (GST_AUDIO_INFO_CHANNELS (&spec->info)) {
        case 1:
          format = AL_FORMAT_MONO_MULAW;
          break;
        case 2:
          format = AL_FORMAT_STEREO_MULAW;
          break;
        case 4:
          format = AL_FORMAT_QUAD_MULAW;
          break;
        case 6:
          format = AL_FORMAT_51CHN_MULAW;
          break;
        case 7:
          format = AL_FORMAT_61CHN_MULAW;
          break;
        case 8:
          format = AL_FORMAT_71CHN_MULAW;
          break;
        default:
          break;
      }
      break;

    default:
      break;
  }

  sink->bytes_per_sample = GST_AUDIO_INFO_BPS (&spec->info);
  sink->rate = GST_AUDIO_INFO_RATE (&spec->info);
  sink->channels = GST_AUDIO_INFO_CHANNELS (&spec->info);
  sink->format = format;
  sink->buffer_count = spec->segtotal;
  sink->buffer_length = spec->segsize;
}

static gboolean
gst_openal_sink_prepare (GstAudioSink * audiosink,
    GstAudioRingBufferSpec * spec)
{
  GstOpenALSink *sink = GST_OPENAL_SINK (audiosink);
  ALCcontext *context, *old;

  if (sink->default_context && !gst_openal_sink_unprepare (audiosink))
    return FALSE;

  if (sink->user_context)
    context = sink->user_context;
  else {
    ALCint attribs[3] = { 0, 0, 0 };

    /* Don't try to change the playback frequency of an app's device */
    if (!sink->user_device) {
      attribs[0] = ALC_FREQUENCY;
      attribs[1] = GST_AUDIO_INFO_RATE (&spec->info);
      attribs[2] = 0;
    }

    context = alcCreateContext (sink->default_device, attribs);
    if (!context) {
      GST_ELEMENT_ERROR (sink, RESOURCE, FAILED,
          ("Unable to prepare device."), GST_ALC_ERROR (sink->default_device));
      return FALSE;
    }
  }

  old = pushContext (context);

  if (sink->user_source) {
    if (!sink->user_context || !alIsSource (sink->user_source)) {
      GST_ELEMENT_ERROR (sink, RESOURCE, NOT_FOUND, (NULL),
          ("Invalid source specified for context"));
      goto fail;
    }
    sink->default_source = sink->user_source;
  } else {
    ALuint source;

    alGenSources (1, &source);
    if (checkALError () != AL_NO_ERROR) {
      GST_ELEMENT_ERROR (sink, RESOURCE, NO_SPACE_LEFT, (NULL),
          ("Unable to generate source"));
      goto fail;
    }
    sink->default_source = source;
  }

  gst_openal_sink_parse_spec (sink, spec);
  if (sink->format == AL_NONE) {
    GST_ELEMENT_ERROR (sink, RESOURCE, SETTINGS, (NULL),
        ("Unable to get type %d, format %d, and %d channels", spec->type,
            GST_AUDIO_INFO_FORMAT (&spec->info),
            GST_AUDIO_INFO_CHANNELS (&spec->info)));
    goto fail;
  }

  sink->buffers = g_malloc (sink->buffer_count * sizeof (*sink->buffers));
  if (!sink->buffers) {
    GST_ELEMENT_ERROR (sink, RESOURCE, FAILED, ("Out of memory."),
        ("Unable to allocate buffers"));
    goto fail;
  }

  alGenBuffers (sink->buffer_count, sink->buffers);
  if (checkALError () != AL_NO_ERROR) {
    GST_ELEMENT_ERROR (sink, RESOURCE, NO_SPACE_LEFT, (NULL),
        ("Unable to generate %d buffers", sink->buffer_count));
    goto fail;
  }
  sink->buffer_idx = 0;

  popContext (old, context);
  sink->default_context = context;
  return TRUE;

fail:
  if (!sink->user_source && sink->default_source)
    alDeleteSources (1, &sink->default_source);
  sink->default_source = 0;

  g_free (sink->buffers);
  sink->buffers = NULL;
  sink->buffer_count = 0;
  sink->buffer_length = 0;

  popContext (old, context);
  if (!sink->user_context)
    alcDestroyContext (context);
  return FALSE;
}

static gboolean
gst_openal_sink_unprepare (GstAudioSink * audiosink)
{
  GstOpenALSink *sink = GST_OPENAL_SINK (audiosink);
  ALCcontext *old;

  if (!sink->default_context)
    return TRUE;

  old = pushContext (sink->default_context);

  alSourceStop (sink->default_source);
  alSourcei (sink->default_source, AL_BUFFER, 0);

  if (!sink->user_source)
    alDeleteSources (1, &sink->default_source);
  sink->default_source = 0;

  alDeleteBuffers (sink->buffer_count, sink->buffers);
  g_free (sink->buffers);
  sink->buffers = NULL;
  sink->buffer_idx = 0;
  sink->buffer_count = 0;
  sink->buffer_length = 0;

  checkALError ();
  popContext (old, sink->default_context);
  if (!sink->user_context)
    alcDestroyContext (sink->default_context);
  sink->default_context = NULL;

  return TRUE;
}

static gint
gst_openal_sink_write (GstAudioSink * audiosink, gpointer data, guint length)
{
  GstOpenALSink *sink = GST_OPENAL_SINK (audiosink);
  ALint processed, queued, state;
  ALCcontext *old;
  gulong rest_us;

  g_assert (length == sink->buffer_length);

  old = pushContext (sink->default_context);

  rest_us =
      (guint64) (sink->buffer_length / sink->bytes_per_sample) *
      G_USEC_PER_SEC / sink->rate / sink->channels;
  do {
    alGetSourcei (sink->default_source, AL_SOURCE_STATE, &state);
    alGetSourcei (sink->default_source, AL_BUFFERS_QUEUED, &queued);
    alGetSourcei (sink->default_source, AL_BUFFERS_PROCESSED, &processed);
    if (checkALError () != AL_NO_ERROR) {
      GST_ELEMENT_ERROR (sink, RESOURCE, WRITE, (NULL),
          ("Source state error detected"));
      length = 0;
      goto out_nolock;
    }

    if (processed > 0 || queued < sink->buffer_count)
      break;
    if (state != AL_PLAYING)
      alSourcePlay (sink->default_source);
    g_usleep (rest_us);
  }
  while (1);

  GST_OPENAL_SINK_LOCK (sink);
  if (sink->write_reset != AL_FALSE) {
    sink->write_reset = AL_FALSE;
    length = 0;
    goto out;
  }

  queued -= processed;
  while (processed-- > 0) {
    ALuint bid;
    alSourceUnqueueBuffers (sink->default_source, 1, &bid);
  }
  if (state == AL_STOPPED) {
    /* "Restore" from underruns (not actually needed, but it keeps delay
     * calculations correct while rebuffering) */
    alSourceRewind (sink->default_source);
  }

  alBufferData (sink->buffers[sink->buffer_idx], sink->format,
      data, sink->buffer_length, sink->rate);
  alSourceQueueBuffers (sink->default_source, 1,
      &sink->buffers[sink->buffer_idx]);
  sink->buffer_idx = (sink->buffer_idx + 1) % sink->buffer_count;
  queued++;

  if (state != AL_PLAYING && queued == sink->buffer_count)
    alSourcePlay (sink->default_source);

  if (checkALError () != AL_NO_ERROR) {
    GST_ELEMENT_ERROR (sink, RESOURCE, WRITE, (NULL),
        ("Source queue error detected"));
    goto out;
  }

out:
  GST_OPENAL_SINK_UNLOCK (sink);
out_nolock:
  popContext (old, sink->default_context);
  return length;
}

static guint
gst_openal_sink_delay (GstAudioSink * audiosink)
{
  GstOpenALSink *sink = GST_OPENAL_SINK (audiosink);
  ALint queued, state, offset, delay;
  ALCcontext *old;

  if (!sink->default_context)
    return 0;

  GST_OPENAL_SINK_LOCK (sink);
  old = pushContext (sink->default_context);

  delay = 0;
  alGetSourcei (sink->default_source, AL_BUFFERS_QUEUED, &queued);
  /* Order here is important. If the offset is queried after the state and an
   * underrun occurs in between the two calls, it can end up with a 0 offset
   * in a playing state, incorrectly reporting a len*queued/bps delay. */
  alGetSourcei (sink->default_source, AL_BYTE_OFFSET, &offset);
  alGetSourcei (sink->default_source, AL_SOURCE_STATE, &state);

  /* Note: state=stopped is an underrun, meaning all buffers are processed
   * and there's no delay when writing the next buffer. Pre-buffering is
   * state=initial, which will introduce a delay while writing. */
  if (checkALError () == AL_NO_ERROR && state != AL_STOPPED)
    delay =
        ((queued * sink->buffer_length) -
        offset) / sink->bytes_per_sample / sink->channels / GST_MSECOND;

  popContext (old, sink->default_context);
  GST_OPENAL_SINK_UNLOCK (sink);

  if (G_UNLIKELY (delay < 0)) {
    /* make sure we never return a negative delay */
    GST_WARNING_OBJECT (openal_debug, "negative delay");
    delay = 0;
  }

  return delay;
}

static void
gst_openal_sink_reset (GstAudioSink * audiosink)
{
  GstOpenALSink *sink = GST_OPENAL_SINK (audiosink);
  ALCcontext *old;

  GST_OPENAL_SINK_LOCK (sink);
  old = pushContext (sink->default_context);

  sink->write_reset = AL_TRUE;
  alSourceStop (sink->default_source);
  alSourceRewind (sink->default_source);
  alSourcei (sink->default_source, AL_BUFFER, 0);
  checkALError ();

  popContext (old, sink->default_context);
  GST_OPENAL_SINK_UNLOCK (sink);
}
