[mbedbot] 音声データをBLEでリアルタイムにPCに送る

February 05, 2017

さて、前回はコンデンサマイクを使って音声を取り込むための回路を作りましたので、次はそれをPCに送るためのプログラミングの話です。

BLEは低消費電力を重視しているため、電池のもちはいいのですが、その分通信速度には制約があります。具体的には、一回の通信で送れるのは高々20バイトです。mbedからPCに連続してNotificationを送信するテストを行って実測してみたところ、安定して1秒間に送れたのは最高でもたったの600バイト程度でした。

一方、音声データのサイズはどうかというと、例えば量子化ビット数16bit、サンプリング周波数8kHz、モノラルのPCMだと、1秒間の音声データは2byteの8000倍、つまり16kバイトになりますので、リアルタイムに送ることはまず無理です。また、mbedbotに使用しているHRM1017はRAMが16kバイトしかないので、一旦音声を録音したものを一時メモリに置いて、後からおくる、ということも不可能です。

ということで、色々と模索した結果、音声データ(mbedのAnalogInで取得)はADPCMで圧縮(16bit→3bit)し、20バイトのバッファが一杯になったらBLEのNotificationで送る、というようにしました。ただ、8kHzだと通信速度が足りないので、1,600Hzまでサンプリング周波数を下げることにしました。音声認識に必要な周波数に達していないと思いますが、これ以上の圧縮をリアルタイムに行うのは厳しそうですので、妥協しました。ADPCMを採用したのは、高速に圧縮できそうですし、固定長だからです。ADPCMのコーデック(G723)については、Sun Microsystemsのソースを使いました。mbedのコンパイラで普通にコンパイルできました(Warningはたくさんでましたが)。

プログラムとしては音声認識をクラウドで行う、という似たようなことをされているこちらを参考にさせていただき、Tickerを使って、一定期間ごとにサンプリングを行い、20バイトバッファに溜まったらBLEのNotificationで送るようなロジックで実装しました(音声に関連したソースを下に貼ってます)。データの開始と終了を表すため、送信する音声データの前後に特定の文字列を送るようにしています。ちなみに、waitForEventはTickerによる割り込み発生で抜けるそう(参考)なので、イベントループのswitchは音声の取り込み処理毎に実行される想定です。

 

volatile int vbuf_sending; // 音声送信状態(0:IDLE, 1:SENDING)
unsigned char vbuf[AUDIOCHARPAYLOAD_SIZE]; // 音声データ格納バッファ
Ticker sample_ticker; // 音声取り込み用タイマー
volatile int tick_status; // 0:Idle, 1: 音声取り込み中, 2:フッター送信完了待ち, 5:エラー発生終了, 6:正常終了, 8:フッターの送信リクエスト中, 9:音声の送信リクエスト中
int vsend_size; // 送信したサンプル数
volatile int send_wait_counter; // 送信完了待ち受け回数

void reset_vrec() {
    vbuf_sending = 0;
    vsend_size = 0;
    tick_status = 0;
}

/* 音声データを送信する */
ble_error_t notify_voice() {
    if(vbuf_sending == 0){
        ble_error_t error;
        error = ble.updateCharacteristicValue(AudioChar.getValueAttribute().getHandle(),
            (uint8_t *)&AudioCharPayload, sizeof(AudioCharPayload));
        if(error != BLE_ERROR_NONE){
            return error;
        }
        else {
            vbuf_sending = 1;
            return BLE_ERROR_NONE;
        }
    }
    else return BLE_STACK_BUSY;
}    

/* 音声取り込みステートマシン */
void onTick(void) {
    if(tick_status == 1) { // 取り込み中
        if(vsend_size >= RECORD_VOICE_SIZE){   // 録音時間に達したらフッター送信
            tick_status = 8; // フッター送信リクエスト
            send_wait_counter = 0;
        }
        else {
            int isfull;
            short sample;
            sample = (short)mic.read_u16() - 648;
            isfull = encode_to_adpcm(sample);
            vsend_size++;
            if(isfull) { 
                tick_status = 9; // 音声送信リクエスト
                send_wait_counter = 0;
            }
        }
    }
}

/* 音声送信アクションの終了処理 */
void voice_sent_finish(ble_error_t error) {
    sample_ticker.detach();
    reset_vrec();
    speak_preset_recvoice_finish(error);
    switchOff();
}

/* マイクから音声データを取得する */
void send_voice() {
    ble_error_t  error;
    
    /* スタートパケット */
    strncpy((char *)AudioCharPayload, "TOP_OF_VOICE________", sizeof(AudioCharPayload));
        
    /* START */
    init_adpcm_encoder(vbuf, sizeof(vbuf));
    error = notify_voice(); // 送信スタート
    if(error != BLE_ERROR_NONE){
        voice_sent_finish(error);
    }
    tick_status = 1; // 取り込み中
    sample_ticker.attach_us(onTick, SAMPLE_TICKER_INTERVAL);
}


/* 音声データ取得アクション */
void voice_rcv_action()
{
    switchOn();
    wait(3); // アンプが立ち上がるまで待機
    speak_preset_recvvoice();
    send_voice();
}

void DataSentCallback(unsigned count)
{
    /* 音声データ送信完了ならフラグをリセット */
    if(vbuf_sending == 1){
        if(tick_status == 2) {// フッター送信完了待ちなら終了状態
            tick_status = 6; // 正常終了
        }
        vbuf_sending = 0;
    }
}

void init()
{
    reset_vrec();
}    

int main(void)
{
    init();

    while (true) {
        ble_error_t error;
        ble.waitForEvent();
        // 音声送信アクション
        switch(tick_status) {
            case 5: // エラー発生により終了        
                voice_sent_finish(BLE_ERROR_UNSPECIFIED);
                break;
            case 6: // 正常終了
                voice_sent_finish(BLE_ERROR_NONE);
                break;
            case 8: // フッター送信リクエスト中
                if(vbuf_sending == 0) {
                    strncpy((char *)AudioCharPayload, "END_OF_VOICE________", AUDIOCHARPAYLOAD_SIZE);
                    error = notify_voice();
                    if(error != BLE_ERROR_NONE){
                        tick_status = 5;
                    }
                    else {
                        tick_status = 2; // フッター送信完了待ち
                    }
                }
                else{ // 送信完了待ち受け回数が上限に達したらエラー
                    send_wait_counter++;
                    if(send_wait_counter >= SEND_WAIT_COUNTER_LIMIT) {
                        tick_status = 5;
                    }
                }
                break;
            case 9: // 音声送信リクエスト中
                if(vbuf_sending == 0) {
                    memcpy(AudioCharPayload, vbuf, AUDIOCHARPAYLOAD_SIZE);
                    error = notify_voice();
                    if(error != BLE_ERROR_NONE){
                        tick_status = 5;
                    }
                    else {
                        tick_status = 1; // 音声取り込み中
                    }
                }
                else{ // 送信完了待ち受け回数が上限に達したらエラー
                    send_wait_counter++;
                    if(send_wait_counter >= SEND_WAIT_COUNTER_LIMIT) {
                        tick_status = 5;
                    }
                }
                break;
        }
    }
}