/*****************************************************************************
 * wasapi.c : Windows Audio Session API output plugin for VLC
 *****************************************************************************
 * Copyright (C) 2012 RĂ©mi Denis-Courmont
 *
 * 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

#define INITGUID
#define COBJMACROS
#define CONST_VTABLE

#include <stdlib.h>
#include <assert.h>
#include <audioclient.h>
#include <audiopolicy.h>
#include <mmdeviceapi.h>

#include <vlc_common.h>
#include <vlc_plugin.h>
#include <vlc_aout.h>
#include <vlc_charset.h>

DEFINE_GUID (GUID_VLC_AUD_OUT, 0x4533f59d, 0x59ee, 0x00c6,
   0xad, 0xb2, 0xc6, 0x8b, 0x50, 0x1a, 0x66, 0x55);

DEFINE_PROPERTYKEY(PKEY_Device_FriendlyName, 0xa45c254e, 0xdf1c, 0x4efd,
   0x80, 0x20, 0x67, 0xd1, 0x46, 0xa8, 0x50, 0xe0, 14);

static int Open(vlc_object_t *);
static void Close(vlc_object_t *);

vlc_module_begin()
    set_shortname("WASAPI")
    set_description(N_("Windows Audio Session output") )
    set_capability("audio output", 150)
    set_category(CAT_AUDIO)
    set_subcategory(SUBCAT_AUDIO_AOUT)
    add_shortcut("was", "audioclient")
    set_callbacks(Open, Close)
vlc_module_end()

static LARGE_INTEGER freq; /* performance counters frequency */

BOOL WINAPI DllMain(HINSTANCE, DWORD, LPVOID); /* avoid warning */

BOOL WINAPI DllMain(HINSTANCE dll, DWORD reason, LPVOID reserved)
{
    (void) dll;
    (void) reserved;

    switch (reason)
    {
        case DLL_PROCESS_ATTACH:
            if (!QueryPerformanceFrequency(&freq))
                return FALSE;
            break;
    }
    return TRUE;
}

static UINT64 GetQPC(void)
{
    LARGE_INTEGER counter;

    if (!QueryPerformanceCounter(&counter))
        abort();

    lldiv_t d = lldiv(counter.QuadPart, freq.QuadPart);
    return (d.quot * 10000000) + ((d.rem * 10000000) / freq.QuadPart);
}

static int TryEnter(vlc_object_t *obj)
{
    HRESULT hr = CoInitializeEx(NULL, COINIT_MULTITHREADED);
    if (unlikely(FAILED(hr)))
    {
        msg_Err (obj, "cannot initialize COM (error 0x%lx)", hr);
        return -1;
    }
    return 0;
}
#define TryEnter(o) TryEnter(VLC_OBJECT(o))

static void Enter(void)
{
    HRESULT hr = CoInitializeEx(NULL, COINIT_MULTITHREADED);
    if (unlikely(FAILED(hr)))
        abort();
}

static void Leave(void)
{
    CoUninitialize();
}

struct aout_sys_t
{
    audio_output_t *aout;
    IMMDeviceEnumerator *it;
    IAudioClient *client;
    IAudioRenderClient *render;
    IAudioClock *clock;

    IAudioSessionControl *control;
    struct IAudioSessionEvents events;
    LONG refs;

    uint8_t chans_table[AOUT_CHAN_MAX];
    uint8_t chans_to_reorder;
    uint8_t bits; /**< Bits per sample */
    unsigned rate; /**< Sample rate */
    unsigned bytes_per_frame;
    UINT32 written; /**< Frames written to the buffer */
    UINT32 frames; /**< Total buffer size (frames) */

    float volume_hack; /**< Deferred volume request */
    int mute_hack; /**< Deferred mute request */

    HANDLE ready; /**< Semaphore from MTA thread */
    HANDLE done; /**< Semaphore to MTA thread */
};


/*** VLC audio output callbacks ***/
static int TimeGet(audio_output_t *aout, mtime_t *restrict delay)
{
    aout_sys_t *sys = aout->sys;
    UINT64 pos, qpcpos;
    HRESULT hr;

    if (sys->clock == NULL)
        return -1;

    Enter();
    hr = IAudioClock_GetPosition(sys->clock, &pos, &qpcpos);
    Leave();
    if (FAILED(hr))
    {
        msg_Err(aout, "cannot get position (error 0x%lx)", hr);
        return -1;
    }

    if (pos == 0)
    {
        *delay = sys->written * CLOCK_FREQ / sys->rate;
        msg_Dbg(aout, "extrapolating position: still propagating buffers");
        return 0;
    }

    *delay = ((GetQPC() - qpcpos) / (10000000 / CLOCK_FREQ));
    static_assert((10000000 % CLOCK_FREQ) == 0, "Frequency conversion broken");
    return 0;
}

static void CheckVolumeHack(audio_output_t *aout)
{
    aout_sys_t *sys = aout->sys;

    if (unlikely(sys->volume_hack >= 0.f))
    {   /* Apply volume now, if it failed earlier */
        aout->volume_set(aout, sys->volume_hack);
        sys->volume_hack = -1.f;
    }
    if (unlikely(sys->mute_hack >= 0))
    {   /* Apply volume now, if it failed earlier */
        aout->mute_set(aout, sys->mute_hack);
        sys->mute_hack = -1;
    }
}

static void Play(audio_output_t *aout, block_t *block)
{
    aout_sys_t *sys = aout->sys;
    HRESULT hr = S_OK;

    CheckVolumeHack(aout);

    if (sys->chans_to_reorder)
        aout_ChannelReorder(block->p_buffer, block->i_buffer,
                          sys->chans_to_reorder, sys->chans_table, sys->bits);

    Enter();
    for (;;)
    {
        UINT32 frames;
        hr = IAudioClient_GetCurrentPadding(sys->client, &frames);
        if (FAILED(hr))
        {
            msg_Err(aout, "cannot get current padding (error 0x%lx)", hr);
            break;
        }

        assert(frames <= sys->frames);
        frames = sys->frames - frames;
        if (frames > block->i_nb_samples)
            frames = block->i_nb_samples;

        BYTE *dst;
        hr = IAudioRenderClient_GetBuffer(sys->render, frames, &dst);
        if (FAILED(hr))
        {
            msg_Err(aout, "cannot get buffer (error 0x%lx)", hr);
            break;
        }

        const size_t copy = frames * sys->bytes_per_frame;

        memcpy(dst, block->p_buffer, copy);
        hr = IAudioRenderClient_ReleaseBuffer(sys->render, frames, 0);
        if (FAILED(hr))
        {
            msg_Err(aout, "cannot release buffer (error 0x%lx)", hr);
            break;
        }
        IAudioClient_Start(sys->client);

        block->p_buffer += copy;
        block->i_buffer -= copy;
        block->i_nb_samples -= frames;
        sys->written += frames;
        if (block->i_nb_samples == 0)
            break; /* done */

        /* Out of buffer space, sleep */
        msleep(AOUT_MIN_PREPARE_TIME
             + block->i_nb_samples * CLOCK_FREQ / sys->rate);
    }

    Leave();
    block_Release(block);

    /* Restart on unplug */
    if (unlikely(hr == AUDCLNT_E_DEVICE_INVALIDATED))
        var_TriggerCallback(aout, "audio-device");
}

static void Pause(audio_output_t *aout, bool paused, mtime_t date)
{
    aout_sys_t *sys = aout->sys;
    HRESULT hr;

    CheckVolumeHack(aout);

    Enter();
    if (paused)
        hr = IAudioClient_Stop(sys->client);
    else
        hr = IAudioClient_Start(sys->client);
    if (FAILED(hr))
        msg_Warn(aout, "cannot %s stream (error 0x%lx)",
                 paused ? "stop" : "start", hr);
    Leave();

    (void) date;
}

static void Flush(audio_output_t *aout, bool wait)
{
    aout_sys_t *sys = aout->sys;
    HRESULT hr;

    CheckVolumeHack(aout);

    if (wait)
        return; /* Drain not implemented */

    Enter();
    IAudioClient_Stop(sys->client);
    hr = IAudioClient_Reset(sys->client);
    Leave();

    if (FAILED(hr))
        msg_Warn(aout, "cannot reset stream (error 0x%lx)", hr);
    else
        sys->written = 0;
}

static int SimpleVolumeSet(audio_output_t *aout, float vol)
{
    aout_sys_t *sys = aout->sys;
    ISimpleAudioVolume *simple;
    HRESULT hr;

    if (TryEnter(aout))
        return -1;
    hr = IAudioClient_GetService(sys->client, &IID_ISimpleAudioVolume,
                                 (void **)&simple);
    if (SUCCEEDED(hr))
    {
        hr = ISimpleAudioVolume_SetMasterVolume(simple, vol, NULL);
        ISimpleAudioVolume_Release(simple);
    }
    Leave();

    if (FAILED(hr))
    {
        msg_Err(aout, "cannot set volume (error 0x%lx)", hr);
        sys->volume_hack = vol;
        return -1;
    }
    sys->volume_hack = -1.f;
    return 0;
}

static int SimpleMuteSet(audio_output_t *aout, bool mute)
{
    aout_sys_t *sys = aout->sys;
    ISimpleAudioVolume *simple;
    HRESULT hr;

    if (TryEnter(aout))
        return -1;
    hr = IAudioClient_GetService(sys->client, &IID_ISimpleAudioVolume,
                                 (void **)&simple);
    if (SUCCEEDED(hr))
    {
        hr = ISimpleAudioVolume_SetMute(simple, mute, NULL);
        ISimpleAudioVolume_Release(simple);
    }
    Leave();

    if (FAILED(hr))
    {
        msg_Err(aout, "cannot set mute (error 0x%lx)", hr);
        sys->mute_hack = mute;
        return -1;
    }
    sys->mute_hack = -1;
    return 0;
}


/*** Audio devices ***/
static int DeviceChanged(vlc_object_t *obj, const char *varname,
                         vlc_value_t prev, vlc_value_t cur, void *data)
{
    aout_ChannelsRestart(obj, varname, prev, cur, data);

    if (!var_Type (obj, "wasapi-audio-device"))
        var_Create (obj, "wasapi-audio-device", VLC_VAR_STRING);
    var_SetString (obj, "wasapi-audio-device", cur.psz_string);
    return VLC_SUCCESS;
}

static void GetDevices(vlc_object_t *obj, IMMDeviceEnumerator *it)
{
    HRESULT hr;
    vlc_value_t val, text;

    var_Create (obj, "audio-device", VLC_VAR_STRING | VLC_VAR_HASCHOICE);
    text.psz_string = _("Audio Device");
    var_Change (obj, "audio-device", VLC_VAR_SETTEXT, &text, NULL);

    IMMDeviceCollection *devs;
    hr = IMMDeviceEnumerator_EnumAudioEndpoints(it, eRender,
                                                DEVICE_STATE_ACTIVE, &devs);
    if (FAILED(hr))
    {
        msg_Warn (obj, "cannot enumerate audio endpoints (error 0x%lx)", hr);
        return;
    }

    UINT n;
    hr = IMMDeviceCollection_GetCount(devs, &n);
    if (FAILED(hr))
    {
        msg_Warn (obj, "cannot count audio endpoints (error 0x%lx)", hr);
        n = 0;
    }
    else
        msg_Dbg(obj, "Available Windows Audio devices:");

    while (n > 0)
    {
        IMMDevice *dev;

        hr = IMMDeviceCollection_Item(devs, --n, &dev);
        if (FAILED(hr))
            continue;

        /* Unique device ID */
        LPWSTR devid;
        hr = IMMDevice_GetId(dev, &devid);
        if (FAILED(hr))
        {
            IMMDevice_Release(dev);
            continue;
        }
        val.psz_string = FromWide(devid);
        CoTaskMemFree(devid);
        text.psz_string = val.psz_string;

        /* User-readable device name */
        IPropertyStore *props;
        hr = IMMDevice_OpenPropertyStore(dev, STGM_READ, &props);
        if (SUCCEEDED(hr))
        {
            PROPVARIANT v;

            PropVariantInit(&v);
            hr = IPropertyStore_GetValue(props, &PKEY_Device_FriendlyName, &v);
            if (SUCCEEDED(hr))
                text.psz_string = FromWide(v.pwszVal);
            PropVariantClear(&v);
            IPropertyStore_Release(props);
        }
        IMMDevice_Release(dev);

        msg_Dbg(obj, "%s (%s)", val.psz_string, text.psz_string);
        var_Change(obj, "audio-device", VLC_VAR_ADDCHOICE, &val, &text);
        if (likely(text.psz_string != val.psz_string))
            free(text.psz_string);
        free(val.psz_string);
    }
    IMMDeviceCollection_Release(devs);
}


/*** Audio session events ***/
static inline aout_sys_t *vlc_AudioSessionEvents_sys(IAudioSessionEvents *this)
{
    return (aout_sys_t *)(((char *)this) - offsetof(aout_sys_t, events));
}

static STDMETHODIMP
vlc_AudioSessionEvents_QueryInterface(IAudioSessionEvents *this, REFIID riid,
                                      void **ppv)
{
    if (IsEqualIID(riid, &IID_IUnknown)
     || IsEqualIID(riid, &IID_IAudioSessionEvents))
    {
        *ppv = this;
        IUnknown_AddRef(this);
        return S_OK;
    }
    else
    {
       *ppv = NULL;
        return E_NOINTERFACE;
    }
}

static STDMETHODIMP_(ULONG)
vlc_AudioSessionEvents_AddRef(IAudioSessionEvents *this)
{
    aout_sys_t *sys = vlc_AudioSessionEvents_sys(this);
    return InterlockedIncrement(&sys->refs);
}

static STDMETHODIMP_(ULONG)
vlc_AudioSessionEvents_Release(IAudioSessionEvents *this)
{
    aout_sys_t *sys = vlc_AudioSessionEvents_sys(this);
    return InterlockedDecrement(&sys->refs);
}

static STDMETHODIMP
vlc_AudioSessionEvents_OnDisplayNameChanged(IAudioSessionEvents *this,
                                            LPCWSTR wname, LPCGUID ctx)
{
    aout_sys_t *sys = vlc_AudioSessionEvents_sys(this);
    audio_output_t *aout = sys->aout;

    msg_Dbg(aout, "display name changed: %ls", wname);
    (void) ctx;
    return S_OK;
}

static STDMETHODIMP
vlc_AudioSessionEvents_OnIconPathChanged(IAudioSessionEvents *this,
                                         LPCWSTR wpath, LPCGUID ctx)
{
    aout_sys_t *sys = vlc_AudioSessionEvents_sys(this);
    audio_output_t *aout = sys->aout;

    msg_Dbg(aout, "icon path changed: %ls", wpath);
    (void) ctx;
    return S_OK;
}

static STDMETHODIMP
vlc_AudioSessionEvents_OnSimpleVolumeChanged(IAudioSessionEvents *this, float vol,
                                             WINBOOL mute, LPCGUID ctx)
{
    aout_sys_t *sys = vlc_AudioSessionEvents_sys(this);
    audio_output_t *aout = sys->aout;

    msg_Dbg(aout, "simple volume changed: %f, muting %sabled", vol,
            mute ? "en" : "dis");
    aout_VolumeReport(aout, vol);
    aout_MuteReport(aout, mute == TRUE);
    (void) ctx;
    return S_OK;
}

static STDMETHODIMP
vlc_AudioSessionEvents_OnChannelVolumeChanged(IAudioSessionEvents *this,
                                              DWORD count, float *vols,
                                              DWORD changed, LPCGUID ctx)
{
    aout_sys_t *sys = vlc_AudioSessionEvents_sys(this);
    audio_output_t *aout = sys->aout;

    msg_Dbg(aout, "channel volume %lu of %lu changed: %f", changed, count,
            vols[changed]);
    (void) ctx;
    return S_OK;
}

static STDMETHODIMP
vlc_AudioSessionEvents_OnGroupingParamChanged(IAudioSessionEvents *this,
                                              LPCGUID param, LPCGUID ctx)

{
    aout_sys_t *sys = vlc_AudioSessionEvents_sys(this);
    audio_output_t *aout = sys->aout;

    msg_Dbg(aout, "grouping parameter changed");
    (void) param;
    (void) ctx;
    return S_OK;
}

static STDMETHODIMP
vlc_AudioSessionEvents_OnStateChanged(IAudioSessionEvents *this,
                                      AudioSessionState state)
{
    aout_sys_t *sys = vlc_AudioSessionEvents_sys(this);
    audio_output_t *aout = sys->aout;

    msg_Dbg(aout, "state changed: %d", state);
    return S_OK;
}

static STDMETHODIMP
vlc_AudioSessionEvents_OnSessionDisconnected(IAudioSessionEvents *this,
                                             AudioSessionDisconnectReason reason)
{
    aout_sys_t *sys = vlc_AudioSessionEvents_sys(this);
    audio_output_t *aout = sys->aout;

    msg_Dbg(aout, "session disconnected: reason %d", reason);
    return S_OK;
}

static const struct IAudioSessionEventsVtbl vlc_AudioSessionEvents =
{
    vlc_AudioSessionEvents_QueryInterface,
    vlc_AudioSessionEvents_AddRef,
    vlc_AudioSessionEvents_Release,

    vlc_AudioSessionEvents_OnDisplayNameChanged,
    vlc_AudioSessionEvents_OnIconPathChanged,
    vlc_AudioSessionEvents_OnSimpleVolumeChanged,
    vlc_AudioSessionEvents_OnChannelVolumeChanged,
    vlc_AudioSessionEvents_OnGroupingParamChanged,
    vlc_AudioSessionEvents_OnStateChanged,
    vlc_AudioSessionEvents_OnSessionDisconnected,
};


/*** Initialization / deinitialization **/
static const uint32_t chans_out[] = {
    SPEAKER_FRONT_LEFT, SPEAKER_FRONT_RIGHT,
    SPEAKER_FRONT_CENTER, SPEAKER_LOW_FREQUENCY,
    SPEAKER_BACK_LEFT, SPEAKER_BACK_RIGHT, SPEAKER_BACK_CENTER,
    SPEAKER_SIDE_LEFT, SPEAKER_SIDE_RIGHT, 0
};
static const uint32_t chans_in[] = {
    SPEAKER_FRONT_LEFT, SPEAKER_FRONT_RIGHT,
    SPEAKER_SIDE_LEFT, SPEAKER_SIDE_RIGHT,
    SPEAKER_BACK_LEFT, SPEAKER_BACK_RIGHT, SPEAKER_BACK_CENTER,
    SPEAKER_FRONT_CENTER, SPEAKER_LOW_FREQUENCY, 0
};

static void vlc_ToWave(WAVEFORMATEXTENSIBLE *restrict wf,
                       audio_sample_format_t *restrict audio)
{
    switch (audio->i_format)
    {
        case VLC_CODEC_FL64:
            audio->i_format = VLC_CODEC_FL32;
        case VLC_CODEC_FL32:
            wf->SubFormat = KSDATAFORMAT_SUBTYPE_IEEE_FLOAT;
            break;

        case VLC_CODEC_S8:
        case VLC_CODEC_U8:
            audio->i_format = VLC_CODEC_S16N;
        case VLC_CODEC_S16N:
            wf->SubFormat = KSDATAFORMAT_SUBTYPE_PCM;
            break;

        default:
            audio->i_format = VLC_CODEC_FL32;
            wf->SubFormat = KSDATAFORMAT_SUBTYPE_IEEE_FLOAT;
            break;
    }
    aout_FormatPrepare (audio);

    wf->Format.wFormatTag = WAVE_FORMAT_EXTENSIBLE;
    wf->Format.nChannels = audio->i_channels;
    wf->Format.nSamplesPerSec = audio->i_rate;
    wf->Format.nAvgBytesPerSec = audio->i_bytes_per_frame * audio->i_rate;
    wf->Format.nBlockAlign = audio->i_bytes_per_frame;
    wf->Format.wBitsPerSample = audio->i_bitspersample;
    wf->Format.cbSize = sizeof (*wf) - sizeof (wf->Format);

    wf->Samples.wValidBitsPerSample = audio->i_bitspersample;

    wf->dwChannelMask = 0;
    for (unsigned i = 0; pi_vlc_chan_order_wg4[i]; i++)
        if (audio->i_physical_channels & pi_vlc_chan_order_wg4[i])
            wf->dwChannelMask |= chans_in[i];
}

static int vlc_FromWave(const WAVEFORMATEX *restrict wf,
                        audio_sample_format_t *restrict audio)
{
    audio->i_rate = wf->nSamplesPerSec;
    audio->i_physical_channels = 0;

    if (wf->wFormatTag == WAVE_FORMAT_EXTENSIBLE)
    {
        const WAVEFORMATEXTENSIBLE *wfe = (void *)wf;

        for (unsigned i = 0; chans_in[i]; i++)
            if (wfe->dwChannelMask & chans_in[i])
                audio->i_physical_channels |= pi_vlc_chan_order_wg4[i];
    }

    audio->i_original_channels = audio->i_physical_channels;
    aout_FormatPrepare (audio);

    if (wf->nChannels != audio->i_channels)
        return -1;
    return 0;
}

static unsigned vlc_CheckWaveOrder (const WAVEFORMATEX *restrict wf,
                                    uint8_t *restrict table)
{
    uint32_t mask = 0;

    if (wf->wFormatTag == WAVE_FORMAT_EXTENSIBLE)
    {
        const WAVEFORMATEXTENSIBLE *wfe = (void *)wf;

        mask = wfe->dwChannelMask;
    }
    return aout_CheckChannelReorder(chans_in, chans_out, mask, table);
}

static wchar_t *var_InheritWide(vlc_object_t *obj, const char *name)
{
    char *v8 = var_InheritString(obj, name);
    if (v8 == NULL)
        return NULL;

    wchar_t *v16 = ToWide(v8);
    free(v8);
    return v16;
}
#define var_InheritWide(o,n) var_InheritWide(VLC_OBJECT(o),n)

static int var_SetWide(vlc_object_t *obj, const char *name, const wchar_t *val)
{
    char *str = FromWide(val);
    if (unlikely(str == NULL))
        return VLC_ENOMEM;

    int ret = var_SetString(obj, name, str);
    free(str);
    return ret;
}
#define var_SetWide(o,n,v) var_SetWide(VLC_OBJECT(o),n,v)

/* Dummy thread to create and release COM interfaces when needed. */
static void MTAThread(void *data)
{
    audio_output_t *aout = data;
    aout_sys_t *sys = aout->sys;
    HRESULT hr;

    Enter();

    hr = IAudioClient_GetService(sys->client, &IID_IAudioRenderClient,
                                 (void **)&sys->render);
    if (FAILED(hr))
    {
        msg_Err(aout, "cannot get audio render service (error 0x%lx)", hr);
        goto fail;
    }

    hr = IAudioClient_GetService(sys->client, &IID_IAudioClock,
                                 (void **)&sys->clock);
    if (FAILED(hr))
        msg_Warn(aout, "cannot get audio clock (error 0x%lx)", hr);

    hr = IAudioClient_GetService(sys->client, &IID_IAudioSessionControl,
                                 (void **)&sys->control);
    if (FAILED(hr))
        msg_Warn(aout, "cannot get audio session control (error 0x%lx)", hr);
    else
    {
        wchar_t *ua = var_InheritWide(aout, "user-agent");
        IAudioSessionControl_SetDisplayName(sys->control, ua, NULL);
        free(ua);
    }

    /* do nothing until the audio session terminates */
    ReleaseSemaphore(sys->ready, 1, NULL);
    WaitForSingleObject(sys->done, INFINITE);

    if (sys->control != NULL)
        IAudioSessionControl_Release(sys->control);
    if (sys->clock != NULL)
        IAudioClock_Release(sys->clock);
    IAudioRenderClient_Release(sys->render);
fail:
    Leave();
    ReleaseSemaphore(sys->ready, 1, NULL);
}

static int Start(audio_output_t *aout, audio_sample_format_t *restrict fmt)
{
    aout_sys_t *sys = aout->sys;
    HRESULT hr;

    sys->client = NULL;
    sys->render = NULL;
    sys->clock = NULL;
    sys->events.lpVtbl = &vlc_AudioSessionEvents;
    sys->refs = 1;
    sys->ready = NULL;
    sys->done = NULL;

    Enter();
retry:
    /* Get audio device according to policy */
    // Without configuration item, the variable must be created explicitly.
    var_Create (aout, "wasapi-audio-device", VLC_VAR_STRING);
    LPWSTR devid = var_InheritWide (aout, "wasapi-audio-device");
    var_Destroy (aout, "wasapi-audio-device");

    IMMDevice *dev = NULL;
    if (devid != NULL)
    {
        msg_Dbg (aout, "using selected device %ls", devid);
        hr = IMMDeviceEnumerator_GetDevice (sys->it, devid, &dev);
        if (FAILED(hr))
            msg_Warn(aout, "cannot get audio endpoint (error 0x%lx)", hr);
        free (devid);
    }
    if (dev == NULL)
    {
        msg_Dbg (aout, "using default device");
        hr = IMMDeviceEnumerator_GetDefaultAudioEndpoint(sys->it, eRender,
                                                         eConsole, &dev);
    }
    if (FAILED(hr))
    {
        msg_Err(aout, "cannot get audio endpoint (error 0x%lx)", hr);
        goto error;
    }

    hr = IMMDevice_GetId(dev, &devid);
    if (SUCCEEDED(hr))
    {
        msg_Dbg(aout, "using device %ls", devid);
        var_SetWide (aout, "audio-device", devid);
        CoTaskMemFree(devid);
    }

    hr = IMMDevice_Activate(dev, &IID_IAudioClient, CLSCTX_ALL, NULL,
                            (void **)&sys->client);
    IMMDevice_Release(dev);
    if (FAILED(hr))
    {
        msg_Err(aout, "cannot activate audio client (error 0x%lx)", hr);
        goto error;
    }

    /* Configure audio stream */
    WAVEFORMATEXTENSIBLE wf;
    WAVEFORMATEX *pwf;

    vlc_ToWave(&wf, fmt);
    hr = IAudioClient_IsFormatSupported(sys->client, AUDCLNT_SHAREMODE_SHARED,
                                        &wf.Format, &pwf);
    if (FAILED(hr))
    {
        msg_Err(aout, "cannot negotiate audio format (error 0x%lx)", hr);
        goto error;
    }

    if (hr == S_FALSE)
    {
        assert(pwf != NULL);
        if (vlc_FromWave(pwf, fmt))
        {
            CoTaskMemFree(pwf);
            msg_Err(aout, "unsupported audio format");
            goto error;
        }
        msg_Dbg(aout, "modified format");
    }
    else
        assert(pwf == NULL);

    sys->chans_to_reorder = vlc_CheckWaveOrder((hr == S_OK) ? &wf.Format : pwf,
                                               sys->chans_table);
    sys->bits = fmt->i_bitspersample;

    hr = IAudioClient_Initialize(sys->client, AUDCLNT_SHAREMODE_SHARED, 0,
                                 AOUT_MAX_PREPARE_TIME * 10, 0,
                                 (hr == S_OK) ? &wf.Format : pwf,
                                 &GUID_VLC_AUD_OUT);
    CoTaskMemFree(pwf);
    if (FAILED(hr))
    {
        msg_Err(aout, "cannot initialize audio client (error 0x%lx)", hr);
        goto error;
    }

    hr = IAudioClient_GetBufferSize(sys->client, &sys->frames);
    if (FAILED(hr))
    {
        msg_Err(aout, "cannot get buffer size (error 0x%lx)", hr);
        goto error;
    }

    sys->ready = CreateSemaphore(NULL, 0, 1, NULL);
    sys->done = CreateSemaphore(NULL, 0, 1, NULL);
    if (unlikely(sys->ready == NULL || sys->done == NULL))
        goto error;
    /* Note: thread handle released by CRT, ignore it. */
    if (_beginthread(MTAThread, 0, aout) == (uintptr_t)-1)
        goto error;

    WaitForSingleObject(sys->ready, INFINITE);
    if (sys->render == NULL)
        goto error;

    Leave();

    sys->rate = fmt->i_rate;
    sys->bytes_per_frame = fmt->i_bytes_per_frame;
    sys->written = 0;
    aout->time_get = TimeGet;
    aout->play = Play;
    aout->pause = Pause;
    aout->flush = Flush;
    if (likely(sys->control != NULL))
       IAudioSessionControl_RegisterAudioSessionNotification(sys->control,
                                                             &sys->events);
    var_AddCallback (aout, "audio-device", DeviceChanged, NULL);

    return VLC_SUCCESS;
error:
    if (sys->done != NULL)
        CloseHandle(sys->done);
    if (sys->ready != NULL)
        CloseHandle(sys->done);
    if (sys->client != NULL)
        IAudioClient_Release(sys->client);
    if (hr == AUDCLNT_E_DEVICE_INVALIDATED)
    {
        var_SetString(aout, "audio-device", "");
        msg_Warn(aout, "device invalidated, retrying");
        goto retry;
    }
    Leave();
    return VLC_EGENERIC;
}

static void Stop(audio_output_t *aout)
{
    aout_sys_t *sys = aout->sys;

    Enter();
    if (likely(sys->control != NULL))
       IAudioSessionControl_UnregisterAudioSessionNotification(sys->control,
                                                               &sys->events);
    ReleaseSemaphore(sys->done, 1, NULL); /* tell MTA thread to finish */
    WaitForSingleObject(sys->ready, INFINITE); /* wait for that ^ */
    IAudioClient_Stop(sys->client); /* should not be needed */
    IAudioClient_Release(sys->client);
    Leave();

    var_DelCallback (aout, "audio-device", DeviceChanged, NULL);

    CloseHandle(sys->done);
    CloseHandle(sys->ready);
}

static int Open(vlc_object_t *obj)
{
    audio_output_t *aout = (audio_output_t *)obj;
    void *pv;
    HRESULT hr;

    if (!aout->b_force && var_InheritBool(aout, "spdif"))
        /* Fallback to other plugin until pass-through is implemented */
        return VLC_EGENERIC;

    aout_sys_t *sys = malloc(sizeof (*sys));
    if (unlikely(sys == NULL))
        return VLC_ENOMEM;
    sys->aout = aout;

    if (TryEnter(aout))
        goto error;

    hr = CoCreateInstance(&CLSID_MMDeviceEnumerator, NULL, CLSCTX_ALL,
                          &IID_IMMDeviceEnumerator, &pv);
    if (FAILED(hr))
    {
        msg_Dbg(aout, "cannot create device enumerator (error 0x%lx)", hr);
        Leave();
        goto error;
    }
    sys->it = pv;
    GetDevices(obj, sys->it);
    Leave();

    sys->volume_hack = -1.f;
    sys->mute_hack = -1;

    aout->sys = sys;
    aout->start = Start;
    aout->stop = Stop;
    aout->volume_set = SimpleVolumeSet; /* FIXME */
    aout->mute_set = SimpleMuteSet;
    return VLC_SUCCESS;
error:
    free(sys);
    return VLC_EGENERIC;
}

static void Close(vlc_object_t *obj)
{
    audio_output_t *aout = (audio_output_t *)obj;
    aout_sys_t *sys = aout->sys;

    var_Destroy (aout, "audio-device");

    Enter();
    IMMDeviceEnumerator_Release(sys->it);
    Leave();

    free(sys);
}