mumble/ASIOInput.cpp
Thorvald Natvig de4b674b32 ASIO Audio Input
git-svn-id: https://mumble.svn.sourceforge.net/svnroot/mumble/trunk@152 05730e5d-ab1b-0410-a4ac-84af385074fa
2005-09-18 01:11:35 +00:00

715 lines
19 KiB
C++

/* Copyright (C) 2005, Thorvald Natvig <thorvald@natvig.com>
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 <QMessageBox>
#include <QGridLayout>
#include <QVBoxLayout>
#include <QPushButton>
#include <QGroupBox>
#include <QMutex>
#include <math.h>
#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<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) {
QString qsCls = QString::fromUtf16(reinterpret_cast<ushort *>(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.first == "Creative ASIO") {
qcbDevice->setCurrentIndex(qcbDevice->count() - 1);
}
}
qgbDevices=new QGroupBox(tr("Device selection"));
qgbCapab = new QGroupBox(tr("Capabilities"));
qgbChannels = new QGroupBox(tr("Channels"));
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"));
l->setBuddy(qcbDevice);
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<wchar_t *>(reinterpret_cast<const wchar_t *>(qsCls.utf16())), &clsid);
if (CoCreateInstance(clsid, NULL, CLSCTX_INPROC_SERVER, clsid, reinterpret_cast<void **>(&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<ichannels;cnum++) {
ASIOChannelInfo aciInfo;
aciInfo.channel = cnum;
aciInfo.isInput = true;
iasio.getChannelInfo(&aciInfo);
if (aciInfo.type == ASIOSTInt16LSB) {
QListWidget *widget = qlwUnused;
QVariant v = static_cast<int>(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<int>(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<wchar_t *>(reinterpret_cast<const wchar_t *>(qsCls.utf16())), &clsid);
if (CoCreateInstance(clsid, NULL, CLSCTX_INPROC_SERVER, clsid, reinterpret_cast<void **>(&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<QVariant> list;
for(int i=0;i<qlwMic->count();i++) {
QListWidgetItem *item = qlwMic->item(i);
list << item->data(Qt::UserRole);
}
g.s.qlASIOmic = list;
list.clear();
for(int i=0;i<qlwSpeaker->count();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;
// 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<MUMBLE_SFP_LENGTH;i++)
pdMicDelayLine[i]=pdSpeakerDelayLine[i] = 0.0;
// Sanity check things first.
iNumMic=g.s.qlASIOmic.count();
iNumSpeaker=g.s.qlASIOspeaker.count();
if ((iNumMic == 0) || (iNumSpeaker == 0)) {
QMessageBox::warning(NULL, tr("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<wchar_t *>(reinterpret_cast<const wchar_t *>(qsCls.utf16())), &clsid);
if (CoCreateInstance(clsid, NULL, CLSCTX_INPROC_SERVER, clsid, reinterpret_cast<void **>(&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;i<iNumMic;i++) {
abiInfo[idx].isInput = true;
abiInfo[idx].channelNum = g.s.qlASIOmic[i].toInt();
idx++;
}
for(i=0;i<iNumSpeaker;i++) {
abiInfo[idx].isInput = true;
abiInfo[idx].channelNum = g.s.qlASIOspeaker[i].toInt();
idx++;
}
ASIOCallbacks asioCallbacks;
ZeroMemory(&asioCallbacks, sizeof(asioCallbacks));
asioCallbacks.bufferSwitch = &bufferSwitch;
asioCallbacks.sampleRateDidChange = &sampleRateChanged;
asioCallbacks.asioMessage = &asioMessages;
asioCallbacks.bufferSwitchTimeInfo = &bufferSwitchTimeInfo;
if (iasio->createBuffers(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;
short *buf = static_cast<short *>(abiInfo[0].buffers[buffindex]);
for(i=0;i<960;i++)
pdInputBuffer[i]=buf[i];
for(c=1;c<iNumMic;c++) {
buf = static_cast<short *>(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<short>(pdOutputBuffer[i] * 32768.0);
encodeAudioFrame();
qWarning("Got Buffer %d", buffindex);
}
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 */
}
}