mirror of
https://github.com/RetroDECK/Duckstation.git
synced 2024-12-02 10:35:39 +00:00
1068 lines
29 KiB
C
1068 lines
29 KiB
C
/*
|
|
* Copyright © 2011 Mozilla Foundation
|
|
*
|
|
* This program is made available under an ISC-style license. See the
|
|
* accompanying file LICENSE for details.
|
|
*/
|
|
#undef WINVER
|
|
#define WINVER 0x0501
|
|
#undef WIN32_LEAN_AND_MEAN
|
|
|
|
#include <malloc.h>
|
|
#include <windows.h>
|
|
#include <mmreg.h>
|
|
#include <mmsystem.h>
|
|
#include <process.h>
|
|
#include <stdio.h>
|
|
#include <stdlib.h>
|
|
#include <math.h>
|
|
#include "cubeb/cubeb.h"
|
|
#include "cubeb-internal.h"
|
|
|
|
/* This is missing from the MinGW headers. Use a safe fallback. */
|
|
#if !defined(MEMORY_ALLOCATION_ALIGNMENT)
|
|
#define MEMORY_ALLOCATION_ALIGNMENT 16
|
|
#endif
|
|
|
|
/**This is also missing from the MinGW headers. It also appears to be undocumented by Microsoft.*/
|
|
#ifndef WAVE_FORMAT_48M08
|
|
#define WAVE_FORMAT_48M08 0x00001000 /* 48 kHz, Mono, 8-bit */
|
|
#endif
|
|
#ifndef WAVE_FORMAT_48M16
|
|
#define WAVE_FORMAT_48M16 0x00002000 /* 48 kHz, Mono, 16-bit */
|
|
#endif
|
|
#ifndef WAVE_FORMAT_48S08
|
|
#define WAVE_FORMAT_48S08 0x00004000 /* 48 kHz, Stereo, 8-bit */
|
|
#endif
|
|
#ifndef WAVE_FORMAT_48S16
|
|
#define WAVE_FORMAT_48S16 0x00008000 /* 48 kHz, Stereo, 16-bit */
|
|
#endif
|
|
#ifndef WAVE_FORMAT_96M08
|
|
#define WAVE_FORMAT_96M08 0x00010000 /* 96 kHz, Mono, 8-bit */
|
|
#endif
|
|
#ifndef WAVE_FORMAT_96M16
|
|
#define WAVE_FORMAT_96M16 0x00020000 /* 96 kHz, Mono, 16-bit */
|
|
#endif
|
|
#ifndef WAVE_FORMAT_96S08
|
|
#define WAVE_FORMAT_96S08 0x00040000 /* 96 kHz, Stereo, 8-bit */
|
|
#endif
|
|
#ifndef WAVE_FORMAT_96S16
|
|
#define WAVE_FORMAT_96S16 0x00080000 /* 96 kHz, Stereo, 16-bit */
|
|
#endif
|
|
|
|
/**Taken from winbase.h, also not in MinGW.*/
|
|
#ifndef STACK_SIZE_PARAM_IS_A_RESERVATION
|
|
#define STACK_SIZE_PARAM_IS_A_RESERVATION 0x00010000 // Threads only
|
|
#endif
|
|
|
|
#ifndef DRVM_MAPPER
|
|
#define DRVM_MAPPER (0x2000)
|
|
#endif
|
|
#ifndef DRVM_MAPPER_PREFERRED_GET
|
|
#define DRVM_MAPPER_PREFERRED_GET (DRVM_MAPPER+21)
|
|
#endif
|
|
#ifndef DRVM_MAPPER_CONSOLEVOICECOM_GET
|
|
#define DRVM_MAPPER_CONSOLEVOICECOM_GET (DRVM_MAPPER+23)
|
|
#endif
|
|
|
|
#define CUBEB_STREAM_MAX 32
|
|
#define NBUFS 4
|
|
|
|
const GUID KSDATAFORMAT_SUBTYPE_PCM =
|
|
{ 0x00000001, 0x0000, 0x0010, { 0x80, 0x00, 0x00, 0xaa, 0x00, 0x38, 0x9b, 0x71 } };
|
|
const GUID KSDATAFORMAT_SUBTYPE_IEEE_FLOAT =
|
|
{ 0x00000003, 0x0000, 0x0010, { 0x80, 0x00, 0x00, 0xaa, 0x00, 0x38, 0x9b, 0x71 } };
|
|
|
|
struct cubeb_stream_item {
|
|
SLIST_ENTRY head;
|
|
cubeb_stream * stream;
|
|
};
|
|
|
|
static struct cubeb_ops const winmm_ops;
|
|
|
|
struct cubeb {
|
|
struct cubeb_ops const * ops;
|
|
HANDLE event;
|
|
HANDLE thread;
|
|
int shutdown;
|
|
PSLIST_HEADER work;
|
|
CRITICAL_SECTION lock;
|
|
unsigned int active_streams;
|
|
unsigned int minimum_latency_ms;
|
|
};
|
|
|
|
struct cubeb_stream {
|
|
/* Note: Must match cubeb_stream layout in cubeb.c. */
|
|
cubeb * context;
|
|
void * user_ptr;
|
|
/**/
|
|
cubeb_stream_params params;
|
|
cubeb_data_callback data_callback;
|
|
cubeb_state_callback state_callback;
|
|
WAVEHDR buffers[NBUFS];
|
|
size_t buffer_size;
|
|
int next_buffer;
|
|
int free_buffers;
|
|
int shutdown;
|
|
int draining;
|
|
HANDLE event;
|
|
HWAVEOUT waveout;
|
|
CRITICAL_SECTION lock;
|
|
uint64_t written;
|
|
float soft_volume;
|
|
};
|
|
|
|
static size_t
|
|
bytes_per_frame(cubeb_stream_params params)
|
|
{
|
|
size_t bytes;
|
|
|
|
switch (params.format) {
|
|
case CUBEB_SAMPLE_S16LE:
|
|
bytes = sizeof(signed short);
|
|
break;
|
|
case CUBEB_SAMPLE_FLOAT32LE:
|
|
bytes = sizeof(float);
|
|
break;
|
|
default:
|
|
XASSERT(0);
|
|
}
|
|
|
|
return bytes * params.channels;
|
|
}
|
|
|
|
static WAVEHDR *
|
|
winmm_get_next_buffer(cubeb_stream * stm)
|
|
{
|
|
WAVEHDR * hdr = NULL;
|
|
|
|
XASSERT(stm->free_buffers > 0 && stm->free_buffers <= NBUFS);
|
|
hdr = &stm->buffers[stm->next_buffer];
|
|
XASSERT(hdr->dwFlags & WHDR_PREPARED ||
|
|
(hdr->dwFlags & WHDR_DONE && !(hdr->dwFlags & WHDR_INQUEUE)));
|
|
stm->next_buffer = (stm->next_buffer + 1) % NBUFS;
|
|
stm->free_buffers -= 1;
|
|
|
|
return hdr;
|
|
}
|
|
|
|
static void
|
|
winmm_refill_stream(cubeb_stream * stm)
|
|
{
|
|
WAVEHDR * hdr;
|
|
long got;
|
|
long wanted;
|
|
MMRESULT r;
|
|
|
|
EnterCriticalSection(&stm->lock);
|
|
stm->free_buffers += 1;
|
|
XASSERT(stm->free_buffers > 0 && stm->free_buffers <= NBUFS);
|
|
|
|
if (stm->draining) {
|
|
LeaveCriticalSection(&stm->lock);
|
|
if (stm->free_buffers == NBUFS) {
|
|
stm->state_callback(stm, stm->user_ptr, CUBEB_STATE_DRAINED);
|
|
}
|
|
SetEvent(stm->event);
|
|
return;
|
|
}
|
|
|
|
if (stm->shutdown) {
|
|
LeaveCriticalSection(&stm->lock);
|
|
SetEvent(stm->event);
|
|
return;
|
|
}
|
|
|
|
hdr = winmm_get_next_buffer(stm);
|
|
|
|
wanted = (DWORD) stm->buffer_size / bytes_per_frame(stm->params);
|
|
|
|
/* It is assumed that the caller is holding this lock. It must be dropped
|
|
during the callback to avoid deadlocks. */
|
|
LeaveCriticalSection(&stm->lock);
|
|
got = stm->data_callback(stm, stm->user_ptr, NULL, hdr->lpData, wanted);
|
|
EnterCriticalSection(&stm->lock);
|
|
if (got < 0) {
|
|
LeaveCriticalSection(&stm->lock);
|
|
/* XXX handle this case */
|
|
XASSERT(0);
|
|
return;
|
|
} else if (got < wanted) {
|
|
stm->draining = 1;
|
|
}
|
|
stm->written += got;
|
|
|
|
XASSERT(hdr->dwFlags & WHDR_PREPARED);
|
|
|
|
hdr->dwBufferLength = got * bytes_per_frame(stm->params);
|
|
XASSERT(hdr->dwBufferLength <= stm->buffer_size);
|
|
|
|
if (stm->soft_volume != -1.0) {
|
|
if (stm->params.format == CUBEB_SAMPLE_FLOAT32NE) {
|
|
float * b = (float *) hdr->lpData;
|
|
uint32_t i;
|
|
for (i = 0; i < got * stm->params.channels; i++) {
|
|
b[i] *= stm->soft_volume;
|
|
}
|
|
} else {
|
|
short * b = (short *) hdr->lpData;
|
|
uint32_t i;
|
|
for (i = 0; i < got * stm->params.channels; i++) {
|
|
b[i] = (short) (b[i] * stm->soft_volume);
|
|
}
|
|
}
|
|
}
|
|
|
|
r = waveOutWrite(stm->waveout, hdr, sizeof(*hdr));
|
|
if (r != MMSYSERR_NOERROR) {
|
|
LeaveCriticalSection(&stm->lock);
|
|
stm->state_callback(stm, stm->user_ptr, CUBEB_STATE_ERROR);
|
|
return;
|
|
}
|
|
|
|
LeaveCriticalSection(&stm->lock);
|
|
}
|
|
|
|
static unsigned __stdcall
|
|
winmm_buffer_thread(void * user_ptr)
|
|
{
|
|
cubeb * ctx = (cubeb *) user_ptr;
|
|
XASSERT(ctx);
|
|
|
|
for (;;) {
|
|
DWORD r;
|
|
PSLIST_ENTRY item;
|
|
|
|
r = WaitForSingleObject(ctx->event, INFINITE);
|
|
XASSERT(r == WAIT_OBJECT_0);
|
|
|
|
/* Process work items in batches so that a single stream can't
|
|
starve the others by continuously adding new work to the top of
|
|
the work item stack. */
|
|
item = InterlockedFlushSList(ctx->work);
|
|
while (item != NULL) {
|
|
PSLIST_ENTRY tmp = item;
|
|
winmm_refill_stream(((struct cubeb_stream_item *) tmp)->stream);
|
|
item = item->Next;
|
|
_aligned_free(tmp);
|
|
}
|
|
|
|
if (ctx->shutdown) {
|
|
break;
|
|
}
|
|
}
|
|
|
|
return 0;
|
|
}
|
|
|
|
static void CALLBACK
|
|
winmm_buffer_callback(HWAVEOUT waveout, UINT msg, DWORD_PTR user_ptr, DWORD_PTR p1, DWORD_PTR p2)
|
|
{
|
|
cubeb_stream * stm = (cubeb_stream *) user_ptr;
|
|
struct cubeb_stream_item * item;
|
|
|
|
if (msg != WOM_DONE) {
|
|
return;
|
|
}
|
|
|
|
item = _aligned_malloc(sizeof(struct cubeb_stream_item), MEMORY_ALLOCATION_ALIGNMENT);
|
|
XASSERT(item);
|
|
item->stream = stm;
|
|
InterlockedPushEntrySList(stm->context->work, &item->head);
|
|
|
|
SetEvent(stm->context->event);
|
|
}
|
|
|
|
static unsigned int
|
|
calculate_minimum_latency(void)
|
|
{
|
|
OSVERSIONINFOEX osvi;
|
|
DWORDLONG mask;
|
|
|
|
/* Running under Terminal Services results in underruns with low latency. */
|
|
if (GetSystemMetrics(SM_REMOTESESSION) == TRUE) {
|
|
return 500;
|
|
}
|
|
|
|
/* Vista's WinMM implementation underruns when less than 200ms of audio is buffered. */
|
|
memset(&osvi, 0, sizeof(OSVERSIONINFOEX));
|
|
osvi.dwOSVersionInfoSize = sizeof(OSVERSIONINFOEX);
|
|
osvi.dwMajorVersion = 6;
|
|
osvi.dwMinorVersion = 0;
|
|
|
|
mask = 0;
|
|
VER_SET_CONDITION(mask, VER_MAJORVERSION, VER_EQUAL);
|
|
VER_SET_CONDITION(mask, VER_MINORVERSION, VER_EQUAL);
|
|
|
|
if (VerifyVersionInfo(&osvi, VER_MAJORVERSION | VER_MINORVERSION, mask) != 0) {
|
|
return 200;
|
|
}
|
|
|
|
return 100;
|
|
}
|
|
|
|
static void winmm_destroy(cubeb * ctx);
|
|
|
|
/*static*/ int
|
|
winmm_init(cubeb ** context, char const * context_name)
|
|
{
|
|
cubeb * ctx;
|
|
|
|
XASSERT(context);
|
|
*context = NULL;
|
|
|
|
/* Don't initialize a context if there are no devices available. */
|
|
if (waveOutGetNumDevs() == 0) {
|
|
return CUBEB_ERROR;
|
|
}
|
|
|
|
ctx = calloc(1, sizeof(*ctx));
|
|
XASSERT(ctx);
|
|
|
|
ctx->ops = &winmm_ops;
|
|
|
|
ctx->work = _aligned_malloc(sizeof(*ctx->work), MEMORY_ALLOCATION_ALIGNMENT);
|
|
XASSERT(ctx->work);
|
|
InitializeSListHead(ctx->work);
|
|
|
|
ctx->event = CreateEvent(NULL, FALSE, FALSE, NULL);
|
|
if (!ctx->event) {
|
|
winmm_destroy(ctx);
|
|
return CUBEB_ERROR;
|
|
}
|
|
|
|
ctx->thread = (HANDLE) _beginthreadex(NULL, 256 * 1024, winmm_buffer_thread, ctx, STACK_SIZE_PARAM_IS_A_RESERVATION, NULL);
|
|
if (!ctx->thread) {
|
|
winmm_destroy(ctx);
|
|
return CUBEB_ERROR;
|
|
}
|
|
|
|
SetThreadPriority(ctx->thread, THREAD_PRIORITY_TIME_CRITICAL);
|
|
|
|
InitializeCriticalSection(&ctx->lock);
|
|
ctx->active_streams = 0;
|
|
|
|
ctx->minimum_latency_ms = calculate_minimum_latency();
|
|
|
|
*context = ctx;
|
|
|
|
return CUBEB_OK;
|
|
}
|
|
|
|
static char const *
|
|
winmm_get_backend_id(cubeb * ctx)
|
|
{
|
|
return "winmm";
|
|
}
|
|
|
|
static void
|
|
winmm_destroy(cubeb * ctx)
|
|
{
|
|
DWORD r;
|
|
|
|
XASSERT(ctx->active_streams == 0);
|
|
XASSERT(!InterlockedPopEntrySList(ctx->work));
|
|
|
|
DeleteCriticalSection(&ctx->lock);
|
|
|
|
if (ctx->thread) {
|
|
ctx->shutdown = 1;
|
|
SetEvent(ctx->event);
|
|
r = WaitForSingleObject(ctx->thread, INFINITE);
|
|
XASSERT(r == WAIT_OBJECT_0);
|
|
CloseHandle(ctx->thread);
|
|
}
|
|
|
|
if (ctx->event) {
|
|
CloseHandle(ctx->event);
|
|
}
|
|
|
|
_aligned_free(ctx->work);
|
|
|
|
free(ctx);
|
|
}
|
|
|
|
static void winmm_stream_destroy(cubeb_stream * stm);
|
|
|
|
static int
|
|
winmm_stream_init(cubeb * context, cubeb_stream ** stream, char const * stream_name,
|
|
cubeb_devid input_device,
|
|
cubeb_stream_params * input_stream_params,
|
|
cubeb_devid output_device,
|
|
cubeb_stream_params * output_stream_params,
|
|
unsigned int latency_frames,
|
|
cubeb_data_callback data_callback,
|
|
cubeb_state_callback state_callback,
|
|
void * user_ptr)
|
|
{
|
|
MMRESULT r;
|
|
WAVEFORMATEXTENSIBLE wfx;
|
|
cubeb_stream * stm;
|
|
int i;
|
|
size_t bufsz;
|
|
|
|
XASSERT(context);
|
|
XASSERT(stream);
|
|
XASSERT(output_stream_params);
|
|
|
|
if (input_stream_params) {
|
|
/* Capture support not yet implemented. */
|
|
return CUBEB_ERROR_NOT_SUPPORTED;
|
|
}
|
|
|
|
if (input_device || output_device) {
|
|
/* Device selection not yet implemented. */
|
|
return CUBEB_ERROR_DEVICE_UNAVAILABLE;
|
|
}
|
|
|
|
if (output_stream_params->prefs & CUBEB_STREAM_PREF_LOOPBACK) {
|
|
/* Loopback is not supported */
|
|
return CUBEB_ERROR_NOT_SUPPORTED;
|
|
}
|
|
|
|
*stream = NULL;
|
|
|
|
memset(&wfx, 0, sizeof(wfx));
|
|
if (output_stream_params->channels > 2) {
|
|
wfx.Format.wFormatTag = WAVE_FORMAT_EXTENSIBLE;
|
|
wfx.Format.cbSize = sizeof(wfx) - sizeof(wfx.Format);
|
|
} else {
|
|
wfx.Format.wFormatTag = WAVE_FORMAT_PCM;
|
|
if (output_stream_params->format == CUBEB_SAMPLE_FLOAT32LE) {
|
|
wfx.Format.wFormatTag = WAVE_FORMAT_IEEE_FLOAT;
|
|
}
|
|
wfx.Format.cbSize = 0;
|
|
}
|
|
wfx.Format.nChannels = output_stream_params->channels;
|
|
wfx.Format.nSamplesPerSec = output_stream_params->rate;
|
|
|
|
/* XXX fix channel mappings */
|
|
wfx.dwChannelMask = SPEAKER_FRONT_LEFT | SPEAKER_FRONT_RIGHT;
|
|
|
|
switch (output_stream_params->format) {
|
|
case CUBEB_SAMPLE_S16LE:
|
|
wfx.Format.wBitsPerSample = 16;
|
|
wfx.SubFormat = KSDATAFORMAT_SUBTYPE_PCM;
|
|
break;
|
|
case CUBEB_SAMPLE_FLOAT32LE:
|
|
wfx.Format.wBitsPerSample = 32;
|
|
wfx.SubFormat = KSDATAFORMAT_SUBTYPE_IEEE_FLOAT;
|
|
break;
|
|
default:
|
|
return CUBEB_ERROR_INVALID_FORMAT;
|
|
}
|
|
|
|
wfx.Format.nBlockAlign = (wfx.Format.wBitsPerSample * wfx.Format.nChannels) / 8;
|
|
wfx.Format.nAvgBytesPerSec = wfx.Format.nSamplesPerSec * wfx.Format.nBlockAlign;
|
|
wfx.Samples.wValidBitsPerSample = wfx.Format.wBitsPerSample;
|
|
|
|
EnterCriticalSection(&context->lock);
|
|
/* CUBEB_STREAM_MAX is a horrible hack to avoid a situation where, when
|
|
many streams are active at once, a subset of them will not consume (via
|
|
playback) or release (via waveOutReset) their buffers. */
|
|
if (context->active_streams >= CUBEB_STREAM_MAX) {
|
|
LeaveCriticalSection(&context->lock);
|
|
return CUBEB_ERROR;
|
|
}
|
|
context->active_streams += 1;
|
|
LeaveCriticalSection(&context->lock);
|
|
|
|
stm = calloc(1, sizeof(*stm));
|
|
XASSERT(stm);
|
|
|
|
stm->context = context;
|
|
|
|
stm->params = *output_stream_params;
|
|
|
|
stm->data_callback = data_callback;
|
|
stm->state_callback = state_callback;
|
|
stm->user_ptr = user_ptr;
|
|
stm->written = 0;
|
|
|
|
uint32_t latency_ms = latency_frames * 1000 / output_stream_params->rate;
|
|
|
|
if (latency_ms < context->minimum_latency_ms) {
|
|
latency_ms = context->minimum_latency_ms;
|
|
}
|
|
|
|
bufsz = (size_t) (stm->params.rate / 1000.0 * latency_ms * bytes_per_frame(stm->params) / NBUFS);
|
|
if (bufsz % bytes_per_frame(stm->params) != 0) {
|
|
bufsz += bytes_per_frame(stm->params) - (bufsz % bytes_per_frame(stm->params));
|
|
}
|
|
XASSERT(bufsz % bytes_per_frame(stm->params) == 0);
|
|
|
|
stm->buffer_size = bufsz;
|
|
|
|
InitializeCriticalSection(&stm->lock);
|
|
|
|
stm->event = CreateEvent(NULL, FALSE, FALSE, NULL);
|
|
if (!stm->event) {
|
|
winmm_stream_destroy(stm);
|
|
return CUBEB_ERROR;
|
|
}
|
|
|
|
stm->soft_volume = -1.0;
|
|
|
|
/* winmm_buffer_callback will be called during waveOutOpen, so all
|
|
other initialization must be complete before calling it. */
|
|
r = waveOutOpen(&stm->waveout, WAVE_MAPPER, &wfx.Format,
|
|
(DWORD_PTR) winmm_buffer_callback, (DWORD_PTR) stm,
|
|
CALLBACK_FUNCTION);
|
|
if (r != MMSYSERR_NOERROR) {
|
|
winmm_stream_destroy(stm);
|
|
return CUBEB_ERROR;
|
|
}
|
|
|
|
r = waveOutPause(stm->waveout);
|
|
if (r != MMSYSERR_NOERROR) {
|
|
winmm_stream_destroy(stm);
|
|
return CUBEB_ERROR;
|
|
}
|
|
|
|
for (i = 0; i < NBUFS; ++i) {
|
|
WAVEHDR * hdr = &stm->buffers[i];
|
|
|
|
hdr->lpData = calloc(1, bufsz);
|
|
XASSERT(hdr->lpData);
|
|
hdr->dwBufferLength = bufsz;
|
|
hdr->dwFlags = 0;
|
|
|
|
r = waveOutPrepareHeader(stm->waveout, hdr, sizeof(*hdr));
|
|
if (r != MMSYSERR_NOERROR) {
|
|
winmm_stream_destroy(stm);
|
|
return CUBEB_ERROR;
|
|
}
|
|
|
|
winmm_refill_stream(stm);
|
|
}
|
|
|
|
*stream = stm;
|
|
|
|
return CUBEB_OK;
|
|
}
|
|
|
|
static void
|
|
winmm_stream_destroy(cubeb_stream * stm)
|
|
{
|
|
int i;
|
|
|
|
if (stm->waveout) {
|
|
MMTIME time;
|
|
MMRESULT r;
|
|
int device_valid;
|
|
int enqueued;
|
|
|
|
EnterCriticalSection(&stm->lock);
|
|
stm->shutdown = 1;
|
|
|
|
waveOutReset(stm->waveout);
|
|
|
|
/* Don't need this value, we just want the result to detect invalid
|
|
handle/no device errors than waveOutReset doesn't seem to report. */
|
|
time.wType = TIME_SAMPLES;
|
|
r = waveOutGetPosition(stm->waveout, &time, sizeof(time));
|
|
device_valid = !(r == MMSYSERR_INVALHANDLE || r == MMSYSERR_NODRIVER);
|
|
|
|
enqueued = NBUFS - stm->free_buffers;
|
|
LeaveCriticalSection(&stm->lock);
|
|
|
|
/* Wait for all blocks to complete. */
|
|
while (device_valid && enqueued > 0) {
|
|
DWORD rv = WaitForSingleObject(stm->event, INFINITE);
|
|
XASSERT(rv == WAIT_OBJECT_0);
|
|
|
|
EnterCriticalSection(&stm->lock);
|
|
enqueued = NBUFS - stm->free_buffers;
|
|
LeaveCriticalSection(&stm->lock);
|
|
}
|
|
|
|
EnterCriticalSection(&stm->lock);
|
|
|
|
for (i = 0; i < NBUFS; ++i) {
|
|
if (stm->buffers[i].dwFlags & WHDR_PREPARED) {
|
|
waveOutUnprepareHeader(stm->waveout, &stm->buffers[i], sizeof(stm->buffers[i]));
|
|
}
|
|
}
|
|
|
|
waveOutClose(stm->waveout);
|
|
|
|
LeaveCriticalSection(&stm->lock);
|
|
}
|
|
|
|
if (stm->event) {
|
|
CloseHandle(stm->event);
|
|
}
|
|
|
|
DeleteCriticalSection(&stm->lock);
|
|
|
|
for (i = 0; i < NBUFS; ++i) {
|
|
free(stm->buffers[i].lpData);
|
|
}
|
|
|
|
EnterCriticalSection(&stm->context->lock);
|
|
XASSERT(stm->context->active_streams >= 1);
|
|
stm->context->active_streams -= 1;
|
|
LeaveCriticalSection(&stm->context->lock);
|
|
|
|
free(stm);
|
|
}
|
|
|
|
static int
|
|
winmm_get_max_channel_count(cubeb * ctx, uint32_t * max_channels)
|
|
{
|
|
XASSERT(ctx && max_channels);
|
|
|
|
/* We don't support more than two channels in this backend. */
|
|
*max_channels = 2;
|
|
|
|
return CUBEB_OK;
|
|
}
|
|
|
|
static int
|
|
winmm_get_min_latency(cubeb * ctx, cubeb_stream_params params, uint32_t * latency)
|
|
{
|
|
// 100ms minimum, if we are not in a bizarre configuration.
|
|
*latency = ctx->minimum_latency_ms * params.rate / 1000;
|
|
|
|
return CUBEB_OK;
|
|
}
|
|
|
|
static int
|
|
winmm_get_preferred_sample_rate(cubeb * ctx, uint32_t * rate)
|
|
{
|
|
WAVEOUTCAPS woc;
|
|
MMRESULT r;
|
|
|
|
r = waveOutGetDevCaps(WAVE_MAPPER, &woc, sizeof(WAVEOUTCAPS));
|
|
if (r != MMSYSERR_NOERROR) {
|
|
return CUBEB_ERROR;
|
|
}
|
|
|
|
/* Check if we support 48kHz, but not 44.1kHz. */
|
|
if (!(woc.dwFormats & WAVE_FORMAT_4S16) &&
|
|
woc.dwFormats & WAVE_FORMAT_48S16) {
|
|
*rate = 48000;
|
|
return CUBEB_OK;
|
|
}
|
|
/* Prefer 44.1kHz between 44.1kHz and 48kHz. */
|
|
*rate = 44100;
|
|
|
|
return CUBEB_OK;
|
|
}
|
|
|
|
static int
|
|
winmm_stream_start(cubeb_stream * stm)
|
|
{
|
|
MMRESULT r;
|
|
|
|
EnterCriticalSection(&stm->lock);
|
|
r = waveOutRestart(stm->waveout);
|
|
LeaveCriticalSection(&stm->lock);
|
|
|
|
if (r != MMSYSERR_NOERROR) {
|
|
return CUBEB_ERROR;
|
|
}
|
|
|
|
stm->state_callback(stm, stm->user_ptr, CUBEB_STATE_STARTED);
|
|
|
|
return CUBEB_OK;
|
|
}
|
|
|
|
static int
|
|
winmm_stream_stop(cubeb_stream * stm)
|
|
{
|
|
MMRESULT r;
|
|
|
|
EnterCriticalSection(&stm->lock);
|
|
r = waveOutPause(stm->waveout);
|
|
LeaveCriticalSection(&stm->lock);
|
|
|
|
if (r != MMSYSERR_NOERROR) {
|
|
return CUBEB_ERROR;
|
|
}
|
|
|
|
stm->state_callback(stm, stm->user_ptr, CUBEB_STATE_STOPPED);
|
|
|
|
return CUBEB_OK;
|
|
}
|
|
|
|
static int
|
|
winmm_stream_get_position(cubeb_stream * stm, uint64_t * position)
|
|
{
|
|
MMRESULT r;
|
|
MMTIME time;
|
|
|
|
EnterCriticalSection(&stm->lock);
|
|
time.wType = TIME_SAMPLES;
|
|
r = waveOutGetPosition(stm->waveout, &time, sizeof(time));
|
|
LeaveCriticalSection(&stm->lock);
|
|
|
|
if (r != MMSYSERR_NOERROR || time.wType != TIME_SAMPLES) {
|
|
return CUBEB_ERROR;
|
|
}
|
|
|
|
*position = time.u.sample;
|
|
|
|
return CUBEB_OK;
|
|
}
|
|
|
|
static int
|
|
winmm_stream_get_latency(cubeb_stream * stm, uint32_t * latency)
|
|
{
|
|
MMRESULT r;
|
|
MMTIME time;
|
|
uint64_t written;
|
|
|
|
EnterCriticalSection(&stm->lock);
|
|
time.wType = TIME_SAMPLES;
|
|
r = waveOutGetPosition(stm->waveout, &time, sizeof(time));
|
|
written = stm->written;
|
|
LeaveCriticalSection(&stm->lock);
|
|
|
|
if (r != MMSYSERR_NOERROR || time.wType != TIME_SAMPLES) {
|
|
return CUBEB_ERROR;
|
|
}
|
|
|
|
XASSERT(written - time.u.sample <= UINT32_MAX);
|
|
*latency = (uint32_t) (written - time.u.sample);
|
|
|
|
return CUBEB_OK;
|
|
}
|
|
|
|
static int
|
|
winmm_stream_set_volume(cubeb_stream * stm, float volume)
|
|
{
|
|
EnterCriticalSection(&stm->lock);
|
|
stm->soft_volume = volume;
|
|
LeaveCriticalSection(&stm->lock);
|
|
return CUBEB_OK;
|
|
}
|
|
|
|
#define MM_11025HZ_MASK (WAVE_FORMAT_1M08 | WAVE_FORMAT_1M16 | WAVE_FORMAT_1S08 | WAVE_FORMAT_1S16)
|
|
#define MM_22050HZ_MASK (WAVE_FORMAT_2M08 | WAVE_FORMAT_2M16 | WAVE_FORMAT_2S08 | WAVE_FORMAT_2S16)
|
|
#define MM_44100HZ_MASK (WAVE_FORMAT_4M08 | WAVE_FORMAT_4M16 | WAVE_FORMAT_4S08 | WAVE_FORMAT_4S16)
|
|
#define MM_48000HZ_MASK (WAVE_FORMAT_48M08 | WAVE_FORMAT_48M16 | WAVE_FORMAT_48S08 | WAVE_FORMAT_48S16)
|
|
#define MM_96000HZ_MASK (WAVE_FORMAT_96M08 | WAVE_FORMAT_96M16 | WAVE_FORMAT_96S08 | WAVE_FORMAT_96S16)
|
|
static void
|
|
winmm_calculate_device_rate(cubeb_device_info * info, DWORD formats)
|
|
{
|
|
if (formats & MM_11025HZ_MASK) {
|
|
info->min_rate = 11025;
|
|
info->default_rate = 11025;
|
|
info->max_rate = 11025;
|
|
}
|
|
if (formats & MM_22050HZ_MASK) {
|
|
if (info->min_rate == 0) info->min_rate = 22050;
|
|
info->max_rate = 22050;
|
|
info->default_rate = 22050;
|
|
}
|
|
if (formats & MM_44100HZ_MASK) {
|
|
if (info->min_rate == 0) info->min_rate = 44100;
|
|
info->max_rate = 44100;
|
|
info->default_rate = 44100;
|
|
}
|
|
if (formats & MM_48000HZ_MASK) {
|
|
if (info->min_rate == 0) info->min_rate = 48000;
|
|
info->max_rate = 48000;
|
|
info->default_rate = 48000;
|
|
}
|
|
if (formats & MM_96000HZ_MASK) {
|
|
if (info->min_rate == 0) {
|
|
info->min_rate = 96000;
|
|
info->default_rate = 96000;
|
|
}
|
|
info->max_rate = 96000;
|
|
}
|
|
}
|
|
|
|
#define MM_S16_MASK (WAVE_FORMAT_1M16 | WAVE_FORMAT_1S16 | WAVE_FORMAT_2M16 | WAVE_FORMAT_2S16 | WAVE_FORMAT_4M16 | \
|
|
WAVE_FORMAT_4S16 | WAVE_FORMAT_48M16 | WAVE_FORMAT_48S16 | WAVE_FORMAT_96M16 | WAVE_FORMAT_96S16)
|
|
static int
|
|
winmm_query_supported_formats(UINT devid, DWORD formats,
|
|
cubeb_device_fmt * supfmt, cubeb_device_fmt * deffmt)
|
|
{
|
|
WAVEFORMATEXTENSIBLE wfx;
|
|
|
|
if (formats & MM_S16_MASK)
|
|
*deffmt = *supfmt = CUBEB_DEVICE_FMT_S16LE;
|
|
else
|
|
*deffmt = *supfmt = 0;
|
|
|
|
ZeroMemory(&wfx, sizeof(WAVEFORMATEXTENSIBLE));
|
|
wfx.Format.wFormatTag = WAVE_FORMAT_EXTENSIBLE;
|
|
wfx.Format.nChannels = 2;
|
|
wfx.Format.nSamplesPerSec = 44100;
|
|
wfx.Format.wBitsPerSample = 32;
|
|
wfx.Format.nBlockAlign = (wfx.Format.wBitsPerSample * wfx.Format.nChannels) / 8;
|
|
wfx.Format.nAvgBytesPerSec = wfx.Format.nSamplesPerSec * wfx.Format.nBlockAlign;
|
|
wfx.Format.cbSize = 22;
|
|
wfx.Samples.wValidBitsPerSample = wfx.Format.wBitsPerSample;
|
|
wfx.dwChannelMask = SPEAKER_FRONT_LEFT | SPEAKER_FRONT_RIGHT;
|
|
wfx.SubFormat = KSDATAFORMAT_SUBTYPE_IEEE_FLOAT;
|
|
if (waveOutOpen(NULL, devid, &wfx.Format, 0, 0, WAVE_FORMAT_QUERY) == MMSYSERR_NOERROR)
|
|
*supfmt = (cubeb_device_fmt)(*supfmt | CUBEB_DEVICE_FMT_F32LE);
|
|
|
|
return (*deffmt != 0) ? CUBEB_OK : CUBEB_ERROR;
|
|
}
|
|
|
|
static char *
|
|
guid_to_cstr(LPGUID guid)
|
|
{
|
|
char * ret = malloc(40);
|
|
if (!ret) {
|
|
return NULL;
|
|
}
|
|
_snprintf_s(ret, 40, _TRUNCATE,
|
|
"{%08X-%04X-%04X-%02X%02X-%02X%02X%02X%02X%02X%02X}",
|
|
guid->Data1, guid->Data2, guid->Data3,
|
|
guid->Data4[0], guid->Data4[1], guid->Data4[2], guid->Data4[3],
|
|
guid->Data4[4], guid->Data4[5], guid->Data4[6], guid->Data4[7]);
|
|
return ret;
|
|
}
|
|
|
|
static cubeb_device_pref
|
|
winmm_query_preferred_out_device(UINT devid)
|
|
{
|
|
DWORD mmpref = WAVE_MAPPER, compref = WAVE_MAPPER, status;
|
|
cubeb_device_pref ret = CUBEB_DEVICE_PREF_NONE;
|
|
|
|
if (waveOutMessage((HWAVEOUT) WAVE_MAPPER, DRVM_MAPPER_PREFERRED_GET,
|
|
(DWORD_PTR)&mmpref, (DWORD_PTR)&status) == MMSYSERR_NOERROR &&
|
|
devid == mmpref)
|
|
ret |= CUBEB_DEVICE_PREF_MULTIMEDIA | CUBEB_DEVICE_PREF_NOTIFICATION;
|
|
|
|
if (waveOutMessage((HWAVEOUT) WAVE_MAPPER, DRVM_MAPPER_CONSOLEVOICECOM_GET,
|
|
(DWORD_PTR)&compref, (DWORD_PTR)&status) == MMSYSERR_NOERROR &&
|
|
devid == compref)
|
|
ret |= CUBEB_DEVICE_PREF_VOICE;
|
|
|
|
return ret;
|
|
}
|
|
|
|
static char *
|
|
device_id_idx(UINT devid)
|
|
{
|
|
char * ret = malloc(16);
|
|
if (!ret) {
|
|
return NULL;
|
|
}
|
|
_snprintf_s(ret, 16, _TRUNCATE, "%u", devid);
|
|
return ret;
|
|
}
|
|
|
|
static void
|
|
winmm_create_device_from_outcaps2(cubeb_device_info * ret, LPWAVEOUTCAPS2A caps, UINT devid)
|
|
{
|
|
XASSERT(ret);
|
|
ret->devid = (cubeb_devid) devid;
|
|
ret->device_id = device_id_idx(devid);
|
|
ret->friendly_name = _strdup(caps->szPname);
|
|
ret->group_id = guid_to_cstr(&caps->ProductGuid);
|
|
ret->vendor_name = guid_to_cstr(&caps->ManufacturerGuid);
|
|
|
|
ret->type = CUBEB_DEVICE_TYPE_OUTPUT;
|
|
ret->state = CUBEB_DEVICE_STATE_ENABLED;
|
|
ret->preferred = winmm_query_preferred_out_device(devid);
|
|
|
|
ret->max_channels = caps->wChannels;
|
|
winmm_calculate_device_rate(ret, caps->dwFormats);
|
|
winmm_query_supported_formats(devid, caps->dwFormats,
|
|
&ret->format, &ret->default_format);
|
|
|
|
/* Hardcoded latency estimates... */
|
|
ret->latency_lo = 100 * ret->default_rate / 1000;
|
|
ret->latency_hi = 200 * ret->default_rate / 1000;
|
|
}
|
|
|
|
static void
|
|
winmm_create_device_from_outcaps(cubeb_device_info * ret, LPWAVEOUTCAPSA caps, UINT devid)
|
|
{
|
|
XASSERT(ret);
|
|
ret->devid = (cubeb_devid) devid;
|
|
ret->device_id = device_id_idx(devid);
|
|
ret->friendly_name = _strdup(caps->szPname);
|
|
ret->group_id = NULL;
|
|
ret->vendor_name = NULL;
|
|
|
|
ret->type = CUBEB_DEVICE_TYPE_OUTPUT;
|
|
ret->state = CUBEB_DEVICE_STATE_ENABLED;
|
|
ret->preferred = winmm_query_preferred_out_device(devid);
|
|
|
|
ret->max_channels = caps->wChannels;
|
|
winmm_calculate_device_rate(ret, caps->dwFormats);
|
|
winmm_query_supported_formats(devid, caps->dwFormats,
|
|
&ret->format, &ret->default_format);
|
|
|
|
/* Hardcoded latency estimates... */
|
|
ret->latency_lo = 100 * ret->default_rate / 1000;
|
|
ret->latency_hi = 200 * ret->default_rate / 1000;
|
|
}
|
|
|
|
static cubeb_device_pref
|
|
winmm_query_preferred_in_device(UINT devid)
|
|
{
|
|
DWORD mmpref = WAVE_MAPPER, compref = WAVE_MAPPER, status;
|
|
cubeb_device_pref ret = CUBEB_DEVICE_PREF_NONE;
|
|
|
|
if (waveInMessage((HWAVEIN) WAVE_MAPPER, DRVM_MAPPER_PREFERRED_GET,
|
|
(DWORD_PTR)&mmpref, (DWORD_PTR)&status) == MMSYSERR_NOERROR &&
|
|
devid == mmpref)
|
|
ret |= CUBEB_DEVICE_PREF_MULTIMEDIA | CUBEB_DEVICE_PREF_NOTIFICATION;
|
|
|
|
if (waveInMessage((HWAVEIN) WAVE_MAPPER, DRVM_MAPPER_CONSOLEVOICECOM_GET,
|
|
(DWORD_PTR)&compref, (DWORD_PTR)&status) == MMSYSERR_NOERROR &&
|
|
devid == compref)
|
|
ret |= CUBEB_DEVICE_PREF_VOICE;
|
|
|
|
return ret;
|
|
}
|
|
|
|
static void
|
|
winmm_create_device_from_incaps2(cubeb_device_info * ret, LPWAVEINCAPS2A caps, UINT devid)
|
|
{
|
|
XASSERT(ret);
|
|
ret->devid = (cubeb_devid) devid;
|
|
ret->device_id = device_id_idx(devid);
|
|
ret->friendly_name = _strdup(caps->szPname);
|
|
ret->group_id = guid_to_cstr(&caps->ProductGuid);
|
|
ret->vendor_name = guid_to_cstr(&caps->ManufacturerGuid);
|
|
|
|
ret->type = CUBEB_DEVICE_TYPE_INPUT;
|
|
ret->state = CUBEB_DEVICE_STATE_ENABLED;
|
|
ret->preferred = winmm_query_preferred_in_device(devid);
|
|
|
|
ret->max_channels = caps->wChannels;
|
|
winmm_calculate_device_rate(ret, caps->dwFormats);
|
|
winmm_query_supported_formats(devid, caps->dwFormats,
|
|
&ret->format, &ret->default_format);
|
|
|
|
/* Hardcoded latency estimates... */
|
|
ret->latency_lo = 100 * ret->default_rate / 1000;
|
|
ret->latency_hi = 200 * ret->default_rate / 1000;
|
|
}
|
|
|
|
static void
|
|
winmm_create_device_from_incaps(cubeb_device_info * ret, LPWAVEINCAPSA caps, UINT devid)
|
|
{
|
|
XASSERT(ret);
|
|
ret->devid = (cubeb_devid) devid;
|
|
ret->device_id = device_id_idx(devid);
|
|
ret->friendly_name = _strdup(caps->szPname);
|
|
ret->group_id = NULL;
|
|
ret->vendor_name = NULL;
|
|
|
|
ret->type = CUBEB_DEVICE_TYPE_INPUT;
|
|
ret->state = CUBEB_DEVICE_STATE_ENABLED;
|
|
ret->preferred = winmm_query_preferred_in_device(devid);
|
|
|
|
ret->max_channels = caps->wChannels;
|
|
winmm_calculate_device_rate(ret, caps->dwFormats);
|
|
winmm_query_supported_formats(devid, caps->dwFormats,
|
|
&ret->format, &ret->default_format);
|
|
|
|
/* Hardcoded latency estimates... */
|
|
ret->latency_lo = 100 * ret->default_rate / 1000;
|
|
ret->latency_hi = 200 * ret->default_rate / 1000;
|
|
}
|
|
|
|
static int
|
|
winmm_enumerate_devices(cubeb * context, cubeb_device_type type,
|
|
cubeb_device_collection * collection)
|
|
{
|
|
UINT i, incount, outcount, total;
|
|
cubeb_device_info * devices;
|
|
cubeb_device_info * dev;
|
|
|
|
outcount = waveOutGetNumDevs();
|
|
incount = waveInGetNumDevs();
|
|
total = outcount + incount;
|
|
|
|
devices = calloc(total, sizeof(cubeb_device_info));
|
|
collection->count = 0;
|
|
|
|
if (type & CUBEB_DEVICE_TYPE_OUTPUT) {
|
|
WAVEOUTCAPSA woc;
|
|
WAVEOUTCAPS2A woc2;
|
|
|
|
ZeroMemory(&woc, sizeof(woc));
|
|
ZeroMemory(&woc2, sizeof(woc2));
|
|
|
|
for (i = 0; i < outcount; i++) {
|
|
dev = &devices[collection->count];
|
|
if (waveOutGetDevCapsA(i, (LPWAVEOUTCAPSA)&woc2, sizeof(woc2)) == MMSYSERR_NOERROR) {
|
|
winmm_create_device_from_outcaps2(dev, &woc2, i);
|
|
collection->count += 1;
|
|
} else if (waveOutGetDevCapsA(i, &woc, sizeof(woc)) == MMSYSERR_NOERROR) {
|
|
winmm_create_device_from_outcaps(dev, &woc, i);
|
|
collection->count += 1;
|
|
}
|
|
}
|
|
}
|
|
|
|
if (type & CUBEB_DEVICE_TYPE_INPUT) {
|
|
WAVEINCAPSA wic;
|
|
WAVEINCAPS2A wic2;
|
|
|
|
ZeroMemory(&wic, sizeof(wic));
|
|
ZeroMemory(&wic2, sizeof(wic2));
|
|
|
|
for (i = 0; i < incount; i++) {
|
|
dev = &devices[collection->count];
|
|
if (waveInGetDevCapsA(i, (LPWAVEINCAPSA)&wic2, sizeof(wic2)) == MMSYSERR_NOERROR) {
|
|
winmm_create_device_from_incaps2(dev, &wic2, i);
|
|
collection->count += 1;
|
|
} else if (waveInGetDevCapsA(i, &wic, sizeof(wic)) == MMSYSERR_NOERROR) {
|
|
winmm_create_device_from_incaps(dev, &wic, i);
|
|
collection->count += 1;
|
|
}
|
|
}
|
|
}
|
|
|
|
collection->device = devices;
|
|
|
|
return CUBEB_OK;
|
|
}
|
|
|
|
static int
|
|
winmm_device_collection_destroy(cubeb * ctx,
|
|
cubeb_device_collection * collection)
|
|
{
|
|
uint32_t i;
|
|
XASSERT(collection);
|
|
|
|
(void) ctx;
|
|
|
|
for (i = 0; i < collection->count; i++) {
|
|
free((void *) collection->device[i].device_id);
|
|
free((void *) collection->device[i].friendly_name);
|
|
free((void *) collection->device[i].group_id);
|
|
free((void *) collection->device[i].vendor_name);
|
|
}
|
|
|
|
free(collection->device);
|
|
return CUBEB_OK;
|
|
}
|
|
|
|
static struct cubeb_ops const winmm_ops = {
|
|
/*.init =*/ winmm_init,
|
|
/*.get_backend_id =*/ winmm_get_backend_id,
|
|
/*.get_max_channel_count=*/ winmm_get_max_channel_count,
|
|
/*.get_min_latency=*/ winmm_get_min_latency,
|
|
/*.get_preferred_sample_rate =*/ winmm_get_preferred_sample_rate,
|
|
/*.enumerate_devices =*/ winmm_enumerate_devices,
|
|
/*.device_collection_destroy =*/ winmm_device_collection_destroy,
|
|
/*.destroy =*/ winmm_destroy,
|
|
/*.stream_init =*/ winmm_stream_init,
|
|
/*.stream_destroy =*/ winmm_stream_destroy,
|
|
/*.stream_start =*/ winmm_stream_start,
|
|
/*.stream_stop =*/ winmm_stream_stop,
|
|
/*.stream_reset_default_device =*/ NULL,
|
|
/*.stream_get_position =*/ winmm_stream_get_position,
|
|
/*.stream_get_latency = */ winmm_stream_get_latency,
|
|
/*.stream_set_volume =*/ winmm_stream_set_volume,
|
|
/*.stream_get_current_device =*/ NULL,
|
|
/*.stream_device_destroy =*/ NULL,
|
|
/*.stream_register_device_changed_callback=*/ NULL,
|
|
/*.register_device_collection_changed =*/ NULL
|
|
};
|