はじめに
Spresense sdk 3.3.0 にBLEモジュールを使ったtoioの制御プログラムが公開されました。BLE1507モジュールはおそらく2022年頃には発売されていたのですが、
なかなか動かすことが出来なったのですがこのキッカケに、toioとの連携が出来るようになったので
toio本体のセンサやマットの絶対位置の推定機能とSpresenseのAudioプレーヤー機能を使った子供向け音声ゲームを作ってみました。
用意するもの
| 部品 | 個数 | 備考 |
|---|---|---|
| Spresenseメインボード | 1 | https://ssci.to/3900 |
| Spresense拡張ボード | 1 | https://ssci.to/3901 |
| BLE拡張ボード | 1 | NUS firmware版 https://www.marutsu.co.jp/pc/i/2493407/ |
| マイクロSDカード | 1 | 32G版 |
| Line入力ありスピーカー | 1 | https://amzn.asia/d/bJKkYGi |
| toioキューブ及びプレイマット | 1 |
https://www.switch-science.com/collections/toio/products/6300 に付属しています |
| toio充電器 | 1 | https://ssci.to/9578 |
| マイクロUSBケーブル | 1 | ー |
作成するゲームについて
プレイマットとtoioをコントローラとしてプレイマットの盤面に応じた鳴き声を再生し、動物を当てるゲームを作りました。
その他、toioの振動検出機能を使って音楽再生をするような機能も
加えています。
動作画像
ソフトウエア開発
サンプルコードのble_toio_centralとaudio_playerをマージする必要があります。
当方ソフトウェア開発の専門家ではないので、gemini2.5と相談しながら開発を進めました
最終的には、LLMと相談の結果toioの通信機能が実装されているサンプルコードをベースに
Audio機能を追加する方針で作成しました。
また、SDKのコンフィグは以下にて実行しました。
./tools/config.py examples/ble_toio_central feature/audio_player feature/startup_script
ソースコード
※ソースコードの解説はgeminiにお願いしました。↓ここから↓
【Spresense】toioの動きに合わせて音を奏でる:BLE制御とAudio Playerの統合実装
はじめに
SonyのSpresense SDK 3.3.0に含まれるサンプルコード ble_toio_central(toio Core CubeのBLE制御)をベースに、ハイレゾ対応オーディオプレーヤーの機能を統合し、「toioがぶつかったら効果音」「マットの上を走ると音量が変わる」といったインタラクティブなデモを作成しました。
この記事では、Spresenseで**マルチスレッド(BLE通信とオーディオ処理)**をどのように連携させるか、その実装ポイントを解説します。
システム構成
このアプリケーションは、主に以下の3つのスレッドで構成されています。
BLEのイベント処理と、負荷の高いオーディオ処理を分離し、排他制御を用いて安全に連携させる設計です。
-
Main スレッド
- BLEの初期化、スキャン開始。
- 各スレッドの生成。
-
Audio Playback スレッド (
audio_playback_thread)- Spresense Audio Subsystem (AS) の管理。
- 普段は待機状態 (
cond_wait)。 - 再生リクエストが来るとMP3ファイルのデコード・再生を実行。
-
toio Control スレッド (
toio_thread_main)- toioからのBLE通知(Notification)をポーリング。
- センサー値を解析し、条件に応じてAudioスレッドへシグナルを送信。
動作環境と準備
SDカードの準備
SpresenseのAudio Playerを動作させるため、SDカード直下に以下のディレクトリとファイルを配置する必要があります。
-
/mnt/sd0/BIN/- Spresense SDK提供のDSPバイナリ(MP3デコーダ等)
-
/mnt/sd0/AUDIO/- 再生用MP3ファイル
- 実装で使用しているファイル名例:
Sound3.mp3,Sound4.mp3,Sound130.mp3など
実装のポイント
1. スレッド間の排他制御と連携
SpresenseのAudio APIは、ステートマシンを厳密に管理する必要があります。そのため、toioスレッドから直接Audio APIを叩くのではなく、リクエストを投げてAudioスレッドに処理を任せる方式を採りました。
// グローバル変数:スレッド間通信用
static pthread_mutex_t g_audio_mutex = PTHREAD_MUTEX_INITIALIZER;
static pthread_cond_t g_audio_cond = PTHREAD_COND_INITIALIZER;
static volatile bool g_play_next_track_signal = false; // 再生要求フラグ
static volatile bool g_stop_audio_signal = false; // 停止要求フラグ
static char g_requested_track_title[64] = {0}; // 曲名受け渡し用
// toioスレッドから呼び出すヘルパー関数
static void audio_stop_and_prepare_next(const char* track_title) {
pthread_mutex_lock(&g_audio_mutex);
// 再生中なら停止フラグを立てる
if (g_is_audio_playing) {
g_stop_audio_signal = true;
}
// 次の曲名をセットして再生フラグを立てる
strncpy(g_requested_track_title, track_title, sizeof(g_requested_track_title) - 1);
g_play_next_track_signal = true;
// Audioスレッドを起こす
pthread_cond_signal(&g_audio_cond);
pthread_mutex_unlock(&g_audio_mutex);
}
2. 衝突検知 (Collision) で効果音再生
toioの sensor.collision_detected() を監視し、物理的な衝突を検知したら即座に効果音を再生します。
if (sensor.collision_detected())
{
printf("Motion: Collision detected!!\n");
// LEDを赤に点灯
light.on(255, 0, 0);
// "Sound3.mp3" の再生をリクエスト
audio_stop_and_prepare_next("Sound3.mp3");
}
3. 傾き (Posture) によるモード切替
toio本体のボタンが押されたとき、toioの傾き(姿勢)に応じて再生するトラックを切り替えるロジックです。
if (button.is_pressed())
{
int dir = sensor.posture();
if (dir == 4) { // 上向き
audio_stop_and_prepare_next("Sound4.mp3");
light.on(0, 255, 0); // 緑
} else if (dir == 5) { // 下向き
audio_stop_and_prepare_next("Sound5.mp3");
light.on(0, 0, 255); // 青
} else {
audio_stop_and_prepare_next("Sound6.mp3");
light.on(0, 255, 255); // 水色
}
}
4. マットの座標によるダイナミックな音量制御
これが今回の面白い機能の一つです。toioプレイマット上のX座標を取得し、位置によってSpresenseのマスターボリュームをリアルタイムに変更します。
- マットの左側 (X <= 900) : 音量を下げる
- マットの右側 (X > 900) : 音量を上げる
if (id.is_valid_position())
{
// 現在の座標デバッグ表示
printf("(x, y, angle) = (%4d, %4d, %3d)\n", id.x(), id.y(), id.angle());
// X座標に応じたボリューム調整
if (id.x() <= 900) {
// 音量を下げる(最小値クリップ付き)
current_volume -= 20;
if (current_volume < -1000) current_volume = -1000;
} else {
// 音量を上げる(最大値クリップ付き)
current_volume += 20;
if (current_volume > 0) current_volume = 0;
}
// Audio Subsystemへボリューム変更コマンドを送信
app_set_volume(current_volume);
}
5. Standard ID (カード/ステッカー) 連動
「トイオ・コレクション」などに付属するカードやステッカーをタッチした際、そのID (id.value()) に紐付いた音声を再生します。
if (id.is_valid_standard())
{
// 特定のIDを検知したら対応するMP3を再生
if(id.value() == 0x380130){
audio_stop_and_prepare_next("Sound130.mp3");
}
if(id.value() == 0x380131){
audio_stop_and_prepare_next("Sound131.mp3");
}
// ... (他のIDも同様に判定)
}
出力先の設定変更
Spresense SDKのAudio PlayerサンプルはデフォルトでI2S出力になっていることが多いですが、今回は拡張ボードのジャックから音を出したいため、audio_high_level_api.h の定数を使って以下のように変更しています。
/* 出力先をスピーカー/ヘッドホン端子に変更 */
#define PLAYER_OUTPUT_DEV AS_SETPLAYER_OUTPUTDEVICE_SPHP
#define PLAYER_MIXER_OUT AS_OUT_SP
まとめ
Spresenseの強力なマルチコア・マルチスレッド機能を活かすことで、BLEによるロボット制御を行いながら、同時に高音質なオーディオ再生を制御することができました。
特に、センサー値をトリガーにして pthread_cond_signal でオーディオスレッドを叩く構成は、Spresenseで「何かを検知して音を鳴らす」アプリケーションを作る際の定石として応用が効きそうです。
注意点
toio_thread_main 内で usleep(100 * 1000) していますが、ボリューム変更コマンド等のオーディオAPI呼び出しが頻発しすぎるとシステム全体のレスポンスが悪化する可能性があります。実運用ではコマンド送信間隔の間引き処理などを入れるとより安定します。