obnizでWAV再生/その場で話して音声再生

スポンサーリンク

かんたんにIoT工作ができるobnizはとても便利ですが、音については不満を持っている方もいらっしゃるのではないでしょうか。

公式のサンプルコードのページを探してもピーという音を音程を変えて出す機能しか見つかりません。

TwitterでWAV再生する方法が無いか質問したところ、obnizの中の人から、ユーザの@haseguruさんが作ったSPIkerというWAV再生環境があるとの情報を頂きました。

試したところ、ノイズは乗るもののWAVファイルを再生できました。

さらに、ブラウザ側に音声を録音してWAVデータをSPIkerに渡す機能を加えることで、しゃべった内容をobnizで再生することもできました。

本記事では、SPIkerでWAVファイルを再生する方法とブラウザで音声を録音してWAVデータを作り、SPIkerに渡して再生する方法を紹介します。

以下の動画で音声再生の様子が見られますので、よろしければご覧ください。

試行環境

以下の機材を使用して動作確認しました。

項目内容
obnizobniz Board
圧電スピーカ電子ブザー
Androidケータイ、ブラウザAndroid 10、Chrome 91.0.4472.101
パソコン(プログラム作成用)macOS Catalina 10.15.7

SPIkerの原理

SPIkerはSPI(Serial Peripheral Interface)という通信規格のデータライン(MOSI)に音声波形を乗せ、そのまま圧電スピーカに渡すことで音声を再生するという仕掛けです。

下図の左がobniz、右が圧電スピーカになります。

SPI signals
SPI通信(Wikipediaより)

4本の配線でつながっていますが、obnizと圧電スピーカではMOSIのみ使用します。

圧電スピーカのGND(黒)をpin0、MOSI信号(赤)をpin1につなぎます。

obniz and speaker
obnizと圧電スピーカの接続(obniz画像は公式サイトより)

SPI通信規格上、MOSIのデータをすべて音声情報で埋め尽くすことはできず、下図の灰色の部分のように、音声と無関係の情報が部分的に乗るため、これがノイズとして聞こえると思われます。

この原理を使う以上、仕方のないことと思います。

SPI通信波形(Wikipediaより)

WAVファイルの再生

@haseguruさんのページからSPIKerをダウンロードします。(一番下にSPIKerのリンクがあります)

GitHubにサンプルプログラムと再生に成功したWAVファイルを置いていますので、使用したい方はgit cloneまたは「Code」ボタンからダウンロードします。

git clone https://github.com/tak6uch1/obniz-SPIker

プログラムとWAVファイルを開発者コンソールリポジトリに登録して再生します。

obniz developer console

プログラムを新たに作成するには、「新規作成」を押します。

Newly create

以下の表示が出るので、公開・非公開を選んでファイル名を付けて作成します。

New program

新規にプログラムを作成すると最初からプラグラムが書かれていますが、全て消して以下をspeaker.htmlに貼り付けます。(GitHubからダウンロードしたファイルにもあります)

<!doctype html>
<html>
<head>
<meta charset="utf-8">
<title>効果音テスト</title>
<script src="https://unpkg.com/obniz@3.x/obniz.js"></script>
<script src="SPIker.js"></script>

<script>
var obniz = new Obniz("OBNIZ_ID_HERE");

var okSound = null;
var ngSound = null;
var speaker = null;

obniz.onconnect = async function () {
    Obniz.PartsRegistrate(SPIker);
    
    speaker = obniz.wired("SPIker", { signal:1, gnd:0 });
    wav0 = await speaker.convertFromURI("https://obniz.com/users/1365/repo/hello.wav");
    wav1 = await speaker.convertFromURI("https://obniz.com/users/1365/repo/hello_filter.wav");
}

async function play0() {
    speaker.play(wav0);
}

async function play1() {
    speaker.play(wav1);
}
</script>
</head>

<body>
<input type="button" value="wav0" onclick="play0();"><br>
<br>
<input type="button" value="wav1" onclick="play1();"><br>
</body>
</html>

OBNIZ_ID_HEREの部分は、お持ちのobnizの画面に表示されるID(4桁の数字、ハイフン、4桁の数字)で置き換えます。

続いて、WAVファイル(音声)をリポジトリにアップロードします。

Upload

上でダウンロードしたSPIker.jsをアップロードします。

hello.wavhello_filter.wavも同様にアップロードして以下のように表示されることを確認します。

File list

上の画像では、次の章で使用するinput_audio.htmlも作成済みの状態になっています。

再度speaker.htmlを開いて、画面上部にある「▶実行」ボタンを押すと以下の画面が出ます。

Execure speaker.html

wav0を押すとhello.wavを、wav1を押すとhello_filter.wavを再生します。

obnizにつないだスピーカから「こんにちは」と聞こえれば成功です。

どちらもノイズが乗った感じに聞こえますが、私の環境ではwav1の方が若干聞き取りやすかったです。

ブラウザで録音して再生

次はその場で録音してobnizで再生することを試していきます。

以下のプログラムをinput_audio.htmlとして作成します。(GitHubからダウンロードしたファイルにもあります)

<!doctype html>
<html>
<head>
<meta charset="utf-8">
<title>Input Audio</title>
<script src="https://unpkg.com/obniz@3.x/obniz.js"></script>
<script src="SPIker.js"></script>
<script src="https://code.jquery.com/jquery-2.0.3.min.js"></script>


<script>
var obniz = new Obniz("OBNIZ_ID_HERE");
var speaker = null;

obniz.onconnect = async function () {
    Obniz.PartsRegistrate(SPIker);
    
    speaker = obniz.wired("SPIker", { signal:1, gnd:0 });
    wav0 = await speaker.convertFromURI("https://obniz.com/users/1365/repo/hello.wav");
    wav1 = await speaker.convertFromURI("https://obniz.com/users/1365/repo/hello_filter.wav");
}

// Input Audio
window.AudioContext = window.AudioContext || window.webkitAudioContext;
var audioContext = new AudioContext();
const bufferSize = 1024;

var localMediaStream = null;
var localScriptProcessor = null;
audioData = []; // 録音データ

var onAudioProcess = function(e) {
  var input = e.inputBuffer.getChannelData(0);
  var bufferData = new Float32Array(bufferSize);
  for (var i = 0; i < bufferSize; i++) {
    bufferData[i] = input[i];
  }

  audioData.push(bufferData);
};
var startRecording = function() {
  navigator.getUserMedia(
    { audio: true },
    function(stream) {
      localMediaStream = stream;
      var scriptProcessor = audioContext.createScriptProcessor(bufferSize, 1, 1);
      localScriptProcessor = scriptProcessor;
      var mediastreamsource = audioContext.createMediaStreamSource(stream);
      mediastreamsource.connect(scriptProcessor);
      scriptProcessor.onaudioprocess = onAudioProcess;
      scriptProcessor.connect(audioContext.destination);
    },
    function(e) {
      console.log(e);
    }
  );
};
var exportWAV = function(audioData) {

  var encodeWAV = function(samples, sampleRate) {
    var buffer = new ArrayBuffer(44 + samples.length * 2);
    var view = new DataView(buffer);

    var writeString = function(view, offset, string) {
      for (var i = 0; i < string.length; i++){
        view.setUint8(offset + i, string.charCodeAt(i));
      }
    };

    var floatTo16BitPCM = function(output, offset, input) {
      for (var i = 0; i < input.length; i++, offset += 2){
        var s = Math.max(-1, Math.min(1, input[i]));
        output.setInt16(offset, s < 0 ? s * 0x8000 : s * 0x7FFF, true);
      }
    };

    writeString(view, 0, 'RIFF');  // RIFFヘッダ
    view.setUint32(4, 32 + samples.length * 2, true); // これ以降のファイルサイズ
    writeString(view, 8, 'WAVE'); // WAVEヘッダ
    writeString(view, 12, 'fmt '); // fmtチャンク
    view.setUint32(16, 16, true); // fmtチャンクのバイト数
    view.setUint16(20, 1, true); // フォーマットID
    view.setUint16(22, 1, true); // チャンネル数
    view.setUint32(24, sampleRate, true); // サンプリングレート
    view.setUint32(28, sampleRate * 2, true); // データ速度
    view.setUint16(32, 2, true); // ブロックサイズ
    view.setUint16(34, 16, true); // サンプルあたりのビット数
    writeString(view, 36, 'data'); // dataチャンク
    view.setUint32(40, samples.length * 2, true); // 波形データのバイト数
    floatTo16BitPCM(view, 44, samples); // 波形データ

    return view;
  };

  var mergeBuffers = function(audioData) {
    var sampleLength = 0;
    for (var i = 0; i < audioData.length; i++) {
      sampleLength += audioData[i].length;
    }
    var samples = new Float32Array(sampleLength);
    var sampleIdx = 0;
    for (var i = 0; i < audioData.length; i++) {
      for (var j = 0; j < audioData[i].length; j++) {
        samples[sampleIdx] = audioData[i][j];
        sampleIdx++;
      }
    }
    return samples;
  };

  var dataview = encodeWAV(mergeBuffers(audioData), audioContext.sampleRate);
  var audioBlob = new Blob([dataview], { type: 'audio/wav' });

  var myURL = window.URL || window.webkitURL;
  var url = myURL.createObjectURL(audioBlob);
  return url;
};        

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

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

async function stopRec(){
  $('#recBtn').css( 'display', 'block' );
  $('#stopBtn').css( 'display', 'none' );
  if( localScriptProcessor ){
    localScriptProcessor.disconnect();
    localScriptProcessor.onaudioprocess = null;
    localScriptProcessor = null;
  }
  var convertedData = await speaker.convertFromURI( exportWAV( audioData ) );
  speaker.play( convertedData );
  audioData = [];
}
</script>
</head>

<body>
  <div id="page">
    <div>
      <input type="button" id="recBtn" value="声かけ" onClick="startRec();" style="display:block;"/>
      <input type="button" id="stopBtn" value="ストップ" onClick="stopRec();" style="display:none;"/>
    </div>
  </div>
</body>
</html>

OBNIZ_ID_HEREの部分は、お持ちのobnizの画面に表示されるID(4桁の数字、ハイフン、4桁の数字)で置き換えます。

ブラウザ機能で音声をWAV形式のデータに変換し、Blobで一時ファイルとして保存、このファイルのURLをSPIkerに渡して音声再生という仕掛けになっています。

初回実行時は、マイクへのアクセス許可のポップアップが出る場合がありますので、許可してください。

実行すると以下の画面になりますので、「声かけ」を押して何か話しかけ「ストップ」(声かけボタンを押すとストップに変わる)を押してください。

Input Voice

obnizにつないだスピーカから、話した内容が再生されれば成功です。

何度か試して、聞き取りやすい発声のコツをつかんでください。

上記はMacとAndroidスマホの両方で動作確認できています。

まとめ

WAV再生環境SPIkerを利用して、WAV音声ファイルの再生とブラウザで録音した音声の再生について紹介しました。

片側のみの音声送信ですが、用途によっては使えると思います。

SPIのデータラインをそのまま音声再生に利用するという原理上、ノイズが乗るのは仕方ないと思いますが、ピー音だけだったのに比べるとできることの幅が広がりました。

この場をお借りして、SPIker開発者の@haseguruさんと紹介していただいたobnizの中の方にお礼申し上げます。

参考文献

タイトルとURLをコピーしました