なでしこv3ブラウザ版で音を扱う

いつもの通り、自分が悩んだ末にたどり着いた記録を残したいと思います。
[日本語プログラミング言語「なでしこ」 Advent Calendar 2020](https://qiita.com/advent-calendar/2020/nadesiko)に連続投下することになるのを，少しためらったのですが…。空気を読まずに参加したいと思います（ごめんなさい）。

この記事は、なでしこのv3ブラウザ版で実行できるようにしています。

# v3ブラウザ版で音声ファイルを再生したい

以下に，自分が悩んだ経過をまとめていますが，[完成したテストプログラムはここ](https://oza-jv.github.io/sample-audio.html)に置いておきます。

## 標準の命令

なでしこv3ブラウザ版では，`オーディオ開く`等の命令が用意されています。

```
A=「https://nadesi.com/v3/common/sound/ki.mp3」をオーディオ開く。
Aをオーディオ再生。
```

ところが，なでしこv3簡易エディタでこれを実行すると，最初の１回だけ音が鳴りますが，２回目以降は音が鳴らないのです（私だけ？）。さらに，ローカルで音声ファイルを用意して試したい時に，`file:///d:/xxx.wav`のようにローカルパスを指定できませんでした。

## どうしてもローカルのファイルを再生したかった
Webサーバが無いと音声ファイルを扱えないということになると，学校などでローカルで学習するのが難しくなってしまいます。

そこで，どうしてもローカルのファイルを再生したかったので，簡易エディタをあきらめて，ローカルにHTMLファイルを作り，それになでしこのプログラムを組み込んでみることにしました。

# ローカルで音声ファイルを再生する

## フォルダとファイルの準備

まず，自分のPC上（WindwosでもChromebookでも同じです）に，プログラムを保存するためのフォルダを作ります。

その中に，次のファイルを入れておきます。

- なでしこのプログラムを実行するためのHTMLファイル
- 再生したい音声ファイル
- 今回作ったプラグインファイル plugin_media試作

> フォルダの例
![pic1.PNG](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/942750/f6580745-5d3e-797d-dd9a-f2f51781ef2d.png)

## なでしこ用HTMLファイルのひな形

自分でHTMLファイルを作って「なでしこ」を実行したかったので，次のようにひな形を作りました。

`<body>`要素の前半では，画面レイアウトをあらかじめhtmlで作っておきます。このサンプルでは，audio要素をあらかじめ設定していますが，このようにあらかじめaudio要素を埋め込んでおいてもいいし，後で動的に生成してもいいです。

`<body>`要素の後半では，なでしこ3のエンジンを読み込む処理と，今回作ったプラグイン`plugin_media`(試作)を読み込む処理があります。
`//初期設定`にある2行は，必ず実行します。その続きから，なでしこのプログラムを書いていきます。

``` template.html
<!DOCTYPE html>
<html lang="ja">
<head>
  <meta charset="UTF-8">
  <title>なでしこ　音読込のテスト</title>
</head>

<body>
<!-- 画面レイアウトをあらかじめ作っておく -->
<h1>なでしこ３ブラウザ版で音を扱うテスト</h1>
<audio id="audio1"></audio>
<hr />
<div id="div1"></div>

<!-- なでしこ3のエンジンを取り込み -->
<script type="text/javascript" src="https://nadesi.com/v3/cdn.php?v=3.1.8&f=release/wnako3.js&run"></script>
<!-- プラグインを取り込み -->
<script type="text/javascript" src="plugin_media.js"></script>

<!-- なでしこのプログラム -->
<script type="なでしこ">
// 初期設定 -- HTML内でなでしこを使うときは必ず実行する
結果領域＝「#div1」。
結果領域へDOM親要素設定。結果領域に「」をHTML設定。

// ここから，なでしこのプログラムを書いていきます



</script>
</body></html>
```

## 音を扱うプログラム(HTML+なでしこ)

テンプレートを使って，音を扱う命令を組み込んでみます。
今回作った命令は`音読込`，`音再生`，`音停止`，`音再開`の4つです。

```audio1.html
<!DOCTYPE html>
<html lang="ja">
<head>
  <meta charset="UTF-8">
  <title>なでしこ　音読込のテスト</title>
</head>

<body>
<!-- 画面レイアウトをあらかじめ作っておく -->
<h1>なでしこ３ブラウザ版で音を扱うテスト</h1>
<audio id="audio1"></audio>
<hr />
<div id="div1"></div>

<!-- なでしこ3のエンジンを取り込み -->
<script type="text/javascript" src="https://nadesi.com/v3/cdn.php?v=3.1.8&f=release/wnako3.js&run"></script>
<!-- プラグインを取り込み -->
<script type="text/javascript" src="plugin_media.js"></script>

<!-- なでしこのプログラム -->
<script type="なでしこ">
// 初期設定 -- HTML内でなでしこを使うときは必ず実行する
結果領域＝「#div1」。
結果領域へDOM親要素設定。結果領域に「」をHTML設定。

// ここから，なでしこのプログラムを書いていきます

// ----- あらかじめ埋め込んだaudio要素を使う方法
// audio要素に音声ファイルを指定しておく
「audio1」に「bgm1.mp3」を音読込。  // #1

// コントロールボタンの設置
ボタン１＝「再生」のボタン作成。
ボタン１をクリックした時には
　　「audio1」を音再生。  // #2
ここまで。

ボタン２＝「停止」のボタン作成。
ボタン２をクリックした時には
　　「audio1」を音停止。  // #3
ここまで。

ボタン３＝「再開」のボタン作成。
ボタン３をクリックした時には
　　「audio1」を音再開。  // #4
ここまで。

</script>
</body></html>
```

**#1**では，`<id名>に<ファイル名>を音読込`と書いています。あらかじめ組み込んでいる`id=audio1`のaudio要素に，ファイル名を設定しています。ローカルで作っている場合，ファイル名には`file:///d:/xxx.wav`形式も使えます（やったぜ！）。

**#2**，**#3**，**#4**では，指定したIDのaudio要素を操作しています。`音再生`では音声の開始位置を冒頭(0)に戻しています。`音再開`では，開始位置を戻さずに再生しています。

## plugin_media(試作)

そして，これらの命令を作ろうと思い，`plugin_media.js`を作成しました。音以外の命令も作ったので，一部だけ抜き出して載せます。

```plugin_media-audio1.js
// audio1.htmlに関係する部分のみ抜き出しています。

const PluginMedia = {
  // --- 音関係 ---
  '音読込': { // @id=aIDのaudio要素にaSrcファイルを読み込む // @オトヨミコミ
    // あらかじめaudio要素を設置しておく場合はこっち。
    type: 'func',
    josi: [['に'],['を']],
    fn: function (aID, aSrc, sys) {
      try {
        const audio = document.querySelector("#" + aID);
        audio.src = aSrc;
      } catch(e) {
	    // エラーを表示
	    window.alert('音読込 ' + e.message);
	    return -1;
      }
    }
  },
  
  '音再生': { // @id=aIDのaudio要素に設定されている音を頭から再生する // @オトサイセイ
    type: 'func',
    josi: [['を']],
    fn: function (aID, sys) {
      try {
        const audio = document.querySelector("#" + aID);
        audio.currentTime = 0;
        audio.play();
      } catch(e) {
        // エラーを表示
        window.alert('音再生 ' + e.message);
        return -1;
      }
    }
  },

  '音再開': { // @id=aIDのaudio要素に設定されている音を停止位置から再生する // @オトサイカイ
    type: 'func',
    josi: [['を']],
    fn: function (aID, sys) {
      try {
        const audio = document.querySelector("#" + aID);
        audio.play();
      } catch(e) {
        // エラーを表示
        window.alert('音再開 ' + e.message);
        return -1;
      }
    }
  },

  '音停止': { // @id=aIDのaudio要素に設定されている音を一時停止する // @オトテイシ
    type: 'func',
    josi: [['を']],
    fn: function (aID, sys) {
      try {
        const audio = document.querySelector("#" + aID);
        audio.pause();
      } catch(e) {
        // エラーを表示
        window.alert('音停止 ' + e.message);
        return -1;
      }
    }
  }
}

// モジュールのエクスポート(必ず必要)
if (typeof module !== 'undefined' && module.exports) {
  module.exports = PluginMedia
}
//プラグインの自動登録
if (typeof (navigator) === 'object') {
  navigator.nako3.addPluginObject('PluginMedia', PluginMedia)
}
```

特に難しいことをやっているわけではありませんが，Javascriptの命令をそれぞれ呼び出してみました。うまく動作しているので，これで大丈夫かと。

迷ったことは，冒頭にある`const PluginMedia`と，末尾にある`module.exports = PluginMedia`，`navigator.nako3.addPluginObject('PluginMedia', PluginMedia)`とで，`PluginMedia`という名前を全て揃えないと，正しく実行されないということです。

また，それぞれの命令に例外処理用に`try ～ catch(e) ～`を入れています。HTMLにPluginを組み込んで試していると，エラーが起こった時にconsoleを見ないと間違った箇所が分からなくて困ったので，アラートを表示するようにしています。不要ならば削除してもいいと思います。

## HTMLファイルをブラウザで表示してみる

ここまで作って，[HTMLファイルをブラウザで開く](https://oza-jv.github.io/audio1.html)と，画面にボタンが表示されます。そのボタンを押すと，音声ファイルを操作できます。

> ![pic2.PNG](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/942750/ee2fc907-02a8-4305-0557-5b3634f7e781.png)

## 動的にaudio要素を生成する

先ほどのプラグインに`音追加`という命令を加えます。こちらは，audio要素を新たに生成するための命令なので，HTMLファイルにaudio要素を組み込んでいなくても使えるようにしています。

```plugin_media-audio2.js
// 先ほどのplugin_media-audio1.jsの中に追加します。
const PluginMedia = {
  // 途中省略

  '音追加': { // @audio要素を追加して，aSrcファイルを読み込む // @オトツイカ
    // aIDを指定するとそれを親要素とする。省略するとbodyの子要素として追加。
    // 生成されたid名を返します。
    type: 'func',
    josi: [['を'],['へ', 'に']],
    isVariableJosi: true,
    return_none: true,
    fn: function (aSrc, ...pID) {
      try {
	      const sys = pID.pop();
	      var parent = document.body;
	      if ( pID.length > 0 ) {
	        parent = document.querySelector("#" + pID[0]);
	      };
	      const audio = document.createElement('audio');
	      audio.src = aSrc;
	      audio.id = 'nadesi-dom-' + sys.__v0['DOM生成個数'];
	      parent.appendChild(audio);
	      sys.__v0['DOM生成個数']++;
	      return audio.id;
	  } catch(e) {
		// エラーを表示
	    window.alert('音追加 ' + e.message);
	    return -1;
	  }
    }
  },

  // 以降省略
}
```

なでしこ側では，`<親id>へ<ファイル名>を音追加`と書きます。親IDの子要素としてaudio要素を追加し，指定したファイルを読み込みます。
親idを省略して`<ファイル名>を音追加`と書いた場合は，なでしこ側で「DOM親要素設定」しているidの子要素として追加されます。

### 引数を省略できるようにする

- 引数を省略できるようにしたかったので，`josi:`の値は`<親id>へ`の助詞を，一番最後にしています。ソースを見ると分かりますが，実は`<親id>に`と書いても動作します。また`isVariableJosi: true`が必要です（これを書いていなかったのでずっとハマった…）。
- 次の`fn: function (aSrc, ...pID)`の部分は，1つ目の引数`aSrc`（ファイル名を指定する引数）は省略できないので，変数を指定します。次の`...pID`は省略できるのでこのようにしています。JavaScriptではこれを**レストパラメータ**と言うそうです。
- ここでは，pID[ ]という配列に値が入っており，その末尾はシステム変数`sys`になっているので，自作関数の冒頭で`const sys = pID.pop();`を入れて，システム変数を取り出しています。
- すると，引数があった場合は配列`pID[ ]`から`sys`を取り出しても，まだ要素が残っているはずなので，次のif文では`pID.length`を使って`pID[ ]`の配列の要素数を数えます。`pID.length > 0`ならば，引数があった（つまり親idをプログラムから指定した）ということなので，変数`parent`にそれを設定する，ということです。
- 最後に`return audio.id;`とすることで，生成されたaudio要素のidを戻り値として取得できます。そのために`return_none: true`が必要です（これも書いてなかったのでハマった…）。

「なでしこ3」のプラグインは，引数を省略可能にしようと思ったら手強かったです…（JavaScriptに長けている方なら，カンタンだと思います）。でも上手くいったからよかった。

## HTMLファイルを修正する

この命令をテストしたかったので，HTMLファイルを次のように修正します。（なでしこの部分だけ抜き出しています）

```audio2.html
// ここから，なでしこのプログラムを書いていきます

// ----- 引数を省略して生成させる方法
ID＝「bgm1.mp3」を音追加。

// コントロールボタンの設置
ボタン１＝「再生」のボタン作成。
ボタン１をクリックした時には
　　IDを音再生。
ここまで。

ボタン２＝「停止」のボタン作成。
ボタン２をクリックした時には
　　IDを音停止。
ここまで。

ボタン３＝「再開」のボタン作成。
ボタン３をクリックした時には
　　IDを音再開。
ここまで。
改行作成。

// ----- 親要素idを指定する方法
ID2＝「div1」に「poka.wav」を音追加。

// コントロールボタンの設置
ボタン４＝「再生２」のボタン作成。
ボタン４をクリックした時には
　　ID2を音再生。
ここまで。
```

## HTMLファイルをブラウザで表示してみる

ここまで作って，[HTMLファイルをブラウザで開く](https://oza-jv.github.io/audio2.html)と，画面にボタンが表示されます。そのボタンを押すと，音声ファイルを操作できます。

## 最後に

ホントはやらなくてはいけない仕事が山のようにあるのですが，どうしてもプラグインを作りたくなってしまい，久々に夜更かししてしまいました。でも，なでしこv3ブラウザ版で，ChromeBookでも音を扱えるようになったら楽しいですね！　ということで，調子に乗って画像と動画も表示できるようにしたので，それは別の記事にします。

一応，音，画像，動画を扱えるようにした試作バージョン[plugin_media.js](https://oza-jv.github.io/plugin_media.js)も置いておきます。間違っていたらアドバイスをいただけると嬉しいです。

## 参考

- v3マニュアル [オーディオ](https://nadesi.com/v3/doc/index.php?plugin_browser%2F%E3%82%AA%E3%83%BC%E3%83%87%E3%82%A3%E3%82%AA&show)
- なでしこv3 [ユーザープラグイン](https://nadesi.com/v3/doc/index.php?%E3%83%A6%E3%83%BC%E3%82%B6%E3%83%BC%E3%83%97%E3%83%A9%E3%82%B0%E3%82%A4%E3%83%B3&show)
- [プラグインAPIの仕様](https://github.com/kujirahand/nadesiko3/blob/master/doc/plugins.md)