Qt - 如何同时录制和播放声音

13
我在Qt论坛上发布了这个问题,但没有得到答案。所以我在这里发帖。
我想知道在Qt中是否有任何方法可以同时录制和播放声音。我想从麦克风录制声音,同时在扬声器/耳机中播放声音。
在Qt中有没有这样的方法?还是我需要使用其他库?
如果解决方案是跨平台的(我需要覆盖Windows、Linux和Mac),那将是很棒的。如果不可能,那么Linux的解决方案也可以。
顺便说一下,我正在使用Qt 4.7。
编辑

我的最新实现在这里给出here。我创建了一个QIODevice的子类,并重新实现了它的writeDatareadData方法,以便可以使用循环缓冲区进行读写操作。我按照this suggestion的建议来完成这个任务。这段代码也无法正常工作,因为QAudioOutput实例会遇到Underrun Error,根据this documentation的解释,这意味着:

音频数据没有以足够快的速度提供给音频设备

我已经应用了一个hack来暂时解决这个问题。在outputStateChanged方法中,我检查输出状态是否已更改为IDLE,如果是,我将再次调用start()方法,指定公共缓冲区。我不想将其作为永久解决方案,因为它感觉非常hacky,而且我没有正确地调查错误的原因。

我应该怎么做来解决这个问题?

我也尝试使用Phonon来解决这个问题,但失败了,因为我对这个模块的知识不足。


@BrianRoach:我还没有尝试过任何东西,因为我找不到开始的方法。我知道可以使用QAudioInput进行声音输入,使用QAudioOutput播放声音,但这两个都是在文件上工作,即QAudioInput将输入存储在文件中,然后QAudioOutput从该文件播放声音。这种方法肯定在全双工场景下行不通,对吧?我找到了一些以前的答案,但它们都很旧,并且建议使用其他库,如openAL、portAudio等。我想知道是否有使用Qt库的解决方案。 - MD Sayem Ahmed
5个回答

9

我对Qt不是很熟悉,但我很擅长处理媒体,如果我的回答不够具体,而是从更一般的角度来解决您的问题,请原谅。

我查看了您的代码,我认为总的来说,您的想法应该可行。但我发现了一些问题:

  • writeData方法似乎没有准备好处理缓冲区满的情况。当循环缓冲区填满时,它将仅覆盖旧数据,并错误地继续增加currentBufferLength变量。我认为这里正确的做法是更新readPosition以跳过丢失的数据,并防止currentBufferLength超过缓冲区大小。

  • 您同时启动了写入器和读取器。相反,您应该启动写入器并启动循环缓冲区,然后再启动读取器。请记住,您永远无法零延迟录制和播放。至少您的延迟将是单个缓冲区写入的大小,但实际上您可能需要写入器提前几个缓冲区以避免出现问题。

  • 您应该单独调试读取器和写入器。仅设置写入器并验证循环缓冲区是否以固定间隔写入数据(首先按照我上面的建议修复溢出条件)。要调试,请将缓冲区转储到文件中,然后在音频播放器(例如Audacity)中检查该文件,或者您可以使用printf调试来确保您不断获取数据。然后以类似的方式处理只有读取器的情况。

  • 最后一点想法。调用readDatawriteData方法的代码可能在其他线程上运行,很可能是两个不同的线程,一个用于读取器,另一个用于写入器。如果我的猜测正确,则您的循环结构存在大问题。如果没有保护确定读取和写入位置和大小的变量的访问权限,那么您将会遇到竞争条件。

祝好运。


谢谢Miguel。你的回答非常详细,我会记住的 :-) 。 - MD Sayem Ahmed

3
我不认为在您的评论中提到的类会有任何问题。它们都不仅限于使用文件。
QAudioInputstart()方法返回的QIODevice,可以传递给QAudioOutputstart()方法。
QIODevice *myDevice = myQAudioInput->start();
myQAudioOutput->start( myDevice ); 

我尝试了你的方法。起初它似乎有效,但是一段时间后,输出状态变为闲置。这可能是由于同步问题或其他原因,我不知道。我将我的代码发布在编辑中,以便您查看。 - MD Sayem Ahmed
我已经弄清楚了发生了什么。audioOutput 对象面临着欠流错误。 - MD Sayem Ahmed
我现在正在使用一个程序来做这件事。它可以工作,但是输出的声音有很强的静电噪音。有什么原因会导致这种情况吗?这里是我的问题以及我的代码样式。 - developer01

2
以下是在QT5中编写的读取音频输入(麦克风)并将其放入64K循环缓冲区的代码。一旦缓冲区有数据,它就会将其写入音频输出(PC上的扬声器)。这是一个非常基础的代码,可以作为熟悉音频设备的起点。注意,此处音频输入和输出在一个对象中,可能会导致缓冲区问题。要解决这个问题,需要创建单独的输入和输出对象。 程序分为两个文件,第一个是qt配置文件(.pro),第二个是main.cpp文件。
#AudioEcho.pro file for QT5.2.1

QT       += core
QT       -= gui
QT += multimedia widgets
TARGET = AudioEcho
CONFIG   += console
CONFIG   -= app_bundle
TEMPLATE = app
SOURCES += main.cpp


//main.cpp file
#include <QDebug>
#include <QIODevice>
#include <QAudioInput>
#include <QAudioOutput>
#include <QCoreApplication>

class myAudio :public QIODevice
{
  // Q_OBJECT

public:
     QAudioOutput *audioOut;
     QAudioInput  *audioIn;

     myAudio();
    ~myAudio(){}
    void fillBuffer();
     QAudioFormat formatIn,formatOut;
     QByteArray buff;
     char *pbuff;
     quint64 RXbuff;
     quint64 buffPtr;
protected:
     qint64 readData(char *data, qint64 maxlen);
     qint64 writeData(const char *data, qint64 len);
     qint64 bytesAvailable() const;
};

#define SAMPLE_RATE 22050
#define CHANNELS 1
#define SAMPLE_SIZE 16
#define SAMPLE_TYPE SignedInt

int main(int argc, char *argv[])
{
    QCoreApplication a(argc, argv);
    myAudio *m= new myAudio();
    return a.exec();
}
myAudio::myAudio()
    {
    formatIn.setSampleRate(SAMPLE_RATE);
    formatIn.setChannelCount(CHANNELS);
    formatIn.setSampleSize(SAMPLE_SIZE);
    formatIn.setCodec("audio/pcm");
    formatIn.setByteOrder(QAudioFormat::LittleEndian);
    formatIn.setSampleType(QAudioFormat::SAMPLE_TYPE);

    formatOut.setSampleRate(SAMPLE_RATE);
    formatOut.setChannelCount(CHANNELS);
    formatOut.setSampleSize(SAMPLE_SIZE);
    formatOut.setCodec("audio/pcm");
    formatOut.setByteOrder(QAudioFormat::LittleEndian);
    formatOut.setSampleType(QAudioFormat::SAMPLE_TYPE);

//print out the output device setup parameters
     QAudioDeviceInfo          deviceOut(QAudioDeviceInfo::availableDevices(QAudio::AudioOutput).at(0));     //select output device 0
     qDebug()<<"Selected Output device ="<<deviceOut.deviceName();

//print out the input device setup parameters
     QAudioDeviceInfo     deviceIn(QAudioDeviceInfo::availableDevices(QAudio::AudioInput).at(0));     //select output device 0
     qDebug()<<"Selected input device ="<<deviceIn.deviceName();

//configure device
     audioOut = new QAudioOutput(deviceOut,formatOut,0);
     audioIn  = new QAudioInput (deviceIn, formatIn,0);

//print out the device specifications
     foreach(const QAudioDeviceInfo &deviceInfo,     QAudioDeviceInfo::availableDevices(QAudio::AudioInput))
          {
          qDebug() << "\nSuported Input devices";
          qDebug() << "\nDevice name: "             << deviceInfo.deviceName();
          qDebug() << "Supported channel count: "   << deviceInfo.supportedChannelCounts();
          qDebug() << "Supported Codec: "           << deviceInfo.supportedCodecs();
          qDebug() << "Supported byte order: "      << deviceInfo.supportedByteOrders();
          qDebug() << "Supported Sample Rate: "     << deviceInfo.supportedSampleRates();
          qDebug() << "Supported Sample Size: "     << deviceInfo.supportedSampleSizes();
          qDebug() << "Supported Sample Type: "     << deviceInfo.supportedSampleTypes();
          qDebug() << "Preferred Device settings:"  << deviceInfo.preferredFormat();
          }
     foreach(const QAudioDeviceInfo &deviceInfo, QAudioDeviceInfo::availableDevices(QAudio::AudioOutput))
         {
         qDebug() << "\nSuported output devices";
         qDebug() << "Device name: "             << deviceInfo.deviceName();
         qDebug() << "Supported channel count: "   << deviceInfo.supportedChannelCounts();
         qDebug() << "Supported Codec: "           << deviceInfo.supportedCodecs();
         qDebug() << "Supported byte order: "      << deviceInfo.supportedByteOrders();
         qDebug() << "Supported Sample Rate: "     << deviceInfo.supportedSampleRates();
         qDebug() << "Supported Sample Size: "     << deviceInfo.supportedSampleSizes();
         qDebug() << "Supported Sample Type: "     << deviceInfo.supportedSampleTypes();
         qDebug() << "Preferred Device settings:"  << deviceInfo.preferredFormat();
         }

      buff.resize(0x10000);   //create a rx buffer

      pbuff=buff.data();       //get the buff address;
      RXbuff=0;                //set RX buffer pointer

      qDebug()<<"File open"<<open(QIODevice::ReadWrite);
      qDebug()<<"is device Sequential="<<isSequential();
      audioIn->start(this); //start reading device

      audioOut->setVolume(0.5);  //volume 0 to 1.0
      audioOut->start(this);    //start writing to device
}

//QIODevice Class (Protected Functions)This function is called by QIODevice.
//send to output(Speaker)
qint64 myAudio::readData(char *data, qint64 len)
{
static quint64 TXbuff=0;
qint64 total = 0;
while (len > total  && RXbuff>TXbuff)//write and synchonise buffers
       {
         //write data to speaker
        memcpy(&data[total],&pbuff[TXbuff%0x10000],2);    //copy 2 Bytes
        TXbuff+=2; //point to next buffer 16 bit location
        total+=2;
       }
return total;  //the reset interval
}


//audio input (from Microphone)
qint64 myAudio::writeData(const char *data, qint64 len)
{
int total=0;
while (len > total)
       {
        memcpy(&pbuff[RXbuff%0x10000],&data[total], 2); //write 2Bytes into circular buffer(64K)
        RXbuff+=2; //next 16bit buffer location
        total+=2;  //next data location
      }
return (total); //return total number of bytes received
}

qint64 myAudio::bytesAvailable() const{return 0;}

2

按照以下方式启动输入和输出设备

m_output= m_audioOutput->start();
    m_input = m_audioInput->start();
    connect(m_input, SIGNAL(readyRead()), SLOT(readMore()));

在readMore()函数中,将输入示例写入输出。
m_output->write(outdata, len);

请查看这篇文章获取更多信息。这个示例应用程序是用Qt创建的,可以同时从麦克风录制和播放音频。链接:http://www.codeproject.com/Articles/421287/Cross-Platform-Microphone-Audio-Processing-Utility

1
你可以使用从启动QAudioInput获得的QIOStream来创建一个Phonon :: MediaSource。然后,您可以在该Phonon :: MediaSource和Phonon :: AudioOutput对象之间创建路径。有关更多详细信息,请查看Phonon :: AudioOutputPhonon :: MediaSource的文档。

不,我还没有尝试过那种方法,事实上我不知道有这样的方法(我是Qt的初学者)。让我尝试一下这种方法。 - MD Sayem Ahmed
我不知道如何在这两个类之间创建路径,因为它们都不是 Phonon 的一部分。 - MD Sayem Ahmed
@SayemAhmed 好观点。我修改了我的回答以回答您的问题。 - Kurtis Nusbaum
我也尝试了你的方法,但是出现了一些异常,请查看编辑。 - MD Sayem Ahmed

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接