michael@0: /* michael@0: * Copyright © 2011 Mozilla Foundation michael@0: * michael@0: * This program is made available under an ISC-style license. See the michael@0: * accompanying file LICENSE for details. michael@0: */ michael@0: #undef NDEBUG michael@0: #define __MSVCRT_VERSION__ 0x0700 michael@0: #define WINVER 0x0501 michael@0: #define WIN32_LEAN_AND_MEAN michael@0: #include michael@0: #include michael@0: #include michael@0: #include michael@0: #include michael@0: #include michael@0: #include michael@0: #include "cubeb/cubeb.h" michael@0: #include "cubeb-internal.h" michael@0: michael@0: /* This is missing from the MinGW headers. Use a safe fallback. */ michael@0: #if !defined(MEMORY_ALLOCATION_ALIGNMENT) michael@0: #define MEMORY_ALLOCATION_ALIGNMENT 16 michael@0: #endif michael@0: michael@0: #define CUBEB_STREAM_MAX 32 michael@0: #define NBUFS 4 michael@0: michael@0: const GUID KSDATAFORMAT_SUBTYPE_PCM = michael@0: { 0x00000001, 0x0000, 0x0010, { 0x80, 0x00, 0x00, 0xaa, 0x00, 0x38, 0x9b, 0x71 } }; michael@0: const GUID KSDATAFORMAT_SUBTYPE_IEEE_FLOAT = michael@0: { 0x00000003, 0x0000, 0x0010, { 0x80, 0x00, 0x00, 0xaa, 0x00, 0x38, 0x9b, 0x71 } }; michael@0: michael@0: struct cubeb_stream_item { michael@0: SLIST_ENTRY head; michael@0: cubeb_stream * stream; michael@0: }; michael@0: michael@0: static struct cubeb_ops const winmm_ops; michael@0: michael@0: struct cubeb { michael@0: struct cubeb_ops const * ops; michael@0: HANDLE event; michael@0: HANDLE thread; michael@0: int shutdown; michael@0: PSLIST_HEADER work; michael@0: CRITICAL_SECTION lock; michael@0: unsigned int active_streams; michael@0: unsigned int minimum_latency; michael@0: }; michael@0: michael@0: struct cubeb_stream { michael@0: cubeb * context; michael@0: cubeb_stream_params params; michael@0: cubeb_data_callback data_callback; michael@0: cubeb_state_callback state_callback; michael@0: void * user_ptr; michael@0: WAVEHDR buffers[NBUFS]; michael@0: size_t buffer_size; michael@0: int next_buffer; michael@0: int free_buffers; michael@0: int shutdown; michael@0: int draining; michael@0: HANDLE event; michael@0: HWAVEOUT waveout; michael@0: CRITICAL_SECTION lock; michael@0: uint64_t written; michael@0: }; michael@0: michael@0: static size_t michael@0: bytes_per_frame(cubeb_stream_params params) michael@0: { michael@0: size_t bytes; michael@0: michael@0: switch (params.format) { michael@0: case CUBEB_SAMPLE_S16LE: michael@0: bytes = sizeof(signed short); michael@0: break; michael@0: case CUBEB_SAMPLE_FLOAT32LE: michael@0: bytes = sizeof(float); michael@0: break; michael@0: default: michael@0: assert(0); michael@0: } michael@0: michael@0: return bytes * params.channels; michael@0: } michael@0: michael@0: static WAVEHDR * michael@0: winmm_get_next_buffer(cubeb_stream * stm) michael@0: { michael@0: WAVEHDR * hdr = NULL; michael@0: michael@0: assert(stm->free_buffers > 0 && stm->free_buffers <= NBUFS); michael@0: hdr = &stm->buffers[stm->next_buffer]; michael@0: assert(hdr->dwFlags & WHDR_PREPARED || michael@0: (hdr->dwFlags & WHDR_DONE && !(hdr->dwFlags & WHDR_INQUEUE))); michael@0: stm->next_buffer = (stm->next_buffer + 1) % NBUFS; michael@0: stm->free_buffers -= 1; michael@0: michael@0: return hdr; michael@0: } michael@0: michael@0: static void michael@0: winmm_refill_stream(cubeb_stream * stm) michael@0: { michael@0: WAVEHDR * hdr; michael@0: long got; michael@0: long wanted; michael@0: MMRESULT r; michael@0: michael@0: EnterCriticalSection(&stm->lock); michael@0: stm->free_buffers += 1; michael@0: assert(stm->free_buffers > 0 && stm->free_buffers <= NBUFS); michael@0: michael@0: if (stm->draining) { michael@0: LeaveCriticalSection(&stm->lock); michael@0: if (stm->free_buffers == NBUFS) { michael@0: stm->state_callback(stm, stm->user_ptr, CUBEB_STATE_DRAINED); michael@0: } michael@0: SetEvent(stm->event); michael@0: return; michael@0: } michael@0: michael@0: if (stm->shutdown) { michael@0: LeaveCriticalSection(&stm->lock); michael@0: SetEvent(stm->event); michael@0: return; michael@0: } michael@0: michael@0: hdr = winmm_get_next_buffer(stm); michael@0: michael@0: wanted = (DWORD) stm->buffer_size / bytes_per_frame(stm->params); michael@0: michael@0: /* It is assumed that the caller is holding this lock. It must be dropped michael@0: during the callback to avoid deadlocks. */ michael@0: LeaveCriticalSection(&stm->lock); michael@0: got = stm->data_callback(stm, stm->user_ptr, hdr->lpData, wanted); michael@0: EnterCriticalSection(&stm->lock); michael@0: if (got < 0) { michael@0: LeaveCriticalSection(&stm->lock); michael@0: /* XXX handle this case */ michael@0: assert(0); michael@0: return; michael@0: } else if (got < wanted) { michael@0: stm->draining = 1; michael@0: } michael@0: stm->written += got; michael@0: michael@0: assert(hdr->dwFlags & WHDR_PREPARED); michael@0: michael@0: hdr->dwBufferLength = got * bytes_per_frame(stm->params); michael@0: assert(hdr->dwBufferLength <= stm->buffer_size); michael@0: michael@0: r = waveOutWrite(stm->waveout, hdr, sizeof(*hdr)); michael@0: if (r != MMSYSERR_NOERROR) { michael@0: LeaveCriticalSection(&stm->lock); michael@0: stm->state_callback(stm, stm->user_ptr, CUBEB_STATE_ERROR); michael@0: return; michael@0: } michael@0: michael@0: LeaveCriticalSection(&stm->lock); michael@0: } michael@0: michael@0: static unsigned __stdcall michael@0: winmm_buffer_thread(void * user_ptr) michael@0: { michael@0: cubeb * ctx = (cubeb *) user_ptr; michael@0: assert(ctx); michael@0: michael@0: for (;;) { michael@0: DWORD rv; michael@0: PSLIST_ENTRY item; michael@0: michael@0: rv = WaitForSingleObject(ctx->event, INFINITE); michael@0: assert(rv == WAIT_OBJECT_0); michael@0: michael@0: /* Process work items in batches so that a single stream can't michael@0: starve the others by continuously adding new work to the top of michael@0: the work item stack. */ michael@0: item = InterlockedFlushSList(ctx->work); michael@0: while (item != NULL) { michael@0: PSLIST_ENTRY tmp = item; michael@0: winmm_refill_stream(((struct cubeb_stream_item *) tmp)->stream); michael@0: item = item->Next; michael@0: _aligned_free(tmp); michael@0: } michael@0: michael@0: if (ctx->shutdown) { michael@0: break; michael@0: } michael@0: } michael@0: michael@0: return 0; michael@0: } michael@0: michael@0: static void CALLBACK michael@0: winmm_buffer_callback(HWAVEOUT waveout, UINT msg, DWORD_PTR user_ptr, DWORD_PTR p1, DWORD_PTR p2) michael@0: { michael@0: cubeb_stream * stm = (cubeb_stream *) user_ptr; michael@0: struct cubeb_stream_item * item; michael@0: michael@0: if (msg != WOM_DONE) { michael@0: return; michael@0: } michael@0: michael@0: item = _aligned_malloc(sizeof(struct cubeb_stream_item), MEMORY_ALLOCATION_ALIGNMENT); michael@0: assert(item); michael@0: item->stream = stm; michael@0: InterlockedPushEntrySList(stm->context->work, &item->head); michael@0: michael@0: SetEvent(stm->context->event); michael@0: } michael@0: michael@0: static unsigned int michael@0: calculate_minimum_latency(void) michael@0: { michael@0: OSVERSIONINFOEX osvi; michael@0: DWORDLONG mask; michael@0: michael@0: /* Running under Terminal Services results in underruns with low latency. */ michael@0: if (GetSystemMetrics(SM_REMOTESESSION) == TRUE) { michael@0: return 500; michael@0: } michael@0: michael@0: /* Vista's WinMM implementation underruns when less than 200ms of audio is buffered. */ michael@0: memset(&osvi, 0, sizeof(OSVERSIONINFOEX)); michael@0: osvi.dwOSVersionInfoSize = sizeof(OSVERSIONINFOEX); michael@0: osvi.dwMajorVersion = 6; michael@0: osvi.dwMinorVersion = 0; michael@0: michael@0: mask = 0; michael@0: VER_SET_CONDITION(mask, VER_MAJORVERSION, VER_EQUAL); michael@0: VER_SET_CONDITION(mask, VER_MINORVERSION, VER_EQUAL); michael@0: michael@0: if (VerifyVersionInfo(&osvi, VER_MAJORVERSION | VER_MINORVERSION, mask) != 0) { michael@0: return 200; michael@0: } michael@0: michael@0: return 100; michael@0: } michael@0: michael@0: static void winmm_destroy(cubeb * ctx); michael@0: michael@0: /*static*/ int michael@0: winmm_init(cubeb ** context, char const * context_name) michael@0: { michael@0: cubeb * ctx; michael@0: michael@0: assert(context); michael@0: *context = NULL; michael@0: michael@0: ctx = calloc(1, sizeof(*ctx)); michael@0: assert(ctx); michael@0: michael@0: ctx->ops = &winmm_ops; michael@0: michael@0: ctx->work = _aligned_malloc(sizeof(*ctx->work), MEMORY_ALLOCATION_ALIGNMENT); michael@0: assert(ctx->work); michael@0: InitializeSListHead(ctx->work); michael@0: michael@0: ctx->event = CreateEvent(NULL, FALSE, FALSE, NULL); michael@0: if (!ctx->event) { michael@0: winmm_destroy(ctx); michael@0: return CUBEB_ERROR; michael@0: } michael@0: michael@0: ctx->thread = (HANDLE) _beginthreadex(NULL, 64 * 1024, winmm_buffer_thread, ctx, STACK_SIZE_PARAM_IS_A_RESERVATION, NULL); michael@0: if (!ctx->thread) { michael@0: winmm_destroy(ctx); michael@0: return CUBEB_ERROR; michael@0: } michael@0: michael@0: SetThreadPriority(ctx->thread, THREAD_PRIORITY_TIME_CRITICAL); michael@0: michael@0: InitializeCriticalSection(&ctx->lock); michael@0: ctx->active_streams = 0; michael@0: michael@0: ctx->minimum_latency = calculate_minimum_latency(); michael@0: michael@0: *context = ctx; michael@0: michael@0: return CUBEB_OK; michael@0: } michael@0: michael@0: static char const * michael@0: winmm_get_backend_id(cubeb * ctx) michael@0: { michael@0: return "winmm"; michael@0: } michael@0: michael@0: static void michael@0: winmm_destroy(cubeb * ctx) michael@0: { michael@0: DWORD rv; michael@0: michael@0: assert(ctx->active_streams == 0); michael@0: assert(!InterlockedPopEntrySList(ctx->work)); michael@0: michael@0: DeleteCriticalSection(&ctx->lock); michael@0: michael@0: if (ctx->thread) { michael@0: ctx->shutdown = 1; michael@0: SetEvent(ctx->event); michael@0: rv = WaitForSingleObject(ctx->thread, INFINITE); michael@0: assert(rv == WAIT_OBJECT_0); michael@0: CloseHandle(ctx->thread); michael@0: } michael@0: michael@0: if (ctx->event) { michael@0: CloseHandle(ctx->event); michael@0: } michael@0: michael@0: _aligned_free(ctx->work); michael@0: michael@0: free(ctx); michael@0: } michael@0: michael@0: static void winmm_stream_destroy(cubeb_stream * stm); michael@0: michael@0: static int michael@0: winmm_stream_init(cubeb * context, cubeb_stream ** stream, char const * stream_name, michael@0: cubeb_stream_params stream_params, unsigned int latency, michael@0: cubeb_data_callback data_callback, michael@0: cubeb_state_callback state_callback, michael@0: void * user_ptr) michael@0: { michael@0: MMRESULT r; michael@0: WAVEFORMATEXTENSIBLE wfx; michael@0: cubeb_stream * stm; michael@0: int i; michael@0: size_t bufsz; michael@0: michael@0: assert(context); michael@0: assert(stream); michael@0: michael@0: *stream = NULL; michael@0: michael@0: memset(&wfx, 0, sizeof(wfx)); michael@0: if (stream_params.channels > 2) { michael@0: wfx.Format.wFormatTag = WAVE_FORMAT_EXTENSIBLE; michael@0: wfx.Format.cbSize = sizeof(wfx) - sizeof(wfx.Format); michael@0: } else { michael@0: wfx.Format.wFormatTag = WAVE_FORMAT_PCM; michael@0: if (stream_params.format == CUBEB_SAMPLE_FLOAT32LE) { michael@0: wfx.Format.wFormatTag = WAVE_FORMAT_IEEE_FLOAT; michael@0: } michael@0: wfx.Format.cbSize = 0; michael@0: } michael@0: wfx.Format.nChannels = stream_params.channels; michael@0: wfx.Format.nSamplesPerSec = stream_params.rate; michael@0: michael@0: /* XXX fix channel mappings */ michael@0: wfx.dwChannelMask = SPEAKER_FRONT_LEFT | SPEAKER_FRONT_RIGHT; michael@0: michael@0: switch (stream_params.format) { michael@0: case CUBEB_SAMPLE_S16LE: michael@0: wfx.Format.wBitsPerSample = 16; michael@0: wfx.SubFormat = KSDATAFORMAT_SUBTYPE_PCM; michael@0: break; michael@0: case CUBEB_SAMPLE_FLOAT32LE: michael@0: wfx.Format.wBitsPerSample = 32; michael@0: wfx.SubFormat = KSDATAFORMAT_SUBTYPE_IEEE_FLOAT; michael@0: break; michael@0: default: michael@0: return CUBEB_ERROR_INVALID_FORMAT; michael@0: } michael@0: michael@0: wfx.Format.nBlockAlign = (wfx.Format.wBitsPerSample * wfx.Format.nChannels) / 8; michael@0: wfx.Format.nAvgBytesPerSec = wfx.Format.nSamplesPerSec * wfx.Format.nBlockAlign; michael@0: wfx.Samples.wValidBitsPerSample = wfx.Format.wBitsPerSample; michael@0: michael@0: EnterCriticalSection(&context->lock); michael@0: /* CUBEB_STREAM_MAX is a horrible hack to avoid a situation where, when michael@0: many streams are active at once, a subset of them will not consume (via michael@0: playback) or release (via waveOutReset) their buffers. */ michael@0: if (context->active_streams >= CUBEB_STREAM_MAX) { michael@0: LeaveCriticalSection(&context->lock); michael@0: return CUBEB_ERROR; michael@0: } michael@0: context->active_streams += 1; michael@0: LeaveCriticalSection(&context->lock); michael@0: michael@0: stm = calloc(1, sizeof(*stm)); michael@0: assert(stm); michael@0: michael@0: stm->context = context; michael@0: michael@0: stm->params = stream_params; michael@0: michael@0: stm->data_callback = data_callback; michael@0: stm->state_callback = state_callback; michael@0: stm->user_ptr = user_ptr; michael@0: stm->written = 0; michael@0: michael@0: if (latency < context->minimum_latency) { michael@0: latency = context->minimum_latency; michael@0: } michael@0: michael@0: bufsz = (size_t) (stm->params.rate / 1000.0 * latency * bytes_per_frame(stm->params) / NBUFS); michael@0: if (bufsz % bytes_per_frame(stm->params) != 0) { michael@0: bufsz += bytes_per_frame(stm->params) - (bufsz % bytes_per_frame(stm->params)); michael@0: } michael@0: assert(bufsz % bytes_per_frame(stm->params) == 0); michael@0: michael@0: stm->buffer_size = bufsz; michael@0: michael@0: InitializeCriticalSection(&stm->lock); michael@0: michael@0: stm->event = CreateEvent(NULL, FALSE, FALSE, NULL); michael@0: if (!stm->event) { michael@0: winmm_stream_destroy(stm); michael@0: return CUBEB_ERROR; michael@0: } michael@0: michael@0: /* winmm_buffer_callback will be called during waveOutOpen, so all michael@0: other initialization must be complete before calling it. */ michael@0: r = waveOutOpen(&stm->waveout, WAVE_MAPPER, &wfx.Format, michael@0: (DWORD_PTR) winmm_buffer_callback, (DWORD_PTR) stm, michael@0: CALLBACK_FUNCTION); michael@0: if (r != MMSYSERR_NOERROR) { michael@0: winmm_stream_destroy(stm); michael@0: return CUBEB_ERROR; michael@0: } michael@0: michael@0: r = waveOutPause(stm->waveout); michael@0: if (r != MMSYSERR_NOERROR) { michael@0: winmm_stream_destroy(stm); michael@0: return CUBEB_ERROR; michael@0: } michael@0: michael@0: for (i = 0; i < NBUFS; ++i) { michael@0: WAVEHDR * hdr = &stm->buffers[i]; michael@0: michael@0: hdr->lpData = calloc(1, bufsz); michael@0: assert(hdr->lpData); michael@0: hdr->dwBufferLength = bufsz; michael@0: hdr->dwFlags = 0; michael@0: michael@0: r = waveOutPrepareHeader(stm->waveout, hdr, sizeof(*hdr)); michael@0: if (r != MMSYSERR_NOERROR) { michael@0: winmm_stream_destroy(stm); michael@0: return CUBEB_ERROR; michael@0: } michael@0: michael@0: winmm_refill_stream(stm); michael@0: } michael@0: michael@0: *stream = stm; michael@0: michael@0: return CUBEB_OK; michael@0: } michael@0: michael@0: static void michael@0: winmm_stream_destroy(cubeb_stream * stm) michael@0: { michael@0: DWORD rv; michael@0: int i; michael@0: int enqueued; michael@0: michael@0: if (stm->waveout) { michael@0: EnterCriticalSection(&stm->lock); michael@0: stm->shutdown = 1; michael@0: michael@0: waveOutReset(stm->waveout); michael@0: michael@0: enqueued = NBUFS - stm->free_buffers; michael@0: LeaveCriticalSection(&stm->lock); michael@0: michael@0: /* Wait for all blocks to complete. */ michael@0: while (enqueued > 0) { michael@0: rv = WaitForSingleObject(stm->event, INFINITE); michael@0: assert(rv == WAIT_OBJECT_0); michael@0: michael@0: EnterCriticalSection(&stm->lock); michael@0: enqueued = NBUFS - stm->free_buffers; michael@0: LeaveCriticalSection(&stm->lock); michael@0: } michael@0: michael@0: EnterCriticalSection(&stm->lock); michael@0: michael@0: for (i = 0; i < NBUFS; ++i) { michael@0: if (stm->buffers[i].dwFlags & WHDR_PREPARED) { michael@0: waveOutUnprepareHeader(stm->waveout, &stm->buffers[i], sizeof(stm->buffers[i])); michael@0: } michael@0: } michael@0: michael@0: waveOutClose(stm->waveout); michael@0: michael@0: LeaveCriticalSection(&stm->lock); michael@0: } michael@0: michael@0: if (stm->event) { michael@0: CloseHandle(stm->event); michael@0: } michael@0: michael@0: DeleteCriticalSection(&stm->lock); michael@0: michael@0: for (i = 0; i < NBUFS; ++i) { michael@0: free(stm->buffers[i].lpData); michael@0: } michael@0: michael@0: EnterCriticalSection(&stm->context->lock); michael@0: assert(stm->context->active_streams >= 1); michael@0: stm->context->active_streams -= 1; michael@0: LeaveCriticalSection(&stm->context->lock); michael@0: michael@0: free(stm); michael@0: } michael@0: michael@0: static int michael@0: winmm_get_max_channel_count(cubeb * ctx, uint32_t * max_channels) michael@0: { michael@0: assert(ctx && max_channels); michael@0: michael@0: /* We don't support more than two channels in this backend. */ michael@0: *max_channels = 2; michael@0: michael@0: return CUBEB_OK; michael@0: } michael@0: michael@0: static int michael@0: winmm_get_min_latency(cubeb * ctx, cubeb_stream_params params, uint32_t * latency) michael@0: { michael@0: // 100ms minimum, if we are not in a bizarre configuration. michael@0: *latency = ctx->minimum_latency; michael@0: michael@0: return CUBEB_OK; michael@0: } michael@0: michael@0: static int michael@0: winmm_get_preferred_sample_rate(cubeb * ctx, uint32_t * rate) michael@0: { michael@0: WAVEOUTCAPS woc; michael@0: MMRESULT r; michael@0: michael@0: r = waveOutGetDevCaps(WAVE_MAPPER, &woc, sizeof(WAVEOUTCAPS)); michael@0: if (r != MMSYSERR_NOERROR) { michael@0: return CUBEB_ERROR; michael@0: } michael@0: michael@0: /* Check if we support 48kHz, but not 44.1kHz. */ michael@0: if (!(woc.dwFormats & WAVE_FORMAT_4S16) && michael@0: woc.dwFormats & WAVE_FORMAT_48S16) { michael@0: *rate = 48000; michael@0: return CUBEB_OK; michael@0: } michael@0: /* Prefer 44.1kHz between 44.1kHz and 48kHz. */ michael@0: *rate = 44100; michael@0: michael@0: return CUBEB_OK; michael@0: } michael@0: michael@0: static int michael@0: winmm_stream_start(cubeb_stream * stm) michael@0: { michael@0: MMRESULT r; michael@0: michael@0: EnterCriticalSection(&stm->lock); michael@0: r = waveOutRestart(stm->waveout); michael@0: LeaveCriticalSection(&stm->lock); michael@0: michael@0: if (r != MMSYSERR_NOERROR) { michael@0: return CUBEB_ERROR; michael@0: } michael@0: michael@0: stm->state_callback(stm, stm->user_ptr, CUBEB_STATE_STARTED); michael@0: michael@0: return CUBEB_OK; michael@0: } michael@0: michael@0: static int michael@0: winmm_stream_stop(cubeb_stream * stm) michael@0: { michael@0: MMRESULT r; michael@0: michael@0: EnterCriticalSection(&stm->lock); michael@0: r = waveOutPause(stm->waveout); michael@0: LeaveCriticalSection(&stm->lock); michael@0: michael@0: if (r != MMSYSERR_NOERROR) { michael@0: return CUBEB_ERROR; michael@0: } michael@0: michael@0: stm->state_callback(stm, stm->user_ptr, CUBEB_STATE_STOPPED); michael@0: michael@0: return CUBEB_OK; michael@0: } michael@0: michael@0: static int michael@0: winmm_stream_get_position(cubeb_stream * stm, uint64_t * position) michael@0: { michael@0: MMRESULT r; michael@0: MMTIME time; michael@0: michael@0: EnterCriticalSection(&stm->lock); michael@0: time.wType = TIME_SAMPLES; michael@0: r = waveOutGetPosition(stm->waveout, &time, sizeof(time)); michael@0: LeaveCriticalSection(&stm->lock); michael@0: michael@0: if (r != MMSYSERR_NOERROR || time.wType != TIME_SAMPLES) { michael@0: return CUBEB_ERROR; michael@0: } michael@0: michael@0: *position = time.u.sample; michael@0: michael@0: return CUBEB_OK; michael@0: } michael@0: michael@0: static int michael@0: winmm_stream_get_latency(cubeb_stream * stm, uint32_t * latency) michael@0: { michael@0: MMRESULT r; michael@0: MMTIME time; michael@0: uint64_t written; michael@0: michael@0: EnterCriticalSection(&stm->lock); michael@0: time.wType = TIME_SAMPLES; michael@0: r = waveOutGetPosition(stm->waveout, &time, sizeof(time)); michael@0: written = stm->written; michael@0: LeaveCriticalSection(&stm->lock); michael@0: michael@0: *latency = written - time.u.sample; michael@0: michael@0: return CUBEB_OK; michael@0: } michael@0: michael@0: static struct cubeb_ops const winmm_ops = { michael@0: /*.init =*/ winmm_init, michael@0: /*.get_backend_id =*/ winmm_get_backend_id, michael@0: /*.get_max_channel_count=*/ winmm_get_max_channel_count, michael@0: /*.get_min_latency=*/ winmm_get_min_latency, michael@0: /*.get_preferred_sample_rate =*/ winmm_get_preferred_sample_rate, michael@0: /*.destroy =*/ winmm_destroy, michael@0: /*.stream_init =*/ winmm_stream_init, michael@0: /*.stream_destroy =*/ winmm_stream_destroy, michael@0: /*.stream_start =*/ winmm_stream_start, michael@0: /*.stream_stop =*/ winmm_stream_stop, michael@0: /*.stream_get_position =*/ winmm_stream_get_position, michael@0: /*.stream_get_latency = */ winmm_stream_get_latency michael@0: };