2020/3/16

rust05 Structs

struct 是自訂資料類別,可包裝多個相關的 values。如果是物件導向程式語言, struct 就像是物件中的 attributes,以下會討論 tuple 跟 struct 的差異,說明 struct 的使用方法,並討論如何定義 method 與相關函數,來限制 structs 資料的行為。Structs 與 Enum 都是建立新類別的方法,並充分運用了 Rust 的編譯時期資料型別檢查的功能。


Defining and Instantiating Structs


struct 跟 tuple 類似,每一個部分可以是不同的資料型別,但不同於 tuple,struct 需要為每一個部分的資料命名,表達其值的意義。因為有了名字,struct 比 tuple 靈活,不需要靠順序來存取裡面的值。


struct 的每一個部分的資料稱為 field


struct User {
    username: String,
    email: String,
    sign_in_count: u64,
    active: bool,
}

定義 struct 後,透過 key:value 名稱來存取裡面的值,例如下面是修改 user1 的 email


let mut user1 = User {
    email: String::from("someone@example.com"),
    username: String::from("someusername123"),
    active: true,
    sign_in_count: 1,
};

user1.email = String::from("anotheremail@example.com");

Using the Filed Init Shorthand when Variables and Fields Have the same value


這是產生 user 的 function


fn build_user(email: String, username: String) -> User {
    User {
        email: email,
        username: username,
        active: true,
        sign_in_count: 1,
    }
}

當 field 的名稱與 value 都相同時,email 以及 username 都是一樣的,可使用 field init shorthand 改寫這個 function


fn build_user(email: String, username: String) -> User {
    User {
        email,
        username,
        active: true,
        sign_in_count: 1,
    }
}

Creating Instances From Other Instances With Struct Update Syntax


透過 struct update syntax 這種語法更新 struct 裡面的 field


這是用 user1 的兩個欄位,產生新的 user2


let user2 = User {
    email: String::from("another@example.com"),
    username: String::from("anotherusername567"),
    active: user1.active,
    sign_in_count: user1.sign_in_count,
};

可改用這種語法


let user2 = User {
    email: String::from("another@example.com"),
    username: String::from("anotherusername567"),
    ..user1
};

Using Tuple Structs without Named Fields to Create Different Types


tuple 跟 struct 類似,但沒有欄位名稱。如果想要為 tuple 命名,可使用 tuple structs


struct Color(i32, i32, i32);
struct Point(i32, i32, i32);

let black = Color(0, 0, 0);
let origin = Point(0, 0, 0);

雖然都是一樣的結構,但 black 跟 origin 是不同的類別


Unit-Like Structs Without Any Fields


unit-line struct 類似於 (),就是 unit 類別,裡面沒有任何 fields。可用在想讓某個類別實現 trait,但不需要再類別中儲存資料的狀況。


Ownership of Struct Data


如果在 Struct 中使用了 String,而不是 &str 的 slice,這就是要讓這個 struct 自己擁有這個欄位的資料,只要 struct 有效,資料就有效。


也可以讓 struct 儲存被其他類別擁有的資料的 reference,但就需要用到 lifetimes 的概念。


lifetime 可確保 struct 引用的資料有效性跟 struct 本身保持一致。


struct User {
    username: &str,
    email: &str,
    sign_in_count: u64,
    active: bool,
}

fn main() {
    let user1 = User {
        email: "someone@example.com",
        username: "someusername123",
        active: true,
        sign_in_count: 1,
    };
}

編譯錯誤


error[E0106]: missing lifetime specifier
 --> src/main.rs:2:15
  |
2 |     username: &str,
  |               ^ expected lifetime parameter

error[E0106]: missing lifetime specifier
 --> src/main.rs:3:12
  |
3 |     email: &str,
  |            ^ expected lifetime parameter

chap 10 會討論 lifetime,解決這邊的錯誤。


一個使用 struct 的範例


撰寫一個計算長方形面積的程式。


先用基本的變數方式撰寫


fn main() {
    let width1 = 30;
    let height1 = 50;

    println!(
        "The area of the rectangle is {} square pixels.",
        area(width1, height1)
    );
}

fn area(width: u32, height: u32) -> u32 {
    width * height
}

fn area 有兩個參數,但沒有表現出相關的特性,可以改用 tuple 或 struct


這是 tuple 的版本,但沒有明確說明哪一個參數是長,哪一個是寬


fn main() {
    let rect1 = (30, 50);

    println!(
        "The area of the rectangle is {} square pixels.",
        area(rect1)
    );
}

fn area(dimensions: (u32, u32)) -> u32 {
    dimensions.0 * dimensions.1
}

用 struct 改寫


struct Rectangle {
    width: u32,
    height: u32,
}

fn main() {
    let rect1 = Rectangle { width: 30, height: 50 };

    println!(
        "The area of the rectangle is {} square pixels.",
        area(&rect1)
    );
}

fn area(rectangle: &Rectangle) -> u32 {
    rectangle.width * rectangle.height
}

利用 derived traits 增加功能


以下程式,在列印 Rectangle 時會發生編譯 error


struct Rectangle {
    width: u32,
    height: u32,
}

fn main() {
    let rect1 = Rectangle { width: 30, height: 50 };

    println!("rect1 is {}", rect1);
}

編譯錯誤


error[E0277]: `Rectangle` doesn't implement `std::fmt::Display`
 --> src/main.rs:9:29
  |
9 |     println!("rect1 is {}", rect1);
  |                             ^^^^^ `Rectangle` cannot be formatted with the default formatter
  |
  = help: the trait `std::fmt::Display` is not implemented for `Rectangle`
  = note: in format strings you may be able to use `{:?}` (or {:#?} for pretty-print) instead
  = note: required by `std::fmt::Display::fmt`

println! 巨集裡面的 {},預設是使用 Display 格式,對於 struct 並沒有 Display。錯誤訊息說明,要加上 #[derive(Debug)],並改用 {:?}


#[derive(Debug)]
struct Rectangle {
    width: u32,
    height: u32,
}

fn main() {
    let rect1 = Rectangle { width: 30, height: 50 };

    println!("rect1 is {:?}", rect1);
}

#[derive(Debug)]是利用註解來 derive Debug trait,而 {:?} 是列印的格式,結果為


rect1 is Rectangle { width: 30, height: 50 }    

如果改用 {:#?} ,輸出資料就會變成


rect1 is Rectangle {
    width: 30,
    height: 50
}

rust 提供了很多透過 dervie 註解使用的 trait,可謂自訂類別增加一些常用的功能。


Method Syntax


method 跟函數一樣,都適用 fn 宣告,可以有參數及回傳值,但 method 跟 function 不同,在 struct (或是 enum, trait) 的 context 中被定義,第一個參數永遠是 self,就是呼叫該 method 的 struct instance。


Defining Methods


#[derive(Debug)]
struct Rectangle {
    width: u32,
    height: u32,
}

impl Rectangle {
    // self 是 Rectangle,必須加上 &
    // method 可以取得 self 的 ownership,也可以用 &self 不可變借用,或是 &mut self 可變借用
    // 如果想要在 method 裡面改變資料,就要改用 &mut self
    // 很少會直接寫成 self
    fn area(&self) -> u32 {
        self.width * self.height
    }
}

fn main() {
    let rect1 = Rectangle { width: 30, height: 50 };

    println!(
        "The area of the rectangle is {} square pixels.",
        rect1.area()
    );
}

where's the -> operator ?


C/C++ 有兩種方式呼叫 method, . 直接在 object 上呼叫 method 或是 -> 在 object 的 pointer 呼叫 method。這邊要先了解 dereference pointer。假設 object 是 pointer,那麼 object->something() 就跟 (*object).somethong() 一樣。


當 object.something() 呼叫 method 時,rust 會自動為 object 加上 &, &mut 或 * ,以便讓 object 跟 method signature 匹配。


這兩種寫法是一樣的


p1.distance(&p2);
(&p1).distance(&p2);

自動引用是因為 method 有明確的接收者 self,在提供了接收者與 method 名稱條件下,rust 可計算出要使用 &self, &mut self 或是 self


Methods with More Parameters


struct Rectangle {
    width: u32,
    height: u32,
}

impl Rectangle {
    fn area(&self) -> u32 {
        self.width * self.height
    }

    // 取得另一個矩形的 immutable reference,只讀取資料,不需要寫入
    fn can_hold(&self, other: &Rectangle) -> bool {
        self.width > other.width && self.height > other.height
    }
}

fn main() {
    let rect1 = Rectangle { width: 30, height: 50 };
    let rect2 = Rectangle { width: 10, height: 40 };
    let rect3 = Rectangle { width: 60, height: 45 };

    println!("Can rect1 hold rect2? {}", rect1.can_hold(&rect2));
    println!("Can rect1 hold rect3? {}", rect1.can_hold(&rect3));
}

Associated Functions


impl 區塊的另一個功能:允許在 impl 中定義,不使用 self 為參數的函數,這種函數稱為 assoicated function。因他們與 struct 結構相關,是函數而不是 method,因為他們不作用於一個 struct 的 instance。


產生 struct 新 instance 的 factory function,常常是用 associated function 實作。


接受一個參數,產生正方形 Rectangle


impl Rectangle {
    fn square(size: u32) -> Rectangle {
        Rectangle { width: size, height: size }
    }
}

用 :: 呼叫 associated function,例如 let sq = Rectangle::square(3);


Multiple impl blocks


每個 struct 都允許有多個 impl blocks


impl Rectangle {
    fn area(&self) -> u32 {
        self.width * self.height
    }
}

impl Rectangle {
    fn can_hold(&self, other: &Rectangle) -> bool {
        self.width > other.width && self.height > other.height
    }
}

References


The Rust Programming Language


中文版


中文版 2

2020/3/9

rust04 Ownership

ownership 是 rust 中特別的功能,因為 rust 沒有 garbage collection 的功能,因此要注意 ownership 的運作方式:borrowing, slices, 並瞭解 rust 如何在記憶體中存放資料。


What is Ownership?


所有的程式都必須要管理如何使用機器的記憶體。第一種使用 GC 機制,裡面內建一個自動處理的排程,不斷地自動回收不被使用的記憶體,第二種是交給 Programmer 自己配置與釋放記憶體。Rust 選擇第三種,利用 a system of ownership 所有權系統管理記憶體。編譯器在編譯時,會進行規則的檢查,而在執行時,所有權系統完全不會拖慢程式的速度(swift 也是使用這個方式,實作了 Automatic Reference Counting ARC)。


Stack and Heap


在很多程式語言中,不需要考慮資料是存在 Heap 或是 Stack,但 Rust 要注意 Stack 或是 Heap 會影響到程式語言的行為。


Stack 是 Last In First Out 的順序,有 Push 資料,Pop 資料的動作。


Stack 的 IO 很快,資料存取的位置一直在 Stack 的頂端,不需要尋找存放或讀取資料的位置。另外 Stack 裡面的資料都必須要佔用已知且固定的大小。


在編譯程式時,大小未知或是可能會改變的資料,要儲存在 Heap,Heap 是混亂的,當儲存資料前,必須要要求一塊固定大小的空間。OS 在 heap 找到一塊夠大的空間,標記為已使用,並回傳 pointer。這個動作稱為 allocating on the heap。因為 pointer 大小已知且固定,可以將 pointer 存放在 stack,但要取得實際資料的內容時,還是要透過 pointer 取得資料。


因 heap 要透過 pointer 存取,會比 stack 慢。OS 在處理比較近(stack)的兩份資料時,會比處理比較遠的 heap 較快。


在程式呼叫一個函數時,傳給函數的值(可能是指到 heap 的 pointer)以及函數的 local 變數,都會被 push 到 stack,當函數結束就會被移出 stack。


Ownership 系統要處理的事:追蹤那個 code 正在使用 heap 的哪些資料,最大限度減少 heap 上重複的資料數量,清理不在被使用的資料,確保不會耗用記憶體空間。因此 ownership 系統就是用來管理 heap 的資料。


Ownership Rules


  1. Each value in Rust has a variable that’s called its owner. 每一個值的背後都有一個稱為 owner 的變數
  2. There can only be one owner at a time. 每一個值都只有一個 owner
  3. When the owner goes out of scope, the value will be dropped. 當 owner(也就是變數) 離開作用域 scope 後,這個值就會被丟棄。

Variable Scope


{                      // s 在這裡無效, 它尚未宣告
    let s = "hello";   // 從此處起,s 是有效的

    // 可以使用 s
}                      // 此作用域已結束,s 不再有效

String 類別


一般的字串 literal 會被直接 hard coded 到程式裡面,雖然方便,但因為是不可變的,不適合在某些狀況使用,有時一開始會不知道字串的值,例如從 stdin 取得輸入的字串。 Rust 有第二種字串類別 String,這種類別會被配置到 heap,可儲存在編譯時期未知的字串。


let s = String::from("hello");

s.push_str(", world!"); // push_str() 增加後面的字串

println!("{}", s); // 列印

String 可變但是 "string literal" 卻不可改變的原因,在於兩種類別處理記憶體的方式不同。


Memory and Allocation


string literal 在編譯時就已經知道內容,故內容會直接編碼到最後的執行檔中,這種方式可快速存取。String 用來處理可變長度的字串,因為可變,就需要在 heap 配置一塊編譯時未知大小的記憶體來存放內容。


  • 要在執行時,向 OS 取得 memory
  • 當使用完 String 後,要有將記憶體退還給 OS 的方法

第一步是當我們呼叫 String::from 時,就會取得 memory。但第二步,在有 GC 的語言中,GC 會自動處理。如果沒有 GC,就要在程式中,自己釋放記憶體,就是 allocate 後的 free,過早或是忘記 free 都會造成 bug。


Rust 是第三種機制,當變數離開 scope 後,就會被自動釋放。


{
    let s = String::from("hello"); // 從此處起,s 是有效的

    // 使用 s
}                                  // 此作用域已結束,
                                   // s 不再有效

當 s 離開 scope,Rust 會呼叫一個特殊的函數 drop,也就是在 } 之前,呼叫 drop。


C++ 這種 item 在生命週期結束時,釋放資源的模式稱為 Resource Acquisition Is Initialization (RAII)


當有多個變數同時在 heap 使用 memory 時,就會有一些特殊的狀況


變數跟資料的互動方式 (一):move


let x = 5;
let y = x;

x, y 都是 5,所以有兩個 5 放入 stack 中


let s1 = String::from("hello");
let s2 = s1;

雖然程式碼跟上面類似,但實際上並不是複製一份。


首先 s1 的 pointer 會指向 heap 的記憶體位置,另外記錄長度及容量。



當 s1 賦值給 s2,並沒有複製 heap 的資料,而是複製了 s1 的 pointer, len, capacity 到 s2



如果用類似 string literal 的做法,複製了 heap 的資料,會造成記憶體的消耗,效能低落。


如果 s1, s2 離開 scope,這時候要釋放記憶體,會發生 s1, s2 做了兩次釋放 double free 的動作,兩次釋放會造成記憶體損壞,也就是安全漏洞。


為確保記憶體安全,在以下這種狀況,當 s1 指派給 s2 後,就不能再使用 s1 了,因為 rust 認為 s1 已經無效。以下這樣的程式碼,無法被執行。


let s1 = String::from("hello");
let s2 = s1;

println!("{}, world!", s1);

warning: unused variable: `s2`
 --> src/main.rs:3:9
  |
3 |     let s2 = s1;
  |         ^^ help: consider prefixing with an underscore: `_s2`
  |
  = note: #[warn(unused_variables)] on by default

error[E0382]: borrow of moved value: `s1`
 --> src/main.rs:5:28
  |
2 |     let s1 = String::from("hello");
  |         -- move occurs because `s1` has type `std::string::String`, which does not implement the `Copy` trait
3 |     let s2 = s1;
  |              -- value moved here
4 |
5 |     println!("{}, world!", s1);
  |                            ^^ value borrowed here after move

這種做法相對於 deep copy,比較像是 shallow copy,但因為 rust 讓第一個變數無效了,也就是用了 move 的動作,這不同於 shallow copy。因為只有 s2 有效,因此離開 scope,就只需要處理 s2 的釋放。



變數跟資料的互動方式 (二):clone


如果確實需要 deep copy 某個 String 在 heap 上的資料,可以使用 clone


let s1 = String::from("hello");
let s2 = s1.clone();

println!("s1 = {}, s2 = {}", s1, s2);

記憶體的狀況會是這樣



Stack-Only Data: Copy


這邊沒有呼叫 clone,但 x 還是可以使用。原因是整數這種已知大小的資料,是儲存在 stack,而 stack 的資料是直接 copy。


let x = 5;
let y = x;

println!("x = {}, y = {}", x, y);

rust 有一種稱為 Copy trait 的特殊註解,可用在這邊。如果某一個類別有 Copy trait,舊的變數賦值給其他變數後,還可以持續使用。


rust 不允許實作了 Drop trait 的類別使用 Copy trait。如果對其值離開 scope 需要做特殊處理的類別,使用 Copy 註解,就會發生編譯錯誤。


可查看該類別的文件,檢查是不是有 Copy trait。但有個規則,任何簡單的 scalar 的組合,都是可以 Copy,不需要配置記憶體或是某個資源的類別,是可以 Copy。


  • 所有整數類別 ex: u32
  • bool 也就是 true, false
  • 所有浮點數類別 ex: f64
  • 字元 chat
  • tuple,且裡面的元素都是可以 Copy 的時候 ex: (i32, i32)

Ownership and Functions


將值傳給函數,在語義上跟給變數賦值類似。向函數傳遞值,可能會被移動 move 或複製 copy


fn main() {
    let s = String::from("hello");  // s 進入作用域

    takes_ownership(s);             // s 的值移動到函數裡 ...
    // ... 所以到這裡 s 不再有效

    let x = 5;                      // x 進入作用域

    makes_copy(x);                  // x 應該移動函數裡,
    // 但 i32 是 Copy 的,所以在後面可繼續使用 x

} // 這裡, x 先移出了作用域,然後是 s。但因為 s 的值已被 move,所以不會有特殊操作

fn takes_ownership(some_string: String) { // some_string 進入作用域
    println!("{}", some_string);
} // 這裡,some_string 離開作用域並呼叫 `drop` 方法。佔用的內存被釋放

fn makes_copy(some_integer: i32) { // some_integer 進入作用域
    println!("{}", some_integer);
} // 這裡,some_integer 離開作用域。不會有特殊操作

Return Values and Scope


return value 也可以轉移 ownership


fn main() {
    let s1 = gives_ownership();         // gives_ownership 將回傳值
    // 移給 s1

    let s2 = String::from("hello");     // s2 進入作用域

    let s3 = takes_and_gives_back(s2);  // s2 被移動到
    // takes_and_gives_back 中, 
    // 它也將回傳值移給 s3
} // 這裡, s3 離開作用域並被丟棄。s2 也離開作用域,但已被 move,所以什麼也不會發生。s1 移出作用域並被丟棄

fn gives_ownership() -> String {             // gives_ownership 將返回值移動給
    // 調用它的函數

    let some_string = String::from("hello"); // some_string 進入作用域.

    some_string                              // 回傳 some_string 並移出給調用的函數
}

// takes_and_gives_back 將傳入字符串並回傳該值
fn takes_and_gives_back(a_string: String) -> String { // a_string 進入作用域

    a_string  // 回傳 a_string 並移出給調用的函數
}

變數的 ownership 遵循相同的模式:賦值給另一個變數,就會 move。當持有 heap 資料的變數離開 scope,就會被 drop,除非該資料被移動給另一個變數,轉移了 ownership


如果想要將某個變數傳給一個函數,但不讓該函數獲取 ownership,簡單的做法就是將該變數在函數回傳的時候,利用 tuple 一併傳回來。


fn main() {
    let s1 = String::from("hello");

    let (s2, len) = calculate_length(s1);

    println!("The length of '{}' is {}.", s2, len);
}

fn calculate_length(s: String) -> (String, usize) {
    let length = s.len();

    (s, length)
}

不過這種做法有點麻煩,rust 提供另一個機制:引用 references


References 引用 and Borrowing 借用


傳給函數的是 &s1,函數定義中是 &String, & 就是引用,可允許使用值,但不獲取其 ownership


fn main() {
    let s1 = String::from("hello");

    let len = calculate_length(&s1);

    println!("The length of '{}' is {}.", s1, len);
}

fn calculate_length(s: &String) -> usize {  // s 是對 String 的引用
    s.len()
} // 這裡,s 離開了作用域。但因為它並不擁有引用值的所有權,所以不會發生什麼問題


這裡將獲取 Reference 作為函數參數的動作稱為 Borrowing 借用。借用得來的值,無法被修改,會發生編譯錯誤。


fn main() {
    let s = String::from("hello");

    change(&s);
}

fn change(some_string: &String) {
    some_string.push_str(", world");
}

error[E0596]: cannot borrow `*some_string` as mutable, as it is behind a `&` reference
 --> src/main.rs:8:5
  |
7 | fn change(some_string: &String) {
  |                        ------- help: consider changing this to be a mutable reference: `&mut std::string::String`
8 |     some_string.push_str(", world");
  |     ^^^^^^^^^^^ `some_string` is a `&` reference, so the data it refers to cannot be borrowed as mutable

Note: 就 C 語言來說 &s 代表該記憶體的位址的值,呼叫函數時,只是將記憶體的位址,傳入函數,函數內再透過 pointer 取得字串的 pointer, len , capacity


Mutable References


上一個程式的問題,可以用 mutable reference 解決


&s 改為&mut s


&String 改為 &mut String


fn main() {
    let mut s = String::from("hello");

    change(&mut s);
}

fn change(some_string: &mut String) {
    some_string.push_str(", world");
}

但 mutale reference 有個限制:特定 scope 中的特定資料,有且只能有一個 mutable reference。


這是錯誤的程式碼


let mut s = String::from("hello");

let r1 = &mut s;
let r2 = &mut s;

println!("{}, {}", r1, r2);

編譯錯誤


error[E0499]: cannot borrow `s` as mutable more than once at a time
 --> src/main.rs:5:14
  |
4 |     let r1 = &mut s;
  |              ------ first mutable borrow occurs here
5 |     let r2 = &mut s;
  |              ^^^^^^ second mutable borrow occurs here
6 |
7 |     println!("{}, {}", r1, r2);
  |                        -- first borrow later used here

只要離開 scope,就可以建立一個新的 mutable reference


let mut s = String::from("hello");

{
    let r1 = &mut s;

} // r1 在這裡離開了作用域,所以我們完全可以創建一個新的引用

let r2 = &mut s;

這種限制,可在編譯其避免 data race,data race 由以下行為發生


  • 兩個或更多 pointer 同時存取同一個資料
  • 至少有一個 pointer 用來寫入資料
  • 沒有同步存取資料的機制

data race 會造成問題,且在執行期很難追蹤跟修復,rust 直接禁止這種行為發生。


在不可變跟可變引用混用時,也會發生同樣的問題


fn main() {
    let mut s = String::from("hello");

    let r1 = &s; // no problem
    let r2 = &s; // no problem
    let r3 = &mut s; // BIG PROBLEM

    println!("{}, {}, and {}", r1, r2, r3);
}

Dangling Reference


在有支援 pointer 的程式語言中,會因為釋放 memory 後,由原本保留的 pointer 取得錯誤的資料,也就是 dangling pointer,rust compiler 可確保 reference 永遠不會發生 dangling reference 的狀況。


fn main() {
    let reference_to_nothing = dangle();
}

// 回傳一個字串的 reference
fn dangle() -> &String {
    let s = String::from("hello");

    &s
} // s 在離開 scope 時,會被丟棄,記憶體被釋放,但回傳了 s 的 reference,會造成存取的問題

編譯錯誤


error[E0106]: missing lifetime specifier
 --> src/main.rs:6:16
  |
6 | fn dangle() -> &String {
  |                ^ help: consider giving it a 'static lifetime: `&'static`
  |
  = help: this function's return type contains a borrowed value, but there is no value for it to be borrowed from

解決方式是修改成直接回傳 String


fn no_dangle() -> String {
    let s = String::from("hello");

    s
} // s 的 ownership 會被 move 出去

The Rules of References


  • 在任意時間點,都只能有一個 mutable reference,要不然就是有多個 immutable refrences
  • references 必須永遠 valid

The Slice Type


另一種沒有 ownership 的資料類別是 slice。slice 可讓我們引用一個 collection 中的連續元素,而不是直接使用整個 collection


sample: 寫一個函數,參數為 String,回傳該 String 找到的第一個 word。如果該 String 中沒有空白,整個字串就是一個 word,也就是要回傳整個 String。


首先嘗試回傳 word 結尾的索引


fn first_word(s: &String) -> usize {
    let bytes = s.as_bytes();

    // 將 s 轉換為 byte array,然後用 iter 逐個 char 檢查是不是空白
    // 當遇到第一個空白,就回傳 index i
    for (i, &item) in bytes.iter().enumerate() {
        if item == b' ' {
            return i;
        }
    }
    // 整個 string 都沒有空白,就回傳字串長度
    s.len()
}

如果 String 在呼叫 first_word 後,又呼叫了 clear(),該字串會被清空,但 index 卻沒有同時改變。另外這個 function 沒有辦法處理一個開頭就有空白字元的字串。


String Slices


String slice 是 String 中一部分值的引用 [start..end],是由 start 開始,直到 end,但不包含 end 的 range。


如果是 [start..=end],就有包含 end


let s = String::from("hello world");

let hello = &s[0..5];
let world = &s[6..11];

let hello = &s[0..=4];
let world = &s[6..=10];

這是 let world = &s[6..11]; let world = &s[6..=10];的狀況



如果開始是 0 可以省略不寫,如果最後面沒寫,就代表是字串結尾


let s = String::from("hello");

let slice = &s[0..2];
let slice = &s[..2];

let len = s.len();

let slice = &s[3..len];
let slice = &s[3..];

// 以下這兩個是一樣的
let slice = &s[0..len];
let slice = &s[..];

slice range 的索引必須要在有效的 UTF-8 字元邊界內,如果要從 multi-byte 文字中間,建立 slice,會發生錯誤。


剛剛的 first_word 就要改用 slice 回傳,對 slice 字串,呼叫 clear,會發生編譯錯誤。原因是 borrowing 的規則,如果有某個值的可變引用時,就不能再取得另一個可變引用。


fn main() {
    let s = String::from("this is a test");
    let s2 = first_word(&s);

        // compile error!
    //s2.clear();

    println!("s2={}", s2);
}

fn first_word(s: &String) -> &str {
    let bytes = s.as_bytes();

    for (i, &item) in bytes.iter().enumerate() {
        if item == b' ' {
            return &s[0..i];
        }
    }

    &s[..]
}

String Literals Are Slices


let s = "Hello, world!";

這是 string literal,s 實際上的類別是 &str ,是指向程式特定位置的 slice。因此 string literal 是不可變的, &str 是不可變引用。


String Slices as Parameters


剛剛的 first_word


fn first_word(s: &String) -> &str {     

實際上如果改為 &str,就可以對 String 及 &str 使用相同的函數,讓函數更通用


fn first_word(s: &str) -> &str {

fn main() {
    let my_string = String::from("hello world");

    // first_word 中傳入 `String` 的 slice
    let word1 = first_word(&my_string[..]);

    let my_string_literal = "hello world";

    // first_word 中傳入字符串字面值的 slice
    let word2 = first_word(&my_string_literal[..]);

    // 因為 string literal 就等於 string slice
    // 不使用 slice 語法這樣寫也可以
    let word3 = first_word(my_string_literal);
}

fn first_word(s: &str) -> &str {
    let bytes = s.as_bytes();

    for (i, &item) in bytes.iter().enumerate() {
        if item == b' ' {
            return &s[0..i];
        }
    }

    &s[..]
}

其他類別的 slice


string slice 是針對 string,而 array 也有提供 slice


let a = [1, 2, 3, 4, 5];

let slice = &a[1..3];

References


The Rust Programming Language


中文版


中文版 2

2020/3/2

Rust03 Common Programming Concepts

這一章介紹所有程式語言都有的概念,在 Rust 中是如何使用的,包含了 變數、資料型別、Function、Comments、Control Flow。


Variable and Mutability


rust 的變數預設是 immutable,這也是 rust 安全性的基礎。


fn main() {
    let x = 5;
    println!("The value of x is: {}", x);
    // 編譯錯誤: cannot assign twice to immutable variable
    x = 6;
    println!("The value of x is: {}", x);

    // 宣告時加上 mut,就可以重新綁定 x
    let mut x = 5;
    println!("The value of x is: {}", x);
    x = 6;
    println!("The value of x is: {}", x);
}

Constants 常數類似 immutable variable,不能對常數使用 mut,宣告時要用 const 而不是 let


const MAX_POINTS: u32 = 100_000;

Shadowing


可以重新定義相同名稱的變數,前一個變數就會被 shadowing


fn main() {
    let x = 5;

    let x = x + 1;

    let x = x * 2;

    println!("The value of x is: {}", x);
}

使用 shadowing 跟 mut 是有差別的,let 會產生一個新的變數,可以改變該變數的資料型別,但使用相同的變數名稱。但 mut 不能改變該變數的資料型別。


Data Types


rust 中每一個值都有一個 data type,rust 才知道要如何使用該 data。


Rust 是 static type language,編譯時就要確定知道每一個變數的資料型別。編譯器可以推測變數的資料型別,但如果有多種可能時,必須自己指定資料型別。例如 parse,轉換後的結果要是數字的話,就要加上 :u32


let guess: u32 = "42".parse().expect("Not a number!");

scalar


rust 有四種 scalar: integers, floating-point numbers, Booleans, and characters


  • integers: 整數,有號跟無號兩種


    Length Signed Unsigned
    8-bit i8 u8
    16-bit i16 u16
    32-bit i32 u32
    64-bit i64 u64
    128-bit i128 u128
    arch isize usize

    有號數可儲存 \(-(2^{n - 1} )\) 到 \(2^{n - 1} - 1\) 的整數,無號數可儲存 0 到 \(2^n -1\) 的整數


    整數可以在數字後面加上型別的後綴 ex: 57u8 ,也可以加上 _ 分隔符號方便閱讀, ex: 1_000


    Number literals Example
    Decimal 98_222
    Hex 0xff
    Octal 0o77
    Binary 0b1111_0000
    Byte (u8 only) b'A'

​ integer overflow


​ 假設有個 u8 變數,如果將值修改為 256,就會發生 integer overflow。當 rust 在 debug mode 編譯時,會檢查這個問題,並讓程式 panic。 但是在 release mode,rust 不檢查 integer overflow,反而會進行 two's complement wrapping, 256 就會變成 0,而 257 變成 1。標準函式庫中有一個類別提供此功能: Wrapping


​


  • floating-point numbers


    rust 有兩種 floating-point numbers 類別: f32, f64。預設是 f64


    fn main() {
        let x = 2.0; // f64
    
        let y: f32 = 3.0; // f32
    }

    浮點數採用 IEEE-754 標準表示。f32 是單精度浮點數,f64 是雙精度浮點數。

  • Numeric Operations


​ 所有數字都支援基本的數學運算: + - * / 以及 % (mod)


     fn main() {
    // 加法
    let sum = 5 + 10;

    // 減法
    let difference = 95.5 - 4.3;

    // 乘法
    let product = 4 * 30;

    // 除法
    let quotient = 56.7 / 32.2;

    // 取餘
    let remainder = 43 % 5;
}

​


  • Booleans


    true 與 false


    fn main() {
        let t = true;
    
        let f: bool = false;
    }
  • characters


char 代表一個 unicode scalar value,從 U+0000 到 U+D7FF 和 U+E000 到 U+10FFFF 在內的值


fn main() {
    let c = 'z';
    let z = 'ℤ';
    let heart_eyed_cat = '😻';
}

Compound Types


可將多種型別的資料合併成一個類別, rust 有兩個原生的 compound types: typle, array


tuple


用圓括號加逗點區隔。可用 pattern matching 取得每一個 tuple element 的數值。也可以用 . 加上 index,


ex: x.0。


fn main() {
    let tup: (i32, f64, u8) = (500, 6.4, 1);

    let (x, y, z) = tup;
    println!("The value of y is: {}", y);

    let five_hundred = x.0;
    let six_point_four = x.1;
    let one = x.2;
}

array


array 中每一個元素的資料型別必須要相同,另外 array 的長度是固定的,宣告後就不能任意加長或縮短。


fn main() {
    let a = [1, 2, 3, 4, 5];
    let months = ["January", "February", "March", "April", "May", "June", "July",
              "August", "September", "October", "November", "December"];
}

如果想要在 stack 而不是 heap 儲存資料,或是想要有固定數量的元素時,可使用 array。要不然,就可以使用 vector,vector 類似 array 但允許增長或縮短。


array 的資料型別看起來像是 [type; number]


let a: [i32; 5] = [1, 2, 3, 4, 5];

array 存放在 stack,可用 index 取得元素


fn main() {
    let a = [1, 2, 3, 4, 5];

    let first = a[0];
    let second = a[1];
}

存取超過 array 長度的 index 會造成程式 panic


fn main() {
    let a = [1, 2, 3, 4, 5];
    let index = 10;

    let element = a[index];

    println!("The value of element is: {}", element);
}

編譯沒有錯誤,但執行會出現 runtime error。 這個特性提供了 rust 安全機制,禁止存取異常的 index 的資料。


$ cargo run
   Compiling guessing_game v0.1.0 (/Users/charley/project/guessing_game)
    Finished dev [unoptimized + debuginfo] target(s) in 0.70s
     Running `target/debug/guessing_game`
thread 'main' panicked at 'index out of bounds: the len is 5 but the index is 10', src/main.rs:5:19
note: Run with `RUST_BACKTRACE=1` environment variable to display a backtrace.

Functions


fn 用來宣告函數, main 是程式的入口點


函數跟變數的名稱,使用 snake case 風格,所有字母都是小寫,且用 _ 分隔單字


fn main() {
    println!("Hello, world!");

    another_function();
}

fn another_function() {
    println!("Another function.");
}

函數可以加上參數 parameters,但實際上應該稱為 arguments。不過大家已經不區分 parameter (定義中的變數) 以及arguments (傳入函數的 value)。


fn main() {
    another_function(5, 6);
}

fn another_function(x: i32, y: i32) {
    println!("The value of x is: {}", x);
    println!("The value of y is: {}", y);
}

function bodies 包含 statements & expressions


function bodies 是由一連串的 statements組成,最後面 optional 可加上一個 expression 結束。


Statements: 執行一些操作但不返回值的指令


Expressions : 計算並產生一個值


fn main() {
    // 錯誤,因為 let statement 不會產生回傳 value
    let x = (let y = 6);
}

換句話說,不能用 x=y=6 這樣類似 C, Ruby 的寫法


fn main() {
    let x = 5;

    // { } 之間是一個 block of codes,最後是一個 expression
    // expression 後面不能加上 ;
    // expression 會產生回傳值,並指定給 y
    let y = {
        // 這裡的 x 不會影響到外面的 x
        let x = 3;
        x + 1
    };

    println!("The value of x is: {}, y is: {}", x, y);
}

有 return value 的 function


function 可定義 return value 的資料型別,可使用 return 在函數中間直接回傳,會是在函數最後面,使用 expression。return 後面可加上 ; ,但 expression 後面不能加上 ;


fn test(flag: bool) -> i32 {
    if flag {
        return 4;
    }
    5
}

fn main() {
    let x = test(true);

    println!("The value of x is: {}", x);
}

Comments


// 後面就是註解


fn main() {
    let lucky_number1 = 7; // I’m feeling lucky today

    // I’m feeling lucky today
    let lucky_number2 = 7;
}

Control Flow


loop 跟 if


if expression


Rust 只會執行第一個條件為真的代碼塊,並且一旦它找到一個以後,就不會檢查剩下的條件了。所以這個程式只會列印 number is divisible by 3


fn main() {
    let number = 6;

    if number % 4 == 0 {
        println!("number is divisible by 4");
    } else if number % 3 == 0 {
        println!("number is divisible by 3");
    } else if number % 2 == 0 {
        println!("number is divisible by 2");
    } else {
        println!("number is not divisible by 4, 3, or 2");
    }
}

在 let statement 中使用 if


因 if 是 expression,可以放在 let 的右邊


fn main() {
    let condition = true;
    let number = if condition {
        5
    } else {
        6
    };

    println!("The value of number is: {}", number);
}

但如果 if 跟 else 回傳的資料型別不同,就會發生編譯錯誤


fn main() {
    let condition = true;
    
    // 編譯錯誤
    let number = if condition {
        5
    } else {
        "six"
    };

    println!("The value of number is: {}", number);
}

Loops


rust 有三種循環: loop, while, for


fn main() {
    let mut counter = 0;

    let result = loop {
        counter += 1;

        // 滿足此條件時, counter 為 10
        if counter == 10 {
            // 以 break 停止循環,並回傳 10 * 2
            break counter * 2;
        }
    };

    assert_eq!(result, 20);
}

while 迴圈,在裡面也可以直接用 break 停止迴圈


fn main() {
    let mut number = 3;

    while number != 0 {
        println!("{}!", number);

        number = number - 1;
    }

    println!("LIFTOFF!!!");
}

可用 while 或是 for,處理 array 中每一個元素


fn main() {
    let a = [10, 20, 30, 40, 50];
    let mut index = 0;

    // 使用 while 要注意 index,不能超過 array 的長度
    while index < 5 {
        println!("the value is: {}", a[index]);

        index = index + 1;
    }

    // 使用 for 就不需要注意 index
    for element in a.iter() {
        println!("the value is: {}", element);
    }

    // (1..4) 是 Range,可用 rev() 反轉順序
    for number in (1..4).rev() {
        println!("{}!", number);
    }
    println!("LIFTOFF!!!");
}

References


The Rust Programming Language


中文版


中文版 2

2020/2/23

Rust Guessing Game

一個基本的猜數字範例程式:程式會產生一個 1 ~ 100 的亂數,提示我們輸入數字猜猜看,填寫數字會得到太大或太小的結果,猜對時就結束遊戲。


Project


首先產生一個空的 guessting game project


$ cargo new guessing_game
     Created binary (application) `guessing_game` package
$ cd guessing_game

因為 src/main.rs 有基本的 hello world 範例,故新的專案都可以直接執行


$ cargo run
   Compiling guessing_game v0.1.0 (/Users/charley/project/idea/rust/guessing_game)
    Finished dev [unoptimized + debuginfo] target(s) in 1.39s
     Running `target/debug/guessing_game`
Hello, world!

Processing a Guess


首先要處理 user 輸入的數字


rust 預設只會匯入 prelude,如果沒有在 prelude 就要用 use 匯入該函式庫


// 匯入 io library
use std::io;

// main() 函式是程式的進入點
// fn 語法宣告一個新函式,() 表示沒有傳入的參數,{ 後面開始是函式的內容
// 因為沒有回傳型別,所以這裡的回傳會是 (),一個空的 tuple
fn main() {
    // println! 是列印字串的巨集
    println!("Guess the number!");

    println!("Please input your guess.");

    // let 是變數跟數值的綁定,預設是 immutable
    // 這裡加上 mut,變成 mutable variable
    // String 是 UTF-8 編碼的可變長度字串型別
    let mut guess = String::new();

    // io::stdin() 是使用 std::io::stdin(),會回傳一個 stdin 的 handle
    // 因最前面有 use std:io,所以這裡可省略 std::
    // 呼叫該 handle 的 read_line method,並提供 guess 變數的 reference

    // read_line() 會把使用者的輸入資料放入 &mut String 參數中。而它會回傳一個值 io::Result
    // Rust 的標準函式庫中有很多 Result 的型別:有一般的 Result 以及子函式庫的特別版本,ex: io::Result
    // Result 是要將錯誤訊息編碼
    // Result 的 variants 是 Ok, Err, Ok 裡面有成功的結果, Err 裡面有錯誤的原因

    // io:Result 提供 expect 方法,可取得呼叫該 method 的結果,如果結果不成功,就會 panic! ,並加上後面的 msg 訊息內容
    // 去掉expect 仍然可以編譯程式,但會出現 warning
    io::stdin().read_line(&mut guess).expect("Failed to read line");

    // 列印 guess 變數的資料, {} 是 placeholder
    println!("You guessed: {}", guess);
}

執行


$ cargo run
   Compiling guessing_game v0.1.0 (/Users/charley/project/idea/rust/guessing_game)
    Finished dev [unoptimized + debuginfo] target(s) in 0.70s
     Running `target/debug/guessing_game`
Guess the number!
Please input your guess.
1
You guessed: 1

Generating a Secret Number


Rust 標準函式庫沒有亂數的功能,但有提供 rand crate,crate 是一包 rust 的程式碼,而 rand 是 library crate,包含了可被其他城市使用的 codes。


要使用 crate 必須修改 Cargo.toml,在 dependencies 裡面加上 rand = "0.4.6"


[package]
name = "guessing_game"
version = "0.1.0"
authors = ["name <email@domain>"]

[dependencies]
rand = "0.4.6"

0.4.6 的部分稱為 Semantic Versioning (有時稱為SemVer),是標準的版本號碼,0.4.6 等同 ^0.4.6,就是任何跟 0.4.6 版本有相容 pulbic API的版本。如果要指定只能使用 0.4.6,要寫成 rand="=0.4.6"


再執行一次,會看到下載了新的 crate library,以及相依的 libc


$ cargo run
    Blocking waiting for file lock on the registry index
    Updating crates.io index
  Downloaded rand v0.3.23
  Downloaded rand v0.4.6
   Compiling libc v0.2.55
   Compiling rand v0.4.6
   Compiling rand v0.3.23
   Compiling guessing_game v0.1.0 (/Users/charley/project/guessing_game)
    Finished dev [unoptimized + debuginfo] target(s) in 20.87s
     Running `target/debug/guessing_game`

cargo 內建了一個機制,確保專案會使用相同版本的 dependencies,第一次建構專案後 (cargo build),就會產生一個 Cargo.lock檔案,該檔案裡面記錄了所有 dependencies 的版本號碼,接下來在重新 build project 時,會先檢查 Cargo.lock,並使用裡面的 versions of the dependencies。


如果要更新 crate 的版本,就要使用 cargo update 指令。


Generating a Random Number


使用 rand::Rng 產生亂數


use std::io;
// Rng 有產生亂數的 methods
use rand::Rng;

fn main() {
    println!("Guess the number!");

    // 1 是下限,101 是 exclusive upper bound
    let secret_number = rand::thread_rng().gen_range(1, 101);

    println!("The secret number is: {}", secret_number);

    println!("Please input your guess.");

    let mut guess = String::new();

    io::stdin().read_line(&mut guess)
        .expect("Failed to read line");

    println!("You guessed: {}", guess);
}

Comparing the Guess to the Secret Number


use std::io;
// Ordering 也是一個 Enum,成員是 Less, Greater, Equal
use std::cmp::Ordering;
use rand::Rng;

fn main() {

    // ---snip---

    println!("You guessed: {}", guess);

    // 把 guess 與 secret_number 做比較
    // 目前這裡會發生 mismatched types 錯誤,因為 Rust 是 strong type language
    // Rust 推測 guess 是 String,而 secret_number 是數字(i32/u32),預設為 i32
    match guess.cmp(&secret_number) {
        Ordering::Less => println!("Too small!"),
        Ordering::Greater => println!("Too big!"),
        Ordering::Equal => println!("You win!"),
    }
}

編譯錯誤


$ cargo run
   Compiling guessing_game v0.1.0 (/Users/charley/project/guessing_game)
error[E0308]: mismatched types
  --> src/main.rs:22:21
   |
22 |     match guess.cmp(&secret_number) {
   |                     ^^^^^^^^^^^^^^ expected struct `std::string::String`, found integer
   |
   = note: expected type `&std::string::String`
              found type `&{integer}`

error: aborting due to previous error

For more information about this error, try `rustc --explain E0308`.
error: Could not compile `guessing_game`.

要修改程式,將 guess 轉換為數字


// --snip--

    let mut guess = String::new();

    io::stdin().read_line(&mut guess)
        .expect("Failed to read line");

    // 重新定義 guess,trim 可去掉前後的空白或換行符號
    // 現在 guess 變成 u32,而 Rust 也會將 secret_number 視為 u32
    let guess: u32 = guess.trim().parse()
        .expect("Please type a number!");

    println!("You guessed: {}", guess);

    match guess.cmp(&secret_number) {
        Ordering::Less => println!("Too small!"),
        Ordering::Greater => println!("Too big!"),
        Ordering::Equal => println!("You win!"),
    }
}           

加上 loop 重複猜測數字


use std::io;
use std::cmp::Ordering;
use rand::Rng;

fn main() {
    println!("Guess the number!");

    let secret_number = rand::thread_rng().gen_range(1, 101);

    println!("The secret number is: {}", secret_number);

    loop {
        println!("Please input your guess.");

        let mut guess = String::new();

        io::stdin().read_line(&mut guess)
            .expect("Failed to read line");

        let guess: u32 = guess.trim().parse()
            .expect("Please type a number!");

        println!("You guessed: {}", guess);

        match guess.cmp(&secret_number) {
            Ordering::Less => println!("Too small!"),
            Ordering::Greater => println!("Too big!"),
            Ordering::Equal => println!("You win!"),
        }
    }

}

目前程式還需要再猜對時,正常跳出程式。另外要針對 guess 的轉型進行檢查。


use std::io;
use std::cmp::Ordering;
use rand::Rng;

fn main() {
    println!("Guess the number!");

    let secret_number = rand::thread_rng().gen_range(1, 101);

    // println!("The secret number is: {}", secret_number);

    loop {
        println!("Please input your guess.");

        let mut guess = String::new();

        io::stdin().read_line(&mut guess)
            .expect("Failed to read line");

        // 將 expect 換成 match
        // parse 返回一個 Result 類型,而 Result 是一個 Ok 或 Err 成員的 enum
        let guess: u32 = match guess.trim().parse() {
            Ok(num) => num,
            Err(_) => continue,
        };

        println!("You guessed: {}", guess);

        match guess.cmp(&secret_number) {
            Ordering::Less => println!("Too small!"),
            Ordering::Greater => println!("Too big!"),
            Ordering::Equal => {
                println!("You win!");
                break;
            },
        }
    }

}

References


The Rust Programming Language


中文版


中文版 2

2020/2/17

Media Resource Control Protocol - MRCP

MRCP 是 speech server 給 client 提供服務(例如 speech recognition, speech synthesis)的傳輸協定,MRCP 無法獨立運作,必須透過 RTSP 或 SIP 建立 control session 與 audio streams。MRCP 是使用類似 http 的 text style protocol,每個訊息包含三個部分:first line, header, body。


MRCP 使用跟 http 一樣的 a request and reponse model,例如 MRCP client 發送 request,要求要發送 audio data 給 server 做語音辨識,server 會回傳一個訊息,裡面包含要接收資料的 port number,因為 MRCP 並沒有規範要如何傳送語音資料,這部分就透過 RTP 處理。


MRCP v2 (RFC 6787)使用 SIP 管理 session 與 audio stream,v1 (RFC 4463) 則沒有規範這部分要使用哪一種 protocol,目前比較常討論的是 MRCP v2,另外因為MRCPv1依賴 RTSP (RFC2326),但在討論 MRCP v2 時,大家一致 RTSP 的這種使用方式,會導致向後兼容性問題,因此在 (Requirements for Distributed Control of Automatic Speech Recognition (ASR), Speaker Identification/Speaker Verification (SI/SV), and Text-to-Speech (TTS) Resources RFC4313) 的3.2節禁止使用,這就是為什麼MRCPv2不能在RTSP上運作的原因。


MRCP V2 中使用了 SIP 負責建立獨立的媒體和會話支持語音媒體資源,增加了對講話者變化和講話者的身份引擎的支援(speaker verification 和 identification),同時增加了未來的擴充能力。


MRCP v2 規範中的架構圖為


     MRCPv2 client                   MRCPv2 Media Resource Server
|--------------------|            |------------------------------------|
||------------------||            ||----------------------------------||
|| Application Layer||            ||Synthesis|Recognition|Verification||
||------------------||            || Engine  |  Engine   |   Engine   ||
||Media Resource API||            ||    ||   |    ||     |    ||      ||
||------------------||            ||Synthesis|Recognizer |  Verifier  ||
|| SIP  |  MRCPv2   ||            ||Resource | Resource  |  Resource  ||
||Stack |           ||            ||     Media Resource Management    ||
||      |           ||            ||----------------------------------||
||------------------||            ||   SIP  |        MRCPv2           ||
||   TCP/IP Stack   ||---MRCPv2---||  Stack |                         ||
||                  ||            ||----------------------------------||
||------------------||----SIP-----||           TCP/IP Stack           ||
|--------------------|            ||                                  ||
         |                        ||----------------------------------||
        SIP                       |------------------------------------|
         |                          /
|-------------------|             RTP
|                   |             /
| Media Source/Sink |------------/
|                   |
|-------------------|

                      Figure 1: Architectural Diagram

W3C 在 1999年建立 Voice Broswer Working Group(VBWG),研究如何透過 web 支援語音辨識及 DTMF 處理,然後發佈了基於 web 的語音介面架構,核心是 VoiceXML。


W3C 的 Speech Recognition Grammar Specification (SRGS) 是一種 XML 標準,支援語音語法的規則,可識別的短詞語。和 SRGS 比較接近的是 W3C Semantic Interpretation for Speech Recognition (SISR),它更常用在標記語義信息支援語音語法,構成了對自然語言理解的基本格式。W3C Speech Synthesis Markup Language (SSML)是基於 XML 的方式指定內容進行語音合成的方式,可控制語音的各種屬性,包括音量大小,發音,語音間距,語速等方面的控制。


SRGS和SSML能互補和控制W3C的發音語法規則(Pronunciation Lexicon Specification (PLS))。PLS可以使用標準的發音字母來指定單字和短詞語發音。


VoiceXML 協助 MRCP,可支援多種第三方語音辨識及合成引擎。


MRCPv2 Media Resource Types


一個 MRCPv2 server 就是一種 SIP server,因此是用 SIP URI 方式定址 (sip:mrcpv2@example.net or sips:mrcpv2@example.net),可提供以下 media processing resources 給 clients


  • Basic Synthesizer


    透過連接 audio clips 產生語音 media stream,speech data 是以 limited subset of the Speech Synthesis Markup Language (SSML) 描述,最簡單的 synthesizer 必須支援這些 SSML tags: <speak>, <audio>, <say-as>, <mark>

  • Speech Synthesizer


    有完整 TTS 功能,必須完整支援 SSML

  • Recorder


    recoding audio 並提供該錄音的 URI,必須支援在錄音的最前面及後面要 supressing silence,錄音檔的中間可選擇要不要 supress silence,如果有做靜音處理,要記錄 timting metadata,才能知道原始錄音 media 實際發生語音的 timestamp

  • DTMF Recognizer


    能取得 media stream 中的 Dual-Tone Multi-Frequency (DTMF) digits,並對應到 supplied sigit grammar 中

  • Speech Recognizer


    完整的 speech recognition resource 可接收 audio media stream 並辨識取得結果,另外包含一個 natural language semantic interpreter 做辨識結果的 post-process,轉為 grammar 中的 semantic data

  • Speaker Verifier


    可辨別已存在的 voice print 的 speaker


Resource Type Resource Description
speechrecog Speech Recognizer
dtmfrecog DTMF Recognizer
speechsynth Speech Synthesizer
basicsynth Basic Synthesizer
speakverify Speaker Verification
recorder Speech Recorder

MRCPv2 的規範中,整個應用的使用過程如下:


  1. MRCP Client 通過SIP&SDP建立與MRCP Server的MRCP control channel(使用MRCP 通道ID進行唯一標識,MRCP Server返回200消息時,通過a==channel屬性指定)

  2. 可以使用SIP的Re-INVITE消息添加或者刪除一個會話中的MRCP control channel,所以一個 session 可以擁有多個MRCP control channels(比如:一個會話可以同時擁有ASR&TTS channel)

  3. 多個MRCP control channel 可以共享同一個TCP connection

  4. 一個 MRCP message 只能攜帶一個MRCP channel ID。

  5. MRCP控制消息不能更改 SIP dialog 的狀態。

  6. 由於MRCP不保證傳輸的可靠性,所以必須使用TCP/TLS來保證其傳輸


resourse control channel


MRCPv2 附在 SIP 的 SDP 裡面,client 透過 SIP Invite 連接 MRCPv2 server,產生 SIP dialog,SDP 讓兩個端點協調所有要建立的 resource control channel,並產生 server 與 source/sink of audio 之間的 media session。


client 需要建立獨立的 MRCPv2 resource control channel,控制 SIP dialog 裡面要處理的 media resource,因此需要產生一個唯一的 channel identifier string。


在 SDP 中,要有一行 "m=" 給 session 中每一個 MRCPv2 resource 使用,transport type 必須要是 "TCP/MRCPv2" or "TCP/TLS/MRCPv2",client 可透過 TCP 或 TCP/TLS 連接到 MRCPv2 server。


example:


連接到 synthesizer 的範例,server 會產生一個單向 audio stream 傳給 client


  1. 產生 Synthesizer Control Channel

C->S:  INVITE sip:mresources@example.com SIP/2.0
          Via:SIP/2.0/TCP client.atlanta.example.com:5060;
           branch=z9hG4bK74bf1
          Max-Forwards:6
          To:MediaServer <sip:mresources@example.com>
          From:sarvi <sip:sarvi@example.com>;tag=1928301774
          Call-ID:a84b4c76e66710
          CSeq:314161 INVITE
          Contact:<sip:sarvi@client.example.com>
          Content-Type:application/sdp
          Content-Length:...

          v=0
          o=sarvi 2890844526 2890844526 IN IP4 192.0.2.12
          s=-
          c=IN IP4 192.0.2.12
          t=0 0
          m=application 9 TCP/MRCPv2 1
          a=setup:active
          a=connection:new
          a=resource:speechsynth
          a=cmid:1
          m=audio 49170 RTP/AVP 0
          a=rtpmap:0 pcmu/8000
          a=recvonly
          a=mid:1

   S->C:  SIP/2.0 200 OK
          Via:SIP/2.0/TCP client.atlanta.example.com:5060;
           branch=z9hG4bK74bf1;received=192.0.32.10
          To:MediaServer <sip:mresources@example.com>;tag=62784
          From:sarvi <sip:sarvi@example.com>;tag=1928301774
          Call-ID:a84b4c76e66710
          CSeq:314161 INVITE
          Contact:<sip:mresources@server.example.com>
          Content-Type:application/sdp
          Content-Length:...

          v=0
          o=- 2890842808 2890842808 IN IP4 192.0.2.11
          s=-
          c=IN IP4 192.0.2.11
          t=0 0
          m=application 32416 TCP/MRCPv2 1
          a=setup:passive
          a=connection:new
          a=channel:32AECB234338@speechsynth
          a=cmid:1
          m=audio 48260 RTP/AVP 0
          a=rtpmap:0 pcmu/8000
          a=sendonly
          a=mid:1

   C->S:  ACK sip:mresources@server.example.com SIP/2.0
          Via:SIP/2.0/TCP client.atlanta.example.com:5060;
           branch=z9hG4bK74bf2
          Max-Forwards:6
          To:MediaServer <sip:mresources@example.com>;tag=62784
          From:Sarvi <sip:sarvi@example.com>;tag=1928301774
          Call-ID:a84b4c76e66710
          CSeq:314161 ACK
          Content-Length:0

上面的 RTP 資源,另外再對 recognizer 要求取得一個 resource control channel 的資源,並改為 sendrecv 雙向傳輸語音


   C->S:  INVITE sip:mresources@server.example.com SIP/2.0
          Via:SIP/2.0/TCP client.atlanta.example.com:5060;
           branch=z9hG4bK74bf3
          Max-Forwards:6
          To:MediaServer <sip:mresources@example.com>;tag=62784
          From:sarvi <sip:sarvi@example.com>;tag=1928301774
          Call-ID:a84b4c76e66710
          CSeq:314162 INVITE
          Contact:<sip:sarvi@client.example.com>
          Content-Type:application/sdp
          Content-Length:...

          v=0
          o=sarvi 2890844526 2890844527 IN IP4 192.0.2.12
          s=-
          c=IN IP4 192.0.2.12
          t=0 0
          m=application 9 TCP/MRCPv2 1
          a=setup:active
          a=connection:existing
          a=resource:speechsynth
          a=cmid:1
          m=audio 49170 RTP/AVP 0 96
          a=rtpmap:0 pcmu/8000
          a=rtpmap:96 telephone-event/8000
          a=fmtp:96 0-15
          a=sendrecv
          a=mid:1
          m=application 9 TCP/MRCPv2 1
          a=setup:active
          a=connection:existing
          a=resource:speechrecog
          a=cmid:1

   S->C:  SIP/2.0 200 OK
          Via:SIP/2.0/TCP client.atlanta.example.com:5060;
           branch=z9hG4bK74bf3;received=192.0.32.10
          To:MediaServer <sip:mresources@example.com>;tag=62784
          From:sarvi <sip:sarvi@example.com>;tag=1928301774
          Call-ID:a84b4c76e66710
          CSeq:314162 INVITE
          Contact:<sip:mresources@server.example.com>
          Content-Type:application/sdp
          Content-Length:...

          v=0
          o=- 2890842808 2890842809 IN IP4 192.0.2.11
          s=-
          c=IN IP4 192.0.2.11
          t=0 0
          m=application 32416 TCP/MRCPv2 1
          a=setup:passive
          a=connection:existing
          a=channel:32AECB234338@speechsynth
          a=cmid:1
          m=audio 48260 RTP/AVP 0 96
          a=rtpmap:0 pcmu/8000
          a=rtpmap:96 telephone-event/8000
          a=fmtp:96 0-15
          a=sendrecv
          a=mid:1
          m=application 32416 TCP/MRCPv2 1
          a=setup:passive
          a=connection:existing
          a=channel:32AECB234338@speechrecog
          a=cmid:1

   C->S:  ACK sip:mresources@server.example.com SIP/2.0
          Via:SIP/2.0/TCP client.atlanta.example.com:5060;
           branch=z9hG4bK74bf4
          Max-Forwards:6
          To:MediaServer <sip:mresources@example.com>;tag=62784
          From:Sarvi <sip:sarvi@example.com>;tag=1928301774
          Call-ID:a84b4c76e66710
          CSeq:314162 ACK
          Content-Length:0

釋放 recofnizer channel 的資源,改回 recvonly


   C->S:  INVITE sip:mresources@server.example.com SIP/2.0
          Via:SIP/2.0/TCP client.atlanta.example.com:5060;
           branch=z9hG4bK74bf5
          Max-Forwards:6
          To:MediaServer <sip:mresources@example.com>;tag=62784
          From:sarvi <sip:sarvi@example.com>;tag=1928301774
          Call-ID:a84b4c76e66710
          CSeq:314163 INVITE
          Contact:<sip:sarvi@client.example.com>
          Content-Type:application/sdp
          Content-Length:...

          v=0
          o=sarvi 2890844526 2890844528 IN IP4 192.0.2.12
          s=-
          c=IN IP4 192.0.2.12
          t=0 0
          m=application 9 TCP/MRCPv2 1
          a=resource:speechsynth
          a=cmid:1
          m=audio 49170 RTP/AVP 0
          a=rtpmap:0 pcmu/8000
          a=recvonly
          a=mid:1
          m=application 0 TCP/MRCPv2 1
          a=resource:speechrecog
          a=cmid:1


   S->C:  SIP/2.0 200 OK
          Via:SIP/2.0/TCP client.atlanta.example.com:5060;
           branch=z9hG4bK74bf5;received=192.0.32.10
          To:MediaServer <sip:mresources@example.com>;tag=62784
          From:sarvi <sip:sarvi@example.com>;tag=1928301774
          Call-ID:a84b4c76e66710
          CSeq:314163 INVITE
          Contact:<sip:mresources@server.example.com>
          Content-Type:application/sdp
          Content-Length:...

          v=0
          o=- 2890842808 2890842810 IN IP4 192.0.2.11
          s=-
          c=IN IP4 192.0.2.11
          t=0 0
          m=application 32416 TCP/MRCPv2 1
          a=channel:32AECB234338@speechsynth
          a=cmid:1
          m=audio 48260 RTP/AVP 0
          a=rtpmap:0 pcmu/8000
          a=sendonly
          a=mid:1
          m=application 0 TCP/MRCPv2 1
          a=channel:32AECB234338@speechrecog
          a=cmid:1

   C->S:  ACK sip:mresources@server.example.com SIP/2.0
          Via:SIP/2.0/TCP client.atlanta.example.com:5060;
           branch=z9hG4bK74bf6
          Max-Forwards:6
          To:MediaServer <sip:mresources@example.com>;tag=62784
          From:Sarvi <sip:sarvi@example.com>;tag=1928301774
          Call-ID:a84b4c76e66710
          CSeq:314163 ACK
          Content-Length:0

MRCPv2 message


MRCPv2 訊息包含 client 給 server 的 request,及server 發給 client 的 response 與asynchronous events,資料格式包含一行 start-line,多個 headers,一行 empty line 代表 header 結束,然後是 optional message body,跟 http protocol 類似


generic-message  =    start-line
                      message-header
                      CRLF
                      [ message-body ]

message-body     =    *OCTET

start-line       =    request-line / response-line / event-line

message-header   =  1*(generic-header / resource-header / generic-field)

resource-header  =    synthesizer-header
                 /    recognizer-header
                 /    recorder-header
                 /    verifier-header

ex:


   C->S:   MRCP/2.0 877 INTERPRET 543266
           Channel-Identifier:32AECB23433801@speechrecog
           Interpret-Text:may I speak to Andre Roy
           Content-Type:application/srgs+xml
           Content-ID:<request1@form-level.store>
           Content-Length:661

           <?xml version="1.0"?>
           <!-- the default grammar language is US English -->
           <grammar xmlns="http://www.w3.org/2001/06/grammar"
                    xml:lang="en-US" version="1.0" root="request">
           <!-- single language attachment to tokens -->
               <rule id="yes">
                   <one-of>
                       <item xml:lang="fr-CA">oui</item>
                       <item xml:lang="en-US">yes</item>
                   </one-of>
               </rule>
           <!-- single language attachment to a rule expansion -->
               <rule id="request">
                   may I speak to
                   <one-of xml:lang="fr-CA">
                       <item>Michel Tremblay</item>
                       <item>Andre Roy</item>
                   </one-of>
               </rule>
           </grammar>

   S->C:   MRCP/2.0 82 543266 200 IN-PROGRESS
           Channel-Identifier:32AECB23433801@speechrecog

   S->C:   MRCP/2.0 634 INTERPRETATION-COMPLETE 543266 200 COMPLETE
           Channel-Identifier:32AECB23433801@speechrecog
           Completion-Cause:000 success
           Content-Type:application/nlsml+xml
           Content-Length:441

           <?xml version="1.0"?>
           <result xmlns="urn:ietf:params:xml:ns:mrcpv2"
                   xmlns:ex="http://www.example.com/example"
                   grammar="session:request1@form-level.store">
               <interpretation>
                   <instance name="Person">
                       <ex:Person>
                           <ex:Name> Andre Roy </ex:Name>
                       </ex:Person>
                   </instance>
                   <input>   may I speak to Andre Roy </input>
               </interpretation>
           </result>

request-line 的格式為


   request-line   =    mrcp-version SP message-length SP method-name SP request-id CRLF

   method-name    =    generic-method
                  /    synthesizer-method
                  /    recognizer-method
                  /    recorder-method
                  /    verifier-method
                  
   request-id     =    1*10DIGIT

response 的格式為


response-line  =    mrcp-version SP message-length SP request-id
                       SP status-code SP request-state CRLF
status-code     =    3DIGIT
request-state    =  "COMPLETE"
                    /  "IN-PROGRESS"
                    /  "PENDING"

event-line 的格式為


event-line       =  mrcp-version SP message-length SP event-name
                       SP request-id SP request-state CRLF
event-name       =  synthesizer-event
                    /  recognizer-event
                    /  recorder-event
                    /  verifier-event

注意到訊息格式中,分別對 synthesizer, recognizer, recorder, verifier 四種 resource type,有不同的定義 methods, headers, events


Generic Methods, Headers, Result Structure


所有 resource 通用的 methods, headers


MRCPv2 支援兩種 generic methods,可 reading, writing 相關資源的 state


   generic-method      =    "SET-PARAMS"
                       /    "GET-PARAMS"

SET-PARAMS


​ client 發送給 server,通知該 session 的 MRCPv2 resource 要定義 parameter


   C->S:  MRCP/2.0 ... SET-PARAMS 543256
          Channel-Identifier:32AECB23433802@speechsynth
          Voice-gender:female
          Voice-variant:3

   S->C:  MRCP/2.0 ... 543256 200 COMPLETE
          Channel-Identifier:32AECB23433802@speechsynth

GET-PARAMS


​ client 發送給 server,通知要取得 MRCPv2 resource 目前的 session parameters


   C->S:   MRCP/2.0 ... GET-PARAMS 543256
           Channel-Identifier:32AECB23433802@speechsynth
           Voice-gender:
           Voice-variant:
           Vendor-Specific-Parameters:com.example.param1;
                         com.example.param2
   S->C:   MRCP/2.0 ... 543256 200 COMPLETE
           Channel-Identifier:32AECB23433802@speechsynth
           Voice-gender:female
           Voice-variant:3
           Vendor-Specific-Parameters:com.example.param1="Company Name";
                         com.example.param2="124324234@example.com"

所有 MRCPv2 header 中,包含 generic-headers 及 resource-specific headers


header 的定義如下


   generic-field  = field-name ":" [ field-value ]
   field-name     = token
   field-value    = *LWS field-content *( CRLF 1*LWS field-content)
   field-content  = <the OCTETs making up the field-value
                    and consisting of either *TEXT or combinations
                    of token, separators, and quoted-string>

generic header 有


   generic-header      =    channel-identifier
                       /    accept
                       /    active-request-id-list
                       /    proxy-sync-id
                       /    accept-charset
                       /    content-type
                       /    content-id
                       /    content-base
                       /    content-encoding
                       /    content-location
                       /    content-length
                       /    fetch-timeout
                       /    cache-control
                       /    logging-tag
                       /    set-cookie
                       /    vendor-specific

  • Channel-Identifier


    在產生一個 control channel 時,由 server 指定一個 Channel Id


   channel-identifier  = "Channel-Identifier" ":" channel-id CRLF
   channel-id          = 1*alphanum "@" 1*alphanum

  • Accept

  • Active-Request-Id-List


    在 request 裡面,這個 header 代表這個 request 對這個 list of request-ids 有作用。在 response ,這個 header 代表該 method 影響到的 list of request-ids


       active-request-id-list  =  "Active-Request-Id-List" ":"
                                  request-id *("," request-id) CRLF
  • Proxy-Sync-Id


    當某個 server resource 產生 "barge-in-able" event,也會產生一個 unique tag,該 tag 會透過這個 header 放在 event 裡面,傳給 client


       proxy-sync-id    =  "Proxy-Sync-Id" ":" 1*VCHAR CRLF
  • Accept-Charset


    在 request 裡面指定 response or event 可接受能夠處理的 character sets。


    例如指定 Natural Language Semantic Markup Language (NLSML) results 的 RECOGNITION-COMPLETE event 可使用的 character set

  • Content-Type


    MRCPv2 的 content 支援有限 media types,例如 speech markup, grammer, recofnition results


       content-type     =    "Content-Type" ":" media-type-value CRLF
    
       media-type-value =    type "/" subtype *( ";" parameter )
    
       type             =    token
    
       subtype          =    token
    
       parameter        =    attribute "=" value
    
       attribute        =    token
    
       value            =    token / quoted-string
  • Content-ID


    該 content 參考或引用的 ID or name

  • Content-Base


    指定 base URI


    content-base      = "Content-Base" ":" absoluteURI CRLF
  • Content-Encoding


    某個 Content-Type 的附加資訊,例如 Content-Encoding:gzip


       content-encoding  = "Content-Encoding" ":"
                           *WSP content-coding
                           *(*WSP "," *WSP content-coding *WSP )
                           CRLF
  • Content-Location


       content-location  =  "Content-Location" ":"
                            ( absoluteURI / relativeURI ) CRLF
  • Content-Length


    message body 的長度


       content-length  =  "Content-Length" ":" 1*19DIGIT CRLF
  • Fetch Timeout


    當 recognizer/synthesizer 需要取得文件或其他資源,定義 server 透過網路取得資源的 timeout 時間


       fetch-timeout       =   "Fetch-Timeout" ":" 1*19DIGIT CRLF
  • Cache-Control


    如果 server 有支援 content caching,遵循 http 1.1 的規則提供 cache


       cache-control    =    "Cache-Control" ":"
                             [*WSP cache-directive
                             *( *WSP "," *WSP cache-directive *WSP )]
                             CRLF
    
       cache-directive     = "max-age" "=" delta-seconds
                           / "max-stale" [ "=" delta-seconds ]
                           / "min-fresh" "=" delta-seconds
    
       delta-seconds       = 1*19DIGIT
  • Logging-Tag


    SET-PARAMS/GET-PARAMS method 的 header,可 set/retrieve server 產生的 log 的 logging tag


       logging-tag    = "Logging-Tag" ":" 1*UTFCHAR CRLF
  • Set-Cookie


    類似 http 的 cookie,讓 server 在 client 存放 cookie values

  • Vendor-Specific Parameters


    ex:


       com.example.companyA.paramxyz=256
       com.example.companyA.paramabc=High
       com.example.companyB.paramxyz=Low



Generic Result Structure


Recognizer 與 Verifier resource server 產生的 result data,以 Natural Language Semantics Markup Language (NLSML) 格式提供


ex:


   Content-Type:application/nlsml+xml
   Content-Length:...

   <?xml version="1.0"?>
   <result xmlns="urn:ietf:params:xml:ns:mrcpv2"
           xmlns:ex="http://www.example.com/example"
           grammar="http://theYesNoGrammar">
       <interpretation>
           <instance>
                   <ex:response>yes</ex:response>
           </instance>
           <input>OK</input>
       </interpretation>
   </result>

Resource Discovery


透過 SIP OPTIONS 向 server 詢問 server capabilities


server 必須以 SDP 回應 capabilities,包含 media type, transport type: m=application 0 TCP/TLS/MRCPv2 1,以及 resource: a=resource:speechsynth


ex:


   C->S:
        OPTIONS sip:mrcp@server.example.com SIP/2.0
        Via:SIP/2.0/TCP client.atlanta.example.com:5060;
         branch=z9hG4bK74bf7
        Max-Forwards:6
        To:<sip:mrcp@example.com>
        From:Sarvi <sip:sarvi@example.com>;tag=1928301774
        Call-ID:a84b4c76e66710
        CSeq:63104 OPTIONS
        Contact:<sip:sarvi@client.example.com>
        Accept:application/sdp
        Content-Length:0


   S->C:
        SIP/2.0 200 OK
        Via:SIP/2.0/TCP client.atlanta.example.com:5060;
         branch=z9hG4bK74bf7;received=192.0.32.10
        To:<sip:mrcp@example.com>;tag=62784
        From:Sarvi <sip:sarvi@example.com>;tag=1928301774
        Call-ID:a84b4c76e66710
        CSeq:63104 OPTIONS
        Contact:<sip:mrcp@server.example.com>
        Allow:INVITE, ACK, CANCEL, OPTIONS, BYE
         Accept:application/sdp
        Accept-Encoding:gzip
        Accept-Language:en
        Supported:foo
        Content-Type:application/sdp
        Content-Length:...

        v=0
        o=sarvi 2890844536 2890842811 IN IP4 192.0.2.12
        s=-
        i=MRCPv2 server capabilities
        c=IN IP4 192.0.2.12/127
        t=0 0
        m=application 0 TCP/TLS/MRCPv2 1
        a=resource:speechsynth
        a=resource:speechrecog
        a=resource:speakverify
        m=audio 0 RTP/AVP 0 3
        a=rtpmap:0 PCMU/8000
        a=rtpmap:3 GSM/8000

Speech Synthesizer Resource


client 發送 text markup,讓server 即時產生 audio stream,可指定語音合成的參數,例如 voice characteristics, speaker speed


有兩種: speech synth, basicsynth


Synthesizer State Machine


pending 的 SPEAK request 可以被 deleted/stopped


   Idle                    Speaking                  Paused
   State                   State                     State
     |                        |                          |
     |----------SPEAK-------->|                 |--------|
     |<------STOP-------------|             CONTROL      |
     |<----SPEAK-COMPLETE-----|                 |------->|
     |<----BARGE-IN-OCCURRED--|                          |
     |              |---------|                          |
     |          CONTROL       |-----------PAUSE--------->|
     |              |-------->|<----------RESUME---------|
     |                        |               |----------|
     |----------|             |              PAUSE       |
     |    BARGE-IN-OCCURRED   |               |--------->|
     |<---------|             |----------|               |
     |                        |      SPEECH-MARKER       |
     |                        |<---------|               |
     |----------|             |----------|               |
     |         STOP           |       RESUME             |
     |          |             |<---------|               |
     |<---------|             |                          |
     |<---------------------STOP-------------------------|
     |----------|             |                          |
     |     DEFINE-LEXICON     |                          |
     |          |             |                          |
     |<---------|             |                          |
     |<---------------BARGE-IN-OCCURRED------------------|

Synthesizer Methods


   synthesizer-method   =  "SPEAK"
                        /  "STOP"
                        /  "PAUSE"
                        /  "RESUME"
                        /  "BARGE-IN-OCCURRED"
                        /  "CONTROL"
                        /  "DEFINE-LEXICON"

Synthesizer Events


   synthesizer-event    =  "SPEECH-MARKER"
                        /  "SPEAK-COMPLETE"

Synthesizer Header Fields


   synthesizer-header  =  jump-size
                       /  kill-on-barge-in
                       /  speaker-profile
                       /  completion-cause
                       /  completion-reason
                       /  voice-parameter
                       /  prosody-parameter
                       /  speech-marker
                       /  speech-language
                       /  fetch-hint
                       /  audio-fetch-hint
                       /  failed-uri
                       /  failed-uri-cause
                       /  speak-restart
                       /  speak-length
                       /  load-lexicon
                       /  lexicon-search-order



Example:


text 會被合成並播放到 media stream,resource 會產生 IN-PROGRESS, SPEAK-COMPLETE event


   C->S: MRCP/2.0 ... SPEAK 543257
         Channel-Identifier:32AECB23433802@speechsynth
         Voice-gender:neutral
         Voice-Age:25
         Prosody-volume:medium
         Content-Type:application/ssml+xml
         Content-Length:...

         <?xml version="1.0"?>
            <speak version="1.0"
                xmlns="http://www.w3.org/2001/10/synthesis"
                xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
                xsi:schemaLocation="http://www.w3.org/2001/10/synthesis
                   http://www.w3.org/TR/speech-synthesis/synthesis.xsd"
                xml:lang="en-US">
            <p>
             <s>You have 4 new messages.</s>
             <s>The first is from Stephanie Williams and arrived at
                <break/>
                <say-as interpret-as="vxml:time">0342p</say-as>.
                </s>
             <s>The subject is
                    <prosody rate="-20%">ski trip</prosody>
             </s>
            </p>
           </speak>

   S->C: MRCP/2.0 ... 543257 200 IN-PROGRESS
         Channel-Identifier:32AECB23433802@speechsynth
         Speech-Marker:timestamp=857206027059

   S->C: MRCP/2.0 ... SPEAK-COMPLETE 543257 COMPLETE
         Channel-Identifier:32AECB23433802@speechsynth
         Completion-Cause:000 normal
         Speech-Marker:timestamp=857206027059

Speech Recognizer Resource


接收 client 提供的 voice stream,轉換為文字


有兩種: speechrecog, dtmfrecog


recognizer resource 的能力有:


  1. Normal Mode Recognition:會將整個語音或 DTMF 判斷是否吻合

  2. Hotword Mode Recognition


    判斷是否有出現某個特定的 speech grammar or DTMF sequence

  3. Voice Enrolled Grammars


    (optional) enrollment 是用某個人的 voice 進行判斷, server 會維護 a list of contacts,包含人員的名稱以及 voice,這個技術也稱為 speaker-dependent recognition

  4. Interpretation


    natural language interpretation


    以 text 作為 input,產生該文字的 grammar


Recognizer State Machine


   Idle                   Recognizing               Recognized
   State                  State                     State
    |                       |                          |
    |---------RECOGNIZE---->|---RECOGNITION-COMPLETE-->|
    |<------STOP------------|<-----RECOGNIZE-----------|
    |                       |                          |
    |              |--------|              |-----------|
    |       START-OF-INPUT  |       GET-RESULT         |
    |              |------->|              |---------->|
    |------------|          |                          |
    |      DEFINE-GRAMMAR   |----------|               |
    |<-----------|          | START-INPUT-TIMERS       |
    |                       |<---------|               |
    |------|                |                          |
    |  INTERPRET            |                          |
    |<-----|                |------|                   |
    |                       |   RECOGNIZE              |
    |-------|               |<-----|                   |
    |      STOP                                        |
    |<------|                                          |
    |<-------------------STOP--------------------------|
    |<-------------------DEFINE-GRAMMAR----------------|

Recognizer Methods


   recognizer-method    =  recog-only-method
                        /  enrollment-method
   recog-only-method    =  "DEFINE-GRAMMAR"
                        /  "RECOGNIZE"
                        /  "INTERPRET"
                        /  "GET-RESULT"
                        /  "START-INPUT-TIMERS"
                        /  "STOP"
   enrollment-method    =  "START-PHRASE-ENROLLMENT"
                        /  "ENROLLMENT-ROLLBACK"
                        /  "END-PHRASE-ENROLLMENT"
                        /  "MODIFY-PHRASE"
                        /  "DELETE-PHRASE"

Recognizer Events


   recognizer-event     =  "START-OF-INPUT"
                        /  "RECOGNITION-COMPLETE"
                        /  "INTERPRETATION-COMPLETE"

Recognizer Header Fields


   recognizer-header    =  recog-only-header
                        /  enrollment-header

   recog-only-header    =  confidence-threshold
                        /  sensitivity-level
                        /  speed-vs-accuracy
                        /  n-best-list-length
                        /  no-input-timeout
                        /  input-type
                        /  recognition-timeout
                        /  waveform-uri
                        /  input-waveform-uri
                        /  completion-cause
                        /  completion-reason
                        /  recognizer-context-block
                        /  start-input-timers
                        /  speech-complete-timeout
                        /  speech-incomplete-timeout
                        /  dtmf-interdigit-timeout
                        /  dtmf-term-timeout
                        /  dtmf-term-char
                        /  failed-uri
                        /  failed-uri-cause
                        /  save-waveform
                        /  media-type
                        /  new-audio-channel
                        /  speech-language
                        /  ver-buffer-utterance
                        /  recognition-mode
                        /  cancel-if-queue
                        /  hotword-max-duration
                        /  hotword-min-duration
                        /  interpret-text
                        /  dtmf-buffer-time
                        /  clear-dtmf-buffer
                        /  early-no-match
                        
   enrollment-header    =  num-min-consistent-pronunciations
                        /  consistency-threshold
                        /  clash-threshold
                        /  personal-grammar-uri
                        /  enroll-utterance
                        /  phrase-id
                        /  phrase-nl
                        /  weight
                        /  save-best-waveform
                        /  new-phrase-id
                        /  confusable-phrases-uri
                        /  abort-phrase-enrollment



Example


   C->S:MRCP/2.0 ... RECOGNIZE 543257
   Channel-Identifier:32AECB23433801@speechrecog
           Confidence-Threshold:0.9
   Content-Type:application/srgs+xml
   Content-ID:<request1@form-level.store>
   Content-Length:...

   <?xml version="1.0"?>

   <!-- the default grammar language is US English -->
   <grammar xmlns="http://www.w3.org/2001/06/grammar"
            xml:lang="en-US" version="1.0" root="request">

   <!-- single language attachment to tokens -->
       <rule id="yes">
               <one-of>
                     <item xml:lang="fr-CA">oui</item>
                     <item xml:lang="en-US">yes</item>
               </one-of>
         </rule>

   <!-- single language attachment to a rule expansion -->
         <rule id="request">
               may I speak to
               <one-of xml:lang="fr-CA">
                     <item>Michel Tremblay</item>
                     <item>Andre Roy</item>
               </one-of>
         </rule>

     </grammar>

   S->C: MRCP/2.0 ... 543257 200 IN-PROGRESS
   Channel-Identifier:32AECB23433801@speechrecog

   S->C:MRCP/2.0 ... START-OF-INPUT 543257 IN-PROGRESS
   Channel-Identifier:32AECB23433801@speechrecog

   S->C:MRCP/2.0 ... RECOGNITION-COMPLETE 543257 COMPLETE
   Channel-Identifier:32AECB23433801@speechrecog
   Completion-Cause:000 success
   Waveform-URI:<http://web.media.com/session123/audio.wav>;
                 size=424252;duration=2543
   Content-Type:application/nlsml+xml
   Content-Length:...
   <?xml version="1.0"?>
   <result xmlns="urn:ietf:params:xml:ns:mrcpv2"
           xmlns:ex="http://www.example.com/example"
           grammar="session:request1@form-level.store">
       <interpretation>
           <instance name="Person">
               <ex:Person>
                   <ex:Name> Andre Roy </ex:Name>
               </ex:Person>
           </instance>
               <input>   may I speak to Andre Roy </input>
       </interpretation>
   </result>

Recorder Resource


將收到的 audio/video 存到指定的 URI


Recorder State Machine


   Idle                   Recording
   State                  State
    |                       |
    |---------RECORD------->|
    |                       |
    |<------STOP------------|
    |                       |
    |<--RECORD-COMPLETE-----|
    |                       |
    |              |--------|
    |       START-OF-INPUT  |
    |              |------->|
    |                       |
    |              |--------|
    |    START-INPUT-TIMERS |
    |              |------->|
    |                       |

Recorder Methods


   recorder-method      =  "RECORD"
                        /  "STOP"
                        /  "START-INPUT-TIMERS"

Recorder Events


   recorder-event       =  "START-OF-INPUT"
                        /  "RECORD-COMPLETE"

Recorder Header Fields


   recorder-header      =  sensitivity-level
                        /  no-input-timeout
                        /  completion-cause
                        /  completion-reason
                        /  failed-uri
                        /  failed-uri-cause
                        /  record-uri
                        /  media-type
                        /  max-time
                        /  trim-length
                        /  final-silence
                        /  capture-on-speech
                        /  ver-buffer-utterance
                        /  start-input-timers
                        /  new-audio-channel



example


   C->S:  MRCP/2.0 ... RECORD 543257
          Channel-Identifier:32AECB23433802@recorder
          Record-URI:<file://mediaserver/recordings/myfile.wav>
          Media-Type:audio/wav
          Capture-On-Speech:true
          Final-Silence:300
          Max-Time:6000

   S->C:  MRCP/2.0 ... 543257 200 IN-PROGRESS
          Channel-Identifier:32AECB23433802@recorder

   S->C:  MRCP/2.0 ... START-OF-INPUT 543257 IN-PROGRESS
          Channel-Identifier:32AECB23433802@recorder

   S->C:  MRCP/2.0 ... RECORD-COMPLETE 543257 COMPLETE
          Channel-Identifier:32AECB23433802@recorder
          Completion-Cause:000 success-silence
          Record-URI:<file://mediaserver/recordings/myfile.wav>;
                     size=242552;duration=25645

Speaker Verification and Identification


辨識 speaker 的身份


Speaker Verification State Machine


     Idle              Session Opened       Verifying/Training
     State             State                State
      |                   |                         |
      |--START-SESSION--->|                         |
      |                   |                         |
      |                   |----------|              |
      |                   |     START-SESSION       |
      |                   |<---------|              |
      |                   |                         |
      |<--END-SESSION-----|                         |
      |                   |                         |
      |                   |---------VERIFY--------->|
      |                   |                         |
      |                   |---VERIFY-FROM-BUFFER--->|
      |                   |                         |
      |                   |----------|              |
      |                   |  VERIFY-ROLLBACK        |
      |                   |<---------|              |
      |                   |                         |
      |                   |                |--------|
      |                   | GET-INTERMEDIATE-RESULT |
      |                   |                |------->|
      |                   |                         |
      |                   |                |--------|
      |                   |     START-INPUT-TIMERS  |
      |                   |                |------->|
      |                   |                         |
      |                   |                |--------|
      |                   |         START-OF-INPUT  |
      |                   |                |------->|
      |                   |                         |
      |                   |<-VERIFICATION-COMPLETE--|
      |                   |                         |
      |                   |<--------STOP------------|
      |                   |                         |
      |                   |----------|              |
      |                   |         STOP            |
      |                   |<---------|              |
      |                   |                         |
      |----------|        |                         |
      |         STOP      |                         |
      |<---------|        |                         |
      |                   |----------|              |
      |                   |    CLEAR-BUFFER         |
      |                   |<---------|              |
      |                   |                         |
      |----------|        |                         |
      |   CLEAR-BUFFER    |                         |
      |<---------|        |                         |
      |                   |                         |
      |                   |----------|              |
      |                   |   QUERY-VOICEPRINT      |
      |                   |<---------|              |
      |                   |                         |
      |----------|        |                         |
      | QUERY-VOICEPRINT  |                         |
      |<---------|        |                         |
      |                   |                         |
      |                   |----------|              |
      |                   |  DELETE-VOICEPRINT      |
      |                   |<---------|              |
      |                   |                         |
      |----------|        |                         |
      | DELETE-VOICEPRINT |                         |
      |<---------|        |                         |

Speaker Verification Methods


   verifier-method          =  "START-SESSION"
                            / "END-SESSION"
                            / "QUERY-VOICEPRINT"
                            / "DELETE-VOICEPRINT"
                            / "VERIFY"
                            / "VERIFY-FROM-BUFFER"
                            / "VERIFY-ROLLBACK"
                            / "STOP"
                            / "CLEAR-BUFFER"
                            / "START-INPUT-TIMERS"
                            / "GET-INTERMEDIATE-RESULT"

Verification Events


   verifier-event       =  "VERIFICATION-COMPLETE"
                        /  "START-OF-INPUT"

Verification Header Fields


   verification-header      =  repository-uri
                            /  voiceprint-identifier
                            /  verification-mode
                            /  adapt-model
                            /  abort-model
                            /  min-verification-score
                            /  num-min-verification-phrases
                            /  num-max-verification-phrases
                            /  no-input-timeout
                            /  save-waveform
                            /  media-type
                            /  waveform-uri
                            /  voiceprint-exists
                            /  ver-buffer-utterance
                            /  input-waveform-uri
                            /  completion-cause
                            /  completion-reason
                            /  speech-complete-timeout
                            /  new-audio-channel
                            /  abort-verification
                            /  start-input-timers

References


MRCP wiki


MRCP協議學習筆記-MRCP背景知識介紹


MRCP學習筆記-語音識別資源的事件和Headers詳解


MRCP協議學習筆記-語音識別資源的概括和全部Methods


MRCP協議學習筆記-關於媒體資源伺服器的定位路由策略


MRCPv2概述


MRCPv2在電信智能語音識別業務中的應用


MRCPv2 - Speech Synthesizer Resource


MRCP v2.0 規範 - RFC6787中文翻譯(1)


cisco 使用MRCPv1 ASR/TTS的IOS語音XML網關到CVP呼叫流