/*****************************************************************************
 * 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

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

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

#include <windows.h>
#include <mmreg.h>
#include <audioclient.h>
#include "mmdevice.h"

#include <vlc_codecs.h> // GUID_FMT/GUID_PRINT

#define TIMING_REPORT_DELAY VLC_TICK_FROM_MS(1000)

/* 00000092-0000-0010-8000-00aa00389b71 */
DEFINE_GUID(_KSDATAFORMAT_SUBTYPE_IEC61937_DOLBY_DIGITAL,
            WAVE_FORMAT_DOLBY_AC3_SPDIF, 0x0000, 0x0010, 0x80, 0x00,
            0x00, 0xaa, 0x00, 0x38, 0x9b, 0x71);

/* 00000008-0000-0010-8000-00aa00389b71 */
DEFINE_GUID(_KSDATAFORMAT_SUBTYPE_IEC61937_DTS,
            WAVE_FORMAT_DTS, 0x0000, 0x0010, 0x80, 0x00,
            0x00, 0xaa, 0x00, 0x38, 0x9b, 0x71);

/* 0000000b-0cea-0010-8000-00aa00389b71 */
DEFINE_GUID(_KSDATAFORMAT_SUBTYPE_IEC61937_DTS_HD,
            0x000b, 0x0cea, 0x0010, 0x80, 0x00,
            0x00, 0xaa, 0x00, 0x38, 0x9b, 0x71);

/* 0000000a-0cea-0010-8000-00aa00389b71 */
DEFINE_GUID(_KSDATAFORMAT_SUBTYPE_IEC61937_DOLBY_DIGITAL_PLUS,
            0x000a, 0x0cea, 0x0010, 0x80, 0x00,
            0x00, 0xaa, 0x00, 0x38, 0x9b, 0x71);

/* 0000000c-0cea-0010-8000-00aa00389b71 */
DEFINE_GUID(_KSDATAFORMAT_SUBTYPE_IEC61937_DOLBY_MLP,
            0x000c, 0x0cea, 0x0010, 0x80, 0x00,
            0x00, 0xaa, 0x00, 0x38, 0x9b, 0x71);

static LARGE_INTEGER freq; /* performance counters frequency */

static msftime_t GetQPC(void)
{
    LARGE_INTEGER counter;

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

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

static msftime_t GetQPC_100ns(void)
{
    LARGE_INTEGER counter;

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

    return counter.QuadPart;
}

static msftime_t (*get_qpc)(void);

static BOOL CALLBACK InitFreq(INIT_ONCE *once, void *param, void **context)
{
    (void) once; (void) context;
    LARGE_INTEGER *qpc_freq = param;
    BOOL res = QueryPerformanceFrequency(qpc_freq);
    if (res)
    {
        if (qpc_freq->QuadPart == 10000000)
            get_qpc = GetQPC_100ns;
        else
            get_qpc = GetQPC;
    }
    return res;
}

typedef struct aout_stream_sys
{
    IAudioClient *client;

#define STARTED_STATE_INIT 0
#define STARTED_STATE_OK 1
#define STARTED_STATE_ERROR 2
    uint8_t started_state;

    uint8_t chans_table[AOUT_CHAN_MAX];
    uint8_t chans_to_reorder;

    vlc_fourcc_t format; /**< Sample format */
    unsigned rate; /**< Sample rate */
    unsigned block_align;
    UINT64 written; /**< Frames written to the buffer */
    UINT32 frames; /**< Total buffer size (frames) */
    bool s24s32; /**< Output configured as S24N, but input as S32N */
    vlc_tick_t first_pts;
} aout_stream_sys_t;

/*** VLC audio output callbacks ***/
static void TimingReport(aout_stream_t *s)
{
    aout_stream_sys_t *sys = s->sys;
    void *pv;
    UINT64 pos, qpcpos, clock_freq;
    HRESULT hr;

    assert(sys->started_state == STARTED_STATE_OK);

    hr = IAudioClient_GetService(sys->client, &IID_IAudioClock, &pv);
    if (FAILED(hr))
    {
        msg_Err(s, "cannot get clock (error 0x%lX)", hr);
        return;
    }

    IAudioClock *clock = pv;

    hr = IAudioClock_GetPosition(clock, &pos, &qpcpos);
    if (SUCCEEDED(hr))
        hr = IAudioClock_GetFrequency(clock, &clock_freq);
    IAudioClock_Release(clock);
    if (FAILED(hr))
    {
        msg_Err(s, "cannot get position (error 0x%lX)", hr);
        return;
    }

    aout_stream_TimingReport(s, VLC_TICK_FROM_MSFTIME(qpcpos),
                             vlc_tick_from_frac(pos, clock_freq) +
                             sys->first_pts);

    aout_stream_TriggerTimer(s, TimingReport,
                             vlc_tick_now() + TIMING_REPORT_DELAY);

    static_assert((10000000 % CLOCK_FREQ) == 0, "Frequency conversion broken");
}

static HRESULT StartNow(aout_stream_t *s)
{
    aout_stream_sys_t *sys = s->sys;

    HRESULT hr = IAudioClient_Start(sys->client);

    sys->started_state = SUCCEEDED(hr) ? STARTED_STATE_OK : STARTED_STATE_ERROR;

    if (SUCCEEDED(hr))
        TimingReport(s);
    else
        msg_Err(s, "stream failed to start: 0x%lX", hr);

    return hr;
}

static void StartDeferredCallback(aout_stream_t *s)
{
    StartNow(s);
}

static HRESULT StartDeferred(aout_stream_t *s, vlc_tick_t date)
{
    aout_stream_sys_t *sys = s->sys;
    vlc_tick_t written = vlc_tick_from_frac(sys->written, sys->rate);
    vlc_tick_t start_date = date - written;
    vlc_tick_t start_delay = start_date - vlc_tick_now();

    /* Create or update the current timer */
    if (start_delay > 0)
    {
        aout_stream_TriggerTimer(s, StartDeferredCallback, start_date);
        msg_Dbg(s, "deferring start (%"PRId64" us)", start_delay);
    }
    else
    {
        aout_stream_DisarmTimer(s);

        /* Check started_state again, since the timer callback could have been
         * called before or while disarming the timer */
        if (sys->started_state == STARTED_STATE_INIT)
            return StartNow(s);
    }

    return S_OK;
}

static HRESULT Play(aout_stream_t *s, block_t *block, vlc_tick_t date)
{
    (void) date;
    aout_stream_sys_t *sys = s->sys;
    void *pv;
    HRESULT hr;

    if (unlikely(sys->started_state == STARTED_STATE_ERROR))
    {
        hr = E_FAIL;
        goto out;
    }
    else if (sys->started_state == STARTED_STATE_INIT)
    {
        hr = StartDeferred(s, date);
        if (FAILED(hr))
            goto out;
    }

    if (sys->first_pts == VLC_TICK_INVALID)
        sys->first_pts = block->i_pts;

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

    hr = IAudioClient_GetService(sys->client, &IID_IAudioRenderClient, &pv);
    if (FAILED(hr))
    {
        msg_Err(s, "cannot get render client (error 0x%lX)", hr);
        goto out;
    }

    IAudioRenderClient *render = pv;
    for (;;)
    {
        UINT32 frames;
        hr = IAudioClient_GetCurrentPadding(sys->client, &frames);
        if (FAILED(hr))
        {
            msg_Err(s, "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(render, frames, &dst);
        if (FAILED(hr))
        {
            msg_Err(s, "cannot get buffer (error 0x%lX)", hr);
            break;
        }

        const size_t copy = frames * sys->block_align;

        if (!sys->s24s32)
        {
            memcpy(dst, block->p_buffer, copy);
            block->p_buffer += copy;
            block->i_buffer -= copy;
        }
        else
        {
            /* Convert back S32L to S24L. The following is doing the opposite
             * of S24LDecode() from codec/araw.c */
            BYTE *end = dst + copy;
            while (dst < end)
            {
                dst[0] = block->p_buffer[1];
                dst[1] = block->p_buffer[2];
                dst[2] = block->p_buffer[3];
                dst += 3;
                block->p_buffer += 4;
                block->i_buffer -= 4;
            }

        }
        hr = IAudioRenderClient_ReleaseBuffer(render, frames, 0);
        if (FAILED(hr))
        {
            msg_Err(s, "cannot release buffer (error 0x%lX)", hr);
            break;
        }

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

        block->i_length -= vlc_tick_from_samples(frames, sys->rate);
        /* Out of buffer space, keep the block and notify the owner */
        IAudioRenderClient_Release(render);
        return S_FALSE;
    }
    IAudioRenderClient_Release(render);
out:
    block_Release(block);

    return hr;
}

static HRESULT Pause(aout_stream_t *s, bool paused)
{
    aout_stream_sys_t *sys = s->sys;
    HRESULT hr;

    if (paused)
    {
        aout_stream_DisarmTimer(s);

        if (sys->started_state == STARTED_STATE_OK)
        {
            hr = IAudioClient_Stop(sys->client);
            if (FAILED(hr))
                msg_Warn(s, "cannot stop stream (error 0x%lX)", hr);
        }
        else
            hr = S_OK;
        /* Don't reset the timer state, we won't have to start deferred again. */
    }
    else
        hr = StartNow(s);
    return hr;
}

static HRESULT Flush(aout_stream_t *s)
{
    aout_stream_sys_t *sys = s->sys;
    HRESULT hr;

    aout_stream_DisarmTimer(s);
    /* Reset the timer state, the next start need to be deferred. */
    if (sys->started_state == STARTED_STATE_OK)
    {
        sys->started_state = STARTED_STATE_INIT;
        IAudioClient_Stop(sys->client);
        hr = IAudioClient_Reset(sys->client);
    }
    else
        hr = S_OK;

    sys->first_pts = VLC_TICK_INVALID;
    if (SUCCEEDED(hr))
    {
        msg_Dbg(s, "reset");
        sys->written = 0;
    }
    else
        msg_Warn(s, "cannot reset stream (error 0x%lX)", hr);
    return hr;
}


/*** 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_HdmiToWave(WAVEFORMATEXTENSIBLE_IEC61937 *restrict wf_iec61937,
                           audio_sample_format_t *restrict audio)
{
    WAVEFORMATEXTENSIBLE *wf = &wf_iec61937->FormatExt;

    switch (audio->i_format)
    {
    case VLC_CODEC_DTSHD:
        wf->SubFormat = _KSDATAFORMAT_SUBTYPE_IEC61937_DTS_HD;
        wf->Format.nChannels = 8;
        wf->dwChannelMask = KSAUDIO_SPEAKER_7POINT1;
        audio->i_rate = 768000;
        break;
    case VLC_CODEC_EAC3:
        wf->SubFormat = _KSDATAFORMAT_SUBTYPE_IEC61937_DOLBY_DIGITAL_PLUS;
        wf->Format.nChannels = 2;
        wf->dwChannelMask = KSAUDIO_SPEAKER_5POINT1;
        break;
    case VLC_CODEC_TRUEHD:
    case VLC_CODEC_MLP:
        wf->SubFormat = _KSDATAFORMAT_SUBTYPE_IEC61937_DOLBY_MLP;
        wf->Format.nChannels = 8;
        wf->dwChannelMask = KSAUDIO_SPEAKER_7POINT1;
        audio->i_rate = 768000;
        break;
    default:
        vlc_assert_unreachable();
    }
    wf->Format.wFormatTag = WAVE_FORMAT_EXTENSIBLE;
    wf->Format.nSamplesPerSec = 192000;
    wf->Format.wBitsPerSample = 16;
    wf->Format.nBlockAlign = wf->Format.wBitsPerSample / 8 * wf->Format.nChannels;
    wf->Format.nAvgBytesPerSec = wf->Format.nSamplesPerSec * wf->Format.nBlockAlign;
    wf->Format.cbSize = sizeof (*wf_iec61937) - sizeof (wf->Format);

    wf->Samples.wValidBitsPerSample = wf->Format.wBitsPerSample;

    wf_iec61937->dwEncodedSamplesPerSec = audio->i_rate;
    wf_iec61937->dwEncodedChannelCount = audio->i_channels;
    wf_iec61937->dwAverageBytesPerSec = 0;

    audio->i_format = VLC_CODEC_SPDIFL;
    audio->i_bytes_per_frame = wf->Format.nBlockAlign;
    audio->i_frame_length = 1;
}

static void vlc_SpdifToWave(WAVEFORMATEXTENSIBLE *restrict wf,
                            audio_sample_format_t *restrict audio)
{
    switch (audio->i_format)
    {
    case VLC_CODEC_DTS:
        wf->SubFormat = _KSDATAFORMAT_SUBTYPE_IEC61937_DTS;
        break;
    case VLC_CODEC_SPDIFL:
    case VLC_CODEC_SPDIFB:
    case VLC_CODEC_A52:
        wf->SubFormat = _KSDATAFORMAT_SUBTYPE_IEC61937_DOLBY_DIGITAL;
        break;
    default:
        vlc_assert_unreachable();
    }

    wf->Format.wFormatTag = WAVE_FORMAT_EXTENSIBLE;
    wf->Format.nChannels = 2; /* To prevent channel re-ordering */
    wf->Format.nSamplesPerSec = audio->i_rate;
    wf->Format.wBitsPerSample = 16;
    wf->Format.nBlockAlign = 4; /* wf->Format.wBitsPerSample / 8 * wf->Format.nChannels  */
    wf->Format.nAvgBytesPerSec = wf->Format.nSamplesPerSec * wf->Format.nBlockAlign;
    wf->Format.cbSize = sizeof (*wf) - sizeof (wf->Format);

    wf->Samples.wValidBitsPerSample = wf->Format.wBitsPerSample;

    wf->dwChannelMask = SPEAKER_FRONT_LEFT | SPEAKER_FRONT_RIGHT;

    audio->i_format = VLC_CODEC_SPDIFL;
    audio->i_bytes_per_frame = wf->Format.nBlockAlign;
    audio->i_frame_length = 1;
}

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;
            /* fall through */
        case VLC_CODEC_FL32:
            wf->SubFormat = KSDATAFORMAT_SUBTYPE_IEEE_FLOAT;
            break;

        case VLC_CODEC_U8:
            audio->i_format = VLC_CODEC_S16N;
            /* fall through */
        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 void LogWaveFormat(struct vlc_logger *l, const WAVEFORMATEX *restrict wf)
{
    vlc_debug(l, "nChannels %d", wf->nChannels);
    vlc_debug(l, "wBitsPerSample %d", wf->wBitsPerSample);
    vlc_debug(l, "nAvgBytesPerSec %lu", wf->nAvgBytesPerSec);
    vlc_debug(l, "nSamplesPerSec %lu", wf->nSamplesPerSec);
    vlc_debug(l, "nBlockAlign %d", wf->nBlockAlign);
    vlc_debug(l, "cbSize %d", wf->cbSize);
    vlc_debug(l, "wFormatTag 0x%04X", wf->wFormatTag);

    if (wf->wFormatTag == WAVE_FORMAT_EXTENSIBLE &&
        wf->cbSize >= sizeof(WAVEFORMATEXTENSIBLE) - sizeof(WAVEFORMATEX))
    {
        const WAVEFORMATEXTENSIBLE *wfe = container_of(wf, WAVEFORMATEXTENSIBLE, Format);
        if (IsEqualIID(&wfe->SubFormat, &KSDATAFORMAT_SUBTYPE_IEEE_FLOAT))
            vlc_debug(l, "SubFormat IEEE_FLOAT");
        else if (IsEqualIID(&wfe->SubFormat, &KSDATAFORMAT_SUBTYPE_PCM))
            vlc_debug(l, "SubFormat PCM");
        else
            vlc_debug(l, "SubFormat " GUID_FMT, GUID_PRINT(wfe->SubFormat));
        vlc_debug(l, "wValidBitsPerSample %d", wfe->Samples.wValidBitsPerSample);
    }
}

static int vlc_FromWave(const WAVEFORMATEX *restrict wf,
                        audio_sample_format_t *restrict audio)
{
    uint32_t physical_channels = 0;
    if (wf->wFormatTag == WAVE_FORMAT_EXTENSIBLE &&
        wf->cbSize >= sizeof(WAVEFORMATEXTENSIBLE) - sizeof(WAVEFORMATEX))
    {
        const WAVEFORMATEXTENSIBLE *wfe = container_of(wf, WAVEFORMATEXTENSIBLE, Format);

        if (IsEqualIID(&wfe->SubFormat, &KSDATAFORMAT_SUBTYPE_IEEE_FLOAT))
        {
            switch (wf->wBitsPerSample)
            {
                case 64:
                    audio->i_format = VLC_CODEC_FL64;
                    break;
                case 32:
                    audio->i_format = VLC_CODEC_FL32;
                    break;
                default:
                    return -1;
            }
        }
        else if (IsEqualIID(&wfe->SubFormat, &KSDATAFORMAT_SUBTYPE_PCM))
        {
            switch (wf->wBitsPerSample)
            {
                case 32:
                case 24:
                    audio->i_format = VLC_CODEC_S32N;
                    break;
                case 16:
                    audio->i_format = VLC_CODEC_S16N;
                    break;
                default:
                    return -1;
            }
        }

        if (wfe->Samples.wValidBitsPerSample != wf->wBitsPerSample)
            return -1;

        for (unsigned i = 0; chans_in[i]; i++)
            if (wfe->dwChannelMask & chans_in[i])
                physical_channels |= pi_vlc_chan_order_wg4[i];
    }
    else
        return -1;

    audio->i_rate = wf->nSamplesPerSec;
    audio->i_physical_channels = 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 void Stop(aout_stream_t *s)
{
    aout_stream_sys_t *sys = s->sys;

    aout_stream_DisarmTimer(s);

    if (sys->started_state == STARTED_STATE_OK)
        IAudioClient_Stop(sys->client);

    IAudioClient_Release(sys->client);

    free(sys);
}

/*
 * This function will try to find the closest PCM format that is accepted by
 * the sound card. Exclusive here means a direct access to the sound card. The
 * format arguments and the return code of this function behave exactly like
 * IAudioClient_IsFormatSupported().
 */
static HRESULT GetExclusivePCMFormat(IAudioClient *c, const WAVEFORMATEX *pwf,
                                     WAVEFORMATEX **ppwf_closest)
{
    HRESULT hr;
    const AUDCLNT_SHAREMODE exclusive = AUDCLNT_SHAREMODE_EXCLUSIVE;

    *ppwf_closest = NULL;

    /* First try the input format */
    hr = IAudioClient_IsFormatSupported(c, exclusive, pwf, NULL);

    if (hr != AUDCLNT_E_UNSUPPORTED_FORMAT)
    {
        assert(hr != S_FALSE); /* S_FALSE reserved for shared mode */
        return hr;
    }

    /* This format come from vlc_ToWave() */
    assert(pwf->wFormatTag == WAVE_FORMAT_EXTENSIBLE);
    const WAVEFORMATEXTENSIBLE *pwfe = (void *) pwf;
    assert(IsEqualIID(&pwfe->SubFormat, &KSDATAFORMAT_SUBTYPE_IEEE_FLOAT)
        || IsEqualIID(&pwfe->SubFormat, &KSDATAFORMAT_SUBTYPE_PCM));

    /* Allocate the output closest format */
    WAVEFORMATEXTENSIBLE *pwfe_closest =
        CoTaskMemAlloc(sizeof(WAVEFORMATEXTENSIBLE));
    if (!pwfe_closest)
        return E_FAIL;
    WAVEFORMATEX *pwf_closest = &pwfe_closest->Format;

    /* Setup the fallback arrays. There are 3 properties to check: the format,
     * the samplerate and the channels. There are maximum of 4 formats to
     * check, 3 samplerates, and 2 channels configuration. So, that is a
     * maximum of 4x3x2=24 checks */

    /* The format candidates order is dependent of the input format. We don't
     * want to use a high quality format when it's not needed but we prefer to
     * use a high quality format instead of a lower one */
    static const uint16_t bits_pcm8_candidates[] =  {  8, 16, 24, 32 };
    static const uint16_t bits_pcm16_candidates[] = { 16, 24, 32, 8  };
    static const uint16_t bits_pcm24_candidates[] = { 24, 32, 16, 8  };
    static const uint16_t bits_pcm32_candidates[] = { 32, 24, 16, 8  };

    static const size_t bits_candidates_size = ARRAY_SIZE(bits_pcm8_candidates);

    const uint16_t *bits_candidates;
    switch (pwf->wBitsPerSample)
    {
        case 64: /* fall through */
        case 32: bits_candidates = bits_pcm32_candidates; break;
        case 24: bits_candidates = bits_pcm24_candidates; break;
        case 16: bits_candidates = bits_pcm16_candidates; break;
        case 8:  bits_candidates = bits_pcm8_candidates;  break;
        default: vlc_assert_unreachable();
    }

    /* Check the input samplerate, then 48kHz and 44.1khz */
    const uint32_t samplerate_candidates[] = {
        pwf->nSamplesPerSec,
        pwf->nSamplesPerSec == 48000 ? 0 : 48000,
        pwf->nSamplesPerSec == 44100 ? 0 : 44100,
    };
    const size_t samplerate_candidates_size = ARRAY_SIZE(samplerate_candidates);

    /* Check the input number of channels, then stereo */
    const uint16_t channels_candidates[] = {
        pwf->nChannels,
        pwf->nChannels == 2 ? 0 : 2,
    };
    const size_t channels_candidates_size = ARRAY_SIZE(channels_candidates);

    /* Let's try everything */
    for (size_t bits_idx = 0; bits_idx < bits_candidates_size; ++bits_idx)
    {
        uint16_t bits = bits_candidates[bits_idx];

        for (size_t samplerate_idx = 0;
             samplerate_idx < samplerate_candidates_size;
             ++samplerate_idx)
        {
            const uint32_t samplerate = samplerate_candidates[samplerate_idx];

            if (samplerate == 0)
                continue;

            for (size_t channels_idx = 0;
                 channels_idx < channels_candidates_size;
                 ++channels_idx)
            {
                const uint16_t channels = channels_candidates[channels_idx];
                if (channels == 0)
                    continue;

                pwfe_closest->Format.wFormatTag = WAVE_FORMAT_EXTENSIBLE;
                pwfe_closest->SubFormat = KSDATAFORMAT_SUBTYPE_PCM;
                pwfe_closest->Format.nSamplesPerSec = samplerate;
                pwfe_closest->Format.wBitsPerSample = bits;
                pwfe_closest->Samples.wValidBitsPerSample = bits;
                pwfe_closest->Format.nChannels = channels;
                pwfe_closest->Format.nBlockAlign = bits / 8 * channels;
                pwfe_closest->Format.nAvgBytesPerSec =
                    pwfe_closest->Format.nBlockAlign * samplerate;

                if (channels == pwf->nChannels)
                {
                    /* Use The input channel configuration */
                    pwfe_closest->dwChannelMask = pwfe->dwChannelMask;
                }
                else
                {
                    assert(channels == 2);
                    pwfe_closest->dwChannelMask =
                        SPEAKER_FRONT_LEFT|SPEAKER_FRONT_RIGHT;
                }
                pwfe_closest->Format.cbSize = pwfe->Format.cbSize;

                hr = IAudioClient_IsFormatSupported(c, exclusive,
                                                    pwf_closest, NULL);

                if (hr != AUDCLNT_E_UNSUPPORTED_FORMAT)
                {
                    if (hr == S_OK)
                    {
                        *ppwf_closest = pwf_closest;
                        /* return S_FALSE when the closest format need to be
                         * used (like IAudioClient_IsFormatSupported()) */
                        return S_FALSE;
                    }
                    assert(hr != S_FALSE); /* S_FALSE reserved for shared mode */
                    CoTaskMemFree(pwfe_closest);
                    return hr; /* Unknown error */
                }
            }
        }
    }

    /* No format found */
    CoTaskMemFree(pwfe_closest);
    return AUDCLNT_E_UNSUPPORTED_FORMAT;
}

static HRESULT Start(aout_stream_t *s, audio_sample_format_t *restrict pfmt,
                     const GUID *sid)
{
    static INIT_ONCE freq_once = INIT_ONCE_STATIC_INIT;

    if (!InitOnceExecuteOnce(&freq_once, InitFreq, &freq, NULL))
        return E_FAIL;

    aout_stream_sys_t *sys = malloc(sizeof (*sys));
    if (unlikely(sys == NULL))
        return E_OUTOFMEMORY;
    sys->client = NULL;
    sys->started_state = STARTED_STATE_INIT;

    /* Configure audio stream */
    WAVEFORMATEXTENSIBLE_IEC61937 wf_iec61937;
    WAVEFORMATEXTENSIBLE *pwfe = &wf_iec61937.FormatExt;
    WAVEFORMATEX *pwf = &pwfe->Format, *pwf_closest, *pwf_mix = NULL;
    AUDCLNT_SHAREMODE shared_mode;
    REFERENCE_TIME buffer_duration;
    audio_sample_format_t fmt = *pfmt;
    bool b_spdif = AOUT_FMT_SPDIF(&fmt);
    bool b_hdmi = AOUT_FMT_HDMI(&fmt);

    void *pv;
    HRESULT hr = aout_stream_Activate(s, &IID_IAudioClient, NULL, &pv);
    if (FAILED(hr))
    {
        msg_Err(s, "cannot activate client (error 0x%lX)", hr);
        goto error;
    }
    sys->client = pv;


    if (b_spdif || b_hdmi)
    {
        if (b_spdif)
            vlc_SpdifToWave(pwfe, &fmt);
        else
            vlc_HdmiToWave(&wf_iec61937, &fmt);

        shared_mode = AUDCLNT_SHAREMODE_EXCLUSIVE;
        /* The max buffer duration in exclusive mode is 200ms */
        buffer_duration = MSFTIME_FROM_MS(200);

        hr = IAudioClient_IsFormatSupported(sys->client, shared_mode,
                                            pwf, NULL);
        pwf_closest = NULL;
    }
    else if (AOUT_FMT_LINEAR(&fmt))
    {
        if (fmt.channel_type == AUDIO_CHANNEL_TYPE_AMBISONICS)
        {
            fmt.channel_type = AUDIO_CHANNEL_TYPE_BITMAP;

            /* Render Ambisonics on the native mix format */
            hr = IAudioClient_GetMixFormat(sys->client, &pwf_mix);
            if (FAILED(hr) || vlc_FromWave(pwf_mix, &fmt))
            {
                msg_Dbg(s, "failed to use mix format");
                LogWaveFormat(vlc_object_logger(s), pwf_mix);
                vlc_ToWave(pwfe, &fmt); /* failed, fallback to default */
            }
            else
                pwf = pwf_mix;

            /* Setup low latency in order to quickly react to ambisonics filters
             * viewpoint changes. */
            buffer_duration = MSFTIME_FROM_MS(200);
        }
        else
        {
            vlc_ToWave(pwfe, &fmt);
            buffer_duration = MSFTIME_FROM_VLC_TICK(AOUT_MAX_PREPARE_TIME);
        }

        /* Cache the var in the parent object (audio_output_t). */
        if (var_CreateGetBool(vlc_object_parent(s), "wasapi-exclusive"))
        {
            shared_mode = AUDCLNT_SHAREMODE_EXCLUSIVE;
            buffer_duration = MSFTIME_FROM_MS(200);
            hr = GetExclusivePCMFormat(sys->client, pwf, &pwf_closest);
        }
        else
        {
            shared_mode = AUDCLNT_SHAREMODE_SHARED;
            hr = IAudioClient_IsFormatSupported(sys->client, shared_mode,
                                                pwf, &pwf_closest);
        }
    }
    else
        hr = E_FAIL;


    if (FAILED(hr))
    {
        msg_Err(s, "cannot negotiate audio format (error 0x%lX)%s", hr,
                hr == AUDCLNT_E_UNSUPPORTED_FORMAT
                && fmt.i_format == VLC_CODEC_SPDIFL ?
                ": digital pass-through not supported" : "");
        goto error;
    }

    if (hr == S_FALSE)
    {
        assert(pwf_closest != NULL);
        if (vlc_FromWave(pwf_closest, &fmt))
        {
            msg_Err(s, "unsupported closest audio format");
            LogWaveFormat(vlc_object_logger(s), pwf_closest);
            CoTaskMemFree(pwf_closest);
            hr = E_INVALIDARG;
            goto error;
        }
        msg_Dbg(s, "modified format");
        pwf = pwf_closest;
    }
    else
        assert(pwf_closest == NULL);

    sys->chans_to_reorder = fmt.i_format != VLC_CODEC_SPDIFL ?
                            vlc_CheckWaveOrder(pwf, sys->chans_table) : 0;
    sys->format = fmt.i_format;
    sys->block_align = pwf->nBlockAlign;
    sys->rate = pwf->nSamplesPerSec;
    sys->s24s32 = pwf->wBitsPerSample == 24 && fmt.i_format == VLC_CODEC_S32N;
    if (sys->s24s32)
        msg_Dbg(s, "audio device configured as s24");

    if (shared_mode == AUDCLNT_SHAREMODE_SHARED)
        buffer_duration = 0;

    hr = IAudioClient_Initialize(sys->client, shared_mode,
                                 AUDCLNT_STREAMFLAGS_EVENTCALLBACK,
                                 buffer_duration,
                                 shared_mode == AUDCLNT_SHAREMODE_EXCLUSIVE ? buffer_duration : 0,
                                 pwf, sid);
    CoTaskMemFree(pwf_closest);
    if (FAILED(hr))
    {
        msg_Err(s, "cannot initialize audio client (error 0x%lX)", hr);
        goto error;
    }

    hr = IAudioClient_SetEventHandle(sys->client,
                                     aout_stream_GetBufferReadyEvent(s));
    if (FAILED(hr))
    {
        msg_Err(s, "cannot set audio client EventHandle (error 0x%lX)", hr);
        goto error;
    }

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

    REFERENCE_TIME latT, defT, minT;
    if (SUCCEEDED(IAudioClient_GetStreamLatency(sys->client, &latT))
     && SUCCEEDED(IAudioClient_GetDevicePeriod(sys->client, &defT, &minT)))
    {
        msg_Dbg(s, "maximum latency: %"PRIu64"00 ns", latT);
        msg_Dbg(s, "default period : %"PRIu64"00 ns", defT);
        msg_Dbg(s, "minimum period : %"PRIu64"00 ns", minT);
    }

    CoTaskMemFree(pwf_mix);
    *pfmt = fmt;
    sys->written = 0;
    sys->first_pts = VLC_TICK_INVALID;
    s->sys = sys;
    s->play = Play;
    s->pause = Pause;
    s->flush = Flush;
    s->stop = Stop;
    return S_OK;
error:
    CoTaskMemFree(pwf_mix);
    if (sys->client != NULL)
        IAudioClient_Release(sys->client);
    free(sys);
    return hr;
}

#define WASAPI_EXCLUSIVE_TEXT N_("Use exclusive mode")
#define WASAPI_EXCLUSIVE_LONGTEXT N_( \
    "VLC will have a direct connection of the audio endpoint device. " \
    "This mode can be used to reduce the audio latency or " \
    "to assure that the audio stream won't be modified by the OS. " \
    "This mode is more likely to fail if the soundcard format is not " \
    "handled by VLC.")

vlc_module_begin()
    set_shortname("WASAPI")
    set_description(N_("Windows Audio Session API output"))
    set_capability("aout stream", 50)
    add_bool("wasapi-exclusive", false, WASAPI_EXCLUSIVE_TEXT,
             WASAPI_EXCLUSIVE_LONGTEXT)
    set_subcategory(SUBCAT_AUDIO_AOUT)
    set_callback(Start)
vlc_module_end()
