かんたんにIoT工作ができるobnizはとても便利ですが、音については不満を持っている方もいらっしゃるのではないでしょうか。
公式のサンプルコードのページを探してもピーという音を音程を変えて出す機能しか見つかりません。
TwitterでWAV再生する方法が無いか質問したところ、obnizの中の人から、ユーザの@haseguruさんが作ったSPIkerというWAV再生環境があるとの情報を頂きました。
試したところ、ノイズは乗るもののWAVファイルを再生できました。
さらに、ブラウザ側に音声を録音してWAVデータをSPIkerに渡す機能を加えることで、しゃべった内容をobnizで再生することもできました。
本記事では、SPIkerでWAVファイルを再生する方法とブラウザで音声を録音してWAVデータを作り、SPIkerに渡して再生する方法を紹介します。
以下の動画で音声再生の様子が見られますので、よろしければご覧ください。
試行環境
以下の機材を使用して動作確認しました。
| 項目 | 内容 |
|---|---|
| obniz | obniz Board |
| 圧電スピーカ | 電子ブザー |
| Androidケータイ、ブラウザ | Android 10、Chrome 91.0.4472.101 |
| パソコン(プログラム作成用) | macOS Catalina 10.15.7 |
SPIkerの原理
SPIkerはSPI(Serial Peripheral Interface)という通信規格のデータライン(MOSI)に音声波形を乗せ、そのまま圧電スピーカに渡すことで音声を再生するという仕掛けです。
下図の左がobniz、右が圧電スピーカになります。

4本の配線でつながっていますが、obnizと圧電スピーカではMOSIのみ使用します。
圧電スピーカのGND(黒)をpin0、MOSI信号(赤)をpin1につなぎます。

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

WAVファイルの再生
@haseguruさんのページからSPIKerをダウンロードします。(一番下にSPIKerのリンクがあります)
GitHubにサンプルプログラムと再生に成功したWAVファイルを置いていますので、使用したい方はgit cloneまたは「Code」ボタンからダウンロードします。
git clone https://github.com/tak6uch1/obniz-SPIker
プログラムとWAVファイルを開発者コンソールのリポジトリに登録して再生します。

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

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

新規にプログラムを作成すると最初からプラグラムが書かれていますが、全て消して以下を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ファイル(音声)をリポジトリにアップロードします。

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

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

上の画像では、次の章で使用するinput_audio.htmlも作成済みの状態になっています。
再度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に渡して音声再生という仕掛けになっています。
初回実行時は、マイクへのアクセス許可のポップアップが出る場合がありますので、許可してください。
実行すると以下の画面になりますので、「声かけ」を押して何か話しかけ「ストップ」(声かけボタンを押すとストップに変わる)を押してください。

obnizにつないだスピーカから、話した内容が再生されれば成功です。
何度か試して、聞き取りやすい発声のコツをつかんでください。
上記はMacとAndroidスマホの両方で動作確認できています。
まとめ
WAV再生環境SPIkerを利用して、WAV音声ファイルの再生とブラウザで録音した音声の再生について紹介しました。
片側のみの音声送信ですが、用途によっては使えると思います。
SPIのデータラインをそのまま音声再生に利用するという原理上、ノイズが乗るのは仕方ないと思いますが、ピー音だけだったのに比べるとできることの幅が広がりました。
この場をお借りして、SPIker開発者の@haseguruさんと紹介していただいたobnizの中の方にお礼申し上げます。
書籍紹介
obnizの使い方はもちろんのこと、センサーやモーターを使った電子工作やmicro:bitなど他の機器との接続方法も説明されていて、入門から応用まで幅広い知識が身につきます。
参考文献
- パーツライブラリSPIkerをobnizのリポジトリに置いて効果音を鳴らす(https://theokiba.awe.jp/iot/tips_spiker_on_obn_rep/index.html)
- パーツライブラリ – SPIker(https://theokiba.awe.jp/iot/pl_spiker/index.html#example2)
- getUserMediaで録音したデータをWAVファイルとして保存する(https://qiita.com/HirokiTanaka/items/56f80844f9a32020ee3b)