/*************************************************************************** * Copyright (C) 2016-2019 by Ilya Kotov * * forkotov02@ya.ru * * * * This program is free software; you can redistribute it and/or modify * * it under the terms of the GNU General Public License as published by * * the Free Software Foundation; either version 2 of the License, or * * (at your option) any later version. * * * * This program is distributed in the hope that it will be useful, * * but WITHOUT ANY WARRANTY; without even the implied warranty of * * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * * GNU General Public License for more details. * * * * You should have received a copy of the GNU General Public License * * along with this program; if not, write to the * * Free Software Foundation, Inc., * * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. * ***************************************************************************/ #include #include #include #include #include #include #include #include #include #include #include #include #include #include "outputwasapi.h" #define WASAPI_BUFSIZE 10000000LL //1s #ifndef AUDCLNT_STREAMFLAGS_AUTOCONVERTPCM #define AUDCLNT_STREAMFLAGS_AUTOCONVERTPCM 0x80000000 #endif OutputWASAPI *OutputWASAPI::instance = nullptr; VolumeWASAPI *OutputWASAPI::volumeControl = nullptr; OutputWASAPI::DWASAPIChannels OutputWASAPI::m_wasapi_pos[10] = { {Qmmp::CHAN_FRONT_LEFT, SPEAKER_FRONT_LEFT}, {Qmmp::CHAN_FRONT_RIGHT, SPEAKER_FRONT_RIGHT}, {Qmmp::CHAN_FRONT_CENTER, SPEAKER_FRONT_CENTER}, {Qmmp::CHAN_LFE, SPEAKER_LOW_FREQUENCY}, {Qmmp::CHAN_REAR_LEFT, SPEAKER_BACK_LEFT}, {Qmmp::CHAN_REAR_RIGHT, SPEAKER_BACK_RIGHT}, {Qmmp::CHAN_REAR_CENTER, SPEAKER_BACK_CENTER}, {Qmmp::CHAN_SIDE_LEFT, SPEAKER_SIDE_LEFT}, {Qmmp::CHAN_SIDE_RIGHT, SPEAKER_BACK_RIGHT}, {Qmmp::CHAN_NULL, 0} }; OutputWASAPI::OutputWASAPI() : Output() { m_pEnumerator = nullptr; m_pDevice = nullptr; m_pAudioClient = nullptr; m_pRenderClient = nullptr; m_pSimpleAudioVolume = nullptr; instance = this; QSettings settings(Qmmp::configFile(), QSettings::IniFormat); m_id = settings.value("WASAPI/device", "default").toString(); m_exclusive = settings.value("WASAPI/exclusive_mode", false).toBool(); } OutputWASAPI::~OutputWASAPI() { instance = nullptr; uninitialize(); } bool OutputWASAPI::initialize(quint32 freq, ChannelMap map, Qmmp::AudioFormat format) { HRESULT result = CoCreateInstance(CLSID_MMDeviceEnumerator, NULL, CLSCTX_ALL, IID_IMMDeviceEnumerator, (void**)&m_pEnumerator); if(result != S_OK) { qWarning("OutputWASAPI: CoCreateInstance failed, error code = 0x%lx", result); m_pEnumerator = nullptr; return false; } if(m_id != "default" && !m_id.isEmpty()) { WCHAR id[m_id.length() + 1]; m_id.toWCharArray(id); id[m_id.length()] = 0; if((result = m_pEnumerator->GetDevice(id, &m_pDevice)) != S_OK) { qWarning("OutputWASAPI: IMMDeviceEnumerator::GetDevice failed, error code = 0x%lx", result); m_pDevice = nullptr; } else qDebug("OutputWASAPI: using device id: %s", qPrintable(m_id)); } if(!m_pDevice) { if((result = m_pEnumerator->GetDefaultAudioEndpoint(eRender, eConsole, &m_pDevice)) != S_OK) { qWarning("OutputWASAPI: IMMDeviceEnumerator::GetDefaultAudioEndpoint failed, error code = 0x%lx", result); m_pDevice = nullptr; return false; } } if((result = m_pDevice->Activate(IID_IAudioClient, CLSCTX_ALL, NULL, (void**)&m_pAudioClient)) != S_OK) { qWarning("OutputWASAPI: IMMDevice::Activate failed, error code = 0x%lx", result); m_pAudioClient = nullptr; return false; } WAVEFORMATEXTENSIBLE wfex; wfex.Format.wFormatTag = WAVE_FORMAT_EXTENSIBLE; wfex.Format.nChannels = map.count(); wfex.Format.nSamplesPerSec = freq; wfex.Format.cbSize = sizeof(WAVEFORMATEXTENSIBLE); if(format == Qmmp::PCM_S16LE) { wfex.Format.wBitsPerSample = 16; wfex.Samples.wValidBitsPerSample = 16; } else if(format == Qmmp::PCM_S24LE) { wfex.Format.wBitsPerSample = 32; wfex.Samples.wValidBitsPerSample = 24; } else if(format == Qmmp::PCM_S32LE) { wfex.Format.wBitsPerSample = 32; wfex.Samples.wValidBitsPerSample = 32; } else { format = Qmmp::PCM_S16LE; wfex.Format.wBitsPerSample = 16; wfex.Samples.wValidBitsPerSample = 16; } wfex.Format.nBlockAlign = (wfex.Format.wBitsPerSample / 8) * wfex.Format.nChannels; wfex.Format.nAvgBytesPerSec = wfex.Format.nSamplesPerSec * wfex.Format.nBlockAlign; //generate channel order ChannelMap out_map; int i = 0; DWORD mask = 0; while(m_wasapi_pos[i].pos != Qmmp::CHAN_NULL) { if(map.contains(m_wasapi_pos[i].pos)) { mask |= m_wasapi_pos[i].chan_mask; out_map << m_wasapi_pos[i].pos; } i++; } wfex.dwChannelMask = mask; wfex.SubFormat = KSDATAFORMAT_SUBTYPE_PCM; AUDCLNT_SHAREMODE mode = m_exclusive ? AUDCLNT_SHAREMODE_EXCLUSIVE : AUDCLNT_SHAREMODE_SHARED; DWORD streamFlags = 0; //enable channel matrixer and a sample rate converter if format is unsupported if(m_pAudioClient->IsFormatSupported(mode, (WAVEFORMATEX *)&wfex, nullptr) != S_OK) { streamFlags |= AUDCLNT_STREAMFLAGS_AUTOCONVERTPCM; qDebug("OutputWASAPI: format is not supported, using converter"); } if((result = m_pAudioClient->Initialize(mode, streamFlags, WASAPI_BUFSIZE, 0, (WAVEFORMATEX *)&wfex, NULL)) != S_OK) { qWarning("OutputWASAPI: IAudioClient::Initialize failed, error code = 0x%lx", result); return false; } if((result = m_pAudioClient->GetBufferSize(&m_bufferFrames)) != S_OK) { qWarning("OutputWASAPI: IAudioClient::GetBufferSize failed, error code = 0x%lx", result); return false; } if((result = m_pAudioClient->GetService(IID_IAudioRenderClient, (void**)&m_pRenderClient)) != S_OK) { qWarning("OutputWASAPI: IAudioClient::GetService failed, error code = 0x%lx", result); return false; } if((result = m_pAudioClient->GetService(IID_ISimpleAudioVolume, (void**)&m_pSimpleAudioVolume)) != S_OK) { qWarning("OutputWASAPI: IAudioClient::GetService failed, error code = 0x%lx", result); return false; } if((result = m_pAudioClient->Start()) != S_OK) { qWarning("OutputWASAPI: IAudioClient::Start failed, error code = 0x%lx", result); return false; } configure(freq, out_map, format); m_frameSize = sampleSize() * channels(); if(volumeControl) volumeControl->restore(); return true; } qint64 OutputWASAPI::latency() { UINT32 frames = 0; m_pAudioClient->GetCurrentPadding(&frames); return frames * 1000 / sampleRate(); } qint64 OutputWASAPI::writeAudio(unsigned char *data, qint64 len) { UINT32 frames = 0; BYTE *pData = nullptr; DWORD flags = 0; DWORD result = 0; m_pAudioClient->GetCurrentPadding(&frames); UINT32 framesAvailable = m_bufferFrames - frames; UINT32 framesToWrite = qMin(framesAvailable, (UINT32)len / m_frameSize); //wait until buffer is not full if(framesToWrite == 0) { usleep(len * 1000000L / sampleRate() / m_frameSize / 2); m_pAudioClient->GetCurrentPadding(&frames); framesAvailable = m_bufferFrames - frames; framesToWrite = qMin(framesAvailable, (UINT32)len / m_frameSize); } if(format() == Qmmp::PCM_S24LE) { for(DWORD i = 0; i < framesToWrite * channels(); ++i) { ((quint32*) data)[i] <<= 8; } } if((result = m_pRenderClient->GetBuffer(framesToWrite, &pData)) != S_OK) { qWarning("OutputWASAPI: IAudioClient::GetBuffer failed, error code = 0x%lx", result); return -1; } memcpy(pData, data, framesToWrite * m_frameSize); m_pRenderClient->ReleaseBuffer(framesToWrite, flags); return framesToWrite * m_frameSize; } void OutputWASAPI::drain() { UINT32 frames = 0; while((m_pAudioClient->GetCurrentPadding(&frames) == S_OK) && (frames > 0)) usleep(50); } void OutputWASAPI::suspend() { m_pAudioClient->Stop(); } void OutputWASAPI::resume() { m_pAudioClient->Start(); } void OutputWASAPI::reset() { m_pAudioClient->Stop(); m_pAudioClient->Reset(); m_pAudioClient->Start(); } ISimpleAudioVolume *OutputWASAPI::simpleAudioVolume() { return m_pSimpleAudioVolume; } void OutputWASAPI::uninitialize() { if(m_pAudioClient) { m_pAudioClient->Stop(); m_pAudioClient->Release(); m_pAudioClient = nullptr; } if(m_pEnumerator) { m_pEnumerator->Release(); m_pEnumerator = nullptr; } if(m_pDevice) { m_pDevice->Release(); m_pDevice = nullptr; } if(m_pRenderClient) { m_pRenderClient->Release(); m_pRenderClient = nullptr; } if(m_pSimpleAudioVolume) { m_pSimpleAudioVolume->Release(); m_pSimpleAudioVolume = nullptr; } } /***** MIXER *****/ VolumeWASAPI::VolumeWASAPI() { OutputWASAPI::volumeControl = this; QSettings settings(Qmmp::configFile(), QSettings::IniFormat); m_volume.left = settings.value("WASAPI/left_volume", 100).toInt(); m_volume.right = settings.value("WASAPI/right_volume", 100).toInt(); } VolumeWASAPI::~VolumeWASAPI() { m_volume = volume(); QSettings settings(Qmmp::configFile(), QSettings::IniFormat); settings.setValue("WASAPI/left_volume", m_volume.left); settings.setValue("WASAPI/right_volume", m_volume.right); OutputWASAPI::volumeControl = nullptr; } void VolumeWASAPI::setVolume(const VolumeSettings &vol) { if(OutputWASAPI::instance && OutputWASAPI::instance->simpleAudioVolume()) { OutputWASAPI::instance->simpleAudioVolume()->SetMasterVolume(float(qMax(vol.left, vol.right)) / 100.0f, nullptr); } m_volume = vol; } VolumeSettings VolumeWASAPI::volume() const { VolumeSettings vol; if(OutputWASAPI::instance && OutputWASAPI::instance->simpleAudioVolume()) { float level = 0; OutputWASAPI::instance->simpleAudioVolume()->GetMasterVolume(&level); vol.left = ceilf(level * 100.0f); vol.right = vol.left; return vol; } return m_volume; } void VolumeWASAPI::restore() { setVolume(m_volume); }