/*
  ==============================================================================

   This file is part of the JUCE framework.
   Copyright (c) Raw Material Software Limited

   JUCE is an open source framework subject to commercial or open source
   licensing.

   By downloading, installing, or using the JUCE framework, or combining the
   JUCE framework with any other source code, object code, content or any other
   copyrightable work, you agree to the terms of the JUCE End User Licence
   Agreement, and all incorporated terms including the JUCE Privacy Policy and
   the JUCE Website Terms of Service, as applicable, which will bind you. If you
   do not agree to the terms of these agreements, we will not license the JUCE
   framework to you, and you must discontinue the installation or download
   process and cease use of the JUCE framework.

   JUCE End User Licence Agreement: https://juce.com/legal/juce-8-licence/
   JUCE Privacy Policy: https://juce.com/juce-privacy-policy
   JUCE Website Terms of Service: https://juce.com/juce-website-terms-of-service/

   Or:

   You may also use this code under the terms of the AGPLv3:
   https://www.gnu.org/licenses/agpl-3.0.en.html

   THE JUCE FRAMEWORK IS PROVIDED "AS IS" WITHOUT ANY WARRANTY, AND ALL
   WARRANTIES, WHETHER EXPRESSED OR IMPLIED, INCLUDING WARRANTY OF
   MERCHANTABILITY OR FITNESS FOR A PARTICULAR PURPOSE, ARE DISCLAIMED.

  ==============================================================================
*/

extern "C"
{
    // Declare just the minimum number of interfaces for the DSound objects that we need..
    struct DSBUFFERDESC
    {
        DWORD dwSize;
        DWORD dwFlags;
        DWORD dwBufferBytes;
        DWORD dwReserved;
        LPWAVEFORMATEX lpwfxFormat;
        GUID guid3DAlgorithm;
    };

    struct IDirectSoundBuffer;

    #undef INTERFACE
    #define INTERFACE IDirectSound
    DECLARE_INTERFACE_ (IDirectSound, IUnknown)
    {
        STDMETHOD  (QueryInterface)       (THIS_ REFIID, LPVOID*) PURE;
        STDMETHOD_ (ULONG,AddRef)         (THIS) PURE;
        STDMETHOD_ (ULONG,Release)        (THIS) PURE;
        STDMETHOD  (CreateSoundBuffer)    (THIS_ DSBUFFERDESC*, IDirectSoundBuffer**, LPUNKNOWN) PURE;
        STDMETHOD  (GetCaps)              (THIS_ void*) PURE;
        STDMETHOD  (DuplicateSoundBuffer) (THIS_ IDirectSoundBuffer*, IDirectSoundBuffer**) PURE;
        STDMETHOD  (SetCooperativeLevel)  (THIS_ HWND, DWORD) PURE;
        STDMETHOD  (Compact)              (THIS) PURE;
        STDMETHOD  (GetSpeakerConfig)     (THIS_ LPDWORD) PURE;
        STDMETHOD  (SetSpeakerConfig)     (THIS_ DWORD) PURE;
        STDMETHOD  (Initialize)           (THIS_ const GUID*) PURE;
    };

    #undef INTERFACE
    #define INTERFACE IDirectSoundBuffer
    DECLARE_INTERFACE_ (IDirectSoundBuffer, IUnknown)
    {
        STDMETHOD  (QueryInterface)       (THIS_ REFIID, LPVOID*) PURE;
        STDMETHOD_ (ULONG,AddRef)         (THIS) PURE;
        STDMETHOD_ (ULONG,Release)        (THIS) PURE;
        STDMETHOD  (GetCaps)              (THIS_ void*) PURE;
        STDMETHOD  (GetCurrentPosition)   (THIS_ LPDWORD, LPDWORD) PURE;
        STDMETHOD  (GetFormat)            (THIS_ LPWAVEFORMATEX, DWORD, LPDWORD) PURE;
        STDMETHOD  (GetVolume)            (THIS_ LPLONG) PURE;
        STDMETHOD  (GetPan)               (THIS_ LPLONG) PURE;
        STDMETHOD  (GetFrequency)         (THIS_ LPDWORD) PURE;
        STDMETHOD  (GetStatus)            (THIS_ LPDWORD) PURE;
        STDMETHOD  (Initialize)           (THIS_ IDirectSound*, DSBUFFERDESC*) PURE;
        STDMETHOD  (Lock)                 (THIS_ DWORD, DWORD, LPVOID*, LPDWORD, LPVOID*, LPDWORD, DWORD) PURE;
        STDMETHOD  (Play)                 (THIS_ DWORD, DWORD, DWORD) PURE;
        STDMETHOD  (SetCurrentPosition)   (THIS_ DWORD) PURE;
        STDMETHOD  (SetFormat)            (THIS_ const WAVEFORMATEX*) PURE;
        STDMETHOD  (SetVolume)            (THIS_ LONG) PURE;
        STDMETHOD  (SetPan)               (THIS_ LONG) PURE;
        STDMETHOD  (SetFrequency)         (THIS_ DWORD) PURE;
        STDMETHOD  (Stop)                 (THIS) PURE;
        STDMETHOD  (Unlock)               (THIS_ LPVOID, DWORD, LPVOID, DWORD) PURE;
        STDMETHOD  (Restore)              (THIS) PURE;
    };

    //==============================================================================
    struct DSCBUFFERDESC
    {
        DWORD dwSize;
        DWORD dwFlags;
        DWORD dwBufferBytes;
        DWORD dwReserved;
        LPWAVEFORMATEX lpwfxFormat;
    };

    struct IDirectSoundCaptureBuffer;

    #undef INTERFACE
    #define INTERFACE IDirectSoundCapture
    DECLARE_INTERFACE_ (IDirectSoundCapture, IUnknown)
    {
        STDMETHOD  (QueryInterface)       (THIS_ REFIID, LPVOID*) PURE;
        STDMETHOD_ (ULONG,AddRef)         (THIS) PURE;
        STDMETHOD_ (ULONG,Release)        (THIS) PURE;
        STDMETHOD  (CreateCaptureBuffer)  (THIS_ DSCBUFFERDESC*, IDirectSoundCaptureBuffer**, LPUNKNOWN) PURE;
        STDMETHOD  (GetCaps)              (THIS_ void*) PURE;
        STDMETHOD  (Initialize)           (THIS_ const GUID*) PURE;
    };

    #undef INTERFACE
    #define INTERFACE IDirectSoundCaptureBuffer
    DECLARE_INTERFACE_ (IDirectSoundCaptureBuffer, IUnknown)
    {
        STDMETHOD  (QueryInterface)       (THIS_ REFIID, LPVOID*) PURE;
        STDMETHOD_ (ULONG,AddRef)         (THIS) PURE;
        STDMETHOD_ (ULONG,Release)        (THIS) PURE;
        STDMETHOD  (GetCaps)              (THIS_ void*) PURE;
        STDMETHOD  (GetCurrentPosition)   (THIS_ LPDWORD, LPDWORD) PURE;
        STDMETHOD  (GetFormat)            (THIS_ LPWAVEFORMATEX, DWORD, LPDWORD) PURE;
        STDMETHOD  (GetStatus)            (THIS_ LPDWORD) PURE;
        STDMETHOD  (Initialize)           (THIS_ IDirectSoundCapture*, DSCBUFFERDESC*) PURE;
        STDMETHOD  (Lock)                 (THIS_ DWORD, DWORD, LPVOID*, LPDWORD, LPVOID*, LPDWORD, DWORD) PURE;
        STDMETHOD  (Start)                (THIS_ DWORD) PURE;
        STDMETHOD  (Stop)                 (THIS) PURE;
        STDMETHOD  (Unlock)               (THIS_ LPVOID, DWORD, LPVOID, DWORD) PURE;
    };

    #undef INTERFACE
}

namespace juce
{

//==============================================================================
namespace DSoundLogging
{
    static String getErrorMessage (HRESULT hr)
    {
        const char* result = nullptr;

        switch (hr)
        {
            case MAKE_HRESULT (1, 0x878, 10):    result = "Device already allocated"; break;
            case MAKE_HRESULT (1, 0x878, 30):    result = "Control unavailable"; break;
            case E_INVALIDARG:                   result = "Invalid parameter"; break;
            case MAKE_HRESULT (1, 0x878, 50):    result = "Invalid call"; break;
            case E_FAIL:                         result = "Generic error"; break;
            case MAKE_HRESULT (1, 0x878, 70):    result = "Priority level error"; break;
            case E_OUTOFMEMORY:                  result = "Out of memory"; break;
            case MAKE_HRESULT (1, 0x878, 100):   result = "Bad format"; break;
            case E_NOTIMPL:                      result = "Unsupported function"; break;
            case MAKE_HRESULT (1, 0x878, 120):   result = "No driver"; break;
            case MAKE_HRESULT (1, 0x878, 130):   result = "Already initialised"; break;
            case CLASS_E_NOAGGREGATION:          result = "No aggregation"; break;
            case MAKE_HRESULT (1, 0x878, 150):   result = "Buffer lost"; break;
            case MAKE_HRESULT (1, 0x878, 160):   result = "Another app has priority"; break;
            case MAKE_HRESULT (1, 0x878, 170):   result = "Uninitialised"; break;
            case E_NOINTERFACE:                  result = "No interface"; break;
            case S_OK:                           result = "No error"; break;
            default:                             return "Unknown error: " + String ((int) hr);
        }

        return result;
    }

    //==============================================================================
   #if JUCE_DIRECTSOUND_LOGGING
    static void logMessage (String message)
    {
        message = "DSOUND: " + message;
        DBG (message);
        Logger::writeToLog (message);
    }

    static void logError (HRESULT hr, int lineNum)
    {
        if (FAILED (hr))
        {
            String error ("Error at line ");
            error << lineNum << ": " << getErrorMessage (hr);
            logMessage (error);
        }
    }

    #define JUCE_DS_LOG(a)        DSoundLogging::logMessage(a);
    #define JUCE_DS_LOG_ERROR(a)  DSoundLogging::logError(a, __LINE__);
   #else
    #define JUCE_DS_LOG(a)
    #define JUCE_DS_LOG_ERROR(a)
   #endif
}

//==============================================================================
namespace
{
    #define DSOUND_FUNCTION(functionName, params) \
        typedef HRESULT (WINAPI *type##functionName) params; \
        static type##functionName ds##functionName = nullptr;

    #define DSOUND_FUNCTION_LOAD(functionName) \
        JUCE_BEGIN_IGNORE_WARNINGS_GCC_LIKE ("-Wcast-function-type") \
        ds##functionName = (type##functionName) GetProcAddress (h, #functionName); \
        JUCE_END_IGNORE_WARNINGS_GCC_LIKE \
        jassert (ds##functionName != nullptr);

    typedef BOOL (CALLBACK *LPDSENUMCALLBACKW) (LPGUID, LPCWSTR, LPCWSTR, LPVOID);
    typedef BOOL (CALLBACK *LPDSENUMCALLBACKA) (LPGUID, LPCSTR, LPCSTR, LPVOID);

    DSOUND_FUNCTION (DirectSoundCreate, (const GUID*, IDirectSound**, LPUNKNOWN))
    DSOUND_FUNCTION (DirectSoundCaptureCreate, (const GUID*, IDirectSoundCapture**, LPUNKNOWN))
    DSOUND_FUNCTION (DirectSoundEnumerateW, (LPDSENUMCALLBACKW, LPVOID))
    DSOUND_FUNCTION (DirectSoundCaptureEnumerateW, (LPDSENUMCALLBACKW, LPVOID))

    void initialiseDSoundFunctions()
    {
        if (dsDirectSoundCreate == nullptr)
        {
            if (auto* h = LoadLibraryA ("dsound.dll"))
            {
                DSOUND_FUNCTION_LOAD (DirectSoundCreate)
                DSOUND_FUNCTION_LOAD (DirectSoundCaptureCreate)
                DSOUND_FUNCTION_LOAD (DirectSoundEnumerateW)
                DSOUND_FUNCTION_LOAD (DirectSoundCaptureEnumerateW)

                return;
            }

            jassertfalse;
        }
    }

    // the overall size of buffer used is this value x the block size
    enum { blocksPerOverallBuffer = 16 };
}

//==============================================================================
class DSoundInternalOutChannel
{
public:
    DSoundInternalOutChannel (const String& name_, const GUID& guid_, int rate,
                              int bufferSize, float* left, float* right)
        : bitDepth (16), name (name_), guid (guid_), sampleRate (rate),
          bufferSizeSamples (bufferSize), leftBuffer (left), rightBuffer (right),
          pDirectSound (nullptr), pOutputBuffer (nullptr)
    {
    }

    ~DSoundInternalOutChannel()
    {
        close();
    }

    void close()
    {
        if (pOutputBuffer != nullptr)
        {
            JUCE_DS_LOG ("closing output: " + name);
            [[maybe_unused]] HRESULT hr = pOutputBuffer->Stop();
            JUCE_DS_LOG_ERROR (hr);

            pOutputBuffer->Release();
            pOutputBuffer = nullptr;
        }

        if (pDirectSound != nullptr)
        {
            pDirectSound->Release();
            pDirectSound = nullptr;
        }
    }

    String open()
    {
        JUCE_DS_LOG ("opening output: " + name + "  rate=" + String (sampleRate)
                       + " bits=" + String (bitDepth) + " buf=" + String (bufferSizeSamples));

        pDirectSound = nullptr;
        pOutputBuffer = nullptr;
        writeOffset = 0;
        xruns = 0;

        firstPlayTime = true;
        lastPlayTime = 0;

        String error;
        HRESULT hr = E_NOINTERFACE;

        if (dsDirectSoundCreate != nullptr)
            hr = dsDirectSoundCreate (&guid, &pDirectSound, nullptr);

        if (SUCCEEDED (hr))
        {
            bytesPerBuffer = (bufferSizeSamples * (bitDepth >> 2)) & ~15;
            ticksPerBuffer = bytesPerBuffer * Time::getHighResolutionTicksPerSecond() / (sampleRate * (bitDepth >> 2));
            totalBytesPerBuffer = (blocksPerOverallBuffer * bytesPerBuffer) & ~15;
            const int numChannels = 2;

            hr = pDirectSound->SetCooperativeLevel (GetDesktopWindow(), 2 /* DSSCL_PRIORITY */);
            JUCE_DS_LOG_ERROR (hr);

            if (SUCCEEDED (hr))
            {
                IDirectSoundBuffer* pPrimaryBuffer;

                DSBUFFERDESC primaryDesc = {};
                primaryDesc.dwSize = sizeof (DSBUFFERDESC);
                primaryDesc.dwFlags = 1 /* DSBCAPS_PRIMARYBUFFER */;
                primaryDesc.dwBufferBytes = 0;
                primaryDesc.lpwfxFormat = nullptr;

                JUCE_DS_LOG ("co-op level set");
                hr = pDirectSound->CreateSoundBuffer (&primaryDesc, &pPrimaryBuffer, nullptr);
                JUCE_DS_LOG_ERROR (hr);

                if (SUCCEEDED (hr))
                {
                    WAVEFORMATEX wfFormat;
                    wfFormat.wFormatTag       = WAVE_FORMAT_PCM;
                    wfFormat.nChannels        = (unsigned short) numChannels;
                    wfFormat.nSamplesPerSec   = (DWORD) sampleRate;
                    wfFormat.wBitsPerSample   = (unsigned short) bitDepth;
                    wfFormat.nBlockAlign      = (unsigned short) (wfFormat.nChannels * wfFormat.wBitsPerSample / 8);
                    wfFormat.nAvgBytesPerSec  = wfFormat.nSamplesPerSec * wfFormat.nBlockAlign;
                    wfFormat.cbSize = 0;

                    hr = pPrimaryBuffer->SetFormat (&wfFormat);
                    JUCE_DS_LOG_ERROR (hr);

                    if (SUCCEEDED (hr))
                    {
                        DSBUFFERDESC secondaryDesc = {};
                        secondaryDesc.dwSize = sizeof (DSBUFFERDESC);
                        secondaryDesc.dwFlags =  0x8000 /* DSBCAPS_GLOBALFOCUS */
                                                  | 0x10000 /* DSBCAPS_GETCURRENTPOSITION2 */;
                        secondaryDesc.dwBufferBytes = (DWORD) totalBytesPerBuffer;
                        secondaryDesc.lpwfxFormat = &wfFormat;

                        hr = pDirectSound->CreateSoundBuffer (&secondaryDesc, &pOutputBuffer, nullptr);
                        JUCE_DS_LOG_ERROR (hr);

                        if (SUCCEEDED (hr))
                        {
                            JUCE_DS_LOG ("buffer created");

                            DWORD dwDataLen;
                            unsigned char* pDSBuffData;

                            hr = pOutputBuffer->Lock (0, (DWORD) totalBytesPerBuffer,
                                                      (LPVOID*) &pDSBuffData, &dwDataLen, nullptr, nullptr, 0);
                            JUCE_DS_LOG_ERROR (hr);

                            if (SUCCEEDED (hr))
                            {
                                zeromem (pDSBuffData, dwDataLen);

                                hr = pOutputBuffer->Unlock (pDSBuffData, dwDataLen, nullptr, 0);

                                if (SUCCEEDED (hr))
                                {
                                    hr = pOutputBuffer->SetCurrentPosition (0);

                                    if (SUCCEEDED (hr))
                                    {
                                        hr = pOutputBuffer->Play (0, 0, 1 /* DSBPLAY_LOOPING */);

                                        if (SUCCEEDED (hr))
                                            return {};
                                    }
                                }
                            }
                        }
                    }
                }
            }
        }

        error = DSoundLogging::getErrorMessage (hr);
        close();
        return error;
    }

    void synchronisePosition()
    {
        if (pOutputBuffer != nullptr)
        {
            DWORD playCursor;
            pOutputBuffer->GetCurrentPosition (&playCursor, &writeOffset);
        }
    }

    bool service()
    {
        if (pOutputBuffer == nullptr)
            return true;

        DWORD playCursor, writeCursor;

        for (;;)
        {
            HRESULT hr = pOutputBuffer->GetCurrentPosition (&playCursor, &writeCursor);

            if (hr == MAKE_HRESULT (1, 0x878, 150)) // DSERR_BUFFERLOST
            {
                pOutputBuffer->Restore();
                continue;
            }

            if (SUCCEEDED (hr))
                break;

            JUCE_DS_LOG_ERROR (hr);
            jassertfalse;
            return true;
        }

        auto currentPlayTime = Time::getHighResolutionTicks();
        if (! firstPlayTime)
        {
            auto expectedBuffers = (currentPlayTime - lastPlayTime) / ticksPerBuffer;

            playCursor += static_cast<DWORD> (expectedBuffers * bytesPerBuffer);
        }
        else
            firstPlayTime = false;

        lastPlayTime = currentPlayTime;

        int playWriteGap = (int) (writeCursor - playCursor);
        if (playWriteGap < 0)
            playWriteGap += totalBytesPerBuffer;

        int bytesEmpty = (int) (playCursor - writeOffset);
        if (bytesEmpty < 0)
            bytesEmpty += totalBytesPerBuffer;

        if (bytesEmpty > (totalBytesPerBuffer - playWriteGap))
        {
            writeOffset = writeCursor;
            bytesEmpty = totalBytesPerBuffer - playWriteGap;

            // buffer underflow
            xruns++;
        }

        if (bytesEmpty >= bytesPerBuffer)
        {
            int* buf1 = nullptr;
            int* buf2 = nullptr;
            DWORD dwSize1 = 0;
            DWORD dwSize2 = 0;

            HRESULT hr = pOutputBuffer->Lock (writeOffset, (DWORD) bytesPerBuffer,
                                              (void**) &buf1, &dwSize1,
                                              (void**) &buf2, &dwSize2, 0);

            if (hr == MAKE_HRESULT (1, 0x878, 150)) // DSERR_BUFFERLOST
            {
                pOutputBuffer->Restore();

                hr = pOutputBuffer->Lock (writeOffset, (DWORD) bytesPerBuffer,
                                          (void**) &buf1, &dwSize1,
                                          (void**) &buf2, &dwSize2, 0);
            }

            if (SUCCEEDED (hr))
            {
                if (bitDepth == 16)
                {
                    const float* left = leftBuffer;
                    const float* right = rightBuffer;
                    int samples1 = (int) (dwSize1 >> 2);
                    int samples2 = (int) (dwSize2 >> 2);

                    if (left == nullptr)
                    {
                        for (int* dest = buf1; --samples1 >= 0;)  *dest++ = convertInputValues (0, *right++);
                        for (int* dest = buf2; --samples2 >= 0;)  *dest++ = convertInputValues (0, *right++);
                    }
                    else if (right == nullptr)
                    {
                        for (int* dest = buf1; --samples1 >= 0;)  *dest++ = convertInputValues (*left++, 0);
                        for (int* dest = buf2; --samples2 >= 0;)  *dest++ = convertInputValues (*left++, 0);
                    }
                    else
                    {
                        for (int* dest = buf1; --samples1 >= 0;)  *dest++ = convertInputValues (*left++, *right++);
                        for (int* dest = buf2; --samples2 >= 0;)  *dest++ = convertInputValues (*left++, *right++);
                    }
                }
                else
                {
                    jassertfalse;
                }

                writeOffset = (writeOffset + dwSize1 + dwSize2) % (DWORD) totalBytesPerBuffer;

                pOutputBuffer->Unlock (buf1, dwSize1, buf2, dwSize2);
            }
            else
            {
                jassertfalse;
                JUCE_DS_LOG_ERROR (hr);
            }

            bytesEmpty -= bytesPerBuffer;
            return true;
        }
        else
        {
            return false;
        }
    }

    int bitDepth, xruns;
    bool doneFlag;

private:
    String name;
    GUID guid;
    int sampleRate, bufferSizeSamples;
    float* leftBuffer;
    float* rightBuffer;

    IDirectSound* pDirectSound;
    IDirectSoundBuffer* pOutputBuffer;
    DWORD writeOffset;
    int totalBytesPerBuffer, bytesPerBuffer;

    bool firstPlayTime;
    int64 lastPlayTime, ticksPerBuffer;

    static int convertInputValues (const float l, const float r) noexcept
    {
        return jlimit (-32768, 32767, roundToInt (32767.0f * r)) << 16
                | (0xffff & jlimit (-32768, 32767, roundToInt (32767.0f * l)));
    }

    JUCE_DECLARE_NON_COPYABLE (DSoundInternalOutChannel)
};

//==============================================================================
struct DSoundInternalInChannel
{
public:
    DSoundInternalInChannel (const String& name_, const GUID& guid_, int rate,
                             int bufferSize, float* left, float* right)
        : name (name_), guid (guid_), sampleRate (rate),
          bufferSizeSamples (bufferSize), leftBuffer (left), rightBuffer (right)
    {
    }

    ~DSoundInternalInChannel()
    {
        close();
    }

    void close()
    {
        if (pInputBuffer != nullptr)
        {
            JUCE_DS_LOG ("closing input: " + name);
            [[maybe_unused]] HRESULT hr = pInputBuffer->Stop();
            JUCE_DS_LOG_ERROR (hr);

            pInputBuffer->Release();
            pInputBuffer = nullptr;
        }

        if (pDirectSoundCapture != nullptr)
        {
            pDirectSoundCapture->Release();
            pDirectSoundCapture = nullptr;
        }

        if (pDirectSound != nullptr)
        {
            pDirectSound->Release();
            pDirectSound = nullptr;
        }
    }

    String open()
    {
        JUCE_DS_LOG ("opening input: " + name
                       + "  rate=" + String (sampleRate) + " bits=" + String (bitDepth) + " buf=" + String (bufferSizeSamples));

        pDirectSound = nullptr;
        pDirectSoundCapture = nullptr;
        pInputBuffer = nullptr;
        readOffset = 0;
        totalBytesPerBuffer = 0;

        HRESULT hr = dsDirectSoundCaptureCreate != nullptr
                        ? dsDirectSoundCaptureCreate (&guid, &pDirectSoundCapture, nullptr)
                        : E_NOINTERFACE;

        if (SUCCEEDED (hr))
        {
            const int numChannels = 2;
            bytesPerBuffer = (bufferSizeSamples * (bitDepth >> 2)) & ~15;
            totalBytesPerBuffer = (blocksPerOverallBuffer * bytesPerBuffer) & ~15;

            WAVEFORMATEX wfFormat;
            wfFormat.wFormatTag       = WAVE_FORMAT_PCM;
            wfFormat.nChannels        = (unsigned short)numChannels;
            wfFormat.nSamplesPerSec   = (DWORD) sampleRate;
            wfFormat.wBitsPerSample   = (unsigned short) bitDepth;
            wfFormat.nBlockAlign      = (unsigned short) (wfFormat.nChannels * (wfFormat.wBitsPerSample / 8));
            wfFormat.nAvgBytesPerSec  = wfFormat.nSamplesPerSec * wfFormat.nBlockAlign;
            wfFormat.cbSize = 0;

            DSCBUFFERDESC captureDesc = {};
            captureDesc.dwSize = sizeof (DSCBUFFERDESC);
            captureDesc.dwFlags = 0;
            captureDesc.dwBufferBytes = (DWORD) totalBytesPerBuffer;
            captureDesc.lpwfxFormat = &wfFormat;

            JUCE_DS_LOG ("object created");
            hr = pDirectSoundCapture->CreateCaptureBuffer (&captureDesc, &pInputBuffer, nullptr);

            if (SUCCEEDED (hr))
            {
                hr = pInputBuffer->Start (1 /* DSCBSTART_LOOPING */);

                if (SUCCEEDED (hr))
                    return {};
            }
        }

        JUCE_DS_LOG_ERROR (hr);
        const String error (DSoundLogging::getErrorMessage (hr));
        close();

        return error;
    }

    void synchronisePosition()
    {
        if (pInputBuffer != nullptr)
        {
            DWORD capturePos;
            pInputBuffer->GetCurrentPosition (&capturePos, (DWORD*) &readOffset);
        }
    }

    bool service()
    {
        if (pInputBuffer == nullptr)
            return true;

        DWORD capturePos, readPos;
        HRESULT hr = pInputBuffer->GetCurrentPosition (&capturePos, &readPos);
        JUCE_DS_LOG_ERROR (hr);

        if (FAILED (hr))
            return true;

        int bytesFilled = (int) (readPos - readOffset);

        if (bytesFilled < 0)
            bytesFilled += totalBytesPerBuffer;

        if (bytesFilled >= bytesPerBuffer)
        {
            short* buf1 = nullptr;
            short* buf2 = nullptr;
            DWORD dwsize1 = 0;
            DWORD dwsize2 = 0;

            hr = pInputBuffer->Lock ((DWORD) readOffset, (DWORD) bytesPerBuffer,
                                             (void**) &buf1, &dwsize1,
                                             (void**) &buf2, &dwsize2, 0);

            if (SUCCEEDED (hr))
            {
                if (bitDepth == 16)
                {
                    const float g = 1.0f / 32768.0f;

                    float* destL = leftBuffer;
                    float* destR = rightBuffer;
                    int samples1 = (int) (dwsize1 >> 2);
                    int samples2 = (int) (dwsize2 >> 2);

                    if (destL == nullptr)
                    {
                        for (const short* src = buf1; --samples1 >= 0;) { ++src; *destR++ = *src++ * g; }
                        for (const short* src = buf2; --samples2 >= 0;) { ++src; *destR++ = *src++ * g; }
                    }
                    else if (destR == nullptr)
                    {
                        for (const short* src = buf1; --samples1 >= 0;) { *destL++ = *src++ * g; ++src; }
                        for (const short* src = buf2; --samples2 >= 0;) { *destL++ = *src++ * g; ++src; }
                    }
                    else
                    {
                        for (const short* src = buf1; --samples1 >= 0;) { *destL++ = *src++ * g; *destR++ = *src++ * g; }
                        for (const short* src = buf2; --samples2 >= 0;) { *destL++ = *src++ * g; *destR++ = *src++ * g; }
                    }
                }
                else
                {
                    jassertfalse;
                }

                readOffset = (readOffset + dwsize1 + dwsize2) % (DWORD) totalBytesPerBuffer;

                pInputBuffer->Unlock (buf1, dwsize1, buf2, dwsize2);
            }
            else
            {
                JUCE_DS_LOG_ERROR (hr);
                jassertfalse;
            }

            bytesFilled -= bytesPerBuffer;

            return true;
        }
        else
        {
            return false;
        }
    }

    unsigned int readOffset;
    int bytesPerBuffer, totalBytesPerBuffer;
    int bitDepth = 16;
    bool doneFlag;

private:
    String name;
    GUID guid;
    int sampleRate, bufferSizeSamples;
    float* leftBuffer;
    float* rightBuffer;

    IDirectSound* pDirectSound = nullptr;
    IDirectSoundCapture* pDirectSoundCapture = nullptr;
    IDirectSoundCaptureBuffer* pInputBuffer = nullptr;

    JUCE_DECLARE_NON_COPYABLE (DSoundInternalInChannel)
};

//==============================================================================
class DSoundAudioIODevice final : public AudioIODevice,
                                  public Thread
{
public:
    DSoundAudioIODevice (const String& deviceName,
                         const int outputDeviceIndex_,
                         const int inputDeviceIndex_)
        : AudioIODevice (deviceName, "DirectSound"),
          Thread (SystemStats::getJUCEVersion() + ": DSound"),
          outputDeviceIndex (outputDeviceIndex_),
          inputDeviceIndex (inputDeviceIndex_)
    {
        if (outputDeviceIndex_ >= 0)
        {
            outChannels.add (TRANS ("Left"));
            outChannels.add (TRANS ("Right"));
        }

        if (inputDeviceIndex_ >= 0)
        {
            inChannels.add (TRANS ("Left"));
            inChannels.add (TRANS ("Right"));
        }
    }

    ~DSoundAudioIODevice() override
    {
        close();
    }

    String open (const BigInteger& inputChannels,
                 const BigInteger& outputChannels,
                 double newSampleRate, int newBufferSize) override
    {
        lastError = openDevice (inputChannels, outputChannels, newSampleRate, newBufferSize);
        isOpen_ = lastError.isEmpty();

        return lastError;
    }

    void close() override
    {
        stop();

        if (isOpen_)
        {
            closeDevice();
            isOpen_ = false;
        }
    }

    bool isOpen() override                              { return isOpen_ && isThreadRunning(); }
    int getCurrentBufferSizeSamples() override          { return bufferSizeSamples; }
    double getCurrentSampleRate() override              { return sampleRate; }
    BigInteger getActiveOutputChannels() const override { return enabledOutputs; }
    BigInteger getActiveInputChannels() const override  { return enabledInputs; }
    int getOutputLatencyInSamples() override            { return (int) (getCurrentBufferSizeSamples() * 1.5); }
    int getInputLatencyInSamples() override             { return getOutputLatencyInSamples(); }
    StringArray getOutputChannelNames() override        { return outChannels; }
    StringArray getInputChannelNames() override         { return inChannels; }

    Array<double> getAvailableSampleRates() override
    {
        return { 44100.0, 48000.0, 88200.0, 96000.0 };
    }

    Array<int> getAvailableBufferSizes() override
    {
        Array<int> r;
        int n = 64;

        for (int i = 0; i < 50; ++i)
        {
            r.add (n);
            n += (n < 512) ? 32
                           : ((n < 1024) ? 64
                                         : ((n < 2048) ? 128 : 256));
        }

        return r;
    }

    int getDefaultBufferSize() override                 { return 2560; }

    int getCurrentBitDepth() override
    {
        int bits = 256;

        for (int i = inChans.size(); --i >= 0;)
            bits = jmin (bits, inChans[i]->bitDepth);

        for (int i = outChans.size(); --i >= 0;)
            bits = jmin (bits, outChans[i]->bitDepth);

        if (bits > 32)
            bits = 16;

        return bits;
    }

    void start (AudioIODeviceCallback* call) override
    {
        if (isOpen_ && call != nullptr && ! isStarted)
        {
            if (! isThreadRunning())
            {
                // something gone wrong and the thread's stopped..
                isOpen_ = false;
                return;
            }

            call->audioDeviceAboutToStart (this);

            const ScopedLock sl (startStopLock);
            callback = call;
            isStarted = true;
        }
    }

    void stop() override
    {
        if (isStarted)
        {
            auto* callbackLocal = callback;

            {
                const ScopedLock sl (startStopLock);
                isStarted = false;
            }

            if (callbackLocal != nullptr)
                callbackLocal->audioDeviceStopped();
        }
    }

    bool isPlaying() override            { return isStarted && isOpen_ && isThreadRunning(); }
    String getLastError() override       { return lastError; }

    int getXRunCount() const noexcept override
    {
        return outChans[0] != nullptr ? outChans[0]->xruns : -1;
    }

    //==============================================================================
    StringArray inChannels, outChannels;
    int outputDeviceIndex, inputDeviceIndex;

private:
    bool isOpen_ = false;
    bool isStarted = false;
    String lastError;

    OwnedArray<DSoundInternalInChannel> inChans;
    OwnedArray<DSoundInternalOutChannel> outChans;
    WaitableEvent startEvent;

    int bufferSizeSamples = 0;
    double sampleRate = 0;
    BigInteger enabledInputs, enabledOutputs;
    AudioBuffer<float> inputBuffers, outputBuffers;

    AudioIODeviceCallback* callback = nullptr;
    CriticalSection startStopLock;

    String openDevice (const BigInteger& inputChannels,
                       const BigInteger& outputChannels,
                       double sampleRate_, int bufferSizeSamples_);

    void closeDevice()
    {
        isStarted = false;
        stopThread (5000);

        inChans.clear();
        outChans.clear();
    }

    void resync()
    {
        if (! threadShouldExit())
        {
            sleep (5);

            for (int i = 0; i < outChans.size(); ++i)
                outChans.getUnchecked (i)->synchronisePosition();

            for (int i = 0; i < inChans.size(); ++i)
                inChans.getUnchecked (i)->synchronisePosition();
        }
    }

public:
    void run() override
    {
        while (! threadShouldExit())
        {
            if (wait (100))
                break;
        }

        const auto latencyMs = (uint32) (bufferSizeSamples * 1000.0 / sampleRate);
        const auto maxTimeMS = jmax ((uint32) 5, 3 * latencyMs);

        while (! threadShouldExit())
        {
            int numToDo = 0;
            uint32 startTime = Time::getMillisecondCounter();

            for (int i = inChans.size(); --i >= 0;)
            {
                inChans.getUnchecked (i)->doneFlag = false;
                ++numToDo;
            }

            for (int i = outChans.size(); --i >= 0;)
            {
                outChans.getUnchecked (i)->doneFlag = false;
                ++numToDo;
            }

            if (numToDo > 0)
            {
                const int maxCount = 3;
                int count = maxCount;

                for (;;)
                {
                    for (int i = inChans.size(); --i >= 0;)
                    {
                        DSoundInternalInChannel* const in = inChans.getUnchecked (i);

                        if ((! in->doneFlag) && in->service())
                        {
                            in->doneFlag = true;
                            --numToDo;
                        }
                    }

                    for (int i = outChans.size(); --i >= 0;)
                    {
                        DSoundInternalOutChannel* const out = outChans.getUnchecked (i);

                        if ((! out->doneFlag) && out->service())
                        {
                            out->doneFlag = true;
                            --numToDo;
                        }
                    }

                    if (numToDo <= 0)
                        break;

                    if (Time::getMillisecondCounter() > startTime + maxTimeMS)
                    {
                        resync();
                        break;
                    }

                    if (--count <= 0)
                    {
                        Sleep (1);
                        count = maxCount;
                    }

                    if (threadShouldExit())
                        return;
                }
            }
            else
            {
                sleep (1);
            }

            const ScopedLock sl (startStopLock);

            if (isStarted)
            {
                callback->audioDeviceIOCallbackWithContext (inputBuffers.getArrayOfReadPointers(),
                                                            inputBuffers.getNumChannels(),
                                                            outputBuffers.getArrayOfWritePointers(),
                                                            outputBuffers.getNumChannels(),
                                                            bufferSizeSamples,
                                                            {});
            }
            else
            {
                outputBuffers.clear();
                sleep (1);
            }
        }
    }

    JUCE_DECLARE_NON_COPYABLE_WITH_LEAK_DETECTOR (DSoundAudioIODevice)
};

//==============================================================================
class DSoundDeviceList
{
    auto tie() const
    {
        return std::tie (outputDeviceNames, inputDeviceNames, outputGuids, inputGuids);
    }

public:
    StringArray outputDeviceNames, inputDeviceNames;
    Array<GUID> outputGuids, inputGuids;

    void scan()
    {
        outputDeviceNames.clear();
        inputDeviceNames.clear();
        outputGuids.clear();
        inputGuids.clear();

        if (dsDirectSoundEnumerateW != nullptr)
        {
            dsDirectSoundEnumerateW (outputEnumProcW, this);
            dsDirectSoundCaptureEnumerateW (inputEnumProcW, this);
        }
    }

    bool operator== (const DSoundDeviceList& other) const noexcept { return tie() == other.tie(); }
    bool operator!= (const DSoundDeviceList& other) const noexcept { return tie() != other.tie(); }

private:
    static BOOL enumProc (LPGUID lpGUID, String desc, StringArray& names, Array<GUID>& guids)
    {
        desc = desc.trim();

        if (desc.isNotEmpty())
        {
            const String origDesc (desc);

            int n = 2;
            while (names.contains (desc))
                desc = origDesc + " (" + String (n++) + ")";

            names.add (desc);
            guids.add (lpGUID != nullptr ? *lpGUID : GUID());
        }

        return TRUE;
    }

    BOOL outputEnumProc (LPGUID guid, LPCWSTR desc)  { return enumProc (guid, desc, outputDeviceNames, outputGuids); }
    BOOL inputEnumProc  (LPGUID guid, LPCWSTR desc)  { return enumProc (guid, desc, inputDeviceNames,  inputGuids); }

    static BOOL CALLBACK outputEnumProcW (LPGUID lpGUID, LPCWSTR description, LPCWSTR, LPVOID object)
    {
        return static_cast<DSoundDeviceList*> (object)->outputEnumProc (lpGUID, description);
    }

    static BOOL CALLBACK inputEnumProcW (LPGUID lpGUID, LPCWSTR description, LPCWSTR, LPVOID object)
    {
        return static_cast<DSoundDeviceList*> (object)->inputEnumProc (lpGUID, description);
    }
};

//==============================================================================
String DSoundAudioIODevice::openDevice (const BigInteger& inputChannels,
                                        const BigInteger& outputChannels,
                                        double sampleRate_, int bufferSizeSamples_)
{
    closeDevice();

    sampleRate = sampleRate_ > 0.0 ? sampleRate_ : 44100.0;

    if (bufferSizeSamples_ <= 0)
        bufferSizeSamples_ = 960; // use as a default size if none is set.

    bufferSizeSamples = bufferSizeSamples_ & ~7;

    DSoundDeviceList dlh;
    dlh.scan();

    enabledInputs = inputChannels;
    enabledInputs.setRange (inChannels.size(),
                            enabledInputs.getHighestBit() + 1 - inChannels.size(),
                            false);

    inputBuffers.setSize (enabledInputs.countNumberOfSetBits(), bufferSizeSamples);
    inputBuffers.clear();
    int numIns = 0;

    for (int i = 0; i <= enabledInputs.getHighestBit(); i += 2)
    {
        float* left  = enabledInputs[i]     ? inputBuffers.getWritePointer (numIns++) : nullptr;
        float* right = enabledInputs[i + 1] ? inputBuffers.getWritePointer (numIns++) : nullptr;

        if (left != nullptr || right != nullptr)
            inChans.add (new DSoundInternalInChannel (dlh.inputDeviceNames [inputDeviceIndex],
                                                      dlh.inputGuids [inputDeviceIndex],
                                                      (int) sampleRate, bufferSizeSamples,
                                                      left, right));
    }

    enabledOutputs = outputChannels;
    enabledOutputs.setRange (outChannels.size(),
                             enabledOutputs.getHighestBit() + 1 - outChannels.size(),
                             false);

    outputBuffers.setSize (enabledOutputs.countNumberOfSetBits(), bufferSizeSamples);
    outputBuffers.clear();
    int numOuts = 0;

    for (int i = 0; i <= enabledOutputs.getHighestBit(); i += 2)
    {
        float* left  = enabledOutputs[i]     ? outputBuffers.getWritePointer (numOuts++) : nullptr;
        float* right = enabledOutputs[i + 1] ? outputBuffers.getWritePointer (numOuts++) : nullptr;

        if (left != nullptr || right != nullptr)
            outChans.add (new DSoundInternalOutChannel (dlh.outputDeviceNames[outputDeviceIndex],
                                                        dlh.outputGuids [outputDeviceIndex],
                                                        (int) sampleRate, bufferSizeSamples,
                                                        left, right));
    }

    String error;

    // boost our priority while opening the devices to try to get better sync between them
    const int oldThreadPri = GetThreadPriority (GetCurrentThread());
    const DWORD oldProcPri = GetPriorityClass (GetCurrentProcess());
    SetThreadPriority (GetCurrentThread(), THREAD_PRIORITY_TIME_CRITICAL);
    SetPriorityClass (GetCurrentProcess(), REALTIME_PRIORITY_CLASS);

    for (int i = 0; i < outChans.size(); ++i)
    {
        error = outChans[i]->open();

        if (error.isNotEmpty())
        {
            error = "Error opening " + dlh.outputDeviceNames[i] + ": \"" + error + "\"";
            break;
        }
    }

    if (error.isEmpty())
    {
        for (int i = 0; i < inChans.size(); ++i)
        {
            error = inChans[i]->open();

            if (error.isNotEmpty())
            {
                error = "Error opening " + dlh.inputDeviceNames[i] + ": \"" + error + "\"";
                break;
            }
        }
    }

    if (error.isEmpty())
    {
        for (int i = 0; i < outChans.size(); ++i)
            outChans.getUnchecked (i)->synchronisePosition();

        for (int i = 0; i < inChans.size(); ++i)
            inChans.getUnchecked (i)->synchronisePosition();

        startThread (Priority::highest);
        sleep (10);

        notify();
    }
    else
    {
        JUCE_DS_LOG ("Opening failed: " + error);
    }

    SetThreadPriority (GetCurrentThread(), oldThreadPri);
    SetPriorityClass (GetCurrentProcess(), oldProcPri);

    return error;
}

//==============================================================================
class DSoundAudioIODeviceType final : public AudioIODeviceType
{
public:
    DSoundAudioIODeviceType()
        : AudioIODeviceType ("DirectSound")
    {
        initialiseDSoundFunctions();
    }

    void scanForDevices() override
    {
        hasScanned = true;
        deviceList.scan();
    }

    StringArray getDeviceNames (bool wantInputNames) const override
    {
        jassert (hasScanned); // need to call scanForDevices() before doing this

        return wantInputNames ? deviceList.inputDeviceNames
                              : deviceList.outputDeviceNames;
    }

    int getDefaultDeviceIndex (bool /*forInput*/) const override
    {
        jassert (hasScanned); // need to call scanForDevices() before doing this
        return 0;
    }

    int getIndexOfDevice (AudioIODevice* device, bool asInput) const override
    {
        jassert (hasScanned); // need to call scanForDevices() before doing this

        if (DSoundAudioIODevice* const d = dynamic_cast<DSoundAudioIODevice*> (device))
            return asInput ? d->inputDeviceIndex
                           : d->outputDeviceIndex;

        return -1;
    }

    bool hasSeparateInputsAndOutputs() const override   { return true; }

    AudioIODevice* createDevice (const String& outputDeviceName,
                                 const String& inputDeviceName) override
    {
        jassert (hasScanned); // need to call scanForDevices() before doing this

        const int outputIndex = deviceList.outputDeviceNames.indexOf (outputDeviceName);
        const int inputIndex = deviceList.inputDeviceNames.indexOf (inputDeviceName);

        if (outputIndex >= 0 || inputIndex >= 0)
            return new DSoundAudioIODevice (outputDeviceName.isNotEmpty() ? outputDeviceName
                                                                          : inputDeviceName,
                                            outputIndex, inputIndex);

        return nullptr;
    }

private:
    DeviceChangeDetector detector { L"DirectSound", [this] { systemDeviceChanged(); } };
    DSoundDeviceList deviceList;
    bool hasScanned = false;

    void systemDeviceChanged()
    {
        DSoundDeviceList newList;
        newList.scan();

        if (std::exchange (deviceList, newList) != newList)
            callDeviceChangeListeners();
    }

    JUCE_DECLARE_NON_COPYABLE_WITH_LEAK_DETECTOR (DSoundAudioIODeviceType)
};

} // namespace juce
