概略の説明
RaspberryPi2BからGoogleスピーカに音声データを送信してGoogleスピーカ から再生する際に、Soxを使って変調した音声をリアルタイムで生成するシステムについての説明です。
RaspberryPi内に(A)node.js+Expressによるwebサーバおよび(B)Googleスピーカに対して再生する音声のURLを指示する機能を構築し、更に(A)に対して(A2)HTTPリクエストに応じてSoxを起動しその出力結果をExpressを通して送信する機能、を付加することでこの動作が実現できます。
システム構成
ここまでの説明をシステム構成にまとめると次の図のようになります。
「User Web Browser」の部分は上記の説明にはありませんが、実用的にシステムにアクセスするためのUIとして作成する必要があります。
本記事での説明箇所
本記事では、(A2)HTTPリクエストに応じてSoxを起動しその出力結果をExpressを通して送信する機能について説明します。
その他、Googleスピーカ・RaspberryPiを使用したシステムの過去記事はこちらを参照してください。(A)node.js+Expressによるwebサーバなども説明しています。
本記事作成に当たっての使用機器および開発環境
本記事は、以下の機器を使用してシステムの動作を検証しています。
- RaspberryPi 2B (OS : Raspberry Pi OS (32-bit) Lite)
(node.jsおよびTypeScript使用) - Google Nest mini
SOXのインストール
ターミナル等でRaspberryPi にログインして、以下のコマンドでインストールします。
sudo apt-get install alsa-utils sox libsox-fmt-all
以下、参考記事
https://mukudori-noise.skr.jp/RaspberryPi3_3.html
ソースコード(A2)
RaspberryPi上のnode.js環境に、TypeScriptで記述しています。以下は、(A2)HTTPリクエストに応じてSoxを起動しその出力結果をExpressを通して送信する機能に関するものです
import express from 'express';
const app: express.Express = express();
app.get("*.wav", (req: express.Request, res: express.Response, next: express.NextFunction) => {
const fs = require('fs');
// ここでファイルパスを取得
// path.joinの箇所は実際のサーバのディレクトリ構成等に合わせて適当に変更してください
const filepath = path.join(fileDir, decodeURI(req.path));
try {
fs.stat(filepath, (err, stat) => {
if (err) {
next(err)
}
// ファイル名をエンコードする
const basename = path.basename(filepath);
const filename = encodeURIComponent(basename);
// ヘッダーセットする
res.setHeader('Content-Type', 'audio/wav');
res.setHeader('Content-disposition', `inline; filename*=utf-8''${filename}`);
const { spawn } = require('node:child_process');
// 適当なsox effectsを設定
// req.queryで変数を渡して条件分岐をすると面白い
const command = `sox ${filename} -t wav - reverb`; // リバーブ
// const command = `sox ${filename} -t wav - echo 0.8 0.9 400 0.3 800 0.25 1200 0.1 1600 0.05`; // エコー
// const command = `sox -m -t sox "|sox ${filename} -t sox - pitch -190 echo 0.8 0.9 50 0.5" -t sox "|sox ${filename} -t sox - pitch 270 echo 0.8 0.9 60 0.8" -t wav -`; // 不協和音
// コマンドエラーになる場合があるのでタイムアウトを設定
let sp = spawn(command, [], { shell: true, timeout: 30000 });
sp.on('error', (err) => {
next(err);
sp.kill();
});
sp.on('close', (err) => {
console.log('spawn close');
});
// 標準出力をパイプでexpress.Responseに送る
sp.stdout.pipe(res).on('error', (err) => {
sp.kill();
slk.Err(err);
});
// express.Responseのcloseイベントを検出
// 念のため、確実にspawnプロセスをkillしておく
res.on('close', ()=> {
console.log('the response has been sent');
setTimeout(() => {
if (sp) if (!sp.killed) { sp?.kill(); }
}, 30000);
});
});
} catch (err) {
next(err);
}
});