// Copyright 2007-2021 The Mumble Developers. All rights reserved. // Use of this source code is governed by a BSD-style license // that can be found in the LICENSE file at the root of the // Mumble source tree or at . #include "ASIOInput.h" #include "MainWindow.h" #include "Utils.h" #include #include #include "Global.h" // From os_win.cpp. extern HWND mumble_mw_hwnd; const QString ASIOConfig::name = QLatin1String("ASIOConfig"); class ASIOAudioInputRegistrar : public AudioInputRegistrar { public: ASIOAudioInputRegistrar(); virtual AudioInput *create(); virtual const QList< audioDevice > getDeviceChoices(); virtual void setDeviceChoice(const QVariant &, Settings &); virtual bool canEcho(EchoCancelOptionID echoCancelID, const QString &outputSystem) const; virtual bool isMicrophoneAccessDeniedByOS() { return false; }; }; ASIOAudioInputRegistrar::ASIOAudioInputRegistrar() : AudioInputRegistrar(QLatin1String("ASIO")) { echoOptions.push_back(EchoCancelOptionID::SPEEX_MIXED); echoOptions.push_back(EchoCancelOptionID::SPEEX_MULTICHANNEL); } AudioInput *ASIOAudioInputRegistrar::create() { return new ASIOInput(); } const QList< audioDevice > ASIOAudioInputRegistrar::getDeviceChoices() { QList< audioDevice > qlReturn; return qlReturn; } bool ASIOAudioInputRegistrar::canEcho(EchoCancelOptionID echoOption, const QString &) const { return (echoOption == EchoCancelOptionID::SPEEX_MIXED || echoOption == EchoCancelOptionID::SPEEX_MULTICHANNEL); } void ASIOAudioInputRegistrar::setDeviceChoice(const QVariant &, Settings &) { } static ConfigWidget *ASIOConfigDialogNew(Settings &st) { return new ASIOConfig(st); } class ASIOInit : public DeferInit { ASIOAudioInputRegistrar *airASIO; ConfigRegistrar *crASIO; public: ASIOInit() : airASIO(nullptr), crASIO(nullptr) {} void initialize(); void destroy(); }; void ASIOInit::initialize() { HKEY hkDevs; HKEY hk; FILETIME ft; airASIO = nullptr; crASIO = nullptr; bool bFound = false; if (!Global::get().s.bASIOEnable) { qWarning("ASIOInput: ASIO forcefully disabled via 'asio/enable' config option."); return; } // List of devices known to misbehave or be totally useless QStringList blacklist; blacklist << QLatin1String("{a91eaba1-cf4c-11d3-b96a-00a0c9c7b61a}"); // ASIO DirectX blacklist << QLatin1String("{e3186861-3a74-11d1-aef8-0080ad153287}"); // ASIO Multimedia #ifdef QT_NO_DEBUG blacklist << QLatin1String("{232685c6-6548-49d8-846d-4141a3ef7560}"); // ASIO4ALL #endif if (RegOpenKeyEx(HKEY_LOCAL_MACHINE, L"Software\\ASIO", 0, KEY_READ, &hkDevs) == ERROR_SUCCESS) { DWORD idx = 0; DWORD keynamelen = 255; WCHAR keyname[255]; while (RegEnumKeyEx(hkDevs, idx++, keyname, &keynamelen, nullptr, nullptr, nullptr, &ft) == ERROR_SUCCESS) { QString name = QString::fromUtf16(reinterpret_cast< ushort * >(keyname), keynamelen); if (RegOpenKeyEx(hkDevs, keyname, 0, KEY_READ, &hk) == ERROR_SUCCESS) { DWORD dtype = REG_SZ; WCHAR wclsid[255]; DWORD datasize = 255; CLSID clsid; if (RegQueryValueEx(hk, L"CLSID", 0, &dtype, reinterpret_cast< BYTE * >(wclsid), &datasize) == ERROR_SUCCESS) { if (datasize > 76) datasize = 76; QString qsCls = QString::fromUtf16(reinterpret_cast< ushort * >(wclsid), datasize / 2).toLower().trimmed(); if (!blacklist.contains(qsCls.toLower()) && !FAILED(CLSIDFromString(wclsid, &clsid))) { bFound = true; } } RegCloseKey(hk); } keynamelen = 255; } RegCloseKey(hkDevs); } if (bFound) { airASIO = new ASIOAudioInputRegistrar(); crASIO = new ConfigRegistrar(2002, ASIOConfigDialogNew); } else { qWarning("ASIO: No valid devices found, disabling"); } } void ASIOInit::destroy() { delete airASIO; delete crASIO; } static class ASIOInit asioinit; ASIOInput *ASIOInput::aiSelf; ASIOConfig::ASIOConfig(Settings &st) : ConfigWidget(st) { setupUi(this); qcbDevice->setAccessibleName(tr("Device to use for microphone")); qlwMic->setAccessibleName(tr("List of microphones")); qlwSpeaker->setAccessibleName(tr("List of speakers")); // List of devices known to misbehave or be totally useless QStringList blacklist; blacklist << QLatin1String("{a91eaba1-cf4c-11d3-b96a-00a0c9c7b61a}"); // ASIO DirectX blacklist << QLatin1String("{e3186861-3a74-11d1-aef8-0080ad153287}"); // ASIO Multimedia #ifdef QT_NO_DEBUG blacklist << QLatin1String("{232685c6-6548-49d8-846d-4141a3ef7560}"); // ASIO4ALL #endif HKEY hkDevs; if (RegOpenKeyEx(HKEY_LOCAL_MACHINE, L"Software\\ASIO", 0, KEY_READ, &hkDevs) == ERROR_SUCCESS) { const DWORD keynamebufsize = 255; WCHAR keyname[keynamebufsize]; FILETIME ft; DWORD idx = 0; DWORD keynamelen = keynamebufsize; while (RegEnumKeyEx(hkDevs, idx++, keyname, &keynamelen, nullptr, nullptr, nullptr, &ft) == ERROR_SUCCESS) { QString name = QString::fromUtf16(reinterpret_cast< ushort * >(keyname), keynamelen); HKEY hk; if (RegOpenKeyEx(hkDevs, keyname, 0, KEY_READ, &hk) == ERROR_SUCCESS) { DWORD dtype = REG_SZ; WCHAR wclsid[255]; DWORD datasize = 255; if (RegQueryValueEx(hk, L"CLSID", 0, &dtype, reinterpret_cast< BYTE * >(wclsid), &datasize) == ERROR_SUCCESS) { if (datasize > 76) datasize = 76; QString qsCls = QString::fromUtf16(reinterpret_cast< ushort * >(wclsid), datasize / 2).toLower().trimmed(); CLSID clsid; if (!blacklist.contains(qsCls) && !FAILED(CLSIDFromString(wclsid, &clsid))) { ASIODev ad(name, qsCls); qlDevs << ad; } } RegCloseKey(hk); } keynamelen = keynamebufsize; } RegCloseKey(hkDevs); } bOk = false; ASIODev ad; foreach (ad, qlDevs) { qcbDevice->addItem(ad.first, QVariant(ad.second)); } if (qlDevs.count() == 0) { qpbQuery->setEnabled(false); qpbConfig->setEnabled(false); } } void ASIOConfig::on_qpbQuery_clicked() { QString qsCls = qcbDevice->itemData(qcbDevice->currentIndex()).toString(); CLSID clsid; IASIO *iasio; clearQuery(); CLSIDFromString(const_cast< wchar_t * >(reinterpret_cast< const wchar_t * >(qsCls.utf16())), &clsid); if (CoCreateInstance(clsid, nullptr, CLSCTX_INPROC_SERVER, clsid, reinterpret_cast< void ** >(&iasio)) == S_OK) { SleepEx(10, false); if (iasio->init(mumble_mw_hwnd)) { SleepEx(10, false); char buff[512]; memset(buff, 0, 512); iasio->getDriverName(buff); SleepEx(10, false); long ver = iasio->getDriverVersion(); SleepEx(10, false); ASIOSampleRate srate = 0.0; iasio->setSampleRate(48000.0); iasio->getSampleRate(&srate); SleepEx(10, false); long minSize, maxSize, prefSize, granSize; iasio->getBufferSize(&minSize, &maxSize, &prefSize, &granSize); SleepEx(10, false); QString str = tr("%1 (version %2)").arg(QLatin1String(buff)).arg(ver); qlName->setText(str); str = tr("%1 -> %2 samples buffer, with %3 sample resolution (%4 preferred) at %5 Hz") .arg(minSize) .arg(maxSize) .arg(granSize) .arg(prefSize) .arg(srate, 0, 'f', 0); qlBuffers->setText(str); long ichannels, ochannels; iasio->getChannels(&ichannels, &ochannels); SleepEx(10, false); long cnum; bool match = (s.qsASIOclass == qsCls); for (cnum = 0; cnum < ichannels; cnum++) { ASIOChannelInfo aci; aci.channel = cnum; aci.isInput = true; iasio->getChannelInfo(&aci); SleepEx(10, false); switch (aci.type) { case ASIOSTFloat32LSB: case ASIOSTInt32LSB: case ASIOSTInt24LSB: case ASIOSTInt16LSB: { QListWidget *widget = qlwUnused; QVariant v = static_cast< int >(cnum); if (match && s.qlASIOmic.contains(v)) widget = qlwMic; else if (match && s.qlASIOspeaker.contains(v)) widget = qlwSpeaker; QListWidgetItem *item = new QListWidgetItem(QLatin1String(aci.name), widget); item->setData(Qt::UserRole, static_cast< int >(cnum)); } break; default: qWarning("ASIOInput: Channel %ld %s (Unusable format %ld)", cnum, aci.name, aci.type); } } bOk = true; } else { SleepEx(10, false); char err[255]; iasio->getErrorMessage(err); SleepEx(10, false); QMessageBox::critical(this, QLatin1String("Mumble"), tr("ASIO Initialization failed: %1").arg(QString::fromLatin1(err).toHtmlEscaped()), QMessageBox::Ok, QMessageBox::NoButton); } iasio->Release(); } else { QMessageBox::critical(this, QLatin1String("Mumble"), tr("Failed to instantiate ASIO driver"), QMessageBox::Ok, QMessageBox::NoButton); } } void ASIOConfig::on_qpbConfig_clicked() { QString qsCls = qcbDevice->itemData(qcbDevice->currentIndex()).toString(); CLSID clsid; IASIO *iasio; CLSIDFromString(const_cast< wchar_t * >(reinterpret_cast< const wchar_t * >(qsCls.utf16())), &clsid); if (CoCreateInstance(clsid, nullptr, CLSCTX_INPROC_SERVER, clsid, reinterpret_cast< void ** >(&iasio)) == S_OK) { SleepEx(10, false); if (iasio->init(mumble_mw_hwnd)) { SleepEx(10, false); iasio->controlPanel(); SleepEx(10, false); } else { SleepEx(10, false); char err[255]; iasio->getErrorMessage(err); SleepEx(10, false); QMessageBox::critical(this, QLatin1String("Mumble"), tr("ASIO Initialization failed: %1").arg(QString::fromLatin1(err).toHtmlEscaped()), QMessageBox::Ok, QMessageBox::NoButton); } iasio->Release(); } else { QMessageBox::critical(this, QLatin1String("Mumble"), tr("Failed to instantiate ASIO driver"), QMessageBox::Ok, QMessageBox::NoButton); } } void ASIOConfig::on_qcbDevice_activated(int) { clearQuery(); } void ASIOConfig::on_qpbAddMic_clicked() { int row = qlwUnused->currentRow(); if (row < 0) return; qlwMic->addItem(qlwUnused->takeItem(row)); } void ASIOConfig::on_qpbRemMic_clicked() { int row = qlwMic->currentRow(); if (row < 0) return; qlwUnused->addItem(qlwMic->takeItem(row)); } void ASIOConfig::on_qpbAddSpeaker_clicked() { int row = qlwUnused->currentRow(); if (row < 0) return; qlwSpeaker->addItem(qlwUnused->takeItem(row)); } void ASIOConfig::on_qpbRemSpeaker_clicked() { int row = qlwSpeaker->currentRow(); if (row < 0) return; qlwUnused->addItem(qlwSpeaker->takeItem(row)); } QString ASIOConfig::title() const { return tr("ASIO"); } const QString &ASIOConfig::getName() const { return ASIOConfig::name; } QIcon ASIOConfig::icon() const { return QIcon(QLatin1String("skin:config_asio.png")); } void ASIOConfig::save() const { if (!bOk) return; s.qsASIOclass = qcbDevice->itemData(qcbDevice->currentIndex()).toString(); QList< QVariant > list; for (int i = 0; i < qlwMic->count(); i++) { QListWidgetItem *item = qlwMic->item(i); list << item->data(Qt::UserRole); } s.qlASIOmic = list; list.clear(); for (int i = 0; i < qlwSpeaker->count(); i++) { QListWidgetItem *item = qlwSpeaker->item(i); list << item->data(Qt::UserRole); } s.qlASIOspeaker = list; } void ASIOConfig::load(const Settings &r) { int i = 0; ASIODev ad; foreach (ad, qlDevs) { if (ad.second == r.qsASIOclass) { loadComboBox(qcbDevice, i); } i++; } s.qlASIOmic = r.qlASIOmic; s.qlASIOspeaker = r.qlASIOspeaker; qlName->setText(QString()); qlBuffers->setText(QString()); qlwMic->clear(); qlwUnused->clear(); qlwSpeaker->clear(); } void ASIOConfig::clearQuery() { bOk = false; qlName->setText(QString()); qlBuffers->setText(QString()); qlwMic->clear(); qlwUnused->clear(); qlwSpeaker->clear(); } ASIOInput::ASIOInput() { QString qsCls = Global::get().s.qsASIOclass; CLSID clsid; iasio = nullptr; abiInfo = nullptr; aciInfo = nullptr; // Sanity check things first. iNumMic = Global::get().s.qlASIOmic.count(); iNumSpeaker = Global::get().s.qlASIOspeaker.count(); if ((iNumMic == 0) || (iNumSpeaker == 0)) { QMessageBox::warning(nullptr, QLatin1String("Mumble"), tr("You need to select at least one microphone and one speaker source to use ASIO."), QMessageBox::Ok, QMessageBox::NoButton); return; } CLSIDFromString(const_cast< wchar_t * >(reinterpret_cast< const wchar_t * >(qsCls.utf16())), &clsid); if (CoCreateInstance(clsid, nullptr, CLSCTX_INPROC_SERVER, clsid, reinterpret_cast< void ** >(&iasio)) == S_OK) { if (iasio->init(nullptr)) { iasio->setSampleRate(48000.0); ASIOSampleRate srate = 0.0; iasio->getSampleRate(&srate); if (srate <= 0.0) return; long minSize, maxSize, prefSize, granSize; iasio->getBufferSize(&minSize, &maxSize, &prefSize, &granSize); bool halfit = true; double wbuf = (srate / 100.0); long wantBuf = lround(wbuf); lBufSize = wantBuf; if (static_cast< double >(wantBuf) == wbuf) { qWarning("ASIOInput: Exact buffer match possible."); if ((wantBuf >= minSize) && (wantBuf <= maxSize)) { if (wantBuf == minSize) halfit = false; else if ((granSize >= 1) && (((wantBuf - minSize) % granSize) == 0)) halfit = false; } } if (halfit) { if (granSize == 0) { qWarning("ASIOInput: Single buffer size"); lBufSize = minSize; } else { long target = wantBuf / 2; lBufSize = target; while (lBufSize < target) { if (granSize < 0) lBufSize *= 2; else lBufSize += granSize; } } qWarning("ASIOInput: Buffer mismatch mode. Wanted %li, got %li", wantBuf, lBufSize); } abiInfo = new ASIOBufferInfo[iNumMic + iNumSpeaker]; aciInfo = new ASIOChannelInfo[iNumMic + iNumSpeaker]; int i, idx = 0; for (i = 0; i < iNumMic; i++) { abiInfo[idx].isInput = true; abiInfo[idx].channelNum = Global::get().s.qlASIOmic[i].toInt(); aciInfo[idx].channel = abiInfo[idx].channelNum; aciInfo[idx].isInput = true; iasio->getChannelInfo(&aciInfo[idx]); SleepEx(10, false); idx++; } for (i = 0; i < iNumSpeaker; i++) { abiInfo[idx].isInput = true; abiInfo[idx].channelNum = Global::get().s.qlASIOspeaker[i].toInt(); aciInfo[idx].channel = abiInfo[idx].channelNum; aciInfo[idx].isInput = true; iasio->getChannelInfo(&aciInfo[idx]); SleepEx(10, false); idx++; } iEchoChannels = iNumSpeaker; iMicChannels = iNumMic; iEchoFreq = iMicFreq = iroundf(srate); initializeMixer(); ASIOCallbacks asioCallbacks; ZeroMemory(&asioCallbacks, sizeof(asioCallbacks)); asioCallbacks.bufferSwitch = &bufferSwitch; asioCallbacks.sampleRateDidChange = &sampleRateChanged; asioCallbacks.asioMessage = &asioMessages; asioCallbacks.bufferSwitchTimeInfo = &bufferSwitchTimeInfo; if (iasio->createBuffers(abiInfo, idx, lBufSize, &asioCallbacks) == ASE_OK) { bRunning = true; return; } } } if (iasio) { iasio->Release(); iasio = nullptr; } QMessageBox::critical(nullptr, QLatin1String("Mumble"), tr("Opening selected ASIO device failed. No input will be done."), QMessageBox::Ok, QMessageBox::NoButton); } ASIOInput::~ASIOInput() { qwDone.wakeAll(); wait(); if (iasio) { iasio->stop(); iasio->disposeBuffers(); iasio->Release(); iasio = nullptr; } delete[] abiInfo; abiInfo = nullptr; delete[] aciInfo; aciInfo = nullptr; } void ASIOInput::run() { QMutex m; m.lock(); if (iasio) { aiSelf = this; iasio->start(); qwDone.wait(&m); } } ASIOTime *ASIOInput::bufferSwitchTimeInfo(ASIOTime *, long index, ASIOBool) { aiSelf->bufferReady(index); return 0L; } void ASIOInput::addBuffer(ASIOSampleType sampType, int interleave, void *src, float *RESTRICT dst) { switch (sampType) { case ASIOSTInt16LSB: { const float m = 1.0f / 32768.f; const short *RESTRICT buf = static_cast< short * >(src); for (int i = 0; i < lBufSize; i++) dst[i * interleave] = buf[i] * m; } break; case ASIOSTInt32LSB: { const float m = 1.0f / 2147483648.f; const int *RESTRICT buf = static_cast< int * >(src); for (int i = 0; i < lBufSize; i++) dst[i * interleave] = buf[i] * m; } break; case ASIOSTInt24LSB: { const float m = 1.0f / static_cast< float >(0x7FFFFFFF - 0xFF); const unsigned char *RESTRICT buf = static_cast< unsigned char * >(src); for (int i = 0; i < lBufSize; i++) dst[i * interleave] = (buf[i * 3] << 24 | buf[i * 3 + 1] << 16 | buf[i * 3 + 2] << 8) * m; } break; case ASIOSTFloat32LSB: { const float *RESTRICT buf = static_cast< float * >(src); for (int i = 0; i < lBufSize; i++) dst[i * interleave] = buf[i]; } break; } } void ASIOInput::bufferReady(long buffindex) { STACKVAR(float, buffer, lBufSize *qMax(iNumMic, iNumSpeaker)); for (int c = 0; c < iNumSpeaker; ++c) addBuffer(aciInfo[iNumMic + c].type, iNumSpeaker, abiInfo[iNumMic + c].buffers[buffindex], buffer + c); addEcho(buffer, lBufSize); for (int c = 0; c < iNumMic; ++c) addBuffer(aciInfo[c].type, iNumMic, abiInfo[c].buffers[buffindex], buffer + c); addMic(buffer, lBufSize); } void ASIOInput::bufferSwitch(long index, ASIOBool processNow) { ASIOTime timeInfo; memset(&timeInfo, 0, sizeof(timeInfo)); if (aiSelf->iasio->getSamplePosition(&timeInfo.timeInfo.samplePosition, &timeInfo.timeInfo.systemTime) == ASE_OK) timeInfo.timeInfo.flags = kSystemTimeValid | kSamplePositionValid; bufferSwitchTimeInfo(&timeInfo, index, processNow); } void ASIOInput::sampleRateChanged(ASIOSampleRate) { qFatal("ASIOInput: sampleRateChanged"); } long ASIOInput::asioMessages(long selector, long value, void *, double *) { long ret = 0; switch (selector) { case kAsioSelectorSupported: if (value == kAsioResetRequest || value == kAsioEngineVersion || value == kAsioResyncRequest || value == kAsioLatenciesChanged || value == kAsioSupportsTimeInfo || value == kAsioSupportsTimeCode || value == kAsioSupportsInputMonitor) ret = 1L; break; case kAsioResetRequest: qFatal("ASIOInput: kAsioResetRequest"); ret = 1L; break; case kAsioResyncRequest: ret = 1L; break; case kAsioLatenciesChanged: ret = 1L; break; case kAsioEngineVersion: ret = 2L; break; case kAsioSupportsTimeInfo: ret = 1; break; case kAsioSupportsTimeCode: ret = 0; break; } return ret; }