michael@0: /* michael@0: * Copyright © 2013 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: #if defined(HAVE_CONFIG_H) michael@0: #include "config.h" michael@0: #endif michael@0: #include 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: #include "cubeb/cubeb-stdint.h" michael@0: #include "cubeb-speex-resampler.h" michael@0: #include michael@0: michael@0: #if 0 michael@0: # define LOG(...) do { \ michael@0: fprintf(stderr, __VA_ARGS__); \ michael@0: fprintf(stderr, "\n"); \ michael@0: } while(0); michael@0: #else michael@0: # define LOG(...) michael@0: #endif michael@0: michael@0: #define ARRAY_LENGTH(array_) \ michael@0: (sizeof(array_) / sizeof(array_[0])) michael@0: michael@0: namespace { michael@0: uint32_t michael@0: ms_to_hns(uint32_t ms) michael@0: { michael@0: return ms * 10000; michael@0: } michael@0: michael@0: uint32_t michael@0: hns_to_ms(uint32_t hns) michael@0: { michael@0: return hns / 10000; michael@0: } michael@0: michael@0: double michael@0: hns_to_s(uint32_t hns) michael@0: { michael@0: return static_cast(hns) / 10000000; michael@0: } michael@0: michael@0: long michael@0: frame_count_at_rate(long frame_count, float rate) michael@0: { michael@0: return static_cast(ceilf(rate * frame_count) + 1); michael@0: } michael@0: michael@0: void michael@0: SafeRelease(HANDLE handle) michael@0: { michael@0: if (handle) { michael@0: CloseHandle(handle); michael@0: } michael@0: } michael@0: michael@0: template michael@0: void SafeRelease(T * ptr) michael@0: { michael@0: if (ptr) { michael@0: ptr->Release(); michael@0: } michael@0: } michael@0: michael@0: typedef void (*refill_function2)(cubeb_stream * stm, michael@0: float * data, long frames_needed); michael@0: michael@0: typedef HANDLE (WINAPI *set_mm_thread_characteristics_function)( michael@0: const char* TaskName, LPDWORD TaskIndex); michael@0: typedef BOOL (WINAPI *revert_mm_thread_characteristics_function)(HANDLE handle); michael@0: michael@0: extern cubeb_ops const wasapi_ops; michael@0: } michael@0: michael@0: struct cubeb michael@0: { michael@0: cubeb_ops const * ops; michael@0: /* Library dynamically opened to increase the render michael@0: * thread priority, and the two function pointers we need. */ michael@0: HMODULE mmcss_module; michael@0: set_mm_thread_characteristics_function set_mm_thread_characteristics; michael@0: revert_mm_thread_characteristics_function revert_mm_thread_characteristics; michael@0: }; michael@0: michael@0: struct cubeb_stream michael@0: { michael@0: cubeb * context; michael@0: /* Mixer pameters. We need to convert the input michael@0: * stream to this samplerate/channel layout, as WASAPI michael@0: * does not resample nor upmix itself. */ michael@0: cubeb_stream_params mix_params; michael@0: cubeb_stream_params stream_params; michael@0: cubeb_state_callback state_callback; michael@0: cubeb_data_callback data_callback; michael@0: void * user_ptr; michael@0: /* Main handle on the WASAPI stream. */ michael@0: IAudioClient * client; michael@0: /* Interface pointer to use the event-driven interface. */ michael@0: IAudioRenderClient * render_client; michael@0: /* Interface pointer to use the clock facilities. */ michael@0: IAudioClock * audio_clock; michael@0: /* This event is set by the stream_stop and stream_destroy michael@0: * function, so the render loop can exit properly. */ michael@0: HANDLE shutdown_event; michael@0: /* This is set by WASAPI when we should refill the stream. */ michael@0: HANDLE refill_event; michael@0: /* Each cubeb_stream has its own thread. */ michael@0: HANDLE thread; michael@0: uint64_t clock_freq; michael@0: /* Maximum number of frames we can be requested in a callback. */ michael@0: uint32_t buffer_frame_count; michael@0: /* Resampler instance. If this is !NULL, resampling should happen. */ michael@0: SpeexResamplerState * resampler; michael@0: /* Buffer to resample from, into the mix buffer or the final buffer. */ michael@0: float * resampling_src_buffer; michael@0: /* Pointer to the function used to refill the buffer, depending michael@0: * on the respective samplerate of the stream and the mix. */ michael@0: refill_function2 refill_function; michael@0: /* Leftover frames handling, only used when resampling. */ michael@0: uint32_t leftover_frame_count; michael@0: uint32_t leftover_frame_size; michael@0: float * leftover_frames_buffer; michael@0: /* Buffer used to downmix or upmix to the number of channels the mixer has. michael@0: * its size is |frames_to_bytes_before_mix(buffer_frame_count)|. */ michael@0: float * mix_buffer; michael@0: /* True if the stream is draining. */ michael@0: bool draining; michael@0: }; michael@0: michael@0: namespace { michael@0: bool should_upmix(cubeb_stream * stream) michael@0: { michael@0: return stream->mix_params.channels > stream->stream_params.channels; michael@0: } michael@0: michael@0: bool should_downmix(cubeb_stream * stream) michael@0: { michael@0: return stream->mix_params.channels < stream->stream_params.channels; michael@0: } michael@0: michael@0: /* Upmix function, copies a mono channel in two interleaved michael@0: * stereo channel. |out| has to be twice as long as |in| */ michael@0: template michael@0: void michael@0: mono_to_stereo(T * in, long insamples, T * out) michael@0: { michael@0: int j = 0; michael@0: for (int i = 0; i < insamples; ++i, j += 2) { michael@0: out[j] = out[j + 1] = in[i]; michael@0: } michael@0: } michael@0: michael@0: template michael@0: void michael@0: upmix(T * in, long inframes, T * out, int32_t in_channels, int32_t out_channels) michael@0: { michael@0: assert(out_channels >= in_channels); michael@0: /* If we are playing a mono stream over stereo speakers, copy the data over. */ michael@0: if (in_channels == 1 && out_channels == 2) { michael@0: mono_to_stereo(in, inframes, out); michael@0: return; michael@0: } michael@0: /* Otherwise, put silence in other channels. */ michael@0: long out_index = 0; michael@0: for (long i = 0; i < inframes * in_channels; i += in_channels) { michael@0: for (int j = 0; j < in_channels; ++j) { michael@0: out[out_index + j] = in[i + j]; michael@0: } michael@0: for (int j = in_channels; j < out_channels; ++j) { michael@0: out[out_index + j] = 0.0; michael@0: } michael@0: out_index += out_channels; michael@0: } michael@0: } michael@0: michael@0: template michael@0: void michael@0: downmix(T * in, long inframes, T * out, int32_t in_channels, int32_t out_channels) michael@0: { michael@0: assert(in_channels >= out_channels); michael@0: /* We could use a downmix matrix here, applying mixing weight based on the michael@0: * channel, but directsound and winmm simply drop the channels that cannot be michael@0: * rendered by the hardware, so we do the same for consistency. */ michael@0: long out_index = 0; michael@0: for (long i = 0; i < inframes * in_channels; i += in_channels) { michael@0: for (int j = 0; j < out_channels; ++j) { michael@0: out[out_index + j] = in[i + j]; michael@0: } michael@0: out_index += out_channels; michael@0: } michael@0: } michael@0: michael@0: /* This returns the size of a frame in the stream, michael@0: * before the eventual upmix occurs. */ michael@0: static size_t michael@0: frames_to_bytes_before_mix(cubeb_stream * stm, size_t frames) michael@0: { michael@0: size_t stream_frame_size = stm->stream_params.channels * sizeof(float); michael@0: return stream_frame_size * frames; michael@0: } michael@0: michael@0: void michael@0: refill_with_resampling(cubeb_stream * stm, float * data, long frames_needed) michael@0: { michael@0: /* Use more input frames that strictly necessary, so in the worst case, michael@0: * we have leftover unresampled frames at the end, that we can use michael@0: * during the next iteration. */ michael@0: float rate = michael@0: static_cast(stm->stream_params.rate) / stm->mix_params.rate; michael@0: michael@0: long before_resampling = frame_count_at_rate(frames_needed, rate); michael@0: michael@0: long frame_requested = before_resampling - stm->leftover_frame_count; michael@0: michael@0: size_t leftover_bytes = michael@0: frames_to_bytes_before_mix(stm, stm->leftover_frame_count); michael@0: michael@0: /* Copy the previous leftover frames to the front of the buffer. */ michael@0: memcpy(stm->resampling_src_buffer, stm->leftover_frames_buffer, leftover_bytes); michael@0: uint8_t * buffer_start = reinterpret_cast( michael@0: stm->resampling_src_buffer) + leftover_bytes; michael@0: michael@0: long got = stm->data_callback(stm, stm->user_ptr, buffer_start, frame_requested); michael@0: michael@0: if (got != frame_requested) { michael@0: stm->draining = true; michael@0: } michael@0: michael@0: uint32_t in_frames = before_resampling; michael@0: uint32_t out_frames = frames_needed; michael@0: michael@0: /* If we need to upmix after resampling, resample into the mix buffer to michael@0: * avoid a copy. */ michael@0: float * resample_dest; michael@0: if (should_upmix(stm) || should_downmix(stm)) { michael@0: resample_dest = stm->mix_buffer; michael@0: } else { michael@0: resample_dest = data; michael@0: } michael@0: michael@0: speex_resampler_process_interleaved_float(stm->resampler, michael@0: stm->resampling_src_buffer, michael@0: &in_frames, michael@0: resample_dest, michael@0: &out_frames); michael@0: michael@0: /* Copy the leftover frames to buffer for the next time. */ michael@0: stm->leftover_frame_count = before_resampling - in_frames; michael@0: size_t unresampled_bytes = michael@0: frames_to_bytes_before_mix(stm, stm->leftover_frame_count); michael@0: michael@0: uint8_t * leftover_frames_start = michael@0: reinterpret_cast(stm->resampling_src_buffer); michael@0: leftover_frames_start += frames_to_bytes_before_mix(stm, in_frames); michael@0: michael@0: assert(stm->leftover_frame_count <= stm->leftover_frame_size); michael@0: memcpy(stm->leftover_frames_buffer, leftover_frames_start, unresampled_bytes); michael@0: michael@0: /* If this is not true, there will be glitches. michael@0: * It is alright to have produced less frames if we are draining, though. */ michael@0: assert(out_frames == frames_needed || stm->draining); michael@0: michael@0: if (should_upmix(stm)) { michael@0: upmix(resample_dest, out_frames, data, michael@0: stm->stream_params.channels, stm->mix_params.channels); michael@0: } else if (should_downmix(stm)) { michael@0: downmix(resample_dest, out_frames, data, michael@0: stm->stream_params.channels, stm->mix_params.channels); michael@0: } michael@0: } michael@0: michael@0: void michael@0: refill(cubeb_stream * stm, float * data, long frames_needed) michael@0: { michael@0: /* If we need to upmix/downmix, get the data into the mix buffer to avoid a michael@0: * copy, then do the processing process. */ michael@0: float * dest; michael@0: if (should_upmix(stm) || should_downmix(stm)) { michael@0: dest = stm->mix_buffer; michael@0: } else { michael@0: dest = data; michael@0: } michael@0: michael@0: long got = stm->data_callback(stm, stm->user_ptr, dest, frames_needed); michael@0: assert(got <= frames_needed); michael@0: if (got != frames_needed) { michael@0: LOG("draining."); michael@0: stm->draining = true; michael@0: } michael@0: michael@0: if (should_upmix(stm)) { michael@0: upmix(dest, got, data, michael@0: stm->stream_params.channels, stm->mix_params.channels); michael@0: } else if (should_downmix(stm)) { michael@0: downmix(dest, got, data, michael@0: stm->stream_params.channels, stm->mix_params.channels); michael@0: } michael@0: } michael@0: michael@0: static unsigned int __stdcall michael@0: wasapi_stream_render_loop(LPVOID stream) michael@0: { michael@0: cubeb_stream * stm = static_cast(stream); michael@0: michael@0: bool is_playing = true; michael@0: HANDLE wait_array[2] = {stm->shutdown_event, stm->refill_event}; michael@0: HANDLE mmcss_handle = NULL; michael@0: HRESULT hr; michael@0: bool first = true; michael@0: DWORD mmcss_task_index = 0; michael@0: michael@0: /* We could consider using "Pro Audio" here for WebAudio and michael@0: * maybe WebRTC. */ michael@0: mmcss_handle = michael@0: stm->context->set_mm_thread_characteristics("Audio", &mmcss_task_index); michael@0: if (!mmcss_handle) { michael@0: /* This is not fatal, but we might glitch under heavy load. */ michael@0: LOG("Unable to use mmcss to bump the render thread priority: %x", GetLastError()); michael@0: } michael@0: michael@0: hr = CoInitializeEx(NULL, COINIT_MULTITHREADED); michael@0: if (FAILED(hr)) { michael@0: LOG("could not initialize COM in render thread: %x", hr); michael@0: return hr; michael@0: } michael@0: michael@0: while (is_playing) { michael@0: DWORD waitResult = WaitForMultipleObjects(ARRAY_LENGTH(wait_array), michael@0: wait_array, michael@0: FALSE, michael@0: INFINITE); michael@0: michael@0: switch (waitResult) { michael@0: case WAIT_OBJECT_0: { /* shutdown */ michael@0: is_playing = false; michael@0: /* We don't check if the drain is actually finished here, we just want to michael@0: * shutdown. */ michael@0: if (stm->draining) { michael@0: stm->state_callback(stm, stm->user_ptr, CUBEB_STATE_DRAINED); michael@0: } michael@0: continue; michael@0: } michael@0: case WAIT_OBJECT_0 + 1: { /* refill */ michael@0: UINT32 padding; michael@0: michael@0: hr = stm->client->GetCurrentPadding(&padding); michael@0: if (FAILED(hr)) { michael@0: LOG("Failed to get padding"); michael@0: is_playing = false; michael@0: continue; michael@0: } michael@0: assert(padding <= stm->buffer_frame_count); michael@0: michael@0: if (stm->draining) { michael@0: if (padding == 0) { michael@0: stm->state_callback(stm, stm->user_ptr, CUBEB_STATE_DRAINED); michael@0: is_playing = false; michael@0: } michael@0: continue; michael@0: } michael@0: michael@0: long available = stm->buffer_frame_count - padding; michael@0: michael@0: if (available == 0) { michael@0: continue; michael@0: } michael@0: michael@0: BYTE* data; michael@0: hr = stm->render_client->GetBuffer(available, &data); michael@0: if (SUCCEEDED(hr)) { michael@0: stm->refill_function(stm, reinterpret_cast(data), available); michael@0: michael@0: hr = stm->render_client->ReleaseBuffer(available, 0); michael@0: if (FAILED(hr)) { michael@0: LOG("failed to release buffer."); michael@0: is_playing = false; michael@0: } michael@0: } else { michael@0: LOG("failed to get buffer."); michael@0: is_playing = false; michael@0: } michael@0: } michael@0: break; michael@0: default: michael@0: LOG("case %d not handled in render loop.", waitResult); michael@0: abort(); michael@0: } michael@0: } michael@0: michael@0: if (FAILED(hr)) { michael@0: stm->state_callback(stm, stm->user_ptr, CUBEB_STATE_STOPPED); michael@0: } michael@0: michael@0: stm->context->revert_mm_thread_characteristics(mmcss_handle); michael@0: michael@0: CoUninitialize(); michael@0: return 0; michael@0: } michael@0: michael@0: void wasapi_destroy(cubeb * context); michael@0: michael@0: HANDLE WINAPI set_mm_thread_characteristics_noop(const char *, LPDWORD mmcss_task_index) michael@0: { michael@0: return (HANDLE)1; michael@0: } michael@0: michael@0: BOOL WINAPI revert_mm_thread_characteristics_noop(HANDLE mmcss_handle) michael@0: { michael@0: return true; michael@0: } michael@0: michael@0: HRESULT get_default_endpoint(IMMDevice ** device) michael@0: { michael@0: IMMDeviceEnumerator * enumerator; michael@0: HRESULT hr = CoCreateInstance(__uuidof(MMDeviceEnumerator), michael@0: NULL, CLSCTX_INPROC_SERVER, michael@0: IID_PPV_ARGS(&enumerator)); michael@0: if (FAILED(hr)) { michael@0: LOG("Could not get device enumerator."); michael@0: return hr; michael@0: } michael@0: /* eMultimedia is okay for now ("Music, movies, narration, [...]"). michael@0: * We will need to change this when we distinguish streams by use-case, other michael@0: * possible values being eConsole ("Games, system notification sounds [...]") michael@0: * and eCommunication ("Voice communication"). */ michael@0: hr = enumerator->GetDefaultAudioEndpoint(eRender, eMultimedia, device); michael@0: if (FAILED(hr)) { michael@0: LOG("Could not get default audio endpoint."); michael@0: SafeRelease(enumerator); michael@0: return hr; michael@0: } michael@0: michael@0: SafeRelease(enumerator); michael@0: michael@0: return ERROR_SUCCESS; michael@0: } michael@0: } // namespace anonymous michael@0: michael@0: extern "C" { michael@0: int wasapi_init(cubeb ** context, char const * context_name) michael@0: { michael@0: HRESULT hr; michael@0: michael@0: hr = CoInitializeEx(NULL, COINIT_MULTITHREADED); michael@0: if (FAILED(hr)) { michael@0: LOG("Could not init COM."); michael@0: return CUBEB_ERROR; michael@0: } michael@0: michael@0: /* We don't use the device yet, but need to make sure we can initialize one michael@0: so that this backend is not incorrectly enabled on platforms that don't michael@0: support WASAPI. */ michael@0: IMMDevice * device; michael@0: hr = get_default_endpoint(&device); michael@0: if (FAILED(hr)) { michael@0: LOG("Could not get device."); michael@0: return CUBEB_ERROR; michael@0: } michael@0: SafeRelease(device); michael@0: michael@0: cubeb * ctx = (cubeb *)calloc(1, sizeof(cubeb)); michael@0: michael@0: ctx->ops = &wasapi_ops; michael@0: michael@0: ctx->mmcss_module = LoadLibraryA("Avrt.dll"); michael@0: michael@0: if (ctx->mmcss_module) { michael@0: ctx->set_mm_thread_characteristics = michael@0: (set_mm_thread_characteristics_function) GetProcAddress( michael@0: ctx->mmcss_module, "AvSetMmThreadCharacteristicsA"); michael@0: ctx->revert_mm_thread_characteristics = michael@0: (revert_mm_thread_characteristics_function) GetProcAddress( michael@0: ctx->mmcss_module, "AvRevertMmThreadCharacteristics"); michael@0: if (!(ctx->set_mm_thread_characteristics && ctx->revert_mm_thread_characteristics)) { michael@0: LOG("Could not load AvSetMmThreadCharacteristics or AvRevertMmThreadCharacteristics: %x", GetLastError()); michael@0: FreeLibrary(ctx->mmcss_module); michael@0: } michael@0: } else { michael@0: // This is not a fatal error, but we might end up glitching when michael@0: // the system is under high load. michael@0: LOG("Could not load Avrt.dll"); michael@0: ctx->set_mm_thread_characteristics = &set_mm_thread_characteristics_noop; michael@0: ctx->revert_mm_thread_characteristics = &revert_mm_thread_characteristics_noop; michael@0: } michael@0: michael@0: *context = ctx; michael@0: michael@0: return CUBEB_OK; michael@0: } michael@0: } michael@0: michael@0: namespace { michael@0: michael@0: void wasapi_destroy(cubeb * context) michael@0: { michael@0: if (context->mmcss_module) { michael@0: FreeLibrary(context->mmcss_module); michael@0: } michael@0: free(context); michael@0: } michael@0: michael@0: char const* wasapi_get_backend_id(cubeb * context) michael@0: { michael@0: return "wasapi"; michael@0: } michael@0: michael@0: int michael@0: wasapi_get_max_channel_count(cubeb * ctx, uint32_t * max_channels) michael@0: { michael@0: IAudioClient * client; michael@0: WAVEFORMATEX * mix_format; michael@0: michael@0: assert(ctx && max_channels); michael@0: michael@0: IMMDevice * device; michael@0: HRESULT hr = get_default_endpoint(&device); michael@0: if (FAILED(hr)) { michael@0: return CUBEB_ERROR; michael@0: } michael@0: michael@0: hr = device->Activate(__uuidof(IAudioClient), michael@0: CLSCTX_INPROC_SERVER, michael@0: NULL, (void **)&client); michael@0: SafeRelease(device); michael@0: if (FAILED(hr)) { michael@0: return CUBEB_ERROR; michael@0: } michael@0: michael@0: hr = client->GetMixFormat(&mix_format); michael@0: if (FAILED(hr)) { michael@0: SafeRelease(client); michael@0: return CUBEB_ERROR; michael@0: } michael@0: michael@0: *max_channels = mix_format->nChannels; michael@0: michael@0: CoTaskMemFree(mix_format); michael@0: SafeRelease(client); michael@0: michael@0: return CUBEB_OK; michael@0: } michael@0: michael@0: int michael@0: wasapi_get_min_latency(cubeb * ctx, cubeb_stream_params params, uint32_t * latency_ms) michael@0: { michael@0: HRESULT hr; michael@0: IAudioClient * client; michael@0: REFERENCE_TIME default_period; michael@0: michael@0: IMMDevice * device; michael@0: hr = get_default_endpoint(&device); michael@0: if (FAILED(hr)) { michael@0: return CUBEB_ERROR; michael@0: } michael@0: michael@0: hr = device->Activate(__uuidof(IAudioClient), michael@0: CLSCTX_INPROC_SERVER, michael@0: NULL, (void **)&client); michael@0: SafeRelease(device); michael@0: if (FAILED(hr)) { michael@0: return CUBEB_ERROR; michael@0: } michael@0: michael@0: /* The second parameter is for exclusive mode, that we don't use. */ michael@0: hr = client->GetDevicePeriod(&default_period, NULL); michael@0: if (FAILED(hr)) { michael@0: SafeRelease(client); michael@0: return CUBEB_ERROR; michael@0: } michael@0: michael@0: /* According to the docs, the best latency we can achieve is by synchronizing michael@0: * the stream and the engine. michael@0: * http://msdn.microsoft.com/en-us/library/windows/desktop/dd370871%28v=vs.85%29.aspx */ michael@0: *latency_ms = hns_to_ms(default_period); michael@0: michael@0: SafeRelease(client); michael@0: michael@0: return CUBEB_OK; michael@0: } michael@0: michael@0: int michael@0: wasapi_get_preferred_sample_rate(cubeb * ctx, uint32_t * rate) michael@0: { michael@0: HRESULT hr; michael@0: IAudioClient * client; michael@0: WAVEFORMATEX * mix_format; michael@0: michael@0: IMMDevice * device; michael@0: hr = get_default_endpoint(&device); michael@0: if (FAILED(hr)) { michael@0: return CUBEB_ERROR; michael@0: } michael@0: michael@0: hr = device->Activate(__uuidof(IAudioClient), michael@0: CLSCTX_INPROC_SERVER, michael@0: NULL, (void **)&client); michael@0: SafeRelease(device); michael@0: if (FAILED(hr)) { michael@0: return CUBEB_ERROR; michael@0: } michael@0: michael@0: hr = client->GetMixFormat(&mix_format); michael@0: if (FAILED(hr)) { michael@0: SafeRelease(client); michael@0: return CUBEB_ERROR; michael@0: } michael@0: michael@0: *rate = mix_format->nSamplesPerSec; michael@0: michael@0: CoTaskMemFree(mix_format); michael@0: SafeRelease(client); michael@0: michael@0: return CUBEB_OK; michael@0: } michael@0: michael@0: void wasapi_stream_destroy(cubeb_stream * stm); michael@0: michael@0: /* Based on the mix format and the stream format, try to find a way to play what michael@0: * the user requested. */ michael@0: static void michael@0: handle_channel_layout(cubeb_stream * stm, WAVEFORMATEX ** mix_format, const cubeb_stream_params * stream_params) michael@0: { michael@0: /* Common case: the hardware is stereo. Up-mixing and down-mixing will be michael@0: * handled in the callback. */ michael@0: if ((*mix_format)->nChannels <= 2) { michael@0: return; michael@0: } michael@0: michael@0: /* Otherwise, the hardware supports more than two channels. */ michael@0: WAVEFORMATEX hw_mixformat = **mix_format; michael@0: michael@0: /* The docs say that GetMixFormat is always of type WAVEFORMATEXTENSIBLE [1], michael@0: * so the reinterpret_cast below should be safe. In practice, this is not michael@0: * true, and we just want to bail out and let the rest of the code find a good michael@0: * conversion path instead of trying to make WASAPI do it by itself. michael@0: * [1]: http://msdn.microsoft.com/en-us/library/windows/desktop/dd370811%28v=vs.85%29.aspx*/ michael@0: if ((*mix_format)->wFormatTag != WAVE_FORMAT_EXTENSIBLE) { michael@0: return; michael@0: } michael@0: michael@0: /* The hardware is in surround mode, we want to only use front left and front michael@0: * right. Try that, and check if it works. */ michael@0: WAVEFORMATEXTENSIBLE * format_pcm = reinterpret_cast((*mix_format)); michael@0: switch (stream_params->channels) { michael@0: case 1: /* Mono */ michael@0: format_pcm->dwChannelMask = KSAUDIO_SPEAKER_MONO; michael@0: break; michael@0: case 2: /* Stereo */ michael@0: format_pcm->dwChannelMask = KSAUDIO_SPEAKER_STEREO; michael@0: break; michael@0: default: michael@0: assert(false && "Channel layout not supported."); michael@0: break; michael@0: } michael@0: (*mix_format)->nChannels = stream_params->channels; michael@0: (*mix_format)->nBlockAlign = ((*mix_format)->wBitsPerSample * (*mix_format)->nChannels) / 8; michael@0: (*mix_format)->nAvgBytesPerSec = (*mix_format)->nSamplesPerSec * (*mix_format)->nBlockAlign; michael@0: format_pcm->SubFormat = KSDATAFORMAT_SUBTYPE_IEEE_FLOAT; michael@0: (*mix_format)->wBitsPerSample = 32; michael@0: format_pcm->Samples.wValidBitsPerSample = (*mix_format)->wBitsPerSample; michael@0: michael@0: /* Check if wasapi will accept our channel layout request. */ michael@0: WAVEFORMATEX * closest; michael@0: HRESULT hr = stm->client->IsFormatSupported(AUDCLNT_SHAREMODE_SHARED, michael@0: *mix_format, michael@0: &closest); michael@0: michael@0: if (hr == S_FALSE) { michael@0: /* Not supported, but WASAPI gives us a suggestion. Use it, and handle the michael@0: * eventual upmix/downmix ourselves */ michael@0: LOG("Using WASAPI suggested format: channels: %d", closest->nChannels); michael@0: WAVEFORMATEXTENSIBLE * closest_pcm = reinterpret_cast(closest); michael@0: assert(closest_pcm->SubFormat == format_pcm->SubFormat); michael@0: CoTaskMemFree(*mix_format); michael@0: *mix_format = closest; michael@0: } else if (hr == AUDCLNT_E_UNSUPPORTED_FORMAT) { michael@0: /* Not supported, no suggestion. This should not happen, but it does in the michael@0: * field with some sound cards. We restore the mix format, and let the rest michael@0: * of the code figure out the right conversion path. */ michael@0: **mix_format = hw_mixformat; michael@0: } else if (hr == S_OK) { michael@0: LOG("Requested format accepted by WASAPI."); michael@0: } michael@0: } michael@0: michael@0: int michael@0: wasapi_stream_init(cubeb * context, cubeb_stream ** stream, michael@0: char const * stream_name, cubeb_stream_params stream_params, michael@0: unsigned int latency, cubeb_data_callback data_callback, michael@0: cubeb_state_callback state_callback, void * user_ptr) michael@0: { michael@0: HRESULT hr; michael@0: WAVEFORMATEX * mix_format; michael@0: michael@0: assert(context && stream); michael@0: michael@0: hr = CoInitializeEx(NULL, COINIT_MULTITHREADED); michael@0: if (FAILED(hr)) { michael@0: LOG("Could not initialize COM."); michael@0: return CUBEB_ERROR; michael@0: } michael@0: michael@0: cubeb_stream * stm = (cubeb_stream *)calloc(1, sizeof(cubeb_stream)); michael@0: michael@0: assert(stm); michael@0: michael@0: stm->context = context; michael@0: stm->data_callback = data_callback; michael@0: stm->state_callback = state_callback; michael@0: stm->user_ptr = user_ptr; michael@0: stm->stream_params = stream_params; michael@0: stm->draining = false; michael@0: michael@0: stm->shutdown_event = CreateEvent(NULL, 0, 0, NULL); michael@0: stm->refill_event = CreateEvent(NULL, 0, 0, NULL); michael@0: michael@0: if (!stm->shutdown_event) { michael@0: LOG("Can't create the shutdown event, error: %x.", GetLastError()); michael@0: wasapi_stream_destroy(stm); michael@0: return CUBEB_ERROR; michael@0: } michael@0: michael@0: if (!stm->refill_event) { michael@0: SafeRelease(stm->shutdown_event); michael@0: LOG("Can't create the refill event, error: %x.", GetLastError()); michael@0: wasapi_stream_destroy(stm); michael@0: return CUBEB_ERROR; michael@0: } michael@0: michael@0: IMMDevice * device; michael@0: hr = get_default_endpoint(&device); michael@0: if (FAILED(hr)) { michael@0: LOG("Could not get default endpoint, error: %x", hr); michael@0: wasapi_stream_destroy(stm); michael@0: return CUBEB_ERROR; michael@0: } michael@0: michael@0: /* Get a client. We will get all other interfaces we need from michael@0: * this pointer. */ michael@0: hr = device->Activate(__uuidof(IAudioClient), michael@0: CLSCTX_INPROC_SERVER, michael@0: NULL, (void **)&stm->client); michael@0: SafeRelease(device); michael@0: if (FAILED(hr)) { michael@0: LOG("Could not activate the device to get an audio client: error: %x", hr); michael@0: wasapi_stream_destroy(stm); michael@0: return CUBEB_ERROR; michael@0: } michael@0: michael@0: /* We have to distinguish between the format the mixer uses, michael@0: * and the format the stream we want to play uses. */ michael@0: hr = stm->client->GetMixFormat(&mix_format); michael@0: if (FAILED(hr)) { michael@0: LOG("Could not fetch current mix format from the audio client: error: %x", hr); michael@0: wasapi_stream_destroy(stm); michael@0: return CUBEB_ERROR; michael@0: } michael@0: michael@0: handle_channel_layout(stm, &mix_format, &stream_params); michael@0: michael@0: /* Shared mode WASAPI always supports float32 sample format, so this michael@0: * is safe. */ michael@0: stm->mix_params.format = CUBEB_SAMPLE_FLOAT32NE; michael@0: michael@0: stm->mix_params.rate = mix_format->nSamplesPerSec; michael@0: stm->mix_params.channels = mix_format->nChannels; michael@0: michael@0: float resampling_rate = static_cast(stm->stream_params.rate) / michael@0: stm->mix_params.rate; michael@0: michael@0: if (resampling_rate != 1.0) { michael@0: /* If we are playing a mono stream, we only resample one channel, michael@0: * and copy it over, so we are always resampling the number michael@0: * of channels of the stream, not the number of channels michael@0: * that WASAPI wants. */ michael@0: stm->resampler = speex_resampler_init(stm->stream_params.channels, michael@0: stm->stream_params.rate, michael@0: stm->mix_params.rate, michael@0: SPEEX_RESAMPLER_QUALITY_DESKTOP, michael@0: NULL); michael@0: if (!stm->resampler) { michael@0: LOG("Could not get a resampler"); michael@0: CoTaskMemFree(mix_format); michael@0: wasapi_stream_destroy(stm); michael@0: return CUBEB_ERROR; michael@0: } michael@0: michael@0: /* Get a little buffer so we can store the leftover frames, michael@0: * that is, the samples not consumed by the resampler that we will end up michael@0: * using next time the render callback is called. */ michael@0: stm->leftover_frame_size = static_cast(ceilf(1 / resampling_rate * 2) + 1); michael@0: stm->leftover_frames_buffer = (float *)malloc(frames_to_bytes_before_mix(stm, stm->leftover_frame_size)); michael@0: michael@0: stm->refill_function = &refill_with_resampling; michael@0: } else { michael@0: stm->refill_function = &refill; michael@0: } michael@0: michael@0: hr = stm->client->Initialize(AUDCLNT_SHAREMODE_SHARED, michael@0: AUDCLNT_STREAMFLAGS_EVENTCALLBACK | michael@0: AUDCLNT_STREAMFLAGS_NOPERSIST, michael@0: ms_to_hns(latency), michael@0: 0, michael@0: mix_format, michael@0: NULL); michael@0: michael@0: CoTaskMemFree(mix_format); michael@0: michael@0: if (FAILED(hr)) { michael@0: LOG("Unable to initialize audio client: %x.", hr); michael@0: wasapi_stream_destroy(stm); michael@0: return CUBEB_ERROR; michael@0: } michael@0: michael@0: hr = stm->client->GetBufferSize(&stm->buffer_frame_count); michael@0: if (FAILED(hr)) { michael@0: LOG("Could not get the buffer size from the client %x.", hr); michael@0: wasapi_stream_destroy(stm); michael@0: return CUBEB_ERROR; michael@0: } michael@0: michael@0: if (should_upmix(stm) || should_downmix(stm)) { michael@0: stm->mix_buffer = (float *) malloc(frames_to_bytes_before_mix(stm, stm->buffer_frame_count)); michael@0: } michael@0: michael@0: /* If we are going to resample, we will end up needing a buffer michael@0: * to resample from, because speex's resampler does not do michael@0: * in-place processing. Of course we need to take the resampling michael@0: * factor and the channel layout into account. */ michael@0: if (stm->resampler) { michael@0: size_t frames_needed = static_cast(frame_count_at_rate(stm->buffer_frame_count, resampling_rate)); michael@0: stm->resampling_src_buffer = (float *)malloc(frames_to_bytes_before_mix(stm, frames_needed)); michael@0: } michael@0: michael@0: hr = stm->client->SetEventHandle(stm->refill_event); michael@0: if (FAILED(hr)) { michael@0: LOG("Could set the event handle for the client %x.", hr); michael@0: wasapi_stream_destroy(stm); michael@0: return CUBEB_ERROR; michael@0: } michael@0: michael@0: hr = stm->client->GetService(__uuidof(IAudioRenderClient), michael@0: (void **)&stm->render_client); michael@0: if (FAILED(hr)) { michael@0: LOG("Could not get the render client %x.", hr); michael@0: wasapi_stream_destroy(stm); michael@0: return CUBEB_ERROR; michael@0: } michael@0: michael@0: hr = stm->client->GetService(__uuidof(IAudioClock), michael@0: (void **)&stm->audio_clock); michael@0: if (FAILED(hr)) { michael@0: LOG("Could not get the IAudioClock, %x", hr); michael@0: wasapi_stream_destroy(stm); michael@0: return CUBEB_ERROR; michael@0: } michael@0: michael@0: hr = stm->audio_clock->GetFrequency(&stm->clock_freq); michael@0: if (FAILED(hr)) { michael@0: LOG("failed to get audio clock frequency, %x", hr); michael@0: return CUBEB_ERROR; michael@0: } michael@0: michael@0: *stream = stm; michael@0: michael@0: return CUBEB_OK; michael@0: } michael@0: michael@0: void wasapi_stream_destroy(cubeb_stream * stm) michael@0: { michael@0: assert(stm); michael@0: michael@0: if (stm->thread) { michael@0: SetEvent(stm->shutdown_event); michael@0: WaitForSingleObject(stm->thread, INFINITE); michael@0: CloseHandle(stm->thread); michael@0: stm->thread = 0; michael@0: } michael@0: michael@0: SafeRelease(stm->shutdown_event); michael@0: SafeRelease(stm->refill_event); michael@0: michael@0: SafeRelease(stm->client); michael@0: SafeRelease(stm->render_client); michael@0: SafeRelease(stm->audio_clock); michael@0: michael@0: if (stm->resampler) { michael@0: speex_resampler_destroy(stm->resampler); michael@0: } michael@0: michael@0: free(stm->leftover_frames_buffer); michael@0: free(stm->resampling_src_buffer); michael@0: free(stm->mix_buffer); michael@0: free(stm); michael@0: CoUninitialize(); michael@0: } michael@0: michael@0: int wasapi_stream_start(cubeb_stream * stm) michael@0: { michael@0: HRESULT hr; michael@0: michael@0: assert(stm); michael@0: michael@0: stm->thread = (HANDLE) _beginthreadex(NULL, 64 * 1024, wasapi_stream_render_loop, stm, STACK_SIZE_PARAM_IS_A_RESERVATION, NULL); michael@0: if (stm->thread == NULL) { michael@0: LOG("could not create WASAPI render thread."); michael@0: return CUBEB_ERROR; michael@0: } michael@0: michael@0: hr = stm->client->Start(); michael@0: if (FAILED(hr)) { michael@0: LOG("could not start the stream."); 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: int wasapi_stream_stop(cubeb_stream * stm) michael@0: { michael@0: assert(stm && stm->shutdown_event); michael@0: michael@0: SetEvent(stm->shutdown_event); michael@0: michael@0: HRESULT hr = stm->client->Stop(); michael@0: if (FAILED(hr)) { michael@0: LOG("could not stop AudioClient"); michael@0: } michael@0: michael@0: if (stm->thread) { michael@0: WaitForSingleObject(stm->thread, INFINITE); michael@0: CloseHandle(stm->thread); michael@0: stm->thread = NULL; 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: int wasapi_stream_get_position(cubeb_stream * stm, uint64_t * position) michael@0: { michael@0: assert(stm && position); michael@0: michael@0: UINT64 pos; michael@0: HRESULT hr; michael@0: michael@0: hr = stm->audio_clock->GetPosition(&pos, NULL); michael@0: if (FAILED(hr)) { michael@0: LOG("Could not get accurate position: %x\n", hr); michael@0: return CUBEB_ERROR; michael@0: } michael@0: michael@0: *position = static_cast(static_cast(pos) / stm->clock_freq * stm->stream_params.rate); michael@0: michael@0: return CUBEB_OK; michael@0: } michael@0: michael@0: int wasapi_stream_get_latency(cubeb_stream * stm, uint32_t * latency) michael@0: { michael@0: assert(stm && latency); michael@0: michael@0: /* The GetStreamLatency method only works if the michael@0: * AudioClient has been initialized. */ michael@0: if (!stm->client) { michael@0: return CUBEB_ERROR; michael@0: } michael@0: michael@0: REFERENCE_TIME latency_hns; michael@0: stm->client->GetStreamLatency(&latency_hns); michael@0: double latency_s = hns_to_s(latency_hns); michael@0: *latency = static_cast(latency_s * stm->stream_params.rate); michael@0: michael@0: return CUBEB_OK; michael@0: } michael@0: michael@0: cubeb_ops const wasapi_ops = { michael@0: /*.init =*/ wasapi_init, michael@0: /*.get_backend_id =*/ wasapi_get_backend_id, michael@0: /*.get_max_channel_count =*/ wasapi_get_max_channel_count, michael@0: /*.get_min_latency =*/ wasapi_get_min_latency, michael@0: /*.get_preferred_sample_rate =*/ wasapi_get_preferred_sample_rate, michael@0: /*.destroy =*/ wasapi_destroy, michael@0: /*.stream_init =*/ wasapi_stream_init, michael@0: /*.stream_destroy =*/ wasapi_stream_destroy, michael@0: /*.stream_start =*/ wasapi_stream_start, michael@0: /*.stream_stop =*/ wasapi_stream_stop, michael@0: /*.stream_get_position =*/ wasapi_stream_get_position, michael@0: /*.stream_get_latency =*/ wasapi_stream_get_latency michael@0: }; michael@0: } // namespace anonymous michael@0: