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

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

タグ:api

(この記事は IBM Cloud アドベントカレンダー 2018 に参加しています。3日目の記事です)

IBM Cloudant (Apache CouchDB) にあまり詳しくない人が他のデータベースと同じ感覚でデータを扱っている時に、特に既存データを更新している時にふと気づくことがあります。例えば以下のような現象を目の当たりにした時、何が起こっているのか正しく理解できるでしょうか?


IBM Cloudant のダッシュボード画面にアクセスし、今回は "testdb" という名称のデータベースを IBM Cloudant 上に新規に作成しました。以下の手順はすべてこのデータベースを対象に行います(CouchDB でも同様の結果になります)。作成したばかりなのでまだドキュメント数はゼロです:
2018100201


testdb データベースを選択した画面です。普通はここで testdb 内のドキュメント一覧が表示されますが、まだ1つも存在していないので "No Documents Found" と表示されています。ここでドキュメントを新規に作成するため "Create Document" ボタンをクリックします:
2018100202


新規に JSON ドキュメントを作成する画面に切り替わります。Cloudant(CouchDB) のドキュメントは "_id" というユニーク ID を含める必要があります(API 経由で _id を含めずに作ると自動的に割り振られます)。自動的に設定された "_id" 以外に "name" というキーを作り、適当な値(下図では "kkimura")を設定して "Create Document" ボタンをクリックします(JSON ドキュメントなので "_id" キーの最後にカンマをつけることを忘れずに):
2018100203


先程のドキュメントが作成され、ドキュメント一覧に1つのドキュメントが表示されるようになりました:
2018100204


ちなみに、この段階でデータベース一覧に戻ると testdb データベースのドキュメント数もゼロから 1 に変わっていることが確認できます:
2018100205


またドキュメント一覧からこのドキュメントを選択するとドキュメントの確認/編集画面になります。"_rev" という先ほど指定しなかったキーと値が追加されていますが、こちらは後で説明します:
2018100206


ここまでは特別におかしな所はないと思います。この文書を編集するあたりから Cloudant 特有のクセというか、「あれ?」と感じる所が出てくるようになってきます。

この画面から JSON ドキュメントを編集してみます。試しに "name" の値を(下図では "Kei Kimura" に)変更し、"Save Changes" ボタンをクリックします:
2018100207


変更内容が保存されて、ドキュメント一覧に戻ります。既存文書を編集して保存したので文書数は変わらずに1つのままです。ではこの文書を選択して開いてみます:
2018100208


"name" の値が "Kei Kimura" になった文書が開きました。が、よく見ると "_rev" の値が先程と異なっています。最初に作った直後は "1-" で始まる値だったのが、 "2-" で始まる値になっています。ここは変更しなかったはずなんですが・・・:
2018100209


また、このタイミングでデータベース一覧の画面に戻ると、testdb の文書数は1のままなんですが、データベースサイズが微妙に増えています。これほどの差がでるような変更をしたつもりはないのですが・・・:
2018100210


更にこの文書を開いて、再度 "name" 値を "kkimura" に変更して(元に戻して)みます。値を変更して "Save Changes" ボタンをクリックします:
2018100211


すると(中を開いて確認してもいいのですが)また "_rev" の値が変わっていることが一覧からもわかります。今度は "3-" で始まる値になっていました:
2018100212


この辺りから「???」と感じることが増えてきました。では最後にこの文書を削除してみます。一覧からチェックをつけてゴミ箱ボタンをクリックします:
2018100213


削除すると一覧からは文書は消えて、元通りの "No Documents Found" が表示されます:
2018100214


しかしデータベース一覧に戻って testdb を見ると、文書数は "0" ですが、横に!マークが付いています。また文書を削除した割にはデータベースサイズがあまり減っていないように見えます:
2018100215


この!マーク部分にマウスカーソルをあわせると、"This database has just 0 docs and 1 deleted docs" と表示されます。このメッセージの意味はいったい・・・:
2018100216


ドキュメントに勝手に "_rev"(と "_id")が付与されること、編集して保存すると "_rev" の値が勝手に変更されること、文書を削除してもデータベースサイズが減らないこと、文書を削除した時の謎のメッセージ、・・・ と、この辺りが Cloudant(CouchDB) を始めて使うと戸惑う点でしょうか? 前置きが長くなってしまいましたが、以下にこの謎を解くための説明を記載します。


上記の振る舞いを理解するには、まず自動付与される2つの値 "_id" と "_rev" の意味と役割を正しく理解する必要があります。

"_id" はいわゆる「文書 ID」です。この値はデータベース内でユニークな値をなっており、各文書を一意に取得することができるキー値となっています。正しい ID 値が与えられるだけで(他の絞り込み条件がなくても)データベース内から目的の文書を特定して取得することができます。ID 値については普通のデータベースでも扱うものなので、あまり難しくないと思っています。

一方、もうひとつの "_rev" 、こちらは IBM Cloudant(CouchDB) の特徴的な予約語となっており、「文書のリビジョン」を管理する値となっています。「リビジョン」は「バージョン」と読み替えていただいてもいいです。

上記の例だと、最初に "name" = "kkimura" という値で文書を作成しました。この時点ではこの文書のリビジョン(バージョン)は 1 で、"_rev" 値は "1-" で始まる値になっていました:
2018100204


次に同じ文書を "name" = "Kei Kimura" と変更して保存しました。この時点でこの文書のリビジョンは 2 となり、"_rev" 値も "2-" で始まる値に更新されました:
2018100208


更に同じ文書を "name" = "kkimura" に戻して保存しました。この時点でこの文書のリビジョンは 3 となり、"_rev" 値も "3-" で始まる値に更新されました:
2018100212


つまり "_rev" 値は "_id" 値で決まる文書のバージョンを管理する役割を持って自動的に更新されるシステム値ということになります。ただ Cloudant(CouchDB) でドキュメントが更新される際にはもう1つの特徴があります。

実は Cloudant(CouchDB) ではドキュメントが更新されることはほぼなく、「新しいドキュメントが新しい "_rev" 値を持って新規作成」されます。つまり厳密には同じ "_id" 値を持った複数のドキュメントがデータベース内には存在しているが、その中で最も大きな "_rev" 値を持ったドキュメントだけが有効になります。論理的にドキュメントを更新したつもりでいても、物理的には古いドキュメントは消えずに残っていて、新しいドキュメントが同じ "_id" 値&新しい "_rev" 値で作成されるのでした。なお最新でないリビジョンのドキュメントは _id 値を指定してドキュメントを取得する時に { revs_info: true } というオプションを指定することで取得することができます(このオプションをつけない限り、最新 _rev のものだけで取得できます):
http://docs.couchdb.org/en/stable/api/document/common.html


上記で Cloudant(CouchDB) のドキュメントが更新されることは「ほぼ」ないと書いたのですが、厳密にはあります。それが文書削除時です。Cloudant(CouchDB) の文書削除はいわゆる「ソフトデリート(論理削除)」であって、「ハードデリート(物理削除)」ではありません。文書に削除フラグ( { _deleted: true } )をつけて更新し、最新 "_rev" の文書が削除されているようにすることで、論理的に文書が削除されたことにしています。そしてこの論理削除を行う際には _id 値だけではなく、_rev 値と合わせて指定して、「この ID 値の、このリビジョンの文書を削除する」ことを明示的に指定する必要があります。論理的には _id 値だけで削除できそうな感覚を持ってしまいますが、その場合はまずその _id 値を持ったドキュメントの最新リビジョンを取得し、取得したドキュメントから _rev 値を取り出し、改めて _id 値と _rev 値を指定して論理削除する、という流れになります。


これらの部分を理解していると、文書を更新したり、削除した時にデータベースサイズが増える謎が理解できると思います。要は物理的に書き換えたり、物理的に削除しているわけではなく、新リビジョンのドキュメントを追加したり、削除フラグをつけたりしているだけなので、(別途物理削除するまでは)データベースサイズという観点では減ることがないのでした。








 

楽器苦手なオッサンエンジニアの Web Audio API 勉強シリーズ(!?)、前回はマイクから入力した音声をオーディオバッファ・インスタンスに変換して再生するコードを紹介しました。今回は前回作成したコードを改良して、音声データを折れ線グラフで可視化することに調整してみました。

作成したコードはこんな感じです。前回のものからの差分をで示しています:
<html>
<head>
<title>Audio Buffer Chart</title>
<script src="https://code.jquery.com/jquery-2.0.3.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/Chart.js/2.1.4/Chart.min.js"></script>
<script>
window.AudioContext = window.AudioContext || window.webkitAudioContext;
var context = new AudioContext();

window.onload = function(){
}

var processor = null;
var num = 0;
var duration = 0.0;
var length = 0;
var sampleRate = 0;
var floatData = null;
function handleSuccess( stream ){
  var source = context.createBufferSource();
  var input = context.createMediaStreamSource( stream );
  processor = context.createScriptProcessor( 1024, 1, 1 );
  
  //window.dotnsf_hack_for_mozzila = input;
  input.connect( processor );
  processor.onaudioprocess = function( e ){
    //. 音声データ
    var inputdata = e.inputBuffer.getChannelData(0);
    //console.log( inputdata );

    if( !num ){
      num = e.inputBuffer.numberOfChannels;
      floatData = new Array(num);
      for( var i = 0; i < num; i ++ ){
        floatData[i] = [];
      }
      sampleRate = e.inputBuffer.sampleRate;
    }
    
    var float32Array = e.inputBuffer.getChannelData( 0 );
    if( availableData( float32Array ) ){
      duration += e.inputBuffer.duration;
      length += e.inputBuffer.length;
      for( var i = 0; i < num ; i ++ ){
        float32Array = e.inputBuffer.getChannelData( i );
        Array.prototype.push.apply( floatData[i], float32Array );
      }
    }
  };
  processor.connect( context.destination );
}

function startRec(){
  $('#recBtn').css( 'display', 'none' );
  $('#stopBtn').css( 'display', 'block' );

  navigator.mediaDevices.getUserMedia( { audio: true } ).then( handleSuccess );
}

function stopRec(){
  $('#recBtn').css( 'display', 'block' );
  $('#stopBtn').css( 'display', 'none' );

  if( processor ){
    processor.disconnect();
    processor.onaudioprocess = null;
    processor = null;
    
    var audioBuffer = context.createBuffer( num, length, sampleRate );
    for( var i = 0; i < num; i ++ ){
      audioBuffer.getChannelData( i ).set( floatData[i] );
    }
    
    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;
      num = 0;
      duration = 0.0;
      length = 0;

      //. オーディオ終了
      source.stop( 0 );

      console.log( 'audio stopped.' );
    };
    
    //. floatData[] (の先頭の一部)をグラフ描画する
    var dotnum = 1024;
    var ctx = document.getElementById( 'myChart' ).getContext( '2d' );
    var labels = [];
    var datasets = [];
    var colors = [ "rgba( 255, 0, 0, 0.4 )", "rgba( 0, 0, 255, 0.4 )" ];
    for( var i = 0; i < dotnum; i ++ ){
      labels.push( "" + ( i + 1 ) );
    }
    for( var i = 0; i < num; i ++ ){
      datasets.push({
        label: "data " + i,
        data: floatData[i].slice(1024,1024+dotnum),
        backgroundColor: colors[i%2]
      });
    }
    
    var myChart = new Chart( ctx, {
      type: 'line',
      data: {
        labels: labels,
        datasets: datasets
      }
    });
  }
}

function availableData( arr ){
  var b = false;
  for( var i = 0; i < arr.length && !b; i ++ ){
    b = ( arr[i] != 0 );
  }
  
  return b;
}
</script>
</head>
<body>
  <div id="page">
    <div>
      <h2>オーディオバッファ視覚化</h2>
      <input type="button" id="recBtn" value="Rec" onClick="startRec();" style="display:block;"/>
      <input type="button" id="stopBtn" value="Stop" onClick="stopRec();" style="display:none;"/>
    </div>
    <div>
      <canvas id="myChart"></canvas>
    </div>
  </div>
</body>
</html>

本当は再生する音声データ全てを折れ線グラフとして表示できるとよかったのですが、数10万~数100万個の配列データを扱うことになり、処理がかなり重くなる(見栄えもつまりすぎて折れ線に見えなくなる)ことがわかったので、上記では全データの 1024 個目から 500 個だけ取り出して折れ線グラフにするようなコードにしています(後述のように、このくらいだとかろうじて折れ線に見えます)。

変更点としてはまず HTML ボディ内に折れ線グラフを表示するための canvas 要素を記述しています。そしてこの canvas の中に Chart.js を使って折れ線グラフを描いていきます。今回は Chart.js の CDN を使ってロードしています。

今回の例ではマイクから入力した音声データ(波形データ)は floatData[] 配列に格納しています。入力がモノラルの場合は floatData[0] に、ステレオの場合は floatData[0] (左)と floatData[1] (右)に、それぞれ波形データが配列で格納されます。この中身を折れ線表示すればよいことになります。

※マイクからの入力であれば、このように波形データをあらかじめ配列に格納させておくことができますが、オーディオファイルを再生させる場合であればオーディオバッファ・インスタンスを取得してから
  audioBuffer.getChannelData( n );  ※ n: 0 または 1
を実行することで同じ配列を取り出すことができます。


このコードを実行して、マイクから音声入力するとこんな感じで音声が折れ線グラフ表示されます(以下はモノラルの例):
2018100201


最初はチンプンカンプンでしたが、だんだんオーディオバッファの中身がわかってきました。 v(^o^)


勉強中の HTML5 Web Audio API の備忘録を兼ねたブログエントリ記事です。前回はローカルシステム上のオーディオファイルを File API で読み取った上でオーディオバッファに変換して Audio API で再生する、というオーディオ出力処理に挑戦しました。今回は逆にオーディオ入力処理を扱ってみました。具体的にはマイクから入力した音声データをオーディオバッファに変換する処理を調べてみました(コードではオーディオバッファに変換した上で前回同様にそのまま再生させています)。

とりあえず完成形の HTML ファイルは以下のとおりです。「とりあえず」と書いたのは現時点では環境によって動いたり動かなかったりする挙動が見られて、まだ不安定版といった感じな所が理由です。手元の Windows 10 の FireFox および Chrome では動作しましたが Ubuntu 16.04 の FireFox だとエラーになりました:
<html>
<head>
<title>Voice Replay</title>
<script src="https://code.jquery.com/jquery-2.0.3.min.js"></script>
<script>
window.AudioContext = window.AudioContext || window.webkitAudioContext;
var context = new AudioContext();

var processor = null;
var num = 0;
var duration = 0.0;
var length = 0;
var sampleRate = 0;
var floatData = null;
function handleSuccess( stream ){
  var source = context.createBufferSource();
  var input = context.createMediaStreamSource( stream );
  processor = context.createScriptProcessor( 1024, 1, 1 );
  
  input.connect( processor );
  processor.onaudioprocess = function( e ){
    //. 音声データ
    var inputdata = e.inputBuffer.getChannelData(0);
    //console.log( inputdata );

    if( !num ){
      num = e.inputBuffer.numberOfChannels;
      floatData = new Array(num);
      for( var i = 0; i < num; i ++ ){
        floatData[i] = [];
      }
      sampleRate = e.inputBuffer.sampleRate;
    }
    
    var float32Array = e.inputBuffer.getChannelData( 0 );
    if( availableData( float32Array ) ){
      duration += e.inputBuffer.duration;
      length += e.inputBuffer.length;
      for( var i = 0; i < num ; i ++ ){
        float32Array = e.inputBuffer.getChannelData( i );
        Array.prototype.push.apply( floatData[i], float32Array );
      }
    }
  };
  processor.connect( context.destination );
}

function startRec(){
  $('#recBtn').css( 'display', 'none' );
  $('#stopBtn').css( 'display', 'block' );

  navigator.mediaDevices.getUserMedia( { audio: true } ).then( handleSuccess );
}

function stopRec(){
  $('#recBtn').css( 'display', 'block' );
  $('#stopBtn').css( 'display', 'none' );

  if( processor ){
    processor.disconnect();
    processor.onaudioprocess = null;
    processor = null;
    
    var audioBuffer = context.createBuffer( num, length, sampleRate );
    for( var i = 0; i < num; i ++ ){
      audioBuffer.getChannelData( i ).set( floatData[i] );
    }
    
    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;
      num = 0;
      duration = 0.0;
      length = 0;

      //. オーディオ終了
      source.stop( 0 );

      console.log( 'audio stopped.' );
    };
  }
}

function availableData( arr ){
  var b = false;
  for( var i = 0; i < arr.length && !b; i ++ ){
    b = ( arr[i] != 0 );
  }
  
  return b;
}
</script>
</head>
<body>
  <div id="page">
    <div>
      <h2>音声再生</h2>
      <input type="button" id="recBtn" value="Rec" onClick="startRec();" style="display:block;"/>
      <input type="button" id="stopBtn" value="Stop" onClick="stopRec();" style="display:none;"/>
    </div>
  </div>
</body>
</html>

動かし方としてはこの HTML をファイルに保存し、ウェブブラウザで開きます。サーバー上になくても構いません(ローカルファイルとして開く場合は Ctrl+O でファイルを選択します)。開くと "Rec" と書かれたボタンが1つ表示されるシンプルな画面が表示されます:
2018093001


この "Rec" ボタンが「録音」ボタンです。このボタンをクリックすると JavaScript の Audio API にマイクの利用を許可するかどうか聞かれるので、「許可」を選択してください:
2018093002


この時に実行されるコードが以下です。ボタン表示を切り替えると同時に HTML5 Audio API の getUserMedia() を実行してマイクにアクセスします。Web Audio API でマイクを使う時のスタート地点がここになります:
      :
function startRec(){
  $('#recBtn').css( 'display', 'none' );
  $('#stopBtn').css( 'display', 'block' );

  navigator.mediaDevices.getUserMedia( { audio: true } ).then( handleSuccess );
}
      :

マイクへのアクセスが成功するとボタンの表示が "Rec" から "Stop" に切り替わり、同時に録音モードになります。コンピュータの標準マイクを使って、周りの音の録音が開始されます。またマイクへのアクセス成功時に handleSuccess 関数がコールバック実行され、マイクに入力されたストリーミングデータが引数として渡されます。ここでは onaudioprocess イベントをハンドリングしてチャンネル毎のサンプリングデータを floatData 配列に格納しています。この例ではサンプリングデータを 1024 個ずつ送られてくるようにしていて「その中身が全て 0 ではない(無音データではない)」という確認をした上で配列の最後に追加して、後でまとめて再生できるようにしています:
      :
window.AudioContext = window.AudioContext || window.webkitAudioContext;
var context = new AudioContext();

var processor = null;
var num = 0;
var duration = 0.0;
var length = 0;
var sampleRate = 0;
var floatData = null;
function handleSuccess( stream ){
  var source = context.createBufferSource();
  var input = context.createMediaStreamSource( stream );
  processor = context.createScriptProcessor( 1024, 1, 1 );
  
  input.connect( processor );
  processor.onaudioprocess = function( e ){
    //. 音声データ
    var inputdata = e.inputBuffer.getChannelData(0);
    //console.log( inputdata );

    if( !num ){
      num = e.inputBuffer.numberOfChannels;
      floatData = new Array(num);
      for( var i = 0; i < num; i ++ ){
        floatData[i] = [];
      }
      sampleRate = e.inputBuffer.sampleRate;
    }
    
    var float32Array = e.inputBuffer.getChannelData( 0 );
    if( availableData( float32Array ) ){
      duration += e.inputBuffer.duration;
      length += e.inputBuffer.length;
      for( var i = 0; i < num ; i ++ ){
        float32Array = e.inputBuffer.getChannelData( i );
        Array.prototype.push.apply( floatData[i], float32Array );
      }
    }
  };
  processor.connect( context.destination );
}
      :

このタイミングでもう一度 "Stop" をクリックすると録音を終了します:
2018093003


"Stop" をクリックすると同時にこれまでに配列に集めてきたサンプリングデータをオーディオバッファ・インスタンスに変換します。オーディオバッファ側になれば前回紹介したのと同様の方法で再生できるので、マイクに入力した音声がそのままオウム返しのように再生されます:
function stopRec(){
  $('#recBtn').css( 'display', 'block' );
  $('#stopBtn').css( 'display', 'none' );

  if( processor ){
    processor.disconnect();
    processor.onaudioprocess = null;
    processor = null;
    
    var audioBuffer = context.createBuffer( num, length, sampleRate );
    for( var i = 0; i < num; i ++ ){
      audioBuffer.getChannelData( i ).set( floatData[i] );
    }
    
    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;
      num = 0;
      duration = 0.0;
      length = 0;

      //. オーディオ終了
      source.stop( 0 );

      console.log( 'audio stopped.' );
    };
  }
}

いくつかの環境ではこれで録音→再生できることを確認していますが、エラーになる環境もあります。手元で試した限りでは Windows10 の Firefox & Chrome は大丈夫そうで、Ubuntu 16.04 の FireFox はダメそうでした。この辺りはまだよくわかっていないのですが、動く環境があるということは根本的な考え方が間違っていることはないと思っています。


前回のと合わせて、これで音声データをマイクから入力したり、スピーカーから出力したり、という 基本的な I/O 部分を取り扱うレベルのものは作れました。次はサンプリングデータ(波形データ)の中身をもう少し深掘りしてみたいと思ってます。




これまでの人生の中で楽器とか音楽全般にあまり縁がなかったこともあって、ソフトウェア開発の中でもオーディオ系のデータを扱う話になると途端にチンプンカンプンでした。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 );
    };

      :

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


久しぶりに The Weather Company API を使ってみました。なお以下で紹介するサンプルのソースコードはこちらで公開しています:
https://github.com/dotnsf/twc_api

2018081400


使い方は Node.js が導入されているシステムに git clone(またはダウンロード&展開)して、npm install して、node app.js します。成功すると "server starting on XXXX ..." というメッセージが表示されます(この XXXX が動的に決まるポート番号です。以下の例では 6039 番):
$ npm install
$ node app.js
server starting on 6039 ...

ウェブブラウザで上記のポート番号を指定してサンプルアプリケーションに接続します。成功すると東京周辺の(OpenStreetMap の)地図が表示されます:
2018081401


2018/08/14 時点では、このサンプルアプリケーションで3つの The Weather Company API を試すことができ、それぞれ画面上部にある3つのボタンで実行可能です:
2018081402


一番左の "alertsByCoundryCode" ボタンは GET /v1/country/{countrycode}/alerts.json を countrycode = "US" で実行します。つまり "US"(アメリカ合衆国)で現在発生している天候に関する警報の一覧を取得します:
https://twcservice.mybluemix.net/rest-api/#!/Weather_Alerts/v1ccalertheadln


このサンプルアプリケーションでは上位30個の警報を取り出して、その発生位置にマーカーを置き、各警報の内容を参照できるようにしています:
2018081403
(↑この API は以前には存在してなかったような・・・)


真ん中の "currentConditions" ボタンをクリックすると、そのタイミングで地図の中心にある地点を座標を使って GET /v1/geocode/{latitute}/{longitude}/observations.json を実行します。地図の中心にある地点の現在の天候の様子を取得して表示するので、最初に地図をある程度スクロール(マウスドラッグでスクロールします)してから実行し、このサンプルアプリケーションではその情報の一部を表示します:
2018081404


一番右の "historicalData" ボタンをクリックすると、そのタイミングで地図の中心にある地点を座標を使って GET /v1/geocode/{latitute}/{longitude}/observations/timeseries.json を実行します。地図の中心にある地点の過去 24 時間の天候の移り変わりの様子を取得します。このサンプルアプリケーションでは過去 24 時間の気温の移り変わりを表示するようにしています:
2018081405


実装方法など、詳しくは Github 上のソースコードと、The Weather Company API の API Reference を参照ください。

このページのトップヘ