【File API】JavaScriptでローカル上の任意の音声データを再生する

先日、私が公開している[やまだのタイマー](https://r-40021.github.io/countdown-timer/)にアラーム音を任意のものに変更できる機能を追加しました。実装するうえで色々調べたのですが、File APIで音声ファイルを扱う記事が少ない、というかほぼなかったので、自分で記事にしてみようと思いました。
※ここでご紹介するコートはご自由に使っていただいて結構です。クレジットも不要です。ただし、動作の保証は致しかねます。
# DEMO
<p class="codepen" data-height="265" data-theme-id="dark" data-default-tab="html,result" data-user="r-40021" data-slug-hash="oNYKYLe" style="height: 265px; box-sizing: border-box; display: flex; align-items: center; justify-content: center; border: 2px solid; margin: 1em 0; padding: 1em;" data-pen-title="File APIで音を鳴らす">
  <span>See the Pen <a href="https://codepen.io/r-40021/pen/oNYKYLe">
  File APIで音を鳴らす</a> by Kenichi Yamada (<a href="https://codepen.io/r-40021">@r-40021</a>)
  on <a href="https://codepen.io">CodePen</a>.</span>
</p>
<script async src="https://cpwebassets.codepen.io/assets/embed/ei.js"></script>
「音声ファイルを選択」をクリックして音声ファイルを選択後、「再生」をクリックすると、選択した音がなります。
こんな感じのものを作っていこうと思います。
# 元となるHTML
初めに、元となるHTMLを作成します。今後、このHTMLに加筆を行ってください。

```HTML:index.html
<html>
<head>
    <meta charset="UTF-8">
    <title>TEST</title>
</head>
<body>
 <div class="select">
    <input id="file1" type="file" accept=".mp3,.m4a,.aac,.wav,.flac">
    <button onclick="play()" class="btn active">再生</button>
  </div>
</body>
</html>
```

これをブラウザで開くと以下のようになります。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1172197/4616bdb1-b6d6-c8d6-3a19-5a488ab27a08.png)
この状態で「ファイルを選択」をクリックするとエクスプローラーが開くことを確認してください。
### HTMLの解説
`<input id="file1" type="file" accept=".mp3,.m4a,.aac,.wav,.flac">`でエクスプローラーを起動するボタンができます。意外と簡単。ダサいと思われるかもしれませんが、それについてはこの記事の最後で説明します。
`id="file1"`はJavaScriptやCSSで使います。
`accept=".mp3,.m4a,.aac,.wav,.flac"`で、エクスプローラーに表示するファイル形式を指定しています。
`accept="audio/*"`のほうが簡単ですが、iPhoneで正常に動作しないので、あえて拡張子を指定しています。
# プログラムの構成
こんな感じのプログラムを組んでいきます。
1. File APIで、ローカル上の任意の音声データのパスを取得する
2. 「再生」ボタンをクリックしたら、1．で取得したパスの音声を再生
# File APIでローカル上の任意の音声データのパスを取得する
最初にコード全体を見せます。

```HTML:index.html
<!--省略-->
<body>
 <div class="select">
    <input id="file1" type="file" accept=".mp3,.m4a,.aac,.wav,.flac">
    <button onclick="play()" class="btn active">再生</button>
  </div>
    <script>
    var alarm;
    alarm = new Audio();
    window.addEventListener('load', () => {
      const f = document.getElementById('file1');
      f.addEventListener('change', evt => {
        let input = evt.target;
        if (input.files.length == 0) {
          return;
        }
        const file = input.files[0];
        if (!file.type.match('audio.*')) {
          alert("音声ファイルを選択してください。");
          return;
        }
        const reader = new FileReader();
        reader.onload = () => {
          alarm.pause();
          alarm.src = reader.result;
        };
        reader.readAsDataURL(file);
      });
    });

    function play() {
      alarm.play();
    }
  </script>
</body>

</html>
```
### 解説

```JavaScript
f.addEventListener('change', evt => {
/*省略*/
});
```

`f`、つまりHTMLの「ファイルを選択」ボタンの要素の内容が変更されたら実行するようにイベントを設定しています。
また、このイベントの中身では、File APIをコネコネしています。この記事では詳しく説明しないので、気になる方は以下のサイトをご覧ください。
[File - Web API | MDN](https://developer.mozilla.org/ja/docs/Web/API/File)

```JavaScript
if (!file.type.match('audio.*')) {
     /*音声ファイルではなかったとき*/
     alert("音声ファイルを選択してください。");
     return;
}
```

一応、HTMLの方でもエクスプローラーに音声ファイルのみを表示するように設定しましたが、簡単にすり抜けることが可能なので、JavaScriptでもチェックしています。もし音声ファイルではなかったら、アラートを出し、プログラムの実行を中断します。
こちらは、HTMLの`accept="audio/*"`とは違って、拡張子を指定しなくてもiPhoneで動きます。

```JavaScript
const reader = new FileReader();
/*省略*/
reader.readAsDataURL(file);
```

ここで出てくる`new FileReader()`で、選択したファイルのURLやテキストなどを取得するためのオブジェクトを作成しています。`readAsDataURL`で選択したファイルのURL（パス）を取得します。
※実際にはURLとは異なるのですが、ほぼ同じようなものなのでここでは「URL」と表現しています。

```JavaScript
reader.onload = () => {
	alarm.pause();
	alarm.src = reader.result;
};
```

先ほど作成したオブジェクトが読み込まれたら、`alarm`の`src`を変更しています。
普段は、`alarm.src = "alarm.mp3"`のように、再生したいファイルのURLを指定するのに使います。

# 「再生」ボタンで音声を再生
```HTML:index.html
<!--省略-->
<body>
    <input id="file1" type="file" accept="audio/*">
    <button onclick="play()">再生</button>
    <script>
 	/*省略*/
	function play(){
            alarm.play();
        }
    </script>
</body>

</html>
```
おなじみのやつです。「再生」ボタンを押したら`play()`を実行するようになっています。

# 【おまけ】「ファイルを選択」ボタンがダサいので変更する
デフォルトの「ファイルを選択」ボタンのデザインを変更するには、以下のようにします。
`<input id="file1" type="file" accept="audio/*">`の直後に以下のコードを記述してください。

```HTML:index.html
<label for="file1"><!--ここに「ファイルを選択」ボタンの代わりとなるものを記述（テキストでも、画像でも...）--></label>
```

~~[やまだのタイマー](https://r-40021.github.io/countdown-timer/)では[Font Awesome](https://fontawesome.com/)のアイコンを設定しました。~~
[2021/06/20　追記]
現在、やまだのタイマーでFont Awesomeのアイコンは使用されていません。

他にも、Googleが提唱する[マテリアルデザインのアイコン](https://material.io/resources/icons/?style=baseline)もあります。
しかし、ここで問題発生。このままではデフォルトのボタンも表示されてしまうので、CSSで隠します。

```HTML:index.html
<html>
<head>
    <style>
        #file1 {
            display:none;
	}
    </style>
    <!--省略-->
</head>
<!--省略-->
```

# ついでに他の部分もかっこよくする
まず、HTMLのコードを以下のように書き換えてください
【書き換え前】

```html:index.html
<button onclick="play()" class="btn active">再生</button>
```

【書き換え後】

```html:index.html
<a onclick="play()" class="btn active">再生</a>
```

次に、CSSを追加していきます。

```HTML:index.html
<html>
<head>
    <style>
        #file1 {
            display:none;
	}
    /*ここから追加分*/
    body {
      font-family: sans-serif;
    }

    .select {
      display: flex;
      justify-content: flex-start;
    }

    .select label,
    .select .btn {
      cursor: pointer;
    }

    .select .btn {
      position: relative;
      display: inline-block;
      font-weight: bold;
      padding: 0.25em 0.5em;
      text-decoration: none;
      color: #00bcd4;
      background: #ececec;
      transition: 0.2s;
      white-spacing: 10;
      border: 0.5px solid #bdbdbd;
      border-right: 0;
    }

    .select .btn.active {
      background: #00bcd4;
      color: #ececec;
    }

    .select .btn:hover {
      opacity: 0.4;
    }

    .select .btn:first-of-type {
      border-radius: 10px 0 0 10px;
    }

    .select .btn:last-of-type {
      border-radius: 0 10px 10px 0;
      border: 0.5px solid #bdbdbd;
    }
    /*追加分はここまで*/
    </style>
    <!--省略-->
</head>
<!--省略-->
```

# コード全体
完成形はこんな感じになります。

```HTML:index.html
<html>

<head>
  <title>TEST</title>
  <style>
   
    #file1 {
      display: none;
    }
    body {
      font-family: sans-serif;
    }

    .select {
      display: flex;
      justify-content: flex-start;
    }

    .select label,
    .select .btn {
      cursor: pointer;
    }

    .select .btn {
      position: relative;
      display: inline-block;
      font-weight: bold;
      padding: 0.25em 0.5em;
      text-decoration: none;
      color: #00bcd4;
      background: #ececec;
      transition: 0.2s;
      white-spacing: 10;
      border: 0.5px solid #bdbdbd;
      border-right: 0;
    }

    .select .btn.active {
      background: #00bcd4;
      color: #ececec;
    }

    .select .btn:hover {
      opacity: 0.4;
    }

    .select .btn:first-of-type {
      border-radius: 10px 0 0 10px;
    }

    .select .btn:last-of-type {
      border-radius: 0 10px 10px 0;
      border: 0.5px solid #bdbdbd;
    }
  </style>
</head>

<body>
  <div class="select">
    <input id="file1" type="file" accept=".mp3,.m4a,.aac,.wav,.flac">
    <a class="btn"><label for="file1" id="audioInput">ファイルを選択<label></a>
    <a onclick="play()" class="btn active">再生</a>
  </div>
   <script>
    var alarm;
     alarm = new Audio();
    window.addEventListener('load', () => {
      const f = document.getElementById('file1');
      f.addEventListener('change', evt => {
        let input = evt.target;
        if (input.files.length == 0) {
          return;
        }
        const file = input.files[0];
        if (!file.type.match('audio.*')) {
          alert("音声ファイルを選択してください。");
          return;
        }
        const reader = new FileReader();
        reader.onload = () => {
          alarm.pause();
          alarm.src = reader.result;
        };
        reader.readAsDataURL(file);
      });
    });

    function play() {
      alarm.play();
    }
  </script>
      </body>
    </html>
```

今回はわかりやすさを優先してJavaScriptやCSSをHTMLの中に突っ込みましたが、もちろんJavaScriptやCSSを外部ファイル化してもOKです。
外部ファイル化すると以下のようになります。

```HTML:index.html
<html>
<head>
    <meta charset="UTF-8">
    <title>TEST</title>
    <!--CSS読み込み-->
    <link rel="stylesheet" type="text/css" href="style.css">
</head>

<body>
  <div class="select">
    <input id="file1" type="file" accept=".mp3,.m4a,.aac,.wav,.flac">
    <a class="btn"><label for="file1" id="audioInput">ファイルを選択<label></a>
    <a onclick="play()" class="btn active">再生</a>
  </div>
    <!--JavaScript読み込み-->
    <script src="script.js"></script>
  </body>
</html>
```

```JavaScript:script.js
var alarm;
alarm = new Audio();
window.addEventListener("load", () => {
  const f = document.getElementById("file1");
  f.addEventListener("change", (evt) => {
    let input = evt.target;
    if (input.files.length == 0) {
      return;
    }
    const file = input.files[0];
    if (!file.type.match("audio.*")) {
      alert("音声ファイルを選択してください。");
      return;
    }
    const reader = new FileReader();
    reader.onload = () => {
      alarm.pause();
      alarm.src = reader.result;
    };
    reader.readAsDataURL(file);
  });
});

function play() {
  alarm.play();
}
```

```CSS:style.css
#file1 {
  display: none;
}
body {
  font-family: sans-serif;
}

.select {
  display: flex;
  justify-content: flex-start;
}

.select label,
.select .btn {
  cursor: pointer;
}

.select .btn {
  position: relative;
  display: inline-block;
  font-weight: bold;
  padding: 0.25em 0.5em;
  text-decoration: none;
  color: #00bcd4;
  background: #ececec;
  transition: 0.2s;
  white-spacing: 10;
  border: 0.5px solid #bdbdbd;
  border-right: 0;
}

.select .btn.active {
  background: #00bcd4;
  color: #ececec;
}

.select .btn:hover {
  opacity: 0.4;
}

.select .btn:first-of-type {
  border-radius: 10px 0 0 10px;
}

.select .btn:last-of-type {
  border-radius: 0 10px 10px 0;
  border: 0.5px solid #bdbdbd;
}

```

いかがでしたか？少しでもお役に立てたならば幸いです。
# 参考サイト
- [File - Web API | MDN](https://developer.mozilla.org/ja/docs/Web/API/File)
- [File APIを使ってローカルのファイルを読み込んでみる](https://www.tam-tam.co.jp/tipsnote/javascript/post11736.html)
- [JavaScript でファイルを読み込む方法](https://javascript.keicode.com/newjs/how-to-read-file-with-file-api.php)

# 筆者紹介
**やまだけんいち(Kenichi Yamada)**
インターネット上で活動している、匿名のプログラマーです。Webアプリを開発しています。

- [Twitter](https://twitter.com/r_40120)
- [GitHub](https://github.com/r-40021)