2018-02-10 13:03:27 -07:00
|
|
|
// Copyright 2018 Dolphin Emulator Project
|
|
|
|
// Licensed under GPLv2+
|
|
|
|
// Refer to the license.txt file included.
|
|
|
|
|
|
|
|
#include "AudioCommon/WASAPIStream.h"
|
|
|
|
|
|
|
|
#ifdef _WIN32
|
|
|
|
|
|
|
|
// clang-format off
|
|
|
|
#include <Audioclient.h>
|
|
|
|
#include <comdef.h>
|
|
|
|
#include <mmdeviceapi.h>
|
|
|
|
#include <devpkey.h>
|
|
|
|
#include <functiondiscoverykeys_devpkey.h>
|
2019-08-16 15:53:47 -06:00
|
|
|
#include <wil/resource.h>
|
2018-02-10 13:03:27 -07:00
|
|
|
// clang-format on
|
|
|
|
|
2019-08-16 15:53:47 -06:00
|
|
|
#include <thread>
|
|
|
|
|
|
|
|
#include "Common/Assert.h"
|
2018-02-10 13:03:27 -07:00
|
|
|
#include "Common/Logging/Log.h"
|
|
|
|
#include "Common/StringUtil.h"
|
|
|
|
#include "Common/Thread.h"
|
|
|
|
#include "Core/ConfigManager.h"
|
|
|
|
#include "VideoCommon/OnScreenDisplay.h"
|
|
|
|
|
2019-08-16 15:53:47 -06:00
|
|
|
using Microsoft::WRL::ComPtr;
|
|
|
|
|
2018-02-10 13:03:27 -07:00
|
|
|
WASAPIStream::WASAPIStream()
|
|
|
|
{
|
2019-08-16 15:53:47 -06:00
|
|
|
if (SUCCEEDED(CoInitializeEx(nullptr, COINIT_MULTITHREADED)))
|
|
|
|
m_coinitialize.activate();
|
2018-02-10 13:03:27 -07:00
|
|
|
|
|
|
|
m_format.Format.wFormatTag = WAVE_FORMAT_EXTENSIBLE;
|
|
|
|
m_format.Format.nChannels = 2;
|
|
|
|
m_format.Format.nSamplesPerSec = GetMixer()->GetSampleRate();
|
|
|
|
m_format.Format.nAvgBytesPerSec = m_format.Format.nSamplesPerSec * 4;
|
|
|
|
m_format.Format.nBlockAlign = 4;
|
|
|
|
m_format.Format.wBitsPerSample = 16;
|
|
|
|
m_format.Format.cbSize = sizeof(WAVEFORMATEXTENSIBLE) - sizeof(WAVEFORMATEX);
|
|
|
|
m_format.Samples.wValidBitsPerSample = m_format.Format.wBitsPerSample;
|
|
|
|
m_format.dwChannelMask = SPEAKER_FRONT_LEFT | SPEAKER_FRONT_RIGHT;
|
|
|
|
m_format.SubFormat = KSDATAFORMAT_SUBTYPE_PCM;
|
|
|
|
}
|
|
|
|
|
|
|
|
WASAPIStream::~WASAPIStream()
|
|
|
|
{
|
|
|
|
if (m_running)
|
|
|
|
{
|
|
|
|
m_running = false;
|
|
|
|
if (m_thread.joinable())
|
|
|
|
m_thread.join();
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
bool WASAPIStream::isValid()
|
|
|
|
{
|
|
|
|
return true;
|
|
|
|
}
|
|
|
|
|
2020-10-21 11:32:25 -06:00
|
|
|
static bool HandleWinAPI(std::string_view message, HRESULT result)
|
2018-02-10 13:03:27 -07:00
|
|
|
{
|
2019-08-16 12:36:50 -06:00
|
|
|
if (FAILED(result))
|
2018-02-10 13:03:27 -07:00
|
|
|
{
|
|
|
|
_com_error err(result);
|
2020-10-21 11:32:25 -06:00
|
|
|
std::string error = TStrToUTF8(err.ErrorMessage());
|
2018-02-10 13:03:27 -07:00
|
|
|
|
|
|
|
switch (result)
|
|
|
|
{
|
|
|
|
case AUDCLNT_E_DEVICE_IN_USE:
|
|
|
|
error = "Audio endpoint already in use!";
|
|
|
|
break;
|
|
|
|
}
|
|
|
|
|
2020-10-21 11:32:25 -06:00
|
|
|
ERROR_LOG_FMT(AUDIO, "WASAPI: {}: {}", message, error);
|
2018-02-10 13:03:27 -07:00
|
|
|
}
|
|
|
|
|
2019-08-16 12:36:50 -06:00
|
|
|
return SUCCEEDED(result);
|
2018-02-10 13:03:27 -07:00
|
|
|
}
|
|
|
|
|
|
|
|
std::vector<std::string> WASAPIStream::GetAvailableDevices()
|
|
|
|
{
|
2019-08-16 15:53:47 -06:00
|
|
|
HRESULT result = CoInitializeEx(nullptr, COINIT_MULTITHREADED);
|
|
|
|
// RPC_E_CHANGED_MODE means that thread has COM already initialized with a different threading
|
|
|
|
// model. We don't necessarily need multithreaded model here, so don't treat this as an error
|
|
|
|
if (result != RPC_E_CHANGED_MODE && !HandleWinAPI("Failed to call CoInitialize", result))
|
|
|
|
return {};
|
2018-02-10 13:03:27 -07:00
|
|
|
|
2019-08-16 15:53:47 -06:00
|
|
|
wil::unique_couninitialize_call cleanup;
|
|
|
|
if (FAILED(result))
|
|
|
|
cleanup.release(); // CoUninitialize must be matched with each successful CoInitialize call, so
|
|
|
|
// don't call it if initialize fails
|
|
|
|
|
|
|
|
ComPtr<IMMDeviceEnumerator> enumerator;
|
2018-02-10 13:03:27 -07:00
|
|
|
|
2019-08-16 15:53:47 -06:00
|
|
|
result = CoCreateInstance(__uuidof(MMDeviceEnumerator), nullptr, CLSCTX_INPROC_SERVER,
|
|
|
|
IID_PPV_ARGS(enumerator.GetAddressOf()));
|
2018-02-10 13:03:27 -07:00
|
|
|
|
|
|
|
if (!HandleWinAPI("Failed to create MMDeviceEnumerator", result))
|
|
|
|
return {};
|
|
|
|
|
2019-08-16 15:53:47 -06:00
|
|
|
ComPtr<IMMDeviceCollection> devices;
|
|
|
|
result = enumerator->EnumAudioEndpoints(eRender, DEVICE_STATE_ACTIVE, devices.GetAddressOf());
|
2018-02-10 13:03:27 -07:00
|
|
|
|
|
|
|
if (!HandleWinAPI("Failed to get available devices", result))
|
|
|
|
return {};
|
|
|
|
|
|
|
|
UINT count;
|
|
|
|
devices->GetCount(&count);
|
|
|
|
|
2019-08-16 15:53:47 -06:00
|
|
|
std::vector<std::string> device_names;
|
|
|
|
device_names.reserve(count);
|
|
|
|
|
2018-02-10 13:03:27 -07:00
|
|
|
for (u32 i = 0; i < count; i++)
|
|
|
|
{
|
2019-08-16 15:53:47 -06:00
|
|
|
ComPtr<IMMDevice> device;
|
|
|
|
devices->Item(i, device.GetAddressOf());
|
2018-02-10 13:03:27 -07:00
|
|
|
if (!HandleWinAPI("Failed to get device " + std::to_string(i), result))
|
|
|
|
continue;
|
|
|
|
|
2019-08-16 15:53:47 -06:00
|
|
|
ComPtr<IPropertyStore> device_properties;
|
2018-02-10 13:03:27 -07:00
|
|
|
|
2019-08-16 15:53:47 -06:00
|
|
|
result = device->OpenPropertyStore(STGM_READ, device_properties.GetAddressOf());
|
2018-02-10 13:03:27 -07:00
|
|
|
|
|
|
|
if (!HandleWinAPI("Failed to initialize IPropertyStore", result))
|
|
|
|
continue;
|
|
|
|
|
2019-08-16 15:53:47 -06:00
|
|
|
wil::unique_prop_variant device_name;
|
|
|
|
device_properties->GetValue(PKEY_Device_FriendlyName, device_name.addressof());
|
2018-02-10 13:03:27 -07:00
|
|
|
|
|
|
|
device_names.push_back(TStrToUTF8(device_name.pwszVal));
|
|
|
|
}
|
|
|
|
|
|
|
|
return device_names;
|
|
|
|
}
|
|
|
|
|
2019-08-16 15:53:47 -06:00
|
|
|
ComPtr<IMMDevice> WASAPIStream::GetDeviceByName(std::string name)
|
2018-02-10 13:03:27 -07:00
|
|
|
{
|
2019-08-16 15:53:47 -06:00
|
|
|
HRESULT result = CoInitializeEx(nullptr, COINIT_MULTITHREADED);
|
|
|
|
// RPC_E_CHANGED_MODE means that thread has COM already initialized with a different threading
|
|
|
|
// model. We don't necessarily need multithreaded model here, so don't treat this as an error
|
|
|
|
if (result != RPC_E_CHANGED_MODE && !HandleWinAPI("Failed to call CoInitialize", result))
|
|
|
|
return nullptr;
|
|
|
|
|
|
|
|
wil::unique_couninitialize_call cleanup;
|
|
|
|
if (FAILED(result))
|
|
|
|
cleanup.release(); // CoUninitialize must be matched with each successful CoInitialize call, so
|
|
|
|
// don't call it if initialize fails
|
2018-02-10 13:03:27 -07:00
|
|
|
|
2019-08-16 15:53:47 -06:00
|
|
|
ComPtr<IMMDeviceEnumerator> enumerator;
|
2018-02-10 13:03:27 -07:00
|
|
|
|
2019-08-16 15:53:47 -06:00
|
|
|
result = CoCreateInstance(__uuidof(MMDeviceEnumerator), nullptr, CLSCTX_INPROC_SERVER,
|
|
|
|
IID_PPV_ARGS(enumerator.GetAddressOf()));
|
2018-02-10 13:03:27 -07:00
|
|
|
|
|
|
|
if (!HandleWinAPI("Failed to create MMDeviceEnumerator", result))
|
2019-08-15 09:49:57 -06:00
|
|
|
return nullptr;
|
2018-02-10 13:03:27 -07:00
|
|
|
|
2019-08-16 15:53:47 -06:00
|
|
|
ComPtr<IMMDeviceCollection> devices;
|
|
|
|
result = enumerator->EnumAudioEndpoints(eRender, DEVICE_STATE_ACTIVE, devices.GetAddressOf());
|
2018-02-10 13:03:27 -07:00
|
|
|
|
|
|
|
if (!HandleWinAPI("Failed to get available devices", result))
|
2019-08-16 15:53:47 -06:00
|
|
|
return nullptr;
|
2018-02-10 13:03:27 -07:00
|
|
|
|
|
|
|
UINT count;
|
|
|
|
devices->GetCount(&count);
|
|
|
|
|
|
|
|
for (u32 i = 0; i < count; i++)
|
|
|
|
{
|
2019-08-16 15:53:47 -06:00
|
|
|
ComPtr<IMMDevice> device;
|
|
|
|
devices->Item(i, device.GetAddressOf());
|
2018-02-10 13:03:27 -07:00
|
|
|
if (!HandleWinAPI("Failed to get device " + std::to_string(i), result))
|
|
|
|
continue;
|
|
|
|
|
2019-08-16 15:53:47 -06:00
|
|
|
ComPtr<IPropertyStore> device_properties;
|
2018-02-10 13:03:27 -07:00
|
|
|
|
2019-08-16 15:53:47 -06:00
|
|
|
result = device->OpenPropertyStore(STGM_READ, device_properties.GetAddressOf());
|
2018-02-10 13:03:27 -07:00
|
|
|
|
|
|
|
if (!HandleWinAPI("Failed to initialize IPropertyStore", result))
|
|
|
|
continue;
|
|
|
|
|
2019-08-16 15:53:47 -06:00
|
|
|
wil::unique_prop_variant device_name;
|
|
|
|
device_properties->GetValue(PKEY_Device_FriendlyName, device_name.addressof());
|
2018-02-10 13:03:27 -07:00
|
|
|
|
|
|
|
if (TStrToUTF8(device_name.pwszVal) == name)
|
|
|
|
return device;
|
|
|
|
}
|
|
|
|
|
|
|
|
return nullptr;
|
|
|
|
}
|
|
|
|
|
|
|
|
bool WASAPIStream::Init()
|
|
|
|
{
|
2019-08-16 15:53:47 -06:00
|
|
|
ASSERT(m_enumerator == nullptr);
|
|
|
|
HRESULT result = CoCreateInstance(__uuidof(MMDeviceEnumerator), nullptr, CLSCTX_INPROC_SERVER,
|
|
|
|
IID_PPV_ARGS(m_enumerator.GetAddressOf()));
|
2018-02-10 13:03:27 -07:00
|
|
|
|
|
|
|
if (!HandleWinAPI("Failed to create MMDeviceEnumerator", result))
|
|
|
|
return false;
|
|
|
|
|
|
|
|
return true;
|
|
|
|
}
|
|
|
|
|
|
|
|
bool WASAPIStream::SetRunning(bool running)
|
|
|
|
{
|
|
|
|
if (running)
|
|
|
|
{
|
2019-08-16 15:53:47 -06:00
|
|
|
ComPtr<IMMDevice> device;
|
2018-02-10 13:03:27 -07:00
|
|
|
|
|
|
|
HRESULT result;
|
|
|
|
|
|
|
|
if (SConfig::GetInstance().sWASAPIDevice == "default")
|
|
|
|
{
|
2019-08-16 15:53:47 -06:00
|
|
|
result = m_enumerator->GetDefaultAudioEndpoint(eRender, eConsole, device.GetAddressOf());
|
2018-02-10 13:03:27 -07:00
|
|
|
}
|
|
|
|
else
|
|
|
|
{
|
|
|
|
result = S_OK;
|
|
|
|
device = GetDeviceByName(SConfig::GetInstance().sWASAPIDevice);
|
|
|
|
|
|
|
|
if (!device)
|
|
|
|
{
|
2020-10-21 11:32:25 -06:00
|
|
|
ERROR_LOG_FMT(AUDIO, "Can't find device '{}', falling back to default",
|
|
|
|
SConfig::GetInstance().sWASAPIDevice);
|
2019-08-16 15:53:47 -06:00
|
|
|
result = m_enumerator->GetDefaultAudioEndpoint(eRender, eConsole, device.GetAddressOf());
|
2018-02-10 13:03:27 -07:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
if (!HandleWinAPI("Failed to obtain default endpoint", result))
|
|
|
|
return false;
|
|
|
|
|
|
|
|
// Show a friendly name in the log
|
2019-08-16 15:53:47 -06:00
|
|
|
ComPtr<IPropertyStore> device_properties;
|
2018-02-10 13:03:27 -07:00
|
|
|
|
2019-08-16 15:53:47 -06:00
|
|
|
result = device->OpenPropertyStore(STGM_READ, device_properties.GetAddressOf());
|
2018-02-10 13:03:27 -07:00
|
|
|
|
|
|
|
if (!HandleWinAPI("Failed to initialize IPropertyStore", result))
|
|
|
|
return false;
|
|
|
|
|
2019-08-16 15:53:47 -06:00
|
|
|
wil::unique_prop_variant device_name;
|
|
|
|
device_properties->GetValue(PKEY_Device_FriendlyName, device_name.addressof());
|
2018-02-10 13:03:27 -07:00
|
|
|
|
2020-10-21 11:32:25 -06:00
|
|
|
INFO_LOG_FMT(AUDIO, "Using audio endpoint '{}'", TStrToUTF8(device_name.pwszVal));
|
2018-02-10 13:03:27 -07:00
|
|
|
|
2019-08-16 15:53:47 -06:00
|
|
|
ComPtr<IAudioClient> audio_client;
|
2018-02-10 13:03:27 -07:00
|
|
|
|
|
|
|
// Get IAudioDevice
|
|
|
|
result = device->Activate(__uuidof(IAudioClient), CLSCTX_INPROC_SERVER, nullptr,
|
2019-08-16 15:53:47 -06:00
|
|
|
reinterpret_cast<LPVOID*>(audio_client.GetAddressOf()));
|
2018-02-10 13:03:27 -07:00
|
|
|
|
|
|
|
if (!HandleWinAPI("Failed to activate IAudioClient", result))
|
|
|
|
return false;
|
|
|
|
|
|
|
|
REFERENCE_TIME device_period = 0;
|
|
|
|
|
2019-08-16 15:53:47 -06:00
|
|
|
result = audio_client->GetDevicePeriod(nullptr, &device_period);
|
2018-02-10 13:03:27 -07:00
|
|
|
|
|
|
|
device_period += SConfig::GetInstance().iLatency * (10000 / m_format.Format.nChannels);
|
2020-10-21 11:32:25 -06:00
|
|
|
INFO_LOG_FMT(AUDIO, "Audio period set to {}", device_period);
|
2018-02-10 13:03:27 -07:00
|
|
|
|
|
|
|
if (!HandleWinAPI("Failed to obtain device period", result))
|
|
|
|
return false;
|
|
|
|
|
2019-08-16 15:53:47 -06:00
|
|
|
result = audio_client->Initialize(
|
2018-02-10 13:03:27 -07:00
|
|
|
AUDCLNT_SHAREMODE_EXCLUSIVE,
|
|
|
|
AUDCLNT_STREAMFLAGS_EVENTCALLBACK | AUDCLNT_STREAMFLAGS_NOPERSIST, device_period,
|
|
|
|
device_period, reinterpret_cast<WAVEFORMATEX*>(&m_format), nullptr);
|
|
|
|
|
|
|
|
if (result == AUDCLNT_E_UNSUPPORTED_FORMAT)
|
|
|
|
{
|
|
|
|
OSD::AddMessage("Your current audio device doesn't support 16-bit 48000 hz PCM audio. WASAPI "
|
|
|
|
"exclusive mode won't work.",
|
|
|
|
6000U);
|
|
|
|
return false;
|
|
|
|
}
|
|
|
|
|
|
|
|
if (result == AUDCLNT_E_BUFFER_SIZE_NOT_ALIGNED)
|
|
|
|
{
|
2019-08-16 15:53:47 -06:00
|
|
|
result = audio_client->GetBufferSize(&m_frames_in_buffer);
|
2018-02-10 13:03:27 -07:00
|
|
|
|
|
|
|
if (!HandleWinAPI("Failed to get aligned buffer size", result))
|
|
|
|
return false;
|
|
|
|
|
|
|
|
// Get IAudioDevice
|
|
|
|
result = device->Activate(__uuidof(IAudioClient), CLSCTX_INPROC_SERVER, nullptr,
|
2019-08-16 15:53:47 -06:00
|
|
|
reinterpret_cast<LPVOID*>(audio_client.ReleaseAndGetAddressOf()));
|
2018-02-10 13:03:27 -07:00
|
|
|
|
|
|
|
if (!HandleWinAPI("Failed to reactivate IAudioClient", result))
|
|
|
|
return false;
|
|
|
|
|
|
|
|
device_period =
|
|
|
|
static_cast<REFERENCE_TIME>(
|
|
|
|
10000.0 * 1000 * m_frames_in_buffer / m_format.Format.nSamplesPerSec + 0.5) +
|
|
|
|
SConfig::GetInstance().iLatency * 10000;
|
|
|
|
|
2019-08-16 15:53:47 -06:00
|
|
|
result = audio_client->Initialize(
|
2018-02-10 13:03:27 -07:00
|
|
|
AUDCLNT_SHAREMODE_EXCLUSIVE,
|
|
|
|
AUDCLNT_STREAMFLAGS_EVENTCALLBACK | AUDCLNT_STREAMFLAGS_NOPERSIST, device_period,
|
|
|
|
device_period, reinterpret_cast<WAVEFORMATEX*>(&m_format), nullptr);
|
|
|
|
}
|
|
|
|
|
|
|
|
if (!HandleWinAPI("Failed to initialize IAudioClient", result))
|
|
|
|
return false;
|
|
|
|
|
2019-08-16 15:53:47 -06:00
|
|
|
result = audio_client->GetBufferSize(&m_frames_in_buffer);
|
2018-02-10 13:03:27 -07:00
|
|
|
|
|
|
|
if (!HandleWinAPI("Failed to get buffer size from IAudioClient", result))
|
|
|
|
return false;
|
|
|
|
|
2019-08-16 15:53:47 -06:00
|
|
|
ComPtr<IAudioRenderClient> audio_renderer;
|
|
|
|
|
|
|
|
result = audio_client->GetService(IID_PPV_ARGS(audio_renderer.GetAddressOf()));
|
2018-02-10 13:03:27 -07:00
|
|
|
|
|
|
|
if (!HandleWinAPI("Failed to get IAudioRenderClient from IAudioClient", result))
|
|
|
|
return false;
|
|
|
|
|
2019-08-16 15:53:47 -06:00
|
|
|
wil::unique_event_nothrow need_data_event;
|
|
|
|
need_data_event.create();
|
|
|
|
|
|
|
|
audio_client->SetEventHandle(need_data_event.get());
|
2018-02-10 13:03:27 -07:00
|
|
|
|
2019-08-16 15:53:47 -06:00
|
|
|
result = audio_client->Start();
|
2018-02-10 13:03:27 -07:00
|
|
|
|
|
|
|
if (!HandleWinAPI("Failed to get IAudioRenderClient from IAudioClient", result))
|
|
|
|
return false;
|
|
|
|
|
2020-10-21 11:32:25 -06:00
|
|
|
INFO_LOG_FMT(AUDIO, "WASAPI: Successfully initialized!");
|
2018-02-10 13:03:27 -07:00
|
|
|
|
2019-08-16 15:53:47 -06:00
|
|
|
// "Commit" audio client and audio renderer now
|
|
|
|
m_audio_client = std::move(audio_client);
|
|
|
|
m_audio_renderer = std::move(audio_renderer);
|
|
|
|
m_need_data_event = std::move(need_data_event);
|
|
|
|
|
2018-02-10 13:03:27 -07:00
|
|
|
m_running = true;
|
|
|
|
m_thread = std::thread([this] { SoundLoop(); });
|
|
|
|
m_thread.detach();
|
|
|
|
}
|
|
|
|
else
|
|
|
|
{
|
|
|
|
m_running = false;
|
|
|
|
|
|
|
|
if (m_thread.joinable())
|
|
|
|
m_thread.join();
|
|
|
|
|
|
|
|
while (!m_stopped)
|
|
|
|
{
|
|
|
|
}
|
|
|
|
|
2019-08-16 15:53:47 -06:00
|
|
|
m_need_data_event.reset();
|
|
|
|
m_audio_renderer.Reset();
|
|
|
|
m_audio_client.Reset();
|
2018-02-10 13:03:27 -07:00
|
|
|
}
|
|
|
|
|
|
|
|
return true;
|
|
|
|
}
|
|
|
|
|
|
|
|
void WASAPIStream::SoundLoop()
|
|
|
|
{
|
|
|
|
Common::SetCurrentThreadName("WASAPI Handler");
|
|
|
|
BYTE* data;
|
|
|
|
|
|
|
|
if (m_audio_renderer)
|
|
|
|
{
|
|
|
|
m_audio_renderer->GetBuffer(m_frames_in_buffer, &data);
|
|
|
|
m_audio_renderer->ReleaseBuffer(m_frames_in_buffer, AUDCLNT_BUFFERFLAGS_SILENT);
|
|
|
|
}
|
|
|
|
|
|
|
|
m_stopped = false;
|
|
|
|
|
|
|
|
while (m_running)
|
|
|
|
{
|
|
|
|
if (!m_audio_renderer)
|
|
|
|
continue;
|
|
|
|
|
2019-08-16 15:53:47 -06:00
|
|
|
WaitForSingleObject(m_need_data_event.get(), 1000);
|
2018-02-10 13:03:27 -07:00
|
|
|
|
|
|
|
m_audio_renderer->GetBuffer(m_frames_in_buffer, &data);
|
|
|
|
GetMixer()->Mix(reinterpret_cast<s16*>(data), m_frames_in_buffer);
|
|
|
|
|
|
|
|
float volume = SConfig::GetInstance().m_IsMuted ? 0 : SConfig::GetInstance().m_Volume / 100.;
|
|
|
|
|
|
|
|
for (u32 i = 0; i < m_frames_in_buffer * 2; i++)
|
|
|
|
reinterpret_cast<s16*>(data)[i] = static_cast<s16>(reinterpret_cast<s16*>(data)[i] * volume);
|
|
|
|
|
|
|
|
m_audio_renderer->ReleaseBuffer(m_frames_in_buffer, 0);
|
|
|
|
}
|
|
|
|
|
|
|
|
m_stopped = true;
|
|
|
|
}
|
|
|
|
|
|
|
|
#endif // _WIN32
|