つくったもの
SpresenseでARDUBOYのようなゲーム実行基盤をつくり、オリジナルゲームを作り楽しみたいと思いました。
今回は椅子取りゲームをつくりました。
椅子取りゲーム実行中の動画:
@youtube
コンセプト
Spresenseでオリジナルゲームを作るにあたりつぎのコンセプトを考えました。
プレイ環境
ゲームを楽しめる要素は何か?、なぜ往年のファミコン・スーパーファミコンは流行したのか、を考えてみました。
結果、つぎの要素があると思いました。
- 専用ハードウェアで遊べる
- 共通の操作性で遊べる
専用ハードウェアで遊べる
ファミコン・スーパーファミコンは専用ハードウェアを用意すればソフトウェアを入れ替えて遊べます。
またソフトウェアを持ち運べる、という特徴もあります。
今回のSpresenseを使ったオリジナルゲームもこれを実現したいと思いました。
Spresenseと後述するハードウェアを用意すればゲームをプレイできることを考えました。
共通の操作性で遊べる
ゲーム機にはコントローラが付属しています。コントローラを通じてゲームを進行します。
コントローラがあればソフトウェアごとの振る舞いは変わりますがゲームができます。
今回のSpresenseを使ったオリジナルゲームではAPS学習ボードという基板を使うことで椅子取りゲーム以外のゲームでも同じ操作感でゲームできることを考えて開発しました。
コンセプトを実現するための技術要素
前述のコンセプトを実現するためにどのような技術要素を使っているか説明します。
NuttX, Spresense SDK
SpresenseはArduino, Spresense SDKの2つの環境で開発・実行することが可能です。
Spresense SDKは組込みリアルタイムOS NuttXをベースにして動きます。
Arduinoは電源ONでお馴染みのsetup, loop関数を実行します。
Spresense SDKは電源ONでシェルが起動し、実行するアプリケーションをユーザーが選択します。
ユーザーがプレイしたいゲームを選べる・任意のタイミングでゲームを終了し別なゲームをプレイする、というシーンを考えた場合にSpresense SDKがゲームの実行環境に適切だと思いました。
そういった理由からSpresense SDKでゲームを開発しました。
Spresense SDKのバージョンは執筆当時の最新版【v2.6.0 (2022/08/05)】で確認しています。
ローダブル ELF
ローダブルELFとはOSとアプリケーションを別々のバイナリで作成し、動作時にアプリケーションをロードして実行できる機能です。
ゲームを別々のバイナリにしてSDカードに保存し、ゲームするときはシェルから実行するようにしたいと思いました。
この機能を使うことで専用ハードウェアで遊べるに書いた【Spresenseを用意すればソフトウェアを持ち運べる】のコンセプトを達成することができます。
ローダブルELF 参考URL:
https://developer.sony.com/develop/spresense/docs/sdk_tutorials_ja.html#_ローダブルelfチュートリアル
- ローダブルELFチュートリアル
部品
使用するハードウェアはつぎのとおりです。
Spresense メインボード
プロセッサCXD5602が搭載されているボードです。
Spresense メインボード 参考URL:
https://developer.sony.com/develop/spresense/docs/introduction_ja.html#spresense%E3%83%A1%E3%82%A4%E3%83%B3%E3%83%9C%E3%83%BC%E3%83%892.1.1. Spresense メインボード
Spresense 拡張ボード
Spresense拡張ボードはArduino Uno互換のピンソケットに加えて次の端子があります。
- ヘッドホンジャック
- micro SD カードスロット
- USB コネクタ
- マイク接続用ピンヘッダー
Spresense拡張ボード 参考URL
https://developer.sony.com/develop/spresense/docs/introduction_ja.html#spresense%E6%8B%A1%E5%BC%B5%E3%83%9C%E3%83%BC%E3%83%892.1.2. Spresense 拡張ボード
APS 学習ボード
APS学習ボードはSpresense拡張ボードにスタックして使うボードです。
ボードにつぎのデバイスを搭載可能です。
- LED 2個
- タクトスイッチ 2個
- アナログコンデンサマイク 4個
- ILI9341 2.2インチLCD
- リセット用タクトスイッチ
椅子取りゲームではLED、タクトスイッチを使います。
現在だとchip1stopから購入可能です。実装部品は別途購入する必要があります。
APS学習ボード 参考URL:
https://www.aps-web.jp/academy/amp/8467/
APS学習ボード(SPRESENSE Extension Board用)|取扱説明書
APS学習ボード購入先 参考URL
https://www.chip1stop.com/view/dispDetail/DispDetail?partId=APS1-0000001
実装部品
実装部品はつぎの表のとおりです。
No | 品名 | 個数 | 型番 |
---|---|---|---|
1 | 高輝度3mm青色LED | x1 | OSB56A3131A |
2 | 超高輝度3mmオレンジ色LED | x1 | OS5OAA3131A |
3 | 超高輝度3mm緑色LED | x1 | OSG58A3131A |
4 | ピンヘッダ 1x40pin 2.54mmピッチ、リード長6.1mm | x1 | PH-1x40SG |
5 | ピンソケット 1x10pin | x2 | FH-1x10SG/RH |
6 | ピンソケット 1x3pin | x1 | FH-1x3SG/RH |
7 | ピンソケット 1x10pin | x1 | FH-1x10SG/RH |
8 | タクトスイッチ(黒色) | x3 | P-03647 |
9 | 炭素皮膜抵抗 1/6W 1kΩ | x3 | |
10 | 炭素皮膜抵抗 1/6W 10kΩ | x3 | |
11 | 炭素皮膜抵抗 1/6W 2.2kΩ | x4 |
No. 1〜3 LED
参考入手先 http://akizukidenshi.com/catalog/g/gI-06411/
No. 4 ピンヘッダ
参考入手先 http://akizukidenshi.com/catalog/g/gC-00167/切断して以下のピンヘッダをつくる
- 1x6 pin x1
- 1x8 pin x2
- 1x10 pin x1
No. 7 ピンソケット
切断してピンソケット 1x9pin をつくる
No. 8 タクトスイッチ
参考入手先 https://akizukidenshi.com/catalog/g/gP-03647/
開発環境構築
各種開発環境を構築します。
Spresense SDK
Spresense SDKの開発環境を構築します。
私はmacOS Monterey バージョン12.5.1のホストPCで開発環境を構築しました。
Linux、Windowsでも環境構築可能です。
Spresense SDKの開発環境構築はWebのドキュメントとおりに進めれば完了できます。
SpresenseドキュメントにはCLI・IDE(Visual Studio Code)の2とおりの環境構築方法が記載されています。
私はCLIで環境構築したのでこの記事の記載はCLIの記載・手順となります。
Spresense SDKの開発環境構築 参考URL
https://developer.sony.com/develop/spresense/docs/sdk_set_up_ja.html#_開発ツールのセットアップSpresense SDK スタートガイド (CLI 版)
2. 開発ツールのセットアップ
ゲームのソースコード取得
今回作ったゲームのソースコードを私のGitHubからZIPでダウンロードします。
GitHub URL
https://github.com/grace2riku/spresense_game
ダウンロードしたZIPファイルは開発構築したSpresenseディレクトリに解凍します。
デフォルトだとつぎのディレクトリになります。
- /Users/ユーザー名/spresense
ここにコピーするのでつぎのようになります。
- /Users/ユーザー名/spresense/spresense_game-main
spresense_game-mainディレクトリのmusical_chairsが椅子取りゲームです。
椅子取りゲームの作成はWebドキュメント の手順とおりに作業しました。
アプリケーション作成方法
https://developer.sony.com/develop/spresense/docs/sdk_set_up_ja.html#_ツールを使用する
6. ユーザーアプリの追加方法 -> 6.3. ツールを使用する
コンフィグ
コンフィグレーションの手順を書きます。
/Users/ユーザー名/spresense/sdk ディレクトリに移動します(カレントディレクトリがspresenseの前提とします)。
$ cd sdk
インストールしたツールを使用可能にするために、つぎのおまじないのコマンドを実行します。
source ~/spresenseenv/setup
こちらのコンフィグレーションを指定します。
$ tools/config.py examples/audio_player examples/audio_beep examples/sixaxis feature/loadable device/sdcard feature/usbmsc
コンフィグレーションを変更します。
$ tools/config.py -m
Application Configurationを選択します。
BMI160 sensor exampleを選択しスペースキーを押下します。
BMI160 sensor exampleの*を空白スペースに変更します。
Escキーを押下し、ひとつ上の階層に移動します。下図のようにSpresense SDKを選択します。
Audio beep example、Audio player exampleを選択しスペースキーを押下します。
下図のように*を空白スペースに変更します。
Escキーを押下し、ひとつ上の階層に移動します。
下図のようにSpSesense_gameを選択します。
electric_guitar・musical chairs・shooting_watchを選択し下図のようにコンフィグレーションを変更します。
- electric_guitar: Mに設定(バイナリを別ファイルにする。SDカードに保存し実行する想定)
- musical chairs: *に設定(nuttx.spkファイルに組み込まれる)
- shooting_watch: Mに設定(バイナリを別ファイルにする。SDカードに保存し実行する想定)
本当であればmusical chairs(椅子取りゲーム)もMに設定し、別ファイルにしたかったのですが、この後のmakeでエラーになり原因理解・問題解決できなかったためnuttx.spkファイルに組み込むようにしました。
※electric_guitar、shooting_watchはエレキギターとは別のゲームです。
Escキーを押下し、ひとつ上の階層に移動します。Escキーを押下し続けて下図の画面に移動します。
コンフィグレーションをセーブするか聞いてくるのでYesを選択します。
makeを実行して、と表示されます。
これでコンフィグレーションは終了です。
$ tools/config.py -m
LN: include/arch to arch/arm/include
LN: include/arch/board to /Users/k-abe/spresense/nuttx/boards/arm/cxd56xx/spresense/include
LN: include/arch/chip to arch/arm/include/cxd56xx
LN: arch/arm/src/board to /Users/k-abe/spresense/nuttx/boards/arm/cxd56xx/spresense/../common
LN: arch/arm/src/board/board to /Users/k-abe/spresense/nuttx/boards/arm/cxd56xx/spresense/src
LN: arch/arm/src/chip to arch/arm/src/cxd56xx
LN: /Users/k-abe/spresense/nuttx/drivers/platform to /Users/k-abe/spresense/nuttx/boards/arm/cxd56xx/spresense/../drivers
LN: platform/board to /Users/k-abe/spresense/sdk/apps/platform/dummy
make[2]: Nothing to be done for `preconfig'.
configuration written to .config
*** End of the configuration.
*** Execute 'make' to start the build or try 'make help'.
make
makeします。-jオプションで並列ビルドが可能です。
$ make -j
makeが正常終了するとつぎの表示になります(make実行結果の最終行付近のみ抽出し引用しました)。
OSとアプリが組み込まれたnuttx.spkファイルが作成されます。
AR (create): libboard.a cxd56_main.o cxd56_clock.o cxd56_bringup.o cxd56_appinit.o cxd56_power.o cxd56_ioctl.o cxd56_ostest.o cxd56_gpioif.o cxd56_pwm.o cxd56_spi.o cxd56_sdcard.o cxd56_boot.o cxd56_audio.o cxd56_uid.o cxd56_crashdump.o cxd56_sensors.o cxd56_bmi160_i2c.o cxd56_netinit.o cxd56_flash.o cxd56_usbmsc.o cxd56_i2cdev.o cxd56_spidev.o
LD: nuttx
Generating: nuttx.spk
make[2]: Nothing to be done for `all'.
tools/cxd56/mkspk -c2 nuttx nuttx nuttx.spk;
File nuttx.spk is successfully created.
Done.
nuttx.spkファイルはsdkディレクトリに作成されます。
書き込み
nuttx.spkファイルをSpresenseに書き込みます。
まずはホストPCとSpresenseメインボードをUSBケーブルで接続します。
つぎのコマンドでSpresenseメインボードのシリアルポートのデバイスファイル名を調べます。
$ ls /dev/cu.usb*
/dev/cu.usbserial-141410
デバイスファイル名は/dev/cu.usbserial-141410であることがわかりました。
デバイスファイル名は読者のみなさんの環境で異なると思いますのでデバイスファイ名は適宜読み替えてください。
つぎのコマンドを実行しmakeで作成されたnuttx.spkをSpresenseに書き込みます。
tools/flash.sh -c /dev/cu.usbserial-141410 nuttx.spk
正常に書き込みが終了するとつぎの表示になり、Spresenseが再起動します。
>>> Install files ...
install -b 115200
Install nuttx.spk
|0%-----------------------------50%------------------------------100%|
######################################################################
301472 bytes loaded.
Package validation is OK.
Saving package to "nuttx"
updater# sync
updater# Restarting the board ...
reboot
シェルと接続
書き込みが終了したらNuttxのシェル(nsh)と接続します。
つぎのコマンドを実行します。
私はターミナルにminicomを使用しました。他のターミナルでも接続できると思います。
minicom -D /dev/cu.usbserial-141410 -b 115200
シェルに接続できるとnsh>のように表示されます。
Welcome to minicom 2.8
OPTIONS:
Compiled on Jan 4 2021, 00:04:46.
Port /dev/cu.usbserial-141410, 12:35:17
Press Meta-Z for help on special keys
NuttShell (NSH) NuttX-10.2.0
nsh>
helpコマンドを実行してみます。Builtin Apps:につぎがあればOKです。
- msconn : USB MSC機能接続コマンド
- msdis : USB MSC機能接続解除コマンド
- musical_chairs : 椅子取りゲーム
nsh> help
help usage: help [-v] [<cmd>]
. cmp false ls nslookup sleep usleep
[ dirname free mkdir poweroff source xd
? date help mkfatfs ps test
basename dd hexdump mkfifo pwd time
break df ifconfig mkrd reboot true
cat echo ifdown mksmartfs rm uname
cd exec ifup mount rmdir umount
cp exit kill mv set unset
Builtin Apps:
msconn msdis musical_chairs nsh
ゲームの説明
椅子取りゲームについて説明します。
ゲーム概要説明
一般的な椅子取りゲームの構成要素はつぎになると思います。
- 音源
- 音源を再生する装置
- 音源を任意の長さで一時停止⇄再生するしかけ
- 椅子: 1つ以上必要
- プレイヤー: (椅子の数 + 1)人
今回作ったSpresenseを使った椅子取りゲームは構成要素1, 2, 3の機能を持ちます。
プレイ画面
以降にゲーム実行開始からの手順を書きます。
シェルに接続し、椅子取りゲームのアプリ名(musical_chairs)を入力します。
SW2押下で音楽が再生されること、SW1とSW2押下でゲームが終了するメッセージが表示されます。
ここでのSW1、SW2とはAPS学習ボードのタクトスイッチSW1、SW2です。
nsh> musical_chairs
Start musical_chairs.
----- Press SW2 to play music.-----
----- Press SW1 and SW2 to end the game.-----
SW2押下でつぎの表示になります。
音楽ファイルが再生されます。
state:stop play_or_pause_trigger.
任意の時間で再生が止まりつぎの表示になります。
音楽再生状態から一時停止の状態になったことを示しています。
state:play goto pause state.
もう一度SW2を押下するとつぎの表示になり、音楽ファイルが再生されます。
【Attention】とエラーコードのような表示がでていますが音楽ファイルは再生されます。
表示されているメッセージはとくに調査していません。
音楽ファイルの最後まで再生⇄一時停止を繰り返します。
state:pause goto play state.
Attention: module[11][0] attention id[1]/code[20] (components/decoder/decoder_component.cpp L511)
Attention!! components/decoder/dAttention: module[11][0] attention id[1]/code[20] (components/decoder/decoder_component.cpp L511)
ecoder_comp L511 ecode 1 subcode 20
Attention!! components/decoder/decoder_comp L511 ecode 1 subcode 20
音楽ファイルを最後まで再生すると停止し、つぎの表示になります。
state:play goto stop state.
SW1とSW2を同時に押すとつぎのメッセージが表示され、ゲームを終了します。
exit switch pressed. goto exit.
Exit musical_chairs.
環境構築
椅子取りゲームの環境構築について書きます。
前述した開発環境構築は完了している前提とします。
ハードウェア構成
椅子取りゲームをプレイするにはコンセプトで書いたハードウェアの他につぎのハードウェアが必要です。
- スピーカー
スピーカー
音源を再生するために必要です。Spresense拡張ボードに接続します。
私は家にあった汎用的なスピーカーを使用しました。
その他
椅子取りゲームの音楽ファイルを再生するためにつぎの準備が必要です。
- デコード処理を行うDSPバイナリ
- 音楽ファイル
- プレイリスト
こちらの準備はWebドキュメントのチュートリアル 4.1.1. ビルド&ロード手順の4〜6に書いてあります。
Audio Player サンプルアプリケーション ビルド&ロード手順 参照先URL:
https://developer.sony.com/develop/spresense/docs/sdk_tutorials_ja.html#_ビルドロード手順4.1.1. ビルド&ロード手順の4〜6
デコード処理を行う DSP バイナリ
音楽ファイル再生にはでコード処理を行うDSPバイナリが必要です。
こちらのバイナリファイルをアプリケーションで読み込み、音楽ファイルを再生します。
今回はMP3の音楽ファイルのデコードを行うDSPバイナリファイル(MP3DEC)をマイクロSDカードに保存します。
MP3のDSPバイナリはつぎのディレクトリに格納されています。
- /Users/ユーザー名/spresense/sdk/modules/audio/dsp/MP3DEC
このファイルを前述したWebドキュメントのチュートリアル 4.1.1. ビルド&ロード手順の手順4のとおり、マイクロSDカードのルートにBINディレクトリをつくりコピーします。
音楽ファイル
音楽ファイルを前述したWebドキュメントのチュートリアル 4.1.1. ビルド&ロード手順の手順5のとおり、マイクロSDカードのルートにAUDIOディレクトリをつくりコピーします。
プレイリスト
椅子取りゲームはサンプルプログラムexamples/audio_playerをベースに作っています。
examples/audio_playerは前述したWebドキュメントのチュートリアル 4.1.1. ビルド&ロード手順の手順6に記載のとおり【簡易PlayListを利用した再生】を行っています。
そのため簡易PlayListをマイクロSDカードのルートにPLAYLISTディレクトリの中に作成します。
プレイリストの中身
プレイリストの中身はつぎのファイルに説明があります。
- /Users/ユーザー名/spresense/sdk/modules/audio/playlist/README.txt
上記ファイルを参考に今回はつぎの内容にしました。
maoutamasii_old-music_boss07.mp3,Anyone,1stAlbum,2,16,44100,mp3
項目の意味はつぎのとおりです。
- maoutamasii_old-music_boss07.mp3 : 音楽ファイル名
- Anyone : 著者名
- 1stAlbum : アルバム名
- 2 : チャンネル数
- 16 : bit長
- 44100 : サンプリングレート
- mp3 : フラグ。音楽ファイルのフォーマットでmp3かwavを指定する。
ソースコード解説
椅子取りゲームのソースコードについて解説します。
状態の導入
椅子取りゲームはexamples/audio_playerのサンプルプログラムをベースに実装します。
examples/audio_playerは10秒間音楽ファイルを5回再生して終了する動作となっています。
この構造を椅子取りゲームを実現するための構造に変更する必要があります。
椅子取りゲームはつぎの状態で振る舞いが変わるのでその状態を実装していきます。
-
停止状態: 音楽ファイルが1秒も再生していない状態です。
ゲーム開始時は停止状態になります。音楽ファイルを最後まで再生するとこの状態に戻ります。 -
再生状態: 音楽ファイルを再生している状態です。
椅子取りゲームのプレイ状況でいうとプレイヤーが椅子の周りを回り、音楽が停止するタイミングを伺っている状況です。 -
一時停止状態: 音楽ファイルの再生が停止した状態です。
椅子取りゲームのプレイ状況でいうとプレイヤーは音楽が停止を認識し競って椅子に座ろうとしている状況です。
椅子取りが決着したら音楽ファイル再生するためにSW2を押し、再生状態に移行します。
再生開始
SW2押下が音楽ファイル再生のトリガです。
停止状態からSW2押下でplay_or_pause_triggerがtrueになります。
音楽ファイルを再生するにはリスト:メインループの再生開始箇所のapp_start関数を呼び出します。
メインループの再生開始箇所
for (;;)
{
switch (game_state) {
case STOP:
if (play_or_pause_trigger) {
play_or_pause_trigger = false;
printf("state:stop play_or_pause_trigger.\n");
/* Running... */
// printf("Running time is %d sec\n", PLAYER_PLAY_TIME);
/* Start player operation. */
if (!app_start())
{
printf("Error: app_start_player() failure.\n");
/* Abnormal termination processing */
goto errout_start;
}
game_state = PLAY;
printf("state:stop goto play state.\n");
}
break;
app_start関数は初期化のapp_init_player関数、再生開始のapp_play_player関数を呼び出しています。
app_start関数
static bool app_start(void)
{
/* Init Player */
Track *t = &s_player_info.file.track;
if (!app_init_player(t->codec_type,
t->sampling_rate,
t->channel_number,
t->bit_length))
{
printf("Error: app_init_player() failure.\n");
app_close_play_file();
return false;
}
/* Play Player */
if (!app_play_player())
{
printf("Error: app_play_player() failure.\n");
app_close_play_file();
return false;
}
return true;
}
リスト:app_init_player関数は音楽再生の初期化です。
音楽再生の初期化ついてはつぎのWebドキュメントの該当箇所を見ると分かると思います。
5.3.3.10. 各機能の詳細からページを送っていくとInitialize and Start playerの記載箇所があります。
音楽再生の初期化
https://developer.sony.com/develop/spresense/docs/sdk_developer_guide_ja.html#_各機能の詳細
5.3.3.10. 各機能の詳細 Initialize and Start player
app_init_player関数
static int app_init_player(uint8_t codec_type,
uint32_t sampling_rate,
uint8_t channel_number,
uint8_t bit_length)
{
AudioCommand command;
command.header.packet_length = LENGTH_INIT_PLAYER;
command.header.command_code = AUDCMD_INITPLAYER;
command.header.sub_code = 0x00;
command.player.player_id = AS_PLAYER_ID_0;
command.player.init_param.codec_type = codec_type;
command.player.init_param.bit_length = bit_length;
command.player.init_param.channel_number = channel_number;
command.player.init_param.sampling_rate = sampling_rate;
snprintf(command.player.init_param.dsp_path,
AS_AUDIO_DSP_PATH_LEN,
"%s",
DSPBIN_FILE_PATH);
AS_SendAudioCommand(&command);
AudioResult result;
AS_ReceiveAudioResult(&result);
return printAudCmdResult(command.header.command_code, result);
}
リスト:app_play_player関数は音楽再生です。
app_play_player関数
static int app_play_player(void)
{
AudioCommand command;
command.header.packet_length = LENGTH_PLAY_PLAYER;
command.header.command_code = AUDCMD_PLAYPLAYER;
command.header.sub_code = 0x00;
command.player.player_id = AS_PLAYER_ID_0;
AS_SendAudioCommand(&command);
AudioResult result;
AS_ReceiveAudioResult(&result);
return printAudCmdResult(command.header.command_code, result);
}
音楽再生についてはつぎのWebドキュメントの該当箇所を見ると分かると思います。
5.3.3.10. 各機能の詳細からページを送っていくとStart Playerの記載箇所があります。
音楽再生参考URL:
https://developer.sony.com/develop/spresense/docs/sdk_developer_guide_ja.html#_各機能の詳細
5.3.3.10. 各機能の詳細 Start Player
うえのリンク先に記載の【図 13. Player State sequence】も初期化、再生の一連のシーケンスがわかりやすいです。
任意時間の再生について
examples/audio_playerは10秒間の音楽ファイル再生ですが、椅子取りゲームでは任意の長さで再生できるように変更しています。
任意の長さはリスト:任意時間の指定のように再生開始する時に3秒〜11秒の間で乱数により指定しています。
任意時間の指定
case PLAY:
music_play_time = 3 + rand() % 8;
app_play_process(music_play_time, &game_event);
再生中の動作
再生中はリスト:再生のapp_play_process関数内をループします。
この関数はつぎの条件を満たすと終了します。
- 音楽ファイルを最後まで再生した場合。引数にMUSIC_ENDをセットし終了する。
- 音楽ファイル再生中にエラーが発生した場合。引数にERROR_HAPPENをセットし終了する。
- ゲーム終了条件(APS学習ボードのSW1・SW2が同時に押された)が成立した場合。引数にEND_REQUESTをセットし終了する。
- 引数で指定された任意時間の再生が終了した場合。引数にMUSIC_PAUSEをセットし終了する。
再生中
void app_play_process(uint32_t play_time, GAME_EVENT* p_game_event)
{
/* Timer Start */
time_t start_time;
time_t cur_time;
int fifo_state_result;
time(&start_time);
do
{
/* Check the FIFO every 2 ms and fill if there is space. */
usleep(2 * 1000);
if (!app_refill_simple_fifo(s_player_info.file.fd, &fifo_state_result))
{
if (fifo_state_result == FIFO_RESULT_EOF) {
*p_game_event = MUSIC_END;
} else {
*p_game_event = ERROR_HAPPEN;
}
return;
}
#ifdef CONFIG_EXAMPLES_AUDIO_PLAYER_USEPOSTPROC2
static int cnt = 0;
if (cnt++ > 100)
{
app_send_setpostproc_command();
cnt = 0;
}
#endif /* CONFIG_EXAMPLES_AUDIO_PLAYER_USEPOSTPROC2 */
if (exit_app) {
*p_game_event = END_REQUEST;
return;
}
} while((time(&cur_time) - start_time) < play_time);
*p_game_event = MUSIC_PAUSE;
}
メインループ
リスト:メインループのようにリスト:再生中の関数を呼び出した側では引数の値により動きが変わります。
- 再生が任意時間経過で一時停止の場合(MUSIC_PAUSE): 一時停止動作(app_pause)を行い、一時停止状態に遷移する。
- 音楽ファイルを最後まで再生した場合(MUSIC_END): 停止動作(app_stop)を行い、停止状態に遷移する。
- ゲーム終了条件が検出した場合(END_REQUEST): 停止動作(app_stop)を行い、ゲーム終了する(fot文の外に出る)。
メインループ
for (;;)
{
switch (game_state) {
case STOP:
if (play_or_pause_trigger) {
play_or_pause_trigger = false;
printf("state:stop play_or_pause_trigger.\n");
/* Running... */
// printf("Running time is %d sec\n", PLAYER_PLAY_TIME);
/* Start player operation. */
if (!app_start())
{
printf("Error: app_start_player() failure.\n");
/* Abnormal termination processing */
goto errout_start;
}
game_state = PLAY;
printf("state:stop goto play state.\n");
}
break;
case PLAY:
music_play_time = 3 + rand() % 8;
app_play_process(music_play_time, &game_event);
if (game_event == MUSIC_PAUSE) {
if (!app_pause())
{
printf("Error: app_pause() failure.\n");
goto errout_start;
}
game_state = PAUSE;
printf("state:play goto pause state.\n");
} else if (game_event == MUSIC_END) {
/* Stop player operation. */
if (!app_stop()) {
printf("Error: app_stop() failure.\n");
return 1;
}
game_state = STOP;
printf("state:play goto stop state.\n");
}
break;
case PAUSE:
if (play_or_pause_trigger) {
play_or_pause_trigger = false;
if (!replay())
{
printf("Error: replay() failure.\n");
goto errout_start;
}
game_state = PLAY;
printf("state:pause goto play state.\n");
}
break;
default:
break;
} // switch
if (exit_app) {
exit_app = false;
if (!app_stop()) {
printf("Error: app_stop() failure.\n");
return 1;
}
game_state = STOP;
printf("exit switch pressed. goto exit.\n");
break;
}
#ifndef CONFIG_AUDIOUTILS_PLAYLIST
break;
#endif
} // for
再生→一時停止
音楽ファイルを再生から一時停止する場合はリスト:一時停止の関数を呼び出します。
関数内部ではAS_STOPPLAYER_NORMALをapp_stop_player関数の引数に指定しています。
一時停止
static bool app_pause(void)
{
bool result = true;
int stop_mode = AS_STOPPLAYER_NORMAL;
if (!app_stop_player(stop_mode))
{
printf("Error: app_stop_player() failure.\n");
result = false;
}
return result;
}
app_stop_player関数はリスト:app_stop_player関数になっています。
引数に指定されたストップモードを構造体メンバにセットし、AS_SendAudioCommand関数を呼び出すと音楽再生が停止します。
AS_SendAudioCommand関数はオーディオを制御するコマンドです。
app_stop_player関数
static bool app_stop_player(int mode)
{
AudioCommand command;
command.header.packet_length = LENGTH_STOP_PLAYER;
command.header.command_code = AUDCMD_STOPPLAYER;
command.header.sub_code = 0x00;
command.player.player_id = AS_PLAYER_ID_0;
command.player.stop_param.stop_mode = mode;
AS_SendAudioCommand(&command);
AudioResult result;
AS_ReceiveAudioResult(&result);
return printAudCmdResult(command.header.command_code, result);
}
この辺りはつぎのWebドキュメントに詳しく書かれています。
オーディオを制御するコマンド 参考URL:
https://developer.sony.com/develop/spresense/docs/sdk_developer_guide_ja.html#_audio_subsystem
Spresense SDK 開発ガイド 5.3. Audio Subsystem
とはいえAudio Subsystem全体を見るのは大変だと思います。
再生停止の動作についてはつぎのWebドキュメントの該当箇所を見ると分かると思います。
5.3.3.10. 各機能の詳細からページを送っていくとstop_modeの記載箇所があります。
再生停止の動作 参考URL:
https://developer.sony.com/develop/spresense/docs/sdk_developer_guide_ja.html#_各機能の詳細
5.3.3.10. 各機能の詳細 stop_mode
一時停止の場合は次回の再生時に音楽ファイルの続きから再生したいです。
そのためリスト:一時停止では通常停止のAS_STOPPLAYER_NORMALを指定しています。
通常停止は停止要求のタイミングでFIFOの中身は残っている状態でできるだけ早く停止します。
対してES終端停止(AS_STOPPLAYER_ESEND)は、停止要求時点でFIFOに入っているデータをすべて発音してから停止します。
一時停止→再生
一時停止から音楽ファイルを再生するにはリスト:replay関数を実行します。
replay関数
static bool replay(void)
{
/* Play Player */
if (!app_play_player())
{
printf("Error: app_play_player() failure.\n");
app_close_play_file();
return false;
}
return true;
}
停止
音楽再生が停止するパターンとしてつぎの2点があります。
- 音楽ファイルを最後まで再生した場合
- SW1とSW2押下を検出しゲーム終了の場合
うえのどちらの場合もリスト:app_stop関数を実行します。
app_stop関数
static bool app_stop(void)
{
bool result = true;
/* Set stop mode.
* If the end of the file is detected, play back until the data empty
* and then stop.(select AS_STOPPLAYER_ESEND)
* Otherwise, stop immediate reproduction.(select AS_STOPPLAYER_NORMAL)
*/
int stop_mode = (s_player_info.file.size != 0) ?
AS_STOPPLAYER_NORMAL : AS_STOPPLAYER_ESEND;
if (!app_stop_player(stop_mode))
{
printf("Error: app_stop_player() failure.\n");
result = false;
}
if (!app_close_play_file())
{
printf("Error: app_close_play_file() failure.\n");
result = false;
}
return result;
}
GPIO
椅子取りゲームではAPS学習ボードのSW1、SW2につぎの機能を割り当てています。
- SW1: SW2と同時押下でゲーム終了
- SW2: 音楽再生のトリガ。SW1と同時押下でゲーム終了
上記機能を満たすためのソースコードについて説明します。
すべてつぎのファイルに実装されています。
- /Users/ユーザー名/spresense/spresense_game-main/musical_chairs/gpio.c
GPIO初期化
GPIO初期化はリスト:GPIO初期化です。
つぎの設定を行います。
- 割り込み設定(対象ピン、割り込みエッジ、割り込みハンドラの登録)
- 割り込み有効化
GPIO初期化
void gpio_create(void)
{
/* 割り込み設定 */
board_gpio_intconfig(SWITCH_1, INT_BOTH_EDGE, true, gpio_switch_1_handler);
board_gpio_intconfig(SWITCH_2, INT_BOTH_EDGE, true, gpio_switch_2_handler);
if (board_gpio_int(SWITCH_1, true) < 0) {
message("gpio_create board_gpio_int(switch_1) failure.\n");
}
if (board_gpio_int(SWITCH_2, true) < 0) {
message("gpio_create board_gpio_int(switch_2) failure.\n");
}
return;
}
割り込み設定
割り込み設定はリスト:gpio_create関数です。
gpio_create関数
board_gpio_intconfig(SWITCH_1, INT_BOTH_EDGE, true, gpio_switch_1_handler);
この場合はつぎの設定をしています。
- APS学習ボードSW1 (SWITCH_1 == 39ピン)を対象とする
- 割り込みは立ち上がり・立ち下がりの両エッジ
- フィルタをON
- 割り込みハンドラはgpio_switch_1_handlerに設定
APIリファレンスはこちらのリンクです。
APIリファレンス 参考URL:
https://developer.sony.com/develop/spresense/developer-tools/api-reference/api-references-spresense-sdk/group__gpioif.html
GPIO Interface driver
割り込み有効化
割り込み有効化はリスト:board_gpio_int関数です。
board_gpio_int関数
if (board_gpio_int(SWITCH_1, true) < 0) {
message("gpio_create board_gpio_int(switch_1) failure.\n");
}
SW1の割り込みを有効にしています。
割り込みハンドラ
割り込みハンドラはリスト:GPIO割り込みハンドラです。
SW1両エッジ検出時の割り込みハンドラがgpio_switch_1_handlerです。
SW2両エッジ検出時の割り込みハンドラがgpio_switch_2_handlerです。
割り込みハンドラではSW1、SW2の読み込みに加えてAPS学習ボードのUSER_LED1、USER_LED2の2つのLEDをSW1のレベルで点灯・消灯しています。SW1・SW2は押下で0、押下なしで1が読み込めます。
LEDは1の書き込みで点灯、0の書き込みで消灯します。
SW2の割り込みハンドラではplay_or_pause_trigger変数に再生開始トリガを示すtrueを設定します。
SW1・SW2のどちらの割り込みハンドラでもSW1・SW2が押下されていたらexit_app変数にゲーム終了を示すtrueを設定します。
GPIO割り込みハンドラ
bool play_or_pause_trigger = false;
bool exit_app = false;
static int gpio_switch_1_handler(int irq, FAR void *context, FAR void *arg)
{
int sw1_status = board_gpio_read(SWITCH_1);
int sw2_status = board_gpio_read(SWITCH_2);
board_gpio_write(USER_LED_1, sw1_status);
if (!sw1_status && !sw2_status) exit_app = true;
return 0;
}
static int gpio_switch_2_handler(int irq, FAR void *context, FAR void *arg)
{
int sw2_status = board_gpio_read(SWITCH_2);
int sw1_status = board_gpio_read(SWITCH_1);
board_gpio_write(USER_LED_2, sw2_status);
if (!sw2_status) play_or_pause_trigger = true;
if (!sw1_status && !sw2_status) exit_app = true;
return 0;
}
GPIO後処理
GPIO後処理はリスト:GPIO後処理です。
GPIO後処理
void gpio_destroy(void)
{
if (board_gpio_int(SWITCH_1, false) < 0) {
message("gpio_destroy board_gpio_int(switch_1) failure.\n");
}
if (board_gpio_int(SWITCH_2, false) < 0) {
message("gpio_destroy board_gpio_int(switch_2) failure.\n");
}
return;
}
つぎの設定を行います。
- 割り込み禁止
割り込み禁止はリスト:board_gpio_int関数のboard_gpio_int関数の第2引数指定が異なります。
割り込み禁止はfalseを指定します。
Tips
プログラム終了時に割り込み禁止にする必要性について
今回、開発・記事を書く過程で個人的に学び、気づいたことがあったので共有させてください。
アプリケーション終了時にGPIO後処理で割り込みを禁止しています。
もし割り込み禁止せずに終了した場合、アプリケーションが動いていないのにSW1またはSW2を押下すると割り込みハンドラが動きます。
椅子取りゲーム以外のアプリケーションがSW1・SW2の割り込みを使う場合(実際に使います)、椅子取りゲームの終了処理が不十分なため他のアプリケーションの実行を邪魔してしまいます。
割り込みを禁止せずに終了しているので当たり前といえば当たり前ですが私には盲点でした。
というのも私はこれまでOSがない・シェルがないベタメタルな組込み機器ばかりをやってきたので
- アプリケーションが終了する == 組込み機器の電源がOFFになる
という認識がありました。
OS動作のもと再びアプリケーションが実行可能な環境の場合は
- アプリケーション終了後も異常な動作をしない(他のアプリケーションの実行に影響を与えない)ように終了処理をしっかりする
ということを今回学びました。
課題
現状の椅子取りゲームの課題です。
ゲーム終了時の不具合
つぎの状態でゲーム終了するとエラーが発生し、次回椅子取りゲームを起動してもエラーが発生しゲームをプレイできません。
- 一時停止状態
- 音楽ファイル最後まで再生し停止状態
リセットでしかエラーを回復できません。
おそらくゲーム終了時の終了処理に不具合があります。
ローダブルELF対応
椅子取りゲームはローダブルELF対応できませんでした。
コンセプトを実現できなかったので原因を理解して対策したいところです。
音量調整機能
音量はベースにしたサンプルプログラムexamples/audio_playerから変更していません。
静かな環境で椅子取りゲームを開始する分は問題ないですが、少し騒がしい環境だと音楽が聞こえません。音量を調節する機能は必要そうです。
選曲機能
選曲ができないため1ファイルの固定再生となります。
1曲のみ固定再生だと飽きてしまうため改善が必要だと考えています。
投稿者の人気記事
-
k_abe
さんが
2022/09/26
に
編集
をしました。
(メッセージ: 初版)
-
k_abe
さんが
2022/09/26
に
編集
をしました。
(メッセージ: 製作品に変更。キャッチアップ画像設定。)
ログインしてコメントを投稿する