100日後に完成するゲームシステム 70日目「WEBにおける音声再生の壁」

2021年4月25日

テクノロジー 特集

eyecatch インターネットにおけるメディア・コンテンツの壁に、いつも悩まされる、弓削田です。 スマートフォンに限らないのですが、動画コンテンツよりも、音声コンテンツを、 webサービスとして動的に扱うことが非常に困難に感じるのは、自分だけでしょうか? 企業のwebサイトなどで、トップページを開いたら、いきなり素敵な動画が流れて、 企業イメージをアピールするような光景を最近はよく見かけますが、 音声をいきなり流すというのは、webブラウザは、未来に進むほど、NGの方向になってきています。

秩序とリテラシは平行進行

かつて、無秩序の時(インターネット黎明期)は、webページを開いたら、自分の好きな音楽を 流しまくっていた、個人webサイトなども、山のようにありましたが、 著作権の問題やら、ブラウザリテラシの問題に加え、スマートフォンで、自宅以外で、webブラウザを 閲覧するタイミングが増えてきたことも、加味されて、 今では、webブラウザを開いてそのまま音声メディアを再生することはできなくなっています。 (2021年4月現在) 確かに、電車などで誰かのブログに書かれている、おもしろページを見ようとクリックした時に、 スマホから意図しない音声が聞こえだしたら、周囲の目が一斉にその人に向いてしまうほど 注目を浴びてしまうでしょう。

音声メディアの需要

webブラウザゲームを作る時に、演出に拘った表現をしたい時や、 通常のコンシューマゲームのように、そのまま表現しようとすると、 おそらく音声再生のタイミングでエラーが発生します。 そもそも、ゲームにおける音声メディアは、SEと呼ばれる、ボタンのクリック音や、 キャラクターの動作音、いろいろな環境音など、数秒の音を細かくたくさん鳴らすというのが一般的ですが、 こうしたSE音源と、BGMと言われる、背景で流れる音楽で、よくPCMという風に表現されますが、 この2種類の音メディアを動的に扱う必要があります。 でも、javascriptのAudioAPIは、webページが表示されてから、何かしらのクリックやタッチイベントが 発生するまでは、使用できないようになっています。 なので、ユーザーインターフェイスを駆使して、 クリックやタッチイベントが発生したタイミングの最初に、一気に音声メディア処理を行うようにしなければいけません。

ブラウザの音声再生処理のアルゴリズム

とりあえず、このゲームではどういうロジックで音メディアを扱っているか、解説してみたいと思います。 まず、ページを表示したタイミングは、ゲームをスタートするタイミングなので、タイトル画面を表示します。 そして、何かしらのボタンやキーボード操作などが行われるはずなので、その時点で、ゲームはスタートするようにしています。 同時に音メディア処理ですね。 でも、音メディアのloading事態は、ページ読み込みと同時に行っています。 音コンテンツへのアクセスができないだけなので、クリックしたタイミングで、コンテンツアクセスすればいいんですね。 とりあえず、以下にコードを載せてみましたので、お好きにコピペしてお使いください。 ※音声ファイルは、手元の物をご利用ください。(jsファイルの最初にリストセットしてあります) <!DOCTYPE html> <html> <head> <meta charset="utf-8"> <meta name="viewport" content="width=device-width, initial-scale=1, user-scalable=no"> <title>Javascript-Audio-API</title> <link rel="stylesheet" href="audioapi.css"> <script src="audioapi.js"></script> </head> <body> <h1>Javascript-Audio-API</h1> <div class="lists"> </div> </body> </html> html,body{ width:100%; height:100%; padding:0; margin:0; border:0; overflow:hidden; } .lists{ width:100%; position:relative; display:block; transform-origin:top left; transform:scale(1.0); white-space:normal; } *, *:before, *:after{ -webkit-box-sizing: border-box; -moz-box-sizing: border-box; -o-box-sizing: border-box; -ms-box-sizing: border-box; box-sizing: border-box; } button{ margin:8px; height:50px; width:100px; cursor:pointer; color:black; border:1px solid black; border-radius:4px; background-color:#eee; } button:hover{ color:red; } button[data-active="1"]{ background-color:#eef; } (function(){ let lists = { "1" : {"path" : "audio/1.mp3" , "name" : "audio-1" }, "2" : {"path" : "audio/2.mp3" , "name" : "audio-2" }, "3" : {"path" : "audio/3.mp3" , "name" : "audio-3" }, "4" : {"path" : "audio/4.mp3" , "name" : "audio-4" }, "5" : {"path" : "audio/5.mp3" , "name" : "audio-5" }, "6" : {"path" : "audio/6.mp3" , "name" : "audio-6" }, "7" : {"path" : "audio/7.mp3" , "name" : "audio-7" }, "8" : {"path" : "audio/8.mp3" , "name" : "audio-8" }, "9" : {"path" : "audio/9.mp3" , "name" : "audio-9" }, "10" : {"path" : "audio/10.mp3" , "name" : "audio-10"}, "11" : {"path" : "audio/11.mp3" , "name" : "audio-11"} }; let MAIN = function(){ this.data = {}; this.load(); this.event(); }; MAIN.prototype.event = function(){ let elms = document.querySelectorAll("button[data-type]"); for(let elm of elms){ elm.addEventListener("click" , this.click.bind(this)); } }; MAIN.prototype.key2path = function(key){ return "audio/"+key+".mp3"; }; MAIN.prototype.click = function(e){ let target = e.target; let key = target.getAttribute("data-type"); this.play(key); }; // ---------- // Audio API MAIN.prototype.load = function(){ for(let key in lists){ this.add_button(key); let path = lists[key].path; var req = new XMLHttpRequest(); req.open('GET', path, true); req.responseType = 'arraybuffer'; req.onload = this.loaded.bind(this , path , key); req.send(null); } }; MAIN.prototype.loaded = function(path , key , e){ this.data[path] = { res : e.target.response }; this.set_audioAPI(e.target.response , key); this.active_button(path); }; MAIN.prototype.play = function(key){ let path = this.key2path(key); this.play2(this.data[path].context , this.data[path].buffer); }; MAIN.prototype.set_audioAPI = function(responce , key){ let path = this.key2path(key); let context = new (window.AudioContext || window.webkitAudioContext)(); this.data[path].context = context; context.decodeAudioData(responce, (function(buffer){ this.data[path].buffer = buffer; }).bind(this)); console.log(path); }; MAIN.prototype.set_webkitAudioAPI = function(responce){ let context = new (window.AudioContext || window.webkitAudioContext)(); context.decodeAudioData(responce, (function(context , buffer){ context.buffer = buffer; let gainSource = context.createBufferSource(); gainSource.buffer = buffer; let gainNode = context.createGain(); gainNode.connect(context.destination); gainSource.connect(gainNode); context.resume(); gainSource.start(0,0); }).bind(this , context)); }; MAIN.prototype.play2 = function(context , buffer){ let gainSource = context.createBufferSource(); gainSource.buffer = buffer; let gainNode = context.createGain(); gainNode.connect(context.destination); gainSource.connect(gainNode); context.resume(); gainSource.start(0,0); }; // ---------- // Element MAIN.prototype.get_area = function(){ if(this.area === undefined){ this.area = document.querySelector(".lists"); } return this.area; }; MAIN.prototype.add_button = function(key){ let area = this.get_area(); let btn = document.createElement("button"); btn.setAttribute("data-type" , key); btn.setAttribute("data-path" , lists[key].path); btn.textContent = lists[key].name; area.appendChild(btn); }; MAIN.prototype.active_button = function(path){ let elm = document.querySelector("button[data-path='"+path+"']"); if(!elm){return;} elm.setAttribute("data-active" , "1"); }; window.addEventListener("load" , (function(){new MAIN()}).bind(this)); })() ページが読み込まれたら、音声コンテンツの数だけ、ボタンが表示され、クリックすると、音がなるようにしています。 ※ちなみに、読み込み確認だけで作ったので、再生停止処理は入れていません。 SE音源をセットすれば、ライブなどで使える、音出しサンプラーみたいにできそうですねwww

このブログを検索

ごあいさつ

このWebサイトは、独自思考で我が道を行くユゲタの少し尖った思考のTechブログです。 毎日興味がどんどん切り替わるので、テーマはマルチになっています。 もしかしたらアイデアに困っている人の助けになるかもしれません。