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

Qtで作るAndroid目覚ましもどきシリーズ４です。まだ画面は全部できてないですが、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です。
[リファレンス](http://doc.qt.io/qt-5/qandroidjniobject.html)の一言の説明は「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");
    // :
}
```

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

``` star.cpp
    m_mediaPlayer.callMethod<void>("setDataSource"
                                  ,"(Ljava/lang/String;)V"
                                  ,QAndroidJniObject::fromString(file).object()
                                  );
```
これは、callMethod()の型の１つが
` T QAndroidJniObject::callMethod(const char *methodName, const char *signature, ...) const `
だからです。
まず第一引数はいつでもメソッドの名前です。
次に第二引数以降は……[リファレンス](http://doc.qt.io/qt-5/qandroidjniobject.html)とにらめっこです。リファレンスを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](https://github.com/hakua-doublemoon/SummerIce/blob/master/summer_ice/summer_clock.cpp)などを解析してください。
画面を表示している間、一定周期でアラームの音声を繰り返すことにします。

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

# 余談
* Q_INVOKABLE  
英語ですが[公式のリファレンス](http://doc.qt.io/qt-5/qtqml-cppintegration-exposecppattributes.html)でサンプルコード付きの説明があります。  
QMLで使用したいクラスのメソッドはpublicにしてQ_INVOKABLEをつけなさいとのこと。  
ただしクラスの宣言をヘッダーファイルにして実体はソースファイルに書くときは、実体の方にはQ_INVOKABLEはつけなくて良い。むしろつけると何故かQt Creatorが赤線を引いてくる時がある。コンパイルは通る。謎。