1461 lines
46 KiB
C
1461 lines
46 KiB
C
/* -*- tab-width: 8; c-basic-offset: 4 -*- */
|
|
/*
|
|
* Sample Wine Driver for Open Sound System (featured in Linux and FreeBSD)
|
|
*
|
|
* Copyright 1994 Martin Ayotte
|
|
* 1999 Eric Pouech (async playing in waveOut)
|
|
*/
|
|
/*
|
|
* FIXME:
|
|
* pause in waveOut does not work correctly
|
|
* implement async handling in waveIn
|
|
*/
|
|
|
|
/*#define EMULATE_SB16*/
|
|
|
|
#include "config.h"
|
|
|
|
#include <stdlib.h>
|
|
#include <string.h>
|
|
#include <unistd.h>
|
|
#include <errno.h>
|
|
#include <fcntl.h>
|
|
#include <sys/ioctl.h>
|
|
#include "wine/winuser16.h"
|
|
#include "driver.h"
|
|
#include "mmddk.h"
|
|
#include "oss.h"
|
|
#include "heap.h"
|
|
#include "ldt.h"
|
|
#include "debugtools.h"
|
|
|
|
DEFAULT_DEBUG_CHANNEL(wave)
|
|
|
|
/* Allow 1% deviation for sample rates (some ES137x cards) */
|
|
#define NEAR_MATCH(rate1,rate2) (((100*((int)(rate1)-(int)(rate2)))/(rate1))==0)
|
|
|
|
#ifdef HAVE_OSS
|
|
|
|
#define SOUND_DEV "/dev/dsp"
|
|
#define MIXER_DEV "/dev/mixer"
|
|
|
|
#define MAX_WAVEOUTDRV (1)
|
|
#define MAX_WAVEINDRV (1)
|
|
|
|
/* state diagram for waveOut writing:
|
|
*
|
|
* +---------+-------------+---------------+---------------------------------+
|
|
* | state | function | event | new state |
|
|
* +---------+-------------+---------------+---------------------------------+
|
|
* | | open() | | STOPPED |
|
|
* | PAUSED | write() | | PAUSED |
|
|
* | STOPPED | write() | <thrd create> | PLAYING |
|
|
* | PLAYING | write() | HEADER | PLAYING |
|
|
* | (other) | write() | <error> | |
|
|
* | (any) | pause() | PAUSING | PAUSED |
|
|
* | PAUSED | restart() | RESTARTING | PLAYING (if no thrd => STOPPED) |
|
|
* | (any) | reset() | RESETTING | STOPPED |
|
|
* | (any) | close() | CLOSING | CLOSED |
|
|
* +---------+-------------+---------------+---------------------------------+
|
|
*/
|
|
|
|
/* states of the playing device */
|
|
#define WINE_WS_PLAYING 0
|
|
#define WINE_WS_PAUSED 1
|
|
#define WINE_WS_STOPPED 2
|
|
#define WINE_WS_CLOSED 3
|
|
|
|
/* events to be send to device */
|
|
#define WINE_WM_PAUSING (WM_USER + 1)
|
|
#define WINE_WM_RESTARTING (WM_USER + 2)
|
|
#define WINE_WM_RESETTING (WM_USER + 3)
|
|
#define WINE_WM_CLOSING (WM_USER + 4)
|
|
#define WINE_WM_HEADER (WM_USER + 5)
|
|
|
|
typedef struct {
|
|
int unixdev;
|
|
volatile int state; /* one of the WINE_WS_ manifest constants */
|
|
DWORD dwFragmentSize; /* size of OSS buffer fragment */
|
|
WAVEOPENDESC waveDesc;
|
|
WORD wFlags;
|
|
PCMWAVEFORMAT format;
|
|
LPWAVEHDR lpQueueHdr; /* pending buffers for playing */
|
|
LPWAVEHDR lpNotifyHdr; /* list of wavehdr for which write() has been called, pending for notification */
|
|
|
|
DWORD dwPlayedTotal; /* number of bytes played since opening */
|
|
DWORD dwPlayed; /* number of bytes played since last DSP_RESET */
|
|
DWORD dwNotifiedBytes; /* number of bytes for which wavehdr notification has been done */
|
|
|
|
/* info on current lpQueueHdr->lpWaveHdr */
|
|
DWORD dwOffCurrHdr; /* offset in lpQueueHdr->lpWaveHdr->lpData for fragments */
|
|
DWORD dwRemain; /* number of bytes to write to end the current fragment */
|
|
|
|
/* synchronization stuff */
|
|
HANDLE hThread;
|
|
DWORD dwThreadID;
|
|
HANDLE hEvent;
|
|
|
|
WORD wMaxFragments; /* max number of fragments that can be written to dsp */
|
|
WORD wFragsUsedInQueue; /* current number of used fragments inside dsp queue */
|
|
} WINE_WAVEOUT;
|
|
|
|
typedef struct {
|
|
int unixdev;
|
|
volatile int state;
|
|
DWORD dwFragmentSize; /* OpenSound '/dev/dsp' give us that size */
|
|
WAVEOPENDESC waveDesc;
|
|
WORD wFlags;
|
|
PCMWAVEFORMAT format;
|
|
LPWAVEHDR lpQueueHdr;
|
|
DWORD dwTotalRecorded;
|
|
} WINE_WAVEIN;
|
|
|
|
static WINE_WAVEOUT WOutDev [MAX_WAVEOUTDRV];
|
|
static WINE_WAVEIN WInDev [MAX_WAVEOUTDRV];
|
|
|
|
/*======================================================================*
|
|
* Low level WAVE implemantation *
|
|
*======================================================================*/
|
|
|
|
/**************************************************************************
|
|
* WAVE_NotifyClient [internal]
|
|
*/
|
|
static DWORD WAVE_NotifyClient(UINT wDevID, WORD wMsg, DWORD dwParam1,
|
|
DWORD dwParam2)
|
|
{
|
|
TRACE("wDevID = %04X wMsg = %d dwParm1 = %04lX dwParam2 = %04lX\n",wDevID, wMsg, dwParam1, dwParam2);
|
|
|
|
switch (wMsg) {
|
|
case WOM_OPEN:
|
|
case WOM_CLOSE:
|
|
case WOM_DONE:
|
|
if (wDevID > MAX_WAVEOUTDRV) return MCIERR_INTERNAL;
|
|
|
|
if (WOutDev[wDevID].wFlags != DCB_NULL &&
|
|
!DriverCallback(WOutDev[wDevID].waveDesc.dwCallback,
|
|
WOutDev[wDevID].wFlags,
|
|
WOutDev[wDevID].waveDesc.hWave,
|
|
wMsg,
|
|
WOutDev[wDevID].waveDesc.dwInstance,
|
|
dwParam1,
|
|
dwParam2)) {
|
|
WARN("can't notify client !\n");
|
|
return MMSYSERR_NOERROR;
|
|
}
|
|
break;
|
|
|
|
case WIM_OPEN:
|
|
case WIM_CLOSE:
|
|
case WIM_DATA:
|
|
if (wDevID > MAX_WAVEINDRV) return MCIERR_INTERNAL;
|
|
|
|
if (WInDev[wDevID].wFlags != DCB_NULL &&
|
|
!DriverCallback(WInDev[wDevID].waveDesc.dwCallback,
|
|
WInDev[wDevID].wFlags,
|
|
WInDev[wDevID].waveDesc.hWave,
|
|
wMsg,
|
|
WInDev[wDevID].waveDesc.dwInstance,
|
|
dwParam1,
|
|
dwParam2)) {
|
|
WARN("can't notify client !\n");
|
|
return MMSYSERR_NOERROR;
|
|
}
|
|
break;
|
|
default:
|
|
FIXME("Unknown CB message %u\n", wMsg);
|
|
break;
|
|
}
|
|
return 0;
|
|
}
|
|
|
|
/*======================================================================*
|
|
* Low level WAVE OUT implemantation *
|
|
*======================================================================*/
|
|
|
|
/**************************************************************************
|
|
* wodPlayer_WriteFragments [internal]
|
|
*
|
|
* wodPlayer helper. Writes as many fragments it can to unixdev.
|
|
* Returns TRUE in case of buffer underrun.
|
|
*/
|
|
static BOOL wodPlayer_WriteFragments(WINE_WAVEOUT* wwo)
|
|
{
|
|
LPWAVEHDR lpWaveHdr;
|
|
LPBYTE lpData;
|
|
int count;
|
|
|
|
for (;;) {
|
|
TRACE("Fragments: %d used on fd %d\n", wwo->wFragsUsedInQueue, wwo->unixdev);
|
|
if (wwo->wFragsUsedInQueue == wwo->wMaxFragments) /* output queue is full, wait a bit */
|
|
return FALSE;
|
|
|
|
lpWaveHdr = wwo->lpQueueHdr;
|
|
if (!lpWaveHdr) {
|
|
if (wwo->dwRemain > 0 && /* still data to send to complete current fragment */
|
|
wwo->dwNotifiedBytes >= wwo->dwFragmentSize && /* first fragment has been played */
|
|
wwo->wFragsUsedInQueue < 2) { /* done with all waveOutWrite()' fragments */
|
|
/* FIXME: should do better handling here */
|
|
TRACE("Oooch, buffer underrun !\n");
|
|
return TRUE; /* force resetting of waveOut device */
|
|
}
|
|
return FALSE; /* wait a bit */
|
|
}
|
|
|
|
if (wwo->dwOffCurrHdr == 0) {
|
|
TRACE("Starting a new wavehdr %p of %ld bytes\n", lpWaveHdr, lpWaveHdr->dwBufferLength);
|
|
if (lpWaveHdr->dwFlags & WHDR_BEGINLOOP)
|
|
FIXME("NIY: loops (%lu) in wavehdr\n", lpWaveHdr->dwLoops);
|
|
}
|
|
|
|
lpData = lpWaveHdr->lpData;
|
|
|
|
/* finish current wave hdr ? */
|
|
if (wwo->dwOffCurrHdr + wwo->dwRemain >= lpWaveHdr->dwBufferLength) {
|
|
DWORD toWrite = lpWaveHdr->dwBufferLength - wwo->dwOffCurrHdr;
|
|
|
|
/* write end of current wave hdr */
|
|
count = write(wwo->unixdev, lpData + wwo->dwOffCurrHdr, toWrite);
|
|
TRACE("write(%p[%5lu], %5lu) => %d\n", lpData, wwo->dwOffCurrHdr, toWrite, count);
|
|
|
|
if (count > 0 || toWrite == 0) {
|
|
LPWAVEHDR* wh;
|
|
|
|
/* move lpWaveHdr to the end of notify list */
|
|
for (wh = &(wwo->lpNotifyHdr); *wh; wh = &((*wh)->lpNext));
|
|
*wh = lpWaveHdr;
|
|
|
|
wwo->lpQueueHdr = lpWaveHdr->lpNext;
|
|
lpWaveHdr->lpNext = 0;
|
|
|
|
wwo->dwOffCurrHdr = 0;
|
|
if ((wwo->dwRemain -= count) == 0) {
|
|
wwo->dwRemain = wwo->dwFragmentSize;
|
|
wwo->wFragsUsedInQueue++;
|
|
}
|
|
}
|
|
continue; /* try to go to use next wavehdr */
|
|
} else {
|
|
count = write(wwo->unixdev, lpData + wwo->dwOffCurrHdr, wwo->dwRemain);
|
|
TRACE("write(%p[%5lu], %5lu) => %d\n", lpData, wwo->dwOffCurrHdr, wwo->dwRemain, count);
|
|
if (count > 0) {
|
|
wwo->dwOffCurrHdr += count;
|
|
wwo->dwRemain = wwo->dwFragmentSize;
|
|
wwo->wFragsUsedInQueue++;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
/**************************************************************************
|
|
* wodPlayer_WriteFragments [internal]
|
|
*
|
|
* wodPlayer helper. Notifies (and remove from queue) all the wavehdr which content
|
|
* have been played (actually to speaker, not to unixdev fd).
|
|
*/
|
|
static void wodPlayer_Notify(WINE_WAVEOUT* wwo, WORD uDevID, BOOL force)
|
|
{
|
|
LPWAVEHDR lpWaveHdr;
|
|
count_info cinfo;
|
|
|
|
/* get effective number of written bytes */
|
|
if (!force) {
|
|
int c;
|
|
|
|
if (ioctl(wwo->unixdev, SNDCTL_DSP_GETOPTR, &cinfo) == -1) {
|
|
perror("ioctl SNDCTL_DSP_GETOPTR");
|
|
wwo->hThread = 0;
|
|
wwo->state = WINE_WS_STOPPED;
|
|
ExitThread(-1);
|
|
}
|
|
TRACE("Played %d bytes (played=%ld) on fd %d\n", cinfo.bytes, wwo->dwPlayed, wwo->unixdev);
|
|
c = cinfo.bytes / wwo->dwFragmentSize - wwo->dwPlayed / wwo->dwFragmentSize;
|
|
if (wwo->wFragsUsedInQueue > c)
|
|
wwo->wFragsUsedInQueue -= c;
|
|
else
|
|
wwo->wFragsUsedInQueue = 0;
|
|
wwo->dwPlayed = cinfo.bytes;
|
|
}
|
|
if (force || cinfo.bytes > wwo->dwNotifiedBytes) {
|
|
/* remove all wavehdr which can be notified */
|
|
while (wwo->lpNotifyHdr &&
|
|
(force || (cinfo.bytes >= wwo->dwNotifiedBytes + wwo->lpNotifyHdr->dwBufferLength))) {
|
|
lpWaveHdr = wwo->lpNotifyHdr;
|
|
|
|
lpWaveHdr->dwFlags &= ~WHDR_INQUEUE;
|
|
lpWaveHdr->dwFlags |= WHDR_DONE;
|
|
if (!force)
|
|
wwo->dwNotifiedBytes += lpWaveHdr->dwBufferLength;
|
|
wwo->lpNotifyHdr = lpWaveHdr->lpNext;
|
|
|
|
TRACE("Notifying client with %p\n", lpWaveHdr);
|
|
if (WAVE_NotifyClient(uDevID, WOM_DONE, (DWORD)lpWaveHdr, 0) != MMSYSERR_NOERROR) {
|
|
WARN("can't notify client !\n");
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
/**************************************************************************
|
|
* wodPlayer_Reset [internal]
|
|
*
|
|
* wodPlayer helper. Resets current output stream.
|
|
*/
|
|
static void wodPlayer_Reset(WINE_WAVEOUT* wwo, WORD uDevID, BOOL reset)
|
|
{
|
|
LPWAVEHDR lpWaveHdr;
|
|
|
|
/* updates current notify list */
|
|
wodPlayer_Notify(wwo, uDevID, FALSE);
|
|
|
|
/* flush all possible output */
|
|
if (ioctl(wwo->unixdev, SNDCTL_DSP_RESET, 0) == -1) {
|
|
perror("ioctl SNDCTL_DSP_RESET");
|
|
wwo->hThread = 0;
|
|
wwo->state = WINE_WS_STOPPED;
|
|
ExitThread(-1);
|
|
}
|
|
|
|
wwo->dwOffCurrHdr = 0;
|
|
if (reset) {
|
|
/* empty notify list */
|
|
wodPlayer_Notify(wwo, uDevID, TRUE);
|
|
if (wwo->lpNotifyHdr) {
|
|
ERR("out of sync\n");
|
|
}
|
|
/* get rid also of all the current queue */
|
|
for (lpWaveHdr = wwo->lpQueueHdr; lpWaveHdr; lpWaveHdr = lpWaveHdr->lpNext) {
|
|
lpWaveHdr->dwFlags &= ~WHDR_INQUEUE;
|
|
lpWaveHdr->dwFlags |= WHDR_DONE;
|
|
|
|
if (WAVE_NotifyClient(uDevID, WOM_DONE, (DWORD)lpWaveHdr, 0) != MMSYSERR_NOERROR) {
|
|
WARN("can't notify client !\n");
|
|
}
|
|
}
|
|
wwo->lpQueueHdr = 0;
|
|
wwo->state = WINE_WS_STOPPED;
|
|
wwo->dwPlayedTotal = 0;
|
|
} else {
|
|
/* move notify list to begining of lpQueueHdr list */
|
|
while (wwo->lpNotifyHdr) {
|
|
lpWaveHdr = wwo->lpNotifyHdr;
|
|
wwo->lpNotifyHdr = lpWaveHdr->lpNext;
|
|
lpWaveHdr->lpNext = wwo->lpQueueHdr;
|
|
wwo->lpQueueHdr = lpWaveHdr;
|
|
}
|
|
wwo->state = WINE_WS_PAUSED;
|
|
wwo->dwPlayedTotal += wwo->dwPlayed;
|
|
}
|
|
wwo->dwNotifiedBytes = wwo->dwPlayed = 0;
|
|
wwo->wFragsUsedInQueue = 0;
|
|
}
|
|
|
|
/**************************************************************************
|
|
* wodPlayer [internal]
|
|
*/
|
|
static DWORD CALLBACK wodPlayer(LPVOID pmt)
|
|
{
|
|
WORD uDevID = (DWORD)pmt;
|
|
WINE_WAVEOUT* wwo = (WINE_WAVEOUT*)&WOutDev[uDevID];
|
|
WAVEHDR* lpWaveHdr;
|
|
DWORD dwSleepTime;
|
|
MSG msg;
|
|
|
|
PeekMessageA(&msg, 0, 0, 0, 0);
|
|
wwo->state = WINE_WS_STOPPED;
|
|
|
|
wwo->dwNotifiedBytes = 0;
|
|
wwo->dwOffCurrHdr = 0;
|
|
wwo->dwRemain = wwo->dwFragmentSize;
|
|
wwo->lpQueueHdr = NULL;
|
|
wwo->lpNotifyHdr = NULL;
|
|
wwo->wFragsUsedInQueue = 0;
|
|
wwo->dwPlayedTotal = 0;
|
|
wwo->dwPlayed = 0;
|
|
|
|
TRACE("imhere[0]\n");
|
|
SetEvent(wwo->hEvent);
|
|
|
|
/* make sleep time to be # of ms to output a fragment */
|
|
dwSleepTime = (wwo->dwFragmentSize * 1000) / wwo->format.wf.nAvgBytesPerSec;
|
|
|
|
for (;;) {
|
|
/* wait for dwSleepTime or an event in thread's queue */
|
|
/* FIXME: could improve wait time depending on queue state,
|
|
* ie, number of queued fragments
|
|
*/
|
|
TRACE("imhere[1]\n");
|
|
MsgWaitForMultipleObjects(0, NULL, FALSE,
|
|
(wwo->state == WINE_WS_PLAYING) ?
|
|
(MAX(wwo->wFragsUsedInQueue, 4) - 2) * dwSleepTime :
|
|
/*INFINITE*/100,
|
|
QS_POSTMESSAGE);
|
|
TRACE("imhere[2]\n");
|
|
wodPlayer_Notify(wwo, uDevID, FALSE);
|
|
while (PeekMessageA(&msg, 0, 0, 0, PM_REMOVE)) {
|
|
switch (msg.message) {
|
|
case WINE_WM_PAUSING:
|
|
wodPlayer_Reset(wwo, uDevID, FALSE);
|
|
wwo->state = WINE_WS_PAUSED;
|
|
SetEvent(wwo->hEvent);
|
|
break;
|
|
case WINE_WM_RESTARTING:
|
|
wwo->state = WINE_WS_PLAYING;
|
|
SetEvent(wwo->hEvent);
|
|
break;
|
|
case WINE_WM_HEADER:
|
|
lpWaveHdr = (LPWAVEHDR)msg.lParam;
|
|
|
|
lpWaveHdr->dwFlags &= ~WHDR_DONE;
|
|
lpWaveHdr->dwFlags |= WHDR_INQUEUE;
|
|
lpWaveHdr->lpNext = 0;
|
|
|
|
/* insert buffer at the end of queue */
|
|
{
|
|
LPWAVEHDR* wh;
|
|
for (wh = &(wwo->lpQueueHdr); *wh; wh = &((*wh)->lpNext));
|
|
*wh = lpWaveHdr;
|
|
}
|
|
if (wwo->state == WINE_WS_STOPPED)
|
|
wwo->state = WINE_WS_PLAYING;
|
|
break;
|
|
case WINE_WM_RESETTING:
|
|
wodPlayer_Reset(wwo, uDevID, TRUE);
|
|
SetEvent(wwo->hEvent);
|
|
break;
|
|
case WINE_WM_CLOSING:
|
|
/* sanity check: this should not happen since the device must have been reset before */
|
|
if (wwo->lpNotifyHdr || wwo->lpQueueHdr) {
|
|
ERR("out of sync\n");
|
|
}
|
|
wwo->hThread = 0;
|
|
wwo->state = WINE_WS_CLOSED;
|
|
SetEvent(wwo->hEvent);
|
|
ExitThread(0);
|
|
/* shouldn't go here */
|
|
default:
|
|
FIXME("unknown message %d\n", msg.message);
|
|
break;
|
|
}
|
|
}
|
|
if (wwo->state == WINE_WS_PLAYING) {
|
|
wodPlayer_WriteFragments(wwo);
|
|
}
|
|
wodPlayer_Notify(wwo, uDevID, FALSE);
|
|
}
|
|
ExitThread(0);
|
|
/* just for not generating compilation warnings... should never be executed */
|
|
return 0;
|
|
}
|
|
|
|
/**************************************************************************
|
|
* wodGetDevCaps [internal]
|
|
*/
|
|
static DWORD wodGetDevCaps(WORD wDevID, LPWAVEOUTCAPSA lpCaps, DWORD dwSize)
|
|
{
|
|
int audio;
|
|
int smplrate;
|
|
int samplesize = 16;
|
|
int dsp_stereo = 1;
|
|
int bytespersmpl;
|
|
int caps;
|
|
int mask;
|
|
|
|
TRACE("(%u, %p, %lu);\n", wDevID, lpCaps, dwSize);
|
|
|
|
if (lpCaps == NULL) return MMSYSERR_NOTENABLED;
|
|
if (access(SOUND_DEV,0) != 0) return MMSYSERR_NOTENABLED;
|
|
|
|
if (wDevID >= MAX_WAVEOUTDRV) {
|
|
TRACE("MAX_WAVOUTDRV reached !\n");
|
|
return MMSYSERR_BADDEVICEID;
|
|
}
|
|
|
|
if (WOutDev[wDevID].unixdev == 0) {
|
|
audio = open(SOUND_DEV, O_WRONLY, 0);
|
|
if (audio == -1) return MMSYSERR_ALLOCATED;
|
|
} else {
|
|
audio = WOutDev[wDevID].unixdev;
|
|
}
|
|
|
|
/* FIXME: some programs compare this string against the content of the registry
|
|
* for MM drivers. The name have to match in order the program to work
|
|
* (e.g. MS win9x mplayer.exe)
|
|
*/
|
|
#ifdef EMULATE_SB16
|
|
lpCaps->wMid = 0x0002;
|
|
lpCaps->wPid = 0x0104;
|
|
strcpy(lpCaps->szPname, "SB16 Wave Out");
|
|
#else
|
|
lpCaps->wMid = 0x00FF; /* Manufac ID */
|
|
lpCaps->wPid = 0x0001; /* Product ID */
|
|
/* strcpy(lpCaps->szPname, "OpenSoundSystem WAVOUT Driver");*/
|
|
strcpy(lpCaps->szPname, "CS4236/37/38");
|
|
#endif
|
|
lpCaps->vDriverVersion = 0x0100;
|
|
lpCaps->dwFormats = 0x00000000;
|
|
lpCaps->dwSupport = WAVECAPS_VOLUME;
|
|
|
|
IOCTL(audio, SNDCTL_DSP_GETFMTS, mask);
|
|
TRACE("OSS dsp mask=%08x\n", mask);
|
|
mask = AFMT_QUERY;
|
|
IOCTL(audio, SNDCTL_DSP_SETFMT, mask);
|
|
TRACE("OSS dsp current=%08x\n", mask);
|
|
|
|
/* First bytespersampl, then stereo */
|
|
bytespersmpl = (IOCTL(audio, SNDCTL_DSP_SAMPLESIZE, samplesize) != 0) ? 1 : 2;
|
|
|
|
lpCaps->wChannels = (IOCTL(audio, SNDCTL_DSP_STEREO, dsp_stereo) != 0) ? 1 : 2;
|
|
if (lpCaps->wChannels > 1) lpCaps->dwSupport |= WAVECAPS_LRVOLUME;
|
|
|
|
smplrate = 44100;
|
|
if (IOCTL(audio, SNDCTL_DSP_SPEED, smplrate) == 0) {
|
|
lpCaps->dwFormats |= WAVE_FORMAT_4M08;
|
|
if (lpCaps->wChannels > 1)
|
|
lpCaps->dwFormats |= WAVE_FORMAT_4S08;
|
|
if (bytespersmpl > 1) {
|
|
lpCaps->dwFormats |= WAVE_FORMAT_4M16;
|
|
if (lpCaps->wChannels > 1)
|
|
lpCaps->dwFormats |= WAVE_FORMAT_4S16;
|
|
}
|
|
}
|
|
smplrate = 22050;
|
|
if (IOCTL(audio, SNDCTL_DSP_SPEED, smplrate) == 0) {
|
|
lpCaps->dwFormats |= WAVE_FORMAT_2M08;
|
|
if (lpCaps->wChannels > 1)
|
|
lpCaps->dwFormats |= WAVE_FORMAT_2S08;
|
|
if (bytespersmpl > 1) {
|
|
lpCaps->dwFormats |= WAVE_FORMAT_2M16;
|
|
if (lpCaps->wChannels > 1)
|
|
lpCaps->dwFormats |= WAVE_FORMAT_2S16;
|
|
}
|
|
}
|
|
smplrate = 11025;
|
|
if (IOCTL(audio, SNDCTL_DSP_SPEED, smplrate) == 0) {
|
|
lpCaps->dwFormats |= WAVE_FORMAT_1M08;
|
|
if (lpCaps->wChannels > 1)
|
|
lpCaps->dwFormats |= WAVE_FORMAT_1S08;
|
|
if (bytespersmpl > 1) {
|
|
lpCaps->dwFormats |= WAVE_FORMAT_1M16;
|
|
if (lpCaps->wChannels > 1)
|
|
lpCaps->dwFormats |= WAVE_FORMAT_1S16;
|
|
}
|
|
}
|
|
if (IOCTL(audio, SNDCTL_DSP_GETCAPS, caps) == 0) {
|
|
if ((caps & DSP_CAP_REALTIME) && !(caps && DSP_CAP_BATCH))
|
|
lpCaps->dwFormats |= WAVECAPS_SAMPLEACCURATE;
|
|
}
|
|
if (WOutDev[wDevID].unixdev == 0) {
|
|
close(audio);
|
|
}
|
|
TRACE("dwFormats = %08lX\n", lpCaps->dwFormats);
|
|
return MMSYSERR_NOERROR;
|
|
}
|
|
|
|
/**************************************************************************
|
|
* wodOpen [internal]
|
|
*/
|
|
static DWORD wodOpen(WORD wDevID, LPWAVEOPENDESC lpDesc, DWORD dwFlags)
|
|
{
|
|
int audio;
|
|
int sample_rate;
|
|
int sample_size;
|
|
int dsp_stereo;
|
|
int audio_fragment;
|
|
int fragment_size;
|
|
WAVEOUTCAPSA woc;
|
|
|
|
TRACE("(%u, %p, %08lX);\n", wDevID, lpDesc, dwFlags);
|
|
if (lpDesc == NULL) {
|
|
WARN("Invalid Parameter !\n");
|
|
return MMSYSERR_INVALPARAM;
|
|
}
|
|
if (wDevID >= MAX_WAVEOUTDRV) {
|
|
TRACE("MAX_WAVOUTDRV reached !\n");
|
|
return MMSYSERR_BADDEVICEID;
|
|
}
|
|
wodGetDevCaps(wDevID, &woc, sizeof(woc));
|
|
|
|
/* only PCM format is supported so far... */
|
|
if (lpDesc->lpFormat->wFormatTag != WAVE_FORMAT_PCM ||
|
|
lpDesc->lpFormat->nChannels == 0 ||
|
|
lpDesc->lpFormat->nSamplesPerSec == 0) {
|
|
WARN("Bad format: tag=%04X nChannels=%d nSamplesPerSec=%ld !\n",
|
|
lpDesc->lpFormat->wFormatTag, lpDesc->lpFormat->nChannels,
|
|
lpDesc->lpFormat->nSamplesPerSec);
|
|
return WAVERR_BADFORMAT;
|
|
}
|
|
|
|
if (dwFlags & WAVE_FORMAT_QUERY) {
|
|
TRACE("Query format: tag=%04X nChannels=%d nSamplesPerSec=%ld !\n",
|
|
lpDesc->lpFormat->wFormatTag, lpDesc->lpFormat->nChannels,
|
|
lpDesc->lpFormat->nSamplesPerSec);
|
|
return MMSYSERR_NOERROR;
|
|
}
|
|
|
|
WOutDev[wDevID].unixdev = 0;
|
|
if (access(SOUND_DEV, 0) != 0)
|
|
return MMSYSERR_NOTENABLED;
|
|
audio = open(SOUND_DEV, O_WRONLY, 0);
|
|
if (audio == -1) {
|
|
WARN("can't open !\n");
|
|
return MMSYSERR_ALLOCATED ;
|
|
}
|
|
|
|
WOutDev[wDevID].wFlags = HIWORD(dwFlags & CALLBACK_TYPEMASK);
|
|
|
|
memcpy(&WOutDev[wDevID].waveDesc, lpDesc, sizeof(WAVEOPENDESC));
|
|
memcpy(&WOutDev[wDevID].format, lpDesc->lpFormat, sizeof(PCMWAVEFORMAT));
|
|
|
|
if (WOutDev[wDevID].format.wBitsPerSample == 0) {
|
|
WOutDev[wDevID].format.wBitsPerSample = 8 *
|
|
(WOutDev[wDevID].format.wf.nAvgBytesPerSec /
|
|
WOutDev[wDevID].format.wf.nSamplesPerSec) /
|
|
WOutDev[wDevID].format.wf.nChannels;
|
|
}
|
|
|
|
/* shockwave player uses only 4 1k-fragments at a rate of 22050 bytes/sec
|
|
* thus leading to 46ms per fragment, and a turnaround time of 185ms
|
|
*/
|
|
/* 2^10=1024 bytes per fragment, 16 fragments max */
|
|
audio_fragment = 0x000F000A;
|
|
sample_size = WOutDev[wDevID].format.wBitsPerSample;
|
|
sample_rate = WOutDev[wDevID].format.wf.nSamplesPerSec;
|
|
dsp_stereo = (WOutDev[wDevID].format.wf.nChannels > 1) ? 1 : 0;
|
|
|
|
IOCTL(audio, SNDCTL_DSP_SETFRAGMENT, audio_fragment);
|
|
/* First size and stereo then samplerate */
|
|
IOCTL(audio, SNDCTL_DSP_SAMPLESIZE, sample_size);
|
|
IOCTL(audio, SNDCTL_DSP_STEREO, dsp_stereo);
|
|
IOCTL(audio, SNDCTL_DSP_SPEED, sample_rate);
|
|
|
|
/* paranoid checks */
|
|
if (sample_size != WOutDev[wDevID].format.wBitsPerSample)
|
|
ERR("Can't set sample_size to %u (%d)\n",
|
|
WOutDev[wDevID].format.wBitsPerSample, sample_size);
|
|
if (dsp_stereo != (WOutDev[wDevID].format.wf.nChannels > 1) ? 1 : 0)
|
|
ERR("Can't set stereo to %u (%d)\n",
|
|
(WOutDev[wDevID].format.wf.nChannels > 1) ? 1 : 0, dsp_stereo);
|
|
if (!NEAR_MATCH(sample_rate,WOutDev[wDevID].format.wf.nSamplesPerSec))
|
|
ERR("Can't set sample_rate to %lu (%d)\n",
|
|
WOutDev[wDevID].format.wf.nSamplesPerSec, sample_rate);
|
|
|
|
/* even if we set fragment size above, read it again, just in case */
|
|
IOCTL(audio, SNDCTL_DSP_GETBLKSIZE, fragment_size);
|
|
|
|
WOutDev[wDevID].unixdev = audio;
|
|
WOutDev[wDevID].dwFragmentSize = fragment_size;
|
|
WOutDev[wDevID].wMaxFragments = HIWORD(audio_fragment) + 1;
|
|
|
|
WOutDev[wDevID].hEvent = CreateEventA(NULL, FALSE, FALSE, NULL);
|
|
WOutDev[wDevID].hThread = CreateThread(NULL, 0, wodPlayer, (LPVOID)(DWORD)wDevID, 0, &(WOutDev[wDevID].dwThreadID));
|
|
WaitForSingleObject(WOutDev[wDevID].hEvent, INFINITE);
|
|
|
|
TRACE("fd=%d fragmentSize=%ld\n",
|
|
WOutDev[wDevID].unixdev, WOutDev[wDevID].dwFragmentSize);
|
|
|
|
TRACE("wBitsPerSample=%u, nAvgBytesPerSec=%lu, nSamplesPerSec=%lu, nChannels=%u nBlockAlign=%u!\n",
|
|
WOutDev[wDevID].format.wBitsPerSample, WOutDev[wDevID].format.wf.nAvgBytesPerSec,
|
|
WOutDev[wDevID].format.wf.nSamplesPerSec, WOutDev[wDevID].format.wf.nChannels,
|
|
WOutDev[wDevID].format.wf.nBlockAlign);
|
|
|
|
if (WAVE_NotifyClient(wDevID, WOM_OPEN, 0L, 0L) != MMSYSERR_NOERROR) {
|
|
WARN("can't notify client !\n");
|
|
return MMSYSERR_INVALPARAM;
|
|
}
|
|
return MMSYSERR_NOERROR;
|
|
}
|
|
|
|
/**************************************************************************
|
|
* wodClose [internal]
|
|
*/
|
|
static DWORD wodClose(WORD wDevID)
|
|
{
|
|
DWORD ret = MMSYSERR_NOERROR;
|
|
|
|
TRACE("(%u);\n", wDevID);
|
|
|
|
if (wDevID > MAX_WAVEOUTDRV || WOutDev[wDevID].unixdev == 0) {
|
|
WARN("bad device ID !\n");
|
|
return MMSYSERR_BADDEVICEID;
|
|
}
|
|
|
|
if (WOutDev[wDevID].lpQueueHdr != NULL || WOutDev[wDevID].lpNotifyHdr != NULL) {
|
|
WARN("buffers still playing !\n");
|
|
ret = WAVERR_STILLPLAYING;
|
|
} else {
|
|
TRACE("imhere[3-close]\n");
|
|
PostThreadMessageA(WOutDev[wDevID].dwThreadID, WINE_WM_CLOSING, 0, 0);
|
|
WaitForSingleObject(WOutDev[wDevID].hEvent, INFINITE);
|
|
CloseHandle(WOutDev[wDevID].hEvent);
|
|
|
|
close(WOutDev[wDevID].unixdev);
|
|
WOutDev[wDevID].unixdev = 0;
|
|
WOutDev[wDevID].dwFragmentSize = 0;
|
|
if (WAVE_NotifyClient(wDevID, WOM_CLOSE, 0L, 0L) != MMSYSERR_NOERROR) {
|
|
WARN("can't notify client !\n");
|
|
ret = MMSYSERR_INVALPARAM;
|
|
}
|
|
}
|
|
return ret;
|
|
}
|
|
|
|
/**************************************************************************
|
|
* wodWrite [internal]
|
|
*
|
|
*/
|
|
static DWORD wodWrite(WORD wDevID, LPWAVEHDR lpWaveHdr, DWORD dwSize)
|
|
{
|
|
TRACE("(%u, %p, %08lX);\n", wDevID, lpWaveHdr, dwSize);
|
|
|
|
/* first, do the sanity checks... */
|
|
if (wDevID >= MAX_WAVEOUTDRV || WOutDev[wDevID].unixdev == 0) {
|
|
WARN("bad dev ID !\n");
|
|
return MMSYSERR_BADDEVICEID;
|
|
}
|
|
|
|
if (lpWaveHdr->lpData == NULL || !(lpWaveHdr->dwFlags & WHDR_PREPARED))
|
|
return WAVERR_UNPREPARED;
|
|
|
|
if (lpWaveHdr->dwFlags & WHDR_INQUEUE)
|
|
return WAVERR_STILLPLAYING;
|
|
|
|
TRACE("imhere[3-HEADER]\n");
|
|
PostThreadMessageA(WOutDev[wDevID].dwThreadID, WINE_WM_HEADER, 0, (DWORD)lpWaveHdr);
|
|
|
|
return MMSYSERR_NOERROR;
|
|
}
|
|
|
|
/**************************************************************************
|
|
* wodPrepare [internal]
|
|
*/
|
|
static DWORD wodPrepare(WORD wDevID, LPWAVEHDR lpWaveHdr, DWORD dwSize)
|
|
{
|
|
TRACE("(%u, %p, %08lX);\n", wDevID, lpWaveHdr, dwSize);
|
|
|
|
if (wDevID > MAX_WAVEOUTDRV || WOutDev[wDevID].unixdev == 0) {
|
|
WARN("bad device ID !\n");
|
|
return MMSYSERR_BADDEVICEID;
|
|
}
|
|
|
|
if (lpWaveHdr->dwFlags & WHDR_INQUEUE)
|
|
return WAVERR_STILLPLAYING;
|
|
|
|
lpWaveHdr->dwFlags |= WHDR_PREPARED;
|
|
lpWaveHdr->dwFlags &= ~WHDR_DONE;
|
|
return MMSYSERR_NOERROR;
|
|
}
|
|
|
|
/**************************************************************************
|
|
* wodUnprepare [internal]
|
|
*/
|
|
static DWORD wodUnprepare(WORD wDevID, LPWAVEHDR lpWaveHdr, DWORD dwSize)
|
|
{
|
|
TRACE("(%u, %p, %08lX);\n", wDevID, lpWaveHdr, dwSize);
|
|
|
|
if (wDevID > MAX_WAVEOUTDRV || WOutDev[wDevID].unixdev == 0) {
|
|
WARN("bad device ID !\n");
|
|
return MMSYSERR_BADDEVICEID;
|
|
}
|
|
|
|
if (lpWaveHdr->dwFlags & WHDR_INQUEUE)
|
|
return WAVERR_STILLPLAYING;
|
|
|
|
lpWaveHdr->dwFlags &= ~WHDR_PREPARED;
|
|
lpWaveHdr->dwFlags |= WHDR_DONE;
|
|
|
|
return MMSYSERR_NOERROR;
|
|
}
|
|
|
|
/**************************************************************************
|
|
* wodPause [internal]
|
|
*/
|
|
static DWORD wodPause(WORD wDevID)
|
|
{
|
|
TRACE("(%u);!\n", wDevID);
|
|
|
|
if (wDevID > MAX_WAVEOUTDRV || WOutDev[wDevID].unixdev == 0) {
|
|
WARN("bad device ID !\n");
|
|
return MMSYSERR_BADDEVICEID;
|
|
}
|
|
|
|
TRACE("imhere[3-PAUSING]\n");
|
|
PostThreadMessageA(WOutDev[wDevID].dwThreadID, WINE_WM_PAUSING, 0, 0);
|
|
WaitForSingleObject(WOutDev[wDevID].hEvent, INFINITE);
|
|
|
|
return MMSYSERR_NOERROR;
|
|
}
|
|
|
|
/**************************************************************************
|
|
* wodRestart [internal]
|
|
*/
|
|
static DWORD wodRestart(WORD wDevID)
|
|
{
|
|
TRACE("(%u);\n", wDevID);
|
|
|
|
if (wDevID > MAX_WAVEOUTDRV || WOutDev[wDevID].unixdev == 0) {
|
|
WARN("bad device ID !\n");
|
|
return MMSYSERR_BADDEVICEID;
|
|
}
|
|
|
|
if (WOutDev[wDevID].state == WINE_WS_PAUSED) {
|
|
TRACE("imhere[3-RESTARTING]\n");
|
|
PostThreadMessageA(WOutDev[wDevID].dwThreadID, WINE_WM_RESTARTING, 0, 0);
|
|
WaitForSingleObject(WOutDev[wDevID].hEvent, INFINITE);
|
|
}
|
|
|
|
/* FIXME: is NotifyClient with WOM_DONE right ? (Comet Busters 1.3.3 needs this notification) */
|
|
/* FIXME: Myst crashes with this ... hmm -MM
|
|
if (WAVE_NotifyClient(wDevID, WOM_DONE, 0L, 0L) != MMSYSERR_NOERROR) {
|
|
WARN("can't notify client !\n");
|
|
return MMSYSERR_INVALPARAM;
|
|
}
|
|
*/
|
|
|
|
return MMSYSERR_NOERROR;
|
|
}
|
|
|
|
/**************************************************************************
|
|
* wodReset [internal]
|
|
*/
|
|
static DWORD wodReset(WORD wDevID)
|
|
{
|
|
TRACE("(%u);\n", wDevID);
|
|
|
|
if (wDevID > MAX_WAVEOUTDRV || WOutDev[wDevID].unixdev == 0) {
|
|
WARN("bad device ID !\n");
|
|
return MMSYSERR_BADDEVICEID;
|
|
}
|
|
|
|
TRACE("imhere[3-RESET]\n");
|
|
PostThreadMessageA(WOutDev[wDevID].dwThreadID, WINE_WM_RESETTING, 0, 0);
|
|
WaitForSingleObject(WOutDev[wDevID].hEvent, INFINITE);
|
|
|
|
return MMSYSERR_NOERROR;
|
|
}
|
|
|
|
|
|
/**************************************************************************
|
|
* wodGetPosition [internal]
|
|
*/
|
|
static DWORD wodGetPosition(WORD wDevID, LPMMTIME lpTime, DWORD uSize)
|
|
{
|
|
int time;
|
|
DWORD val;
|
|
|
|
TRACE("(%u, %p, %lu);\n", wDevID, lpTime, uSize);
|
|
|
|
if (wDevID > MAX_WAVEOUTDRV || WOutDev[wDevID].unixdev == 0) {
|
|
WARN("bad device ID !\n");
|
|
return MMSYSERR_BADDEVICEID;
|
|
}
|
|
|
|
if (lpTime == NULL) return MMSYSERR_INVALPARAM;
|
|
|
|
val = WOutDev[wDevID].dwPlayedTotal + WOutDev[wDevID].dwPlayed;
|
|
|
|
TRACE("wType=%04X wBitsPerSample=%u nSamplesPerSec=%lu nChannels=%u nAvgBytesPerSec=%lu\n",
|
|
lpTime->wType, WOutDev[wDevID].format.wBitsPerSample,
|
|
WOutDev[wDevID].format.wf.nSamplesPerSec, WOutDev[wDevID].format.wf.nChannels,
|
|
WOutDev[wDevID].format.wf.nAvgBytesPerSec);
|
|
TRACE("dwTotalPlayed=%lu\n", val);
|
|
|
|
switch (lpTime->wType) {
|
|
case TIME_BYTES:
|
|
lpTime->u.cb = val;
|
|
TRACE("TIME_BYTES=%lu\n", lpTime->u.cb);
|
|
break;
|
|
case TIME_SAMPLES:
|
|
lpTime->u.sample = val * 8 / WOutDev[wDevID].format.wBitsPerSample;
|
|
TRACE("TIME_SAMPLES=%lu\n", lpTime->u.sample);
|
|
break;
|
|
case TIME_SMPTE:
|
|
time = val / (WOutDev[wDevID].format.wf.nAvgBytesPerSec / 1000);
|
|
lpTime->u.smpte.hour = time / 108000;
|
|
time -= lpTime->u.smpte.hour * 108000;
|
|
lpTime->u.smpte.min = time / 1800;
|
|
time -= lpTime->u.smpte.min * 1800;
|
|
lpTime->u.smpte.sec = time / 30;
|
|
time -= lpTime->u.smpte.sec * 30;
|
|
lpTime->u.smpte.frame = time;
|
|
lpTime->u.smpte.fps = 30;
|
|
TRACE("TIME_SMPTE=%02u:%02u:%02u:%02u\n",
|
|
lpTime->u.smpte.hour, lpTime->u.smpte.min,
|
|
lpTime->u.smpte.sec, lpTime->u.smpte.frame);
|
|
break;
|
|
default:
|
|
FIXME("Format %d not supported ! use TIME_MS !\n", lpTime->wType);
|
|
lpTime->wType = TIME_MS;
|
|
case TIME_MS:
|
|
lpTime->u.ms = val / (WOutDev[wDevID].format.wf.nAvgBytesPerSec / 1000);
|
|
TRACE("TIME_MS=%lu\n", lpTime->u.ms);
|
|
break;
|
|
}
|
|
return MMSYSERR_NOERROR;
|
|
}
|
|
|
|
/**************************************************************************
|
|
* wodGetVolume [internal]
|
|
*/
|
|
static DWORD wodGetVolume(WORD wDevID, LPDWORD lpdwVol)
|
|
{
|
|
int mixer;
|
|
int volume;
|
|
DWORD left, right;
|
|
|
|
TRACE("(%u, %p);\n", wDevID, lpdwVol);
|
|
|
|
if (lpdwVol == NULL)
|
|
return MMSYSERR_NOTENABLED;
|
|
if ((mixer = open(MIXER_DEV, O_RDONLY)) < 0) {
|
|
WARN("mixer device not available !\n");
|
|
return MMSYSERR_NOTENABLED;
|
|
}
|
|
if (ioctl(mixer, SOUND_MIXER_READ_PCM, &volume) == -1) {
|
|
WARN("unable read mixer !\n");
|
|
return MMSYSERR_NOTENABLED;
|
|
}
|
|
close(mixer);
|
|
left = LOBYTE(volume);
|
|
right = HIBYTE(volume);
|
|
TRACE("left=%ld right=%ld !\n", left, right);
|
|
*lpdwVol = ((left * 0xFFFFl) / 100) + (((right * 0xFFFFl) / 100) << 16);
|
|
return MMSYSERR_NOERROR;
|
|
}
|
|
|
|
|
|
/**************************************************************************
|
|
* wodSetVolume [internal]
|
|
*/
|
|
static DWORD wodSetVolume(WORD wDevID, DWORD dwParam)
|
|
{
|
|
int mixer;
|
|
int volume;
|
|
DWORD left, right;
|
|
|
|
TRACE("(%u, %08lX);\n", wDevID, dwParam);
|
|
|
|
left = (LOWORD(dwParam) * 100) / 0xFFFFl;
|
|
right = (HIWORD(dwParam) * 100) / 0xFFFFl;
|
|
volume = left + (right << 8);
|
|
|
|
if ((mixer = open(MIXER_DEV, O_WRONLY)) < 0) {
|
|
WARN("mixer device not available !\n");
|
|
return MMSYSERR_NOTENABLED;
|
|
}
|
|
if (ioctl(mixer, SOUND_MIXER_WRITE_PCM, &volume) == -1) {
|
|
WARN("unable set mixer !\n");
|
|
return MMSYSERR_NOTENABLED;
|
|
}
|
|
close(mixer);
|
|
return MMSYSERR_NOERROR;
|
|
}
|
|
|
|
/**************************************************************************
|
|
* wodGetNumDevs [internal]
|
|
*/
|
|
static DWORD wodGetNumDevs(void)
|
|
{
|
|
DWORD ret = 1;
|
|
|
|
/* FIXME: For now, only one sound device (SOUND_DEV) is allowed */
|
|
int audio = open(SOUND_DEV, O_WRONLY, 0);
|
|
|
|
if (audio == -1) {
|
|
if (errno != EBUSY)
|
|
ret = 0;
|
|
} else {
|
|
close(audio);
|
|
}
|
|
return ret;
|
|
}
|
|
|
|
/**************************************************************************
|
|
* OSS_wodMessage [sample driver]
|
|
*/
|
|
DWORD WINAPI OSS_wodMessage(UINT wDevID, UINT wMsg, DWORD dwUser,
|
|
DWORD dwParam1, DWORD dwParam2)
|
|
{
|
|
TRACE("(%u, %04X, %08lX, %08lX, %08lX);\n",
|
|
wDevID, wMsg, dwUser, dwParam1, dwParam2);
|
|
|
|
switch (wMsg) {
|
|
case DRVM_INIT:
|
|
case DRVM_EXIT:
|
|
case DRVM_ENABLE:
|
|
case DRVM_DISABLE:
|
|
/* FIXME: Pretend this is supported */
|
|
return 0;
|
|
case WODM_OPEN: return wodOpen (wDevID, (LPWAVEOPENDESC)dwParam1, dwParam2);
|
|
case WODM_CLOSE: return wodClose (wDevID);
|
|
case WODM_WRITE: return wodWrite (wDevID, (LPWAVEHDR)dwParam1, dwParam2);
|
|
case WODM_PAUSE: return wodPause (wDevID);
|
|
case WODM_GETPOS: return wodGetPosition (wDevID, (LPMMTIME)dwParam1, dwParam2);
|
|
case WODM_BREAKLOOP: return MMSYSERR_NOTSUPPORTED;
|
|
case WODM_PREPARE: return wodPrepare (wDevID, (LPWAVEHDR)dwParam1, dwParam2);
|
|
case WODM_UNPREPARE: return wodUnprepare (wDevID, (LPWAVEHDR)dwParam1, dwParam2);
|
|
case WODM_GETDEVCAPS: return wodGetDevCaps (wDevID, (LPWAVEOUTCAPSA)dwParam1, dwParam2);
|
|
case WODM_GETNUMDEVS: return wodGetNumDevs ();
|
|
case WODM_GETPITCH: return MMSYSERR_NOTSUPPORTED;
|
|
case WODM_SETPITCH: return MMSYSERR_NOTSUPPORTED;
|
|
case WODM_GETPLAYBACKRATE: return MMSYSERR_NOTSUPPORTED;
|
|
case WODM_SETPLAYBACKRATE: return MMSYSERR_NOTSUPPORTED;
|
|
case WODM_GETVOLUME: return wodGetVolume (wDevID, (LPDWORD)dwParam1);
|
|
case WODM_SETVOLUME: return wodSetVolume (wDevID, dwParam1);
|
|
case WODM_RESTART: return wodRestart (wDevID);
|
|
case WODM_RESET: return wodReset (wDevID);
|
|
default:
|
|
FIXME("unknown message %d!\n", wMsg);
|
|
}
|
|
return MMSYSERR_NOTSUPPORTED;
|
|
}
|
|
|
|
/*======================================================================*
|
|
* Low level WAVE IN implemantation *
|
|
*======================================================================*/
|
|
|
|
/**************************************************************************
|
|
* widGetDevCaps [internal]
|
|
*/
|
|
static DWORD widGetDevCaps(WORD wDevID, LPWAVEINCAPSA lpCaps, DWORD dwSize)
|
|
{
|
|
int audio, smplrate, samplesize=16, dsp_stereo=1, bytespersmpl;
|
|
|
|
TRACE("(%u, %p, %lu);\n", wDevID, lpCaps, dwSize);
|
|
if (lpCaps == NULL) return MMSYSERR_NOTENABLED;
|
|
if (access(SOUND_DEV,0) != 0) return MMSYSERR_NOTENABLED;
|
|
audio = open(SOUND_DEV, O_RDONLY, 0);
|
|
if (audio == -1) return MMSYSERR_ALLOCATED ;
|
|
#ifdef EMULATE_SB16
|
|
lpCaps->wMid = 0x0002;
|
|
lpCaps->wPid = 0x0004;
|
|
strcpy(lpCaps->szPname, "SB16 Wave In");
|
|
#else
|
|
lpCaps->wMid = 0x00FF; /* Manufac ID */
|
|
lpCaps->wPid = 0x0001; /* Product ID */
|
|
strcpy(lpCaps->szPname, "OpenSoundSystem WAVIN Driver");
|
|
#endif
|
|
lpCaps->dwFormats = 0x00000000;
|
|
lpCaps->wChannels = (IOCTL(audio, SNDCTL_DSP_STEREO, dsp_stereo) != 0) ? 1 : 2;
|
|
bytespersmpl = (IOCTL(audio, SNDCTL_DSP_SAMPLESIZE, samplesize) != 0) ? 1 : 2;
|
|
smplrate = 44100;
|
|
if (IOCTL(audio, SNDCTL_DSP_SPEED, smplrate) == 0) {
|
|
lpCaps->dwFormats |= WAVE_FORMAT_4M08;
|
|
if (lpCaps->wChannels > 1)
|
|
lpCaps->dwFormats |= WAVE_FORMAT_4S08;
|
|
if (bytespersmpl > 1) {
|
|
lpCaps->dwFormats |= WAVE_FORMAT_4M16;
|
|
if (lpCaps->wChannels > 1)
|
|
lpCaps->dwFormats |= WAVE_FORMAT_4S16;
|
|
}
|
|
}
|
|
smplrate = 22050;
|
|
if (IOCTL(audio, SNDCTL_DSP_SPEED, smplrate) == 0) {
|
|
lpCaps->dwFormats |= WAVE_FORMAT_2M08;
|
|
if (lpCaps->wChannels > 1)
|
|
lpCaps->dwFormats |= WAVE_FORMAT_2S08;
|
|
if (bytespersmpl > 1) {
|
|
lpCaps->dwFormats |= WAVE_FORMAT_2M16;
|
|
if (lpCaps->wChannels > 1)
|
|
lpCaps->dwFormats |= WAVE_FORMAT_2S16;
|
|
}
|
|
}
|
|
smplrate = 11025;
|
|
if (IOCTL(audio, SNDCTL_DSP_SPEED, smplrate) == 0) {
|
|
lpCaps->dwFormats |= WAVE_FORMAT_1M08;
|
|
if (lpCaps->wChannels > 1)
|
|
lpCaps->dwFormats |= WAVE_FORMAT_1S08;
|
|
if (bytespersmpl > 1) {
|
|
lpCaps->dwFormats |= WAVE_FORMAT_1M16;
|
|
if (lpCaps->wChannels > 1)
|
|
lpCaps->dwFormats |= WAVE_FORMAT_1S16;
|
|
}
|
|
}
|
|
close(audio);
|
|
TRACE("dwFormats = %08lX\n", lpCaps->dwFormats);
|
|
return MMSYSERR_NOERROR;
|
|
}
|
|
|
|
/**************************************************************************
|
|
* widOpen [internal]
|
|
*/
|
|
static DWORD widOpen(WORD wDevID, LPWAVEOPENDESC lpDesc, DWORD dwFlags)
|
|
{
|
|
int audio, abuf_size, smplrate, samplesize, dsp_stereo;
|
|
LPWAVEFORMAT lpFormat;
|
|
|
|
TRACE("(%u, %p, %08lX);\n", wDevID, lpDesc, dwFlags);
|
|
if (lpDesc == NULL) {
|
|
WARN("Invalid Parameter !\n");
|
|
return MMSYSERR_INVALPARAM;
|
|
}
|
|
if (wDevID >= MAX_WAVEINDRV) {
|
|
TRACE("MAX_WAVINDRV reached !\n");
|
|
return MMSYSERR_ALLOCATED;
|
|
}
|
|
|
|
/* only PCM format is supported so far... */
|
|
if (lpDesc->lpFormat->wFormatTag != WAVE_FORMAT_PCM ||
|
|
lpDesc->lpFormat->nChannels == 0 ||
|
|
lpDesc->lpFormat->nSamplesPerSec == 0) {
|
|
WARN("Bad format: tag=%04X nChannels=%d nSamplesPerSec=%ld !\n",
|
|
lpDesc->lpFormat->wFormatTag, lpDesc->lpFormat->nChannels,
|
|
lpDesc->lpFormat->nSamplesPerSec);
|
|
return WAVERR_BADFORMAT;
|
|
}
|
|
|
|
if (dwFlags & WAVE_FORMAT_QUERY) {
|
|
TRACE("Query format: tag=%04X nChannels=%d nSamplesPerSec=%ld !\n",
|
|
lpDesc->lpFormat->wFormatTag, lpDesc->lpFormat->nChannels,
|
|
lpDesc->lpFormat->nSamplesPerSec);
|
|
return MMSYSERR_NOERROR;
|
|
}
|
|
|
|
WInDev[wDevID].unixdev = 0;
|
|
if (access(SOUND_DEV,0) != 0) return MMSYSERR_NOTENABLED;
|
|
audio = open(SOUND_DEV, O_RDONLY, 0);
|
|
if (audio == -1) {
|
|
WARN("can't open !\n");
|
|
return MMSYSERR_ALLOCATED;
|
|
}
|
|
IOCTL(audio, SNDCTL_DSP_GETBLKSIZE, abuf_size);
|
|
if (abuf_size < 1024 || abuf_size > 65536) {
|
|
if (abuf_size == -1)
|
|
{
|
|
WARN("IOCTL can't 'SNDCTL_DSP_GETBLKSIZE' !\n");
|
|
return MMSYSERR_NOTENABLED;
|
|
}
|
|
WARN("SNDCTL_DSP_GETBLKSIZE Invalid dwFragmentSize %d!\n",abuf_size);
|
|
}
|
|
WInDev[wDevID].wFlags = HIWORD(dwFlags & CALLBACK_TYPEMASK);
|
|
|
|
if (WInDev[wDevID].lpQueueHdr) {
|
|
HeapFree(GetProcessHeap(), 0, WInDev[wDevID].lpQueueHdr);
|
|
WInDev[wDevID].lpQueueHdr = NULL;
|
|
}
|
|
WInDev[wDevID].unixdev = audio;
|
|
WInDev[wDevID].dwFragmentSize = abuf_size;
|
|
WInDev[wDevID].dwTotalRecorded = 0;
|
|
memcpy(&WInDev[wDevID].waveDesc, lpDesc, sizeof(WAVEOPENDESC));
|
|
lpFormat = (LPWAVEFORMAT) lpDesc->lpFormat;
|
|
|
|
memcpy(&WInDev[wDevID].format, lpFormat, sizeof(PCMWAVEFORMAT));
|
|
WInDev[wDevID].format.wBitsPerSample = 8; /* <-------------- */
|
|
if (WInDev[wDevID].format.wf.nChannels == 0) return WAVERR_BADFORMAT;
|
|
if (WInDev[wDevID].format.wf.nSamplesPerSec == 0) return WAVERR_BADFORMAT;
|
|
if (WInDev[wDevID].format.wBitsPerSample == 0) {
|
|
WInDev[wDevID].format.wBitsPerSample = 8 *
|
|
(WInDev[wDevID].format.wf.nAvgBytesPerSec /
|
|
WInDev[wDevID].format.wf.nSamplesPerSec) /
|
|
WInDev[wDevID].format.wf.nChannels;
|
|
}
|
|
samplesize = WInDev[wDevID].format.wBitsPerSample;
|
|
smplrate = WInDev[wDevID].format.wf.nSamplesPerSec;
|
|
dsp_stereo = (WInDev[wDevID].format.wf.nChannels > 1) ? TRUE : FALSE;
|
|
IOCTL(audio, SNDCTL_DSP_SPEED, smplrate);
|
|
IOCTL(audio, SNDCTL_DSP_SAMPLESIZE, samplesize);
|
|
IOCTL(audio, SNDCTL_DSP_STEREO, dsp_stereo);
|
|
TRACE("wBitsPerSample=%u !\n", WInDev[wDevID].format.wBitsPerSample);
|
|
TRACE("nSamplesPerSec=%lu !\n", WInDev[wDevID].format.wf.nSamplesPerSec);
|
|
TRACE("nChannels=%u !\n", WInDev[wDevID].format.wf.nChannels);
|
|
TRACE("nAvgBytesPerSec=%lu\n", WInDev[wDevID].format.wf.nAvgBytesPerSec);
|
|
if (WAVE_NotifyClient(wDevID, WIM_OPEN, 0L, 0L) != MMSYSERR_NOERROR) {
|
|
WARN("can't notify client !\n");
|
|
return MMSYSERR_INVALPARAM;
|
|
}
|
|
return MMSYSERR_NOERROR;
|
|
}
|
|
|
|
/**************************************************************************
|
|
* widClose [internal]
|
|
*/
|
|
static DWORD widClose(WORD wDevID)
|
|
{
|
|
TRACE("(%u);\n", wDevID);
|
|
if (wDevID > MAX_WAVEINDRV) return MMSYSERR_INVALPARAM;
|
|
if (WInDev[wDevID].unixdev == 0) {
|
|
WARN("can't close !\n");
|
|
return MMSYSERR_NOTENABLED;
|
|
}
|
|
if (WInDev[wDevID].lpQueueHdr != NULL) {
|
|
WARN("still buffers open !\n");
|
|
return WAVERR_STILLPLAYING;
|
|
}
|
|
close(WInDev[wDevID].unixdev);
|
|
WInDev[wDevID].unixdev = 0;
|
|
WInDev[wDevID].dwFragmentSize = 0;
|
|
if (WAVE_NotifyClient(wDevID, WIM_CLOSE, 0L, 0L) != MMSYSERR_NOERROR) {
|
|
WARN("can't notify client !\n");
|
|
return MMSYSERR_INVALPARAM;
|
|
}
|
|
return MMSYSERR_NOERROR;
|
|
}
|
|
|
|
/**************************************************************************
|
|
* widAddBuffer [internal]
|
|
*/
|
|
static DWORD widAddBuffer(WORD wDevID, LPWAVEHDR lpWaveHdr, DWORD dwSize)
|
|
{
|
|
int count = 1;
|
|
LPWAVEHDR lpWIHdr;
|
|
|
|
TRACE("(%u, %p, %08lX);\n", wDevID, lpWaveHdr, dwSize);
|
|
if (WInDev[wDevID].unixdev == 0) {
|
|
WARN("can't do it !\n");
|
|
return MMSYSERR_NOTENABLED;
|
|
}
|
|
if (!(lpWaveHdr->dwFlags & WHDR_PREPARED)) {
|
|
TRACE("never been prepared !\n");
|
|
return WAVERR_UNPREPARED;
|
|
}
|
|
if (lpWaveHdr->dwFlags & WHDR_INQUEUE) {
|
|
TRACE("header already in use !\n");
|
|
return WAVERR_STILLPLAYING;
|
|
}
|
|
lpWaveHdr->dwFlags |= WHDR_PREPARED;
|
|
lpWaveHdr->dwFlags |= WHDR_INQUEUE;
|
|
lpWaveHdr->dwFlags &= ~WHDR_DONE;
|
|
lpWaveHdr->dwBytesRecorded = 0;
|
|
lpWaveHdr->lpNext = NULL;
|
|
if (WInDev[wDevID].lpQueueHdr == NULL) {
|
|
WInDev[wDevID].lpQueueHdr = lpWaveHdr;
|
|
} else {
|
|
lpWIHdr = WInDev[wDevID].lpQueueHdr;
|
|
while (lpWIHdr->lpNext != NULL) {
|
|
lpWIHdr = lpWIHdr->lpNext;
|
|
count++;
|
|
}
|
|
lpWIHdr->lpNext = lpWaveHdr;
|
|
count++;
|
|
}
|
|
TRACE("buffer added ! (now %u in queue)\n", count);
|
|
return MMSYSERR_NOERROR;
|
|
}
|
|
|
|
/**************************************************************************
|
|
* widPrepare [internal]
|
|
*/
|
|
static DWORD widPrepare(WORD wDevID, LPWAVEHDR lpWaveHdr, DWORD dwSize)
|
|
{
|
|
TRACE("(%u, %p, %08lX);\n", wDevID, lpWaveHdr, dwSize);
|
|
if (WInDev[wDevID].unixdev == 0) {
|
|
WARN("can't prepare !\n");
|
|
return MMSYSERR_NOTENABLED;
|
|
}
|
|
if (lpWaveHdr->dwFlags & WHDR_INQUEUE)
|
|
return WAVERR_STILLPLAYING;
|
|
lpWaveHdr->dwFlags |= WHDR_PREPARED;
|
|
lpWaveHdr->dwFlags &= ~WHDR_INQUEUE;
|
|
lpWaveHdr->dwFlags &= ~WHDR_DONE;
|
|
lpWaveHdr->dwBytesRecorded = 0;
|
|
TRACE("header prepared !\n");
|
|
return MMSYSERR_NOERROR;
|
|
}
|
|
|
|
/**************************************************************************
|
|
* widUnprepare [internal]
|
|
*/
|
|
static DWORD widUnprepare(WORD wDevID, LPWAVEHDR lpWaveHdr, DWORD dwSize)
|
|
{
|
|
TRACE("(%u, %p, %08lX);\n", wDevID, lpWaveHdr, dwSize);
|
|
if (WInDev[wDevID].unixdev == 0) {
|
|
WARN("can't unprepare !\n");
|
|
return MMSYSERR_NOTENABLED;
|
|
}
|
|
lpWaveHdr->dwFlags &= ~WHDR_PREPARED;
|
|
lpWaveHdr->dwFlags &= ~WHDR_INQUEUE;
|
|
lpWaveHdr->dwFlags |= WHDR_DONE;
|
|
|
|
return MMSYSERR_NOERROR;
|
|
}
|
|
|
|
/**************************************************************************
|
|
* widStart [internal]
|
|
*/
|
|
static DWORD widStart(WORD wDevID)
|
|
{
|
|
int count = 1;
|
|
int bytesRead;
|
|
LPWAVEHDR *lpWaveHdr;
|
|
LPBYTE lpData;
|
|
|
|
TRACE("(%u);\n", wDevID);
|
|
if (WInDev[wDevID].unixdev == 0) {
|
|
WARN("can't start recording !\n");
|
|
return MMSYSERR_NOTENABLED;
|
|
}
|
|
|
|
lpWaveHdr = &(WInDev[wDevID].lpQueueHdr);
|
|
TRACE("lpWaveHdr = %08lx\n",(DWORD)lpWaveHdr);
|
|
|
|
if (!*lpWaveHdr || !(*lpWaveHdr)->lpData) {
|
|
TRACE("never been prepared !\n");
|
|
return WAVERR_UNPREPARED;
|
|
}
|
|
|
|
while (*lpWaveHdr != NULL) {
|
|
lpData = (*lpWaveHdr)->lpData;
|
|
TRACE("recording buf#%u=%p size=%lu \n",
|
|
count, lpData, (*lpWaveHdr)->dwBufferLength);
|
|
|
|
bytesRead = read(WInDev[wDevID].unixdev, lpData, (*lpWaveHdr)->dwBufferLength);
|
|
|
|
if (bytesRead == -1)
|
|
perror("read from audio device");
|
|
|
|
TRACE("bytesread=%d (%ld)\n", bytesRead, (*lpWaveHdr)->dwBufferLength);
|
|
(*lpWaveHdr)->dwBytesRecorded = bytesRead;
|
|
WInDev[wDevID].dwTotalRecorded += (*lpWaveHdr)->dwBytesRecorded;
|
|
(*lpWaveHdr)->dwFlags &= ~WHDR_INQUEUE;
|
|
(*lpWaveHdr)->dwFlags |= WHDR_DONE;
|
|
|
|
if (WAVE_NotifyClient(wDevID, WIM_DATA, (DWORD)*lpWaveHdr, (*lpWaveHdr)->dwBytesRecorded) != MMSYSERR_NOERROR) {
|
|
WARN("can't notify client !\n");
|
|
return MMSYSERR_INVALPARAM;
|
|
}
|
|
|
|
/* removes the current block from the queue */
|
|
*lpWaveHdr = (*lpWaveHdr)->lpNext;
|
|
count++;
|
|
}
|
|
TRACE("end of recording !\n");
|
|
return MMSYSERR_NOERROR;
|
|
}
|
|
|
|
/**************************************************************************
|
|
* widStop [internal]
|
|
*/
|
|
static DWORD widStop(WORD wDevID)
|
|
{
|
|
TRACE("(%u);\n", wDevID);
|
|
if (WInDev[wDevID].unixdev == 0) {
|
|
WARN("can't stop !\n");
|
|
return MMSYSERR_NOTENABLED;
|
|
}
|
|
return MMSYSERR_NOERROR;
|
|
}
|
|
|
|
/**************************************************************************
|
|
* widReset [internal]
|
|
*/
|
|
static DWORD widReset(WORD wDevID)
|
|
{
|
|
TRACE("(%u);\n", wDevID);
|
|
if (WInDev[wDevID].unixdev == 0) {
|
|
WARN("can't reset !\n");
|
|
return MMSYSERR_NOTENABLED;
|
|
}
|
|
return MMSYSERR_NOERROR;
|
|
}
|
|
|
|
/**************************************************************************
|
|
* widGetPosition [internal]
|
|
*/
|
|
static DWORD widGetPosition(WORD wDevID, LPMMTIME lpTime, DWORD uSize)
|
|
{
|
|
int time;
|
|
|
|
TRACE("(%u, %p, %lu);\n", wDevID, lpTime, uSize);
|
|
if (WInDev[wDevID].unixdev == 0) {
|
|
WARN("can't get pos !\n");
|
|
return MMSYSERR_NOTENABLED;
|
|
}
|
|
if (lpTime == NULL) return MMSYSERR_INVALPARAM;
|
|
TRACE("wType=%04X !\n", lpTime->wType);
|
|
TRACE("wBitsPerSample=%u\n", WInDev[wDevID].format.wBitsPerSample);
|
|
TRACE("nSamplesPerSec=%lu\n", WInDev[wDevID].format.wf.nSamplesPerSec);
|
|
TRACE("nChannels=%u\n", WInDev[wDevID].format.wf.nChannels);
|
|
TRACE("nAvgBytesPerSec=%lu\n", WInDev[wDevID].format.wf.nAvgBytesPerSec);
|
|
switch (lpTime->wType) {
|
|
case TIME_BYTES:
|
|
lpTime->u.cb = WInDev[wDevID].dwTotalRecorded;
|
|
TRACE("TIME_BYTES=%lu\n", lpTime->u.cb);
|
|
break;
|
|
case TIME_SAMPLES:
|
|
lpTime->u.sample = WInDev[wDevID].dwTotalRecorded * 8 /
|
|
WInDev[wDevID].format.wBitsPerSample;
|
|
TRACE("TIME_SAMPLES=%lu\n", lpTime->u.sample);
|
|
break;
|
|
case TIME_SMPTE:
|
|
time = WInDev[wDevID].dwTotalRecorded /
|
|
(WInDev[wDevID].format.wf.nAvgBytesPerSec / 1000);
|
|
lpTime->u.smpte.hour = time / 108000;
|
|
time -= lpTime->u.smpte.hour * 108000;
|
|
lpTime->u.smpte.min = time / 1800;
|
|
time -= lpTime->u.smpte.min * 1800;
|
|
lpTime->u.smpte.sec = time / 30;
|
|
time -= lpTime->u.smpte.sec * 30;
|
|
lpTime->u.smpte.frame = time;
|
|
lpTime->u.smpte.fps = 30;
|
|
TRACE("TIME_SMPTE=%02u:%02u:%02u:%02u\n",
|
|
lpTime->u.smpte.hour, lpTime->u.smpte.min,
|
|
lpTime->u.smpte.sec, lpTime->u.smpte.frame);
|
|
break;
|
|
case TIME_MS:
|
|
lpTime->u.ms = WInDev[wDevID].dwTotalRecorded /
|
|
(WInDev[wDevID].format.wf.nAvgBytesPerSec / 1000);
|
|
TRACE("TIME_MS=%lu\n", lpTime->u.ms);
|
|
break;
|
|
default:
|
|
FIXME("format not supported (%u) ! use TIME_MS !\n", lpTime->wType);
|
|
lpTime->wType = TIME_MS;
|
|
}
|
|
return MMSYSERR_NOERROR;
|
|
}
|
|
|
|
/**************************************************************************
|
|
* OSS_widMessage [sample driver]
|
|
*/
|
|
DWORD WINAPI OSS_widMessage(WORD wDevID, WORD wMsg, DWORD dwUser,
|
|
DWORD dwParam1, DWORD dwParam2)
|
|
{
|
|
TRACE("(%u, %04X, %08lX, %08lX, %08lX);\n",
|
|
wDevID, wMsg, dwUser, dwParam1, dwParam2);
|
|
|
|
switch (wMsg) {
|
|
case DRVM_INIT:
|
|
case DRVM_EXIT:
|
|
case DRVM_ENABLE:
|
|
case DRVM_DISABLE:
|
|
/* FIXME: Pretend this is supported */
|
|
return 0;
|
|
case WIDM_OPEN: return widOpen(wDevID, (LPWAVEOPENDESC)dwParam1, dwParam2);
|
|
case WIDM_CLOSE: return widClose(wDevID);
|
|
case WIDM_ADDBUFFER: return widAddBuffer(wDevID, (LPWAVEHDR)dwParam1, dwParam2);
|
|
case WIDM_PREPARE: return widPrepare(wDevID, (LPWAVEHDR)dwParam1, dwParam2);
|
|
case WIDM_UNPREPARE: return widUnprepare(wDevID, (LPWAVEHDR)dwParam1, dwParam2);
|
|
case WIDM_GETDEVCAPS: return widGetDevCaps(wDevID, (LPWAVEINCAPSA)dwParam1, dwParam2);
|
|
case WIDM_GETNUMDEVS: return wodGetNumDevs(); /* same number of devices in output as in input */
|
|
case WIDM_GETPOS: return widGetPosition(wDevID, (LPMMTIME)dwParam1, dwParam2);
|
|
case WIDM_RESET: return widReset(wDevID);
|
|
case WIDM_START: return widStart(wDevID);
|
|
case WIDM_STOP: return widStop(wDevID);
|
|
default:
|
|
FIXME("unknown message %u!\n", wMsg);
|
|
}
|
|
return MMSYSERR_NOTSUPPORTED;
|
|
}
|
|
|
|
#else /* !HAVE_OSS */
|
|
|
|
/**************************************************************************
|
|
* wodMessage [sample driver]
|
|
*/
|
|
DWORD WINAPI OSS_wodMessage(WORD wDevID, WORD wMsg, DWORD dwUser,
|
|
DWORD dwParam1, DWORD dwParam2)
|
|
{
|
|
FIXME("(%u, %04X, %08lX, %08lX, %08lX):stub\n", wDevID, wMsg, dwUser, dwParam1, dwParam2);
|
|
return MMSYSERR_NOTENABLED;
|
|
}
|
|
|
|
/**************************************************************************
|
|
* widMessage [sample driver]
|
|
*/
|
|
DWORD WINAPI OSS_widMessage(WORD wDevID, WORD wMsg, DWORD dwUser,
|
|
DWORD dwParam1, DWORD dwParam2)
|
|
{
|
|
FIXME("(%u, %04X, %08lX, %08lX, %08lX):stub\n", wDevID, wMsg, dwUser, dwParam1, dwParam2);
|
|
return MMSYSERR_NOTENABLED;
|
|
}
|
|
|
|
#endif /* HAVE_OSS */
|