貝殼音頻流網關的設計、演進及使用場景

介紹

隨着實時音視頻技術的發展,音視頻聊天已經走入尋常百姓家,微信 2017 年每天有 4.1 億次的音視頻通話。貝殼找房 IM 作爲線上商機來源,也實現了該功能,作爲經紀人和客戶溝通的便捷工具。除了 IM 音視頻通話,實時音視頻技術在貝殼還用到了直播自動審覈、經紀人講盤訓練等各種場景,這些場景中都用到了實時音頻流網關。以經紀人講盤訓練爲例,通過構造一個虛擬人進入音頻房間,可以和經紀人進行實時問答,模擬買房和講盤,提高經紀人作業效率。要達到該效果,虛擬人需要能夠識別經紀人講話並且進行回答或者提問。經紀人講盤訓練整個業務架構如下所示:

圖片

其中,自動語音識別技術 ASR(Automatic Speech Recognition),是一種將人的語音轉換爲文本的技術,反之,TTS(Text To Speech),可以將文本轉換爲語音。實時音視頻房間可以使用騰訊雲或者聲網等公司的技術,或者使用 webRTC 進行自研,本文重點關注圖中標色部分的貝殼音頻流網關,其功能爲將經紀人的語音推送到後端 ASR 服務,進行識別和分析,之後通過 TTS 生成一個回答或者提問並通過音頻流網關將語音回傳給經紀人。本文主要介紹音頻流網關的架構、演進、穩定性建設及使用場景。

音頻基礎知識

聲音是振動產生的聲波,經過空氣、固體、液體這些介質傳播並被人或動物聽覺器官所感知的波動現象。音頻信號採集需要將聲音通過麥克風等轉變爲模擬信號,然後對模擬信號進行抽樣、量化和編碼轉換成離散的數字信號。PCM(Pulse Code Modulation)就是一種將模擬信號數字化的方法,一般也用來表示未經過封裝的音頻原始文件。模數轉換過程中涉及三個基本概念:採樣位深、採樣率和通道數。

音頻採樣過程中持續採樣時間稱爲幀長,可以使用 20ms,也可以使用 200ms,時間越短延時越小。假設一次採樣,採樣位深是 16bit,採樣率爲 16kHz,單通道,幀長爲 20ms,使用 PCM,則每幀的大小爲:

幀大小 = 位深 * 採樣率 * 幀長 * 通道/ (1000*8)
      = 16 * 16000 * 20 * 1 /(1000*8)
      = 640字節

貝殼音頻流網關處理的每幀大小就是 640 字節。

架構介紹

貝殼音頻流網關使用實時音視頻雲平臺提供的服務端 SDK 進入房間、退出房間以及在房間內推拉流,除 SDK 功能外,主要需要解決如下問題:

本文逐個解答這些問題。

架構圖

圖片

整體架構爲 Dispatcher-Worker 模式,Dispatcher 通過對外暴露 http 接口管理任務,控制 Worker 啓動停止,以及與 ASR、TTS 服務交互;每個 Worker 都是獨立的進程,調用實時音視頻雲平臺的服務端 SDK 進入房間進行推流和拉流。Dispatcher 與 Worker 使用雙向管道(PIPE)進行通信,例如推拉流的交互,控制信息的交互。貝殼音頻流網關使用 Go 代碼實現,Go 標準庫的 os/exec 包可以直接調起一個外部程序執行,並且生成和外部程序的雙向管道進行通信,os/exec 包的關鍵結構體及方法如下:

type Cmd struct {

    Path string // 程序名稱

    Args []string // 程序參數

    Env []string // 程序環境變量

    Dir string // 程序路徑

    Stdin io.Reader // 程序標準輸入

    Stdout io.Writer // 程序標準輸出

    Stderr io.Writer // 程序標準錯誤

    ...

    Process *os.Process //生成的進程

    ...

}

func Command(name string, arg ...string) *Cmd // 返回一個Cmd結構體

func (c *Cmd) Start() error // 啓動一個新的進程

...

func (c *Cmd) StdinPipe() (io.WriteCloser, error) // 返回一個pipe,該pipe和新進程的標準輸入進行連接

func (c *Cmd) StdoutPipe() (io.ReadCloser, error) // 返回一個pipe,該pipe和新進程的標準輸出進行連接

...

func (c *Cmd) Wait() error // 等待新進程退出

...

Cmd 結構體中的 Process 成員變量代表新生成的進程,通過向 Process 發送信號可以控制 Worker 進程的退出

cmd.Process.Signal(syscall.SIGTERM)

拉流

進入房間

音頻流網關何時生成一個 Worker 進程進入房間取決於具體的業務場景,例如在經紀人訓練場中,當經紀人點擊開始訓練之後需要將虛擬人拉入房間,進行交互。Dispatcher 通過提供進房接口,由業務方根據業務場景靈活的控制開始拉流的時機,接口及返回值信息如下:

接口:POST  /task HTTP/1.1

返回值:

{

    "errno": 0,

    "errmsg""success",

    "data"{

        "address""10.x.x.x:8888"

    }
}

可以看到,返回值中還包含了處理該次請求的機器 IP + 端口,這是爲了能夠橫向擴展做的一處優化,跟推流有關係,下文詳細描述

流傳輸

業務方調用進房接口後,Dispatcher 會生成一個 Worker 進入房間開始拉取房間音頻流,之後通過管道傳輸給 Dispatcher,Dispatcher 再傳輸給 ASR 服務。在我們的業務場景中,音頻流每幀是 20ms,16KHz 的採樣率,16bit 的位深,單聲道 PCM 格式,因此每幀大小爲 640 字節,我們做個簡單的計算,假設每個房間中有一個經紀人,同時在線房間數爲 5000(即有 5000 個經紀人同時在線訓練),則每秒鐘的 QPS 爲(1000ms/20ms)*5000 = 250000。爲了提高效率,音頻流網關會默認累積 10 幀(200ms)之後傳輸給 ASR 服務。此時示例中的 QPS 相應降爲 250000/10 = 25000。示意圖如下:

圖片

從圖中還可以看出兩點信息:

流重傳

ASR 服務在某些場景丟包之後無法繼續準確識別。因此將包傳輸到 ASR 服務時,除了正常的重試策略,還會在音頻網關保留最近的 30 個包。當 ASR 服務長時間未收到一個包時,可以將該包的 Sequence 返回到音頻流網關,網關啓動時會開啓一定數量的 goroutine,這些 goroutine 收到重傳信息後會從保留包中查找並重傳。

退出房間

退出房間有兩種機制:

推流

推流也通過提供一個接口實現,如下:

POST  /upstream HTTP/1.1

通過 TTS 生成語音之後,需要以 16bit 位深、16kHz 的採樣率、單聲道 PCM 格式上傳,同時需要指定對應的實時音視頻房間號。上傳成功之後音頻流網關啓動一個 20ms 間隔的 Ticker(Ticker 是 Go 語言中的一個標準庫,經過一個固定時間間隔之後執行某項任務的組件),代碼示例如下:

delayTicker := time.NewTicker(20 * time.Millisecond)

defer delayTicker.Stop()

count := len(alls) //alls中保存待推送的音頻流

var err error

for i := 0; i < count; i++ {
   ...
   <-delayTicker.C // 每20ms會返回一次

   err = dispatcher.SendToTrtc(alls[i], e)// 發送音頻流
   ...
}

間隔 20ms 發送一個音頻包,直到發送完畢

如何找到推流機器

上文講述拉流時,可以看到每個 Worker 會進入一個實時音視頻的房間,而 Worker 是跟具體的機器綁定的有狀態服務。那麼推流時如何找到某個房間號對應的 Worker 屬於哪臺機器呢?

有兩種方法,一是調用 task 接口進入房間時會返回該房間對應的機器 IP+Port,業務方可以記錄該返回信息,推流時直接使用。

第二種方法是音頻流網關提供了一個查詢接口,業務方可以通過查詢接口得到房間號對應的機器 IP+PORT。

演進及優化

心跳機制

第一版上線後發現虛擬人推流時,經紀人端的播放會出現缺失開頭幾個字的吞音現象。查看實時音視頻通道側的用戶事件,發現推流之前會發生經紀人退出房間的現象,原因爲經紀人側認爲房間內已沒有其他人員,因此主動退房。改進機制爲每秒鐘發送一個靜音幀作爲心跳。

內存分配

上文提到每個音頻幀是 640 字節,如果每次我們都直接新生成一個代表音頻幀的結構體,則內存分配與 GC 耗時都會成爲瓶頸,因此需要提前生成一批音頻幀結構體進行復用,放入一個池子,使用時取出,使用完畢後放回。在 Go 語言中,很自然會想到 sync.Pool 標準包。

type Pool struct {
    New func() interface{}
}

func (p *Pool) Get() interface{}

func (p *Pool) Put(x interface{})

sync.Pool 包中只有簡單的 Get 與 Put 兩個方法,用來獲取和放回一個結構體;New 成員是一個函數,作用爲生成一個新的結構體。

高可用建設

音頻流網關的高可用建設需要考慮如下幾個方面:

限流

圖片

通過使用 Sentinel,配合配置中心下發限流開關以及限流值,達到接口維度的限流

單機容量

音頻流網關爲計算密集型服務,主要資源瓶頸爲 CPU,經過測試單 Worker CPU 使用率爲 5%,因此單 CPU 支持的 Worker 個數爲 20 個。因此根據服務器總的 CPU 核心個數,計算 Worker 的容量值,通過配置中心下發進行限制。

容量狀態

通過提供一個接口,可以觀察音頻流網關當前容量狀態,並且業務方可以據此決定是否需要進行業務側限流,接口如下:

GET /current/workers
{

    "errno":0,

    "errmsg":"success",

    "data":{

        "current_nums":807,

        "max_total_nums":1620
     }
}

上述示例表明當前總的 Worker 個數爲 807,音頻流網關可承載的總的 Worker 個數爲 1620,即使用了接近 50% 的容量

場景

音頻流網關主要用到如下 5 種場景:

  1. 經紀人講盤訓練:將經紀人的語音推送到後端 ASR 服務,進行識別和分析,之後通過 TTS 生成一個回答或者提問並通過音頻流網關將語音回傳給經紀人。

  2. 錄製轉碼:拉取音頻流之後可以分別按經紀人和客戶錄製爲單流音頻,也可以將多路單流合併後錄製混流音頻,轉碼之後上傳存儲進行音頻分析和音頻回

  3. 經紀人提詞器:通過拉取音頻流轉碼爲文本並進行分析,可以生成對應的答案給到經紀人端

  4. 直播審覈:傳統方式需要審覈人員進入直播間全程觀看,通過音頻流網關可以將多個直播間音頻拉取到 ASR 服務轉換爲文本並通過九宮格方式展示,提高審覈效率

  5. 網絡語音播報:使用 TTS 服務生成語音後向音頻流網關推流並接通經紀人 APP,可以進行網絡語音播報,例如通知類消息或者提醒類消息

結論

從文字、書籍到電話、短信,進一步到 IM、語音通話、視頻通話,技術的進步使得人與人間的溝通交流更加方便。對企業來說,通過技術賦能,可以讓服務者提供更好的服務,客戶擁有更好的體驗,技術的魅力就在於讓人類生活的更美好吧。

本文由 Readfog 進行 AMP 轉碼,版權歸原作者所有。
來源https://mp.weixin.qq.com/s/DEeJMGexYNRFSXux59JMhw