QtMultimedia にパッチを送ってみました

# はじめに

これは [Qt Advent Calendar 2024](https://qiita.com/advent-calendar/2024/qt) 13日目の記事です。

Qt6 で生まれ変わった QtMultimedia も、Qt 6.8 までくるとだいぶ落ち着いてきましたね。

しかし、実際に利用してみるとなにかおかしいな？となるのが Qt のいいところ。

そんなわけで、そういう時にはとりあえずバグレポを書くなり、パッチを送るなりしましょう。

# QtMultimedia で、マイクから音声を取得する方法

Qt では [QAudioSource](https://doc.qt.io/qt-6/qaudiosource.html) というクラスを利用して、簡単にマイクから音声データを取得することが可能です。

```c++
QFile destinationFile;   // Class member
QAudioSource* audio; // Class member
{
    destinationFile.setFileName("/tmp/test.raw");
    destinationFile.open( QIODevice::WriteOnly | QIODevice::Truncate );

    QAudioFormat format;
    // Set up the desired format, for example:
    format.setSampleRate(8000);
    format.setChannelCount(1);
    format.setSampleFormat(QAudioFormat::UInt8);

    QAudioDevice info = QMediaDevices::defaultAudioInput();
    if (!info.isFormatSupported(format)) {
        qWarning() << "Default format not supported, trying to use the nearest.";
    }

    audio = new QAudioSource(format, this);
    connect(audio, &QAudioSource::stateChanged, this, &AudioInputExample::handleStateChanged);

    QTimer::singleShot(3000, this, &AudioInputExample::stopRecording);
    audio->start(&destinationFile);
    // Records audio for 3000ms
}
```

QAudioSource には start() メソッドが２種類用意されています。

- [QIODevice *QAudioSource::start()](https://doc.qt.io/qt-6/qaudiosource.html#start)
- [void QAudioSource::start(QIODevice *device)](https://doc.qt.io/qt-6/qaudiosource.html#start-1)

前者は、QAudioSource （の内部実装）側が QIODevice を用意し、利用する際にはその QIODevice からデータを読み込みます。こちらは `Push Mode` と呼ばれます。

後者は、利用する側が用意した QIODevice に対して QAudioSource がデータを書き込む形になります。`Pull Mode` と呼ばれるようです。

上記のサンプルコードは、後者が利用されています。

# Push Mode が微妙に使いにくい

QtMultimedia には、マイクからデータを取得するサンプルアプリがあります。

[Audio Source Example](https://doc.qt.io/qt-6/qtmultimedia-audiosource-example.html)

[ソースコードは QtMultimedia の中](https://code.qt.io/cgit/qt/qtmultimedia.git/tree/examples/multimedia/audiosource?h=6.8) にあります。

このサンプルは、Push と Pull を切り替える機能が用意されていて、モードが切り替わる際に前述の２つの `start()` を呼び分けています。

```c++
void InputTest::toggleMode()
{
    m_audioInput->stop();
    toggleSuspend();

    // Change between pull and push modes
    if (m_pullMode) {
        m_modeButton->setText(tr("Enable push mode"));
        m_audioInput->start(m_audioInfo.data());
    } else {
        m_modeButton->setText(tr("Enable pull mode"));
        auto *io = m_audioInput->start();
        if (!io)
            return;

        connect(io, &QIODevice::readyRead, [this, io]() {
            static const qint64 BufferSize = 4096;
            const qint64 len = qMin(m_audioInput->bytesAvailable(), BufferSize);

            QByteArray buffer(len, 0);
            qint64 l = io->read(buffer.data(), len);
            if (l > 0) {
                const qreal level = m_audioInfo->calculateLevel(buffer.constData(), l);
                m_canvas->setLevel(level);
            }
        });
    }

    m_pullMode = !m_pullMode;
}
```

https://code.qt.io/cgit/qt/qtmultimedia.git/tree/examples/multimedia/audiosource/audiosource.cpp?h=6.8#n184

Push モードの際には、

```c++
connect(io, &QIODevice::readyRead, [this, io]() {...}
```

の中で、データを受信した際の処理を行っているのですが、受信したデータの大きさの取得を `m_audioInput->bytesAvailable()` という形で行っています。

これはちょっとおかしくて、`io` が `QIODevice` なので、`io->bytesAvailable()` とするのが普通ですよね。

しかし、これは私が試したLinux環境では動きません（でした）。

# 調査と結果

[qtmultimedia/src/multimedia/pulseaudio/qpulseaudiosource_p.h](https://code.qt.io/cgit/qt/qtmultimedia.git/tree/src/multimedia/pulseaudio/qpulseaudiosource_p.h?h=v6.8.1#n98) の以下のクラスが QAudioSource::start() で返ってくる QIODevice の実態です。

```c++
class PulseInputPrivate : public QIODevice
{
    Q_OBJECT
public:
    PulseInputPrivate(QPulseAudioSource *audio);
    ~PulseInputPrivate() override = default;

    qint64 readData(char *data, qint64 len) override;
    qint64 writeData(const char *data, qint64 len) override;

    void trigger();

private:
    QPulseAudioSource *m_audioDevice;
};
```

`bytesAvailable()` が未実装ですね。動いていない原因が判明してすっきりしました。


# なおしてみた

以下の変更を追加して

```diff
diff --git a/src/multimedia/pulseaudio/qpulseaudiosource.cpp b/src/multimedia/pulseaudio/qpulseaudiosource.cpp
index 87bba2427..b41c54769 100644
--- a/src/multimedia/pulseaudio/qpulseaudiosource.cpp
+++ b/src/multimedia/pulseaudio/qpulseaudiosource.cpp
@@ -545,6 +545,11 @@ PulseInputPrivate::PulseInputPrivate(QPulseAudioSource *audio)
     m_audioDevice = qobject_cast<QPulseAudioSource *>(audio);
 }

+qint64 PulseInputPrivate::bytesAvailable() const
+{
+    return m_audioDevice->bytesReady();
+}
+
 qint64 PulseInputPrivate::readData(char *data, qint64 len)
 {
     return m_audioDevice->read(data, len);
diff --git a/src/multimedia/pulseaudio/qpulseaudiosource_p.h b/src/multimedia/pulseaudio/qpulseaudiosource_p.h
index d652f81a0..d3e4adc1b 100644
--- a/src/multimedia/pulseaudio/qpulseaudiosource_p.h
+++ b/src/multimedia/pulseaudio/qpulseaudiosource_p.h
@@ -102,6 +102,7 @@ public:
     PulseInputPrivate(QPulseAudioSource *audio);
     ~PulseInputPrivate() override = default;

+    qint64 bytesAvailable() const override;
     qint64 readData(char *data, qint64 len) override;
     qint64 writeData(const char *data, qint64 len) override;

```

[611208: PulseAudio: Enable bytesAvailable() for QAudioSource::start()](https://codereview.qt-project.org/c/qt/qtmultimedia/+/611208) を作成しました。

~~これからレビューをしてもらうところですが、近い将来修正が取り込まれるといいなと思っています。~~

17日に取り込まれました！


# おわりに

Qt をお使いのみなさん、アドベントカレンダーになんでもいいので記事を書いて、盛り上げていきましょう！

明日は、[Qt 勉強会 ＠ Tokyo R #34](https://qt-users.connpass.com/event/339199/) が新宿で開催されます。興味のある方は、気軽にふらっと遊びにきてください！