/* Copyright (c) 2006 Scott Gasch
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *     http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

#define _WIN32_WINNT (0x0400)
#include <windows.h>
#include <stdio.h>
#include <mmsystem.h>

#define WAV

#ifdef WAV
#define FILENAME "datafile.wav"
#pragma pack(1)
typedef struct _WAV_HEADER_FMT {
    ULONG RIFFString;
    ULONG RIFFSize;
    ULONG WAVEString;
    ULONG FmtString;
    ULONG FmtSize;
    SHORT wFormatTag;
    USHORT wChannels;
    ULONG uSamplesPerSec;
    ULONG uAvgBytesPerSec;
    USHORT wBlockAlign;
    USHORT wBitsPerSample;
    ULONG DataString;
    ULONG DataSize;
} WAV_HEADER_FMT;
#pragma pack()
#else
#define FILENAME "datafile.raw"
#endif

typedef enum _STATE {
    STATE_NONE = 0,
    STATE_PLAYING,
    STATE_RECORDING,
    STATE_PAUSED,
    STATE_STOPPING,
    STATE_FINISHED,
    NUM_STATES 
} STATE;

typedef struct _THREAD_INFO {
    HANDLE hExit;                             // signal child thread to exit
    HANDLE hReady;                            // buffer should be read/written
    HANDLE hChildReady;                       // child thread is initialized
    volatile STATE eState;
    WAVEFORMATEX WaveFormat;
    HWAVEIN hWaveIn;
    HWAVEOUT hWaveOut;
} THREAD_INFO;
THREAD_INFO g_ThreadInfo;

typedef struct _PLAY_BUFFER {
    ULONGLONG uSequenceNumber;
    BOOL fPopulated;
    BOOL fDispatched;
    WAVEHDR WaveHdr;
} PLAY_BUFFER;   

#define NUM_PLAY_BUFFERS   (8)
#define NUM_RECORD_BUFFERS (4)

ULONG WINAPI
PlaySoundProc(void *pUnused) {
    MMRESULT err;
    ULONG u;
    ULONG uRead;
    HANDLE hFile = CreateFile(FILENAME,
                              GENERIC_READ,
                              FILE_SHARE_READ,
                              NULL,
                              OPEN_EXISTING,
                              FILE_ATTRIBUTE_NORMAL |
                              FILE_FLAG_SEQUENTIAL_SCAN,
                              NULL);
    if (INVALID_HANDLE_VALUE == hFile) {
        fprintf(stderr, "PLAY: CreateFile(%s) failed, error=%u\n", FILENAME,
                GetLastError());
        goto fail;
    }

#ifdef WAV
    //
    // Read over the WAV header.  Note: there are many different ways
    // to lay out a WAV file and this program assumes that the WAV is
    // in the same format as one it would create.  We should really be
    // doing parsing of the WAV file format here.
    // 
    WAV_HEADER_FMT hdr;
    if (FALSE == ReadFile(hFile, 
                          (void *)&hdr,
                          sizeof(hdr),
                          &uRead,
                          NULL) ||
        (uRead != sizeof(hdr))) {
        fprintf(stderr, "PLAY: I/O error reading WAV header\n");
        goto fail;
    }
#endif
    
    //
    // Allocate N buffer(s) to hold uncompressed audio data.
    // 
    PLAY_BUFFER PlayBuffers[NUM_PLAY_BUFFERS];
    ZeroMemory(PlayBuffers, sizeof(PlayBuffers));
    for (u = 0; u < NUM_PLAY_BUFFERS; u++) {
        PlayBuffers[u].WaveHdr.dwBufferLength =
            g_ThreadInfo.WaveFormat.nAvgBytesPerSec * 2;
        PlayBuffers[u].WaveHdr.lpData = 
            (CHAR *)VirtualAlloc(0,
                                 PlayBuffers[u].WaveHdr.dwBufferLength,
                                 MEM_RESERVE | MEM_COMMIT,
                                 PAGE_READWRITE);
        if (NULL == PlayBuffers[u].WaveHdr.lpData) {
            fprintf(stderr, "PLAY: Failed to allocate buffer, error=%u\n",
                    GetLastError());
            goto fail;
        }
        
        err = waveOutPrepareHeader(g_ThreadInfo.hWaveOut,
                                   &(PlayBuffers[u].WaveHdr),
                                   sizeof(WAVEHDR));
        if (MMSYSERR_NOERROR != err) {
            fprintf(stderr, "PLAY: waveOutPrepareHeader failed, error=%u\n",
                    err);
            goto fail;
        }
    }
    
    //
    // Set our event telling the main thread we are ready to go.
    // 
    printf("PLAY: ready to playback audio\n");
    if (FALSE == SetEvent(g_ThreadInfo.hChildReady)) {
        goto fail;
    }
    
    //
    // Main playback loop.
    // 
    ULONGLONG uSequenceNumber = 1;
    do {
        //
        // See if there are any vacant buffers that may be populated.
        // 
        for (u = 0; u < NUM_PLAY_BUFFERS; u++) {
            if (FALSE == PlayBuffers[u].fPopulated) {
                if (FALSE == ReadFile(hFile, 
                                      PlayBuffers[u].WaveHdr.lpData,
                                      PlayBuffers[u].WaveHdr.dwBufferLength,
                                      &uRead,
                                      NULL)) {
                    fprintf(stderr, "PLAY: I/O error reading in data\n");
                    goto fail;
                }
                if (0 != uRead) {
                    PlayBuffers[u].fPopulated = TRUE;
                    PlayBuffers[u].fDispatched = FALSE;
                    PlayBuffers[u].uSequenceNumber = uSequenceNumber;
                    PlayBuffers[u].WaveHdr.dwBufferLength = uRead;
                    uSequenceNumber += 1;
                    printf("Populated buffer #%u\n", u);
                }
            }
        }
        
        //
        // See if the sound card is ready for another buffer.
        // 
        ULONG uWait = WaitForMultipleObjects(2, 
                                             &g_ThreadInfo.hExit,
                                             FALSE,
                                             10);
        if (WAIT_OBJECT_0 == uWait) {
            break;
        } else if (WAIT_OBJECT_0 + 1 == uWait) {
            if (g_ThreadInfo.eState == STATE_PLAYING) {
                ULONGLONG uMin = (ULONGLONG)-1;
                ULONG uMinLoc = (ULONG)-1;
                for (u = 0; u < NUM_PLAY_BUFFERS; u++) {
                    if (PlayBuffers[u].WaveHdr.dwFlags & WHDR_DONE) {
                        PlayBuffers[u].fPopulated = FALSE;
                        PlayBuffers[u].uSequenceNumber = 0;
                    } else if (PlayBuffers[u].uSequenceNumber < uMin) {
                        uMin = PlayBuffers[u].uSequenceNumber;
                        uMinLoc = u;
                    }
                }
                if (uMinLoc != (ULONG)-1) {
                    PlayBuffers[uMinLoc].fDispatched = TRUE;
                    err = waveOutWrite(g_ThreadInfo.hWaveOut,
                                       &(PlayBuffers[uMinLoc].WaveHdr),
                                       sizeof(WAVEHDR));                
                    if (MMSYSERR_NOERROR != err) {
                        fprintf(stderr, "PLAY: rendering error=%u\n", err);
                        goto fail;
                    }
                    printf("Dispatched buffer #%u\n", uMinLoc);
                } else {
                    printf("Nothing to do...\n");
                }
            }
        } else if (WAIT_TIMEOUT != uWait) {
            fprintf(stderr, "PLAY: Unknown error\n"); 
            goto fail;
        }
    }
    while(1);
    
 fail:
    g_ThreadInfo.eState = STATE_STOPPING;
    InterlockedIncrement(&u);
    printf("PLAY: cleaning up\n");
    for (ULONG u = 0; u < NUM_PLAY_BUFFERS; u++) {
        (void)waveOutUnprepareHeader(g_ThreadInfo.hWaveOut, 
                                     &(PlayBuffers[u].WaveHdr),
                                     sizeof(WAVEHDR));
        if (NULL != PlayBuffers[u].WaveHdr.lpData) {
            (void)VirtualFree(PlayBuffers[u].WaveHdr.lpData, 0, MEM_RELEASE);
        }
    }
    
    if (INVALID_HANDLE_VALUE != hFile) {
        (void)CloseHandle(hFile);
    }
    
    printf("PLAY: exiting\n");
    g_ThreadInfo.eState = STATE_FINISHED;
    InterlockedIncrement(&u);
    ExitThread(0);
}



ULONG WINAPI 
CaptureSoundProc(void *pUnused) {
    BOOL fStopping = FALSE;
    MMRESULT err;
    ULONG uWritten;
    ULONG uRawBytes = 0;
    ULONG u;
    
    //
    // This is the file we'll store the raw sound in.
    // 
    HANDLE hFile = CreateFile(FILENAME,
                              GENERIC_WRITE, 
                              FILE_SHARE_READ,
                              NULL, 
                              CREATE_ALWAYS, 
                              FILE_ATTRIBUTE_NORMAL, 
                              NULL);
    if (INVALID_HANDLE_VALUE == hFile) {
        fprintf(stderr, "CAPTURE: CreateFile(%s) failed, error=%u\n", FILENAME,
                GetLastError());
        goto fail;
    }
    
#ifdef WAV
    // 
    // Write an empty WAV header as a placeholder for now.
    // 
    WAV_HEADER_FMT hdr;
    ZeroMemory(&hdr, sizeof(hdr));
    if (FALSE == WriteFile(hFile, 
                           &hdr,
                           sizeof(hdr),
                           &uWritten, 
                           NULL) ||
        (uWritten != sizeof(hdr)))
    {
        fprintf(stderr, "CAPTURE: I/O error writing empty WAV header\n");
        goto fail;
    }
#endif
    
    //
    // Allocate N buffer(s) to hold uncompressed audio data.
    // 
    WAVEHDR    WaveHeader[NUM_RECORD_BUFFERS];
    ZeroMemory(WaveHeader, sizeof(WaveHeader));
    for (u = 0; u < NUM_RECORD_BUFFERS; u++) {
        WaveHeader[u].dwBufferLength = 
            g_ThreadInfo.WaveFormat.nAvgBytesPerSec * 2;
        WaveHeader[u].lpData = 
            (CHAR *)VirtualAlloc(0, 
                                 WaveHeader[u].dwBufferLength, 
                                 MEM_RESERVE | MEM_COMMIT, 
                                 PAGE_READWRITE);
        if (NULL == WaveHeader[u].lpData) {
            fprintf(stderr, "CAPTURE: Failed to allocate buffer, error=%u\n", 
                    GetLastError());
            goto fail;
        }
        
        err = waveInPrepareHeader(g_ThreadInfo.hWaveIn,
                                  &(WaveHeader[u]),
                                  sizeof(WAVEHDR));
        if (MMSYSERR_NOERROR != err) {
            fprintf(stderr, "CAPTURE: waveInPrepareHeader failed, error=%u\n",
                    err);
            goto fail;
        }
        
        err = waveInAddBuffer(g_ThreadInfo.hWaveIn, 
                              &(WaveHeader[u]), 
                              sizeof(WAVEHDR));
        if (MMSYSERR_NOERROR != err) {
            fprintf(stderr, "CAPTURE: waveInAddBuffer failed, error=%u\n", 
                    err);
            goto fail;
        }
    }

    //
    // Set our event to tell the main thread we are ready to go.
    // 
    printf("CAPTURE: ready to record\n");
    if (FALSE == SetEvent(g_ThreadInfo.hChildReady)) {
        goto fail;
    }
    
    //
    // Main recording loop.
    // 
    do {
        ULONG uWait = WaitForMultipleObjects(2, 
                                             &g_ThreadInfo.hExit,
                                             FALSE,
                                             INFINITE);
        if (uWait == WAIT_OBJECT_0) {
            break;
        } else if (uWait == WAIT_OBJECT_0 + 1) {
            if (g_ThreadInfo.eState == STATE_RECORD) {
                for (u = 0; u < NUM_RECORD_BUFFERS; u++) {
                    if (0 != WaveHeader[u].dwBytesRecorded) {
                        printf("Buffer #%u is has data... [%u bytes]\n", u,
                               WaveHeader[u].dwBytesRecorded);
                        if (FALSE == WriteFile(hFile, 
                                               WaveHeader[u].lpData, 
                                               WaveHeader[u].dwBytesRecorded, 
                                               &uWritten, 
                                               NULL) ||
                            (uWritten != WaveHeader[u].dwBytesRecorded)) {
                            fprintf(stderr, 
                                    "CAPTURE: I/O error writing data\n");
                            goto fail;
                        }
                        
                        //
                        // Note: guard against overflow here...
                        // 
                        uRawBytes += WaveHeader[u].dwBytesRecorded;
                        
                        WaveHeader[u].dwBytesRecorded = 0;
                        (void)waveInAddBuffer(g_ThreadInfo.hWaveIn, 
                                              &(WaveHeader[u]),
                                              sizeof(WaveHeader[0]));
                        continue;
                    }
                }
            }
        } else {
            fprintf(stderr, "CAPTURE: Unknown error\n");
            goto fail;
        }
    } 
    while(1);
    
 fail:
    g_ThreadInfo.eState = STATE_STOPPING;
    InterlockedIncrement(&u);
    printf("CAPTURE: cleaning up\n");
    
    if (INVALID_HANDLE_VALUE != hFile) {
        (void)FlushFileBuffers(hFile);
#ifdef WAV
        //
        // Go back and populate the WAV header now that we know the size
        // of the sample.
        // 
        hdr.RIFFString = 'FFIR';              // Note: big-endian, strangely
        hdr.RIFFSize = sizeof(WAV_HEADER_FMT) + uRawBytes - 8;
        hdr.WAVEString = 'EVAW';              // Note: big-endian again.
        hdr.FmtString = ' tmf';               // ...and again
        hdr.FmtSize = 16;
        hdr.wFormatTag = 1;
        hdr.wChannels = g_ThreadInfo.WaveFormat.nChannels;
        hdr.uSamplesPerSec = g_ThreadInfo.WaveFormat.nSamplesPerSec;
        hdr.uAvgBytesPerSec = g_ThreadInfo.WaveFormat.nAvgBytesPerSec;
        hdr.wBlockAlign = g_ThreadInfo.WaveFormat.nBlockAlign;
        hdr.wBitsPerSample = g_ThreadInfo.WaveFormat.wBitsPerSample;
        hdr.DataString = 'atad';
        hdr.DataSize = uRawBytes;
        if (0 != SetFilePointer(hFile, 0, 0, FILE_BEGIN)) {
            fprintf(stderr, "CAPTURE: Failed to fixup WAV header\n");
        }
        if (FALSE == WriteFile(hFile,
                               &hdr,
                               sizeof(hdr),
                               &uWritten, 
                               NULL) ||
            (uWritten != sizeof(hdr))) {
            fprintf(stderr, "CAPTURE: Failed to fixup WAV header\n");
        }
#endif
        (void)CloseHandle(hFile);
    }
    
    for (u = 0; u < NUM_RECORD_BUFFERS; u++) {
        (void)waveInUnprepareHeader(g_ThreadInfo.hWaveIn, 
                                    &(WaveHeader[u]), 
                                    sizeof(WAVEHDR));
        if (NULL != WaveHeader[u].lpData) {
            (void)VirtualFree(WaveHeader[u].lpData, 0, MEM_RELEASE);
        }
    }
    printf("CAPTURE: exiting\n");
    g_ThreadInfo.eState = STATE_FINISHED;
    InterlockedIncrement(&u);
    ExitThread(0);
}



void
Playback() {
    HANDLE hThread = NULL;
    
    //
    // Init the WAVEFORMATEX struct and call waveOutOpen.
    // 
    g_ThreadInfo.hWaveIn = NULL;
    ZeroMemory(&g_ThreadInfo.WaveFormat, sizeof(g_ThreadInfo.WaveFormat));
    g_ThreadInfo.WaveFormat.wFormatTag = WAVE_FORMAT_PCM;
    g_ThreadInfo.WaveFormat.nChannels = 2;
    g_ThreadInfo.WaveFormat.nSamplesPerSec = 44100;
    g_ThreadInfo.WaveFormat.wBitsPerSample = 16;
    g_ThreadInfo.WaveFormat.nBlockAlign = g_ThreadInfo.WaveFormat.nChannels * 
        (g_ThreadInfo.WaveFormat.wBitsPerSample / 8);
    g_ThreadInfo.WaveFormat.nAvgBytesPerSec = 
        g_ThreadInfo.WaveFormat.nSamplesPerSec * 
        g_ThreadInfo.WaveFormat.nBlockAlign;
    MMRESULT err = waveOutOpen(&g_ThreadInfo.hWaveOut,
                               WAVE_MAPPER, 
                               &g_ThreadInfo.WaveFormat, 
                               (DWORD_PTR)g_ThreadInfo.hReady, 
                               0,
                               CALLBACK_EVENT);
    if (err != MMSYSERR_NOERROR) {
        fprintf(stderr, "waveOutOpen failed, error=%u\n", err);
        goto end;
    }
    
    //
    // Spin up a child thread that will do the reading/playing...
    // 
    ULONG tid;
    hThread = CreateThread(NULL, 
                           0, 
                           &PlaySoundProc,
                           0, 
                           0, 
                           &tid);
    if (NULL == hThread)
    {
        fprintf(stderr, "CreateThread failed, error=%u\n", GetLastError());
        goto end;
    }
    
    //
    // Jack the child thread's priority: it's not doing anything except 
    // waiting on data and moving it around.
    // 
    if (FALSE == SetThreadPriority(hThread,
                                   THREAD_PRIORITY_TIME_CRITICAL)) {
        fprintf(stderr, "Warning: failed to boost listener thread priority, "
                "error=%u\n", GetLastError());
    }
    
    //
    // TODO: longhorn: SFIO priority level?
    // 
    
    //
    // Wait for the child thread to initialize
    // 
    if (WAIT_OBJECT_0 != WaitForSingleObject(g_ThreadInfo.hChildReady,
                                             INFINITE)) {
        fprintf(stderr, "Child thread isn't ready?!\n");
        goto end;
    }
    
    //
    // Start/stop recording...
    // 
    printf("Press enter to start playback...\n");
    getchar();
    SetEvent(g_ThreadInfo.hReady);
    printf("Playback has started.  Press enter to stop recording early...\n");
    getchar();
    waveOutReset(g_ThreadInfo.hWaveOut);
    
    //
    // Wait for the child thread to stop...
    // 
    if (FALSE == SetEvent(g_ThreadInfo.hExit)) {
        fprintf(stderr, "SetEvent failed, error=%u\n", GetLastError());
    }
    if (WAIT_OBJECT_0 != WaitForSingleObject(hThread, INFINITE)) {
        fprintf(stderr, "Child thread stuck?!\n");
        goto end;
    }
    
 end:
    //
    // Cleanup
    // 
    if (NULL != g_ThreadInfo.hWaveIn) {
        (void)waveInReset(g_ThreadInfo.hWaveIn);
        (void)waveInClose(g_ThreadInfo.hWaveIn);
    }
    if (NULL != hThread) {
        (void)CloseHandle(hThread);
    }
    (void)waveInClose(g_ThreadInfo.hWaveIn);
}

    
void
Capture() {
    HANDLE hThread = NULL;
    
    //
    // Init the WAVEFORMATEX struct and call waveInOpen.
    // 
    g_ThreadInfo.hWaveIn = NULL;
    ZeroMemory(&g_ThreadInfo.WaveFormat, sizeof(g_ThreadInfo.WaveFormat));
    g_ThreadInfo.WaveFormat.wFormatTag = WAVE_FORMAT_PCM;
    g_ThreadInfo.WaveFormat.nChannels = 2;
    g_ThreadInfo.WaveFormat.nSamplesPerSec = 44100;
    g_ThreadInfo.WaveFormat.wBitsPerSample = 16;
    g_ThreadInfo.WaveFormat.nBlockAlign = g_ThreadInfo.WaveFormat.nChannels * 
        (g_ThreadInfo.WaveFormat.wBitsPerSample / 8);
    g_ThreadInfo.WaveFormat.nAvgBytesPerSec = 
        g_ThreadInfo.WaveFormat.nSamplesPerSec * 
        g_ThreadInfo.WaveFormat.nBlockAlign;
    MMRESULT err = waveInOpen(&g_ThreadInfo.hWaveIn,
                              WAVE_MAPPER, 
                              &g_ThreadInfo.WaveFormat, 
                              (DWORD_PTR)g_ThreadInfo.hReady, 
                              0,
                              CALLBACK_EVENT);
    if (err != MMSYSERR_NOERROR) {
        fprintf(stderr, "waveInOpen failed, error=%u\n", err);
        goto end;
    }

    //
    // Spin up a child thread that will do the listening/recording.
    // 
    ULONG tid;
    hThread = CreateThread(NULL, 
                           0, 
                           &CaptureSoundProc,
                           0, 
                           0, 
                           &tid);
    if (NULL == hThread)
    {
        fprintf(stderr, "CreateThread failed, error=%u\n", GetLastError());
        goto end;
    }
    
    //
    // Jack the child thread's priority: it's not doing anything except 
    // waiting on data and moving it around.
    // 
    if (FALSE == SetThreadPriority(hThread,
                                   THREAD_PRIORITY_TIME_CRITICAL)) {
        fprintf(stderr, "Warning: failed to boost listener thread priority, "
                "error=%u\n", GetLastError());
    }
    
    //
    // TODO: longhorn: SFIO priority level?
    // 
    
    //
    // Wait for the child thread to initialize
    // 
    if (WAIT_OBJECT_0 != WaitForSingleObject(g_ThreadInfo.hChildReady,
                                             INFINITE)) {
        fprintf(stderr, "Child thread isn't ready?!\n");
        goto end;
    }
    
    //
    // Start/stop recording...
    // 
    printf("Press enter to start recording...\n");
    getchar();
    err = waveInStart(g_ThreadInfo.hWaveIn);
    if (err != MMSYSERR_NOERROR) {
        fprintf(stderr, "waveInStart failed, error=%u\n", err);
        goto end;
    }
    printf("Recording has started.  Press enter to stop recording...\n");
    getchar();
    waveInReset(g_ThreadInfo.hWaveIn);
    
    //
    // Wait for the child thread to stop...
    // 
    if (FALSE == SetEvent(g_ThreadInfo.hExit)) {
        fprintf(stderr, "SetEvent failed, error=%u\n", GetLastError());
    }
    if (WAIT_OBJECT_0 != WaitForSingleObject(hThread, INFINITE)) {
        fprintf(stderr, "Child thread stuck?!\n");
        goto end;
    }
    
 end:
    //
    // Cleanup
    // 
    if (NULL != g_ThreadInfo.hWaveIn) {
        (void)waveInReset(g_ThreadInfo.hWaveIn);
        (void)waveInClose(g_ThreadInfo.hWaveIn);
    }
    if (NULL != hThread) {
        (void)CloseHandle(hThread);
    }
    (void)waveInClose(g_ThreadInfo.hWaveIn);
}




int __cdecl 
main(int argc , char *argv[])
{
    //
    // Populate the g_hThreadInfo struct.
    // 
    ZeroMemory(&g_ThreadInfo, sizeof(g_ThreadInfo));
    g_ThreadInfo.hExit = CreateEvent(NULL, FALSE, FALSE, NULL);
    g_ThreadInfo.hReady = CreateEvent(NULL, FALSE, FALSE, NULL);
    g_ThreadInfo.hChildReady = CreateEvent(NULL, FALSE, FALSE, NULL);
    g_ThreadInfo.hPlayback = CreateEvent(NULL, FALSE, FALSE, NULL);
    if ((NULL == g_ThreadInfo.hExit) ||
        (NULL == g_ThreadInfo.hReady) ||
        (NULL == g_ThreadInfo.hChildReady) ||
        (NULL == g_ThreadInfo.hPlayback)) {
        fprintf(stderr, "CreateEvent failed, error=%u\n", GetLastError());
        goto end;
    }

    Capture();
    Playback();

 end:
    //
    // Cleanup
    // 
    if (NULL == g_ThreadInfo.hExit) {
        (void)CloseHandle(g_ThreadInfo.hExit);
    }
    
    if (NULL == g_ThreadInfo.hReady) {
        (void)CloseHandle(g_ThreadInfo.hReady);
    }
    
    if (NULL == g_ThreadInfo.hChildReady) {
        (void)CloseHandle(g_ThreadInfo.hChildReady);
    }
    
    if (NULL == g_ThreadInfo.hPlayback) {
        (void)CloseHandle(g_ThreadInfo.hPlayback);
    }
    return 0;
}
