2019年12月8日 星期日

Praat 語音分析筆記 (八) : Praat Script

Praat 最強大的功能是內建了 Praat Script, 可用來撰寫語音分析自動化的腳本程式. 本系列前面的文章參考 :

Praat 語音分析筆記 (一) : 簡介與軟體下載
Praat 語音分析筆記 (二) : 錄音與存檔匯入
Praat 語音分析筆記 (三) : SoundEditor 操作
Praat 語音分析筆記 (四) : 音高分析
Praat 語音分析筆記 (五) : 頻譜分析
Praat 語音分析筆記 (六) : 共振峰分析
Praat 語音分析筆記 (七) : 強度分析

Praat Script 教學文件參考 :

http://www.fon.hum.uva.nl/praat/manual/Scripting.html

本篇所使用的範例語音檔下載 (wav 檔之 zip 壓縮) :

# Praat_wav_files


八. Praat Script 與自動化分析

Praat 雖然提供了方便的圖形化界面讓使用者能快速地進行語音分析, 但由於語音具有多重變異性, 例如性別, 年齡, 與個人差異等, 實驗語音學中的聲學研究通常需透過田野調查採集大量語料進行統計分析, 成千上萬的語音數據若以人工方式分析將耗費大量人力, 且可能引進無法控制之操作或讀取錯誤, 造成統計分析結果失真. 利用 Praat Script 進行自動化分析可解決人力, 時間, 以及誤差等問題.

在學習 Praat Script 自動化技術之前, 先來學習 Praat 繼承自 Kay CSL 語音分析儀的紀錄檔 (log file) 功能, 這是一種半自動擷取語音數據的方法.


8.1 記錄檔 (log file) 功能 

所謂的紀錄檔功能是指, 在 Praat 人工分析語音時, 按下特定之功能鍵 (F12 與 Alt, Ctrl, 或 Shift 組合) 即可擷取預先指定之聲學特徵, 並自動儲存在指定的紀錄檔內, 這種半自動擷取數據的功能適合用在少量語料的人工分析, 或某些難以完全自動化, 需要先經過人工鑑別後再決定抓哪部分數據的情況.紀錄檔功能據說源自古早昂貴的 Kay CSL 語音分析儀.

紀錄檔功能放在 SoundEditor 視窗中, 因此必須先點選 Object 視窗中的聲音物件, 按右邊的 "Edit & View" 按鈕開啟 SoundEditor 視窗後, 再按 Query 選單點選 Log settings 功能項 :





Praat 提供四個紀錄檔 Log file 1~4, 其中 Log 1, 2 為以 format 欄位指定之格式抓取聲學特徵; 而 Log 3, 4 則以執行指定之 Praat Scrript 抓取. Log 1, 2 的參數設定在 Log 1 format 與 Log 2 format 兩欄位中. 預設 Log 1 擷取格式為 :

Time 'time:6' seconds, pitch 'f0:2' Hertz

即擷取游標所在時間 (小數點第 6 位) 與音高 (小數點第 2 位).

預設 Log 2 擷取格式為 :

't1:4''tab$''t2:4''tab$''f1:0''tab$''f2:0''tab$''f3:0'

即擷取游標所在時間 (小數點第 2 位) 與 F1~F3 共陣峰 (整數).

紀錄檔格式中放在單引號裡面的是聲學參數 (f0, f1, intensity 等) 或系統變數 (tab$ 與 editor$), 統稱為變數, 後面可用冒號再接一個整數, 表示取至小數點後第幾位 (對於數值變數而言), 其他沒有用單引號括起來的都是字串 :

'變數:小數點後第幾位'

例如下列擷取格式 :

time='time:5' F1='f1:0' F2='f2:0' F3='f3:0'

會抓出如下結果 :

time=0.12345 F1=523 F2=1024 F3=2781

可用的紀錄檔變數如下表 :


 紀錄檔變數 說明
 time 游標所在時間 (秒), 或選取區段中心點之時間
 freq 游標所在頻率 (Hz)
 t1 選取區段之起始時間
 t2 選取區段之結束時間
 dur 選取區段之時長
 f0 游標位置之音高, 或選取區段內之平均音高
 f1 游標位置之第一共振峰 F1, 或選取區段內 F1 之平均值
 f2 游標位置之第二共振峰 F2, 或選取區段內 F2 之平均值
 f3 游標位置之第三共振峰 F3, 或選取區段內 F3 之平均值
 f4 游標位置之第四共振峰 F4, 或選取區段內 F4 之平均值
 f5 游標位置之第五共振峰 F5, 或選取區段內 F5 之平均值
 b1 游標位置之第一共振峰 F1 之頻寬, 或選取區段內 F1 頻寬之平均值
 b2 游標位置之第二共振峰 F2 之頻寬, 或選取區段內 F2 頻寬之平均值
 b3 游標位置之第三共振峰 F3 之頻寬, 或選取區段內 F3 頻寬之平均值
 b4 游標位置之第四共振峰 F4 之頻寬, 或選取區段內 F4 頻寬之平均值
 b5 游標位置之第五共振峰 F5 之頻寬, 或選取區段內 F5 頻寬之平均值
 intensity 游標位置之強度 (dB), 或選取區段內強度之平均值
 power 游標位置之頻譜功率 (Pa2/Hz)
 tab$ TAB 鍵字元 (間隔用)
 editor$ 編輯器視窗的標題


因為預設的路徑可能不存在, 因此最好將預設的 Log file 紀錄檔位置更改為目前的工作目錄, 例如 D:\praat\ 下 :




擷取數據之方式 Log 1 為直接按下 F12, 其他紀錄檔則是 F12 與 Alt, Ctrl, 以及 Shift 之組合快捷鍵, 如下表所示 :


 紀錄檔 快捷鍵
 Log 1 F12
 Log 2 Shift + F12
 Log 3 (script) Alt + F12
 Log 4 (script) Ctrl + F12


首先在 SoundEditor 上點一下讓游標定在要擷取之時間點, 然後按 F12 鍵 (筆電需同時按 Fn +F12) 擷取 Log 1 數據, 擷取結果會顯示開啟的 Info 視窗中, 例如 :




紀錄檔功能雖然方便好用, 但只適合用來加速人工處理少量語料, 對於大量語料數據擷取, 必須借助 Praat Script 的強大威力來達成自動化, 原本需要數個月的人工處理時間可能只要數天之內即可完成.


8.2 Praat Script 編輯器 :

Praat 內建一個 Script 編輯器, 可直接編輯 Script 程式並執行. 在 Object 視窗按 File 選單點選 "New Praat script" 功能項, 這會開啟一個空白的 Script 編輯器視窗 (點選 "Open Praat script" 則是開啟既有之 Script 檔案) :




輸入如下指令後按 Run 即可執行, 因程式中有 printline 輸出指令, 因此它會開啟一個 Info 視窗顯示輸出資料 : :

a=1
str$="Hello World"
printline 'a'     
printline 'str$'





按編輯器的 File 按鈕點選 "Save as" 可將程式儲存到檔案統中, 也可以在 點選 "Open" 開啟既有之 Script 檔案, 或點選 "New" 開啟新檔, :





8.3 Praat Script  語法

Praat 中的人工操作動作均有相對應之指令, 直接撰寫指令集來代替人工分析, 這些指令集稱為 script, 類似 EXCEL 中的巨集與 VBA. 不過 Praat Script 的語法與一般程式語言如 Python 或 Java 等比起來要簡單好學多了.

所有的程式語言語法基本上都包含下列三要素 :
  • 指定 (assignment) : 賦予變數一個值
  • 判斷 (decision/branch) : 根據變數值決定執行方向
  • 迴圈 (loop) : 重複執行一段程式碼 
不管學哪一種程式語言都需先了解這語言的三種基本語法怎麼寫. 另外還要了解該語言的內建函數與資料結構 (例如陣列), 這樣就差不多學完一個程式語言了.


8.3.1 註解與跳行

Praat Script 使用 # 或 ! 號作為註解開頭, 而且是整行註解, 例如 :

#Author : Tony Y.H.Huang
!E-mail : tony1966@blabla.hinet.net
#Date : March 4,2009
#Praat version 5.1.2
clearinfo

注意, 不要在指令後面用 # 或 ! 號做註解, 例如在 echo 或 print 後面用 # 作註解會被當作輸出字串 :

str$="Hello World"
print 'str$'   #輸出 Hello World

此程式會輸出 "Hello World  #輸出 Hello World", 因此不要在指令後面註解, 要註解就用整列. 註解可用中文, 但在 Praat 中開啟 Script 時中文註解會變亂碼 (不妨礙程式執行).

程式碼如果太長可跳行續寫, 但下一行開頭要用 ... (三個連續小數點) 表示此行程式碼接續上一行, 例如 :

fileappend "'info_out$'" Pitch Mean_Max_Min
... files path='percent_pitch_file_path$''newline$'

此例 第二行之 files path 事實上是接續上一行的指令.


8.3.2 資料型態與變數

Praat Script 資料型態很簡單, 只有數值與字串兩種, 布林值 (比較或邏輯運算子的輸出) 是用整數 0 與 1 代替, 分別代表 false 與 true. 數值包括整數與浮點數, 整數是像 123 或 2019 等沒有小數點的數; 而浮點數則是有小數點的數, 例如 3.14159, 709.23456 等等.

字串資料可以用單引號或雙引號括起來, 例如 "finished" 或 'finished' 均可. 但如果要用內建函數例如 echo 輸出變數, 則必須使用單引號, 不可用雙引號, 例如若變數 x 的值是 2, 則下列程式在 info 視窗輸出 'the value of x is 2' :

echo the value of x is 'x'

單引號字串具有代入功能, Praat Script 在執行時會先檢查單引號內是否有以存在之變數, 若有就會以其值代進去. 雙引號字串則無此功能.

變數用來在程式執行途中暫時儲存資料, 因此需為變數取一個名字, Praat Script 的變數名稱有如下規定 :
  • 必須以小寫英文字母開頭, 後面只能用英數字或底線.
  • 字串變數必須以 $ 結尾, 否則為數值變數.
  • 字串變數有三個保留字不能用 : newline$, tab$, hellDirectory$
因此 Pitch, _formant, $pitch 與 2time 都是不合法的變數名稱 (首字母須為小寫字母). 下列指定敘述也不合法 :

subject_name$=123 
the_date="2019-12-08"   

第一個指令錯誤在於 subject_name$ 以 $ 結尾, 此乃字串變數, 但卻指定一個數值給它; 第二個指令指派了一個字串給 the_date, 應該改成 the_date$ 才對.

Praat Script 沒有特別為布林值


8.3.3 運算子

Praat Script 的運算子如下 :

 算術運算子 說明
 a + b 加法, 5+2 得 7
 a - b 減法, 5-2 得 3
 a * b 乘法, 5*2 得 10
 a / b 除法 (浮點數除法), 5/2 得 2.5
 a ^ b a 的 b 次方, 2^4 得 16
 a div b a 除以 b 取整數 (整數除法), 5 div 2 得 2
 a mod b a 除以 b 取餘數, 5 mod 2 得 1

 比較運算子 說明
 > 大於, 傳回 1 或 0
 >= 大於等於, 傳回 1 或 0
 < 小於, 傳回 1 或 0
 <= 小於等於, 傳回 1 或 0
 = 等於, 傳回 1 或 0
 <> 不等於, 傳回 1 或 0

 邏輯運算子 說明
 and 邏輯且運算, a and b 需 a 與 b 均為 1 才傳回 1
 or 邏輯或運算, a or b 只要 a 或 b 為 1 即傳回 1
 not 邏輯非運算, not a 若 a 為 1 傳回 0, a 為 0 傳回 1  (反相)


+ 與 - 運算子除了作為算術運算子外, 若運算元為字串, 則做為字串串接與刪除運算, a$ - b$ 會從 a$ 字串最右邊刪除 b$ 字串, 例如 :

a$="Hello"
b$="World"
c$=a$ + " " + b$
echo 'c$'

此程式輸出 Hello World

a$="sound002.wav"
b$=a$ - ".wav"
echo 'b$'

此程式輸出 sound002, 亦即將副檔名刪除.

除了比較字串也可以用比較運算子進行比較,

a$=b$ : 需兩個字串雷同才會傳回 1
a$ <> b$ : 只要一個字元不同即傳回 1
a$ < b$ : 逐字元比較 Unicode 大小
a$ > b$ : 逐字元比較 Unicode 大小
a$ <= b$ : 逐字元比較 Unicode 大小
a$ >= b$ : 逐字元比較 Unicode 大小


8.3.4 內建常數

Praat Script 有三個內建數值常數 :
  • e : 自然指數, 其值約為 2.718
  • pi : 圓周率, 其值約為 3.14159
  • undefined : 未賦值的變數值
這三個常數可直接使用.


8.3.5 判斷

判斷也稱為分支 (branch), 是利用條件式是否符合來改變程式執行方向, 其語法為 if ... elsif ... else ... endif 結構 (注意, 是 elsif 不是 elseif), 例如 :

if pitch > 100 state$="over"
elsif pitch=100 state$="just"   
else state$="under"   
endif

此程式首先判斷變數 pitch 值是否大於100, 是則字串變數 state$ 被指定為 "over", 否則再判斷 pitch 是否剛好等於 100, 是則字串變數 state$ 被指定為 "just", 否則被指定為 "under".

注意, 若判斷後面要做的動作指令只有一個, 則可與 if 寫在同一行, 若有兩個以上指令則要跳行縮排寫 (縮排不是必要, 只是增加可讀性), 上面程式也可以這麼寫 :

if pitch > 100
    state$="over"
elsif pitch=100
    state$="just"   
else
    state$="under"   
endif


8.3.6 迴圈

迴圈用來重複執行一段程式碼, Praat Script 有三種迴圈指令 :
  • for 迴圈 (指定次數)
  • while 迴圈 (至少執行 0 次)
  • repeat 迴圈 (至少執行 1 次)
for 迴圈例如 :

for f from 100 to 900      
    Create Sound … tone 0 1 22050  0.5*(2*pi*f*x)      
endfor

此迴圈會從頻率 f=100 開始到 f=900 產生 22K 取樣的弦波聲音.

while 迴圈例如 :

x=10
while x > 0      
    x=x-1      
endwhile

此迴圈會在 x 大於 0 條件下繼續執行, 每次執行會將變數 x 減量 1.

repeat 迴圈例如 :

repeat      
    x=randomInteger(1,6) + randomInteger(1,6)      
until x=12

此例至少會執行一次, 每次會呼叫 Praat 內建函數 randomInteger() 來產生介於 1~6 之間的隨機整數, 若兩個隨機數和等於 12 就跳出結束迴圈, 否則繼續執行.

Praat Script 不支援陣列這種資料結構, 但可以用迴圈來模擬, 例如 :

for i from 1 to 3    
    square'i'=i*i    
endfor   
sum='square1' + 'square2' + 'square3'
echo 'sum'

此例利用單引號的代入功能來模擬陣列元素 square1, square2, 與 square3. 跳出迴圈後得到三個變數 square1=1, square2=4,  square3=9, 因此 sum=1+4+9=14.

此外, Praat 提供了許多好用的內建指令, 例如輸出, 表單, 以及檔案處理等等. 這些指令有的有參數, 參數就直接跟在指令後面.


8.3.7 輸出指令

Praat Script 內建輸出指令如下表 :


 輸出指令 說明
 clearinfo 清除 Info 視窗內容
 echo 先清除 Info 視窗內容後輸出資料 (取代)
 print 將資料輸出於 Info 視窗原內容後面 (append), 不跳行
 printtab 輸出一個 tab 字元到 Info 視窗原內容後面
 printline 輸出資料並跳行 (無輸出資料時則輸出空行並跳行)


輸出指令都會開啟一個 Info 視窗, 然後對此視窗輸出資料, 注意, 輸出變數要放在單引號中, 否則會被當作字串輸出, 例如 :

a=1
str$="Hello World"
printline a         
printline 'a'       
printline str$   
printline 'str$'   

printline a 會輸出 a  (把 a 當成字串); 而 printline 'a' 則輸出 1 (代入變數 a 之值 1); printline str$ 會輸出 str$ (把 str$ 當成字串); 而 printline 'str$'  才會輸出 Hello World.

可呼叫 clearinfo 清除 Info 視窗原有內容 (後面不需帶任何參數), 例如 :

clearinfo

echo 與 print 的差別是 echo 每次都會自動先清空 Info 視窗再輸出 (即 replace 的動作), 因此前一次輸出的資料會消失, 但 print 不會這麼做, 它會把資料接在上次輸出的資料後面 (即 append 的動作). 例如 :

result$="blablabla"

echo the result is 'result$'    #輸出 the result is blablabla

此指令會清空 Info 視窗並輸出 'the result is blablabla', 如果改用 print :

result$="blablabla"
print the result is ‘result$’ 

此指令會將 "the result is blablabla" 貼到 Info 視窗的原內容後面. 

printline 與 print 的差別在於 printline 輸出後會自動跳行, 但 print 不會. printline 後但若無輸出資料則輸出一個空行. printtab 會輸出一個 tab 字元, 這在控制輸出格式時很好用, 例如 : 

result$="blablabla"
printtab   
printline 
printline the result is 'result$' 

此程式會先插入一個 tab 字元, 然後跳行, 輸出 "the result is blablabla"


8.3.8 檔案處理指令

檔案處理指令如下表 :


 檔案處理指令 說明
 fileReadable("test.txt") 檢查檔案 test.txt 是否存在, 是傳回 1, 否傳回 0
 data$ < test,txt 讀取檔案 test.txt 內容到變數 data$
 data$ > test.txt 將資料 data$ 寫入檔案 test.txt (覆蓋)
 data$ >> test.txt 將資料 data$ 寫入檔案 test.txt 尾端 (append)
 fileappend test.txt data$ 將資料 data$ 寫入檔案 test.txt 尾端 (append)
 filedelete test.txt 刪除檔案 test.txt


下列程式利用 Strings 物件來儲存檔案名稱, 再以一個迴圈將檔案讀進 Object 視窗內.

directory$ = "d:\bin\pilot"
Create Strings as file list... list 'directory$'\*.wav
numberOfFiles = Get number of strings    
for ifile to numberOfFiles    
    select Strings list    
    fileName$ = Get string... ifile    
    Read from file... 'directory$'\'fileName$'    
endfor


8.3.9 Script 相關指令

Praat Script 中可用 include 載入其他程式到目前的 Script 中, 例如 :

include "c:\praat\script\fft.praat"       
include "..\script\fft.praat"   
include "script\fft.praat"

第一個 include 載入絕對目錄所指定之程式檔案; 其餘二個 include 使用相對路徑, 第二個 include 用 ".." 表示上一層目錄; 第三個 include 則是載入目前目錄下的 script 子目錄下的檔案. 注意, include 只能用路徑字串, 不可以使用字串變數, 例如下面指令是錯誤的 :

include 'file_path$'


8.4 輸入表單

Praat Script 執行時有時需要操作者輸入一些參數, 例如在音高分析時需要知道語者性別, 以便設定頻率的上下限. Praat 提供 form 表單來製作圖形化輸入表單, 其語法為 :

form  表單標題
integer 整數欄位名稱 預設值
real 浮點數欄位名稱 預設值
word 字串欄位名稱 預設值
endform

放在 form 與 endform 之間的就是表單中要輸入的欄位定義, 若輸入值為浮點數要宣告為 real; 若為整數宣告為 integer; 若為字串則宣告為 word. 欄位名稱格式為 a_b_note (用底線連接), 其中 a_b 就是程式中存取此輸入欄位的變數名稱. 表單變數資料型態如下表 :


 表單變數資料型態 說明
 word 字串
 real 浮點數
 integer 整數
 positive 正數


例如 :

form Specify the WAV/TXT directories
word Working_directory d:\project\pitch
integer minimum_pitch_(Hz) 75
integer maximum_pitch_(Hz) 600
real time_step_(second:0=auto) 0
endform

此表單定義了四個輸入欄位, 其中變數 minimum_pitch 與 maximum_pitch 為整數, 用來輸入音高上下限頻率; Working_directory 為字串, 用來輸入工作目錄; 而 time_step 為浮點數, 用來輸入時間步階值. 將上面程式碼貼到 Script 編輯器再按 Run 即可看到如下輸入視窗 :




注意, 欄位名稱中的底線在表單視窗中都會被改成空格.


8.5 取得人工操作的指令碼紀錄

Praat Script 提供了紀錄操作指令碼的功能 (類似 EXCEL 的錄製巨集), 如果想要將一連串的人工操作自動化, 不需要到使用手冊查詢相關指令 (Praat 的線上手冊雖然完整豐富, 但並非教學手冊), 直接在 Script 編輯器的 Edit 選單中點選 "Paste history" 貼上操作歷史即可.

以擷取音高為例, 首先於 Obect 視窗按 Open 點選載入聲音檔, 然後開啟 SoundEditor 視窗, 再按 Pitch 選單擷取游標所在時間點之音高值, 做完以上操作後, 回 Object 視窗按 File 選單點選 New Praat script 開啟 Script 編輯器, 然後按 Edit 選單中點選 "Paste history" :




結果如下 :

Read from file: "D:\Praat\this.wav"
View & Edit
Get pitch

這樣不須查手冊即可取得對應於操作之指令了. 不過在紀錄操作之前, 最好先用 Clear history 清除之前的操作指令紀錄.

Bingo! 忙了一周終於把 Praat 使用手冊重寫完成, 收工啦!

沒有留言 :