Android で音楽再生のアプリを作ってみる

# 参考サイト

まず、参考サイトを挙げさせていただきますね。

+ [Androidで動く携帯Javaアプリ作成入門（30）：Androidアプリでマルチメディアを扱うための基礎知識](http://www.atmarkit.co.jp/ait/articles/1203/28/news128.html)

ここのサンプルをベースに開発させていただきました。
なお記事が書かれたのが少し前なので、最近のOSで動かすには、外部ストレージの読み出しパーミッションを追加する必要があります。

```AndroidManifest.xml
    <uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE" />
```

# 音楽再生アプリの拡張

そもそもやりたかったのは、グループで歌唱している楽曲と連動して、誰が歌っているかを表示すること。
なので、LED制御の前に、歌っているパートを通知する機能を音楽プレーヤーに追加します。

どうやって誰が歌っているかを判定するの?というのは今回は人力でごまかしています :droplet: 
あらかじめ、時間ごとに歌っている人のデータを作成しています。
(混ざっている音源から分離・解析するのはかなり困難だと思います。変化のタイミングを抽出するなど自動化できることもあるかと思いますが、開発にかなり時間もかかりそうなので今後の課題としています)

そこで、音楽再生サービスを次のように拡張していきます。

+ 歌唱パート・データ(JSON)を読み込む
+ タイマーを使って 100 ミリ秒に一度、歌っている人のデータを Broadcast する

100ミリ秒間隔で送信している理由は、次の2つになります。

+ LED 制御の処理時間や消費電力を考えるとある程度時間を開けたい
+ 人が見るものなので 100ミリ秒くらいのずれは気にならない

データの送信には Broadcast intent を使います。
BLE Nano との通信アプリ側は、Broadcast intent を受信して BLE Nano に LED の制御信号を送信します。
(アプリを分けているのは、それぞれ別のサンプルコードから拡張しているので、分けた方が作りやすかったからです。再設計して、まとまったものにしたいと思っていますが手が回ってません :sweat_drops: )

そこで、データ形式と、読み出し方法、データ送信についてもう少し詳しく書いていきたいと思います。

## 楽曲データ形式

データ形式はこんな感じになっています。

```data.json
{
  "Time":[0,5972,11133,18000,28339],
  "v0":[0,3,3,0,3],
  "v1":[0,3,3,0,0],
  "v2":[0,3,3,0,0],
  "v3":[0,3,3,0,3],
  "v4":[0,3,3,0,0],
  "v5":[0,3,3,0,0],
  "v6":[0,4,4,0,0]
}
```

読み出しやすさから、JSON 形式を使っています。

内容は、歌唱パターンが変化するタイミングと、そのとき各メンバーがどこを歌っているかの表となっています。
(本当は、もっと長いのですが、サンプルなので最初の5つだけにしています)

配列 Time に、パターンが変化するタイミングをミリ秒単位で格納しています。
そして、メンバーごとに、そのタイミングで歌っている高さを1～5で格納しています。

- 0: 歌っていない
- 1: オクターブ上
- 2: 上ハモ
- 3: 主メロ
- 4: 下ハモ
- 5: オクターブ下

(データとしては高さを分けてますが、今のところ LED 表示では歌ってるかどうかだけしか使えてません :sweat_smile:)

# 楽曲データの読み出し

この楽曲データは、音楽再生開始時に読み込んでおきます。
それぞれ "Time" の配列を mJAtime, "v0" を mJAv0 というように mJA.. という JSONArray オブジェクトに読み込んでおきます。

```MusicPlayerService.java
public class MusicPlayerService extends Service implements OnCompletionListener,
:
  private int mJAIndex = 0;
  private JSONArray mJAtime;
  private JSONArray mJAv0;
  private JSONArray mJAv1;
  private JSONArray mJAv2;
  private JSONArray mJAv3;
  private JSONArray mJAv4;
  private JSONArray mJAv5;
  private JSONArray mJAv6;
  :
  public void onPrepared(MediaPlayer player) {
    readFile();
     :
  }
  private void readFile() {
    : 
    try {
      mJObj = new JSONObject(json_data);
      mJAtime = mJObj.getJSONArray("Time");
      mJAv0 = mJObj.getJSONArray("v0");
      mJAv1 = mJObj.getJSONArray("v1");
      mJAv2 = mJObj.getJSONArray("v2");
      mJAv3 = mJObj.getJSONArray("v3");
      mJAv4 = mJObj.getJSONArray("v4");
      mJAv5 = mJObj.getJSONArray("v5");
      mJAv6 = mJObj.getJSONArray("v6");
      mJAIndex = 0;
    } catch (JSONException e) {
      e.printStackTrace();
    }
    :
  }
```

(楽曲に対応したデータ・ファイルの読み込みや、ファイルから読み込んだ文字列を json_data に格納する部分は割愛させてください)

JSONデータの読み出しについては、こちらのサイトなどを参考にしています。

+ [【Android】JSONデータをパースする方法](http://tkm0on.hatenablog.com/entry/2015/05/21/183608)

## 歌唱パターン通知

音楽サービスに対して、定期的に割り込みをかけ、そのタイミングにおける歌唱パターンを intent を使って Broadcast 通知します。

```MusicPlayerService.java
public class MusicPlayerService extends Service implements OnCompletionListener,
:
  private Handler mHandler = new Handler();
:
  public void onCreate() {
:
    mTimer.schedule(new TimerTask() {
      @Override
      public void run() {
         mHandler.post( new Runnable() {
           public void run() {
             sendPlayerState();
           }
         });
      }
    }, 100, 100);
:
  }
```

まず、音楽プレーヤーサービスに、mHandler という 100ミリ秒に1回のタイマーを登録します。
次に、タイマーで呼びたす sendPlayerState() で、歌唱パターンを通知していきます。

```MusicPlayerService.java
public class MusicPlayerService extends Service implements OnCompletionListener,
  :
  public static final String ACTION_STATE_CHANGED = "com.example.android.remotecontrol.ACTION_STATE_CHANGED";
    :
  private void sendPlayerState() {
    :
    Intent intent = new Intent(ACTION_STATE_CHANGED);
    t0 = mPlayer.getCurrentPosition();
    intent.putExtra("currentPosition", t0);
    try {
        t1 = mJAtime.getLong(mJAIndex);
    } catch (JSONException e) {
        e.printStackTrace();
    }
    if (t1 < t0) {
      try {
        intent.putExtra("Update", true);
        intent.putExtra("v0", mJAv0.getInt(mJAIndex));
        intent.putExtra("v1", mJAv1.getInt(mJAIndex));
        intent.putExtra("v2", mJAv2.getInt(mJAIndex));
        intent.putExtra("v3", mJAv3.getInt(mJAIndex));
        intent.putExtra("v4", mJAv4.getInt(mJAIndex));
        intent.putExtra("v5", mJAv5.getInt(mJAIndex));
        intent.putExtra("v6", mJAv6.getInt(mJAIndex));
      } catch (JSONException e) {
        e.printStackTrace();
      }
      mJAIndex += 1;
    }
    sendBroadcast(intent);
  }
```

パターンが変化するタイミングを楽曲の再生位置が越えた場合に、intent に情報を付加して送信します。

それには、まず、t0 = mPlayer.getCurrentPosition() で、タイマ割り込み発生時の再生位置を取得して t0 に格納します。
(mPlayer は、再生中の MediaPlayer インスタンスです)

次に、t1 に mJAIndex 番目のパターン変化タイミングを格納します。
"t1 < t0" の場合は、現在の再生位置 t0 が変化タイミング t1 を越えているので、歌唱パターンが mJAIndex 番目のデータに変化したことになります。
そこで、mJAIndex 番目の歌唱パターン(誰がどこを歌っているかのデータ)を JSONArray を使って読み出し、intent に情報を追加します。
そして、mJAIndex に1を加算し、次のタイミングを指すようにします。

最後に、"sendBroadcat(intent)" で、inetnt を送信します。