音を分析 その2

 前回、音声を周波数分析してカラーマップ表示させたのですが、画像を作る部分の処理が重すぎてあたかもWeb Audio APIの処理が重いかのようなサンプルになってしまっていました。 今回は画像処理回りを修正したのでそのあたりを書いてみます。まずはサンプル。


サンプリング点数 : 
<select id="selectFftSize" >
  <option>32</option>
  <option>64</option>
  <option>128</option>
  <option>256</option>
  <option>512</option>
  <option selected>1024</option>
  <option>2048</option>
  <option>4096</option>
  <option>8192</option>
  <option>16384</option>
  <option>32768</option>
</select>点<br>
(1-音声ファイル) <input type="file" accept="audio/*" capture="microphone" id="recorder"><br>
(1-マイク) マイク On / Off : <button id="startmic">Start Mic</button><br>
(2,3) 再生/ストップ : <button id="playsound">Play</button><br>
(4) 色を変更:
<button id="jet" class="color_btn">jet</button>
<button id="hsv" class="color_btn">hsv</button>
<button id="hot" class="color_btn">hot</button>
<button id="origin"  class="color_btn">元の画像</button><br>

<p id="fft_res"></p>
<p id="monitor"></p>
<canvas id="canvas_id" width="512" height="100" style="border: 1px dashed;"></canvas><br>

<script src="colormap2.js"></script>

<script>
  var recorder  = document.getElementById('recorder');
  var playsound = document.getElementById('playsound');

  //  カラーマップ
  var cve  = document.getElementById("canvas_id");
  var ctx  = cve.getContext('2d');
  let cmid = new ColorMapImageData(ctx);
  let src  = ctx.getImageData(0, 0, 1, 1);

  // 変数初期化
  var timerId;
  var source;

  // AudioContextを作成
  window.AudioContext = window.AudioContext || window.webkitAudioContext;  
  var audioCtx    = new AudioContext();
  // 波形データを格納するバッファ
  var audioBuffer = null;
  // 音声の時間と周波数を解析するAnalyserNodeを生成
  var analyser = audioCtx.createAnalyser();
  // 平均化定数(0.0~1.0)
  // 0の場合は時間平均化しません。1にするとスペクトルはゆっくりと動きます。
  analyser.smoothingTimeConstant = 0.0;

  
  // --------------------
  // FFTしてデータを蓄積
  // --------------------
  var processor = audioCtx.createScriptProcessor(1024,1,1)
  processor.onaudioprocess = function(){
    var dataArray = new Uint8Array(analyser.frequencyBinCount);
    // 周波数分析結果データを取得
    analyser.getByteFrequencyData(dataArray);
    // 周波数分析結果を1ライン描画
    cmid.addRightLine(dataArray);

    //  周波数分析結果のいち部を表示
    // ピーク周波数を探して表示します。
    let N  = analyser.fftSize;
    let fs = analyser.context.sampleRate;
    let df = fs / N;
    let f  = dataArray.indexOf(Math.max(...dataArray),0) * df;
    document.getElementById('monitor').innerHTML = 
      '⊿f  : ' + df + 'Hz<br>' +
      'peak : ' + f + ' Hz';

  }


  // --------------------
  // カラーマップ
  // --------------------
  // 背景初期化
  ctx.fillStyle = "rgba(34, 34, 34, 1.0)";
  ctx.fillRect(0, 0, cve.width, cve.height);


  // --------------------
	// 	256階調のカラーに変換
	// --------------------
	// カラーマップの描画が終わったあとに色を変換
	const btns = document.querySelectorAll('.color_btn');
	btns.forEach(function(btn){
		btn.addEventListener('click', function(){
      if((this.id == 'jet') || (this.id == 'hsv') || (this.id == 'hot') || (this.id == 'origin')){
        if(this.id == 'origin'){
          ctx.putImageData(src, 0, 0);
        }else{
          if(this.id == 'jet'){ n = 'jet'; }
          if(this.id == 'hsv'){ n = 'hsv'; }
          if(this.id == 'hot'){ n = 'hot'; }
          let dst = ctx.createImageData(src.width, src.height);     // 作業領域
          ctx.putImageData( GradationMap(src, dst, colorMapArray(n)), 0, 0);
        }
      }
		});
	});

  // 分析停止
  function stopAnalyser(t=true){
    let sz = document.getElementById('selectFftSize');
    // ボタンを無効化
    btns.forEach(function(btn){
      if(t){
        //  停止
        // ボタンを有効化
        btn.removeAttribute('disabled');
        sz.removeAttribute('disabled');
        // カラーマップの状態を保存
        src = ctx.getImageData(0, 0, cve.width, cve.height);  // 元の画像
        // 分析条件を表示
        let N  = analyser.fftSize;
        let fs = analyser.context.sampleRate;
        let df = fs / N;
        let frange = analyser.frequencyBinCount * df;
        document.getElementById('fft_res').innerHTML=
          'サンプリング周波数:' + fs + ' Hz<br>' +
          'サンプリング点数 :' + N + ' 点<br>' +
          '周波数分解能:' + df + ' Hz<br>' +
          '周波数レンジ:0~' + frange + ' Hz';
      }else{
        //  再生
        // ボタンを無効化
        btn.setAttribute('disabled','disabled');
        sz.setAttribute('disabled','disabled');
        // 分析条件を設定
        analyser.fftSize = sz.value;
      }
    });
  }


  // --------------------
  //   ファイルを指定してバイナリデータを取得
  // --------------------
  recorder.addEventListener('change', function(e) {
    // 指定されたローカルファイルの File オブジェクトを取得
    var file = e.target.files[0];
    var fr = new FileReader();

    // --------------------
    // 読み込んだバイナリデータを音声データに変換
    // --------------------
    fr.onload = function() {
        // 読み込んだデータを再生できる形式に変換
        var arrayBuffer = fr.result;
        audioCtx.decodeAudioData(arrayBuffer, function(abuffer) {
          audioBuffer = abuffer;
        });
    };
    
    // --------------------
    // 指定されたファイルを読み込む
    // --------------------
    fr.readAsArrayBuffer(file);    
  });


  // --------------------
  //   サウンドを再生
  // --------------------
  // 音声ファイルまたはマイクの音の分析を再生する。
  var PlaySound = function(abuffer) {
    // AudioBufferSourceNode を作成
    source = audioCtx.createBufferSource();
    source.buffer = abuffer;
    // source -> analyser -> processor -> destination(分析)
    //      └-> destination(音を再生)
    source.connect(analyser);
    analyser.connect(processor);
    processor.connect(audioCtx.destination);
    source.connect(audioCtx.destination);
    // 再生開始
    source.start(0);

    // --------------------
    //  カラーマップ表示領域を作成
    // --------------------
    // 横軸時間、縦軸周波数とするので、高さをfrequencyBinCountにする。
    cmid.ctHeight = analyser.frequencyBinCount;
    ctx.fillStyle = "rgba(34, 34, 34, 1.0)";
    ctx.fillRect(0, 0, cve.width, cve.height);
  }

  // --------------------
  //   サウンドを再生・ストップ
  // --------------------
  // 音声ファイルまたはマイクの音の分析を再生・ストップする。
  window.onload = function() {  
    // 再生/停止
    document.getElementById("playsound").addEventListener("click", function(event){
        var label; 
        if(event.target.innerHTML=="Stop") {
            // サウンドを停止
            stopAnalyser();
            processor.disconnect();
            source.stop(0);
            label = "Start";
        } else {
            // サウンドを再生
            stopAnalyser(false);
            PlaySound(audioBuffer);
            label = "Stop";
        }
        event.target.innerHTML = label;
    });
  };


  // --------------------
  //   マイクを開始
  // --------------------
  var astream, micsrc;
  function Mic(){
    //  ユーザーにマイクの許可を求めます。
    navigator.mediaDevices.getUserMedia({ audio: true, video: false })
      .then(function(stream){
            astream = stream; // マイクを停止する際に使用
            micsrc = audioCtx.createMediaStreamSource(stream);
            // micsrc.connect(audioCtx.destination); // マイクの音は再生しない
            micsrc.connect(analyser);
            analyser.connect(processor);
            processor.connect(audioCtx.destination);
            // source -> analyser -> processor -> destination(分析)
            //    └☓-> destination(音を再生)
          })
      .catch(function(e) { console.error(e)});
  }

  // --------------------
  //   マイクのON/OFF
  // --------------------
  document.querySelector("button#startmic").addEventListener("click", function(event){
      var label;
      if(event.target.innerHTML=="Start Mic") {
          // マイク ON
          Mic();
          label="Stop Mic";
      } else {
          // マイク OFF
          (astream.getAudioTracks())[0].stop();
          processor.disconnect();
          label="Start Mic";
      }
      event.target.innerHTML=label;
  });


</script>

サンプル

 長いなー。本体は前回と殆ど変わっていません。描画処理を変更しました。描画を最後に行うのではなく、分析結果が出たらその都度描画するようにしています。 また、マイクからの音声を再生しないように変更しました。よくよく考えてみるとマイクの音をスピーカーから出すとハウリングしちゃうんで、邪魔なだけでした。

 描画負荷を軽くすると周波数分析結果をほぼタイムラグ無しで表示することが出来ることが確認できました。 スマホでもちゃんと動きました。専用アプリいらないとかブラウザすごいな。

 今回作って見て気がついたのですが、どうやらgetByteFrequencyData()での分析は、ステレオ音声はモノラル音声に変換されてから行われるようです。 それどころかファイルから再生した音とマイクが拾った音を同時再生すると、合成した音声を分析していました。 左チャンネルと右チャンネルを別々に周波数分析したい場合は別の手を考える必要がありそうです。 位相も出せないですし、あくまで簡易的な実装…というかそこまで細かい需要を想定していないんでしょうね。そりゃそうか。(´・ω・`)