/* Copyright (C) 2005, Thorvald Natvig All rights reserved. Redistribution and use in source and binary forms, with or without modification, are permitted provided that the following conditions are met: - Redistributions of source code must retain the above copyright notice, this list of conditions and the following disclaimer. - Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the following disclaimer in the documentation and/or other materials provided with the distribution. - Neither the name of the Mumble Developers nor the names of its contributors may be used to endorse or promote products derived from this software without specific prior written permission. THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS ``AS IS'' AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE FOUNDATION OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. */ #include #include #include #include #include #include #include #include "ASIOInput.h" #include "MainWindow.h" #include "Global.h" /* * This file blatanly assumes the sampling frequency is 48000Hz. * Anything else is an error, and to fix it all the constants * of 960 need to be replaced. * (960 == 20 ms of samples at 48000) */ static AudioInput *ASIOAudioInputNew() { return new ASIOInput(); } static ConfigWidget *ASIOAudioInputConfig(QWidget *parent) { return NULL; } AudioInputRegistrar airASIO("ASIO", ASIOAudioInputNew, ASIOAudioInputConfig); ASIOInput *ASIOInput::aiSelf; ASIOConfig::ASIOConfig(QWidget *p) : ConfigWidget(p) { HKEY hkDevs; HKEY hk; DWORD idx = 0; WCHAR keyname[255]; DWORD keynamelen = 255; FILETIME ft; HRESULT hr; // List of devices known to misbehave or be totally useless QStringList blacklist; blacklist << "{a91eaba1-cf4c-11d3-b96a-00a0c9c7b61a}"; // ASIO DirectX blacklist << "{e3186861-3a74-11d1-aef8-0080ad153287}"; // ASIO Multimedia if (RegOpenKeyEx(HKEY_LOCAL_MACHINE, L"Software\\ASIO", 0, KEY_READ, &hkDevs) == ERROR_SUCCESS) { 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) { QString qsCls = QString::fromUtf16(reinterpret_cast(wclsid), datasize / 2); if (! blacklist.contains(qsCls.toLatin1()) && ! FAILED(hr =CLSIDFromString(wclsid, &clsid))) { ASIODev ad(name, qsCls); qlDevs << ad; } } RegCloseKey(hk); } keynamelen = 255; } RegCloseKey(hkDevs); } bOk = false; QGroupBox *qgbDevices, *qgbCapab, *qgbChannels; QGridLayout *grid; QVBoxLayout *v; QHBoxLayout *h; ASIODev ad; QLabel *l; qcbDevice = new QComboBox(); qcbDevice->setObjectName("Device"); foreach(ad, qlDevs) { qcbDevice->addItem(ad.first, QVariant(ad.second)); if (ad.second == g.s.qsASIOclass) { qcbDevice->setCurrentIndex(qcbDevice->count() - 1); } } qgbDevices=new QGroupBox(tr("Device selection")); qgbCapab = new QGroupBox(tr("Capabilities")); qgbChannels = new QGroupBox(tr("Channels")); qgbChannels->setToolTip(tr("Configure input channels")); qgbChannels->setWhatsThis(tr("This will configure the input channels for ASIO. Make sure you select at least one " "channel as microphone and speaker. Microphone should be where your mic is attached, " "and Speaker should be a channel that samples \"What you hear\".
" "For example, on the Audigy 2 ZS, a good selection for Mic would be \"Mic L\" while " "Speaker should be \"Mix L\" and \"Mix R\"")); grid=new QGridLayout(); qcbDevice->setToolTip(tr("Device to use for microphone")); qcbDevice->setWhatsThis(tr("This chooses what device to query. You still need to actually query the device and " "select which channels to use")); l = new QLabel(tr("Device")); QPushButton *queryButton=new QPushButton("&Query"); queryButton->setObjectName("Query"); queryButton->setToolTip(tr("Query selected device")); queryButton->setWhatsThis(tr("This queries the selected device for channels. Be aware that many ASIO drivers are " "buggy to the extreme, and querying them might cause a crash of either the application " "or the system.")); QPushButton *configButton=new QPushButton("&Config"); configButton->setObjectName("Config"); configButton->setToolTip(tr("Configure selected device")); configButton->setWhatsThis(tr("This configures the selected device. Be aware that many ASIO drivers are " "buggy to the extreme, and querying them might cause a crash of either the application " "or the system.")); grid->addWidget(l, 0, 0); grid->addWidget(qcbDevice, 0, 1); grid->addWidget(queryButton, 0, 2); grid->addWidget(configButton, 0, 3); qgbDevices->setLayout(grid); grid = new QGridLayout(); qlName=new QLabel(); qlBuffers=new QLabel(); l = new QLabel(tr("Driver name")); l->setBuddy(qlName); grid->addWidget(l, 0, 0); grid->addWidget(qlName, 0, 1); l = new QLabel(tr("Buffersize")); l->setBuddy(qlBuffers); grid->addWidget(l, 1, 0); grid->addWidget(qlBuffers, 1, 1); grid->setColumnStretch(1, 1); qgbCapab->setLayout(grid); h = new QHBoxLayout; v = new QVBoxLayout; qlwMic = new QListWidget(); qlwMic->setMinimumWidth(100); qlwMic->setMaximumWidth(100); l = new QLabel(tr("Microphone")); v->addWidget(l); v->addWidget(qlwMic); h->addLayout(v); QPushButton *addMicButton=new QPushButton(tr("<-")); addMicButton->setObjectName("AddMic"); addMicButton->setMaximumWidth(20); QPushButton *remMicButton=new QPushButton(tr("->")); remMicButton->setObjectName("RemMic"); remMicButton->setMaximumWidth(20); v = new QVBoxLayout; v->addStretch(1); v->addWidget(addMicButton); v->addWidget(remMicButton); v->addStretch(1); h->addLayout(v); v = new QVBoxLayout; qlwUnused = new QListWidget(); qlwUnused->setMinimumWidth(100); qlwUnused->setMaximumWidth(100); l = new QLabel(tr("Unused")); v->addWidget(l); v->addWidget(qlwUnused); h->addLayout(v); QPushButton *addSpeakerButton=new QPushButton(tr("->")); addSpeakerButton->setObjectName("AddSpeaker"); addSpeakerButton->setMaximumWidth(20); QPushButton *remSpeakerButton=new QPushButton(tr("<-")); remSpeakerButton->setObjectName("RemSpeaker"); remSpeakerButton->setMaximumWidth(20); v = new QVBoxLayout; v->addStretch(1); v->addWidget(remSpeakerButton); v->addWidget(addSpeakerButton); v->addStretch(1); h->addLayout(v); v = new QVBoxLayout; qlwSpeaker = new QListWidget(); qlwSpeaker->setMinimumWidth(100); qlwSpeaker->setMaximumWidth(100); l = new QLabel(tr("Speakers")); v->addWidget(l); v->addWidget(qlwSpeaker); h->addLayout(v); qgbChannels->setLayout(h); if (qlDevs.count() == 0) { queryButton->setEnabled(false); configButton->setEnabled(false); } v = new QVBoxLayout(); v->addWidget(qgbDevices); v->addWidget(qgbCapab); v->addWidget(qgbChannels); v->addStretch(1); setLayout(v); QMetaObject::connectSlotsByName(this); } #include "iasiothiscallresolver.h" void ASIOConfig::on_Query_clicked() { QString qsCls = qcbDevice->itemData(qcbDevice->currentIndex()).toString(); CLSID clsid; IASIO *iorigasio; bool ok = false; clearQuery(); CLSIDFromString(const_cast(reinterpret_cast(qsCls.utf16())), &clsid); if (CoCreateInstance(clsid, NULL, CLSCTX_INPROC_SERVER, clsid, reinterpret_cast(&iorigasio)) == S_OK) { IASIOThiscallResolver iasio(iorigasio); if (iasio.init(NULL)) { char buff[255]; memset(buff, 0, 255); iasio.getDriverName(buff); long ver = iasio.getDriverVersion(); long ilat, olat; ilat = olat = 0; iasio.getLatencies(&ilat, &olat); ASIOSampleRate srate; iasio.getSampleRate(&srate); if (fabs(srate-48000.0) > 1.0) { iasio.setSampleRate(48000.0); iasio.getSampleRate(&srate); } long minSize, maxSize, prefSize, granSize; iasio.getBufferSize(&minSize, &maxSize, &prefSize, &granSize); // We need sample sizes in ms for 2 bytes at 48khz bOk = false; if ((fabs(srate-48000.0) < 1.0) && (minSize <= 1920) && (maxSize >= 1920)) { if (granSize > 0) { long remain; remain = 1920 - minSize; if ((remain % granSize) == 0) bOk = true; } else if (granSize == -1) { long v = minSize; for(int i=0;i<100;i++) { if (v == 1920) { bOk = true; break; } v = v * 2; } } } long divider = lround(srate * 2.0 / 1000.0); if (divider < 1) divider = 1; if (granSize > 0) granSize /= divider; minSize /= divider; maxSize /= divider; prefSize /= divider; QString s = tr("%1 (ver %2)").arg(buff).arg(ver); qlName->setText(s); if (bOk) s = tr("%1 ms -> %2 ms (%3 ms resolution) %4Hz").arg(minSize).arg(maxSize).arg(granSize).arg(srate,0,'f',0); else s = tr("%1 ms -> %2 ms (%3 ms resolution) %4Hz -- Unusable").arg(minSize).arg(maxSize).arg(granSize).arg(srate,0,'f',0); qlBuffers->setText(s); if (bOk) { long ichannels, ochannels; iasio.getChannels(&ichannels, &ochannels); long cnum; bool match = (g.s.qsASIOclass == qsCls); for(cnum=0;cnum(cnum); if (match && g.s.qlASIOmic.contains(v)) widget = qlwMic; else if (match && g.s.qlASIOspeaker.contains(v)) widget = qlwSpeaker; QListWidgetItem *item = new QListWidgetItem(aciInfo.name, widget); item->setData(Qt::UserRole, static_cast(cnum)); } } } ok = true; } if (! ok) { char err[255]; iasio.getErrorMessage(err); QMessageBox::critical(this, tr("Mumble"), tr("ASIO Initialization failed: %1").arg(err), QMessageBox::Ok, QMessageBox::NoButton); } iasio.Release(); } else { QMessageBox::critical(this, tr("Mumble"), tr("Failed to instanciate ASIO driver"), QMessageBox::Ok, QMessageBox::NoButton); } } void ASIOConfig::on_Config_clicked() { QString qsCls = qcbDevice->itemData(qcbDevice->currentIndex()).toString(); CLSID clsid; IASIO *iorigasio; bool ok = false; CLSIDFromString(const_cast(reinterpret_cast(qsCls.utf16())), &clsid); if (CoCreateInstance(clsid, NULL, CLSCTX_INPROC_SERVER, clsid, reinterpret_cast(&iorigasio)) == S_OK) { IASIOThiscallResolver iasio(iorigasio); if (iasio.init(winId())) { ok = (iasio.controlPanel() == ASE_OK); } else { char err[255]; iasio.getErrorMessage(err); QMessageBox::critical(this, tr("Mumble"), tr("ASIO Initialization failed: %1").arg(err), QMessageBox::Ok, QMessageBox::NoButton); } iasio.Release(); } else { QMessageBox::critical(this, tr("Mumble"), tr("Failed to instanciate ASIO driver"), QMessageBox::Ok, QMessageBox::NoButton); } } void ASIOConfig::on_Device_activated(int index) { clearQuery(); } void ASIOConfig::on_AddMic_clicked() { int row = qlwUnused->currentRow(); if (row < 0) return; qlwMic->addItem(qlwUnused->takeItem(row)); } void ASIOConfig::on_RemMic_clicked() { int row = qlwMic->currentRow(); if (row < 0) return; qlwUnused->addItem(qlwMic->takeItem(row)); } void ASIOConfig::on_AddSpeaker_clicked() { int row = qlwUnused->currentRow(); if (row < 0) return; qlwSpeaker->addItem(qlwUnused->takeItem(row)); } void ASIOConfig::on_RemSpeaker_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(":/icons/config_dsound.png"); } void ASIOConfig::accept() { if (! bOk) return; g.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); } g.s.qlASIOmic = list; list.clear(); for(int i=0;icount();i++) { QListWidgetItem *item = qlwSpeaker->item(i); list << item->data(Qt::UserRole); } g.s.qlASIOspeaker = list; } void ASIOConfig::clearQuery() { bOk = false; qlName->setText(""); qlBuffers->setText(""); qlwMic->clear(); qlwUnused->clear(); qlwSpeaker->clear(); } #include "firmumble.inc" ASIOInput::ASIOInput() { QString qsCls = g.s.qsASIOclass; CLSID clsid; IASIO *iorigasio; iorigasio = false; iasio = NULL; abiInfo = NULL; int i, idx; bHasSpeaker = true; // Allocate buffers pdInputBuffer = new double[960]; pdOutputBuffer = new double[iFrameSize]; pdMicDelayLine = new double[MUMBLE_SFP_LENGTH]; pdSpeakerDelayLine = new double[MUMBLE_SFP_LENGTH]; for(i=0;i(reinterpret_cast(qsCls.utf16())), &clsid); if (CoCreateInstance(clsid, NULL, CLSCTX_INPROC_SERVER, clsid, reinterpret_cast(&iaOriginal)) == S_OK) { iasio = new IASIOThiscallResolver(iaOriginal); if (iasio->init(NULL)) { ASIOSampleRate srate = 0.0; iasio->getSampleRate(&srate); if ((fabs(srate - 48000.0) < 1.0) || (iasio->setSampleRate(48000.0) == ASE_OK)) { abiInfo = new ASIOBufferInfo[iNumMic + iNumSpeaker]; idx = 0; for(i=0;icreateBuffers(abiInfo, idx, 960, &asioCallbacks) == ASE_OK) return; } } } if (abiInfo) { delete [] abiInfo; abiInfo = NULL; } if (iasio) { iaOriginal->Release(); delete iasio; iasio = NULL; iaOriginal = NULL; } QMessageBox::critical(NULL, tr("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(); iaOriginal->Release(); delete iasio; } if (abiInfo) { delete [] abiInfo; abiInfo = NULL; } delete [] pdInputBuffer; delete [] pdOutputBuffer; delete [] pdMicDelayLine; delete [] pdSpeakerDelayLine; } void ASIOInput::run() { QMutex m; m.lock(); bRunning = true; if (iasio) { aiSelf = this; iasio->start(); qwDone.wait(&m); } } ASIOTime *ASIOInput::bufferSwitchTimeInfo(ASIOTime *timeInfo, long index, ASIOBool processNow) { aiSelf->bufferReady(index); return 0L; } void ASIOInput::bufferReady(long buffindex) { int c, i; // Microphone inputs short *buf = static_cast(abiInfo[0].buffers[buffindex]); for(i=0;i<960;i++) pdInputBuffer[i]=buf[i]; for(c=1;c(abiInfo[c].buffers[buffindex]); for(i=0;i<960;i++) pdInputBuffer[i]+=buf[i]; } double mul = 1.0 / (32768.0 * iNumMic); for(i=0;i<960;i++) { pdInputBuffer[i] *= mul; } decim(pdInputBuffer, pdOutputBuffer, pdMicDelayLine); for(i=0;i<320;i++) psMic[i] = static_cast(pdOutputBuffer[i] * 32768.0); // Speaker inputs buf = static_cast(abiInfo[iNumMic].buffers[buffindex]); for(i=0;i<960;i++) pdInputBuffer[i]=buf[i]; for(c=1;c(abiInfo[iNumMic+c].buffers[buffindex]); for(i=0;i<960;i++) pdInputBuffer[i]+=buf[i]; } mul = 1.0 / (32768.0 * iNumSpeaker); for(i=0;i<960;i++) { pdInputBuffer[i] *= mul; } decim(pdInputBuffer, pdOutputBuffer, pdSpeakerDelayLine); for(i=0;i<320;i++) psSpeaker[i] = static_cast(pdOutputBuffer[i] * 32768.0); encodeAudioFrame(); } 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 sRate) { qFatal("ASIOInput: sampleRateChanged"); } long ASIOInput::asioMessages(long selector, long value, void* message, double* opt) { 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; } /**************************************************************************** * * The following subroutine (ASIOInput::decim()) was adopted from decim.c * found at dspguru. * Original copyright notice as follows: * * Name: decim.c * * Synopsis: Decimates a real or complex signal. * * Description: See decim.h. * * by Grant R. Griffin * Provided by Iowegian's "dspGuru" service (http://www.dspguru.com). * Copyright 2001, Iowegian International Corporation (http://www.iowegian.com) * * The Wide Open License (WOL) * * Permission to use, copy, modify, distribute and sell this software and its * documentation for any purpose is hereby granted without fee, provided that * the above copyright notice and this license appear in all source copies. * THIS SOFTWARE IS PROVIDED "AS IS" WITHOUT EXPRESS OR IMPLIED WARRANTY OF * ANY KIND. See http://www.dspguru.com/wol.htm for more information. * *****************************************************************************/ #define factor_M 3 #define H_size MUMBLE_SFP_LENGTH #define p_H mumble_sfp void ASIOInput::decim(const double *p_inp, double *p_out, double *p_Z) { int tap; double sum; int num_inp = 960; while (num_inp >= factor_M) { /* shift Z delay line up to make room for next samples */ for (tap = H_size - 1; tap >= factor_M; tap--) { p_Z[tap] = p_Z[tap - factor_M]; } /* copy next samples from input buffer to bottom of Z delay line */ for (tap = factor_M - 1; tap >= 0; tap--) { p_Z[tap] = *p_inp++; } num_inp -= factor_M; /* calculate FIR sum */ sum = 0.0; for (tap = 0; tap < H_size; tap++) { sum += p_H[tap] * p_Z[tap]; } *p_out++ = sum; /* store sum and point to next output */ } }