10. 音声と動画を活用しよう

PsychoPy Builderでは音声ファイルや動画ファイルを再生したり、マイクを使った音声の録音やカメラを使った動画の記録をしたりすることができます。動画機能についてはいろいろと技術的な難しさがあって、どのような動画ならば問題なく再生、録画できるか簡単には判断できないのですが、試してみることはそんなに難しくないのでぜひみなさん自身のPCで動かして確認してみてください。前章に引き続き、まとまった実験を作成するのではなくデモと解説を中心に進めます。

10.1. Soundコンポーネントで音声ファイルを再生しよう

SoundコンポーネントはBuilderで音声刺激を扱うためのコンポーネントです。 図10.1 にSoundコンポーネントのアイコンを示します。Soundコンポーネントのプロパティの内、これまでに紹介済みのコンポーネントと共通ではないのは「基本」タブの [音] と、新たに登場する「再生」タブ、「デバイス」タブです。

_images/sound-icon.png

図10.1 Soundコンポーネントのアイコン。

[音] には無圧縮WAV形式の音声ファイルを指定できるほか、AやBfl (B♭)、Csh (C#)のようにキーコードで音を指定することもできます。また、2000という具合に正の数値を入力すると、その周波数の音が鳴ります。実行環境によってはWAV以外にOGGなどの音声ファイルを再生できますが、無圧縮WAVならほとんどの環境で再生できるので無難です。

「再生」タブの [ボリューム $] は0.0から1.0の範囲でボリュームを指定します。再生環境や音声ファイル形式によってはうまく機能しませんので、可能なら音声データ作成の時点でボリュームを調整していた方が良いでしょう。 ルーチンが終了した時点でまだ再生が続いていた場合、 [Routine終了時に停止] がチェックされていれば音声ファイルの再生が途中で終了します。 [ハミング窓] は音声のオンセットによるプチノイズを軽減するフィルタを使用するか否かを設定します。チェックしておいた方が無難ですが、およそ1ミリ秒ほど音の立ち上がりが遅れるので、極めて正確な時間制御が必要な場合はチェックをオフにした方が良い結果が得られるかもしれません。

「デバイス」タブでは、PCに複数のオーディオ出力デバイスがある場合にどのデバイスから音声を再生するのかを指定します。 [スピーカー] にはPsychoPyが検出しているオーディオデバイスがリストアップされていて、その中から再生に使用するデバイスをひとつ選択できるようになっています。Blutooth audioなど、PsychoPyを起動した後に接続したオーディオデバイスは検出されない場合があるので、PsychoPyの起動前に接続しておくことをお勧めします。 初期値の「既定値」を選択すると、実行時に自動的にデバイスが選択されます。 「デバイス」タブにもうひとつある [デバイスラベル] は少々ややこしいのですが、Builderがオーディオ出力デバイスを管理する際に使用する名前をつけます。入力欄にマウスカーソルを合わせると表示されるヘルプにあるように、同一デバイスで再生するSoundコンポーネントには同じラベルを指定することが推奨されています。省略した場合はSoundコンポーネント名と同じラベルが自動的に設定されます(つまりすべてのSoundコンポーネントは別のラベルとなる)。

音声ファイルを用いた実験を行う時にしばしば困るのが、「音声ファイルが再生されている間文字列が表示され、再生終了と共に消える」といった処理や、「音声ファイルの再生が終わったら文字列が表示されるようにしたいが、ルーチンは継続したいので [Routineを終了] は使いたくない」という場合です。使用する音声ファイルの再生時間がすべて同じであれば [開始][終了] の値を再生時間に合わせて設定すればいいのですが、ファイルによって再生時間が異なる場合は工夫が必要です。具体的には、Soundコンポーネントに対応するPsychoPyクラスが持っているstatusというデータ属性を利用します。音声または動画ファイルが再生されていなければ、statusはNOT_STARTEDという値が設定されています。再生中であればPLAYING (またはSTARTED)、再生が終了していればSTOPPED (またはFINISHED)です。これを利用すると、Codeコンポーネントを用いて以下のようにstimという名前のSoundコンポーネントの再生終了時にルーチンを強制終了させることができます。

if stim.status == FINISHED:
    continueRoutine = False

ルーチン全体を終了させるのではなく、特定のコンポーネントの描画を開始したり終了したりしたい場合は、そのコンポーネントの [開始] および [終了] で「条件式」 を使用すると便利です。 図10.2 に音声ファイルの再生開始、終了に合わせてコンポーネントの開始、終了する例を示します。

_images/start-stop-by-condition.png

図10.2 [開始] および [終了] に 「条件式」 を指定すると、条件式によってコンポーネントの開始、終了を制御できます

最後にふたつ注意点を挙げておきます。まず、Soundコンポーネントによる音声の再生タイミングは結構「いいかげん」です。例えば「ぴぴっ」と音を短い音を2回鳴らしたいとします。再生時間0.1秒のSoundコンポーネントを2個配置して、それぞれの [開始] を0.5秒ずらしてやると「ぴぴっ」となるはずですが、実行するPCによっては1回しか音がならなかったり、全く音がならなかったりします。元々、PCのオーディオ機能はエラー音などを鳴らしたり、ひとつの音声ファイルを鳴らしたりするためのもので、短時間に複数の音声を正確に再生する機能は保証されていません。このような場合は、2つの音を1つの音声ファイルにまとめるべきです。視覚-聴覚の相互作用の研究を考えておられる方は刺激を動画として作成するのもひとつの対策でしょう。なお、再生タイミングの問題は、実験設定ダイアログの「オーディオ」タブの [オーディオライブラリ] をptbに設定して、 [オーディオ遅延の優先度] を厳しくすることでかなり改善されます。優先度の設定は一般的には3.で十分ですが、4.にするとハードウェアがベストな設定に対応していない場合にエラーとなるので確実です(エラーとなる場合は実験用PCを変えるか妥協するかを選ぶことになるでしょう)。

もうひとつの注意点は、異なるサンプリングレートで作成された音声ファイルをひとつの実験で使用しない方がよいということです。例えば、実験で音声ファイルを10個使用しているうちの8個が44.1kHz、2個が48kHzでサンプリングされているといった状況です。実験の実行環境によっては音声ファイル読み込みの時点でエラーが起こって実験が強制終了されてしまうことがあります。原因がわかりにくいエラーなので気をつけてください。

チェックリスト
  • 無圧縮WAV形式の音声ファイルを再生できる。

  • 指定された周波数の音を鳴らすことができる。

  • 指定されたキーコードの音を鳴らすことができる。

  • 音声のボリュームを指定できる。

  • 音声ファイルの再生を指定された時刻に途中終了できる。

  • 様々な再生時間の音声ファイルの再生開始、終了に合わせて他のコンポーネントを開始または終了させることができる。

  • 短時間に複数のSoundコンポーネントを鳴らそうとした時に期待した結果が得られない理由を説明できる。

  • 異なるサンプリングレートの音声ファイルをひとつの実験で混ぜて使用してはいけない理由を説明できる。

10.2. Movieコンポーネントで動画を再生しよう

Builderで動画を再生するにはMovieコンポーネントを使用します(図10.3)。Movieコンポーネントのプロパティの内、これまでに紹介済みのコンポーネントと共通ではないのは 「基本」タブの [動画ファイル] と 「再生」タブの [バックエンド][音声無し][ループ再生] です。

_images/movie-icon.png

図10.3 Movieコンポーネントのアイコン。

[バックエンド] は、これはPsychoPyが動画データを再生するときに使用するライブラリの指定です。PsyhcoPy Builderのユーザーから見ると「Movieコンポーネント」が操作画面に見えていて実際に操作する対象であり、これを「フロントエンド」と呼びます。それに対して、Movieコンポーネントが動画再生のために内部で利用しているライブラリが「バックエンド」です。ffpyplayer、moviepy、opencv、vlcの4つが選択できます。バージョン2024.2.5現在、推奨されているのはffpyplayerで、その他のバックエンドは主に過去のバージョンで作成された実験との互換性のために残されています。

[動画ファイル] には、再生する動画ファイル名を指定します。再生できる動画ファイルの形式はバックエンドによって決まりますが、moviepy(FFmpeg)なら一般的な形式はほとんど再生できると思います。動画ファイルのフォーマットはお勧めできる定番がないのですが、筆者はMP4形式をよく使用しています。

「レイアウト」タブの [サイズ [w, h] $] を動画ファイルと異なる値に設定することによって、動画を縦横に拡大縮小して再生することができます。ただ、PsychoPy上で拡大縮小できるからといって、実際に描画するサイズより解像度が高い動画ファイルを縮小表示するべきではありません。具体的にいうと、実験用に撮影した動画の解像度が1920×1080で、実験に使用する時の表示サイズが480×270であるならば、実験に使用する前に動画編集ソフトを用いて480×270に縮小すべきです。といいますのも、第一に解像度の高い動画ファイルは(よほど画質を落としていない限り)ファイルサイズが大きいので、その分PCのリソースを消費してリソース不足を起こすかもしれません。第二に、縮小処理をPsychoPyに任せると縮小の品質が悪くて細い線や小さな文字などが鮮明に描画されないかも知れません。できる限り高品質な(恐らく時間がかかる)方法で前もって縮小しておき、画質に問題がないことを確認しておくべきです。

「再生」タブの [音声無し] は文字通り音声なしで再生します。音声を再生せずに済むならその分負荷を軽減できます。実験の目的上音声が必要ないならチェックしておくとよいでしょう。 [ループ再生] は文字通り、これがチェックされていると動画をループ再生します。

チェックリスト
  • 動画ファイルを拡大縮小して再生することができる。

  • 動画ファイルを音声なしで再生することができる。

10.3. Movieコンポーネントを使ってみよう

それでは実際にMovieコンポーネントを使ってみましょう。なにか手ごろな動画ファイルを用意してください。動画ファイルはそのフォーマットと動画データの符号化方法が一対一対応していないので非常にややこしいのですが、mp4形式の動画ファイルなら多くの場合ffpyplayerバックエンドで再生できるはずです。動画ファイルが用意できたら作業を始めましょう。

  • 実験設定ダイアログ

    • [単位] をpixにする(標準のheightでないので注意)。

    • 実験設定ダイアログの「入力」タブの [キーボードバックエンド] がPsychToolboxか確認する。

  • trialルーチン

    • Movieコンポーネントをひとつ配置して以下の通り設定する。

      • 「基本」タブの [名前]movie にする(初期値)。 [終了] を空欄にする。

      • 「基本」タブの [動画ファイル] に使用する動画ファイルを指定する。相対パスが使える点などはImageコンポーネントと同様である。

      • 「レイアウト」タブの [サイズ [w, h] $] を動画の解像度に合わせる(例えば640×480の動画なら (640, 480) )。

    • Textコンポーネントをひとつ配置して以下の通りに設定する。

      • [終了] を「条件式」にして movie.status == FINISHED と入力する。

      • [文字の高さ]48 にする。

      • [文字列]$delay と入力し、「フレーム毎に更新」にする。

    • Codeコンポーネントをひとつ配置して、他のコンポーネントより先に実行されるよう一番上に並び替える。

作業が終了したら、Codeコンポーネントの [フレーム毎] に以下のコードを入力する。

delay = 1000*(t - movie.getCurrentFrameNumber()/movie.getFPS())

完成したら実行してみましょう。動画のフォーマットが非対応でなければ、画面中央に動画が再生されてその上に数値が表示されます。 getCurrentFrameNumber()は現在再生中の動画のフレーム番号、getFPS()は動画が毎秒何フレームかを返すメソッドで、現在の動画フレーム番号を毎秒フレーム数で割ることで動画のタイムスタンプが得られます。 tからこの値を引くことによって、ルーチンの時計tと動画のタイムスタンプがどれだけずれているかを計算できます。1000倍しているのは単位を秒からミリ秒に変換するためです。 この値は0になるのが理想ですが、どうしてもある程度の差は生じます。他のコンポーネントと連携させたいときに、この程度の時間のズレはあるというつもりで実験を作成するようにしてください。

数値の更新が速すぎて読めないという方は「 9.11:軌跡データを間引きしよう 」の方法で 変数 frameN を使って以下のように間引きをするといいでしょう。

if frameN % 10 == 0:
    delay = 1000*(t - movie.getCurrentFrameNumber()/movie.getFPS())

再生の遅延や時間のズレは、同一のPCでも再生する動画の負荷によって変動します。いろいろなサイズの動画を用意して実行してみると良いでしょう。また、 [サイズ [w, h] $] に動画の解像度と異なる値を指定してみて、どの程度の影響が出るかを試してみてください。

tとgetCurrentFrameTime()の差がどの程度だったか実験のたびに保存しておかないと心配だという方は、第7章 で解説した方法を使って差を変数に保持し、trial-by-trial記録ファイルに出力するとよいでしょう。まず、trialルーチンを繰り返すようにループを作成してください。 [名前]trails[繰り返し回数]1 でいいでしょう。ループを作成したら、のCodeコンポーネントの [Routine開始時] に以下のコードを追加します。

delay_list = []

続いて [フレーム毎] の最後に以下のコードを追加しましょう。間引きをした人は字下げに注意してください。

delay_list.append(delay)

最後に [Routine終了時] に以下のコードを追加します。average()とstd()は 表5.2 で出てきた平均値と標準偏差を計算する関数です。

trials.addData('delay_mean', average(delay_list))
trials.addData('delay_std', std(delay_list))

これでtrial-by-trial記録ファイルにtとgetCurrentFrameTime()の差の平均値と標準偏差が出力されるようになりました。なお、このコードを実際の実験で使用するときは、動画再生終了後直ちにルーチンを終了するようにしてください。 動画再生終了後もルーチンを継続する必要がある場合は、動画再生中のみ差をappendするようにすればいいでしょう。Codeコンポーネントに慣れていないとちょっと難しいかもしれませんので、これも例を出しておきましょう。動画再生終了後もルーチンを5秒間継続して、計算した平均値と標準偏差を画面上に表示することにします。以下の通り作業してください。 [文字列] のところでは「 6.9.2:改行文字を使った複数行の文字列の表現(上級) 」で解説した方法を使用しています。

  • trialルーチン

    • Textコンポーネントをひとつ配置して、以下の通り設定する。

      • [開始] を「条件式」にして movie.status == FINISHED と入力する。

      • [終了] を「実行時間(秒)」にして 5 と入力する。

      • [文字の高さ]48 にする。

      • [文字列]$'平均: ' + str(average(delay_list)) + '\n標準偏差:' + str(std(delay_list)) と入力し、「フレーム毎に更新」にする。

そして、Codeコンポーネントの [フレーム毎] に入力しているコードにif文を追加しましょう。2行目と3行目が既に入力済みの部分です。

if movie.status == PLAYING:
    delay = 1000*(t - movie.getCurrentFrameNumber()/movie.getFPS())
    delay_list.append(delay)
else:
    delay = 0

動画が再生されている時にはデータ属性statusの値がPLAYINGになっているので、if文を使ってその時のみ差を計算してappendするようにしました。elseの後の部分は動画が再生されていないときにもdelayという変数が存在するのを保証するために設けています。else以下を削除してしまうと実験開始直後に「delayという変数がない」というエラーメッセージが表示されて実験が止まります。 [Routine開始時] に delay=0 と書いておくことでも回避できます。

チェックリスト

  • Codeコンポーネントを使って動画の再生中のフレーム時刻を得ることができる。

  • Codeコンポーネントを使って動画の再生中のみ実行する処理を記述することができる。

10.4. 動画の再生位置を変更してみよう

実験に使用する動画は、できる限り実験の準備段階で実際に提示するとおりの状態にしておくことが理想ですが、実験によっては条件に応じて動画の特定の時点から再生したいということがあるかも知れません。そのような時に便利なのが動画のシークです。Builderで動画のシークを行うにはCodeコンポーネントを通じて動画オブジェクトのseek()メソッドを用います。これもデモを作成してみましょう。 5秒以上の長さがある動画 を用意してください。

  • 実験設定ダイアログ

    • PsychoPyの設定でheight以外の単位を標準に設定している場合は [単位] をheightにしておく。

  • trialルーチン

    • Codeコンポーネントをひとつ配置する。

    • Movieコンポーネントをひとつ配置して以下の通り設定する。

      • [名前]movie にする(初期値)。

      • [終了] を空欄にする。

      • [動画ファイル] に使用する動画ファイルを指定する。5秒以上の長さがあるものを使用してください。

そして、Codeコンポーネントの [Routine開始時] に以下のコードを入力してください。

movie.seek(5.0)

完成したら実行してみましょう。動画の冒頭を5秒飛ばしたところから再生が始まったはずです。ここで用いたseek()というメソッドは、動画の再生位置を引数で指定した値に変更します。引数の単位は秒です。

「動画の終了の5秒前」のように終了時刻を基準に指定したい場合は、動画の再生時間を保持しているdurationというデータ属性を利用して(単位は秒)、以下のように指定すればよいでしょう。

movie.seek(movie.duration - 5.0)

というわけで動画の頭出しができるようになりましたが、実験作成の時点で何秒飛ばすかがすでに決まっている場合はその分をカットした動画を作成した方が良いのは間違いありません。そうすると、実際にこのテクニックを使う状況は、実験中の参加者の反応によって飛ばす時間が変化する場合くらいかもしれません。

チェックリスト

  • 動画を途中から再生開始することができる。

  • 動画の再生開始位置を先頭から、または末尾からの秒数で指定することができる。

10.5. マウスで一時停止やスキップを行えるようにしよう(上級)

シークの話題があっさり終わってしまったので、ここで少し遊んでみましょう。画面中央下に 図10.4 のようなボタンを表示し、に「5秒戻る」、「5秒進む」、「一時停止/再開」、「終了」の機能を割り振ってマウスで操作できるようにしてみます。「遊び」というのは実験としての実用性があまりないからですが、Builderでマウスベースのユーザーインターフェースを作る際の参考になるのではないかと思います。新たに実験を作成し、以下の通り作業してください。

_images/movie-control.png

図10.4 マウスでボタンをクリックして動画再生をコントールしてみます。

  • 実験設定ダイアログ

    • PsychoPyの設定でheight以外の単位を標準に設定している場合は [単位] をheightにしておく。

    • 実験設定ダイアログの「スクリーン」タブの [マウスカーソルを表示] をチェックする。

    • 実験設定ダイアログの「入力」タブの [キーボードバックエンド] がPsychToolboxか確認する。

  • trialルーチン

    • Movieコンポーネントをひとつ配置して以下の通り設定する。

      • 「基本」タブの [名前]movie にする(初期値)。 [終了] を空欄にする。

      • 「基本」タブの [動画ファイル] に使用する動画ファイルを指定する。数十秒以上の長さがあるものが望ましい。

      • 「レイアウト」タブの [サイズ [w, h] $] を動画ファイルの縦横比に合わせて指定する(例えば1920x1080のように16:9の映像なら (0.64,0.36) 、640x480のように4:3の映像なら (0.64,0.48) など)。Y軸の-0.4の位置にPolygonコンポーネントでボタンを描くので、それらと重ならない程度の大きさにすることを勧める。

    • Polygonコンポーネントをひとつ配置して以下の通り設定する。

      • 「基本」タブの [名前]rewind にする。 [終了] を空欄にする。

      • 「基本」タブの [形状] を「三角形」にする。

      • 「レイアウト」タブの [サイズ [w, h] $](0.05, 0.05) にする。

      • 「レイアウト」タブの [位置 [x, y] $](-0.2, -0.4) にする。

      • 「レイアウト」タブの [回転角度 $]-90 にする。

      • 「外観」タブの [塗りつぶしの色]white にする(初期値)。

    • rewindをコピーして、 forward という名前で貼り付けて以下の通り設定する。
      • 「レイアウト」タブの [位置 [x, y] $](0.0, -0.4) にする。

      • 「レイアウト」タブの [回転角度 $]90 にする。

    • さらにコンポーネントの貼り付けの操作をして(rewindがコピーされた状態になっている)、 pause という名前で貼り付けて以下の通り設定する。

      • 「基本」タブの [形状] を「長方形」にする。

      • 「レイアウト」タブの [位置 [x, y] $](-0.1, -0.4) にする。

      • 「レイアウト」タブの [回転角度 $]0 にする。

    • さらにコンポーネントの貼り付けの操作をして(rewindがコピーされた状態になっている)、 close という名前で貼り付けて以下の通り設定する。

      • 「基本」タブの [形状] を「十字」にする。

      • 「レイアウト」タブの [サイズ [w, h] $](0.07, 0.07) にする。

      • 「レイアウト」タブの [位置 [x, y] $](0.2, -0.4) にする。

      • 「レイアウト」タブの [回転角度 $]45 にする。

    • Mouseコンポーネントをひとつ配置して以下の通り設定する。

      • 「基本」タブの [名前] をmouseに(初期値)、 [終了] を空欄にする。

      • 「基本」タブの [ボタン押しでRoutineを終了] を「有効なクリック」にし、 [クリック可能な視覚刺激]close と入力する。

      • 「データ」タブの [マウスの状態を保存] を「なし」にする。

    • Codeコンポーネントをひとつ配置する。

ここまで作業が終わったら、Codeコンポーネントにコードを入力します。 未解説のメソッドとデータ属性をいくつか使いますのが、それらは本章の最後に表としてまとめておきます。

まず [Routine開始時] に以下のコードを入力します。この変数stepはボタンを押したときに進む/戻る時間を保持しています。この値を変更すると進む/戻る時間が変わります。

step = 5.0

続いて [フレーム毎] に以下のコードを入力します。ここでpause()とplay()というメソッドが出てきますが、これはそれぞれ動画再生の一時停止、再開を行うものです。

ct = movie.getCurrentFrameNumber()/movie.getFPS()

if mouse.isPressedIn(rewind):
    movie.pause()
    movie.seek(max(0,ct-step))
    movie.play()
elif mouse.isPressedIn(forward):
    movie.pause()
    movie.seek(min(movie.duration,ct+step))
    movie.play()
elif mouse.isPressedIn(pause):
     if movie.isPaused:
        movie.play()
        pause.fillColor = 'white'
    else:
        movie.pause()
        pause.fillColor = 'red'

簡単に処理内容を解説しておくと、まずgetCurrentFrameNumber()とgetFPS()を使って現在の再生位置を得て変数ctに代入しておきます。続いて 第9章 で少し触れたマウスオブジェクトのメソッドであるgetPressedInを使ってrewind、forward、pauseの各Polygonオブジェクト上でマウスボタンが押されたかを判定していきます。isPressedInの引数に指定されたオブジェクト内にマウスカーソルがあってボタンが押されていたらTrueが返されるので、if文で処理を分岐します。

rewindとforwardでマウスのボタンが押されていた時の処理では、先ほどの変数ctにstepを加算、もしくは減算してseek()を実行しています。これで「現在再生中の位置からstep秒戻る、または進む」が実現できます。ただ、再生中にseek()を行うと音声は進んでいるのに映像が止まったままになったりすることがあるので、seek()の前にpause()でいったん再生を止め、そしてseek()後にplay()で再開しています。pause()とplay()をコメントアウトして比較してみるとよいでしょう。

pauseでマウスのボタンが押されていた時は、動画の再生状態に応じて処理を分岐します。再生中であればデータ属性statusの値がPLAYING、一時停止中であればPAUSEDとなるのですが、筆者の動作環境(PsychoPy 2024.2.5, Windows 11, ffpyplayer)では一時停止してもstatusが変化しないという問題が生じたので、一時停止中であればTrue、そうでなければFalseを保持しているisPausedというデータ属性を使用しています。if文で分岐してisPausedが真なら一時停止中なので再開のためにplay()を実行し、偽ならば再生中なのでpause()を実行して一時停止します。ついでに、一時停止中かわかりやすいようにpauseボタンの色を一時停止中には赤色に変更しています。一時停止を解除するときには白色に戻します。

ここにはcloseを押したときの終了処理が書かれていませんが、それはMouseコンポーネントの [クリック可能な視覚刺激] にcloseを設定することで実現されているので、Codeコンポーネントを使う必要がありません。

ここまで作業ができたら、一度保存して実行してみましょう。画面中央に動画が再生され、画面中央下に 図10.4 のように表示されますので、クリックして動作を確認してください。PsychoPyにとって動画の一時停止、再生再開は重い処理なので、一般的なスペックのPCだと各ボタンをクリックしてから効果が表れるまで一呼吸待たされますのでそのつもりでいてください。なお、動画の再生が終了してもデモは終了しませんので、closeを押して終了してください。

いかがだったでしょうか。「戻る」と「進む」、「終了」は(やや待たされるかもしれませんが)特に問題なく動作したと思います。でも、「一時停止」はボタンが高速に赤と白に点滅して、一瞬だけ止まってすぐ動きだしたり、一時停止した状態から再生を開始できなかったりしなかったでしょうか? なぜそうなるかというと、isPressedIn()は「現在マウスのボタンが押されているか」を返すメソッドだからです。PCの画面が60fpsで描かれている場合、 [フレーム毎] の処理は1/60秒に1回実行されます。今回のデモではpause()やplay()が少々重い処理なので1/60秒内で終わらないこともありますが、それでも人がマウスのボタンを「カチッ」とクリックした間に数フレームは過ぎてしまいます。そうすると、現在のコードでは1回「カチッ」とボタンを押しただけでpause()とplay()が繰り返し実行されてしまいます。なので、クリックした後にうまく一時停止する場合と再生されて続けてしまう場合があるのです。

_images/mouse-click-frame.png

図10.5 クリックが数フレームにわたった場合に対応する必要がある

ではどうすればいいかというと、いくつか方法があります。ここではmouse.isPressedIn(pause)がTrueになったときの時刻を保持しておいて、一定時間が経過するまではmouse.isPressedIn(pause)がTrueであっても無視するという方法を取り上げましょう。まずCodeコンポーネントの [Routine開始時] に以下の行を追加します。

wait = 0

続いて [フレーム毎] に以下のように追加します。追加した行の最後に目印としてコメントを入れています。皆さんが試してみるときはこれらのコメントを入力する必要はありません。

ct = movie.getCurrentFrameNumber()/movie.getFPS()
if wait > 0:    # (1)
    wait -= 1   # (1)

if mouse.isPressedIn(rewind):
    movie.pause()
    movie.seek(max(0,ct-step))
    movie.play()
elif mouse.isPressedIn(forward):
    movie.pause()
    movie.seek(min(movie.duration,ct+step))
    movie.play()
elif mouse.isPressedIn(pause) and wait <= 0:  # (2)
    wait = 30                                 # (3)
     if movie.isPaused:
        movie.play()
        pause.fillColor = 'white'
    else:
        movie.pause()
        pause.fillColor = 'red'

この例では、waitという変数を用意して、一時停止と再開の処理を行う前に wait=30 をセットしています(コメント(3))。その後、フレーム毎に waitの値が 0 より大きければ1を減算していきます(コメント(1))。そしてコメント(2)のところでボタン押しを判定する際に wait<=0 を条件として追加することで、waitの値が 0より大きい間はボタンを押しても一時停止/再開が行われないようにしています。このwaitという変数はルーチン開始時に存在していないといけないので、 [Routine開始時] にwait=0としているわけです。実行してみて、一時停止が機能することを確認してください。

なお、この方法ではボタンが押されてからの時間をフレーム数でカウントしていることになります。コメント(3)で wait=30 としているので30フレーム、60fpsで刺激提示しているのなら待ち時間は0.5秒です。長すぎる場合は数値を減らしてみましょう。また、この例では戻る、進む処理について対策していませんが、これらについても対策を追加するにはいくつか書き方があるので、考えてみると良い練習になるでしょう。

他の方法としては、直前のフレームのボタンの状態をmouse.getPressed()で保持しておいて、「直前のフレームでボタンが押されていなくて、今のフレームでは押されている」という時だけ処理するという方法が考えられます。この場合、もうボタンが押されていることは確実なのでisGetPressedIn()ではなくcontains()でクリックされたPolygonオブジェクトを判別できます。

フレーム数を数える方法は、操作している人がボタンをずっと長押しすれば処理が繰り返されます。それに対して、直前のフレームの状態を保持する方法では、長押ししても処理されるのは1回のみです。どちらがの方が望ましいかは状況によるでしょうから、どちらの方法もマスターしておくのが理想です。

以上、MovieコンポーネントとMouseコンポーネントで「遊んで」みましたがいかがだったでしょうか。最後に、この章で使用したMovieオブジェクトのデータ属性とメソッドを 表10.1 にまとめておきます。

表10.1 Movieオブジェクトの主なデータ属性とメソッド

status

現在の状態をあらわす。再生前ならNOT_STARTED、再生中ならPLAYING (STARTED)、一時停止中ならPAUSED、再生終了ならSTOPPED (FINISHED)。ただし一時停止亭も

duration

動画の長さを保持している。単位は秒。

isPaused

動画再生中に一時停止していればTrue、そうでなければFalseを保持している。

getCurrentFrameNumber()

動画の現在のフレームを返す。

getFPS()

動画の1秒あたりのフレーム数を返す。

play()

動画の再生を開始する。一時停止している場合は再生を再開する。

pause()

動画の再生を一時停止する。

seek()

動画の再生位置を変更する。単位は秒。

チェックリスト

  • 動画の再生を一時停止、再開できる。

  • Codeコンポーネントで動画が一時停止中であることを条件に処理を分岐できる。

  • マウスでオブジェクトを「クリック」した際にボタンが押されている期間が複数フレームにわたる場合を考慮したコードを記述できる。

10.6. Microphoneコンポーネントで録音してみよう

音声と動画の再生の解説が終わったので、続いて録音、録画の解説に進みましょう。まず音声の録音を行うMicrophoneコンポーネントを取り上げます。Microphoneコンポーネントは「反応」カテゴリにあります( 図10.6 )

_images/microphone-component.png

図10.6 Microphoneコンポーネントのアイコン

Microphoneコンポーネントの「基本」タブは他のコンポーネントと同様なので解説は必要ないと思います。 「デバイス」タブはSoundコンポーネントと同様、録音に使用するデバイスの設定を行います。 [デバイス] は、PsychoPyによって検出された録音デバイスの中から使用するデバイスを指定します。 初期値はdefaultで、OSで設定されている標準の録音デバイスを使用します。BuletoothデバイスなどをPsychoPy起動後に接続しても検出されない場合がありますので、PsychoPy起動前に接続しておいてください。 [最大録音データサイズ(kb)] は、録音が長時間に及んで巨大な音声ファイルが出力されてしまうことを防止するためのものです。 この上限を超えた後は自動的に録音が停止しますが、対応するMicrophoneオブジェクトのisRecBufferFull()というメソッドで上限に達したかどうかを調べることができます(上限に達したらTrueが返される)。 必要に応じて変更してください。

「音声文字変換」タブは録音した音声から自動的に発話を書き起こす機能の設定を行います。 [音声の文字変換] をチェックするとこの機能が有効になり、他の項目の設定が可能になります。まず [音声文字変換バックエンド] で使用するライブラリを指定します。初期状態は「なし」で書き起こし機能は使えません。 GoogleはGoogle Cloud APIを使用します。Google Cloud APIはインターネット接続が必要で、Google Cloud APIのキーを持っている必要がありますが、日本語にも対応しているという強みがあります。Google Cloud APIのキーはJSONファイルに保存してPsychoPyの設定ダイアログの「一般」タブにある [GoogleCloudアプリ―ケーションキー] に指定してください。

「データ」タブの [出力ファイル形式] は音声ファイルの形式を指定します。かなりたくさんの形式がリストアップされるのでどれにしたらよいか迷うかもしれませんが、問題(後述)が生じないならdefaultのままでよいでしょう。 [発話開始/終了時刻の記録] をチェックすると、音量が基準以上/以下になった時刻を記録します。 [無音期間のトリム] をチェックすると、音量が基準以下の部分をファイルに出力しません。

録音が成功するとデータファイルの保存フォルダに「データファイル名に_mic_recordedとついたフォルダ(例えばfoo_expn_2022-09-01-_20h00.00.000.csvというデータファイル名ならfoo_expn_2022-09-01-_20h00.00.000_mic_recorded)」が作成され、その中に音声ファイルが出力されます。ループで繰り返し実行すると、繰り返し毎にファイルが作成されます。

以上で主要なプロパティは解説したので、動作確認してみましょう。 新たに実験を作成して、以下のように作業してください。

  • 実験設定ダイアログ

    • PsychoPyの設定でheight以外の単位を標準に設定している場合は [単位] をheightにしておく。

    • 実験設定ダイアログの「入力」タブの [キーボードバックエンド] がPsychToolboxか確認する。

  • trialルーチン

    • Keyboardコンポーネントをひとつ配置して、以下の通り設定する。

      • 「基本」タブの [終了] を空欄にし、 [Routineを終了] にチェックが入っていることを確認する。

    • Textコンポーネントをひとつ配置して、以下の通り設定する。

      • 「基本」タブの [終了] を空欄にする。 [文字列]$int(t) と入力し、「フレーム毎に更新」にする。

    • Microphoneコンポーネントをひとつ配置して以下の通り設定する。

      • 「基本」タブの [終了] を空欄にする。

      • 「デバイス」タブの [デバイス] がdefaultになっていることを確認する。実行してうまく動作しなかった場合はここを変更してみる。

      • 「音声文字変換」タブの [音声の文字変換] がチェックされていないことを確認する。

できたら実行してみましょう。画面上で時間のカウントアップが始まったら適当に音を鳴らして、キーボードのスペースキーなどを押して実験を終了してください。dataフォルダを開いて音声ファイルを格納したフォルダができていること、中の音声ファイルを再生して音が録音できていることを確認しましょう。

うまく動作しない場合、まずシステムのオーディオ設定で録音可能なデバイスが確かに存在すること、アプリケーションから使用できるように設定されていることを確認してください。セキュリティの設定が厳しい場合、録音デバイスが存在しても任意のプログラムから使用できないようになっている可能性があります。マイクを使用する他のアプリを起動して動作することも確認するとよいでしょう。

チェックリスト

  • Microphoneコンポーネントで録音を行うことができる。

10.7. Cameraコンポーネントで動画撮影してみよう

録音に続いて動画撮影を取り上げます。使用するのはコンポーネントペインの「反応」カテゴリにあるCameraコンポーネントです( 図10.7 )。

_images/camera-component.png

図10.7 Cameraコンポーネントのアイコン

CameraコンポーネントもSoundコンポーネントやMicrophoneコンポーネント同様「デバイス」タブがあり、ここで映像に関する主要な設定を行います。 [バックエンド] はMovieデバイス同様、使用するバックエンドライブラリを指定します。 [ビデオデバイス] にはPsychoPyによって検出されたカメラデバイスがリストアップされていて、撮影に使用するカメラをここで指定します。 [解像度][フレームレート] は文字通り録画される映像の解像度とフレームレートを指定します。 残念ながら、これらのパラメータで選択できるすべての組み合わせが利用可能というわけではありません。各自の環境でどの組み合わせなら利用できるか試行錯誤する必要があります。

「オーディオ」タブには録音に関する設定項目が並んでいて、Microphoneコンポーネントと基本的に同じですが [チャネル][サンプリングレート(Hz)] などの独自項目もあります。 これも映像のパラメータと同様、選択できるすべての組み合わせが利用可能とは限りませんので、各自の環境でどの組み合わせが有効か試行錯誤してください。

「データ」タブには [ファイルへ保存] という項目があります。これをチェックしていたら動画がファイルに保存されます。Microphoneデバイスと同様、データファイルの保存フォルダに「データファイル名に_cam_recordedをつけた名前のフォルダ」が作成され、その中に保存されます。 Cameraコンポーネント独自の項目は以上なので、さっそくサンプルを作ってみましょう。

  • 実験設定ダイアログ

    • PsychoPyの設定でheight以外の単位を標準に設定している場合は [単位] をheightにしておく。

    • 安定して使用できるパラメータの組み合わせが見つかるまではPilotモードで実行するか [フルスクリーンウィンドウ] のチェックを外しておく。

    • 実験設定ダイアログの「入力」タブの [キーボードバックエンド] がPsychToolboxか確認する。

  • trialルーチン

    • Keyboardコンポーネントをひとつ配置して、以下の通り設定する。

      • 「基本」タブの [終了] を空欄であること、 [Routineを終了] にチェックが入っていることを確認する。

    • Textコンポーネントをひとつ配置して、以下の通り設定する。

      • 「基本」タブの [終了] を空欄にする。 [文字列]$int(t) と入力し、「フレーム毎に更新」にする。

    • Cameraコンポーネントをひとつ配置して以下の通り設定する。

      • 「基本」タブの [終了] を空欄にする(初期値)。

      • 「デバイス」タブの [ビデオデバイス][解像度][フレームレート] や「オーディオ」タブの [マイク][チャネル][サンプリングレート(Hz)] を自分の環境に合わせて設定する。どれにしたらいいかわからない場合、320x240 (QVGA) か 640x480 (VGA) などの多くのデバイスがサポートする解像度で30fpsから試してみるとよい。

完成したら実行してみましょう。実験が始まった直後に終了してしまって、Runnerの標準出力に Specified camera format is not supported. といったエラーメッセージが出る場合は [ビデオデバイス] 等の設定を変更して実行する作業を繰り返して、安定的に動作する選択肢を探してください。エラーで停止した時の復帰をしやすくするため、安定的に動作する選択肢が見つかるまでは [フルスクリーンウィンドウ] のチェックを外しておくのがお勧めです。

無事に実験が動作したら、カメラの前で手を叩いて「ぱん!」と鳴らすなど、「動きと同期して音が鳴る」様子を記録してからキーボードのスペースキーなどを押して実験を終了し、動画ファイルが保存されていることを確認しましょう。動画ファイルが出力されていない場合や、出力されていても通常の動画プレイヤーで再生できない場合は [ビデオデバイス] 等の設定が適切ではないので、面倒ですが選択肢探しの作業に戻ってください。

無事に動画が保存されていて再生できた場合は、映像と音のずれ具合を確認してください。ずれの大きさはカメラやPCの性能に依存しますが、筆者がこの原稿の執筆時に使用した環境(Windows11 x64/PsychoPy2022.2.4/LogiCool C922)では0.8秒ほど映像が遅れます(音が先に鳴る)。皆さんのPCでどの程度ずれるかはわかりませんが、いずれにしても時間と映像の精確な同期が必要な用途には向いていないということです。Cameraコンポーネントを実験に使用する場合は、このずれのことをよく頭においておいてください。

さて、時間的なずれにはもうひとつ、画面上に提示した刺激と動画のタイミングのずれもあります。これを確認するために、カメラをPC画面に向けて実験を実行し、画面上に表示されているカウントアップの数字を録画してみましょう。理想的には録画が始まると同時に画面上では0が表示されていて、ちょうど1秒再生したところで数字が1になるはずです。コマ送りできるプレイヤーを使っている場合は、数字が1増える瞬間のコマから1フレームずつコマ送りして、録画時に設定したfpsのコマ数(例えば30fpsなら30コマ)だけ進めたタイミングで数字が増えるかどうかも確認しましょう。筆者の環境の場合、動画の最初から数えて0が表示されるまで約23コマの遅延がありました。30fpsで23コマの遅延ということは0.76秒の遅延ですから、先ほど手を叩く動画で音に対する映像の遅延とほぼ一致しています。数字が増えるタイミングは録画時のfps(30)と一致していましたが、実行後にRunnerに表示されている出力をよく見ると real-time buffer too full or near too full! frame dropped!といった警告が度々出力されていたので、時々フレーム落ちしていたものと思われます。皆さんの環境ではどのような結果になるかわかりませんが、本番の実験に使う前にこういった「ずれ」をしっかり検討しておくことをお勧めします。

続いて、もうひとつサンプルを紹介しましょう。次のサンプルでは動画保存をおこなうのではなく、画面上にカメラの映像をリアルタイムに表示してみます。以下のように作業してください。

  • 実験設定ダイアログ

    • PsychoPyの設定でheight以外の単位を標準に設定している場合は [単位] をheightにしておく。

    • 安定して使用できるパラメータの組み合わせが見つかるまではPilotモードで実行するか [フルスクリーンウィンドウ] のチェックを外しておく。

    • 実験設定ダイアログの「入力」タブの [キーボードバックエンド] がPsychToolboxか確認する。

  • trialルーチン

    • Cameraコンポーネントをひとつ配置して以下の通り設定する。

      • 「基本」タブの [名前]cam であることを確認する。 [終了]10 にする。

      • 「デバイス」タブと「オーディオ」タブの各項目を自分の環境に合わせて設定する。

      • 「データ」タブの [ファイルへ保存] のチェックを外す。

    • Codeコンポーネントをひとつ配置する。

    • Imageコンポーネントをひとつ配置して以下の通り設定する。

      • 「基本」タブの [終了]10 にする。 [画像]$frame_img と入力し、「フレーム毎に更新」に設定する。

      • 「レイアウト」タブの [サイズ [w, h] $][ビデオデバイス] で設定したカメラ映像の縦横比に合わせて適当な値を入力する(例えば1920x1080のように16:9の映像なら (0.64,0.36) 、640x480のように4:3の映像なら (0.64,0.48) など)。

      • 「レイアウト」タブの [垂直に反転] にチェックを入れる。

CodeコンポーネントがImageコンポーネントより先に実行されるように並んでいる(つまりルーチンペイン上でCodeコンポーネントの方が上にある)ことを確認したうえで、Codeコンポーネントの「フレーム毎」のPythonのコード欄に以下のように入力する。

frame = cam.getVideoFrame()
if frame is not None and frame.colorData is not None:
    frame_img = frame.colorData.astype(np.float32).reshape(
        (frame.size[1],frame.size[0],3))
    frame_img /= 256
else:
    frame_img = np.ones((16,16,3),dtype=np.float32)

入力したら実行してみましょう。うまくいけば10秒間カメラの映像が画面上に描かれるはずです。動画が保存されない点も確認しておいてください。CameraコンポーネントとImageコンポーネントの使い方には特に難しいところはないでしょうから、ここではCodeコンポーネントのコードを集中的に解説します。

1行目のcam.getVideFrame()というのはcam、つまりCameraコンポーネントによって作成されたCameraオブジェクトのgetVideFrame()というメソッドの呼び出しです。このメソッドは呼び出された時点で最新のビデオフレームをMovieFrameオブジェクトとして返します。MovieFrameオブジェクトは単なる画像データではなくタイムコードなど動画のさまざまな情報を含んでいて、画像データはcolorDataというデータ属性に格納されています。ただ厄介なことに、これはそのままImageコンポーネントで表示できるようなPsychoPyの画像データではなく、画像の各ピクセルのRGB値をべたっと1次元に並べただけのものです(1920×1080の解像度なら1920×1080×3=6,220,800個)。Imageコンポーネントで使用するには、縦×横×3の3次元のデータで、なおかつ値が0.0から1.0の小数でなければいけません。

そこで今回のコードでは、まず画像データが得られていることを確認して(2行目のif文)、3行目でastype()による小数型への変換とreshape()によるデータの3次元化を一気に行っています。続く5行目でRGB値を0.0から1.0に変換していますが、ここでは変換前のRGBの各チャネルの数値が0から255の整数であることを前提にしています。一般的なwebカメラならこれで問題ないはずです。7行目はMovieFrameオブジェクトが画像を含んでいなかった場合のために16×16の解像度の真っ白の画像を作って代入しています。どうせImageコンポーネントによって拡大されるのですから低解像度で十分です。これで変数frame_imgにImageコンポーネントで表示する画像データが作成されました。あとはImageコンポーネントで表示するだけです。

ひとつ注意が必要なのはImageコンポーネントの [垂直に反転] です。すべてのカメラでそうなるかわかりませんが、筆者が試した範囲ではMovieFrameの画像データはPsychoPyのImagaeコンポーネントの画像データと比べると上下方向に反転しています。そこで正しい向きに表示するためにはデータ自体をさらに変換するか、この例のように [垂直に反転] をチェックする必要があります。もし鏡像のようにしたいのであれば、 [水平に反転] もチェックすると良いでしょう。

この例ではCameraコンポーネントの映像をそのまま画面上に描画しましたが、リアルタイムに動画を解析して参加者のジェスチャーを検出したりできればぐっと応用範囲は広がるでしょう。現状のPCの性能ではフレームレートや遅延の観点から自然な速度で実行するのは難しいと思いますが、この分野の技術の進歩はとても速いので、近い将来に実用的になるかもしれません。

チェックリスト

  • Cameraコンポーネントを使って動画記録を行うことができる。

  • Cameraコンポーネントで記録された動画の遅延を確認することができる。

  • Cameraコンポーネントの映像をリアルタイムに画面上に表示することができる。

10.8. この章のトピックス

10.8.1. Staticコンポーネントを用いた動画の読み込み

動画ファイルは一般的にファイルサイズが大きく、読み込みに時間がかかります。ループで繰り返しのたびに異なる動画を読み込むと、ルーチン開始前に読み込みを行いますので、ここで時間がかかると繰り返しのたびに意図しない空白画面が表示され続けることになります。ただ時間がかかるだけならまだしも、動画ファイルによって読み込みの時間が異なると試行間の間隔がばらばらになってしまって実験によっては望ましくありません。こういう時に便利なのがStaticコンポーネントです。

Staticコンポーネントはコンポーネントペインの「カスタム」の中に含まれる 図10.8 のアイコンです。他のコンポーネントとは異なり、 [名前] の初期値がコンポーネント名と同じではなく ISI となっているので注意してください。 [開始][終了] は他のコンポーネントと同様、Staticコンポーネントが有効な期間を指定します。

_images/static-icon.png

図10.8 Staicコンポーネントのアイコン

プロパティ設定ダイアログのOKをクリックしてダイアログを閉じると、ルーチンペインに 図10.9 上のように赤色の領域が現れます。これがStaticコンポーネントです。削除するときはこの赤い領域内のどこかで右クリックしてメニューから「削除」を選んでください。

Staticコンポーネントを配置した後に、他のコンポーネントのプロパティ設定ダイアログを開いた際に、各プロパティの更新方法として「更新方法: trial.ISI」のような項目が追加されます( 図10.9 下)。ここでtrialはStaticコンポーネントを置いているルーチン名、ISIはスタティックコンポーネントの名前です。testというルーチンにload_stimという名前でStaticコンポーネントを配置したならtest.load_stimとなります。この項目を選択すると、プロパティの更新が指定したStaticコンポーネントの期間中に行われます。例えば実験で使用する動画が最も読み込みに時間がかかるものでも0.4秒で完了するなら、Staticコンポーネントの長さを(少し余裕をもって)0.5秒にしておけば、どの動画も0.5間で読み込むことができてばらつきが生じません。Staticコンポーネントで設定した期間内に終わらない処理を行わせると元も子もないので、あらかじめ動作確認して余裕を持たせておくことが重要です。

_images/static-update.png

図10.9 Staticコンポーネントを設置すると他のコンポーネントの更新方法のところに項目が追加されます

「Staticコンポーネント」の名前の通り、この期間には刺激を描画したりキー押しを検出したりするべきではありません。やってできない事はないのですが、時間的な精度が保障されなくなります。Staticコンポーネントの期間中(あるいは期間開始と同時に)に静止した刺激を描画しておくことには何の問題もありません。

ファイルの読み込みタイミングとして他のルーチンに配置したStaticコンポーネントを選択することも可能なので、実験期間中で都合がよいタイミングにファイルを読み込んでくことが可能です。Staticコンポーネントはファイルサイズが大きくなりがちな動画ファイルの読み込みに特に力を発揮しますが、音声ファイルを読み込む時や、画像ファイルを十数枚一気に読み込む必要がある時などにも役に立ちます。

「カスタム」タブの [カスタムコード] は、他のコンポーネントのパラメータの更新以外の作業を行わせたいときに使用します。入力欄が狭いので、複雑な処理を行わせる場合はCodeコンポーネントで関数を定義してその関数を呼び出すなどの工夫が必要かもしれません。上級者向けの機能だと思います。