Android
Qt

QtからAndroidで音声ファイル再生 - Qtで作るAndroid目覚ましもどき #4-1 -

Qtで作るAndroid目覚ましもどきシリーズ4です。まだ画面は全部できてないですが、Qtの話をだらだらしてもあれなので少し話を飛ばして、QtからAndroidで音声ファイルを再生するためにAndroidのAPIを使う話をします。(お待ちかね、でしょうか)

目的/やること

Android上で動かすQtで音声ファイルの再生を制御する。

方法/やり方

Linux/Windowsでのやりかた

Audioオブジェクトを使っていれば簡単に実現できます。
ただしAudioオブジェクトはImageオブジェクトのように置くだけでは使えません。
まずproファイルに下記を書きます。

<プロジェクト名>.pro
QT += multimedia

そしてAudioオブジェクトを置くqmlファイルに下記のように書いていきます。

<適当なファイル>.qml
// ↓これは冒頭に
import QtMultimedia 5.8

    // で、このqmlのベースオブジェクトの中の好きな場所に↓を置く
    Audio {
        id: _alerm_voice
        volume: 1.0
        source: "qrc:/voices/sayaka_01.mp3" // 再生したいファイル。mp3可。場所も適当に変えられる。
    }

でもこれだとAndroidでは再生できません。そもそもmultimediaをimportしようとしたところでAndroidアプリとしてデプロイできない状態になります。
少なくともQt5.9では。c++とかならできるのかなぁ。

Androidでのやりかた

なので思い切ってAndroidはAndroidのAPIを使って何とかすることにします。
しかしAndroidのAPIといえばJava。c++からJavaのライブラリを使うライブラリを作成し、このc++ライブラリをQMLに提供することにします。

c++/音声再生クラスの作成

クラスの初期化

まずc++のクラスを初期化します。

star.cpp
Star::Star(QObject *parent)
    : QObject(parent)
{
    qDebug() << "SummerIce::Star > construction";
}

なぜクラスの名前をstarにしたのか……なんでなんすかね? qmlファイルは適当な名前を付けていったというのに。
ここにandroid.media.MediaPlayerのインスタンスを追加します。このクラスのインスタンスの入れ物をm_mediaPlayerとしてstarクラスのメンバに追加し、

star.h
    QAndroidJniObject m_mediaPlayer;

starクラスのコンストラクタで一緒に初期化します。

star.cpp
Star::Star(QObject *parent)
    : QObject(parent)
    , m_mediaPlayer("android/media/MediaPlayer") // ★これ!★
{
    qDebug() << "SummerIce::Star > construction";
}

QAndroidJniObject

さて、ここでしれっと出てきたQAndroidJniObjectです。
リファレンスの一言の説明は「c++からJavaのコードを呼ぶAPIを提供する」ものです。私はこれまでJNIもNDKも知りませんでしたが、要するにQtのJNI(Java Native Interface)でありNDK(Native Development Kit)であるようです。
QAndroidJniObjectのコンストラクタの定義の一つはQAndroidJniObject(const char *className)であり、引数にクラス名を与えることでそのクラスのインスタンスとしてc++で使えるようになります。

  • (2018/1/6追記)
    一応説明しておくと、QAndroidJniObjectを使うためには、proファイルにはQT += androidextrasを書き、 c++の部分にQAndroidJniObject(パスがちゃんと通ってないときはQtAndroidExtras/QAndroidJniObject)を インクルードする必要があります。
    詳しくはリファレンス参照。

音声ファイル再生/メソッドの利用

ここではStarクラスの下記メソッドをQMLでファイル名(絶対パス)を渡しながら呼び出せば再生できるようにします。

star.h
    Q_INVOKABLE void playFile(const QString &file);

MediaPlayer APIはsetDataSource()→prepare()→start()の順番で再生するようです。めんどうなのでそのまえにstop()とreset()も使うことにします。(再生してないときにstopすると怒られているような気がするので、気にする人は改良してください)
stop()、reset()はvoid stop()という感じなので下記のように簡易な書き方になります。

star.cpp
void Star::playFile(const QString &file)
{
    m_mediaPlayer.callMethod<void>("stop");
    m_mediaPlayer.callMethod<void>("reset");
    // :
}

リポジトリのソースはもうちょっとごちゃっとなにやら書いてありますが気にしないでください。
すなわちcallMethod()というQAndroidJniObjectのメソッドでインスタンスの正体のクラスのメソッドを呼び出します。
そして次にsetDataSource()。これの型は例えば下記ですね。
void setDataSource(String path)
引数が1つ入ってくると途端に話がややこしくなります。うまく簡潔に話せないのでまず答えは下記です。

star.cpp
    m_mediaPlayer.callMethod<void>("setDataSource"
                                  ,"(Ljava/lang/String;)V"
                                  ,QAndroidJniObject::fromString(file).object()
                                  );

これは、callMethod()の型の1つが
T QAndroidJniObject::callMethod(const char *methodName, const char *signature, ...) const
だからです。
まず第一引数はいつでもメソッドの名前です。
次に第二引数以降は……リファレンスとにらめっこです。リファレンスをsignatureで検索するとこの文が目に入る。

The signature structure is (A)R, where A is the type of the argument(s) and R is the return type.

すなわち"(引数)戻り値"という書き方をしろと。それ以上シグネチャーの書き方について丁寧な説明がないように私の英語力では思うが、要するにJNI Typesを使う書式の模様。
setDataSource()は引数がStringで戻り値はvoid。Stringとはjava.lang.ObjectでJNI TypeのjstringもLjava/lang/String;なのでたぶん同じだろう。voidのシグネチャーはVで良いだろう。で、とりあえず第二引数は下記にする。
"(Ljava/lang/String;)V"
具体的な値の渡し方はもはや見たように書くしかない。幸いjstringについては同じリファレンスにサンプルコードもあり……
QAndroidJniObject::fromString(file).object()
とした。
あとはprepare()とstart()だがこれはまたvoid prepare()な感じなので簡単に書け、とりあえずplayFile()メソッドが完成する。

star.cpp
void Star::playFile(const QString &file)
{
    m_mediaPlayer.callMethod<void>("stop");
    m_mediaPlayer.callMethod<void>("reset");
    m_mediaPlayer.callMethod<void>("setDataSource"
                                  ,"(Ljava/lang/String;)V"
                                  ,QAndroidJniObject::fromString(file).object()
                                  );
    m_mediaPlayer.callMethod<void>("prepare");
    m_mediaPlayer.callMethod<void>("start");
}

QMLからの呼び出し

あとは簡単。QMLにc++のライブラリを組み込むなどの説明は省略。
そこらへんはsummer_clock.cppなどを解析してください。
画面を表示している間、一定周期でアラームの音声を繰り返すことにします。

Asm_toggles.qml
    // 鳴らすためのタイマー
    Timer {
        id: _ring_timer
        interval: 10*1000 // 鳴らすものによって変わる
        running: false
        repeat:  true
        onTriggered: {
            Star.playFile(voice_path);
        }
    }

余談

  • Q_INVOKABLE
    英語ですが公式のリファレンスでサンプルコード付きの説明があります。
    QMLで使用したいクラスのメソッドはpublicにしてQ_INVOKABLEをつけなさいとのこと。
    ただしクラスの宣言をヘッダーファイルにして実体はソースファイルに書くときは、実体の方にはQ_INVOKABLEはつけなくて良い。むしろつけると何故かQt Creatorが赤線を引いてくる時がある。コンパイルは通る。謎。