【TextAlive App API】HTML＋CSS+JavaScriptのみで実装するチュートリアル【マジカルミライ2020プロコン】

# はじめに
　2020/09/18、[初音ミク「マジカルミライ 2020」プログラミング・コンテスト](https://magicalmirai.com/2020/procon/ "初音ミク「マジカルミライ 2020」プログラミング・コンテスト")の開催が発表され、同時に「TextAlive App API」が一般公開された。未だ人気を誇るコンテンツ「初音ミク」を用いたプロコン（プログラミングコンテスト）ということで、熱意に満ちた同志は多いことだろう。

　しかし、一般公開されたばかりのAPIということで公式の文献以外の情報が無いに等しい。しかもサンプルコードもビルドツール仕様がほぼほぼ前提であり、ビルドツールなども何も使わない（つまり、HTML・CSS・JavaScriptベタ書き）&&ローカル環境となると厳しいものばかり。諦めたくなった人も多いのではなかろうか。。。

　今回、プロコンに参加するにあたって、公式の[チュートリアル](https://developer.textalive.jp/app/ "1. 開発の始め方 | TextAlive App API | TextAlive for Developers")と[サンプルコード](https://github.com/TextAliveJp/textalive-app-script-tag "TextAliveJp/textalive-app-script-tag: script タグで API を読み込み、発声中のフレーズとビート情報を表示する TextAlive App API の作例です")、[リファレンス](https://developer.textalive.jp/packages/textalive-app-api/ "textalive-app-api | TextAlive for Developers")を参考にしつつ闘い抜いた戦果をここに書き記しておく。尚、本記事ではあくまでAPIの動作を理解するためにあり、「こう実装しなければならない」というものを示すものではない。本記事読者様の思うように実装し、プロコンに挑むなどして頂きたい。

　尚、JavaScriptの言語自体の詳細な仕様、用語などについては本記事では解説を入れないものとする。ここでは「関数」「メソッド」を呼び分けず、すべて「関数」と呼ぶとする。わからないことについては、私の過去記事「[JavaScript基礎文法 - Qiita](https://qiita.com/yukuduri/items/cff4a1f9c85114600425 "JavaScript基礎文法 - Qiita")」の参照や、各自でググって頂きたい。（もちろんヤフってもらっても構わない。~~検索エンジン的にはググってるのと一緒だけどな~~）

　ここまで読んでお察しだろうが、記事内では割とフザけつつ書いているのでその点注意されたし。

# 今回の戦果
<blockquote class="twitter-tweet"><p lang="ja" dir="ltr">TextAlive App API、数日にわたって闘いに挑んだ結果、<br>なんとかコードを理解した上でここまで動作することにこぎつけた。<br><br>※ビルドツール無し、HTML+CSS+JavaScript環境 <a href="https://t.co/fvzdZxYs6u">pic.twitter.com/fvzdZxYs6u</a></p>&mdash; ゆくどり(🦈❤)(🐱💙)(🐱🎤🎨)ぽんこつ信者🥕 (@durian3960) <a href="https://twitter.com/durian3960/status/1308736008670732292?ref_src=twsrc%5Etfw">September 23, 2020</a></blockquote> <script async src="https://platform.twitter.com/widgets.js" charset="utf-8"></script>
　ちなみに、現時点で不具合も多い。詳細は後ほど。

# 実装内容
　注）今回のソースコードについては、CSSは含まれていない。~~「タイトルにCSSと入れているにも関わらず使っていないのか」とか言わない。~~

　今回は、非推奨かもしれない（※後述）が、ひとまずわかりやすさ重視で`<input type="text">`要素に入力された楽曲URLを読み込み、それを再生＆歌詞を表示してみよう。いちいちURLをコピペするのは面倒なので、ひとまず今回のプロコンの課題曲3曲のURLを`autocomplete`属性で補完可能にしている。また、`jQuery3.5.1`を利用している。

　本記事では、まずソースコードを提示し、その後詳しく見ていくとしよう。

## ソースコード

ディレクトリ構成：
root/
　├ js/
　│ └ script.js
　└ index.html

```index.html
<!DOCTYPE html>
<html lang="ja">
  <head>
    <meta charset="UTF-8">
    <title>API Test</title>
  </head>
  
  <body>
    <label>songURL:</label><input type="text" name="songUrl" size="50" autocomplete="on" list="songs"><button onclick="run()">Run</button>
    <datalist id="songs">
      <option value="https://www.youtube.com/watch?v=ygY2qObZv24"></option>
      <option value="https://www.youtube.com/watch?v=a-Nf3QUFkOU"></option>
      <option value="https://www.youtube.com/watch?v=XSLhsjepelI"></option>
    </datalist>
    <hr>
    <div id="info"></div>
    <div id="contents">
      <h3 id="text"></h3>
    </div>
    <div id="media"></div>
    
    <script src="https://unpkg.com/axios/dist/axios.min.js"></script>
    <script src="https://unpkg.com/textalive-app-api/dist/index.js"></script>
    <script src="https://code.jquery.com/jquery-3.5.1.min.js" integrity="sha256-9/aliU8dGd2tb6OSsuzixeV4y/faTqgFtohetphbbj0=" crossorigin="anonymous"></script>
    <script src="js/script.js"></script>
  </body>
</html>
```

```js/script.js
const { Player } = TextAliveApp;

let songUrl = 'http://www.youtube.com/watch?v=ygY2qObZv24';

// 単語ごとに歌詞を表示
const animateWord = (now, unit) => {
  if (unit.contains(now)) {
    $('#text').html(unit.text);
  }
};

const run = () =>{
  //初期化処理
  $('#info').empty();
  $('#media').empty();
  
  const songUrlVal = $('input[name="songUrl"]').val();
  if (songUrlVal!='') songUrl=songUrlVal;
  const player = new Player({ app: true , mediaElement: document.querySelector('#media')});
  console.log('Run');
  player.addListener({
    onAppReady: (app) => {
      console.log('AppReady');
      player.createFromSongUrl(songUrl);
    },
    
    // 楽曲情報読み込み完了後、呼ばれる
    // この時点ではrequestPlay()等が実行不能
    onVideoReady: (v) => {
      console.log('VideoReady');
      let infoContents = '';
      infoContents += '<h1>楽曲情報</h1>';
      infoContents += '<h2>楽曲名：<br>' + player.data.song.name + '</h2>';
      infoContents += '<h2>アーティスト名：<br>' + player.data.song.artist.name + '</h2>';
      $('#info').html(infoContents);
      $('#text').html('[再生準備待機中]');
    },
    
    // 再生準備完了後、呼ばれる
    // これ以降、requestPlay()等が実行可能
    onTimerReady: () => {
      console.log('TimerReady');
      $('#text').html('[再生開始]');
      player.requestPlay();
      // 定期的に呼ばれる各単語の "animate" 関数をセットする
      let w = player.video.firstWord;
      while (w) {
        w.animate = animateWord;
        w = w.next;
      }
    },
  });
}
```

## 仕様詳細
全体的な流れ：

1. フォーム入力欄に楽曲URLを入力、`Run`ボタンを押して実行
2. 入力されたURLを用いて`TextAlive App API`呼び出し（未入力なら、「ピノキオピー」氏の「愛されなくても君がいる」を使用）
3. 楽曲情報が読み込めたら、「楽曲名」「アーティスト名」を表示
4. 再生準備ができたら、再生を開始し、歌詞が始まれば単語ごとに表示

### １．`Run`ボタン押下直後
```javascript
let songUrl = 'http://www.youtube.com/watch?v=ygY2qObZv24';
const run = () =>{
  //初期化処理
  $('#info').empty();
  $('#media').empty();

  //※以下省略
}
```
初期化処理として「楽曲情報」の部分と「#media（※後述）」の中身を消しているが、歌詞表示の実行中に再度`Run`を押すと歌詞を二重に取得し、その止め方がいくら試しても無理だった（推測だが、歌詞を表示するループが止められていないからだと思われる）ため放置。現状、ページ再読み込みで対処。

```javascript
  const songUrlVal = $('input[name="songUrl"]').val();
  if (songUrlVal!='') songUrl=songUrlVal;
```
フォーム入力欄からURLを取得、何か入力があれば`songUrl`に代入。なければ`songUrl`の初期値を使用。

```javascript
const player = new Player({ app: true , mediaElement: document.querySelector('#media')});
```
`TextAlive App API`で利用する`player`を宣言。サンプルコードを見るに、`app: true`の箇所でアプリ名や作者を定義するなどできるみたいだが、ひとまず放置。
`mediaElement: document.querySelector('#media')`で再生時に表示されるメディア（YouTubeの埋め込み動画）の位置を指定できる模様。CSSなどでいい感じに位置を調整するなどしよう。ちなみに、試してみたがjQueryのセレクタ`$('#media')`などは利用できないみたい。

### ２．`TextAlive App API`呼び出し後
`player.addListener()`に引数として
`イベント１:関数１,イベント２:関数２,...`
のように渡すことで、そのイベント（`onAppReady` , `onVideoReady`など）になった際に対応する関数を実行。
サンプルコードを見るに、`イベント`と同名の`関数`を入れることでも代用可？（未検証）
尚、本記事ではここで渡す関数にはサンプルコードでも使われている「アロー関数」を用いる。


**`onAppReady`イベント：初期化されたプレイヤーが最初に呼ぶイベント**

```javascript
    onAppReady: (app) => {
      console.log('AppReady');
      player.createFromSongUrl(songUrl);
    },
```
`player.createFromSongUrl(url);`で引数の`url`(文字列)から再生する楽曲のURLを直接渡す。
**※本来、楽曲URLはクエリパラメータ`ta_song_url`で渡すことが前提とされており、今回のようにフォーム入力などから取得するのは非推奨と考えられる。チュートリアルでは、クエリパラメータで指定されていない場合のフォールバックとして利用する方法が紹介されている。**
`app.managed`が`true`のとき、ホストが存在する（＝**TextAlive**と接続されている？）。
`app.songUrl`が`true`のとき、クエリパラメータ`ta_song_url`で指定されたURLが存在し、それにより自動的に選曲状態になっている（＝`player.createFromSongUrl();`でURLを渡す必要はない、ハズ）。

### ３．楽曲情報読み込み完了後

**`onVideoReady`イベント：楽曲情報読み込み完了後、呼ばれるイベント**

```javascript
    // 楽曲情報読み込み完了後、呼ばれる
    // この時点ではrequestPlay()等が実行不能
    onVideoReady: (v) => {
      console.log('VideoReady');
      let infoContents = '';
      infoContents += '<h1>楽曲情報</h1>';
      infoContents += '<h2>楽曲名：<br>' + player.data.song.name + '</h2>';
      infoContents += '<h2>アーティスト名：<br>' + player.data.song.artist.name + '</h2>';
      $('#info').html(infoContents);
      $('#text').html('[再生準備待機中]');
    },
```
この時点で楽曲情報は読み込みが終わり、音源の再生準備に入る。`VideoReady`という名前がややこしいが、再生準備が整ったときに起こるイベント**ではなく**、再生**準備に入る**ときに起こるようだ。~~この名前のせいで、この時点で再生は可能だと思ってしまった。紛らわしい。ちくしょう！思えば、他のイベントも同様の名付けなのか？笑~~
楽曲情報の読み込みはできているので、`player.data.song.name`や`player.data.song.artist.name`で楽曲名やアーティスト名を取得可能。
今回利用していないが、`v.firstChar`が`true`のとき、歌詞付き楽曲、`false`のときは歌詞のない楽曲と判別可能。
~~ちなみに本記事最初に貼り付けた動画では`$('#text').html('[再生準備完了]');`にしてたけどややこしいので変えた。~~

### ４．再生準備完了後

**`onTimerReady`イベント：再生準備完了後、呼ばれるイベント**

```javascript
    // 再生準備完了後、呼ばれる
    // これ以降、requestPlay()等が実行可能
    onTimerReady: () => {
      console.log('TimerReady');
      $('#text').html('[再生開始]');
      player.requestPlay();
      // 定期的に呼ばれる各単語の "animate" 関数をセットする
      let w = player.video.firstWord;
      while (w) {
        w.animate = animateWord;
        w = w.next;
      }
```
この時点でようやく再生準備が完了。これ以降、`requestPlay()`など再生の操作ができる。

`TextAlive App API`「おまたせ、待った？」
`俺`「おせえよ！！　あとイベントの名前紛らわしいのやめろ！」

ひとまず再生準備が完了したら強制的に再生する仕様に。
チュートリアル通り、歌詞を単語ごとに表示する`animateWord`関数を呼び出し。

```javascript
// 単語ごとに歌詞を表示
const animateWord = (now, unit) => {
  if (unit.contains(now)) {
    $('#text').html(unit.text);
  }
};
```
`animateWord`関数は、チュートリアルにあるものと多少書き換えてますが、動作は同じ。
強いて言うなら、jQueryがないと動かないくらいかな。
~~いや、それを言うならここ以外もjQueryセレクタ使ってる箇所すべて動かないけどな~~

## その他のイベント
　今回使用していないイベントについてはチュートリアル（[App のライフサイクル](https://developer.textalive.jp/app/life-cycle/ "4. App のライフサイクル | TextAlive App API | TextAlive for Developers")）を参照のこと。

　ちなみに、実行中に`Run`を再度押下しても私の環境下（~~個人的に~~**最強**~~だと思ってる~~**HTMLエディタBrackets**のライブプレビュー機能）では`onAppMediaChange`イベントは発火しなかったので、やはりサンプルコードのように、**楽曲URLはクエリパラメータ`ta_song_url`で指定**するのが**推奨**と言えるだろう。`onAppReady`の説明に
> ホストは App に対して再生したい楽曲の URL をクエリパラメータ ta_song_url で渡します。

とあり、`onAppMediaChange`の説明には

> ホストは App に対して再生したい楽曲の URL を更新することがあります。この際 App の再読み込みは行われません。 App 側では onAppMediaChange イベントが呼ばれるため、これで URL の変更を検知します。

とも書いてあるしな。。。

　なお今の所、クエリパラメータで指定するとしての実装・検証は行っていない。~~まあ動くんちゃう？知らんけど。~~

# さいごに
　最後まで読んでくださり、ありがとうございます。今回実装した手法では楽曲URLの変更が感知できないなど問題がありすぎるので、実際にWebアプリケーションを実装する上では避けたほうがいいだろう。~~取り消し線でここに何か書こうとしてたけど、改めて書いた記事読み返してたら何したかったか忘れた。~~

　本記事を作成したのは、チュートリアルやサンプルコードをもとに、新規参入者さんたちによりわかりやすく伝え、導入段階の敷居を下げるためである。本記事に書いた私の失敗を踏み台にして、それならばどう実装して解決するのか、などはこんな拙い記事を最後まで読んでくれた貴方がた自身で考え、実装し、是非とも"初音ミク「マジカルミライ 2020」プログラミング・コンテスト"にご参加頂きたい。~~というのは建前で、この記事の読者がプログラミングコンテストの好敵手＜ライバル＞となりかねないからだ。あと「審査完了までソースコードを一般公開できない」という規約に抵触の恐れも無いとは言えないからな。~~

　今回のプロコンに関しては、審査完了後に成果物のソースコードの一般公開ができるので、コンテスト応募期間中に記事にしたい内容を書き溜めておき、コンテスト終了後、改めてソース公開とともに軽く解説記事でも書ければと思っている。プロコンに燃える同志よ、健闘を祈る。

### P.S.
この記事書くのに4時間以上かかってた。いつのまにこんな時間に。。。

`心の声`（タグ`TextAliveAppAPI`でどなたか有用な記事を投稿してくださることをひっそり祈っている。。。）