/*******************************Module*Header*********************************\
* Module Name: playwav.c
*
* Sound support routines for NT - ported from Windows 3.1 Sonic
*
* Created:
* Author:
* Jan  92: Ported to Win32 - SteveDav
*
* History:
*
* Copyright (c) 1992 Microsoft Corporation
*
\******************************************************************************/
#define UNICODE

#define MMNOTIMER
#define MMNOSEQ
#define MMNOJOY
#define MMNOMIDI
#define MMNOMCI

#include "winmmi.h"
#include "playwav.h"

//
// These globals are used to keep track of the currently playing sound, and
// the handle to the wave device.  only 1 sound can be playing at a time.
//

STATICDT HWAVEOUT    hWaveOut;         // handle to open wave device
LPWAVEHDR   lpWavHdr;         // current wave file playing
CRITICAL_SECTION WavHdrCritSec;
#define EnterWavHdr()   EnterCriticalSection(&WavHdrCritSec);
#define LeaveWavHdr()   LeaveCriticalSection(&WavHdrCritSec);

/* flags for _lseek */
#define  SEEK_CUR 1
#define  SEEK_END 2
#define  SEEK_SET 0

#define FMEM                (GMEM_MOVEABLE)

STATICFN BOOL  NEAR PASCAL soundInitWavHdr(LPWAVEHDR lpwh, LPBYTE lpMem, DWORD dwLen);
STATICFN BOOL  NEAR PASCAL soundOpen(HANDLE  hSound, UINT wFlags);
STATICFN BOOL  NEAR PASCAL soundClose(void);
STATICFN void  NEAR PASCAL soundWait(void);

/*****************************************************************************
 * @doc INTERNAL
 *
 * @api void | WaveOutNotify  | called by mmWndProc when it receives a
 *                              MM_WOM_DONE message
 * @rdesc None.
 *
 ****************************************************************************/

void FAR PASCAL WaveOutNotify(
    DWORD wParam,
    LONG lParam)
{

#if DBG
    WinAssert(!hWaveOut || lpWavHdr);  // if hWaveOut, then MUST have lpWavHdr
#endif

    if (hWaveOut && !(lpWavHdr->dwFlags & WHDR_DONE)) {
        return;         // wave is not done! get out
    }

    //
    // wave file is done! release the device
    //

    dprintf2(("ASYNC sound done, closing wave device"));

    soundClose();
}

/*****************************************************************************
 * @doc INTERNAL
 *
 * @api BOOL | soundPlay   | Pretty much speaks for itself!
 *
 * @parm HANDLE  | hSound | The sound resource to play.
 *
 * @parm wFlags | UINT | flags controlling sync/async etc.
 *
 *  @flag  SND_SYNC            | play synchronously (default)
 *  @flag  SND_ASYNC           | play asynchronously
 *
 * @rdesc Returns TRUE if successful and FALSE on failure.
 ****************************************************************************/
BOOL NEAR PASCAL soundPlay(
    HANDLE  hSound,
    UINT wFlags)
{
    //
    // Before playing a sound release it
    //
    soundClose();

    //
    // open the sound device and write the sound to it.
    //
    if (!soundOpen(hSound, wFlags)) {
        dprintf1(("Returning false after calling SoundOpen"));
        return FALSE;
    }
    dprintf2(("SoundOpen OK"));

    if (!(wFlags & SND_ASYNC))
    {
        dprintf4(("Calling SoundWait"));
        soundWait();
        dprintf4(("Calling SoundClose"));
        soundClose();
    }
    return TRUE;
}

/*****************************************************************************
 * @doc INTERNAL
 *
 * @api BOOL | soundOpen  | Open the wave device and write a sound to it.
 *
 * @parm HANDLE  | hSound | The sound resource to play.
 *
 * @rdesc Returns TRUE if successful and FALSE on failure.
 ****************************************************************************/
STATICFN BOOL NEAR PASCAL soundOpen(
    HANDLE  hSound,
    UINT    wFlags)
{
    UINT        wErr;
    DWORD       flags = WAVE_ALLOWSYNC;
    BOOL        fResult = FALSE;

    if (!hSound) {
        return FALSE;
    }

    if (hWaveOut)
    {
        dprintf1(("WINMM: soundOpen() wave device is currently open."));
        return FALSE;
    }

    try {
        EnterWavHdr();
        lpWavHdr = (LPWAVEHDR)GlobalLock(hSound);

        if (!lpWavHdr)
        {
#if DBG
            if ((GlobalFlags(hSound) & GMEM_DISCARDED)) {
                dprintf1(("WINMM: sound was discarded before play could begin."));
            }
#endif
            goto exit;
        }

        //
        // open the wave device, open any wave device that supports the
        // format
        //
        if (hwndNotify) {
            flags |= CALLBACK_WINDOW;
        }

        wErr = waveOutOpen(&hWaveOut,           // returns handle to device
                (UINT)WAVE_MAPPER,              // device id (any device)
                (LPWAVEFORMATEX)lpWavHdr->dwUser, // wave format
                (DWORD)hwndNotify,              // callback function
                0L,                             // callback instance data
                flags);                         // flags

        if (wErr != 0)
        {
            dprintf1(("WINMM: soundOpen() unable to open wave device"));
            GlobalUnlock(hSound);
            lpWavHdr = NULL;
            hWaveOut = NULL;
            goto exit;
        }

        wErr = waveOutPrepareHeader(hWaveOut, lpWavHdr, sizeof(WAVEHDR));

        if (wErr != 0)
        {
            dprintf1(("WINMM: soundOpen() waveOutPrepare failed"));
            soundClose();
            goto exit;
        }

        //
        // Only allow sound looping if playing ASYNC sounds
        //
        if ((wFlags & SND_ASYNC) && (wFlags & SND_LOOP))
        {
            lpWavHdr->dwLoops  = 0xFFFFFFFF;     // infinite loop
            lpWavHdr->dwFlags |= WHDR_BEGINLOOP|WHDR_ENDLOOP;
        }
        else
        {
            lpWavHdr->dwLoops  = 0;
            lpWavHdr->dwFlags &=~(WHDR_BEGINLOOP|WHDR_ENDLOOP);
        }

        lpWavHdr->dwFlags &= ~WHDR_DONE;        // mark as not done!
        wErr = waveOutWrite(hWaveOut, lpWavHdr, sizeof(WAVEHDR));

        if (wErr != 0)
        {
            dprintf1(("WINMM: soundOpen() waveOutWrite failed"));
            soundClose();
            goto exit;
        }
        fResult = TRUE;
        exit: ;

    } finally {
        LeaveWavHdr();
    }
    return fResult;
}

/*****************************************************************************
 * @doc INTERNAL
 *
 * @func BOOL | soundClose | This function closes the sound device
 *
 * @rdesc Returns TRUE if successful and FALSE on failure.
 ****************************************************************************/
STATICFN BOOL NEAR PASCAL soundClose(
    void)
{
    UINT        wErr;

    //
    // Do we have the sound device open?
    //
try {
    EnterWavHdr();

    if (!lpWavHdr || !hWaveOut) {
        // return TRUE;
    } else {

        //
        // if the block is still playing, stop it!
        //
        if (!(lpWavHdr->dwFlags & WHDR_DONE)) {
            waveOutReset(hWaveOut);
        }

#if DBG
        if (!(lpWavHdr->dwFlags & WHDR_DONE))
        {
            dprintf1(("WINMM: soundClose() data is not DONE!???"));
            lpWavHdr->dwFlags |= WHDR_DONE;
        }

        if (!(lpWavHdr->dwFlags & WHDR_PREPARED))
        {
            dprintf1(("WINMM: soundClose() data not prepared???"));
        }
#endif

        //
        // unprepare the data anyway!
        //
        wErr = waveOutUnprepareHeader(hWaveOut, lpWavHdr, sizeof(WAVEHDR));

        if (wErr != 0)
        {
            dprintf1(("WINMM: soundClose() waveOutUnprepare failed!"));
        }

        //
        // finally, actually close the device, and unlock the data
        //
        waveOutClose(hWaveOut);
        GlobalUnlock(GlobalHandle(lpWavHdr));

        //
        // update globals, claiming the device is closed.
        //
        hWaveOut = NULL;
        lpWavHdr = NULL;
    }
} finally {
    LeaveWavHdr();
}
    return TRUE;
}

/*****************************************************************************
 * @doc INTERNAL
 *
 * @api void | soundWait | wait for the sound device to complete
 *
 * @rdesc none
 ****************************************************************************/
STATICFN void NEAR PASCAL soundWait(
    void)
{

    try {                         // This should ensure that even WOW
                                  // threads that die on us depart the
                                  // critical section
        EnterWavHdr();
        if (lpWavHdr) {
            LPWAVEHDR   lpExisting;       // current playing wave file
            lpExisting = lpWavHdr;
            while (lpExisting == lpWavHdr && !(lpWavHdr->dwFlags & WHDR_DONE)) {
                dprintf4(("Waiting for buffer to complete"));
                LeaveWavHdr();
                Sleep(75);
                EnterWavHdr();
                // LATER !! We should have an event (on another thread... sigh...)
                // which will be triggered when the buffer is played.  Waiting
                // on the WHDR_DONE bit is ported directly from Win 3.1 and is
                // certainly not the best way of doing this.  The disadvantage of
                // using the thread notification is signalling this thread to
                // continue.
            }
        }
    } finally {
        LeaveWavHdr();
    }
}

/*****************************************************************************
 * @doc INTERNAL
 *
 * @api void | soundFree | This function frees a sound resource created
 *      with soundLoadFile or soundLoadMemory
 *
 * @rdesc Returns TRUE if successful and FALSE on failure.
 ****************************************************************************/
void NEAR PASCAL soundFree(
    HANDLE  hSound)
{
    // Allow a null handle to stop any pending sounds, without discarding
    // the current cached sound
    //
    // !!! we should only close the sound device iff this hSound is playing!
    //
    soundClose();

    if (hSound) {
        GlobalFree(hSound);
    }
}

/*****************************************************************************
 * @doc INTERNAL
 *
 * @api HANDLE  | soundLoadFile | Loads a specified sound resource from a
 *  file into a global, discardable object.
 *
 * @parm LPCSTR | lpszFile | The file from which to load the sound resource.
 *
 * @rdesc Returns NULL on failure, GLOBAL HANDLE to a WAVEHDR iff success
 ****************************************************************************/
HANDLE  NEAR PASCAL soundLoadFile(
    LPCWSTR szFileName)
{
    HANDLE      fh;
    DWORD       dwSize;
    LPBYTE      lpData;
    HANDLE      h;
    UINT        wNameLen;

    // open the file
    fh = CreateFile( szFileName,
                            GENERIC_READ,
                            FILE_SHARE_READ | FILE_SHARE_WRITE,
                            NULL,
                            OPEN_EXISTING,
                            FILE_ATTRIBUTE_NORMAL,
                            NULL );

    if (fh == (HANDLE)HFILE_ERROR) {
        dprintf3(("soundLoadFile: Failed to open %ls  Error is %d",szFileName, GetLastError()));
        return NULL;
    } else {
        dprintf3(("soundLoadFile: opened %ls",szFileName));
    }

    /* Get wNameLen rounded up to next WORD boundary.
     * We do not need to round up to a DWORD boundary as this value is
     * about to be multiplied by sizeof(WCHAR) which will do the additional
     * boundary alignment for us.  If we ever contemplate moving back to
     * non-UNICODE then this statement will have to be changed.  The
     * alignment is needed so that the actual wave data starts on a
     * DWORD boundary.
     */
    wNameLen = ((lstrlen(szFileName) + 1 + sizeof(WORD) - 1) /
            sizeof(WORD)) * sizeof(WORD);

#define BLOCKBYTES (sizeof(SOUNDFILE) + (wNameLen * sizeof(WCHAR)))
//   The amount of space we need to allocate - the WAVEHDR, file size, date
//   time plus the file name and a terminating null.

    dwSize = GetFileSize(fh, NULL);
    // note: could also use the C function FILELENGTH
    if (HFILE_ERROR == dwSize) {
        dprintf2(("Failed to find file size: %ls", szFileName));
        goto error1;
    }

    // allocate some discardable memory for a wave hdr, name and the file data.
    h = GlobalAlloc( FMEM + GMEM_DISCARDABLE,
                    BLOCKBYTES + dwSize );
    if (!h) {
        dprintf3(("soundLoadFile: Failed to allocate memory"));
        goto error1;
    }

    // lock it down
    if (NULL == (lpData = GlobalLock(h))) goto error2;

    // read the file into the memory block

    // NOTE:  We could, and probably should, use the file mapping functions.
    // Do this LATER
    if ( _lread( (HFILE)fh,
                 lpData + BLOCKBYTES,
                 (UINT)dwSize)
        != dwSize ) {
        goto error3;
    }

    // Save the last written time, and the file size
    ((PSOUNDFILE)lpData)->Size = dwSize;
    GetFileTime(fh, NULL, NULL, &(((PSOUNDFILE)lpData)->ft));

    // do the rest of it from the memory image
    //
    // MIPS WARNING !! Unaligned data - wNameLen is arbitrary
    //

    if (!soundInitWavHdr( (LPWAVEHDR)lpData,
                          lpData + BLOCKBYTES,
                          dwSize) )
    {
        dprintf3(("soundLoadFile: Failed to InitWaveHdr"));
        goto error3;
    }

    CloseHandle(fh);

    lstrcpyW( ((PSOUNDFILE)lpData)->Filename, szFileName);
    GlobalUnlock(h);
    return h;

error3:
    GlobalUnlock(h);
error2:
    GlobalFree(h);
error1:
    CloseHandle(fh);
    return NULL;
}

/*****************************************************************************
 * @doc INTERNAL
 *
 * @api HANDLE  | soundLoadMemory | Loads a user specified sound resource from a
 *  a memory block supplied by the caller.
 *
 * @parm LPCSTR | lpMem | Pointer to a memory image of the file
 *
 * @rdesc Returns NULL on failure, GLOBAL HANDLE to a WAVEHDR iff success
 ****************************************************************************/
HANDLE  NEAR PASCAL soundLoadMemory(
    LPBYTE  lpMem)
{
    HANDLE  h;
    LPBYTE  lp;

    // allocate some memory, for a wave hdr
    h = GlobalAlloc(FMEM, (LONG)(sizeof(SOUNDFILE) + sizeof(WCHAR)) );
    if (!h) {
        goto error1;
    }

    // lock it down
    if (NULL == (lp = GlobalLock(h))) goto error2;

    //
    // we must assume the memory pointer is correct! (hence the -1l)
    //
    if (!soundInitWavHdr( (LPWAVEHDR)lp, lpMem, (DWORD)-1l)) {
        goto error3;
    }

    //*(LPWSTR)(lp + sizeof(WAVEHDR)+sizeof(SOUNDFILE)) = '\0';   // No file name for memory file
    ((PSOUNDFILE)lp)->Filename[0] = '\0';   // No file name for memory file
    ((PSOUNDFILE)lp)->Size = 0;
    GlobalUnlock(h);
    return h;

error3:
    GlobalUnlock(h);
error2:
    GlobalFree(h);
error1:
    return NULL;
}

/*****************************************************************************
 * @doc INTERNAL
 *
 * @api BOOL | soundInitWavHdr | Initializes a WAVEHDR data structure from a
 *                         pointer to a memory image of a RIFF WAV file.
 *
 * @parm LPWAVEHDR | lpwh | Pointer to a WAVEHDR
 *
 * @parm LPCSTR | lpMem | Pointer to a memory image of a RIFF WAV file
 *
 * @rdesc Returns FALSE on failure, TRUE on success.
 *
 * @comm the dwUser field of the WAVEHDR structure is initialized to point
 * to the WAVEFORMAT structure that is inside the RIFF data
 *
 ****************************************************************************/
STATICFN BOOL NEAR PASCAL soundInitWavHdr(
    LPWAVEHDR lpwh,
    LPBYTE lpMem,
    DWORD dwLen)
{
    FPFileHeader    fpHead;
    LPWAVEFORMAT    lpFmt;
    LPBYTE          lpData;
    DWORD           dwFileSize,dwCurPos;
    DWORD           dwSize;
    DWORD           AlignError;
    DWORD           FmtSize;

    if (dwLen < sizeof(FileHeader)) {
        dprintf3(("Not a RIFF file, or not a WAVE file"));
        return FALSE;
    }

    // assume the first few bytes are the file header
    fpHead = (FPFileHeader) lpMem;

    // check that it's a valid RIFF file and a valid WAVE form.
    if (fpHead->dwRiff != RIFF_FILE || fpHead->dwWave != RIFF_WAVE ) {
        return FALSE;
    }

    dwFileSize = fpHead->dwSize;
    dwCurPos = sizeof(FileHeader);
    lpData = lpMem + sizeof(FileHeader);

    if (dwLen < dwFileSize) {     // RIFF header
        return FALSE;
    }

    // scan until we find the 'fmt' chunk
    while( 1 ) {
        if( ((FPChunkHeader)lpData)->dwCKID == RIFF_FORMAT ) {
            break; // from the while loop that's looking for it
        }
        dwCurPos += ((FPChunkHeader)lpData)->dwSize + sizeof(ChunkHeader);
        if( dwCurPos >= dwFileSize ) {
            return FALSE;
        }
        lpData += ((FPChunkHeader)lpData)->dwSize + sizeof(ChunkHeader);
    }

    // now we're at the beginning of the 'fmt' chunk data
    lpFmt = (LPWAVEFORMAT) (lpData + sizeof(ChunkHeader));

    // Save the size of the format data and check it.
    FmtSize = ((FPChunkHeader)lpData)->dwSize;
    if (FmtSize < sizeof(WAVEFORMAT)) {
        return FALSE;
    }


    // scan until we find the 'data' chunk
    lpData = lpData + ((FPChunkHeader)lpData)->dwSize + sizeof(ChunkHeader);
    while( 1 ) {
        if ( ((FPChunkHeader)lpData)->dwCKID == RIFF_CHANNEL) {
            break; // from the while loop that's looking for it
        }
        dwCurPos += ((FPChunkHeader)lpData)->dwSize + sizeof(ChunkHeader);
        if( dwCurPos >= dwFileSize ) {
            return 0;
        }
        lpData += ((FPChunkHeader)lpData)->dwSize + sizeof(ChunkHeader);
    }

    //
    // The format chunk must be aligned so move things if necessary
    // Warning - this is a hack to get round alignment problems
    //
    AlignError = ((LPBYTE)lpFmt - lpMem) % sizeof(DWORD);

    if (AlignError != 0) {
        lpFmt = (LPWAVEFORMAT)((LPBYTE)lpFmt - AlignError);
        MoveMemory(lpFmt, (LPBYTE)lpFmt + AlignError, FmtSize);
    }

    // now we're at the beginning of the 'data' chunk data
    dwSize = ((FPChunkHeader)lpData)->dwSize;
    lpData = lpData + sizeof(ChunkHeader);

    // initialize the WAVEHDR

    lpwh->lpData    = (LPSTR)lpData;    // pointer to locked data buffer
    lpwh->dwBufferLength  = dwSize;     // length of data buffer
    lpwh->dwUser    = (DWORD)lpFmt;     // for client's use
    lpwh->dwFlags   = WHDR_DONE;        // assorted flags (see defines)
    lpwh->dwLoops   = 0;

    return TRUE;
}