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

プログラマーネタ中心。たまに作成したウェブサービス関連の話も 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 );
    };

      :

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


以前に PouchDB を使った IBM Cloudant(Apache CouchDB) との同期について紹介しました:
IBM Cloudant と PouchDB で同期をとる

multiuser-couch-pouch


PouchDB は JavaScript 上で動作する CouchDB 互換の NoSQL データベースで、最大の特徴の1つが CouchDB との同期機能です(だと思ってます)。JavaScript で動作するということはブラウザ内のローカル DB で同期することもできて、PWA(Progressive Web Application) を作る際にも非常に有用な機能だと思っています。上記エントリではブラウザ内の PouchDB データベースと、サーバー上の CouchDB データベース間で双方向に全同期したり、双方向に部分同期する方法を紹介しました。

実際の PouchDB の使いみちを考えると、単方向にのみ同期させたいこともあると思います。変更作業はローカルのみで行って、その変更した内容のみをサーバー側に反映させたいとか、逆にサーバーで変更された内容をローカルに取り込みたいとかいったケースです。この実現方法を紹介します。

まずローカル側とサーバー側、両方でデータベースを指定します:
<script src="//cdn.jsdelivr.net/pouchdb/5.4.5/pouchdb.min.js"></script>

   :
   :

<script>
var local_db = new PouchDB( 'localdb' );
var remote_db = new PouchDB( 'https://remoteserver/remotedb' );


この2つのデータベース間で全文書を対象に双方向同期を行うのであれば前回紹介した方法を使って、以下のように実行することで実現できました:
local_db.sync( remote_db, {
  live: true,
  retry: true
});

ここを「ローカルでの変更をサーバー側にのみ反映させる(サーバー側の変更はローカルには反映させない)」場合は以下のように指定します:
local_db.replicate.to( remote_db, {
  live: true,
  retry: true
});

または

remote_db.replicate.from( local_db, {
  live: true,
  retry: true
});

逆に「サーバー側での変更をローカル側にのみ反映させる(ローカル側での変更はサーバーには反映させない)」場合は以下のように指定します:
local_db.replicate.from( remote_db, {
  live: true,
  retry: true
});

または

remote_db.replicate.to( local_db, {
  live: true,
  retry: true
});

全文書を対象とするのではなく、一部の文書を対象とする場合は、前回紹介した方法で doc_ids を指定することで同様に実現できます:
local_db.replicate.from( remote_db, {
  doc_ids: [ 001, 002, 003, ... ],
  live: true,
  retry: true
});

または

remote_db.replicate.to( local_db, {
  doc_ids: [ 001, 002, 003, ... ],
  live: true,
  retry: true
});

参考:
https://pouchdb.com/api.html#replication



 

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回マンホールナイトのチケットも絶賛発売中です!今年の文化の日はマンホール文化を嗜みましょう!!



IBM Cloud から提供されている IBM CloudantApache CouchDB をベースとしたマネージドな NoSQL データベースのサービスです:
2018090300


ベース製品が同じなので、例えば REST API レベルでは互換性があります。注意が必要な点として自分が気づいた限りでは IBM Cloudant は標準で Apache Lucene ベースの検索機能が有効になっており、インデックスとなる Design Document を用意することでテキスト検索が可能になる、ということが挙げられますが、それ以外に大きな差はありません。 一方で IBM Cloud から提供されているライトアカウント(無料プラン)でも IBM Cloudant を利用することができるので、わざわざ Apache CouchDB を用意しなくても気軽に使うことができる DBaaS としてとても手軽で便利だと思っています:
2018090301


さて、自分は業務のプログラミングでは主に Node.js を使うのですが、Node.js のパッケージライブラリには IBM Cloudant 用のものと、Apache CouchDB 用のもの、両方が存在しています:
(IBM Cloudant)
2018090302

(Apache CouchDB)
2018090303


仮に対象となるデータベースが IBM Cloudant であれば前者の方が簡単に使えるという印象を持っています。ただし IBM Cloudant 用ライブラリは IBM Cloud 上の IBM Cloudant を想定していることもあり、例えばオンプレミス上の Apache CouchDB に対して使えるものではありません。

一方、Apache CouchDB 用ライブラリはローカルや社内サーバー、クラウド上にある Apache CouchDB データベース全般に対して利用することが可能です。この対象はクラウド上の IBM Cloudant であっても構いません。要するにこちらのライブラリを使えば Apache CouchDB だけでなく IBM Cloudant にも接続できる、ということです。


実際にこういった需要がどれだけあるのかわからないのですが、例えばあるシステムを作る際に、そのデータストアとして、
(1) 試しに動かす場合は IBM Cloud 上の IBM Cloudant を使って気軽に開発/テストを行い、
(2) 本番運用ではオンプレミスな Apache CouchDB を利用する(IBM Cloudant の独自機能は使わない想定)

といったことが接続先の切り替えだけでできると便利です。ただこれを実現するためには IBM Cloudant 用の便利なライブラリを使って開発しまうと (2) の本番の時に問題が起こってしまいます。以下では IBM Cloudant に対しても Apache CouchDB 用ライブラリ(以下 node-couchdb)を使ってアクセスするように実装してみたコードを紹介します。ベースが同じ製品なのでできることはできるんですが、そのための手順と注意点を含めて紹介します。


【準備】
まず Node.js のコードを記述する前に上述の node-couchdb を npm install しておきます:
$ npm install node-couchdb


【データベース接続】
node-couchdb を使って IBM Cloudant に接続します。こんな感じのコードを記述します:
var dblib = require( 'node-couchdb' );

var option = {
  auth: {
    user: 'username',
    pass: 'password'
  },
  'host': 'username.cloudant.com',
  'protocol': 'https',
  'port': 443
};
var db = dblib( option );

usernamepassword の部分にはそれぞれ IBM Cloudant の username と password を指定します(localhost の Apache CouchDB に接続する場合は option = {} で接続できます)。これで IBM Cloudant との接続ができました。ここで取得した db を使って、以下の主要な操作を行うことができます。


【主要な操作】

ドキュメント追加
insert() メソッドにデータベース名(以下の例では 'testdb')を指定して、ドキュメントを追加します。取得前に db.uniqid() でユニーク ID を取得し、_id に設定している点に注意してください:
var doc = { name: 'dotnsf', height: 170.0 };  //. 追加するドキュメント
db.uniqid().then( function( id ){
  doc._id = id[0];
  db.insert( 'testdb', doc ).then( function( body, headers, status ){
    console.log( body );
  }).catch( function( err ){
    console.log( err );
  });
});

ドキュメント読み取り
同様に get() メソッドにデータベース名と id を指定してドキュメントを取得します:
db.get( 'testdb', id ).then( function( doc, headers, status ){
  console.log( doc );
}).catch( function( err ){
  console.log( err );
});

ドキュメント削除
del() メソッドにデータベース名と id と rev を指定して、データベースからドキュメントを削除します。以下の例では一度 get() メソッドを実行して id 値から rev 値を取り出してから del() を実行しています :
db.get( 'testdb', id ).then( function( doc, headers, status ){
  db.del( 'testdb', doc.data._rev ).then( function( data, headers, status ){
    console.log( data );
  }).catch( function( err ){
    console.log( err );
  });
}).catch( function( err ){
  console.log( err );
});

ビューを指定してドキュメント一覧取得
あらかじめ作成したビューを指定して、そのビューに含まれるドキュメントの一覧を取得します。以下の例ではデザイン名 : library, ビュー名 : byname というビューを指定して文書一覧を取得しています :
db.get( 'testdb', '_design/library/_view/byname', {} ).then( function( data, headers, status ){
  if( data && data.data ){
    var docs = data.data.rows;
    console.log( docs );
  }
}).catch( function( err ){
  console.log( err );
});


IBM Cloudant の npm だと最初にデータベース名を指定してそのデータベースのオブジェクトを取得した上で各種操作を行う、、という流れなんですが、Apache CouchDB 版だと毎回データベース名と一緒に各種操作を行う、、という点が大きな違いだと思いました。ただその辺りさえ理解していればまあ大丈夫かな。。


 

このページのトップヘ