Qtでオーディオクラスを使う


Qtを使ってIP電話アプリを作っていて、音声を入出力する必要がありました。

Qtに限らず一般的に、オーディオデバイスを使うのって難しいよね、という固定観念がありました。WindowsのマルチメディアAPIが複雑怪奇なので、オーディオ制御するの面倒だなぁ、と思い込んでいました。LinuxやMacに至っては、APIの呼び出し方さえ知りません。それを試しにQtを使ってやってみたら、アホみたいに簡単だったというお話です。

Qtがサポートするマルチメディア機能は豊富(だと思う)のですが、初歩中の初歩、音を鳴らして、同時に入力するだけ、という、最も基本の機能を使ってみます。

とりあえず、Qtウィジェットアプリケーションを新規作成して、プログレスバーを配置します。

プロジェクトファイルに、

QT += multimedia

を追加しておきます。

メインウィンドウのヘッダです。

MainWindow.h
#ifndef MAINWINDOW_H
#define MAINWINDOW_H

#include <QAudioInput>
#include <QAudioOutput>
#include <QMainWindow>
#include <memory>

namespace Ui {
class MainWindow;
}

class MainWindow : public QMainWindow
{
    Q_OBJECT
private:
    std::shared_ptr<QAudioInput> audio_input;
    std::shared_ptr<QAudioOutput> audio_output;
    QIODevice *audio_input_device;
    QIODevice *audio_output_device;
    int count = 0;
protected:
    void timerEvent(QTimerEvent *event);
public:
    explicit MainWindow(QWidget *parent = 0);
    ~MainWindow();
private slots:
    void onReadyRead();
private:
    Ui::MainWindow *ui;
};

#endif // MAINWINDOW_H

メインウィンドウのソースです。

MainWindow.cpp
#include "MainWindow.h"
#include "ui_MainWindow.h"
#include <QAudioFormat>

MainWindow::MainWindow(QWidget *parent) :
    QMainWindow(parent),
    ui(new Ui::MainWindow)
{
    ui->setupUi(this);

    QAudioFormat format;
    format.setByteOrder(QAudioFormat::LittleEndian);
    format.setChannelCount(1);
    format.setCodec("audio/pcm");
    format.setSampleRate(8000);
    format.setSampleSize(16);
    format.setSampleType(QAudioFormat::SignedInt);
    audio_input = std::shared_ptr<QAudioInput>(new QAudioInput(format));
    audio_output = std::shared_ptr<QAudioOutput>(new QAudioOutput(format));
    audio_input_device = audio_input->start();
    audio_output_device = audio_output->start();
    connect(audio_input_device, SIGNAL(readyRead()), this, SLOT(onReadyRead()));

    ui->progressBar->setRange(0, 100);
    ui->progressBar->setValue(0);

    startTimer(10);
}

MainWindow::~MainWindow()
{
    delete ui;
}

void MainWindow::onReadyRead()
{
    QByteArray ba = audio_input_device->readAll();
    int n = ba.size() / 2;
    int16_t *p = (int16_t *)ba.data();
    int max = 0;
    for (int i = 0; i < n; i++) {
        int v = p[i];
        if (v < 0) v = -v;
        if (max < v) {
            max = v;
        }
    }
    int pos = max * 100 / 32768;
    ui->progressBar->setValue(pos);
}

void MainWindow::timerEvent(QTimerEvent *event)
{
    int len = audio_output->bytesFree();
    int n = len / 2;
    if (n > 0) {
        std::vector<int16_t> buf(n);
        for (int i = 0; i < n; i++) {
            buf[i] = (count & 8) ? 10000 : -10000;
            count++;
            if (count >= 8000) {
                count = 0;
            }
        }
        audio_output_device->write((char const *)&buf[0], buf.size() * 2);
    }
}

とりあえず定数は決め打ちにしています。8kHz、16ビット、モノラルです。

オーディオ入力からのデータがある程度貯まったら、readyReadシグナルが発行されますので、データを受け取ります。このプログラムでは、最大値を求めてプログレスバーを更新しています。

10msごとのタイマイベントを発生させて、出力に空きがあれば、音声データを書き込みます。単純に500Hzの矩形波を生成しています。

ところで、QAudioInputやQAudioOutputというクラスですが、プラットフォームによって挙動が少し異なります。マルチスレッドの中で使うと、うまく動作しないことがあります。Qtのイベントループが廻るスレッド(メインのGUIスレッド)でQAudioInputやQAudioOutputを使うのが推奨のようです。