まだプログラマーですが何か?

プログラマーネタ中心。たまに作成したウェブサービス関連の話も https://twitter.com/dotnsf

2018/09

これまでの人生の中で楽器とか音楽全般にあまり縁がなかったこともあって、ソフトウェア開発の中でもオーディオ系のデータを扱う話になると途端にチンプンカンプンでした。HTML5 に Web Audio API が存在していることは知っていましたが、そんな理由で(使おうとしても前提知識がサッパリで付いていけず)ほぼ手付かずでしたが、「苦手科目をなくす」目的でちと勉強を始めてみた経緯をブログで記録していこうと思っています。分かる人からすれば超初心者向けの内容になっていると思いますがお許しを。

とりあえずの題材としてやってみようと思ったのは「オーディオファイルの再生」です。念のため書いておきますが、単にウェブページ内でオーディオファイルを再生することが目的であれば <audio> タグを使うのが(ブラウザやバージョンの違いなどを意識することもなく)簡単だということは理解しています:
<audio controls>
  <source type="audio/mp3" src="./xxxx.mp3"/>
</audio>

これと同じ、または近いことを <audio> タグを使わずに HTML5 の Web Audio API だけで実現することを最初の目標にしてみました。で、作ってみたのがこちらです:
<html>
<head>
<title>Audio File Play</title>
<script>
window.AudioContext = window.AudioContext || window.webkitAudioContext;
var context = new AudioContext();

window.onload = function(){
  if( ( document.readyState == 'interactive' ) || ( document.readyState == 'complete' ) ){
    onDOMContentLoaded();
  }else{
    document.addEventListener( 'DOMContentLoaded', onDOMContentLoaded, true );
  }
  
  function onDOMContentLoaded(){
    function loadAudio( node ){
      var successCallback = function( audioBuffer ){
        console.log( audioBuffer );
        var source = context.createBufferSource();
        
        source.buffer = audioBuffer;           //. オーディオデータの実体(AudioBuffer インスタンス)
        source.loop = false;                   //. ループ再生するか?
        source.loopStart = 0;                  //. オーディオ開始位置(秒単位)
        source.loopEnd = audioBuffer.duration; //. オーディオ終了位置(秒単位)
        source.playbackRate.value = 1.0;       //. 再生速度&ピッチ
        
        source.connect( context.destination );
        
        //. for lagacy browsers
        source.start( 0 );
        source.onended = function( event ){
          //. イベントハンドラ削除
          source.onended = null;
          document.onkeydown = null;
          
          //. オーディオ終了
          source.stop( 0 );
          
          console.log( 'audio stopped.' );
        };
      };
      
      var errorCallback = function( error ){
        if( error instanceof Error ){
          window.alert( error.message );
        }else{
          window.alert( 'Error: "decodeAudioData"');
        }
      };
      
      //. オーディオバッファインスタンス作成
      context.decodeAudioData( node, successCallback, errorCallback );
    };
    
    document.querySelector( '[type="file"]' ).addEventListener( 'change', function( event ){
      var uploader = this;
      
      var file = event.target.files[0];
      if( !( file instanceof File ) ){
        window.alert( 'Error: Please upload file.' );
      }else if( file.type.indexOf( 'audio' ) == -1 ){
        window.alert( 'Error: Please upload audio file.' );
      }else{
        var reader = new FileReader();
        reader.onprogress = function( event ){
        };
        
        reader.onerror = function(){
          window.alert( 'Error: FileReader(' + reader.error.code + ')' );
          uploader.value = '';
        };
        
        reader.onload = function(){
          var arrayBuffer = reader.result;   //. ArrayBuffer(Web Audio API では Float32Array 型配列)
          
          loadAudio( arrayBuffer );
        };
        
        reader.readAsArrayBuffer( file );
      }
    }, false );
  }
}
</script>
</head>
<body>
  <div id="page">
    <div>
      <h2>オーディオファイルローダー</h2>
      <input type="file" accept="audio/*"/>
    </div>
  </div>
</body>
</html>

まず最初に、こちらを作っていて今回のテーマ内であっても <audio> タグでできることとできないことがあることに気づきました。<audio> タグでオーディオファイルを再生するには、そのオーディオファイルは(ファイル場所を src 属性で指定する必要があるため)この HTML ファイルが存在するサーバー上か、または HTTP(S) で取得できる場所に存在している必要があります。一方、オーディオファイルが手元の PC 内にある場合はあらかじめそのファイルをサーバー上にコピーしておかないと <audio> タグで指定できるようにはなりません。そのような場合であれば(上記で作成したように)HTML5 の File API を併用して手元のファイルを読み取り、オーディオバッファを取り出して Web Audio API に渡して再生することで実現できます。というわけで、実際にそのような挙動をする例を作ってみました。

<input type="file" accept="audio/*"/> のコントロールにオーディオファイル(MP3 ファイル)を指定するとイベントリスナーで定義された部分がハンドリングして、オーディオファイルであることを確認した上で以下の処理が実行されます:
      :

    document.querySelector( '[type="file"]' ).addEventListener( 'change', function( event ){
      var uploader = this;
      
      var file = event.target.files[0];
      if( !( file instanceof File ) ){
        window.alert( 'Error: Please upload file.' );
      }else if( file.type.indexOf( 'audio' ) == -1 ){
        window.alert( 'Error: Please upload audio file.' );
      }else{
        var reader = new FileReader();
        reader.onprogress = function( event ){
        };
        
        reader.onerror = function(){
          window.alert( 'Error: FileReader(' + reader.error.code + ')' );
          uploader.value = '';
        };
        
        reader.onload = function(){
          var arrayBuffer = reader.result;
          
          loadAudio( arrayBuffer );
        };
        
        reader.readAsArrayBuffer( file );
      }
    }, false );

      :

FileReader インスタンスを作成して readAsArrayBuffer() メソッドを実行し、アップロード指定したファイルを(アップロードせずにローカルで)読み取ります。読み取りが完了すると reader.onload イベントハンドラによって読み取り結果(reader.result)を取り出して、loadAudio() 関数を実行しています。

loadAudio() 関数内では読み取った ArrayBuffer に対して AudioContext の decodeAudioData() 関数を実行してオーディオバッファのインスタンスを生成します。生成に成功したらオーディオコンテキストからバッファソースを作って(createBufferSource())、オーディオバッファの実体等の属性を指定して、出力先に接続(connect())して、再生を開始(start())します:
      :

    function loadAudio( node ){
      var successCallback = function( audioBuffer ){
        console.log( audioBuffer );
        var source = context.createBufferSource();
        
        source.buffer = audioBuffer;           //. オーディオデータの実体(AudioBuffer インスタンス)
        source.loop = false;                   //. ループ再生するか?
        source.loopStart = 0;                  //. オーディオ開始位置(秒単位)
        source.loopEnd = audioBuffer.duration; //. オーディオ終了位置(秒単位)
        source.playbackRate.value = 1.0;       //. 再生速度&ピッチ
        
        source.connect( context.destination );
        
        //. for lagacy browsers
        source.start( 0 );
        source.onended = function( event ){
          //. イベントハンドラ削除
          source.onended = null;
          document.onkeydown = null;
          
          //. オーディオ終了
          source.stop( 0 );
          
          console.log( 'audio stopped.' );
        };
      };
      
      var errorCallback = function( error ){
        if( error instanceof Error ){
          window.alert( error.message );
        }else{
          window.alert( 'Error: "decodeAudioData"');
        }
      };
      
      //. オーディオバッファインスタンス作成
      context.decodeAudioData( node, successCallback, errorCallback );
    };

      :

とりあえず現時点で理解できたのはここまでです。オーディオバッファの中身をもう少し理解して、波形データとか取り出したり、オーディオファイル以外からの再生もできるようになりたいなあ。。


2018/09/15 に開催された WordCamp Tokyo 2018 にスピーカーとして参加してきました:
WordPress meets IBM Watson ! 〜WordPress のコンテンツデータを IBM Watson に機械学習させてみよう〜

kimura-kei


セッションの中で話せたことだけでなく、その背景にあった部分も含めてブログにまとめました。


【WordPress 歴】
まず私自身の WordPress 歴を話しておきます。利用者という意味では結構早い段階から使っていたつもりでしたが、(WordPress をカスタマイズして使ってサービスを作る、という)開発者としては 2013 年からだと思います。この年に転職を経験しており、その転職先では WordPress を使ったサービス開発案件を何度も経験しました。業務で本格的に PHP を使うようになったのもこの頃からです。自分自身の印象として、WordPress は「サービスを高速開発するフレームワークの1つ」だと思っています。実際、テンプレートやプラグインの充実度が半端なく、ちょっとググればやりたいことを実現するプラグインが見つかるので、完成形に近いサービスをすぐに作ることができて感動しました。同時に WordPress のシステム内部についても興味を持つようになりました。当時はまだ REST API が標準ではなかったため、自分で MySQL テーブルの構造を調べて、データを効率よくインポート/エクスポートするにはどうすればよいか、を調べたり、ツールを作ったりしていました。今回発表させていただいた内容でもこの頃の知識が役立ちました。


【2度目の応募】
WordCamp でスピーカーとして登壇させていただいたのは初めてでしたが、スピーカーとしての応募は初めてではありません。WordCamp の存在を知ったのは 2013 年頃だったと思います。記憶が正しければこの頃(おそらく 2014 年)に一度スピーカーとして応募しています。少しビジネス寄りの利用を想定した内容だったのですが、この時は残念ながら登壇することはありませんでした。そしてこの時から「いつかはこの舞台でスピーチしたいなあ」という目標のようなものができたのでした。

そして今年、今回もどちらかというとビジネス寄りの内容だったと思いますが、よく言えば「流行りに乗っかる」形で人工知能と絡めたセッションのスピーチで応募し、念願だった登壇者に選んでいただきました。


【直前のトラブル】
今回のセッションは 15 分間の中で説明とライブデモを見せる、というものでした。このライブデモで実際に動いている所をお見せすることで理解をより具体的に深めていただく、という目的がありました。

ところがこのデモ環境にトラブルがありました。ちゃんと動く環境を(仮想環境で)用意して、そのイメージのバックアップもとって、何かあってもバックアップからリストアすれば大丈夫、と思っていました。ところがこの環境で使って、セッションが expire するまでは普通に使えるのですが、expire 後に何故かログインできなくなる、という問題が発生してしまいました。要は肝心のデモ環境に管理者権限でログインできなくなってしまう、という症状が出てしまったのでした。プラグインを紹介するデモなので、管理者コンソールに入れないのは致命傷でした。

判明したのがデモ直前だったこともあり、原因は未だによくわかっていないのですが、このトラブルに結構振り回されました。結論としては「セッションの直前にデモ環境をデモデータ含めて新規に構築する」という綱渡り的な対処で乗り切りました。冷や汗モノ・・・ (^^;


【セッションは無事に】
そんな綱渡りをしながらもセッションは無事に(3分オーバーでw)終えることができました。事前の練習では1分余らせて終える練習をしていたつもりが、本番になると調子に乗ってしまったのか、話す時間が多くなってしまったようです。

当日の資料の配布版はこちらで公開しています:


IBM Watson の NLC(Natural Language Classifier) というサービスを使って、WordPress の中に溜まったデータを Watson に学習させて、問い合わせすることに挑戦する、という内容を紹介させていただきました。全セッション中の最終セッションだったにもかかわらず多くの皆様に参加いただき、また終了後も多くの質問や感想をその場でいただくことができて、とても充実したセッションでした。この場をお借りしてお礼申し上げます。

セッション内でも触れましたが、機械学習の現場では「学習データ不足」が解決するべき課題となっています。WordPress のような広く使われている CMS のコンテンツデータを学習データとすることができればどんなに楽か・・・という思いもあってのセッションテーマおよびデモでした。同じような悩みを持っていたり、興味を持って参加いただけた皆さんの参考になれば嬉しいです。

また上述の通り、自分にとってはこの場でセッションすることがここ数年の目標の1つでした。とてもいい形で実現することができたと思っています。参加者の皆様、サポートスタッフの皆様、スポンサーの皆様、貴重な機会を本当にありがとうございました。

uXTscPOQ



2018年11月3日(土・祝)に第10回マンホールナイトが開催されます。また同日に会場で投票が行われるマンホール写真コンテストの応募作品の募集が開始されました。

第10回マンホールナイト併設写真コンテスト作品募集

mn10keyvisual


今回は第10回記念大会ということもあり、例年に増してレアなグッズが賞品として用意されているとかいないとか・・・ そんな写真コンテストへの応募方法をご紹介します。

今回、応募はメールで行う必要があります。応募作品画像を添付して、photo@manholenight.info へ送付ください。またその際に本文内に以下の情報を記載してください:
・作品タイトル
・作者名(ペンネーム・ハンドルネーム可)


加えて、当日会場に来られない方の場合は、副賞を送付する必要が生じる可能性があるため、以下の情報も合わせて記載ください:
・住所
・お名前(こちらは本名)
・電話番号


青字は必須、橙字は会場不参加予定の方のみ


(メール例)
2018090501

(添付画像例)
funabashi_sapporo



なお、例年のコンテストでは写真につけるタイトルが受賞の鍵となっている傾向があります。どんな作品にどんなタイトルをつけて応募するのがよいのか、昨年の受賞作も参考にしながら「これぞ!」という応募作品タイトルをつけてご応募ください。

応募締切は9月30日(日)10月14日(日)必着。応募資格はなく(プロ・アマ問わず)、応募そのものに料金はかかりません。お一人何点応募しても構いません(ただし似た画像だと得票が割れて不利になる可能性あり)。詳しくは公式応募ページを参照ください。


多くの皆様のご応募をお待ちしております。合わせて第10回マンホールナイトのチケットも絶賛発売中です!今年の文化の日はマンホール文化を嗜みましょう!!



このページのトップヘ