Praat Scriptでゲームを作ってみよう

# はじめに

Praatは音声分析ソフトですが、グラフ等を表示するためのデモ画面もついています。
図形表示だけでなくボタンやキー入力等もできるので、今回は**Praat Scriptでゲームを作ってみたい**と思います。

これまでのシリーズ一覧

1. [Praat Script基礎文法最速マスター2022](https://qiita.com/Syuparn/items/eaac2dae596546e75208)
2. [Effective Praat Script](https://qiita.com/Syuparn/items/f2e108f370836c4567cb)
3. [悪用厳禁？Praat Scriptの黒魔術、謎文法](https://qiita.com/Syuparn/items/252e121ca7a20502a871)
4. Praat Scriptでゲームを作ってみよう (当記事)

# UI操作

基本的に、公式リファレンスのデモウィンドウの作り方を参考にしています。

https://www.fon.hum.uva.nl/praat/manual/Demo_window.html

## 図形を描く

まずは長方形を描いてみます。座標は数学のグラフ同様左下が原点です。

```praat
# デモ画面をリセット (clearinfo のUI版)
demo Erase all
# ビューポートを設定（100 x 100の範囲。ただしデフォルトだと外側のマージンが表示されてしまうので inner指定）
demo Select inner viewport: 0, 100, 0, 100

# 長方形（塗りつぶし）
# 引数: 色, x開始, x終了, y開始, y終了
demo Paint rectangle: "purple", 50%, 60%, 50%, 60%
```

実行するとデモ画面が表示されます。

![rectangle.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/239887/66754f70-8464-c4f4-a3c3-77d8d1bcfcd0.png)

他にも、線や円などを描くことができます。

```praat
demo Erase all
demo Select inner viewport: 0, 100, 0, 100

# 枠だけの長方形
demo Draw rectangle: 50%, 60%, 50%, 60%

# 線（引数の順番が長方形と違うので注意！）
# 引数: 色, x開始, y開始, x終了, y終了
demo Draw line: 20%, 20%, 30%, 30%

# 円
# 引数: 色, x, y, 半径
demo Paint circle: "blue", 70%, 70%, 5%
```

![shapes.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/239887/e818ed7c-f768-7fe4-222a-73a0b8e853e4.png)

線の書式はペンの設定で変えられます（デフォルトは黒の太さ1）。

```praat
# 線の幅を変更
demo Line width: 3
# 線の色を変更
demo Blue

demo Draw rectangle: 50%, 60%, 50%, 60%
```

![pen.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/239887/337f7b9d-8627-aebc-33a1-364698511b94.png)

## 文字を入れる

```praat
# 引数: x, 水平方向レイアウト, y, 垂直方法レイアウト, 文字列
demo Text: 40%, "left", 40%, "half", "Hello, world!"
```

![text.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/239887/920c44f6-3f2b-39e6-9e22-366a0890d00d.png)

実用上は、書式が指定できる `text special` の方が便利です。フォントは `Times`, `Helvetica`, `Courier`, `Palatino` から選べます。
(デフォルトは "Times", フォントサイズ10, 角度0度)

```praat
# 引数: x, 水平方向レイアウト, y, 垂直方法レイアウト, フォント, サイズ, 回転角度[度], 文字列
demo Text special: 40%, "left", 40%, "half", "Helvetica", 48, "30", "Hello, world!"
```

![text_special.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/239887/2a4bb69e-06be-ee50-1cdc-76b1c519441f.png)

文字色はペンの色が反映されます。

```praat
demo Red
# 引数: x, 水平方向レイアウト, y, 垂直方法レイアウト, フォント, サイズ, 回転角度[度], 文字列
demo Text special: 40%, "left", 40%, "half", "Helvetica", 48, "0", "Hello, world!"
```

![text_color.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/239887/eb5e152a-5399-87e0-7484-9a9df4c3dd74.png)

## 複雑な形の図形を描く

**関数の描画**を利用します。座標はビューポートと同じなので、 `[0, 1] * [0, 1]` の範囲のみ描画されます。

```praat
demo Erase all
demo Select inner viewport: 0, 100, 0, 100
# 引数: x開始, x終了, ステップ数（描画の点の数）, 関数
demo Draw function: 0, 1, 1000, "4*(x-0.5)^2"
```

![graph.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/239887/2f40e594-ebab-9657-6d97-65663b3277e1.png)


他の図形についても知りたい場合は、「Praat Picture」画面で「World」や「Pen」メニューをご覧ください。
引数についても、Paste Historyして頭に `demo` を付けることで確認できます。

## 図形を動かす

デモ画面では、既に描かれた図形を消すことはできません。図形を動かしたい場合は、いったん `demo Erase all` で画面を全消去してから再描画する必要があります。

```praat:ball.praat
demo Erase all
demo Select inner viewport: 0, 100, 0, 100

a = -1%
v = 0
y = 100%

# 図形を動かすたびに、画面をすべて再描画
while 1
    demo Erase all
    demo Select inner viewport: 0, 100, 0, 100
    demo Paint rectangle: "black", 0%, 100%, 0%, 100%
    demo Paint circle: "blue", 50%, y, 5%

    if y <= 5% && v < 0
	y = 5%
        v = -v
    endif
    
    v += a
    y += v

    pauseScript: "click to see next frame"
endwhile
```

(ちなみに、`sleep` を使うとなぜか画面が一面灰色になってしまいアニメーションには使えません...)

## デモ画面のタイトルを表示

ウィンドウに好きなタイトルを付けられます。

```praat
demoWindowTitle: "my demo window"
```

## クリックを検知してイベントを起こす

`demoWaitForInput()` は、デモ画面のイベントを待機します。クリックされたタイミングで、`demoClicked()`がtrueになります。

```praat:クリック回数を数える
procedure drawCount: .cnt
    .text$ = "clicks: " + string$(cnt)
    demo Erase all
    demo Text special: 40%, "left", 40%, "half", "Times", 48, "0", .text$
endproc

demo Select inner viewport: 0, 100, 0, 100
cnt = 0
@drawCount: cnt

while demoWaitForInput()
    if demoClicked()
        cnt += 1
        @drawCount: cnt
    endif
endwhile
```

![clicks.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/239887/304d4db1-9249-2c34-1a05-e43ec1a6a50c.png)

`demoX()` と `demoY()` を使えば、クリックされた座標も取得できます。

```praat:クリック場所を表示
procedure drawPosition: .x, .y
    .text$ = "x: " + fixed$(.x, 2) + ", y: " + fixed$(.y, 2)
    demo Erase all
    demo Text special: 60%, "left", 90%, "half", "Times", 48, "0", .text$
endproc

demo Select inner viewport: 0, 100, 0, 100
@drawPosition: 0, 0

while demoWaitForInput()
    if demoClicked()
        @drawPosition: demoX(), demoY()
    endif
endwhile
```

![position.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/239887/d34078fb-7866-6091-fcc8-7e2faa7cd9fa.png)

## キー入力を検知してイベントを起こす

クリック同様、キー入力も `demoKeyPressed()` で取得可能です。

```praat
procedure drawText: .text$
	demo Erase all
	demo Text special: 40%, "left", 40%, "half", "Times", 48, "0", "text: " + .text$
endproc

demo Select inner viewport: 0, 100, 0, 100
text$ = ""
@drawText: text$

while demoWaitForInput()
	if demoKeyPressed()
		if unicode(demoKey$()) = 8
			# backspace だったら1文字削除
			text$ = left$(text$, length(text$)-1)
		else
			# そうでなければ入力文字を追記
			text$ += demoKey$()
		endif

		@drawText: text$
	endif
endwhile
```

![keys.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/239887/42e6603b-9e1b-f09a-586e-96dec3709205.png)

# 応用例：ピアノを作ってみる

ここまでやってきたことの応用で、ピアノを作ってみたいと思います。なぜピアノかというと、Praatでは簡単に音を流せるからです[^praat]。

![piano.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/239887/de5c2c83-0663-8081-e9bb-738c2981cd5f.png)

鍵盤をクリックすると音が出ます。

コード全文はこちらにあります。ダウンロードして遊んでみてください。

https://github.com/Syuparn/praat_piano/tree/main

## クリック判定

クリックの座標がキーの範囲内であればキーが押されたと判定しています。ただし**鍵盤は重なり合っている（黒鍵が白鍵よりも前面にある）** ので、レイヤーが最も前面のキーを選択するようにしています。

```praat:keyboard.praat
# キーの長方形の情報（描画とクリック判定に使用）
procedure keyboardRectangles:
    # rectangle: {xFrom, xTo, yFrom, yTo, layer}
    # NOTE: 黒鍵が白鍵と重なっているため、座標情報以外にレイヤー情報を持たせている
    .c#      = {10%, 20%, 10%, 70%, 0}
    .cSharp# = {17%, 23%, 30%, 70%, 1}
    .d#      = {20%, 30%, 10%, 70%, 0}
    .dSharp# = {27%, 33%, 30%, 70%, 1}
    .e#      = {30%, 40%, 10%, 70%, 0}
    .f#      = {40%, 50%, 10%, 70%, 0}
    .fSharp# = {47%, 53%, 30%, 70%, 1}
    .g#      = {50%, 60%, 10%, 70%, 0}
    .gSharp# = {57%, 63%, 30%, 70%, 1}
    .a#      = {60%, 70%, 10%, 70%, 0}
    .aSharp# = {67%, 73%, 30%, 70%, 1}
    .b#      = {70%, 80%, 10%, 70%, 0}
    .cHigh#  = {80%, 90%, 10%, 70%, 0}

    # ベクトルをまとめて行列の形で返す
    .return## = {c#, .cSharp#, .d#, .dSharp#, .e#, .f#, .fSharp#, .g#, .gSharp#, .a#, .aSharp#, .b#, .cHigh#}
endproc
```

```praat:click.praat
# キー（長方形）一覧とクリックされた座標から、どのキーがクリックされたかを返す
procedure clickedRectangle: .rectangles##, .x, .y
    .found = -1 ; 見つかった長方形のインデックス (-1 は not found)
    .foundLayer = -1 ; 見つかった長方形のレイヤー

    for .i to numberOfRows(.rectangles##)
        @nth: .rectangles##, .i
        @includes: nth.return#, .x, .y

        if includes.return
            # 新しい長方形が既存のものより手前にあればそちらに交換
            .layer = .rectangles##[.i, 5]
            if .layer > .foundLayer
                .found = .i
                .foundLayer = .layer
            endif
        endif
    endfor

    .return = .found
endproc
```

## 音を流す

クリックされたキーに対応する音を流しています。音は `Create sound as pure tone` で生成した正弦波を使用しています。

```praat:sound.praat
procedure generateSound: .frequencyHz, .timeSec
    .name$ = "sound"
    .channels = 1 ; monoral
    .startSec = 0
    .endSec = .timeSec
    .samplingRateHz = 44100
    .amplitude = 0.2
    .fadeInDurationSec = 0.01
    .fadeOutDurationSec = 0.01

    # 音声オブジェクトを作成。戻り値はオブジェクトID
    .id = Create Sound as pure tone: .name$, .channels, .startSec, .endSec,
    ...                              .samplingRateHz, .frequencyHz, .amplitude,
    ...                              .fadeInDurationSec, .fadeOutDurationSec

    .return = .id
endproc
```

各キーの周波数はvectorで一括生成しています。

```praat:sound.praat
# 各キーの周波数を定義
procedure pianoFrequencies: .size
    .frequencies# = zero#(.size)
    # 周波数生成（C4（ト音記号の下のド）から .size 個）
    # A4 (ト音記号の真ん中のラ)が440Hz, 半音上がるごとに 2 ^ (1/12) 倍される
    .frequencies# ~ 440 * (2 ^ ((col - 10) / 12))

    .return# = .frequencies#
endproc
```

流すときは `Play` を使用します。同期処理だと音が流れている間画面が固まってしまうので、非同期処理にしています。

```praat:main.praat
procedure handleClickEvent: .soundIDs#
    # どのキーがクリックされたか判定
    @keyboardRectangles:
    @clickedRectangle: keyboardRectangles.return##, demoX(), demoY()

    # -1 (見つからない)のときは何もしない
    if clickedRectangle.return == -1
        'endproc$'
    endif

    # クリックしたキーに対応する音声オブジェクトを選択
    selectObject: .soundIDs#[clickedRectangle.return]
    # （非同期で）再生
    asynchronous Play
endproc
```

# おわりに

以上、Praat ScriptでUIを実装する方法の紹介でした。

Praat Scriptシリーズはこの記事で一旦終わりとしたいと思います。
社会人になってPraatをめっきり使わなくなってしまったので、記憶の断片が残っているうちに文書化しました。関数などかなり忘れてしまっていましたが、書いているうちに懐かしい気持ちがよみがえってきました。

ここまでお読みいただきありがとうございました！

[^praat]: ~~言語やUIの話ばかりしていましたが、~~ Praatは音声分析ソフトです。