TL;DR
Gistに上げたのでそちらにどうぞ。
JavaScriptをWordPressサイトのどこかに上げてページ末尾(Audio Album本体のJSより後)で読み込ませるだけです。
https://gist.github.com/YamimakiReru/af7439cd32a4de77709b18d004fbd34e
ライブデモはうちの子の左サイドバー(と言い張る)。
サイドバーはスマホから見られないのでちゃんとWPの記事にしました。
Audio Album プラグインとは
WordPressにはAudio Albumという、オーディオファイルを埋め込んでコンパクトな再生コントロールで表示できるプラグインがあります。
サイドバーに埋め込んであります。なお、Audio Albumには自動再生機能はあるのですが、うちの子にはページ開いたとたん轟音を響かせるとかいう平成女児ムーブは仕込んでいません。あと、今はアナログ寄りになってしまいました手打ちDTMです。
実を言うと、WordPress は標準機能だけでオーディオファイルの埋め込みに対応しています。HTML5の<audio>タグはブラウザ実装が安定しており、動画のような環境差もほとんどありません。見た目や操作性に強いこだわりがなければ、WordPress標準のオーディオブロックだけでも十分実用的です。実際、音楽理論系のサイトなどでは標準機能で書かれていると思われる記事もよく見かけます。
ただし、WordPress標準のUIは単曲再生向けです。複数の曲を並べると曲間に余白ができたり、連続再生ができなかったりと、アルバム単位でまとめたい場合には少し物足りません。
Audio Album は歴史の長いプラグインで、今どきのGUI編集対応プラグインとは異なりショートコードベースのやや古い編集UX ですが、複数曲をコンパクトにまとめて表示できるという点では今でも有用です。
標準のAudio Album(v1.5.1)に足りない機能
本題。Audio Albumは画面表示についてはアルバムトラックを一つのグループとしてまとまりのある表示ができるのですが、(わたしの見落としでなければ)再生機能としてはやはり単曲再生がメインで、例えば次のような機能が欠けています。
- アルバム内のトラックをトラック順に続けて再生する機能
- autoplayやloopといったフラグは一応あるのですが、あくまでトラック単位です。トラックの再生が終わったときに次のトラックの再生を始めたりといった機能はありません
- アルバム内のトラックの音量を一律で調整する機能
- 音量調整はできますが、これもトラック単位です。あるトラックが爆音だったときそのトラックの音量を下げても、次のトラックの再生を始めるとまた爆音で鳴ります(まあ、再生ボタンを押す前に音量を下げるでしょうが)
他にも人によってはジャケ絵を表示する機能がないとか、ページを移動したりリロードしたりすると音量がデフォルト値にリセットされる1という点も気になるかもしれません。ヤミマキさんとしては先の2点がクリティカルだったのでそちらだけ対策しました。
今どきだったらYouTubeの再生リストを埋め込んだり
XFD(クロスフェードデモ)作ったり
こういった外部の便利機能や編集で足りるならわざわざプラグインと格闘するまでもないのかもしれませんが、わざわざ令和に個人サイトを作るなら平成レトロな雰囲気を醸し出したい❣ ということでアルバム再生機能としてクリティカルなところだけカスタマイズしました。
やったこと
Audio Album本体はHTML生成とCSSがほとんどでJSはほとんどないので、特に競合なども気にせずJSで<audio>要素をいじるだけです。<audio>要素はAPIも整理されているので、データ構造どうするか悩まされるようなツリーコントロール等よりはるかに楽ですね。
イベントリスナの登録
- 再生終了は HTMLAudioElement の'ended'イベント
- 音量変更は 'volumechange' イベント
でハンドルできます。
init() {
this.audios = Array.from(this.album.querySelectorAll('audio'));
for (const a of this.audios) {
a.addEventListener('ended', this.onAudioEnded.bind(this));
a.addEventListener('volumechange', this.onVolumeChange.bind(this));
}
}
bind()は親メソッドをベースにメソッド内のthisを第1引数に固定するものです。これでクロージャ構文を書かずに自作クラスのインスタンスメソッドでイベントハンドリングできるようになります。
ちなみにbind()せずクロージャも書かずにel.addEventListener()の第2引数に渡すと、そのメソッド内のthisはDOMノードelになる。これはこれで便利なときがある2ので、分かってる人は使いますが。
前の曲の再生が終わったら、次の曲の再生を始める
<audio>要素のリストから次の要素を探してHTMLAudioElement.play()を呼ぶだけの簡単なお仕事です。
ただし、Chromeでは前の曲が完全に止まる前に次の曲を再生しようとすると例外が飛ぶのでちょっとインターバルを置く必要がありました。Chromeではendedイベントは、「曲の再生は終わった」が「ブラウザの内部的なクリーンアップ処理はまだ」という状態でイベントが発火します。
onAudioEnded(ev) {
/** @type {HTMLAudioElement} */
const a = ev.target;
const nextIndex = (1 + this.audios.findIndex(a2 => a2 == a)) % this.audios.length;
// An exception occurs unless the previous audio has completely stopped.
setTimeout(() => {
this.audios[nextIndex].play();
}, 100);
}
... % this.audios.length は最後の曲に到達したあと頭へ戻すための計算です。最後の曲のインデックス+1を曲数で割った余りを求める計算になるからゼロになる。
正直ちゃんとif文を書いたほうが予後は良いのですが……。
どれか一つの音量を変えたら、アルバム内の全曲の音量を連動させる
音量は HTMLAudioElement.volume、ミュートフラグは... .mutedなので、起点要素の値を他の<audio>要素に代入するだけです。簡単ですね。
/** @type {HTMLAudioElement} */
const a = ev.target;
for (const a2 of this.audios) {
if (a == a2) {
continue;
}
a2.volume = a.volume;
a2.muted = a.muted;
}
ただしvolumechangeイベントはスクリプトからvolumeをいじったときにも当然に発火するので、上記の処理だけだと無限ループになります。
なのでセマフォを獲得して……とかやる必要もなく、JavaScriptは協調型マルチタスクのシングルスレッドモデルなのでフラグ変数をon/offするだけでOKです。仮にboolean値の代入がアトミックでないシステムだったとしても変数が壊れたりしません3。楽ですね。
onVolumeChange(ev) {
if (this.isSyncing) {
return; // 既に音量同期処理中だったら何もしない
}
this.isSyncing = true; // 音量の同期処理を開始する
try {
// ... 音量を同期させる処理
} finally {
// 念の為、イベントプロパゲーションが止まるのを待ってからフラグを下ろす
Promise.resolve().then(() => {
this.isSyncing = false;
});
}
}
この手の同期変数とかI/O関係のリソースは、獲得したら即try-finallyブロックを書いて開放漏らしのないようにするのが鉄則ですね。ただfinallyブロックで変なことをやるとたまにその前に飛んできた例外が潰れるんだけど。
と、当たり前のことを長々書いてもこれだけの文章量にしかならない。HTMLAudioElementのAPIがよく整理されていることが分かります。
画像処理なんかと比べてもオーディオ処理は一次元のベクトル量×せいぜいLR2チャンネルだし音楽は物理学的にも音楽的にも割と理論的に整理されている分野なので(EQ? コンプ? 知らない子ですね……)お勉強が効きやすいです。
音声処理はいいぞ。心が豊かになる。
追記: SWELL+Audio Albumで生じる問題のWorkaround
WordPressテーマSWELL(v2.16.0)とAudio Album(v1.5.1)を併用した場合、サイドバーとかだと問題ないのですが、本文中にAudio Albumのショートコードを入れるとSWELLのCSSとぶつかって問題が起きます。うーんCSS Hell.
SWELL 2.16.0 と Audio Album 1.5.1 だと次のような CSS を当てれば直りました。
.post_content h2.audioalbum:where(:not([class^="swell-block-"]):not(.faq_q):not(.p-postList__title)) {
background:initial;
}
.post_content h2.audioalbum:where(:not([class^="swell-block-"]):not(.faq_q):not(.p-postList__title))::before {
border: initial;
}
.post_content .albumtrack div>button:first-child {
margin-top: 10px !important;
}
.post_content .albumtrack div>.mejs-time-total:first-child {
margin-top: 5px !important;
}

