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

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

2018/10

IBM Cloudant のバイナリ・アタッチメント機能を使って、簡易的なファイルサーバー代わりに使う方法を紹介します。具体的には curl コマンドを使って手元の画像ファイル(でなくてもよい)をアップロードし、URL を指定して画像を表示できるようにする、というものです。

とりあえず以下の作業を行うには IBM Cloudant のデータベースと curl コマンドが必要です。前者は IBM Cloud のライトアカウントを使って無料で入手することも可能です。IBM Cloud にログイン後、IBM Cloudant のインスタンスを(無料であれば Lite プランで)作成しておいてください:
2018101601


また、以下のコマンド実行時に必要になるため、この Cloudant サービスのアクセス情報を確認しておきます。サービスを選択し、「サービス資格情報」と書かれたタブを選んで、「資格情報の表示」をクリックします(「資格情報の表示」がなかったら新規に作成してからクリックします):
2018101603


すると以下のような表示が画面下に出てきます:
2018101604


この中の username の値と、password の値を後でコマンド実行する際に指定することになります。どこかにメモしておくか、コピー&ペーストできるようにしておいてください。


また curl コマンドは使う方のシステムにあわせて適宜入手してください。Linux や MacOS であればたいてい標準で入っているはずですが、Windows の場合は別途インストールが必要です。私はこちらのをダウンロードしてインストールしました:
https://curl.haxx.se/download.html

実際に以下の作業を行う前に、IBM Cloudant 内に今回の作業の対象となるデータベースを用意します。今回は testdb という名称のデータベースを1つ用意しました。このデータベースを簡易ファイルサーバー代わりに使う方法を紹介します:
2018101602


まずはファイルを保存する手順です。今回は仮に cloudant.png という名前の以下のような画像ファイルを保存することにします:
cloudant1


コマンドプロンプト(Linux や MacOS の場合はターミナル)を開き、この cloudant.png が存在しているフォルダに移動して、以下のコマンドを入力します:
$ curl https://(username の値):(password の値)@(username の値).cloudant.com/testdb/(作成するドキュメントの _id)/(作成するアタッチメントの名称) -X PUT -H "Content-Type: image/png" --data-binary @cloudant.png

仮に username の値が "user1"、password の値が "pass1"、ドキュメントIDを "doc001"、アタッチメント名を "att001" とすると以下のようなコマンドになります:
$ curl https://user1:pass1@user1.cloudant.com/testdb/doc001/att001 -X PUT -H "Content-Type: image/png" --data-binary @cloudant.png

このコマンドが成功すると以下のような JSON が返されます:
{ "ok": true, "id": "doc001", "rev": "1-xxxxxxxx" }


コマンド実行が成功すると同じディレクトリにある cloudant.png ファイルを Cloudant の testdb にアップロードします。なお Content-Type の異なるファイルをアップロードする場合は -H オプションで指定する Content-Type を変えて指定してください。


Cloudant に保存したファイルを取り出す場合は以下の URL をブラウザで指定します:
https://(username の値):(password の値)@(username の値).cloudant.com/testdb/(作成するドキュメントの _id)/(作成するアタッチメントの名称)

同様に username の値が "user1"、password の値が "pass1"、ドキュメントIDを "doc001"、アタッチメント名を "att001" とすると以下のような URL になります:
https://user1:pass1@user1.cloudant.com/testdb/doc001/att001

ブラウザのアドレス欄に指定すると、こんな感じで表示できます:
2018101605



Cloudant のアタッチメント機能を使った簡易的なファイル保存のやりかたを紹介しました。Node-RED でウェブページを作る際の静的画像をどうするか、という問題を比較的簡単に解決する方法の1つだと思っています。


楽器苦手なオッサンエンジニアの 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 部分を取り扱うレベルのものは作れました。次はサンプリングデータ(波形データ)の中身をもう少し深掘りしてみたいと思ってます。




このページのトップヘ