audioタグだけでもオーディオ再生はできます。ただ、利用者の環境によって変化する事のないシンプルなUIにしたかったので、少し調べる事にしました。 結果、概ねイメージ通りの仕上がりとなりましたが、割と手間が掛かったので残して置こうかと。
audioタグは、再生機能を提供してくれるだけではなく、JavaScriptを使えばオーディオデータの状態「state」を確認したり、再生する際に所々で「event」を投げてくれるので、「state」、「event」から多様な処理を実装できます。
★2022/6/12 更新★ iPhone(15.4) safariで2度目の再生は、loadなし再生ができるようになった事を確認。 どうやら再生時にオーディオ全体の長さ(duration)が取得できるようになった模様。全デバイスでload一回、複数再生できるようになりました。 Windows Edgeでpause後continueした際の初っ端が、クリップぽっくなるのが気になるが、html5なのでブラウザ毎に動作が異なるのは致し方なしか。
★2022/5/1 更新★ iPhone(15.4.1) safariで2度目再生ができない事象が発生。 再生時にオーディオ全体の長さ(duration)が取得できずInfinityとなった場合に発生する為、play()ではなくload()する仕様に変更しました。 全デバイスでのload一回、複数再生を目標としてましたが、これにより残念ながらiPhone/iPadが例外となってしまいました。
★2021/11/19 更新★ iPhone(15.1) safariで再生が永遠に終わらない事象が発生した為、一部機能を修正しました。 再生時にオーディオ全体の長さ(duration)が取得できずInfinityとなる為、timeupdateイベントのリスナーを追加しています。
★2021/09/26 更新★ iPhone safariでの再生ができなくなっていたので、一部機能を修正しました(iPadは特に問題は無く、iPhoneだけで発生)。 オーディオデータの読み込み後、初回再生はplaySoundFirst()、2回目以降は、playSound()を呼ぶことでiPhoneでのcurrentTimeによる問題(?)を回避しています。
audioタグ
audio: 埋め込み音声要素 - HTML: HyperText Markup Language | MDN
event
audio: 埋め込み音声要素#event - HTML: HyperText Markup Language | MDN
state
HTMLMediaElement.readyState - Web API | MDN
HTMLMediaElement.networkState - Web API | MDN
実現したかった事
- 音量調整やシークバーは表示せず、play機能とpause機能をまとめたボタン1つのみを、UIデザインに合わせた形で配置
- オーディオデータの読み込みは最初の一回で、再生都度の読み込みはしない(iPhone/iPadは使用上、都度読み込みとなってしまいました)
手間の掛かった所
何が面倒かというと、OSやブラウザ毎の差異です。
OSやブラウザ毎で、概ね「event」を投げるタイミングに差異はありませんでしたが、「state」に差異があった為、これを吸収する必要があります。
調べた結果(OSブラウザ差異)
audioタグのpreloadには、auto、metadata、noneの3つを設定できるのですが、この設定により各OS、ブラウザでのページ表示初期の「state」が異なります。調べた結果は下表の通りです。
audioタグ preload |
OS+ブラウザ | readyState | networkState |
---|---|---|---|
none | iPhone/iPad+Safari | 0:HAVE_NOTHING | 1:NETWORK_IDLE |
^ | Windows 10+edge | ^ | ^ |
^ | Windows 10+firefox | ^ | ^ |
^ | Windows 10+chrome | ^ | ^ |
metadata | iPhone/iPad+Safari | 0:HAVE_NOTHING | 2:NETWORK_LOADING |
^ | Windows 10+edge | 1:HAVE_METADATA | 1:NETWORK_IDLE |
^ | Windows 10+firefox | ^ | ^ |
^ | Windows 10+chrome | ^ | ^ |
^ | Windows 10+eclipse | ^ | ^ |
auto | iPhone/iPad+Safari | 4:HAVE_ENOUGH_DATA | 1:NETWORK_IDLE |
^ | Windows 10+edge | ^ | ^ |
^ | Windows 10+firefox | ^ | ^ |
^ | Windows 10+chrome | ^ | ^ |
^ | Windows 10+eclipse | ^ | ^ |
- iPhone(14.4 (18D52))/iPad(14.4.2) Safari
- Windows edge バージョン 89.0.774.57 (公式ビルド) (64 ビット)
- Windows firefox 86.0.1 (64 ビット)
- Windows chrome バージョン: 89.0.4389.90(Official Build)(64 ビット)
実装方針
OSブラウザ差異がないのは「none」か「auto」ですが、「auto」はネットワーク環境による変動が想定される為、「none」で進める事にしました。 ただし、「none」の場合、eclipseのブラウザでは、JavaScriptでaudio操作が出来なくなる(仕様?)ので動作確認の際には、外部ブラウザの利用が必須です。
サイト全体はAdminLTE(https://adminlte.io)をベースに、アイコンはfontawsome(https://fontawesome.com)を利用してます。
コード
実は、preloadを「none」に決めてしまった後は「state」を意識する事なく実装できました。
html
<audio id="sound" src="sound.mp3" preload="none"> Your browser does not support the <code>audio</code> element. </audio> <div class="form-group player"> <a class="btn btn-app"><i class="fas fa-circle-notch fa-spin"></i>initialize</a> </div>
<script> var sound = document.getElementById("sound"); var max_duration = 10; // max sound length sound.autoplay = false; var elemPlayer = divSound.getElementsByClassName('form-group player')[sound_no]; var buttonLoad = '<a class="btn btn-app"><i class="fas fa-circle-notch fa-spin" onclick="playSound()"></i>Loading...</a>'; var buttonPlay = '<a class="btn btn-app"><i class="fas fa-play" onclick="playSound()"></i>Play</a>'; var buttonStop = '<a class="btn btn-app"><i class="fas fa-stop" onclick="stopSound()"></i>Stop</a>'; var buttonPause = '<a class="btn btn-app"><i class="fas fa-pause" onclick="pauseSound()"></i>Pause</a>'; var buttonCont = '<a class="btn btn-app"><i class="fas fa-play" onclick="playSound()"></i>Continue</a>'; var buttonLoadPlay = '<a class="btn btn-app"><i class="fas fa-play" onclick="loadSound()"></i>Load & Play</a>'; var buttonAlert = '<a class="btn btn-app"><i class="fas fa-exclamation-triangle" onclick="loadSound()"></i>error</a>'; function initListener() { /* * readyState=0:HAVE_NOTHING * networkState=0:NETWORK_EMPTY * preload=none init state for win eclipse * networkState=1:NETWORK_IDLE * preload=none init state for win edge,firefox,chrome * preload=none init state for iphone/ipad safari * networkState=2:NETWORK_LOADING * preload=metadata init state for iphone/ipad safari * networkState=3:NETWORK_NO_SOURCE * resource not found * readyState=1:HAVE_METADATA * networkState=1:NETWORK_IDLE * preload=metadata init state for win edge(? ...could be) * networkState=2:NETWORK_LOADING * readyState=2:HAVE_CURRENT_DATA * readyState=3:HAVE_FUTURE_DATA * networkState=2:NETWORK_LOADING event=loadeddata ready to play * readyState=4:HAVE_ENOUGH_DATA * networkState=2:NETWORK_LOADING event=loadeddata ready to play * networkState=1:NETWORK_IDLE event=loadeddata ready to play * preload=metadata init state for win edge,firefox,chrome * preload=auto init state for win edge,firefox,chrome * preload=auto init state for iphone/ipad safari * */ sound.addEventListener("error", function(){ console.log("[event] error"); message = sound.src; console.log(message); message = "readyState :" + sound.readyState; console.log(message); message = "networkState:" + sound.networkState; console.log(message); message = "err=" + sound.readyState +":"+sound.networkState button = '<a class="btn btn-app"><i class="fas fa-exclamation-triangle"></i>'+message+'</a>';; elemPlayer.innerHTML = button; }, false); sound.addEventListener("emptied", function(){ console.log("[event] emptied"); }, false); sound.addEventListener("durationchange", function(){ console.log("[event] durationchange"); message = sound.src; console.log(message); message = "readyState :" + sound.readyState; console.log(message); message = "networkState:" + sound.networkState; console.log(message); }, false); sound.addEventListener("loadstart", function(){ console.log("[event] loadstart"); message = sound.src; console.log(message); message = "readyState :" + sound.readyState; console.log(message); message = "networkState:" + sound.networkState; console.log(message); }, false); sound.addEventListener("loadeddata", function(){ console.log("[event] loadeddata"); message = sound.src; console.log(message); message = "readyState :" + sound.readyState; console.log(message); message = "networkState:" + sound.networkState; console.log(message); message = "load err=" + sound.readyState +":"+sound.networkState buttonError = '<a class="btn btn-app"><i class="fas fa-exclamation-triangle" onclick="location.reload()"></i>'+message+'</a>'; if (sound.readyState === 4){ if (sound.networkState === 1 || sound.networkState === 2){ playSound(); } else { elemPlayer.innerHTML = buttonError; } } else if (sound.readyState === 3){ if (sound.networkState === 2){ playSound(); } else { elemPlayer.innerHTML = buttonError; } } else { elemPlayer.innerHTML = buttonError; } }, false); sound.addEventListener("play", function(){ console.log("[event] play"); message = sound.src; console.log(message); message = "readyState :" + sound.readyState; console.log(message); message = "networkState:" + sound.networkState; console.log(message); elemPlayer.innerHTML = buttonPause; }, false); sound.addEventListener("pause", function(){ console.log("[event] pause"); message = sound.src; console.log(message); message = "readyState :" + sound.readyState; console.log(message); message = "networkState:" + sound.networkState; console.log(message); if (sound.duration == Infinity){ if (sound.currentTime == 0){ elemPlayer.innerHTML = buttonLoadPlay; } else { elemPlayer.innerHTML = buttonCont; } } else { if (sound.currentTime == sound.duration){ elemPlayer.innerHTML = buttonStop; } else { elemPlayer.innerHTML = buttonCont; } } }, false); sound.addEventListener("ended", function(){ console.log("[event] ended"); message = sound.src; console.log(message); message = "readyState :" + sound.readyState; console.log(message); message = "networkState:" + sound.networkState; console.log(message); elemPlayer.innerHTML = buttonPlay; }, false); sound.addEventListener("timeupdate", function(){ console.log("[event] timeupdate:"+sound.currentTime); if (sound.currentTime === 0){ if (sound.duration === Infinity){ stopSound(); } } if (sound.currentTime > max_duration){ stopSound(); } }, false); } function playSound() { console.log("[function] playSound()"); sound.play(); } function stopSound() { console.log("[function] stopSound()"); sound.pause(); sound.currentTime=0; } function pauseSound() { console.log("[function] pauseSound()"); sound.pause(); } function loadSound() { console.log("[function] loadSound()"); sound.load(); message = sound.src; console.log(message); message = "readyState :" + sound.readyState; console.log(message); message = "networkState:" + sound.networkState; console.log(message); elemPlayer.innerHTML = buttonLoad; } console.log("[onload] initListener"); initListener(); message = "init err=" + sound.readyState +":"+sound.networkState buttonError = '<a class="btn btn-app"><i class="fas fa-exclamation-triangle" onclick="location.reload()"></i>'+message+'</a>';; initButton = buttonError; if (sound.readyState === 0){ if (sound.networkState === 1){ //elemPlayer.innerHTML = buttonLoadPlay; initButton = buttonLoadPlay; } else if (sound.networkState === 3){ //elemPlayer.innerHTML = buttonLoadPlay; initButton = buttonLoadPlay; } } elemPlayer.innerHTML = initButton;
実装結果
先日公開したrsynth.netのsound libraryとして実装しています。 sound.rsynth.net
rsynth.netについては、こちらを omittedsounds.hatenablog.com