mirror of
https://github.com/danog/libtgvoip.git
synced 2024-11-30 04:39:03 +01:00
5caaaafa42
I'm now using the entire audio processing module from WebRTC as opposed to individual DSP algorithms pulled from there before. Seems to work better this way.
439 lines
12 KiB
C++
Executable File
439 lines
12 KiB
C++
Executable File
//
|
|
// libtgvoip is free and unencumbered public domain software.
|
|
// For more information, see http://unlicense.org or the UNLICENSE file
|
|
// you should have received with this source code distribution.
|
|
//
|
|
|
|
|
|
#include <assert.h>
|
|
#include "AudioOutputWASAPI.h"
|
|
#include "../../logging.h"
|
|
#include "../../VoIPController.h"
|
|
|
|
#define BUFFER_SIZE 960
|
|
#define CHECK_RES(res, msg) {if(FAILED(res)){LOGE("%s failed: HRESULT=0x%08X", msg, res); failed=true; return;}}
|
|
#define SCHECK_RES(res, msg) {if(FAILED(res)){LOGE("%s failed: HRESULT=0x%08X", msg, res); return;}}
|
|
|
|
template <class T> void SafeRelease(T **ppT)
|
|
{
|
|
if(*ppT)
|
|
{
|
|
(*ppT)->Release();
|
|
*ppT = NULL;
|
|
}
|
|
}
|
|
|
|
#ifdef TGVOIP_WINXP_COMPAT
|
|
|
|
#endif
|
|
|
|
using namespace tgvoip::audio;
|
|
|
|
AudioOutputWASAPI::AudioOutputWASAPI(std::string deviceID){
|
|
isPlaying=false;
|
|
remainingDataLen=0;
|
|
refCount=1;
|
|
HRESULT res;
|
|
res=CoInitializeEx(NULL, COINIT_MULTITHREADED);
|
|
CHECK_RES(res, "CoInitializeEx");
|
|
#ifdef TGVOIP_WINXP_COMPAT
|
|
HANDLE (WINAPI *__CreateEventExA)(LPSECURITY_ATTRIBUTES lpEventAttributes, LPCSTR lpName, DWORD dwFlags, DWORD dwDesiredAccess);
|
|
__CreateEventExA=(HANDLE (WINAPI *)(LPSECURITY_ATTRIBUTES, LPCSTR, DWORD, DWORD))GetProcAddress(GetModuleHandleA("kernel32.dll"), "CreateEventExA");
|
|
#undef CreateEventEx
|
|
#define CreateEventEx __CreateEventExA
|
|
#endif
|
|
shutdownEvent=CreateEventEx(NULL, NULL, 0, EVENT_MODIFY_STATE | SYNCHRONIZE);
|
|
audioSamplesReadyEvent=CreateEventEx(NULL, NULL, 0, EVENT_MODIFY_STATE | SYNCHRONIZE);
|
|
streamSwitchEvent=CreateEventEx(NULL, NULL, 0, EVENT_MODIFY_STATE | SYNCHRONIZE);
|
|
ZeroMemory(&format, sizeof(format));
|
|
format.wFormatTag=WAVE_FORMAT_PCM;
|
|
format.nChannels=1;
|
|
format.nSamplesPerSec=48000;
|
|
format.nBlockAlign=2;
|
|
format.nAvgBytesPerSec=format.nSamplesPerSec*format.nBlockAlign;
|
|
format.wBitsPerSample=16;
|
|
|
|
#ifdef TGVOIP_WINDOWS_DESKTOP
|
|
res=CoCreateInstance(__uuidof(MMDeviceEnumerator), NULL, CLSCTX_INPROC_SERVER, IID_PPV_ARGS(&enumerator));
|
|
CHECK_RES(res, "CoCreateInstance(MMDeviceEnumerator)");
|
|
res=enumerator->RegisterEndpointNotificationCallback(this);
|
|
CHECK_RES(res, "enumerator->RegisterEndpointNotificationCallback");
|
|
audioSessionControl=NULL;
|
|
device=NULL;
|
|
#endif
|
|
|
|
audioClient=NULL;
|
|
renderClient=NULL;
|
|
thread=NULL;
|
|
|
|
SetCurrentDevice(deviceID);
|
|
}
|
|
|
|
AudioOutputWASAPI::~AudioOutputWASAPI(){
|
|
if(audioClient){
|
|
audioClient->Stop();
|
|
}
|
|
|
|
#ifdef TGVOIP_WINDOWS_DESKTOP
|
|
if(audioSessionControl){
|
|
audioSessionControl->UnregisterAudioSessionNotification(this);
|
|
}
|
|
#endif
|
|
|
|
SetEvent(shutdownEvent);
|
|
if(thread){
|
|
WaitForSingleObjectEx(thread, INFINITE, false);
|
|
CloseHandle(thread);
|
|
}
|
|
SafeRelease(&renderClient);
|
|
SafeRelease(&audioClient);
|
|
#ifdef TGVOIP_WINDOWS_DESKTOP
|
|
SafeRelease(&device);
|
|
SafeRelease(&audioSessionControl);
|
|
#endif
|
|
CloseHandle(shutdownEvent);
|
|
CloseHandle(audioSamplesReadyEvent);
|
|
CloseHandle(streamSwitchEvent);
|
|
#ifdef TGVOIP_WINDOWS_DESKTOP
|
|
if(enumerator)
|
|
enumerator->UnregisterEndpointNotificationCallback(this);
|
|
SafeRelease(&enumerator);
|
|
#endif
|
|
}
|
|
|
|
void AudioOutputWASAPI::Start(){
|
|
isPlaying=true;
|
|
if(!thread){
|
|
thread=CreateThread(NULL, 0, AudioOutputWASAPI::StartThread, this, 0, NULL);
|
|
}
|
|
}
|
|
|
|
void AudioOutputWASAPI::Stop(){
|
|
isPlaying=false;
|
|
}
|
|
|
|
bool AudioOutputWASAPI::IsPlaying(){
|
|
return isPlaying;
|
|
}
|
|
|
|
void AudioOutputWASAPI::EnumerateDevices(std::vector<tgvoip::AudioOutputDevice>& devs){
|
|
#ifdef TGVOIP_WINDOWS_DESKTOP
|
|
HRESULT res;
|
|
res=CoInitializeEx(NULL, COINIT_MULTITHREADED);
|
|
SCHECK_RES(res, "CoInitializeEx");
|
|
|
|
IMMDeviceEnumerator *deviceEnumerator = NULL;
|
|
IMMDeviceCollection *deviceCollection = NULL;
|
|
|
|
res=CoCreateInstance(__uuidof(MMDeviceEnumerator), NULL, CLSCTX_INPROC_SERVER, IID_PPV_ARGS(&deviceEnumerator));
|
|
SCHECK_RES(res, "CoCreateInstance(MMDeviceEnumerator)");
|
|
|
|
res=deviceEnumerator->EnumAudioEndpoints(eRender, DEVICE_STATE_ACTIVE, &deviceCollection);
|
|
SCHECK_RES(res, "EnumAudioEndpoints");
|
|
|
|
UINT devCount;
|
|
res=deviceCollection->GetCount(&devCount);
|
|
SCHECK_RES(res, "GetCount");
|
|
|
|
for(UINT i=0;i<devCount;i++){
|
|
IMMDevice* device;
|
|
res=deviceCollection->Item(i, &device);
|
|
SCHECK_RES(res, "GetDeviceItem");
|
|
wchar_t* devID;
|
|
res=device->GetId(&devID);
|
|
SCHECK_RES(res, "get device id");
|
|
|
|
IPropertyStore* propStore;
|
|
res=device->OpenPropertyStore(STGM_READ, &propStore);
|
|
SafeRelease(&device);
|
|
SCHECK_RES(res, "OpenPropertyStore");
|
|
|
|
PROPVARIANT friendlyName;
|
|
PropVariantInit(&friendlyName);
|
|
res=propStore->GetValue(PKEY_Device_FriendlyName, &friendlyName);
|
|
SafeRelease(&propStore);
|
|
|
|
AudioOutputDevice dev;
|
|
|
|
wchar_t actualFriendlyName[128];
|
|
if(friendlyName.vt==VT_LPWSTR){
|
|
wcsncpy(actualFriendlyName, friendlyName.pwszVal, sizeof(actualFriendlyName)/sizeof(wchar_t));
|
|
}else{
|
|
wcscpy(actualFriendlyName, L"Unknown");
|
|
}
|
|
PropVariantClear(&friendlyName);
|
|
|
|
char buf[256];
|
|
WideCharToMultiByte(CP_UTF8, 0, devID, -1, buf, sizeof(buf), NULL, NULL);
|
|
dev.id=buf;
|
|
WideCharToMultiByte(CP_UTF8, 0, actualFriendlyName, -1, buf, sizeof(buf), NULL, NULL);
|
|
dev.displayName=buf;
|
|
devs.push_back(dev);
|
|
|
|
CoTaskMemFree(devID);
|
|
}
|
|
|
|
SafeRelease(&deviceCollection);
|
|
SafeRelease(&deviceEnumerator);
|
|
#endif
|
|
}
|
|
|
|
void AudioOutputWASAPI::SetCurrentDevice(std::string deviceID){
|
|
if(thread){
|
|
streamChangeToDevice=deviceID;
|
|
SetEvent(streamSwitchEvent);
|
|
}else{
|
|
ActuallySetCurrentDevice(deviceID);
|
|
}
|
|
}
|
|
|
|
void AudioOutputWASAPI::ActuallySetCurrentDevice(std::string deviceID){
|
|
currentDevice=deviceID;
|
|
HRESULT res;
|
|
|
|
if(audioClient){
|
|
res=audioClient->Stop();
|
|
CHECK_RES(res, "audioClient->Stop");
|
|
}
|
|
|
|
#ifdef TGVOIP_WINDOWS_DESKTOP
|
|
if(audioSessionControl){
|
|
res=audioSessionControl->UnregisterAudioSessionNotification(this);
|
|
CHECK_RES(res, "audioSessionControl->UnregisterAudioSessionNotification");
|
|
}
|
|
|
|
SafeRelease(&audioSessionControl);
|
|
#endif
|
|
SafeRelease(&renderClient);
|
|
SafeRelease(&audioClient);
|
|
#ifdef TGVOIP_WINDOWS_DESKTOP
|
|
SafeRelease(&device);
|
|
|
|
|
|
IMMDeviceCollection *deviceCollection = NULL;
|
|
|
|
if(deviceID=="default"){
|
|
isDefaultDevice=true;
|
|
res=enumerator->GetDefaultAudioEndpoint(eRender, eCommunications, &device);
|
|
CHECK_RES(res, "GetDefaultAudioEndpoint");
|
|
}else{
|
|
isDefaultDevice=false;
|
|
res=enumerator->EnumAudioEndpoints(eRender, DEVICE_STATE_ACTIVE, &deviceCollection);
|
|
CHECK_RES(res, "EnumAudioEndpoints");
|
|
|
|
UINT devCount;
|
|
res=deviceCollection->GetCount(&devCount);
|
|
CHECK_RES(res, "GetCount");
|
|
|
|
for(UINT i=0;i<devCount;i++){
|
|
IMMDevice* device;
|
|
res=deviceCollection->Item(i, &device);
|
|
CHECK_RES(res, "GetDeviceItem");
|
|
wchar_t* _devID;
|
|
res=device->GetId(&_devID);
|
|
CHECK_RES(res, "get device id");
|
|
|
|
char devID[128];
|
|
WideCharToMultiByte(CP_UTF8, 0, _devID, -1, devID, 128, NULL, NULL);
|
|
|
|
CoTaskMemFree(_devID);
|
|
if(deviceID==devID){
|
|
this->device=device;
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
|
|
if(deviceCollection)
|
|
SafeRelease(&deviceCollection);
|
|
|
|
if(!device){
|
|
LOGE("Didn't find playback device; failing");
|
|
failed=true;
|
|
return;
|
|
}
|
|
|
|
res=device->Activate(__uuidof(IAudioClient), CLSCTX_INPROC_SERVER, NULL, (void**)&audioClient);
|
|
CHECK_RES(res, "device->Activate");
|
|
#else
|
|
Platform::String^ defaultDevID=Windows::Media::Devices::MediaDevice::GetDefaultAudioRenderId(Windows::Media::Devices::AudioDeviceRole::Communications);
|
|
if(defaultDevID==nullptr){
|
|
LOGE("Didn't find playback device; failing");
|
|
failed=true;
|
|
return;
|
|
}
|
|
|
|
HRESULT res1, res2;
|
|
IAudioClient2* audioClient2=WindowsSandboxUtils::ActivateAudioDevice(defaultDevID->Data(), &res1, &res2);
|
|
CHECK_RES(res1, "activate1");
|
|
CHECK_RES(res2, "activate2");
|
|
|
|
AudioClientProperties properties={};
|
|
properties.cbSize=sizeof AudioClientProperties;
|
|
properties.eCategory=AudioCategory_Communications;
|
|
res = audioClient2->SetClientProperties(&properties);
|
|
CHECK_RES(res, "audioClient2->SetClientProperties");
|
|
|
|
audioClient = audioClient2;
|
|
#endif
|
|
|
|
// {2C693079-3F59-49FD-964F-61C005EAA5D3}
|
|
const GUID guid = { 0x2c693079, 0x3f59, 0x49fd, { 0x96, 0x4f, 0x61, 0xc0, 0x5, 0xea, 0xa5, 0xd3 } };
|
|
res = audioClient->Initialize(AUDCLNT_SHAREMODE_SHARED, AUDCLNT_STREAMFLAGS_EVENTCALLBACK | AUDCLNT_STREAMFLAGS_NOPERSIST | AUDCLNT_STREAMFLAGS_AUTOCONVERTPCM | AUDCLNT_STREAMFLAGS_SRC_DEFAULT_QUALITY, 60 * 10000, 0, &format, &guid);
|
|
CHECK_RES(res, "audioClient->Initialize");
|
|
|
|
uint32_t bufSize;
|
|
res = audioClient->GetBufferSize(&bufSize);
|
|
CHECK_RES(res, "audioClient->GetBufferSize");
|
|
|
|
LOGV("buffer size: %u", bufSize);
|
|
estimatedDelay=0;
|
|
REFERENCE_TIME latency, devicePeriod;
|
|
if(SUCCEEDED(audioClient->GetStreamLatency(&latency))){
|
|
if(SUCCEEDED(audioClient->GetDevicePeriod(&devicePeriod, NULL))){
|
|
estimatedDelay=(int32_t)(latency/10000+devicePeriod/10000);
|
|
}
|
|
}
|
|
|
|
res = audioClient->SetEventHandle(audioSamplesReadyEvent);
|
|
CHECK_RES(res, "audioClient->SetEventHandle");
|
|
|
|
res = audioClient->GetService(IID_PPV_ARGS(&renderClient));
|
|
CHECK_RES(res, "audioClient->GetService");
|
|
|
|
BYTE* data;
|
|
res = renderClient->GetBuffer(bufSize, &data);
|
|
CHECK_RES(res, "renderClient->GetBuffer");
|
|
|
|
res = renderClient->ReleaseBuffer(bufSize, AUDCLNT_BUFFERFLAGS_SILENT);
|
|
CHECK_RES(res, "renderClient->ReleaseBuffer");
|
|
|
|
#ifdef TGVOIP_WINDOWS_DESKTOP
|
|
res=audioClient->GetService(IID_PPV_ARGS(&audioSessionControl));
|
|
CHECK_RES(res, "audioClient->GetService(IAudioSessionControl)");
|
|
|
|
res=audioSessionControl->RegisterAudioSessionNotification(this);
|
|
CHECK_RES(res, "audioSessionControl->RegisterAudioSessionNotification");
|
|
#endif
|
|
|
|
audioClient->Start();
|
|
|
|
LOGV("set current output device done");
|
|
}
|
|
|
|
DWORD WINAPI AudioOutputWASAPI::StartThread(void* arg) {
|
|
((AudioOutputWASAPI*)arg)->RunThread();
|
|
return 0;
|
|
}
|
|
|
|
void AudioOutputWASAPI::RunThread() {
|
|
SetThreadPriority(GetCurrentThread(), THREAD_PRIORITY_HIGHEST);
|
|
|
|
HANDLE waitArray[]={shutdownEvent, streamSwitchEvent, audioSamplesReadyEvent};
|
|
HRESULT res=CoInitializeEx(NULL, COINIT_MULTITHREADED);
|
|
CHECK_RES(res, "CoInitializeEx in render thread");
|
|
|
|
uint32_t bufferSize;
|
|
res=audioClient->GetBufferSize(&bufferSize);
|
|
CHECK_RES(res, "audioClient->GetBufferSize");
|
|
uint64_t framesWritten=0;
|
|
|
|
bool running=true;
|
|
//double prevCallback=VoIPController::GetCurrentTime();
|
|
|
|
while(running){
|
|
DWORD waitResult=WaitForMultipleObjectsEx(3, waitArray, false, INFINITE, false);
|
|
if(waitResult==WAIT_OBJECT_0){ // shutdownEvent
|
|
LOGV("render thread shutting down");
|
|
running=false;
|
|
}else if(waitResult==WAIT_OBJECT_0+1){ // streamSwitchEvent
|
|
LOGV("stream switch");
|
|
ActuallySetCurrentDevice(streamChangeToDevice);
|
|
ResetEvent(streamSwitchEvent);
|
|
LOGV("stream switch done");
|
|
}else if(waitResult==WAIT_OBJECT_0+2){ // audioSamplesReadyEvent
|
|
if(!audioClient)
|
|
continue;
|
|
|
|
BYTE* data;
|
|
uint32_t padding;
|
|
uint32_t framesAvailable;
|
|
res=audioClient->GetCurrentPadding(&padding);
|
|
CHECK_RES(res, "audioClient->GetCurrentPadding");
|
|
framesAvailable=bufferSize-padding;
|
|
res=renderClient->GetBuffer(framesAvailable, &data);
|
|
CHECK_RES(res, "renderClient->GetBuffer");
|
|
|
|
//double t=VoIPController::GetCurrentTime();
|
|
//LOGV("framesAvail: %u, time: %f, isPlaying: %d", framesAvailable, t-prevCallback, isPlaying);
|
|
//prevCallback=t;
|
|
|
|
size_t bytesAvailable=framesAvailable*2;
|
|
while(bytesAvailable>remainingDataLen){
|
|
if(isPlaying){
|
|
InvokeCallback(remainingData+remainingDataLen, 960*2);
|
|
}else{
|
|
memset(remainingData+remainingDataLen, 0, 960*2);
|
|
}
|
|
remainingDataLen+=960*2;
|
|
}
|
|
memcpy(data, remainingData, bytesAvailable);
|
|
if(remainingDataLen>bytesAvailable){
|
|
memmove(remainingData, remainingData+bytesAvailable, remainingDataLen-bytesAvailable);
|
|
}
|
|
remainingDataLen-=bytesAvailable;
|
|
|
|
res=renderClient->ReleaseBuffer(framesAvailable, 0);
|
|
CHECK_RES(res, "renderClient->ReleaseBuffer");
|
|
framesWritten+=framesAvailable;
|
|
}
|
|
}
|
|
}
|
|
|
|
#ifdef TGVOIP_WINDOWS_DESKTOP
|
|
HRESULT AudioOutputWASAPI::OnSessionDisconnected(AudioSessionDisconnectReason reason) {
|
|
if(!isDefaultDevice){
|
|
streamChangeToDevice="default";
|
|
SetEvent(streamSwitchEvent);
|
|
}
|
|
return S_OK;
|
|
}
|
|
|
|
HRESULT AudioOutputWASAPI::OnDefaultDeviceChanged(EDataFlow flow, ERole role, LPCWSTR newDevID) {
|
|
if(flow==eRender && role==eCommunications && isDefaultDevice){
|
|
streamChangeToDevice="default";
|
|
SetEvent(streamSwitchEvent);
|
|
}
|
|
return S_OK;
|
|
}
|
|
|
|
ULONG AudioOutputWASAPI::AddRef(){
|
|
return InterlockedIncrement(&refCount);
|
|
}
|
|
|
|
ULONG AudioOutputWASAPI::Release(){
|
|
return InterlockedDecrement(&refCount);
|
|
}
|
|
|
|
HRESULT AudioOutputWASAPI::QueryInterface(REFIID iid, void** obj){
|
|
if(!obj){
|
|
return E_POINTER;
|
|
}
|
|
*obj=NULL;
|
|
|
|
if(iid==IID_IUnknown){
|
|
*obj=static_cast<IUnknown*>(static_cast<IAudioSessionEvents*>(this));
|
|
AddRef();
|
|
}else if(iid==__uuidof(IMMNotificationClient)){
|
|
*obj=static_cast<IMMNotificationClient*>(this);
|
|
AddRef();
|
|
}else if(iid==__uuidof(IAudioSessionEvents)){
|
|
*obj=static_cast<IAudioSessionEvents*>(this);
|
|
AddRef();
|
|
}else{
|
|
return E_NOINTERFACE;
|
|
}
|
|
|
|
return S_OK;
|
|
}
|
|
#endif |