/*****************************************************************************
 * output.c : internal management of output streams for the audio output
 *****************************************************************************
 * Copyright (C) 2002-2004 VLC authors and VideoLAN
 *
 * Authors: Christophe Massiot <massiot@via.ecp.fr>
 *
 * This program 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 program 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 program; if not, write to the Free Software Foundation,
 * Inc., 51 Franklin Street, Fifth Floor, Boston MA 02110-1301, USA.
 *****************************************************************************/

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

#include <stdlib.h>
#include <assert.h>

#include <vlc_common.h>
#include <vlc_configuration.h>
#include <vlc_aout.h>
#include <vlc_modules.h>
#include <vlc_atomic.h>

#include "../libvlc.h"
#include "aout_internal.h"

typedef struct aout_dev
{
    struct vlc_list node;
    char *name;
    char id[1];
} aout_dev_t;


/* Local functions */

static inline float clampf(const float value, const float min, const float max)
{
    if (value < min)
        return min;
    else if (value > max)
        return max;
    else
        return value;
}

static int var_Copy (vlc_object_t *src, const char *name, vlc_value_t prev,
                     vlc_value_t value, void *data)
{
    vlc_object_t *dst = data;

    (void) src; (void) prev;
    return var_Set (dst, name, value);
}

static int var_CopyDevice (vlc_object_t *src, const char *name,
                           vlc_value_t prev, vlc_value_t value, void *data)
{
    vlc_object_t *dst = data;

    (void) src; (void) name; (void) prev;
    return var_Set (dst, "audio-device", value);
}

static void aout_TimingNotify(audio_output_t *aout, vlc_tick_t system_ts,
                              vlc_tick_t audio_ts)
{
    aout_owner_t *owner = aout_owner (aout);
    assert(owner->main_stream);
    vlc_aout_stream_NotifyTiming(owner->main_stream, system_ts, audio_ts);
}

static void aout_DrainedNotify(audio_output_t *aout)
{
    aout_owner_t *owner = aout_owner (aout);
    assert(owner->main_stream);
    vlc_aout_stream_NotifyDrained(owner->main_stream);
}

/**
 * Supply or update the current custom ("hardware") volume.
 *
 * @param aout the audio output notifying the new volume
 * @param volume current custom volume
 *
 * @warning The caller (i.e. the audio output plug-in) is responsible for
 * interlocking and synchronizing call to this function and to the
 * audio_output_t.volume_set callback. This ensures that VLC gets correct
 * volume information (possibly with a latency).
 */
static void aout_VolumeNotify (audio_output_t *aout, float volume)
{
    var_SetFloat (aout, "volume", volume);
}

static void aout_MuteNotify (audio_output_t *aout, bool mute)
{
    var_SetBool (aout, "mute", mute);
}

static void aout_PolicyNotify (audio_output_t *aout, bool cork)
{
    (cork ? var_IncInteger : var_DecInteger)(vlc_object_parent(aout), "corks");
}

static void aout_DeviceNotify (audio_output_t *aout, const char *id)
{
    var_SetString (aout, "device", (id != NULL) ? id : "");
}

static void aout_HotplugNotify (audio_output_t *aout,
                                const char *id, const char *name)
{
    aout_owner_t *owner = aout_owner (aout);
    aout_dev_t *dev = NULL, *p;

    vlc_mutex_lock (&owner->dev.lock);
    vlc_list_foreach(p, &owner->dev.list, node)
    {
        if (!strcmp (id, p->id))
        {
            dev = p;
            break;
        }
    }

    if (name != NULL)
    {
        if (dev == NULL) /* Added device */
        {
            dev = malloc (sizeof (*dev) + strlen (id));
            if (unlikely(dev == NULL))
                goto out;
            strcpy (dev->id, id);
            vlc_list_append(&dev->node, &owner->dev.list);
            owner->dev.count++;
        }
        else /* Modified device */
            free (dev->name);
        dev->name = strdup (name);
    }
    else
    {
        if (dev != NULL) /* Removed device */
        {
            owner->dev.count--;
            vlc_list_remove(&dev->node);
            free (dev->name);
            free (dev);
        }
    }
out:
    vlc_mutex_unlock (&owner->dev.lock);
}

static void aout_RestartNotify (audio_output_t *aout, bool restart_dec)
{
    aout_owner_t *owner = aout_owner (aout);
    if (owner->main_stream)
    {
        unsigned mode = restart_dec ? AOUT_RESTART_OUTPUT_DEC : AOUT_RESTART_OUTPUT;
        vlc_aout_stream_RequestRestart(owner->main_stream, mode);
    }
}

void aout_InputRequestRestart(audio_output_t *aout)
{
    aout_owner_t *owner = aout_owner (aout);
    if (owner->main_stream)
        vlc_aout_stream_RequestRestart(owner->main_stream, AOUT_RESTART_FILTERS);
}

static int aout_GainNotify (audio_output_t *aout, float gain)
{
    aout_owner_t *owner = aout_owner (aout);

    vlc_mutex_assert(&owner->lock);
    /* XXX: ideally, return -1 if format cannot be amplified */
    if (owner->main_stream != NULL)
        vlc_aout_stream_NotifyGain(owner->main_stream, gain);
    return 0;
}

static const struct vlc_audio_output_events aout_events = {
    aout_TimingNotify,
    aout_DrainedNotify,
    aout_VolumeNotify,
    aout_MuteNotify,
    aout_PolicyNotify,
    aout_DeviceNotify,
    aout_HotplugNotify,
    aout_RestartNotify,
    aout_GainNotify,
};

static int FilterCallback (vlc_object_t *obj, const char *var,
                           vlc_value_t prev, vlc_value_t cur, void *data)
{
    if (strcmp(prev.psz_string, cur.psz_string))
        aout_InputRequestRestart ((audio_output_t *)obj);
    (void) var; (void) data;
    return VLC_SUCCESS;
}

static int StereoModeCallback (vlc_object_t *obj, const char *varname,
                               vlc_value_t oldval, vlc_value_t newval, void *data)
{
    audio_output_t *aout = (audio_output_t *)obj;
    (void)varname; (void)oldval; (void)newval; (void)data;

    aout_owner_t *owner = aout_owner (aout);
    vlc_mutex_lock (&owner->lock);
    owner->requested_stereo_mode = newval.i_int;
    vlc_mutex_unlock (&owner->lock);

    if (owner->main_stream)
        vlc_aout_stream_RequestRestart(owner->main_stream, AOUT_RESTART_OUTPUT);
    return 0;
}

static int MixModeCallback (vlc_object_t *obj, const char *varname,
                               vlc_value_t oldval, vlc_value_t newval, void *data)
{
    audio_output_t *aout = (audio_output_t *)obj;
    (void)varname; (void)oldval; (void)newval; (void)data;

    aout_owner_t *owner = aout_owner (aout);
    vlc_mutex_lock (&owner->lock);
    owner->requested_mix_mode = newval.i_int;
    vlc_mutex_unlock (&owner->lock);

    if (owner->main_stream)
        vlc_aout_stream_RequestRestart(owner->main_stream, AOUT_RESTART_OUTPUT);
    return 0;
}

static void aout_ChangeViewpoint(audio_output_t *, const vlc_viewpoint_t *);

static int ViewpointCallback (vlc_object_t *obj, const char *var,
                              vlc_value_t prev, vlc_value_t cur, void *data)
{
    if( cur.p_address != NULL )
        aout_ChangeViewpoint((audio_output_t *)obj, cur.p_address );
    (void) var; (void) data; (void) prev;
    return VLC_SUCCESS;
}

#undef aout_New
/**
 * Creates an audio output object and initializes an output module.
 */
audio_output_t *aout_New (vlc_object_t *parent)
{
    vlc_value_t val;

    audio_output_t *aout = vlc_custom_create (parent, sizeof (aout_instance_t),
                                              "audio output");
    if (unlikely(aout == NULL))
        return NULL;

    aout_owner_t *owner = aout_owner (aout);

    vlc_mutex_init (&owner->lock);
    vlc_mutex_init (&owner->dev.lock);
    vlc_mutex_init (&owner->vp.lock);
    vlc_viewpoint_init (&owner->vp.value);
    vlc_list_init(&owner->dev.list);
    atomic_init (&owner->vp.update, false);
    vlc_atomic_rc_init(&owner->rc);
    vlc_audio_meter_Init(&owner->meter, aout);

    owner->main_stream = NULL;

    /* Audio output module callbacks */
    var_Create (aout, "volume", VLC_VAR_FLOAT);
    var_SetFloat(aout, "volume", -1.f);
    var_AddCallback (aout, "volume", var_Copy, parent);
    var_Create (aout, "mute", VLC_VAR_BOOL);
    var_AddCallback (aout, "mute", var_Copy, parent);
    var_Create (aout, "device", VLC_VAR_STRING);
    var_AddCallback (aout, "device", var_CopyDevice, parent);

    aout->events = &aout_events;

    /* Audio output module initialization */
    aout->start = NULL;
    aout->stop = NULL;
    aout->volume_set = NULL;
    aout->mute_set = NULL;
    aout->device_select = NULL;
    owner->module = module_need_var(aout, "audio output", "aout");
    if (owner->module == NULL)
    {
        msg_Err (aout, "no suitable audio output module");
        vlc_object_delete(aout);
        return NULL;
    }
    assert(aout->start && aout->stop);

    /*
     * Persistent audio output variables
     */
    module_config_t *cfg;
    char *str;

    /* Visualizations */
    var_Create (aout, "visual", VLC_VAR_STRING);
    var_Change(aout, "visual", VLC_VAR_SETTEXT, _("Visualizations"));
    val.psz_string = (char *)"";
    var_Change(aout, "visual", VLC_VAR_ADDCHOICE, val, _("Disable"));
    val.psz_string = (char *)"spectrometer";
    var_Change(aout, "visual", VLC_VAR_ADDCHOICE, val, _("Spectrometer"));
    val.psz_string = (char *)"scope";
    var_Change(aout, "visual", VLC_VAR_ADDCHOICE, val, _("Scope"));
    val.psz_string = (char *)"spectrum";
    var_Change(aout, "visual", VLC_VAR_ADDCHOICE, val, _("Spectrum"));
    val.psz_string = (char *)"vuMeter";
    var_Change(aout, "visual", VLC_VAR_ADDCHOICE, val, _("VU meter"));
    /* Look for goom plugin */
    if (module_exists ("goom"))
    {
        val.psz_string = (char *)"goom";
        var_Change(aout, "visual", VLC_VAR_ADDCHOICE, val, "Goom");
    }
    /* Look for libprojectM plugin */
    if (module_exists ("projectm"))
    {
        val.psz_string = (char *)"projectm";
        var_Change(aout, "visual", VLC_VAR_ADDCHOICE, val, "projectM");
    }
    /* Look for VSXu plugin */
    if (module_exists ("vsxu"))
    {
        val.psz_string = (char *)"vsxu";
        var_Change(aout, "visual", VLC_VAR_ADDCHOICE, val, "Vovoid VSXU");
    }
    /* Look for glspectrum plugin */
    if (module_exists ("glspectrum"))
    {
        val.psz_string = (char *)"glspectrum";
        var_Change(aout, "visual", VLC_VAR_ADDCHOICE, val, "3D spectrum");
    }
    str = var_GetNonEmptyString (aout, "effect-list");
    if (str != NULL)
    {
        var_SetString (aout, "visual", str);
        free (str);
    }

    var_Create (aout, "audio-filter", VLC_VAR_STRING | VLC_VAR_DOINHERIT);
    var_AddCallback (aout, "audio-filter", FilterCallback, NULL);
    var_Change(aout, "audio-filter", VLC_VAR_SETTEXT, _("Audio filters"));

    var_Create (aout, "viewpoint", VLC_VAR_ADDRESS );
    var_AddCallback (aout, "viewpoint", ViewpointCallback, NULL);

    var_Create (aout, "audio-visual", VLC_VAR_STRING | VLC_VAR_DOINHERIT);
    var_Change(aout, "audio-visual", VLC_VAR_SETTEXT,
               _("Audio visualizations"));

    /* Replay gain */
    var_Create (aout, "audio-replay-gain-mode",
                VLC_VAR_STRING | VLC_VAR_DOINHERIT );
    var_Change(aout, "audio-replay-gain-mode", VLC_VAR_SETTEXT,
               _("Replay gain"));
    cfg = config_FindConfig("audio-replay-gain-mode");
    if (likely(cfg != NULL))
        for (unsigned i = 0; i < cfg->list_count; i++)
        {
            val.psz_string = (char *)cfg->list.psz[i];
            var_Change(aout, "audio-replay-gain-mode", VLC_VAR_ADDCHOICE,
                       val, vlc_gettext(cfg->list_text[i]));
        }

    /* Stereo mode */
    var_Create (aout, "stereo-mode", VLC_VAR_INTEGER | VLC_VAR_DOINHERIT);
    owner->requested_stereo_mode = var_GetInteger (aout, "stereo-mode");

    var_AddCallback (aout, "stereo-mode", StereoModeCallback, NULL);
    var_Change(aout, "stereo-mode", VLC_VAR_SETTEXT, _("Stereo audio mode"));

    /* Mix mode */
    var_Create (aout, "mix-mode", VLC_VAR_INTEGER | VLC_VAR_DOINHERIT);
    owner->requested_mix_mode = var_GetInteger (aout, "mix-mode");
    var_AddCallback (aout, "mix-mode", MixModeCallback, NULL);
    var_Change(aout, "mix-mode", VLC_VAR_SETTEXT, _("Audio mix mode"));

    /* Equalizer */
    int doinherit = module_exists("equalizer") ? VLC_VAR_DOINHERIT : 0;
    var_Create (aout, "equalizer-preamp", VLC_VAR_FLOAT | doinherit);
    var_Create (aout, "equalizer-bands", VLC_VAR_STRING | doinherit);
    var_Create (aout, "equalizer-preset", VLC_VAR_STRING | doinherit);

    owner->bitexact = var_InheritBool (aout, "audio-bitexact");

    return aout;
}

audio_output_t *aout_Hold(audio_output_t *aout)
{
    aout_owner_t *owner = aout_owner(aout);

    vlc_atomic_rc_inc(&owner->rc);
    return aout;
}

/**
 * Deinitializes an audio output module and destroys an audio output object.
 */
static void aout_Destroy (audio_output_t *aout)
{
    aout_owner_t *owner = aout_owner (aout);

    vlc_mutex_lock(&owner->lock);
    module_unneed (aout, owner->module);
    /* Protect against late call from intf.c */
    aout->volume_set = NULL;
    aout->mute_set = NULL;
    aout->device_select = NULL;
    vlc_audio_meter_Destroy(&owner->meter);
    vlc_mutex_unlock(&owner->lock);

    var_DelCallback (aout, "viewpoint", ViewpointCallback, NULL);
    var_DelCallback (aout, "audio-filter", FilterCallback, NULL);
    var_DelCallback(aout, "device", var_CopyDevice, vlc_object_parent(aout));
    var_DelCallback(aout, "mute", var_Copy, vlc_object_parent(aout));
    var_SetFloat (aout, "volume", -1.f);
    var_DelCallback(aout, "volume", var_Copy, vlc_object_parent(aout));
    var_DelCallback (aout, "stereo-mode", StereoModeCallback, NULL);
    var_DelCallback (aout, "mix-mode", MixModeCallback, NULL);

    aout_dev_t *dev;
    vlc_list_foreach(dev, &owner->dev.list, node)
    {
        vlc_list_remove(&dev->node);
        free (dev->name);
        free (dev);
    }

    vlc_object_delete(VLC_OBJECT(aout));
}

void aout_Release(audio_output_t *aout)
{
    aout_owner_t *owner = aout_owner(aout);

    if (!vlc_atomic_rc_dec(&owner->rc))
        return;

    aout_Destroy(aout);
}

static int aout_PrepareStereoMode(audio_output_t *aout,
                                  const audio_sample_format_t *restrict fmt)
{
    aout_owner_t *owner = aout_owner (aout);

    /* Fill Stereo mode choices */
    vlc_value_t val;
    const char *txt;
    val.i_int = 0;

    if (!AOUT_FMT_LINEAR(fmt) || fmt->i_channels != 2)
        return AOUT_VAR_CHAN_UNSET;

    int i_default_mode = owner->requested_stereo_mode;

    val.i_int = AOUT_VAR_CHAN_MONO;
    var_Change(aout, "stereo-mode", VLC_VAR_ADDCHOICE, val, _("Mono"));

    if (fmt->i_chan_mode & AOUT_CHANMODE_DOLBYSTEREO)
    {
        val.i_int = AOUT_VAR_CHAN_DOLBYS;
        txt = _("Dolby Surround");
    }
    else
    {
        val.i_int = AOUT_VAR_CHAN_STEREO;
        txt = _("Stereo");
    }
    var_Change(aout, "stereo-mode", VLC_VAR_ADDCHOICE, val, txt);

    if (fmt->i_chan_mode & AOUT_CHANMODE_DUALMONO)
        i_default_mode = AOUT_VAR_CHAN_LEFT;
    else
        i_default_mode = val.i_int; /* Stereo or Dolby Surround */

    val.i_int = AOUT_VAR_CHAN_LEFT;
    var_Change(aout, "stereo-mode", VLC_VAR_ADDCHOICE, val, _("Left"));
    val.i_int = AOUT_VAR_CHAN_RIGHT;
    var_Change(aout, "stereo-mode", VLC_VAR_ADDCHOICE, val, _("Right"));

    val.i_int = AOUT_VAR_CHAN_RSTEREO;
    var_Change(aout, "stereo-mode", VLC_VAR_ADDCHOICE, val,
               _("Reverse stereo"));

    return i_default_mode;
}

static void aout_UpdateStereoMode(audio_output_t *aout, int mode,
                                  audio_sample_format_t *restrict fmt,
                                  aout_filters_cfg_t *filters_cfg)
{
    /* The user may have selected a different channels configuration. */
    switch (mode)
    {
        case AOUT_VAR_CHAN_RSTEREO:
            filters_cfg->remap[AOUT_CHANIDX_LEFT] = AOUT_CHANIDX_RIGHT;
            filters_cfg->remap[AOUT_CHANIDX_RIGHT] = AOUT_CHANIDX_LEFT;
            break;
        case AOUT_VAR_CHAN_STEREO:
            break;
        case AOUT_VAR_CHAN_LEFT:
            filters_cfg->remap[AOUT_CHANIDX_RIGHT] = AOUT_CHANIDX_DISABLE;
            fmt->i_physical_channels = AOUT_CHAN_CENTER;
            aout_FormatPrepare (fmt);
            break;
        case AOUT_VAR_CHAN_RIGHT:
            filters_cfg->remap[AOUT_CHANIDX_LEFT] = AOUT_CHANIDX_DISABLE;
            fmt->i_physical_channels = AOUT_CHAN_CENTER;
            aout_FormatPrepare (fmt);
            break;
        case AOUT_VAR_CHAN_DOLBYS:
            fmt->i_chan_mode = AOUT_CHANMODE_DOLBYSTEREO;
            break;
        case AOUT_VAR_CHAN_MONO:
            /* Remix all channels into one */
            for (size_t i = 0; i < AOUT_CHANIDX_MAX; ++ i)
                filters_cfg->remap[i] = AOUT_CHANIDX_LEFT;
            break;
        default:
            break;
    }

    var_Change(aout, "stereo-mode", VLC_VAR_SETVALUE,
               (vlc_value_t) { .i_int = mode});
}

static bool aout_HasStereoMode(audio_output_t *aout, int mode)
{
    bool mode_available = false;
    vlc_value_t *vals;
    size_t count;

    if (!var_Change(aout, "stereo-mode", VLC_VAR_GETCHOICES,
                    &count, &vals, (char ***)NULL))
    {
        for (size_t i = 0; !mode_available && i < count; ++i)
        {
            if (vals[i].i_int == mode)
                mode_available = true;
        }
        free(vals);
    }
    return mode_available;
}

static void aout_AddMixModeChoice(audio_output_t *aout, int mode,
                                  const char *suffix,
                                  const audio_sample_format_t *restrict fmt)
{
    assert(suffix);
    const char *text;
    char *buffer = NULL;

    if (fmt == NULL)
        text = suffix;
    else
    {
        const char *channels = aout_FormatPrintChannels(fmt);
        if (asprintf(&buffer, "%s: %s", suffix, channels) < 0)
            return;
        text = buffer;
    }

    vlc_value_t val = { .i_int = mode };
    var_Change(aout, "mix-mode", VLC_VAR_ADDCHOICE, val, text);

    free(buffer);
}

static void aout_SetupMixModeChoices (audio_output_t *aout,
                                      const audio_sample_format_t *restrict fmt)
{
    if (fmt->i_channels <= 2)
        return;

    const bool has_spatialaudio = module_exists("spatialaudio");

    /* Don't propose the mix option if we don't have the spatialaudio module
     * and if the content is ambisonics */
    if (fmt->channel_type != AUDIO_CHANNEL_TYPE_AMBISONICS || has_spatialaudio)
    {
        aout_AddMixModeChoice(aout, AOUT_MIX_MODE_UNSET, _("Original"), fmt);
        aout_AddMixModeChoice(aout, AOUT_MIX_MODE_STEREO, _("Stereo"), NULL);
    }

    if (has_spatialaudio)
        aout_AddMixModeChoice(aout, AOUT_MIX_MODE_BINAURAL, _("Binaural"), NULL);

    /* Only propose Original and Binaural for Ambisonics content */
    if (fmt->channel_type == AUDIO_CHANNEL_TYPE_AMBISONICS && has_spatialaudio)
        return;

    if (fmt->i_physical_channels != AOUT_CHANS_4_0)
    {
        static const audio_sample_format_t fmt_4_0 = {
            .i_physical_channels = AOUT_CHANS_4_0,
            .i_channels = 4,
        };
        aout_AddMixModeChoice(aout, AOUT_MIX_MODE_4_0, _("4.0"), &fmt_4_0);
    }

    if (fmt->i_physical_channels != AOUT_CHANS_5_1)
    {
        static const audio_sample_format_t fmt_5_1 = {
            .i_physical_channels = AOUT_CHANS_5_1,
            .i_channels = 6,
        };
        aout_AddMixModeChoice(aout, AOUT_MIX_MODE_5_1, _("5.1"), &fmt_5_1);
    }

    if (fmt->i_physical_channels != AOUT_CHANS_7_1)
    {
        static const audio_sample_format_t fmt_7_1 = {
            .i_physical_channels = AOUT_CHANS_7_1,
            .i_channels = 8,
        };
        aout_AddMixModeChoice(aout, AOUT_MIX_MODE_7_1, _("7.1"), &fmt_7_1);
    }
}

static bool aout_HasMixModeChoice(audio_output_t *aout, int mode)
{
    bool mode_available = false;
    vlc_value_t *vals;
    size_t count;

    if (!var_Change(aout, "mix-mode", VLC_VAR_GETCHOICES,
                    &count, &vals, (char ***)NULL))
    {
        for (size_t i = 0; !mode_available && i < count; ++i)
        {
            if (vals[i].i_int == mode)
                mode_available = true;
        }
        free(vals);
    }
    return mode_available;
}


static void aout_UpdateMixMode(audio_output_t *aout, int mode,
                               audio_sample_format_t *restrict fmt)
{
    /* The user may have selected a different channels configuration. */
    switch (mode)
    {
        case AOUT_MIX_MODE_UNSET:
            break;
        case AOUT_MIX_MODE_BINAURAL:
            fmt->i_physical_channels = AOUT_CHANS_STEREO;
            fmt->i_chan_mode = AOUT_CHANMODE_BINAURAL;
            break;
        case AOUT_MIX_MODE_STEREO:
            fmt->i_physical_channels = AOUT_CHANS_STEREO;
            break;
        case AOUT_MIX_MODE_4_0:
            fmt->i_physical_channels = AOUT_CHANS_4_0;
            break;
        case AOUT_MIX_MODE_5_1:
            fmt->i_physical_channels = AOUT_CHANS_5_1;
            break;
        case AOUT_MIX_MODE_7_1:
            fmt->i_physical_channels = AOUT_CHANS_7_1;
            break;
        default:
            break;
    }

    assert(mode == AOUT_VAR_CHAN_UNSET || aout_HasMixModeChoice(aout, mode));

    var_Change(aout, "mix-mode", VLC_VAR_SETVALUE, (vlc_value_t) { .i_int = mode});
}

int aout_OutputNew(audio_output_t *aout, vlc_aout_stream *stream,
                   audio_sample_format_t *fmt, int input_profile,
                   audio_sample_format_t *filter_fmt,
                   aout_filters_cfg_t *filters_cfg)
{
    aout_owner_t *owner = aout_owner (aout);

    vlc_fourcc_t formats[] = {
        fmt->i_format, 0, 0
    };

    var_Change(aout, "stereo-mode", VLC_VAR_CLEARCHOICES);
    var_Change(aout, "mix-mode", VLC_VAR_CLEARCHOICES);

    /* Ideally, the audio filters would be created before the audio output,
     * and the ideal audio format would be the output of the filters chain.
     * But that scheme would not really play well with digital pass-through. */
    if (AOUT_FMT_LINEAR(fmt))
    {
        if (fmt->channel_type == AUDIO_CHANNEL_TYPE_BITMAP
         && aout_FormatNbChannels(fmt) == 0)
        {
            /* The output channel map is unknown, use the WAVE one. */
            assert(fmt->i_channels > 0);
            aout_SetWavePhysicalChannels(fmt);
        }

        if (fmt->channel_type == AUDIO_CHANNEL_TYPE_AMBISONICS)
        {
            /* Set the maximum of channels to render ambisonics contents. The
             * aout module will still be free to select less channels in order
             * to respect the sink setup. */
            fmt->i_physical_channels = AOUT_CHANS_7_1;
        }

        /* Try to stay in integer domain if possible for no/slow FPU. */
        fmt->i_format = (fmt->i_bitspersample > 16) ? VLC_CODEC_FL32
                                                    : VLC_CODEC_S16N;

        aout_SetupMixModeChoices(aout, fmt);

        /* Prefer the user requested mode if available, otherwise, use the
         * default one */
        if (aout_HasMixModeChoice(aout, owner->requested_mix_mode))
            aout_UpdateMixMode(aout, owner->requested_mix_mode, fmt);

        aout_FormatPrepare (fmt);
        assert (aout_FormatNbChannels(fmt) > 0);
    }
    else
    {
        switch (fmt->i_format)
        {
            static_assert(ARRAY_SIZE(formats) >= 3, "fallback array too small");
            case VLC_CODEC_DTS:
                if (input_profile > 0)
                {
                    /* DTSHD can be played as DTSHD or as DTS */
                    formats[0] = VLC_CODEC_DTSHD;
                    formats[1] = VLC_CODEC_DTS;
                }
                break;
            case VLC_CODEC_A52:
                if (input_profile > 0)
                {
                    formats[0] = VLC_CODEC_EAC3;
                    formats[1] = VLC_CODEC_A52;
                }
                break;
            default:
                break;
        }
    }

    int stereo_mode = aout_PrepareStereoMode(aout, fmt);

    if (stereo_mode != AOUT_VAR_CHAN_UNSET
     && aout_HasStereoMode(aout, stereo_mode))
        aout_UpdateStereoMode(aout, stereo_mode, fmt, filters_cfg);

    aout->current_sink_info.headphones = false;

    vlc_mutex_lock(&owner->lock);
    /* XXX: Remove when aout/stream support is complete (in all modules) */
    assert(owner->main_stream == NULL);

    int ret = VLC_EGENERIC;
    for (size_t i = 0; formats[i] != 0 && ret != VLC_SUCCESS; ++i)
    {
        filter_fmt->i_format = fmt->i_format = formats[i];
        owner->main_stream = stream;
        ret = aout->start(aout, fmt);
        if (ret != 0)
            owner->main_stream = NULL;
    }
    vlc_mutex_unlock(&owner->lock);
    if (ret)
    {
        if (AOUT_FMT_LINEAR(fmt))
            msg_Err (aout, "failed to start audio output");
        else
            msg_Warn (aout, "failed to start passthrough audio output, "
                      "failing back to linear format");
        return -1;
    }
    assert(aout->flush != NULL && aout->play != NULL);

    /* Autoselect the headphones mode if available and if the user didn't
     * request any mode */
    if (aout->current_sink_info.headphones
     && owner->requested_mix_mode == AOUT_VAR_CHAN_UNSET
     && fmt->i_physical_channels == AOUT_CHANS_STEREO
     && aout_HasMixModeChoice(aout, AOUT_MIX_MODE_BINAURAL))
    {
        assert(stereo_mode == AOUT_VAR_CHAN_UNSET);
        aout_UpdateMixMode(aout, AOUT_MIX_MODE_BINAURAL, fmt);
    }

    aout_FormatPrepare (fmt);
    assert (fmt->i_bytes_per_frame > 0 && fmt->i_frame_length > 0);
    aout_FormatPrint (aout, "output", fmt);

    return 0;
}

void aout_OutputDelete (audio_output_t *aout)
{
    aout_owner_t *owner = aout_owner(aout);
    vlc_mutex_lock(&owner->lock);
    aout->stop (aout);
    owner->main_stream = NULL;
    vlc_mutex_unlock(&owner->lock);
}

float aout_VolumeGet (audio_output_t *aout)
{
    return var_GetFloat (aout, "volume");
}

int aout_VolumeSet (audio_output_t *aout, float vol)
{
    aout_owner_t *owner = aout_owner(aout);
    int ret;

    vlc_mutex_lock(&owner->lock);
    ret = aout->volume_set ? aout->volume_set(aout, vol) : -1;
    vlc_mutex_unlock(&owner->lock);
    return ret ? -1 : 0;
}

int aout_VolumeUpdate (audio_output_t *aout, int value, float *volp)
{
    int ret = -1;
    const float defaultVolume = (float)AOUT_VOLUME_DEFAULT;
    const float stepSize = var_InheritFloat (aout, "volume-step") / defaultVolume;
    float vol = aout_VolumeGet (aout);

    if (vol >= 0.f)
    {
        vol += (value * stepSize);
        vol = (roundf (vol / stepSize)) * stepSize;
        vol = clampf(vol, 0.f, AOUT_VOLUME_MAX / defaultVolume);
        if (volp != NULL)
            *volp = vol;
        ret = aout_VolumeSet (aout, vol);
    }
    return ret;
}

int aout_MuteGet (audio_output_t *aout)
{
    return var_InheritBool (aout, "mute");
}

int aout_MuteSet (audio_output_t *aout, bool mute)
{
    aout_owner_t *owner = aout_owner(aout);
    int ret;

    vlc_mutex_lock(&owner->lock);
    ret = aout->mute_set ? aout->mute_set(aout, mute) : -1;
    vlc_mutex_unlock(&owner->lock);
    return ret ? -1 : 0;
}

char *aout_DeviceGet (audio_output_t *aout)
{
    return var_GetNonEmptyString (aout, "device");
}

int aout_DeviceSet (audio_output_t *aout, const char *id)
{
    aout_owner_t *owner = aout_owner(aout);
    int ret;

    vlc_mutex_lock(&owner->lock);
    ret = aout->device_select ? aout->device_select(aout, id) : -1;
    vlc_mutex_unlock(&owner->lock);
    return ret ? -1 : 0;
}

int aout_DevicesList (audio_output_t *aout, char ***ids, char ***names)
{
    aout_owner_t *owner = aout_owner (aout);
    char **tabid, **tabname;
    unsigned i = 0;

    vlc_mutex_lock (&owner->dev.lock);
    tabid = vlc_alloc (owner->dev.count, sizeof (*tabid));
    tabname = vlc_alloc (owner->dev.count, sizeof (*tabname));

    if (unlikely(tabid == NULL || tabname == NULL))
        goto error;

    *ids = tabid;
    *names = tabname;

    aout_dev_t *dev;
    vlc_list_foreach(dev, &owner->dev.list, node)
    {
        tabid[i] = strdup(dev->id);
        if (unlikely(tabid[i] == NULL))
            goto error;

        tabname[i] = strdup(dev->name);
        if (unlikely(tabname[i] == NULL))
        {
            free(tabid[i]);
            goto error;
        }

        i++;
    }
    vlc_mutex_unlock (&owner->dev.lock);

    return i;

error:
    vlc_mutex_unlock(&owner->dev.lock);
    while (i > 0)
    {
        i--;
        free(tabname[i]);
        free(tabid[i]);
    }
    free(tabname);
    free(tabid);
    return -1;
}

static void aout_ChangeViewpoint(audio_output_t *aout,
                                 const vlc_viewpoint_t *p_viewpoint)
{
    aout_owner_t *owner = aout_owner(aout);

    vlc_mutex_lock(&owner->vp.lock);
    owner->vp.value = *p_viewpoint;
    atomic_store_explicit(&owner->vp.update, true, memory_order_relaxed);
    vlc_mutex_unlock(&owner->vp.lock);
}

vlc_audio_meter_plugin *
aout_AddMeterPlugin(audio_output_t *aout, const char *chain,
                    const struct vlc_audio_meter_plugin_owner *meter_plugin_owner)
{
    aout_owner_t *owner = aout_owner(aout);

    return vlc_audio_meter_AddPlugin(&owner->meter, chain, meter_plugin_owner);
}

void
aout_RemoveMeterPlugin(audio_output_t *aout, vlc_audio_meter_plugin *plugin)
{
    aout_owner_t *owner = aout_owner(aout);

    vlc_audio_meter_RemovePlugin(&owner->meter, plugin);
}
