LoginSignup
2
1

More than 1 year has passed since last update.

Effective Praat Script

Last updated at Posted at 2022-04-30

はじめに

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

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

元ネタは、公式リファレンスCPrAN、そして学生時代試行錯誤した私の体験談です。

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

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

シリーズ一覧

  1. Praat Script基礎文法最速マスター2022
  2. Effective Praat Script (当記事)
  3. 悪用厳禁?Praat Scriptの黒魔術、謎文法
  4. Praat Scriptでゲームを作ってみよう

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

バージョン

appendInfoLine: praatVersion$
; 6.2.11

コーディングスタイル

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

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

procedure myProc:
	appendInfoLine: "foo"
end

myStr$ = "foo"

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

; _foo = 1 ; syntax error
; Foo = 1 ; syntax error

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

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

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

どちらも意味的には同じですが、リポジトリ内では統一することをおすすめします。
なお、本記事では公式リファレンスに倣いコロン形式で記述しています。

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

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 は予約語でありながら、なぜか変数名として宣言可能です。もちろん参照時に構文エラーになるので使わないようにしましょう。

and = 1
appendInfoLine: and # error: symbol misplaced

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

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

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

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

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

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

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

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

ちなみに例は音声強度時系列抽出コードからの引用です。詳細については以前の記事をご覧ください。

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

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

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

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

公式リファレンスより

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.
見やすさのため、"#" はコードを構造化するためのコメントに、 ";" は単なる文の「コメントアウト」(例:実行したくない文の先頭に一時的に付ける)に使うことをおすすめします。

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

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

true, falseを変数宣言する

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

特にprocedureの戻り値に 01 と書くとnumberを返すように見えてしまうので、 true, false にした方がぱっと見で分かりやすいです2

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」すると、操作をすべてコード化してくれます。

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

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

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

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

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

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

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

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

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

n = 1e8
appendInfoLine: n ; 100000000

文字列処理

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

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

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

appendInfoLine: "hello"
何度も実行すると...
hello
hello
hello
...

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

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

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

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

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

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 の改行無し版)が便利です。

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$ 関数で部分文字列を切り出しましょう。

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

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

\" は使えません。

appendInfoLine: "name is ""foo""" ; name is "foo"

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

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

appendInfoLine: 1 / 3 ; 0.3333333333333333

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

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

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

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

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の配列」です。第一級オブジェクトで、要素の参照、破壊的変更も可能です。

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同様です。

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

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

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

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

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

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

# 空
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全要素を要素数を含む式で初期化可能です。数列を生成する際に便利です。

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行で加減乗除が可能です。

# 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

# 繰り返し
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を埋め込むことも可能です。うまく使えば初期化がシンプルに書けます。

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

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

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

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

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

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

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

# 末尾要素の取得
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)という配列のような構文が存在しました。ネットでコード例を探してもよく登場します。

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

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

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

# 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の要素書き換えのつもりで # を忘れてしまい疑似配列を宣言してしまうパターンも要注意です!

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

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

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

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

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

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

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

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

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 は古い文法に準拠していて、ハマりどころが多いためです。

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

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

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

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

call printName Johann Sebastian Bach
first: Johann
last: Sebastian Bach

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

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

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

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

@foo() ; foo1

procedure foo()
	appendInfoLine("foo1")
endproc

@foo() ; foo1

procedure foo()
	appendInfoLine("foo2")
endproc

@foo() ; foo1

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

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

foo$ = "global"

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

@myproc:

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

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

foo = 1

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

@myproc: 2
appendInfoLine: foo ; 2

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

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

procedure myProc:
  .val = 1
endproc

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

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

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

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

.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 が分かりやすいでしょう。

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

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

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

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

これは少し裏技です。

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


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

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

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

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

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

# むちゃくちゃな名前でも作れる! (`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の例(他の言語でも同様)
# (説明のために少し冗長な書き方をしています)
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 を入れ、関数から途中で脱出させます。

こういうこと?(※エラーになります)
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 が評価されるようにします。

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 よりも @ を使う」で紹介した、古い文法で変数を引数に渡す構文を悪用しています。
'' で括ると文の評価時に変数の中身が展開されてから実行されます。(イメージとしてはマクロの実行時版です)

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

'endproc$' は、

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

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

また、breakcontinue の代わりにも使えます。

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

繰り返しですが、構文の悪用なので濫用厳禁です。それでも使いたい方は次回の黒魔術の記事へ

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

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

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

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を返しましょう。

これは意味が無い
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
こうしよう
procedure double: .vec#
	.return# = .vec# * 2
endproc

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

スコープ

ループ変数をループの外から参照しない

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

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

appendInfoLine(.i) ; 11

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

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

ファイル操作

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

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

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

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

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

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

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

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

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

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

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

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

objects.png

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

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

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

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

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

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

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

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

# 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$ を使うと、ファイルエクスプローラーが開き、ユーザーがファイルを選ぶことができます。

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

choosefile.png

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

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

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

# ディレクトリ関連
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 で確認できます。バージョンに応じた場合分けに活用できます。(基本的に後方互換を謳っているので対処不要ですが)

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

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

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

appendInfoLine: 0 / 0 ; undefined

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

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

コード例は、昔作ったwavファイル変換コード を新しい構文に書き直したものです7

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 を使うと、スクリプトを一時停止してダイアログを表示します。

pauseScript: "danger!!!!"

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

pausescript.png

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

pause_interrupt.png

ユニットテスト

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

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

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

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の引数にはエラーメッセージを指定します。

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

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

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

asserterror Empty formula.
x += 1

asserterror.png

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

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

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

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

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

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

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

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

greet.praat
procedure greet: .name$
	appendInfoLine: "Hello, " + .name$ + "!"
endproc
main.praat
clearinfo
include greet.praat

@greet: "Taro"

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

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

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

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

procedure greet: .name$
	appendInfoLine: "Hello, " + .name$ + "!"
endproc
main.praat
clearinfo
# procedureが利用したいだけなのに余計な処理が実行されてしまって使いづらい...
include greet.praat

@greet: "Taro"

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

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

ディレクトリ構成
- main.praat
	- sub
		- sub1.praat
		- sub2.praat
sub/sub1.praat
# 「相対パスで指定すればいいんだな」
include sub2.praat

procedure sub1:
	@sub2:
endproc
sub/sub2.praat
procedure sub2:
	appendInfoLine: "sub2"
endproc
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 がライブラリの読み込み向けだったのに対し、こちらは実行スクリプトの呼び出し向けです。

hello.praat
appendInfoLine: "hello"
main.praat
runScript: "hello.praat"

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

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

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

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

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

appendInfoLine: "Hello, " + name$ + "!"
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 関係なしに使うこともできます。

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

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

appendInfoLine: "Hello, " + name$ + "!"
main.praat
runScript: "hello.praat", "" ; エラー: name is invalid!

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

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

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

ライブラリ

CPrANを活用する

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

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

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

その他

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

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

おわりに

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

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

次回は趣向を変えて、実用性皆無のPraat Script黒魔術を紹介します。お楽しみに!

  1. リーダブルコード」で紹介されていた、GoogleのC++フォーマット規約を参考にしています。

  2. このテクニックはグローバル変数を汚すので賛否両論あると思います...慣れている人が多ければ 1, 0 のままでも問題ないと思います。

  3. 私の便乗記事ではなく本家様のほうです(念のため)

  4. 正式な用語ではありません。

  5. もちろん実際のコードでの goto の利用は非推奨です

  6. 字面の圧倒的矛盾。でも書いてある通りです。

  7. 元コードは今見返すとかなり汚いです...(GitHub初コミットなので許して :innocent:)うかつにリファクタリングしても古いPraatを使っているユーザーが使えなくなってしまうので、塩漬けにしています

2
1
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
2
1