// Copyright 2005-2019 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 // We define a global macro called 'g'. This can lead to issues when included code uses 'g' as a type or parameter name (like protobuf 3.7 does). As such, for now, we have to make this our last include. #include "Global.h" // From os_win.cpp. extern HWND mumble_mw_hwnd; class ASIOAudioInputRegistrar : public AudioInputRegistrar { public: ASIOAudioInputRegistrar(); virtual AudioInput *create(); virtual const QList getDeviceChoices(); virtual void setDeviceChoice(const QVariant &, Settings &); virtual bool canEcho(const QString &) const; }; ASIOAudioInputRegistrar::ASIOAudioInputRegistrar() : AudioInputRegistrar(QLatin1String("ASIO")) { } AudioInput *ASIOAudioInputRegistrar::create() { return new ASIOInput(); } const QList ASIOAudioInputRegistrar::getDeviceChoices() { QList qlReturn; return qlReturn; } bool ASIOAudioInputRegistrar::canEcho(const QString &) const { return true; } 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(NULL), crASIO(NULL) {} void initialize(); void destroy(); }; void ASIOInit::initialize() { HKEY hkDevs; HKEY hk; FILETIME ft; airASIO = NULL; crASIO = NULL; bool bFound = false; if (!g.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, NULL, NULL, NULL, &ft) == ERROR_SUCCESS) { QString name=QString::fromUtf16(reinterpret_cast(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(wclsid), &datasize) == ERROR_SUCCESS) { if (datasize > 76) datasize = 76; QString qsCls = QString::fromUtf16(reinterpret_cast(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); // 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, NULL, NULL, NULL, &ft) == ERROR_SUCCESS) { QString name=QString::fromUtf16(reinterpret_cast(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(wclsid), &datasize) == ERROR_SUCCESS) { if (datasize > 76) datasize = 76; QString qsCls = QString::fromUtf16(reinterpret_cast(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(reinterpret_cast(qsCls.utf16())), &clsid); if (CoCreateInstance(clsid, NULL, CLSCTX_INPROC_SERVER, clsid, reinterpret_cast(&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;cnumgetChannelInfo(&aci); SleepEx(10, false); switch (aci.type) { case ASIOSTFloat32LSB: case ASIOSTInt32LSB: case ASIOSTInt24LSB: case ASIOSTInt16LSB: { QListWidget *widget = qlwUnused; QVariant v = static_cast(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(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(Qt::escape(QLatin1String(err))), 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(reinterpret_cast(qsCls.utf16())), &clsid); if (CoCreateInstance(clsid, NULL, CLSCTX_INPROC_SERVER, clsid, reinterpret_cast(&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(Qt::escape(QLatin1String(err))), 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"); } 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 list; for (int i=0;icount();i++) { QListWidgetItem *item = qlwMic->item(i); list << item->data(Qt::UserRole); } s.qlASIOmic = list; list.clear(); for (int i=0;icount();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 = g.s.qsASIOclass; CLSID clsid; iasio = NULL; abiInfo = NULL; aciInfo = NULL; // Sanity check things first. iNumMic=g.s.qlASIOmic.count(); iNumSpeaker=g.s.qlASIOspeaker.count(); if ((iNumMic == 0) || (iNumSpeaker == 0)) { QMessageBox::warning(NULL, QLatin1String("Mumble"), tr("You need to select at least one microphone and one speaker source to use ASIO. " "If you just need microphone sampling, use DirectSound."), QMessageBox::Ok, QMessageBox::NoButton); return; } CLSIDFromString(const_cast(reinterpret_cast(qsCls.utf16())), &clsid); if (CoCreateInstance(clsid, NULL, CLSCTX_INPROC_SERVER, clsid, reinterpret_cast(&iasio)) == S_OK) { if (iasio->init(NULL)) { 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(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;igetChannelInfo(&aciInfo[idx]); SleepEx(10, false); idx++; } for (i=0;igetChannelInfo(&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 = NULL; } QMessageBox::critical(NULL, 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 = NULL; } delete [] abiInfo; abiInfo = NULL; delete [] aciInfo; aciInfo = NULL; } 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(src); for (int i=0;i(src); for (int i=0;i(0x7FFFFFFF - 0xFF); const unsigned char * RESTRICT buf=static_cast(src); for (int i=0;i(src); for (int i=0;iiasio->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; }