Effective Praat Script

# はじめに

Praat Scrptを使っていると、 **「文法は分かった、でも実際にコードを書く時にはどうすればいいの？」** と戸惑うことが多いかと思います。

**「Praat Scriptを活用したい」、「コードを読みやすく整理したい」、そんなあなたに、コーディングのテクニック集をまとめました**。

元ネタは、[公式リファレンス](https://www.fon.hum.uva.nl/praat/manual/Scripting.html)、[CPrAN](https://cpran.net/)、そして学生時代試行錯誤した私の体験談です。

~~かなり主観が入っているところもあるのでご了承ください~~。「もっと楽なやり方あるぞ」等**コメントでのツッコミ大歓迎です**。

なお、Praatの分析機能や音声処理については範囲が広いので本記事の対象外としました。また、Praat Scriptの基本的な構文については前回の記事をご覧ください。

シリーズ一覧

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

以下一覧です。**めちゃくちゃ長い**ですがどこから読み始めても大丈夫です。

# バージョン

```praat
appendInfoLine: praatVersion$
; 6.2.11
```

# コーディングスタイル

## 変数、Procedure名にはcamelCaseを用いる

組み込み関数、定数は全てcamelCaseで定義されています (`appendInfoLine`, `sumOver`, `praatVersion` 等)。自分で定義する場合もこの命名に合わせたほうがよいでしょう。

```praat
procedure myProc:
	appendInfoLine: "foo"
end

myStr$ = "foo"
```

また、変数名の先頭に **`_` や大文字を付けることはできません**。
定数を表したい場合は `MY_CONST` ではなく `kMyConst` のような形式を使う必要があります[^const]。

```praat
; _foo = 1 ; syntax error
; Foo = 1 ; syntax error
```

## procedureの引数の渡し方を統一する

procedureの引数の書き方には、以下の２種類があります。

- かっこでくくる形式 `@foo: a, b`
- コロンの後に羅列する方式 `@foo(a, b)`

どちらも意味的には同じですが、リポジトリ内では統一することをおすすめします。
なお、本記事では[公式リファレンス](https://www.fon.hum.uva.nl/praat/manual/Scripting.html)に倣いコロン形式で記述しています。

```praat
# 混ぜて書くと読みづらい...

procedure printAdd: .a, .b
	appendInfoLine: .a + .b
endproc

procedure printSub(.a, .b)
	appendInfoLine(.a - .b)
endproc

@printAdd: 1, 2
@printSub(1, 2)
```

## 使用する論理演算子を統一する

こちらもスタイルの統一の話です。論理演算子(and, or, not)の書き方はいくつかありますが、

- `&`, `|`, `!`
- `&&`, `||`, `!`
- `and`, `or`, `not`

のどれかの組み合わせにすることをおすすめします。

## 予約語と同じ変数名を使わない

`and`, `or`, `not` は予約語でありながら、なぜか変数名として宣言可能です。もちろん参照時に構文エラーになるので使わないようにしましょう。

```praat
and = 1
appendInfoLine: and # error: symbol misplaced
```

## 比較演算子には `=` より `==` を使用する

`=` は代入演算子としても、比較演算子 (`==` と同じ) としても使用できます。
公式リファレンスでも `=` が使用されていますが、紛らわしいので `==` をおすすめします。

```praat
# 代入？比較？
isSame = a = b
# これなら分かりやすい
isSame = a == b
```

ちなみに等しくない場合は `!=`, `<>` の２通りの書き方がありますが、好みで良いと思います。

## 長すぎる行は `...` で折り返す

1行が長いと、スクロールが必要で見づらいので改行しましょう。

```praat:intensitycontours.praat
procedure main(.wavDirectoryPath$)    
    # ...

	# 長い行を ... で途中改行
    .tableObj = do("Create Table with column names...",
    ...            "stats", .numFiles, "file mean max min")
```

ちなみに例は[音声強度時系列抽出コード](https://github.com/Syuparn/praat_batch_analysis/blob/master/intensitycontours.praat#L44)からの引用です。詳細については以前の記事をご覧ください。

https://qiita.com/Syuparn/items/186b916054fbba87a3f1

## 違う型で同名の変数を定義しない

型が違えば全く別の変数として扱われるので、`a` と `a$` を同時に定義することができます。が、紛らわしいだけなのでやめましょう。

## `#` は説明のためのコメント、`;` はコメントアウトに使う

```praat
# printAddは引数の和を表示します。
procedure printAdd: .a, .b
	appendInfoLine: .a + .b
	;appendInfoLine: "debug: a=", .a, " b=", .b
endproc
```

[公式リファレンス](https://www.fon.hum.uva.nl/praat/manual/Scripting_3_7__Layout.html)より

> Because of its visibility, you are advised to use "#" for comments that structure your script, and ";" perhaps only for "commenting out" a statement, i.e. to temporarily put it before a line that you don't want to execute.
> 見やすさのため、"#" はコードを構造化するためのコメントに、 ";" は単なる文の「コメントアウト」（例：実行したくない文の先頭に一時的に付ける）に使うことをおすすめします。

ただし、文中コメントは `;` しか使えません。

```praat
area = pi * r ^ 2 ; 円の面積の公式 πr^2
```

## true, falseを変数宣言する

Praat Scriptには**bool型はありません**。number型をboolの代わりに使用します(0がfalsy, それ以外がtrythy)。また、真偽値判定する組み込み関数も trueとして `1`, falseとして `0` を返します。
初見ではマジックナンバーに見えてしまうので、`true`, `false` をグローバルに宣言すると意図が伝わりやすいです。

特にprocedureの戻り値に `0` や `1` と書くとnumberを返すように見えてしまうので、 `true`, `false` にした方がぱっと見で分かりやすいです[^bool]。

```praat
true = 1
false = 0

procedure contains: .strings$#, .element$
	.found = false
	for .i from 1 to size(.strings$#)
		if .strings$#[.i] == .element$
			.found = true
		endif
	endfor

	.return = .found
endproc

@contains: {"foo", "bar", "baz"}, "bar"
appendInfoLine: contains.return ; 1
```

## Praatの機能を呼び出す際には `do` を用いる

Praat ScriptはPraat自動化のための言語なので、Praatでできることはなんでもコード化可能です。
UI上でポチポチしたあとPraat Scriptのエディタで「Paste History」すると、操作をすべてコード化してくれます。

```praat:例：「About Praat」を開いてから「Paste History」
About Praat
```

しかし、ここで貼り付けられるコードは古い文法なので、空白の扱いが厄介です（古い文法については「`call` よりも `@` を使う」参照）。**可能な限り `do` を使った形式に書き直しましょう**。

```praat:安全な形式に書き直し
do("About Praat")
```

`Command "xxx" not available` と怒られた場合は、`...` を後ろに付けると大抵うまくいきます。

```praat
# 古い文法
Read from file: "1.wav"

# これはエラー
; do("Read from file", "1.wav")
# これならok
do("Read from file...", "1.wav")
```

`do` 関数の利点については、「Praatスクリプト基礎文法最速マスター」[^master] で詳しく解説されています。

https://id.fnshr.info/2013/11/10/praat-script-renewal/

## 大きな定数は指数表記する

`0` が多いと見づらいので、 `1e8` のような形式で宣言しましょう。

```praat
n = 1e8
appendInfoLine: n ; 100000000
```

# 文字列処理

## ソースコードの先頭に `clearinfo` を必ず記載する

`clearinfo` はinfoへの出力を全消去する命令です。

よく使う `appendInfoLine` は名前の通りinfoへの「追記」なので、実行するたびにinfoの出力が増えてしまいます。

```praat
appendInfoLine: "hello"
```

```:何度も実行すると...
hello
hello
hello
...
```

何度実行しても `hello` を1つだけ表示したいので、先頭に `clearinfo` を付けましょう。

```praat
# 最初にinfoを全消去
clearinfo
appendInfoLine: "hello"
```

一応、最初の文字列出力のみ `writeInfoLine` (infoを消してから表示)を使うことでも可能です。しかし、後でもっと前に文字列出力が必要になった際 `appendInfoLine` に修正が必要で面倒なのでお勧めしません。

## 改行、タブは `\n`, `\t` ではなく `newline$`, `tab$` を用いる

`appendInfoLine` で `\n`, `\t` を使うことはできません。組み込み定数 `newline$`, `tab$` を使いましょう。

```praat
appendInfoLine: "status", newline$, "------------------"
appendInfoLine: tab$, "HP:", tab$, 100/100
appendInfoLine: tab$, "ATK:", tab$, 50
appendInfoLine: tab$, "DEF:", tab$, 70
```

```
status
------------------
	HP:		1
	ATK:	50
	DEF:	70
```

## 改行したくない場合は `appendInfo` を用いる

複数要素を同じ行に出力したい場合は `appendInfo` (`appendInfoLine` の改行無し版)が便利です。

```praat
for i from 1 to 10
	appendInfo: i, ","
endfor
appendInfoLine: ""
```

```
1,2,3,4,5,6,7,8,9,10,
```

## 文字列のn文字目の取得には `mid$` を用いる

他の言語のように `s$[i]` と書くことは**できません**。`s$[i]` は疑似配列という別の文法要素です（「「疑似配列」よりもVectorを使う」参照）。代わりに、 `mid$` 関数で部分文字列を切り出しましょう。

```praat
s$ = "abcde"
# 3文字目から1文字取得
appendInfoLine: mid$(s$, 3, 1) ; c
# こうは書けない！（疑似配列 `s$[3]` の宣言になってしまう！）
; appendInfoLine: s$[3]
```

## ダブルクォーテーションのエスケープは `""` と書く

`\"` は使えません。

```praat
appendInfoLine: "name is ""foo""" ; name is "foo"
```

## 小数の表示には `fixed$` を使用する

Praat Scriptには `printf` はありません。numberを表示すると桁がだらだらと表示されてしまいます。

```praat
appendInfoLine: 1 / 3 ; 0.3333333333333333
```

特定の桁数で丸めたい場合は、`fixed$` を使いましょう。

```praat
appendInfoLine: fixed$(1/3, 4) ; 0.3333
```

## 文字列分割には `splitByWhitespace$#` を用いる

空白限定ですが、`splitByWhitespace$#`でstringをstring vectorに分割可能です。

```praat
animal$ = "cat dog mouse rabbit"
animals$# = splitByWhitespace$#(animal$)

for i from 1 to size(animals$#)
	appendInfoLine: animals$#[i]
endfor
```

```
cat
dog
mouse
rabbit
```

# Vector, Matrix, String Vector

vector (`a#`), matrix (`a##`), string vector (`a$#`) は6系で導入された比較的新しい型のため、あまり使用例が出てきません。しかし、簡潔なコードを書くためにぜひとも押さえておきたいです。

## Vector, Matrix, String Vectorの構文をおさえる

vectorはいわゆる「numberの配列」です。第一級オブジェクトで、要素の参照、破壊的変更も可能です。

```praat
vec# = {2, 4, 6}
appendInfoLine: vec# ; 2 4 6
appendInfoLine: vec#[2] ; 4 (添え字は1始まりなので注意！)
# 破壊的変更
vec#[2] = 5
appendInfoLine: vec# ; 2 5 6
```

matrixは行列、つまり「vectorの配列」です。特徴はvector同様です。

```praat
mat## = {{1, 2}, {3, 4}}
appendInfoLine: mat##
; 1 2
; 3 4

# 添え字の書き方に注意！
appendInfoLine: mat##[1, 2] ; 2
```

string vectorはvectorのstring版です。末尾の記号の覚え方は 「string `$` vector `#`」です。

```praat
strvec$# = {"foo", "bar", "baz"}
appendInfoLine: strvec$#; foo bar baz
appendInfoLine: strvec$#[2]; bar
```

## 組み込み関数を使いシンプルにvectorを初期化する

vectorの生成関数をうまく活用しましょう。

```praat
# 空
appendInfoLine: zero#(3) ; 0 0 0
appendInfoLine: zero##(2, 3)
; 0 0 0
; 0 0 0
appendInfoLine: empty$#(3) ; "" "" ""

# 1からnまで
appendInfoLine: to#(4) ; 1 2 3 4

# 等差数列 (Pythonでいうrange)
# (from, to, 要素数, toを含むかどうか(optional, 1:True, 0:False))
appendInfoLine: linear#(0, 9, 3, 0) ;0 4.5 9
appendInfoLine: linear#(0, 9, 3, 1) ;1.5 4.5 7.5
# 引数は省略可能 
appendInfoLine: linear# (0, 9, 3) ;0 4.5 9
```

## 数列を使用したvector, matrix要素更新を活用する

`~` を利用すると、vector全要素を要素数を含む式で初期化可能です。数列を生成する際に便利です。

```praat
vec# = zero#(5)
# 要素を設定
# vec#[i] = i * 3 + 1
vec# ~ col * 3 + 1
appendInfoLine: vec# ; 4 7 10 13 16

# matrixでも使用可能
m## = zero##(2, 2)
m## ~ row * 10 + col
appendInfoLine: m##
; 11 12
; 21 22
```

## vectorの演算子を使いこなす

名前の通り、vectorはベクトルとしての演算が可能です。for文で各要素を更新せずとも、1行で加減乗除が可能です。

```praat
# vectorどうし
appendInfoLine: {1, 2} + {3, 4} ; 4 6 (連結ではないので注意！)
appendInfoLine: {1, 2} - {3, 4} ; -2 -2
appendInfoLine: {1, 2} * {3, 4} ; 3 8
appendInfoLine: {1, 2} / {3, 4} ; 0.333... 0.5

# vectorとnumber
appendInfoLine: 3 + {1, 2} ; 4 5
appendInfoLine: {1, 2} + 3 ; 4 5
appendInfoLine: 3 * {1, 2} ; 3 6

# matrixとmatrix
appendInfoLine: {{1, 0}, {0, 1}} + {{0, 2}, {2, 0}}
; 1 2
; 2 1
# NOTE: 行列積ではなく要素積
appendInfoLine: {{1, 2}, {3, 4}} * {{1, 2}, {2, 1}}
; 1 4
; 6 4

# matrixとnumber
appendInfoLine: {{1, 0}, {0, 1}} + 3
; 4 3
; 3 4

# matrixとvector
appendInfoLine: {{1, 1}, {1, 1}} + {1, 2}
; 2 3
; 2 3
```

## vectorの関数を使いこなす

vectorやmatrixを扱う関数も豊富に用意されています。for文で各要素を更新せずとも(ry

```praat
# 繰り返し
appendInfoLine: repeat#({1,2}, 3) ; 1 2 1 2 1 2

# 内積、直積
appendInfoLine: inner({1, 2}, {3, 4}) ; 11
appendInfoLine: outer##({1, 2}, {3, 4})
; 3 4
; 6 8

# 行列積
m1## = {{1, 2}, {3, 4}}
m2## = {{2, 0}, {0, 2}}
appendInfoLine: mal##(m1##, m2##)
; 2 4
; 6 8

# 統計
appendInfoLine: sum({1, 2, 100}) ; 103
appendInfoLine: mean({1, 2, 100}) ; 34.33...
appendInfoLine: stdev({1, 2, 100}) ; 56.87...
appendInfoLine: center({1, 2, 100}) ; 2.96...

# 乱数
# 乱数(要素数, min, max)
appendInfoLine: randomInteger#(zero#(10), 1, 100)
# 一様分布(要素数, min, max)
appendInfoLine: randomUniform#(zero#(10), 1, 100)
# 正規分布(要素数, mean, std)
appendInfoLine: randomGauss#(zero#(10), 50, 10)

# 負(とundefined)の要素を0にする
appendInfoLine: rectify#({1, -2, 3, -4}) ; 1 0 3 0

# 要素の合計
squares# = {1, 4, 9, 16, 25}
appendInfoLine: sumOver(i to 5, i * squares#[i]) ;225
```

## matrix初期化にvectorを活用する

matrixにvectorを埋め込むことも可能です。うまく使えば初期化がシンプルに書けます。

```praat
vec# = {1, 2}
mat## ={vec#, vec#*2, vec#*3}
appendInfoLine: mat##
; 1 2
; 2 4
; 3 6
```

ただし、matrixの要素参照でvectorを取得することはできません...

```praat
; appendInfoLine: mat##[1] ; これはむり
```

## 範囲外の要素の参照をさけるため `size` を使う

vector等で範囲外の要素を参照すると `index out of bounds` エラーが発生します。

```praat
vec# = {1, 2, 3}
; appendInfoLine: vec#[2] ; index out of bounds
# 末尾を-1で取ることはできない
; appendInfoLine: vec#[-1] ; the element index should be positive
```

要素数を `size()` で取得しましょう。

```praat
# 末尾要素の取得
appendInfoLine:  vec#[size(vec#)] ; 3

# matrixの場合は行、列それぞれ確認可能
mat## = {{1, 2}, {3, 4}, {5, 6}}
appendInfoLine: numberOfRows(mat##) ; 3
appendInfoLine: numberOfColumns(mat##) ; 2
```

## 「疑似配列」よりもVectorを使う

vector導入の前から、疑似配列（pseudo array）という配列のような構文が存在しました。ネットでコード例を探してもよく登場します。

```praat
pseudoArr[1] = 10
pseudoArr[2] = 20
```

一見vectorと同じように見えますが、疑似配列は単なる変数名にすぎず (`pseudoArr[1]` という名前の変数) 、配列全体を `pseudoArr` という1変数で参照することはできません 。これが「**疑似**」配列と呼ばれるゆえんです。

特に、**疑似配列は要素数を取得することができません**。for文で要素をなめることができないので、理由が無ければvectorを使用しましょう。

```praat
# vector
vec# = {10, 20}
for i from 1 to size(vec#)
	appendInfoLine: i
endfor

# pseudo arrayは要素一覧が分からない...
pseudoArr[1] = 10
pseudoArr[2] = 20
for i from 1 to ???
```

また、**vectorの要素書き換えのつもりで `#` を忘れてしまい疑似配列を宣言してしまう**パターンも要注意です！

```praat
vec# = {10, 20}
# あっ！(vec#[1] = 100)と書きたかった
vec[1] = 100
```

## 連想配列が欲しい場合は疑似配列を使用する

~~前項で疑似配列をディスってしまいましたが~~ 疑似配列にしかできないこととして

- 連続しない添え字を使用できる
- 文字列の添え字を使用できる

があげられます。Enumや辞書が欲しい場合は、変数をまとめられる疑似配列を使うのが良いです。

```praat
price["water"] = 0
price["coffee"] = 450
price["tea"] = 400
```

## 時刻処理には `date$` より `date#` を使う

現在時刻の取得方法は `date$`, `date#` の2種類あります。今までは `date$` を頑張ってパースする必要がありましたが、今は `date#` のvectorを使うのが手軽です。

```praat
appendInfoLine: date$() ; Fri Apr 29 12:45:21 2022
appendInfoLine: date#() ; 2022 4 29 12 45 21

now# = date#()
if now#[4] >= 12
	appendInfoLine: "PM"
else
	appendInfoLine: "AM"
endif
```

# Procedure

procedureはPraat Scriptで**最も複雑な概念の1つ**です(個人の意見です)。関数と思って使うと戸惑いますが、**使いこなせれば「Praat Scriptらしい」コードが書ける**ようになります。

## `call` よりも `@` を使う

procedureの呼び出し方法には `@` と `call` の2種類があります。...が、`@` を使うことをおすすめします。
`call` は古い文法に準拠していて、ハマりどころが多いためです。

```praat
procedure printName: .first$, .last$
	appendInfoLine: .first$, " ", .last$
endproc

# @ を使った呼び出し
@printName: "Taro", "Yamada"
# call を使った呼び出し
call printName Taro Yamada
```

`call` をはじめ、古い文法の組み込み関数では引数をスペース区切りで渡します。
また、`""` で区切らずとも文字列リテラルとして扱われます。ややこしいことに、**スペースの数が引数よりも多いときは、残りがまるごと最後の変数に代入されます**。

```praat
procedure printName: .first$, .last$
	appendInfoLine: "first: ", .first$
	appendInfoLine: "last: ", .last$
endproc

call printName Johann Sebastian Bach
```

```
first: Johann
last: Sebastian Bach
```

また、引数に変数を渡したい場合は、 `''` で囲む必要があります。シングルクォーテーションによる変数の展開は強力な黒魔術なので、使わないに越したことはありません。

```praat
first$ = "Taro"
last$ = "Yamada"
call printName 'first$' 'last$'
```

## 同名のprocedureを定義しないよう注意する

同名のprocedureが複数定義された場合、**最初のものだけが有効です**。バグのもとなので名前が被らないようにしましょう。特に、include (後述) すると名前の衝突に気づきづらいので注意が必要です。

```praat
@foo() ; foo1

procedure foo()
	appendInfoLine("foo1")
endproc

@foo() ; foo1

procedure foo()
	appendInfoLine("foo2")
endproc

@foo() ; foo1
```

## procedure内部では必ずローカル変数 (`.foo`) を使用する

変数の先頭に `.` を付けると、ローカル変数として宣言できます。

```praat
foo$ = "global"

procedure myproc:
	# ローカル変数
	.foo$ = "local"
	# グローバル変数の上書き
	foo$ = "overwrite!"
endproc

@myproc:

# グローバル変数なので参照可能
appendInfoLine: foo$ ; "overwrite!"
# ローカル変数なので参照不可
; appendInfoLine: .foo$ ; unknown variable
```

グローバル変数を触るのはバグの元なので、なるべくローカル変数にしましょう。特に**引数の `.` は忘れがちなので要注意**！

```praat
foo = 1

# 引数がグローバル変数になっている！
procedure myproc: foo
	appendInfoLine: foo
endproc

@myproc: 2
appendInfoLine: foo ; 2
```

## procedureが名前空間であることを意識する

procedureの実態は**関数よりも「名前空間変更を伴うgoto」に近い**です。
先ほどの説明では `.` 付きの変数を「ローカル変数」と説明しましたが、実際には procedureの名前空間[^namespace]に定義されます。**`(procedure名).(変数名)` で、スコープを抜けても参照可能**です。

```praat
procedure myProc:
  .val = 1
endproc

@myProc:
# myProcのローカル変数は外側からも参照可能！
appendInfoLine: myProc.val ; 1
```

上記コード実行時には、以下のことが起こっています。

- myProcの呼び出し (`@myProc`) によって myProc 名前空間の内部にジャンプ
	- ` .val = 1` は `myProc.val = 1` と同等
	- 「関数」ではないので、スコープを抜けた後も変数は死なない
- `endproc` でスコープを抜けることによって、デフォルトの名前空間 (prefix無し)にジャンプ

これだけ読んでも「そんなバカな？ :thinking: 」とお思いでしょう。そこで、実際にgotoを使ってprocedureが名前空間の変更であることを確かめてみます[^goto]。

```praat
.foo$ = "outer"

@myproc:
appendInfoLine: myproc.foo$ ; inner
appendInfoLine: .foo$ ; outer

# gotoをこれ以上踏まないようスクリプト終了
exitScript: "bye"

procedure myproc:
	# gotoでprocedureの外に出る
	goto outOfProc
	label myprocAgain
endproc

label outOfProc
.foo$ = "inner"
# 再びmyprocの中へ
goto myprocAgain
```

procedureの外で代入した `.foo$ = "outer"` は、一見 `goto outOfProc` で飛んだ先の `.foo$ = "inner"` に上書きされそうです。しかし実際には `myproc.foo$ = "inner"` として評価されています。このことから、procedure呼び出しによって現在の名前空間を変更し、endprocで元に戻していることが分かります。

## procedureの（疑似）戻り値には `.return` ローカル変数を用いる

上記の名前空間の仕組みを使えば、 `.return` というローカル変数を（いわゆる「関数」の）戻り値として利用することができます。名前はなんでもいいのですが、 `.return` が分かりやすいでしょう。

```praat
procedure add: .a, .b
	.return = .a + .b
endproc

@add: 1, 3
appendInfoLine: add.return ; 4
@add: 2, 5
appendInfoLine: add.return ; 7
```

疑似配列を使うことで、多値返却も可能です。

https://github.com/Syuparn/praat_batch_analysis/blob/master/f0statistics.praat#L98


## ローカル変数をprivateにしたいときは、procedure名を `_` 始まりにする

これは少し裏技です。

上記の通り、procedureのローカル変数は基本外からでも読み書きすることが可能です [^local]。内部を弄られたくない場合は、 `_` で始まるprocedure名を使用しましょう。

```praat

procedure _secretProc: .a, .b
	.c = .a + .b
	appendInfoLine: .c
endproc

@_secretProc: 1, 2
# 外からは参照不可能！
; c = _secretProc.c ; unknown symbol: c
```

変数名の先頭に `_`  を付けられないことを思い出してください！外部から参照する場合の名前空間が変数として使用できない名前になるため、構文エラーが発生します。

## 変な名前のprocedureを定義しない

変数と違い、procedureに使える文字には制限がありません。とはいえ、変な名前は読みづらいのでやめましょう。

```praat
# むちゃくちゃな名前でも作れる！ (`s$="hello";assign` がprocedure名)
procedure s$="hello";assign: .a, .b
	.c = .a + .b
	appendInfoLine: .c
endproc

@s$="hello";assign: 1, 2 ; 3
```

## 複雑な条件分岐では、早期endprocを用いる

引き続き裏技です。**濫用厳禁**。

関数で複雑な条件分岐が必要な場合、早期returnを使ってネストを減らすテクニックがあります。

```python:Pythonの例（他の言語でも同様）
# (説明のために少し冗長な書き方をしています)
def is_even(x):
	# int以外をあらかじめ弾いておくことで、この後のネストが深くなるのを防ぐ
    if type(x) is not int:
      return False

    if x % 2:
      return False

    return True
```

Praat Scriptでも早期returnでコードをシンプルにしましょう...と言いたいところですが、Praat Scriptには**returnがありません**。そこで、関数の途中で `endproc` を入れ、関数から途中で脱出させます。

```praat:こういうこと？（※エラーになります）
true = 1
false = 0

procedure isEven: .x
	# 整数以外を弾いておく
	if floor(.x) != .x
		.return = false
		endproc
	endif

	if .x mod 2 == 0
		.return = false
		endproc
	endif

	.return = true
endproc
```

`endproc` は関数の末尾に1つしか書けないので、当然上記のコードは正しく実行できません。そこで、**インタープリターを騙してif文に入った時だけ `endproc` が評価される**ようにします。

```praat
endproc$ = "endproc"

procedure isEven: .x
	# 整数以外を弾いておく
	if floor(.x) != .x
		.return = false
		# インタープリターをだます黒魔術！（後述）
		'endproc$'
	endif

	if .x mod 2 == 0
		.return = false
		'endproc$'
	endif

	.return = true
endproc

@isEven: 2
appendInfoLine: isEven.return ; 0
@isEven: 2.5
appendInfoLine: isEven.return ; 0
@isEven: 5
appendInfoLine: isEven.return ; 1
```

今度は正しく実行できました。

種明かしをすると、「`call` よりも `@` を使う」で紹介した、古い文法で変数を引数に渡す構文を悪用しています。
 **`''` で括ると文の評価時に変数の中身が展開されてから実行されます**。（イメージとしてはマクロの実行時版です）

```praat
first$ = "Taro"
last$ = "Yamada"
# 変数を渡すときに '' で括っている
# 文の評価時に変数の中身が展開され、 `call printName "Taro" Yamada` が評価される
call printName 'first$' 'last$'
```

`'endproc$'` は、

- パース時には `endproc` として認識されないので、その後の文もprocedureとしてパースできる
- 実行時に当該の文を評価した際は `endproc` として評価されるので、procedureを抜ける

ので、早期returnの代わりに使えるというわけです。

また、`break`、`continue` の代わりにも使えます。

```praat
procedure contains: .strings$#, .element$
	for .i from 1 to size(.strings$#)
		if .strings$#[.i] == .element$
			.return = true
			# 見つかったらループを抜ける！
			'endproc$'
		endif
	endfor

	.return = false
endproc

@contains: {"foo", "bar", "baz"}, "bar"
appendInfoLine: contains.return ; 1
```

繰り返しですが、**構文の悪用なので濫用厳禁**です。~~それでも使いたい方は次回の[黒魔術](https://qiita.com/Syuparn/items/252e121ca7a20502a871)の記事へ~~

## procedureに状態を持たせたい場合は `variableExists` の使用を検討する

procedureをただの「戻り値を返すのに手間がかかる関数」と捉えるのはもったいないです。procedureにできて関数にできないこととして **状態管理** があげられます。
変数が死なないことを活かし、procedureをオブジェクトやイテレータのように利用することができます。

この際に使えるのが、変数が定義されているかどうか調べる `variableExists` です。未定義であれば初期化、定義済みであれば変更することで、呼び出すたびに戻り値を変えることができます。

```praat
procedure counter:
	# 変数名の文字列を指定することで、定義されているかどうか呼び出し前に確認可能
	if variableExists(".return")
		# 定義済みならインクリメント
		.return += 1
	else
		# 未定義なら初期化
		.return = 1
	endif
endproc

@counter:
appendInfoLine: counter.return ; 1
@counter:
appendInfoLine: counter.return ; 2
@counter:
```

## 引数に破壊的変更を加える代わりに、新しい値を返すことを検討する

Procedureは全て値渡し（「参照の値渡し」でもない）ので、引数に破壊的変更を加えても**呼び出し元の値に影響は及びません**。
引数のvectorに変更を加えただけでは意味が無いので、変更を加えたvectorを返しましょう。

```praat:これは意味が無い
procedure double: .vec#
	.vec# *= 2
endproc

v# = {1, 2, 3}
@double: v#
# 値渡しなのでv#はそのまま
appendInfoLine: v# ; 1 2 3
# もちろん double.vec#は破壊的変更が加わっている
appendInfoLine: double.vec# ; 2 4 6
```

```praat:こうしよう
procedure double: .vec#
	.return# = .vec# * 2
endproc

v# = {1, 2, 3}
@double: v#
appendInfoLine: double.return# ; 2 4 6
```

# スコープ

## ループ変数をループの外から参照しない
**スコープを作れるのはprocedureだけです！** for文のループ変数はループを抜けても生き残りますが、可読性が下がるので触らないようにしましょう。

```praat
for .i from 1 to 10
	appendInfoLine(.i)
endfor

appendInfoLine(.i) ; 11
```

`sumOver` のループ変数も同様です。

```praat
squares# = {1, 4, 9, 16, 25}
appendInfoLine: sumOver(i to 5, i * squares#[i]) ;225
appendInfoLine: i ;6
```

# ファイル操作

## 文字コードをUTF-8に設定する

デフォルトではファイル出力の文字コードに `UTF-16BE` が用いられます。他でも使いやすいように `UTF-8` を設定しましょう。

```praat
# 以降のファイル出力形式がすべてUTF-8になる
do("Text writing preferences...", "UTF-8")

writeFileLine: "~/foo.txt", "あ"
```  

## ファイル読み込み、ファイル名読み込みにstring vectorを活用する

`fileNames$#()` は、パターンにマッチするファイル名の一覧をstring vectorで取得できます。

```praat
appendInfoLine: fileNames$#("wavs/*.wav") ; 1.wav 2.wav 3.wav 4.wav 5.wav
```

また、`readLinesFromFile$#()` はファイルの中身を行ごとのstring vectorで取得できます。

```:text.txt
line1
line2
line3
```

```praat
fileName$ = "path/to/text.txt"
lines$# = readLinesFromFile$#(fileName$)
appendInfoLine: lines$#[2] ; line2
```

どちらもファイル操作を簡素化できるので活用したいです。

## オブジェクトの操作にはIDを用いる

Praatでは、読み込んだファイル(音声、csv等)や生成した情報（韻律、強度等）を「オブジェクト」として管理しています。「Praat Objects」の画面に一覧表示され、各オブジェクトはオートインクリメントで払い出されたIDを持っています。

![objects.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/239887/d89462de-0d8a-70cf-9e2d-a608ee9aa070.png)


Praat Scriptでファイル操作する場合も、基本的にこのオブジェクトIDを使用します。
オブジェクトを生成する関数はオブジェクトIDを返します。

また、オブジェクト生成に別オブジェクトが必要な場合は、現在選択されている（画面上青くなっている）オブジェクトが使用されます。

```praat
fileName$ = "1.wav"
# wavファイル読み込み。戻り値は音声オブジェクトのID
soundObj = do("Read from file...", fileName$)

# 現在選択されているsoundObjを利用して、韻律オブジェクト生成
pitchObj = do("To Pitch...", 0, 75, 600)
```

選択されているオブジェクトIDは `selected` で取得可能です。

```praat
appendInfoLine: selected() ;選択されているオブジェクトのうち先頭のもののID
appendInfoLine: selected#() ; 選択されている全オブジェクトのID
```

## 生成したオブジェクトは使い終わったら削除する

オブジェクトを開きっぱなしにするとメモリを食います。for文でバッチ処理を行う場合最悪落ちる（経験談）ので、使い終わったらすぐに `removeObject` で削除しましょう。

```praat
# wavファイルから韻律情報を生成する例

fileName$ = "1.wav"
# 後で消せるように、ここでオブジェクトIDを取得しておく
soundObj = do("Read from file...", fileName$)

# 韻律オブジェクト生成
do("To Pitch...", 0, 75, 600)

# 使い終わったらお掃除！
removeObject(soundObj)
```

韻律オブジェクトも後で消したい場合は、同様に `pitchObj = do("To Pitch...", 0, 75, 600)` とオブジェクトIDを取っておきましょう。

## 対象ファイルをユーザーに選ばせたい場合は `chooseReadFile$` を使用する

`chooseReadFile$` を使うと、ファイルエクスプローラーが開き、ユーザーがファイルを選ぶことができます。

```praat
# 引数はファイルエクスプローラーのタイトル
fileName$ = chooseReadFile$("open text file")
text$ = readFile$(fileName$)
appendInfoLine: text$
```

![choosefile.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/239887/e57b04f8-9516-802b-544e-1c952a44bd2e.png)

# エラーハンドリング、安全な操作

## 環境依存の処理では、組み込み定数を活用する

Praatには、実行環境情報を得る定数がいくつか存在します。設定ファイル読み込みやOS依存処理がある場合は、以下の定数が役に立ちます。

```praat
# ディレクトリ関連
appendInfoLine: shellDirectory$ ; C:\WINDOWS\system32
appendInfoLine: defaultDirectory$ ; C:\WINDOWS\system32
appendInfoLine: preferencesDirectory$ ; C:\Users\syuparn\Praat
appendInfoLine: homeDirectory$ ; C:\Users\syuparn
appendInfoLine: temporaryDirectory$ ; C:\Users\syuparn

# OS関連
if macintosh
	appendInfoLine: "Mac"
elsif windows
	appendInfoLine: "Win" ; Win
elsif unix
	appendInfoLine: "Unix"
else
	appendInfoLine: "Unknown"
endif
```

また、Praatの本体バージョンも `praatVersion` で確認できます。バージョンに応じた場合分けに活用できます。（基本的に後方互換を謳っているので対処不要ですが）

```praat
appendInfoLine: praatVersion ; 6211
appendInfoLine: praatVersion$ ; "6.2.11"
```

## 0除算はエラーにならないので事前にチェックする

Praat Scriptでは、0除算は `undefined` (他言語の `NaN` に相当)を返し実行し続けてしまうので、事前に弾いておきましょう。

```praat
appendInfoLine: 0 / 0 ; undefined
```

## エラーを握りつぶす場合は `nocheck` を使用する

夜中にしかけたバッチ処理がたった1ファイルのエラーで止まるのは悲しいです（体験談）。エラーを許容できる処理の場合、 `nocheck` を使ってエラーを握りつぶしましょう。

コード例は、[昔作ったwavファイル変換コード](https://github.com/Syuparn/TextGridConverter/blob/master/convertwav_to_16khz.praat) を新しい構文に書き直したものです[^wavconvert]。

```praat
dir$ = "C:/Users/user/work/wavs"

@convertWav: dir$, 16000

procedure convertWav: .directory$, .resampleRateHz
	# wavファイル名リストオブジェクトの作成
	.wavNames$# = fileNames$#(.directory$ + "/*.wav")

	for .i to size(.wavNames$#)
		.wavName$ = .directory$ + "/" + .wavNames$#[.i]
		.file = do("Read from file...", .wavName$)

		# モノラルに変更
		do("Convert to mono")

		# サンプリングレート変更
		do("Resample...", .resampleRateHz, 50)

		# 変更後のwavファイルを上書き保存
		# nocheckを付けることで、失敗しても後続の処理を続けられる！
		nocheck do("Save as WAV file...", .wavName$)

		removeObject(.file)
	endfor
endproc
```

同様に、`nowarn` で警告を握りつぶすことができます。

## 危険な操作を伴う場合は、`pause` でユーザー確認画面を出す

ファイル削除等取り返しのつかない操作は、実行前にユーザーに確認画面を出すと親切です。 `pause` を使うと、スクリプトを一時停止してダイアログを表示します。

```praat
pauseScript: "danger!!!!"

# 実際はファイル削除が走る
appendInfoLine: "deleted all files..."
```

![pausescript.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/239887/cf182552-a9bc-ba8f-3c08-e11acab1af47.png)

`stop` を押すとスクリプトが中断されます。

![pause_interrupt.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/239887/cc9ba926-f2a8-76c3-4dc3-004837edaf76.png)

# ユニットテスト

## `assert` を使ってユニットテストを書く

`assert` を使うと、procedure等の細かい粒度でテストが可能です。
~~ちなみに学生時代の私はTDDを知らなかったので、「スクリプトの生成ファイルに差分が無ければOK」というゴリラな開発をしていました。~~

`assert` に渡せるのは真偽値(number)のみで、0の場合エラーが発生します。

```praat
procedure add: .a, .b
	.return = .a + .b
endproc

@add: 1, 2
assert add.return == 3 ; 成功すれば何も起きない

@add: 3, 4
assert add.return == 6 ; script assertion fails
```

## 異常系のテストには `asserterror` を使用する

`asserterror` は、期待したエラーが **発生しない** 場合にエラーになります。チェック対象は直後の文で、asserterrorの引数にはエラーメッセージを指定します。

```praat
procedure add: .a, .b
	.return .a + .b
endproc

# 引数が足りないときにエラーが発生することを確認
# (残念ながら、引数は古い文法しか対応していません...)
asserterror Empty formula.
@add: 1
```

エラーメッセージが期待したものと異なる場合もエラーが発生します。

```praat
asserterror Empty formula.
x += 1
```

![asserterror.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/239887/c37ea981-cf7c-5d8e-0447-842279e71571.png)

公式リポジトリのテストでも多く使われています。

https://github.com/praat/praat/search?q=asserterror

## 出力内容のテストには `info$()` を使用する

`appendInfoLine` 等の出力結果をテストしたい場合は、 `info$()` でinfoの出力内容を取得して検証しましょう。

```praat
procedure hello:
	appendInfoLine: "Hello, world!"
endproc

# テスト前にclearinfoを忘れずに！
clearinfo
@hello:
assert info$() == "Hello, world!" + newline$
```

## ベンチマークを `stopwatch` で取得する

`stopwatch` を使うと、実行時間の測定が可能です。

```praat
n = 1e7

# 測定開始
stopwatch

sum = 0
for i from 1 to n
	sum += i
endfor

# 直前のstopwatchからの経過時間[s]を返す
time = stopwatch

appendInfoLine: "for loop: ", tab$, time

# 何度も使用可能
stopwatch
sum = sum(to#(n))
time = stopwatch

appendInfoLine: "sum vector: ", tab$, time
```

```
for loop: 	2.46576189994812
sum vector: 	0.015965938568115234
```

# ソースコードの構造化

スクリプトが大規模化すると、1ファイルでの管理が苦しくなってきます。Praat Scriptのファイル分割には大きく

- `include`
- `runScript`

の2種類があります。以下、それぞれの特徴を紹介します。

## ソースコードのモジュール化には include を使用する

`include` を使用すると、別ファイルのソースコードを読み込めます。ファイルパスは呼び出し元から見た相対パスで指定します。

```praat:greet.praat
procedure greet: .name$
	appendInfoLine: "Hello, " + .name$ + "!"
endproc
```

```praat:main.praat
clearinfo
include greet.praat

@greet: "Taro"
```

**includeは名前空間を作りません**。procedure名が衝突しないように注意しましょう。
（大規模化したら、`procedure greet.greet` のように（疑似的な）階層構造のある名前を付けるのもありかもしれません）

## includeする用のソースコードにはprocedure/定数定義以外含めない

includeしたソースコードはすべて実行されます。ライブラリとしての再利用性を上げるため、includeする用のソースコードにはprocedureや定数の定義だけ書くようにしましょう。

```praat:greet.praat
# もし処理を含むと...
appendInfoLine: "this is greet.praat!"

procedure greet: .name$
	appendInfoLine: "Hello, " + .name$ + "!"
endproc
```

```praat:main.praat
clearinfo
# procedureが利用したいだけなのに余計な処理が実行されてしまって使いづらい...
include greet.praat

@greet: "Taro"
```

## includeを使用する場合、ディレクトリ構成をフラットにする

includeは、実行ファイルから見た相対パスで指定します。ディレクトリ構成が入れ子になっていると、思わぬパス指定ミスが起こってしまうこともあります。

```:ディレクトリ構成
- main.praat
	- sub
		- sub1.praat
		- sub2.praat
```

```praat:sub/sub1.praat
# 「相対パスで指定すればいいんだな」
include sub2.praat

procedure sub1:
	@sub2:
endproc
```

```praat:sub/sub2.praat
procedure sub2:
	appendInfoLine: "sub2"
endproc
```

```praat:main.praat
include sub/sub1.praat

@sub1:
```

一見よさそうですが、 `Cannot open file sub2.praat` が発生してしまいます。includeは main.praatの相対パスで参照されるので、 `sub/sub1.praat` には **`include sub/sub2.praat`** と書く必要がありました。

このようにパス指定がややこしくなってしまうので、なるべくフラットなディレクトリ構成にすることをおすすめします。

## スクリプト実行には `runScript` を使用する

もう一つファイルを分ける方法として `runScript` があります。 `include` がライブラリの読み込み向けだったのに対し、こちらは実行スクリプトの呼び出し向けです。

```praat:hello.praat
appendInfoLine: "hello"
```

```praat:main.praat
runScript: "hello.praat"
```

両者の特徴を比較すると以下の通りです。

||include|runScript|
|-|-|-|
|用途|ライブラリの利用|実行スクリプトの呼び出し|
|ファイル名|固定|動的に変えられる|
|パス|相対パス|相対パス|
|呼び出し元との関係|密結合|疎結合（実装は隠蔽される）|

## `runScript` の引数で呼び出し先スクリプトに引数を渡す

`runScript` に引数を指定することで、呼び出し先スクリプトのformに値を渡すことができます。

```praat:hello.praat
form Get Name
	# 型 変数名 デフォルト値
	word name John
endform

appendInfoLine: "Hello, " + name$ + "!"
```

```praat:main.praat
runScript: "hello.praat", "Taro" ; Hello, Taro!
runScript: "hello.praat", "Hanako" ; Hello, Hanako!
# 足りないとエラー（デフォルト値にはならない）
; runScript: "hello.praat" ; Found 0 arguments but expected more.
```

## `exitScript` のエラーメッセージで呼び出し元にエラーを返す

`runScript` されたスクリプトが呼び出し元にエラーを投げたい場合は `exitScript` を使用します。
`exitScript` はスクリプトを異常終了する関数で、`runScript` 関係なしに使うこともできます。

```praat: hello.praat
form Get Name
	# 型 変数名 デフォルト値
	word name John
endform

if name$ == ""
	exitScript: "name is invalid!"
endif

appendInfoLine: "Hello, " + name$ + "!"
```

```praat:main.praat
runScript: "hello.praat", "" ; エラー: name is invalid!
```

## 設定値を環境変数から読み込めるようにする

実行環境によって変えたい設定値は、コードにべた書きせずに環境変数から読み込めるようにしましょう。`environment$` を使うと環境変数を読み込めます。

```praat
appendInfoLine: environment$("TEMP") ; C:\Users\syuparn\AppData\Local\Temp
```

# ライブラリ

## CPrANを活用する

CPrANはPraat Scriptの（非公式）プラグインマネージャで、JSONや文字列処理等の便利ライブラリが多数登録されています。

http://cpran.net/

また、色々なテクニックが使われているので、コードリーディングするだけでも勉強になります（本記事のテクニックもいくつかはCPrANが元ネタです）。

~~とここまで言ったものの、実は使ったことはありません...活用した方の記事お待ちしております~~

# その他

## 公式リポジトリからPraat Scriptの構文を見つける

残念ながら、公式リファレンスに載っていない文法、組み込み関数も多く存在します。そんなときは、**公式リポジトリの以下の実装を確認すると新たな出会いがあるかもしれません**。

- [インタープリター実装](https://github.com/praat/praat/blob/6f8321e2cde4ea2585a3fe705a6c84b599e097ea/sys/Interpreter.cpp)
	-  予約語は `U'xxx'` の形で定義
- [式の定義](https://github.com/praat/praat/blob/6f8321e2cde4ea2585a3fe705a6c84b599e097ea/sys/Formula.cpp)
- [Praat Scriptのテストコード](https://github.com/praat/praat/tree/master/test/script)

# おわりに

以上、Praat Scriptを活用するためのテクニックでした。（もし通読された方がいれば、お疲れさまでした！）

分析ツールは秘伝のタレ化して自分でも読めなくなってしまうことがあるので、記憶が新しいうちにリファクタリングしておきたいですね。今回紹介したテクニックが1つでもお役に立てれば幸いです。

次回は趣向を変えて、実用性皆無の[Praat Script黒魔術](https://qiita.com/Syuparn/items/252e121ca7a20502a871)を紹介します。お楽しみに！

[^const]: 「[リーダブルコード](www.amazon.co.jp/dp/4873115655)」で紹介されていた、GoogleのC++フォーマット規約を参考にしています。
[^master]: 私の~~便乗~~記事ではなく本家様のほうです（念のため）
[^namespace]: 正式な用語ではありません。
[^local]: 字面の圧倒的矛盾。でも書いてある通りです。
[^goto]: もちろん実際のコードでの `goto` の利用は非推奨です
[^bool]: このテクニックはグローバル変数を汚すので賛否両論あると思います...慣れている人が多ければ `1`, `0` のままでも問題ないと思います。
[^wavconvert]: 元コードは今見返すとかなり汚いです...（GitHub初コミットなので許して :innocent:）うかつにリファクタリングしても古いPraatを使っているユーザーが使えなくなってしまうので、塩漬けにしています