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

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

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

SoundコンポーネントはBuilderで音声刺激を扱うためのコンポーネントです。 図10.1 にSoundコンポーネントのアイコン及びプロパティ設定ダイアログを示します。Soundコンポーネントのプロパティの内、これまでに紹介済みのコンポーネントと共通ではないのは「基本」 [音] と「再生」タブの [ボリューム $][ハミング窓] です。 [音] には無圧縮WAV形式の音声ファイルを指定できるほか、AやBfl (B♭)、Csh (C#)のようにキーコードで音を指定することもできます。また、2000という具合に正の数値を入力すると、その周波数の音が鳴ります。実行環境によってはWAV以外にOGGなどの音声ファイルを再生できますが、無圧縮WAVならほとんどの環境で再生できるので無難です。 [ボリューム $] は0.0から1.0の範囲でボリュームを指定します。再生環境や音声ファイル形式によってはうまく機能しませんので、可能なら音声データ作成の時点でボリュームを調整していた方が良いでしょう。 [開始] および [終了] で定められた時間が音声ファイルの時間より短い場合は、音声ファイルの再生が途中で終了します。 [ハミング窓] は音声のオンセットによるプチノイズを軽減するフィルタを使用するか否かを設定します。チェックしておいた方が無難ですが、およそ1ミリ秒ほど音の立ち上がりが遅れるので、極めて正確な時間制御が必要な場合はチェックをオフにした方が良い結果が得られるかもしれません。

_images/sound-icon.png

図10.1 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つの音声ファイルにまとめるべきです。視覚-聴覚の相互作用の研究を考えておられる方は刺激を動画として作成するのもひとつの対策でしょう。なお、再生タイミングの問題は、PsychoPyの設定で「オーディオライブラリ」にPsychToolboxを指定して、オーディオレイテンシの設定を厳しくすることでかなり改善されます。 図10.2 のようにPsychoPyの設定ダイアログの「ハードウェア」のページを開いて「オーディオライブラリ」の先頭にPTBが来るように修正してください。PTBが含まれていない場合は先頭に入力してください。オーディオレイテンシの設定は一般的には3.で十分ですが、4.にするとハードウェアがベストな設定に対応していない場合にエラーとなるので確実です(エラーとなる場合は実験用PCを変えるか妥協するかを選ぶことになるでしょう)。

_images/sound-ptb-backend.png

図10.3 オーディオライブラリにPsychToolboxを指定すると再生の遅延が大幅に改善される場合があります。

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

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

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

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

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

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

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

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

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

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

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

_images/movie-icon.png

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

[バックエンド] は、これはPsychoPyが動画データを再生するときに使用するライブラリの指定です。PsyhcoPy Builderのユーザーから見ると「Movieコンポーネント」が操作画面に見えていて実際に操作する対象であり、これを「フロントエンド」と呼びます。それに対して、Movieコンポーネントが動画再生のために内部で利用しているライブラリが「バックエンド」です。ffpyplayer、moviepy、opencv、vlcの4つが選択できます。それぞれ使用するライブラリが異なります。どれがよいと一概には言えないのですが、とりあえず最新のバックエンドであるffpyplayerを試してみることをお勧めします。

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

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

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

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

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

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

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

  • 実験設定ダイアログ

    • [単位] をpixにする。

  • trialルーチン

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

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

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

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

      • [空間の単位] を pix にする (本来不要だが反映されないことがあるので指定すること)。

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

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

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

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

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

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

delay = t - movie.getCurrentFrameTime()

完成したら実行してみましょう。動画のフォーマットが非対応でなければ、画面中央に動画が再生されてその上に数値が表示されます。この数値はルーチンの時計tと動画の再生位置の時刻の差なのですから、動画が遅延なく再生できていればほとんど変動しないはずです。本来ならばこの値が0になるのが理想ですが、どうしてもある程度の差は生じます。他のコンポーネントと連携させたいときに、この程度の時間のズレはあるというつもりで実験を作成するようにしてください。

数値の更新が速すぎて読めないという方は「 8.11:軌跡データを間引きしよう 」の方法で 変数frameN を使って間引きをするといいでしょう。以下にヒント(というかほとんど答え)を示しますが、よい練習になるので自分で考えてみてください。

if frameN % 10 == 0:
    delay = t - movie.getCurrentFrameTime()

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

このデモのうち、TextコンポーネントとCodeコンポーネントは時刻などの情報を出力するために配置しているものであり、動画を再生するだけなら不要です。Codeコンポーネントに記入したコードについては解説が必要ですね。バックエンドがmoviepyのMovieコンポーネントを配置すると、その [名前] に指定した変数にpsyhcopy.visual.MovieStimオブジェクトが作成されます(moviepyならMovieStim3、opencvならMovieStim2)。getCurrentFrameTime()メソッドは、現在の動画フレームの時刻を返します。1行目で変数mtにgetCurrentFrameTime()の値を代入しておいて、2行目で文字列に埋め込んでいます。tはこれまでの章で使ってきた、現在のルーチンが開始してからの時刻を保持している内部変数です。

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し続けてしまいます。

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

  • 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 = t - movie.getCurrentFrameTime()
    delay_list.append(delay)
else:
    delay = 0

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

チェックリスト

  • 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というデータ属性を利用します。durationには動画の長さが保持されているので(単位は秒)、以下のように指定すればよいでしょう。

movie.seek(movie.duration - 5.0)

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

チェックリスト

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

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

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

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

_images/movie-control.png

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

  • 実験設定ダイアログ

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

  • trialルーチン

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

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

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

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

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

      • [名前] をrewindにする。

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

      • [形状] を「三角形」にする。

      • [塗りつぶしの色] をwhiteにする。

      • [サイズ [w, h] $] を(0.05, 0.05)にする。

      • [回転角度 $] を-90にする。

      • [位置 [x, y] $] を(-0.2, -0.4)にする。

    • rewindをコピーして、forwardという名前で貼り付けて以下の通り設定する。

      • [回転角度 $] を90にする。

      • [位置 [x, y] $] を(0.0, -0.4)にする。

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

      • [名前] をpauseにする。

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

      • [形状] を「長方形」にする。

      • [塗りつぶしの色] をwhiteにする。

      • [サイズ [w, h] $] を(0.05, 0.05)にする。

      • [位置 [x, y] $] を(-0.1, -0.4)にする。

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

      • [名前] をcloseにする。

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

      • [形状] を「十字」にする。

      • [塗りつぶしの色] をwhiteにする。

      • [回転角度 $] を45にする。

      • [サイズ [w, h] $] を(0.07, 0.07)にする。

      • [位置 [x, y] $] を(0.2, -0.4)にする。

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

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

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

      • [ボタン押しでRoutineを終了] を「有効なクリック」にする。

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

      • [クリック可能な視覚刺激] にcloseと入力する。

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

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

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

step = 5.0

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

ct = movie.getCurrentFrameTime()

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.status == PLAYING:
        movie.pause()
    elif movie.status == PAUSED:
        movie.play()

簡単に処理内容を解説しておくと、まずgetCurrentFrameTime()で現在の再生位置を得て変数ctに代入しておきます。続いて 第8章 で少し触れたマウスオブジェクトのメソッドである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となっているので、if文で分岐してPLAYINGならばpause()、PAUSEDならばplay()を実行します。

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

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

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

_images/mouse-click-frame.png

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

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

wait = 0

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

ct = movie.getCurrentFrameTime()
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.status == PLAYING:
        movie.pause()
    elif movie.status == PAUSED:
        movie.play()

この例では、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

動画の長さ。単位は秒。

getCurrentFrameTime()

動画の現在の再生位置を返す。単位は秒。

play()

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

pause()

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

seek()

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

チェックリスト

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

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

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

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

音声と動画の再生の解説が終わったので、続いて録音、録画の解説に進みましょう。まず音声の録音を行うMicrophoneコンポーネントを取り上げますが、バージョン2022.2.4の時点で Microphoneコンポーネントにはバグがあり、かなり用途は制限されると考えておいてください。 Microphoneコンポーネントは「反応」カテゴリにあります( 図10.7 )

_images/microphone-component.png

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

Microphoneコンポーネント独自のプロパティとして、まず「基本」タブの [デバイス] が挙げられます。ここでは録音に使用するデバイスを選択します。初期値はdefaultで、OSで設定されている標準の録音デバイスを使用します。他にPsychoPyで検出されたデバイスが列挙されているので、複数の録音デバイスがPCに存在している場合にどれを使うか指定できます。

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

[文字変換する言語] は en-US や ja-JP のようにロケールIDで指定します(en-USはアメリカ英語、ja-JPは日本の日本語を意味しています)。 [検出する語 $] は特定の単語だけを検出して記録したい場合に、その単語を列挙します。バックエンドがBuilt-inの場合はred:100とかgreen:80といった具合に「指定された単語検出した」と判定する際の信頼度の下限をパーセントで指定することができます。

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

「ハードウェア」の [チャネル] はステレオ、モノラルの選択(初期値はautoで自動検出)、 サンプリングレート(Hz) は音質を指定します。 [最大録音データサイズ(kb)] は1回の録音で作成されるファイルサイズの上限を指定します。この上限を超えた後は自動的に録音が停止しますが、対応するMicrophoneオブジェクトのisRecBufferFull()というメソッドで上限に達したかどうかを調べることができます(上限に達したらTrueが返される)。

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

以上がMicrophoneコンポーネントの概要ですが、最初に書いたとおり2022.2.4の時点でMicrophoneコンポーネントにはバグがあり、使用にはかなり制限があります。具体的には、 [終了] を空欄にすると録音自体できません。また、 [終了] で指定した条件を満たしたあともルーチンが継続した場合、エラーが生じて実験が停止します。どちらも致命的な問題ですが、とりあえず

  • [終了] を空欄にせずに終了時間を指定し、他のコンポーネントの終了時間がそれより長くならないようにする

ことを守れば使用できるはずです。PsychoPyが出力する実験の構造に詳しい方向けに書くと、

  • Microphoneコンポーネントの終了条件を入力しつつ、なおかつその終了条件を満たして終了コードが実行される前に他の方法でルーチンを終了すること

が条件です。 例を挙げましょう。新たに実験を作成して、以下のように作業してください。

  • 実験設定ダイアログ

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

  • trialルーチン

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

      • [終了] に5と入力する。

      • [Routineを終了] にチェックが入っていることを確認する。

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

      • [終了] に5と入力する。

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

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

      • [終了] に5と入力する。

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

できたら実行してみましょう。キーを押さなければ5秒間録音されて実験が終了します。画面上のカウントが5になる前にキーを押して終了しても、きちんと中断するまでの音が録音されるはずです。無制限に反応を待たなければならない実験でなければ、 [終了] に十分長い時間を指定することで何とか使い物になるのではないかと思います。

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

Builderから録音デバイスが認識されていて、実験が正常に動作しているように見えるにも関わらず音声ファイルが出力されない場合は、「ハードウェア」の [チャネル]サンプリングレート(Hz) に問題がある可能性があります。モノラルマイクを使っているなら [チャネル] を auto のままにせず mono に変更する、デバイスが対応しているサンプリングレートを調べて サンプリングレート(Hz) の設定を合わせるなどすると録音できるようになる場合があります。

チェックリスト

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

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

録音に続いて動画撮影を取り上げます。使用するのはコンポーネントペインの「反応」カテゴリにあるCameraコンポーネントです( 図10.8 )。このコンポーネントはMicrophoneコンポーネントのようなバグがあるわけではありませんが、動画というもの自体の扱いの難しさのためこちらもなかなか一筋縄ではいきません。

_images/camera-component.png

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

Cameraコンポーネントの独自のプロパティとしては、まず「基本」タブの [ビデオデバイス] があります。Microphoneコンポーネントではデバイスと記録フォーマットを別々に設定しましたが、Movieコンポーネントでは [ビデオデバイス] に使用するデバイスと記録フォーマットが組み合わされた形でリストアップされます。例えば [HD Pro Webcam C920] 800x600@30fps, h264 と表示されていれば、[HD Pro Webcam C920]がデバイス名、800x600は動画の解像度が幅800×高さ600、@30fpsは秒間フレームが30、h264が動画のフォーマットです。かなりの種類の組み合わせが表示されると思いますが、残念ながらすべての組み合わせが利用可能というわけではありません。各自の環境でどの組み合わせなら利用できるか試行錯誤する必要があります。「基本」タブには他に [オーディオデバイス] という項目もありますが、2022.2.4の時点でこの項目はdefaultから変更できません。

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

  • 実験設定ダイアログ

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

    • 安定して使用できる [ビデオデバイス] の選択肢が判明するまでは [フルスクリーンウィンドウ] のチェックを外しておく。

  • trialルーチン

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

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

      • [Routineを終了] にチェックが入っていることを確認する。

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

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

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

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

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

      • [ビデオデバイス] を自分の環境に合わせて設定する。どれにしたらいいかわからない場合、320x240などの低めの解像度で30fps以下のものから試してみるとよい。また、カメラのカタログ等で「h264対応」などと書いてある場合はそのフォーマットを優先的に試すのもよいだろう。

完成したら実行してみましょう。実験が始まった直後に終了してしまって、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にしておく。

  • 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.colorData is not None:
    frame_img = frame.colorData.astype(np.float).reshape(
        (frame.size[1],frame.size[0],3))
    frame_img /= 256
else:
    frame_img = np.ones((16,16,3),dtype=np.float)

入力したら実行してみよう。うまくいけば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.9 のアイコンです。他のコンポーネントとは異なり、 [名前] の初期値がコンポーネント名と同じではなく ISI となっているので注意してください。 [開始][終了] は他のコンポーネントと同様、Staticコンポーネントが有効な期間を指定します。

_images/static-icon.png

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

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

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

_images/static-update.png

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

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

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

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