實(shí)時(shí)Android語音對(duì)講系統(tǒng)架構(gòu)

本文屬于Android局域網(wǎng)內(nèi)的語音對(duì)講項(xiàng)目系列斧散,《通過UDP廣播實(shí)現(xiàn)Android局域網(wǎng)Peer Discovering》實(shí)現(xiàn)了局域網(wǎng)內(nèi)的廣播及多播通信稽物,本文將重點(diǎn)說明系統(tǒng)架構(gòu)趣席,音頻信號(hào)的實(shí)時(shí)錄制泊柬、播放及編解碼相關(guān)技術(shù)波附。

本文主要包含以下內(nèi)容:

  1. AudioRecord仅醇、AudioTrack
  2. Speex編解碼
  3. Android語音對(duì)講系統(tǒng)架構(gòu)

一冗美、AudioRecord、AudioTrack

AudioRecorder和AudioTracker是Android中獲取實(shí)時(shí)音頻數(shù)據(jù)的接口析二。在網(wǎng)絡(luò)電話粉洼、語音對(duì)講等場(chǎng)景中,由于實(shí)時(shí)性的要求叶摄,不能采用文件傳輸属韧,因此,MediaRecorder和MediaPlayer就無法使用蛤吓。

AudioRecorder和AudioTracker是Android在Java層對(duì)libmedia庫的封裝宵喂,所以效率較高,適合于實(shí)時(shí)語音相關(guān)處理的應(yīng)用会傲。在使用時(shí)锅棕,AudioRecorder和AudioTracker的構(gòu)造器方法入?yún)⑤^多拙泽,這里對(duì)其進(jìn)行詳細(xì)的解釋。

AudioRecord

public AudioRecord(int audioSource, int sampleRateInHz, int channelConfig, int audioFormat, int bufferSizeInBytes)

其中裸燎,audioSource表示錄音來源顾瞻,在AudioSource中列舉了不同的音頻來源,包括:

AudioSource.DEFAULT:默認(rèn)音頻來源
AudioSource.MIC:麥克風(fēng)(常用)
AudioSource.VOICE_UPLINK:電話上行
AudioSource.VOICE_DOWNLINK:電話下行
AudioSource.VOICE_CALL:電話德绿、含上下行
AudioSource.CAMCORDER:攝像頭旁的麥克風(fēng)
AudioSource.VOICE_RECOGNITION:語音識(shí)別
AudioSource.VOICE_COMMUNICATION:語音通信

這里比較常用的有MIC荷荤,VOICE_COMMUNICATIONVOICE_CALL

sampleRateInHz表示采樣頻率移稳。音頻的采集過程要經(jīng)過抽樣梅猿、量化編碼三步。抽樣需要關(guān)注抽樣率秒裕。聲音是機(jī)械波袱蚓,其特征主要包括頻率和振幅(即音調(diào)和音量),頻率對(duì)應(yīng)時(shí)間軸線几蜻,振幅對(duì)應(yīng)電平軸線喇潘。采樣是指間隔固定的時(shí)間對(duì)波形進(jìn)行一次記錄,采樣率就是在1秒內(nèi)采集樣本的次數(shù)梭稚。量化過程就是用數(shù)字表示振幅的過程颖低。編碼是一個(gè)減少信息量的過程,任何數(shù)字音頻編碼方案都是有損的弧烤。PCM編碼(脈沖編碼調(diào)制)是一種保真水平較高的編碼方式忱屑。在Android平臺(tái),44100Hz是唯一目前所有設(shè)備都保證支持的采樣頻率暇昂。但比如22050莺戒、16000、11025也在大多數(shù)設(shè)備上得到支持急波。8000是針對(duì)某些低質(zhì)量的音頻通信使用的从铲。

channelConfig表示音頻通道,即選擇單聲道澄暮、雙聲道等參數(shù)名段。系統(tǒng)提供的選擇如下:

public static final int CHANNEL_IN_DEFAULT = 1;
// These directly match native
public static final int CHANNEL_IN_LEFT = 0x4;
public static final int CHANNEL_IN_RIGHT = 0x8;
public static final int CHANNEL_IN_FRONT = 0x10;
public static final int CHANNEL_IN_BACK = 0x20;
public static final int CHANNEL_IN_LEFT_PROCESSED = 0x40;
public static final int CHANNEL_IN_RIGHT_PROCESSED = 0x80;
public static final int CHANNEL_IN_FRONT_PROCESSED = 0x100;
public static final int CHANNEL_IN_BACK_PROCESSED = 0x200;
public static final int CHANNEL_IN_PRESSURE = 0x400;
public static final int CHANNEL_IN_X_AXIS = 0x800;
public static final int CHANNEL_IN_Y_AXIS = 0x1000;
public static final int CHANNEL_IN_Z_AXIS = 0x2000;
public static final int CHANNEL_IN_VOICE_UPLINK = 0x4000;
public static final int CHANNEL_IN_VOICE_DNLINK = 0x8000;
public static final int CHANNEL_IN_MONO = CHANNEL_IN_FRONT;
public static final int CHANNEL_IN_STEREO = (CHANNEL_IN_LEFT | CHANNEL_IN_RIGHT);

常用的是CHANNEL_IN_MONOCHANNEL_IN_STEREO分別表示單通道輸入和左右兩通道輸入。

audioFormat指定返回音頻數(shù)據(jù)的格式泣懊,常見的選擇包括ENCODING_PCM_16BIT伸辟、ENCODING_PCM_8BITENCODING_PCM_FLOATENCODING_PCM_16BIT表示PCM 16bits每個(gè)樣本馍刮,所有設(shè)備保證支持信夫。ENCODING_PCM_8BIT自然表示PCM 8bits每個(gè)樣本。ENCODING_PCM_FLOAT表示一個(gè)單精度浮點(diǎn)數(shù)表示一個(gè)樣本。

bufferSizeInBytes表示錄音時(shí)音頻數(shù)據(jù)寫入的buffer的大小忙迁。這個(gè)數(shù)值是通過另一個(gè)方法來獲取的:getMinBufferSizegetMinBufferSize是AudioRecord類的靜態(tài)方法碎乃,返回值就是bufferSizeInBytes姊扔。這里我們來看下它的入?yún)ⅲ?/p>

static public int getMinBufferSize(int sampleRateInHz, int channelConfig, int audioFormat)

sampleRateInHz, channelConfig, audioFormat三個(gè)參數(shù)與上面的含義完全一樣,代表錄音的采樣率梅誓、通道以及數(shù)據(jù)輸出的格式恰梢。綜上,AudioRecord的初始化方法如下:

// 獲取音頻數(shù)據(jù)緩沖段大小
inAudioBufferSize = AudioRecord.getMinBufferSize(
        Constants.sampleRateInHz, Constants.inputChannelConfig, Constants.audioFormat);
// 初始化音頻錄制
audioRecord = new AudioRecord(Constants.audioSource,
        Constants.sampleRateInHz, Constants.inputChannelConfig, Constants.audioFormat, inAudioBufferSize);

其中梗掰,參數(shù)設(shè)置如下:

// 采樣頻率嵌言,44100保證兼容性
public static final int sampleRateInHz = 44100;
// 音頻數(shù)據(jù)格式:PCM 16位每個(gè)樣本,保證設(shè)備支持及穗。
public static final int audioFormat = AudioFormat.ENCODING_PCM_16BIT;

// 音頻獲取源
public static final int audioSource = MediaRecorder.AudioSource.MIC;
// 輸入單聲道
public static final int inputChannelConfig = AudioFormat.CHANNEL_IN_MONO;

AudioTrack

public AudioTrack(int streamType, int sampleRateInHz, int channelConfig, int audioFormat,
        int bufferSizeInBytes, int mode) throws IllegalArgumentException {
    this(streamType, sampleRateInHz, channelConfig, audioFormat,
            bufferSizeInBytes, mode, AudioManager.AUDIO_SESSION_ID_GENERATE);
}

與AudioRecord類似摧茴,AudioTrack的構(gòu)造器方法依然有很多需要選擇的參數(shù)距境。其中鞭衩,streamType表示音頻流播放類型,AudioManager中列出了可選的類型如下:

/** The audio stream for phone calls */
public static final int STREAM_VOICE_CALL = AudioSystem.STREAM_VOICE_CALL;
/** The audio stream for system sounds */
public static final int STREAM_SYSTEM = AudioSystem.STREAM_SYSTEM;
/** The audio stream for the phone ring */
public static final int STREAM_RING = AudioSystem.STREAM_RING;
/** The audio stream for music playback */
public static final int STREAM_MUSIC = AudioSystem.STREAM_MUSIC;
/** The audio stream for alarms */
public static final int STREAM_ALARM = AudioSystem.STREAM_ALARM;
/** The audio stream for notifications */
public static final int STREAM_NOTIFICATION = AudioSystem.STREAM_NOTIFICATION;
/** @hide The audio stream for phone calls when connected to bluetooth */
public static final int STREAM_BLUETOOTH_SCO = AudioSystem.STREAM_BLUETOOTH_SCO;
/** @hide The audio stream for enforced system sounds in certain countries (e.g camera in Japan) */
public static final int STREAM_SYSTEM_ENFORCED = AudioSystem.STREAM_SYSTEM_ENFORCED;
/** The audio stream for DTMF Tones */
public static final int STREAM_DTMF = AudioSystem.STREAM_DTMF;
/** @hide The audio stream for text to speech (TTS) */
public static final int STREAM_TTS = AudioSystem.STREAM_TTS;

常用的有STREAM_VOICE_CALL退客,STREAM_MUSIC等焚虱,需要根據(jù)應(yīng)用特點(diǎn)進(jìn)行選擇购裙。

sampleRateInHzaudioFormat需與AudioRecord中的參數(shù)保持一致,這里不再介紹鹃栽。

channelConfig與AudioRecord中的參數(shù)保持對(duì)應(yīng)躏率,比如AudioRecord選擇了AudioFormat.CHANNEL_IN_MONO(單通道音頻輸入),這里需要選擇AudioFormat.CHANNEL_OUT_MONO(單通道音頻輸出)民鼓。

bufferSizeInBytes表述音頻播放緩沖區(qū)大小薇芝,同樣,也需要根據(jù)AudioTrack的靜態(tài)方法getMinBufferSize來獲取丰嘉。

static public int getMinBufferSize(int sampleRateInHz, int channelConfig, int audioFormat) 

sampleRateInHz, channelConfig, audioFormat三個(gè)參數(shù)與上面的含義完全一樣恩掷,代表輸出音頻的采樣率、通道以及數(shù)據(jù)輸出的格式供嚎。

最后說明下modeAudioManager.AUDIO_SESSION_ID_GENERATE黄娘。mode代表音頻輸出的模式:MODE_STATICMODE_STREAM,分別表示靜態(tài)模式和流模式克滴。AudioManager.AUDIO_SESSION_ID_GENERATE表示AudioSessionId逼争,即AudioTrack依附到哪個(gè)音頻會(huì)話。

比如劝赔,要給AudioRecord添加回聲消除AcousticEchoCanceler誓焦,AcousticEchoCanceler的構(gòu)建方法create的入?yún)⒕褪莝essionId,通過AudioRecord實(shí)例的getAudioSessionId()方法獲取。

綜上杂伟,AudioTrack的初始化方法如下:

public Tracker() {
    // 獲取音頻數(shù)據(jù)緩沖段大小
    outAudioBufferSize = AudioTrack.getMinBufferSize(
            Constants.sampleRateInHz, Constants.outputChannelConfig, Constants.audioFormat);
    // 初始化音頻播放
    audioTrack = new AudioTrack(Constants.streamType,
            Constants.sampleRateInHz, Constants.outputChannelConfig, Constants.audioFormat,
            outAudioBufferSize, Constants.trackMode);
}

其中移层,參數(shù)設(shè)置如下:

// 音頻播放端
public static final int streamType = AudioManager.STREAM_VOICE_CALL;
// 輸出單聲道
public static final int outputChannelConfig = AudioFormat.CHANNEL_OUT_MONO;
// 音頻輸出模式
public static final int trackMode = AudioTrack.MODE_STREAM;

二、Speex編解碼

Speex是一個(gè)聲音編碼格式赫粥,目標(biāo)是用于網(wǎng)絡(luò)電話观话、線上廣播使用的語音編碼,基于CELP(一種語音編碼算法)開發(fā)越平,Speex宣稱可以免費(fèi)使用频蛔,以BSD授權(quán)條款開放源代碼。

Speex是由C語言開發(fā)的音頻處理庫秦叛,在Android中使用晦溪,需要通過JNI來調(diào)用。因此挣跋,對(duì)NDK開發(fā)不熟悉的朋友三圆,可以先了解下文檔:向您的項(xiàng)目添加 C 和 C++ 代碼

在Android Studio中使用C/C++庫有兩種方式:cmake和ndk-build避咆。cmake是最新支持的方法嫌术,通過配置CMakeLists.txt文件來實(shí)現(xiàn);ndk-build是傳統(tǒng)的方式牌借,通過配置Android.mk文件來實(shí)現(xiàn)度气。具體語法參考相關(guān)文檔,這里不做深入介紹膨报。配置完上述文件之后磷籍,需要將Gradle關(guān)聯(lián)到原生庫,通過AS的Link C++ Project with Gradle功能實(shí)現(xiàn)现柠。

完成上述配置之后院领,正式開始在Android中使用Speex進(jìn)行音頻編解碼。主要包括以下步驟:

  1. 下載Speex源碼够吩。推薦使用Speex 1.2.0穩(wěn)定版比然,由于目前Speex 已不再繼續(xù)維護(hù),官方建議使用Opus周循。但在某些場(chǎng)合强法,使用Speex已然足夠滿足需求。
    Speex源碼
  2. src/main下創(chuàng)建jni文件夾湾笛,將上述Speex源碼中includelibspeex文件夾拷貝到jni文件夾下饮怯。
  3. 編寫Android.mk文件和Application.mk文件。
    Android.mk
LOCAL_PATH := $(call my-dir)
include $(CLEAR_VARS)
LOCAL_LDLIBS :=-llog
LOCAL_MODULE    := libspeex
LOCAL_CFLAGS = -DFIXED_POINT -DUSE_KISS_FFT -DEXPORT="" -UHAVE_CONFIG_H
LOCAL_C_INCLUDES := $(LOCAL_PATH)/include
LOCAL_SRC_FILES := speex_jni.cpp \
        ./libspeex/bits.c \
        ./libspeex/cb_search.c \
        ./libspeex/exc_10_16_table.c \
        ./libspeex/exc_10_32_table.c \
        ./libspeex/exc_20_32_table.c \
        ./libspeex/exc_5_256_table.c \
        ./libspeex/exc_5_64_table.c \
        ./libspeex/exc_8_128_table.c \
        ./libspeex/filters.c \
        ./libspeex/gain_table_lbr.c \
        ./libspeex/gain_table.c \
        ./libspeex/hexc_10_32_table.c \
        ./libspeex/hexc_table.c \
        ./libspeex/high_lsp_tables.c \
        ./libspeex/kiss_fft.c \
        ./libspeex/kiss_fftr.c \
        ./libspeex/lpc.c \
        ./libspeex/lsp_tables_nb.c \
        ./libspeex/lsp.c \
        ./libspeex/ltp.c \
        ./libspeex/modes_wb.c \
        ./libspeex/modes.c \
        ./libspeex/nb_celp.c \
        ./libspeex/quant_lsp.c \
        ./libspeex/sb_celp.c \
        ./libspeex/smallft.c \
        ./libspeex/speex_callbacks.c \
        ./libspeex/speex_header.c \
        ./libspeex/speex.c \
        ./libspeex/stereo.c \
        ./libspeex/vbr.c \
        ./libspeex/vorbis_psy.c \
        ./libspeex/vq.c \
        ./libspeex/window.c \
include $(BUILD_SHARED_LIBRARY)

Application.mk

APP_ABI := armeabi armeabi-v7a
  1. 新建speex_config_types.h文件嚎研。在jnispeex源碼目錄下的include/speex文件夾下蓖墅,有一個(gè)speex_config_types.h.in文件,在include/speex目錄下創(chuàng)建speex_config_types.h,把speex_config_types.h.in的內(nèi)容拷貝過來论矾,然后把@SIZE16@改成short,把@SIZE32@改成int教翩,對(duì)應(yīng)Java數(shù)據(jù)類型。這個(gè)文件的內(nèi)容如下:
#ifndef __SPEEX_TYPES_H__
#define __SPEEX_TYPES_H__
typedef short spx_int16_t;
typedef unsigned short spx_uint16_t;
typedef int spx_int32_t;
typedef unsigned int spx_uint32_t;
#endif
  1. 在Java層定義編解碼需要的接口贪壳。
public class Speex {
    static {
        try {
            System.loadLibrary("speex");
        } catch (Throwable e) {
            e.printStackTrace();
        }
    }
    public native int open(int compression);
    public native int getFrameSize();
    public native int decode(byte encoded[], short lin[], int size);
    public native int encode(short lin[], int offset, byte encoded[], int size);
    public native void close();
}
  1. 在C層實(shí)現(xiàn)上述方法(以encode為例)饱亿。
extern "C"
JNIEXPORT jint JNICALL Java_com_jd_wly_intercom_audio_Speex_encode
    (JNIEnv *env, jobject obj, jshortArray lin, jint offset, jbyteArray encoded, jint size) {

    jshort buffer[enc_frame_size];
    jbyte output_buffer[enc_frame_size];
    int nsamples = (size-1)/enc_frame_size + 1;
    int i, tot_bytes = 0;

    if (!codec_open)
        return 0;

    speex_bits_reset(&ebits);

    for (i = 0; i < nsamples; i++) {
        env->GetShortArrayRegion(lin, offset + i*enc_frame_size, enc_frame_size, buffer);
        speex_encode_int(enc_state, buffer, &ebits);
    }

    tot_bytes = speex_bits_write(&ebits, (char *)output_buffer, enc_frame_size);
    env->SetByteArrayRegion(encoded, 0, tot_bytes, output_buffer);

    return (jint)tot_bytes;
}
  1. 命令行到Android.mk文件夾下,執(zhí)行命令ndk-build
D:\dev\study\intercom\WlyIntercom\app\src\main\jni>ndk-build
[armeabi] Compile++ thumb: speex <= speex_jni.cpp
[armeabi] Compile thumb  : speex <= bits.c
[armeabi] Compile thumb  : speex <= cb_search.c
[armeabi] Compile thumb  : speex <= exc_10_16_table.c
[armeabi] Compile thumb  : speex <= exc_10_32_table.c
[armeabi] Compile thumb  : speex <= exc_20_32_table.c
[armeabi] Compile thumb  : speex <= exc_5_256_table.c
[armeabi] Compile thumb  : speex <= exc_5_64_table.c
[armeabi] Compile thumb  : speex <= exc_8_128_table.c
[armeabi] Compile thumb  : speex <= filters.c
[armeabi] Compile thumb  : speex <= gain_table_lbr.c
[armeabi] Compile thumb  : speex <= gain_table.c
[armeabi] Compile thumb  : speex <= hexc_10_32_table.c
[armeabi] Compile thumb  : speex <= hexc_table.c
[armeabi] Compile thumb  : speex <= high_lsp_tables.c
[armeabi] Compile thumb  : speex <= kiss_fft.c
[armeabi] Compile thumb  : speex <= kiss_fftr.c
[armeabi] Compile thumb  : speex <= lpc.c
[armeabi] Compile thumb  : speex <= lsp_tables_nb.c
[armeabi] Compile thumb  : speex <= lsp.c
[armeabi] Compile thumb  : speex <= ltp.c
[armeabi] Compile thumb  : speex <= modes_wb.c
[armeabi] Compile thumb  : speex <= modes.c
[armeabi] Compile thumb  : speex <= nb_celp.c
[armeabi] Compile thumb  : speex <= quant_lsp.c
[armeabi] Compile thumb  : speex <= sb_celp.c
[armeabi] Compile thumb  : speex <= smallft.c
[armeabi] Compile thumb  : speex <= speex_callbacks.c
[armeabi] Compile thumb  : speex <= speex_header.c
[armeabi] Compile thumb  : speex <= speex.c
[armeabi] Compile thumb  : speex <= stereo.c
[armeabi] Compile thumb  : speex <= vbr.c
[armeabi] Compile thumb  : speex <= vorbis_psy.c
[armeabi] Compile thumb  : speex <= vq.c
[armeabi] Compile thumb  : speex <= window.c
[armeabi] StaticLibrary  : libstdc++.a
[armeabi] SharedLibrary  : libspeex.so
[armeabi] Install        : libspeex.so => libs/armeabi/libspeex.so

生成libs/armeabi/libspeex.so和對(duì)應(yīng)的obj文件寥袭,如需單獨(dú)使用,將上述過程生成的*.so包拷貝至jniLibs文件夾中关霸。

  1. 最后传黄,在Android中通過Java去調(diào)用encode方法進(jìn)行音頻數(shù)據(jù)的編碼。
/**
 * 將raw原始音頻文件編碼為Speex格式
 *
 * @param audioData 原始音頻數(shù)據(jù)
 * @return 編碼后的數(shù)據(jù)
 */
public static byte[] raw2spx(short[] audioData) {
    // 原始數(shù)據(jù)中包含的整數(shù)個(gè)encFrameSize
    int nSamples = audioData.length / encFrameSize;
    byte[] encodedData = new byte[((audioData.length - 1) / encFrameSize + 1) * encodedFrameSize];
    short[] rawByte;
    // 將原數(shù)據(jù)轉(zhuǎn)換成spx壓縮的文件
    byte[] encodingData = new byte[encFrameSize];
    int readTotal = 0;
    for (int i = 0; i < nSamples; i++) {
        rawByte = new short[encFrameSize];
        System.arraycopy(audioData, i * encFrameSize, rawByte, 0, encFrameSize);
        int encodeSize = Speex.getInstance().encode(rawByte, 0, encodingData, rawByte.length);
        System.arraycopy(encodingData, 0, encodedData, readTotal, encodeSize);
        readTotal += encodeSize;
    }
    rawByte = new short[encFrameSize];
    System.arraycopy(audioData, nSamples * encFrameSize, rawByte, 0, audioData.length - nSamples * encFrameSize);
    int encodeSize = Speex.getInstance().encode(rawByte, 0, encodingData, rawByte.length);
    System.arraycopy(encodingData, 0, encodedData, readTotal, encodeSize);
    return encodedData;
}

這里設(shè)置了每幀處理160個(gè)short型數(shù)據(jù)队寇,壓縮比為5膘掰,每幀輸出為28個(gè)byte型數(shù)據(jù)。Speex壓縮模式特征如下:

Speex壓縮模式特征

原文綜合考慮音頻質(zhì)量佳遣、壓縮比和算法復(fù)雜度识埋,最后選擇了Mode 5。

private static final int DEFAULT_COMPRESSION = 5;

三零渐、Android語音對(duì)講項(xiàng)目系統(tǒng)架構(gòu)

再次說明窒舟,本文實(shí)現(xiàn)參考了論文:Android real-time audio communications over local wireless,因此系統(tǒng)架構(gòu)如下圖所示:

Android對(duì)講機(jī)系統(tǒng)架構(gòu)

數(shù)據(jù)包要經(jīng)過Record诵盼、Encoder惠豺、Transmission、Decoder风宁、Play這一鏈條的處理洁墙,這種數(shù)據(jù)流轉(zhuǎn)就是對(duì)講機(jī)核心抽象。鑒于這種場(chǎng)景戒财,本文的實(shí)現(xiàn)采用了責(zé)任鏈設(shè)計(jì)模式热监。責(zé)任鏈模式屬于行為型模式,表征對(duì)對(duì)象的某種行為饮寞。

創(chuàng)建型模式孝扛,共五種:工廠方法模式、抽象工廠模式幽崩、單例模式疗琉、建造者模式、原型模式歉铝。
結(jié)構(gòu)型模式盈简,共七種:適配器模式、裝飾器模式、代理模式柠贤、外觀模式香浩、橋接模式、組合模式臼勉、享元模式邻吭。
行為型模式,共十一種:策略模式宴霸、模板方法模式囱晴、觀察者模式、迭代子模式瓢谢、責(zé)任鏈模式畸写、命令模式、備忘錄模式氓扛、狀態(tài)模式枯芬、訪問者模式、中介者模式采郎、解釋器模式千所。

責(zé)任鏈設(shè)計(jì)模式的使用場(chǎng)景:在責(zé)任鏈模式里,很多對(duì)象里由每一個(gè)對(duì)象對(duì)其下家的引用而連接起來形成一條鏈蒜埋。請(qǐng)求在這個(gè)鏈上傳遞淫痰,直到鏈上的某一個(gè)對(duì)象決定處理此請(qǐng)求。發(fā)出這個(gè)請(qǐng)求的客戶端并不知道鏈上的哪一個(gè)對(duì)象最終處理這個(gè)請(qǐng)求整份,這使得系統(tǒng)可以在不影響客戶端的情況下動(dòng)態(tài)地重新組織和分配責(zé)任黑界。下面來看下具體的代碼:

首先定義一個(gè)JobHandler,代表每個(gè)對(duì)象皂林,其中包含抽象方法handleRequest():

/**
 * 數(shù)據(jù)處理節(jié)點(diǎn)
 *
 * @param <I> 輸入數(shù)據(jù)類型
 * @param <O> 輸出數(shù)據(jù)類型
 * @author yanghao1
 */
public abstract class JobHandler<I, O> {

    private JobHandler<O, ?> nextJobHandler;

    public JobHandler<O, ?> getNextJobHandler() {
        return nextJobHandler;
    }

    public void setNextJobHandler(JobHandler<O, ?> nextJobHandler) {
        this.nextJobHandler = nextJobHandler;
    }

    public abstract void handleRequest(I audioData);

    /**
     * 釋放資源
     */
    public void free() {

    }
}

JobHandler<I, O>表示輸入數(shù)據(jù)類型為I朗鸠,輸出類型為OnextJobHandler表示下一個(gè)處理請(qǐng)求的節(jié)點(diǎn)础倍,其類型為JobHandler<O, ?>烛占,即輸入數(shù)據(jù)類型必須為上一個(gè)處理節(jié)點(diǎn)的輸出數(shù)據(jù)類型。

繼承類必須實(shí)現(xiàn)抽象方法handleRequest()沟启,參數(shù)類型為I忆家,實(shí)現(xiàn)對(duì)數(shù)據(jù)包的處理。free()方法實(shí)現(xiàn)資源的釋放德迹,繼承類可根據(jù)情況重寫該方法芽卿。這里分別定義RecorderEncoder胳搞、Sender卸例、Receiver称杨、DecoderTracker筷转,均繼承自JobHandler姑原。

RecorderEncoder呜舒、Sender為例說明輸入側(cè)數(shù)據(jù)的處理(這里僅列出部分代碼锭汛,具體代碼參考github地址):

/**
 * 音頻錄制數(shù)據(jù)格式ENCODING_PCM_16BIT,返回?cái)?shù)據(jù)類型為short[]
 *
 * @author yanghao1
 */
public class Recorder extends JobHandler<short[], short[]> {

    @Override
    public void handleRequest(short[] audioData) {
        if (audioRecord.getRecordingState() == AudioRecord.RECORDSTATE_STOPPED) {
            audioRecord.startRecording();
        }
        // 實(shí)例化音頻數(shù)據(jù)緩沖
        audioData = new short[inAudioBufferSize];
        audioRecord.read(audioData, 0, inAudioBufferSize);
        getNextJobHandler().handleRequest(audioData);
    }
}

Recorder完成音頻采集之后袭蝗,通過getNextJobHandler()方法獲取對(duì)下一個(gè)處理節(jié)點(diǎn)的引用唤殴,然后調(diào)用其方法handleRequest(),并且入?yún)㈩愋蜑?code>short[]到腥。Recorder的下一個(gè)處理節(jié)點(diǎn)是Encoder朵逝,在EncoderhandleRequest()方法中,實(shí)現(xiàn)音頻數(shù)據(jù)的編碼左电,其輸入類型為short[]廉侧,輸出為byte[]页响。

/**
 * 音頻編碼篓足,輸入類型為short[],輸出為byte[]
 *
 * @author yanghao1
 */
public class Encoder extends JobHandler<short[], byte[]> {

    @Override
    public void handleRequest(short[] audioData) {
        byte[] encodedData = AudioDataUtil.raw2spx(audioData);
        getNextJobHandler().handleRequest(encodedData);
    }
}

Encoder的下一個(gè)處理節(jié)點(diǎn)是Sender闰蚕,在SenderhandleRequest()方法中栈拖,通過多播(組播),將音頻編碼數(shù)據(jù)發(fā)送給局域網(wǎng)內(nèi)的其它設(shè)備没陡。

/**
 * UDP多播發(fā)送
 *
 * @author yanghao1
 */
public class Sender extends JobHandler<byte[], byte[]> {

    @Override
    public void handleRequest(byte[] audioData) {
        DatagramPacket datagramPacket = new DatagramPacket(
                audioData, audioData.length, inetAddress, Constants.MULTI_BROADCAST_PORT);
        try {
            multicastSocket.send(datagramPacket);
        } catch (IOException e) {
            e.printStackTrace();
        }
    }
}

最后涩哟,在AudioInput類的構(gòu)造函數(shù)中執(zhí)行對(duì)象之間的關(guān)系:

/**
 * 音頻錄制、編碼盼玄、發(fā)送線程
 *
 * @author yanghao1
 */
public class AudioInput implements Runnable {

    private Recorder recorder;
    private Encoder encoder;
    private Sender sender;
    private Handler handler;

    // 錄制狀態(tài)
    private boolean recording = false;

    public AudioInput(Handler handler) {
        this.handler = handler;
        initJobHandler();
    }

    /**
     * 初始化錄制贴彼、編碼、發(fā)送埃儿,并指定關(guān)聯(lián)
     */
    private void initJobHandler() {
        recorder = new Recorder();
        encoder = new Encoder();
        sender = new Sender(handler);
        recorder.setNextJobHandler(encoder);
        encoder.setNextJobHandler(sender);
    }
}

即:在界面初始化AudioInput對(duì)應(yīng)的線程的時(shí)候器仗,就完成這些類的實(shí)例化,并指定Recorder的下一個(gè)處理者是Encoder童番,Encoder的下一個(gè)處理者是Sender精钮。這樣使得整個(gè)處理流程非常靈活,比如剃斧,如果暫時(shí)沒有開發(fā)編解碼的過程轨香,在Encoder的handleRequest()方法中直接指定下一個(gè)處理者:

public class Encoder extends JobHandler {

    @Override
    public void handleRequest(byte[] audioData) {
        getNextJobHandler().handleRequest(audioData);
    }
}

同樣的,在初始化AudioOutput對(duì)應(yīng)的線程時(shí)幼东,完成Receiver臂容、Decoder科雳、Tracker的實(shí)例化,并且指定Receiver的下一個(gè)處理者是Decoder策橘、Decoder的下一個(gè)處理者是Tracker炸渡。

在Activity中,分別申明輸入丽已、輸出Runable蚌堵、線程池對(duì)象、界面更新Handler:

// 界面更新Handler
private AudioHandler audioHandler = new AudioHandler(this);

// 音頻輸入沛婴、輸出Runable
private AudioInput audioInput;
private AudioOutput audioOutput;

// 創(chuàng)建緩沖線程池用于錄音和接收用戶上線消息(錄音線程可能長(zhǎng)時(shí)間不用吼畏,應(yīng)該讓其超時(shí)回收)
private ExecutorService inputService = Executors.newCachedThreadPool();

// 創(chuàng)建循環(huán)任務(wù)線程用于間隔的發(fā)送上線消息,獲取局域網(wǎng)內(nèi)其他的用戶
private ScheduledExecutorService discoverService = Executors.newScheduledThreadPool(1);

// 設(shè)置音頻播放線程為守護(hù)線程
private ExecutorService outputService = Executors.newSingleThreadExecutor(new ThreadFactory() {
    @Override
    public Thread newThread(@NonNull Runnable r) {
        Thread thread = Executors.defaultThreadFactory().newThread(r);
        thread.setDaemon(true);
        return thread;
    }
});

可能有的同學(xué)會(huì)覺得這里的責(zé)任鏈設(shè)計(jì)模式用法并非真正的責(zé)任鏈嘁灯,真正的責(zé)任鏈模式要求一個(gè)具體的處理者對(duì)象只能在兩個(gè)行為中選擇一個(gè):一是承擔(dān)責(zé)任泻蚊,而是把責(zé)任推給下家。不允許出現(xiàn)某一個(gè)具體處理者對(duì)象在承擔(dān)了一部分責(zé)任后又把責(zé)任向下傳的情況丑婿。
本文中責(zé)任鏈設(shè)計(jì)模式的用法確實(shí)不是嚴(yán)格的責(zé)任鏈模式性雄,但學(xué)習(xí)的目的不就是活學(xué)活用嗎?

Android線程池

上述代碼涉及Android中的線程池羹奉,與Android線程池相關(guān)的類包括:Executor秒旋,ExecutorsExecutorService诀拭,Future迁筛,CallableThreadPoolExecutor等耕挨,為了理清它們之間的關(guān)系细卧,首先從Executor開始:

  • Executor接口中定義了一個(gè)方法 execute(Runnable command),該方法接收一個(gè) Runable實(shí)例筒占,它用來執(zhí)行一個(gè)任務(wù)贪庙,任務(wù)即一個(gè)實(shí)現(xiàn)了Runnable 接口的類。
  • ExecutorService接口繼承自Executor 接口翰苫,它提供了更豐富的實(shí)現(xiàn)多線程的方法止邮,比如,ExecutorService提供了關(guān)閉自己的方法革骨,以及可為跟蹤一個(gè)或多個(gè)異步任務(wù)執(zhí)行狀況而生成Future 的方法农尖。 可以調(diào)用ExecutorServiceshutdown()方法來平滑地關(guān)閉 ExecutorService,調(diào)用該方法后良哲,將導(dǎo)致 ExecutorService停止接受任何新的任務(wù)且等待已經(jīng)提交的任務(wù)執(zhí)行完成(已經(jīng)提交的任務(wù)會(huì)分兩類:一類是已經(jīng)在執(zhí)行的盛卡,另一類是還沒有開始執(zhí)行的),當(dāng)所有已經(jīng)提交的任務(wù)執(zhí)行完畢后將會(huì)關(guān)閉 ExecutorService筑凫。因此我們一般用該接口來實(shí)現(xiàn)和管理多線程滑沧。
  • Executors 提供了一系列工廠方法用于創(chuàng)建線程池并村,返回的線程池都實(shí)現(xiàn)了 ExecutorService接口。包括:
    • newCachedThreadPool()
      創(chuàng)建一個(gè)可緩存線程池滓技,如果線程池長(zhǎng)度超過處理需要哩牍,可靈活回收空閑線程,若無可回收令漂,則新建線程膝昆;
    • newFixedThreadPool(int)
      創(chuàng)建一個(gè)定長(zhǎng)線程池,可控制線程最大并發(fā)數(shù)叠必,超出的線程會(huì)在隊(duì)列中等待荚孵。
    • newScheduledThreadPool(int)
      創(chuàng)建一個(gè)定長(zhǎng)線程池,支持定時(shí)及周期性任務(wù)執(zhí)行纬朝。
    • newSingleThreadExecutor()
      創(chuàng)建一個(gè)單線程化的線程池收叶,它只會(huì)用唯一的工作線程來執(zhí)行任務(wù),保證所有任務(wù)按照指定順序(FIFO, LIFO, 優(yōu)先級(jí))執(zhí)行共苛。
  • Callable接口與Runnable接口類似判没,ExecutorService<T> Future<T> submit(Callable<T> task)方法接受Callable作為入?yún)ⅲ?Java 5 之后隅茎,任務(wù)分兩類:一類是實(shí)現(xiàn)了 Runnable接口的類澄峰,一類是實(shí)現(xiàn)了 Callable 接口的類。兩者都可以被 ExecutorService 執(zhí)行患膛,但是 Runnable任務(wù)沒有返回值摊阀,而 Callable任務(wù)有返回值耻蛇。并且Callablecall()方法只能通過ExecutorServicesubmit(Callable task)方法來執(zhí)行踪蹬,并且返回一個(gè) Future,是表示任務(wù)等待完成的Future臣咖。
  • ThreadPoolExecutor繼承自AbstractExecutorService跃捣,AbstractExecutorService實(shí)現(xiàn)了ExecutorService接口。ThreadPoolExecutor的構(gòu)造器由于參數(shù)較多夺蛇,不宜直接暴露給使用者愿伴。所以粘室,Executors 中定義 ExecutorService實(shí)例的工廠方法,其實(shí)是通過定義ThreadPoolExecutor不同入?yún)韺?shí)現(xiàn)的。

下面來看下ThreadPoolExecutor的構(gòu)造器方法:

public ThreadPoolExecutor(int corePoolSize, int maximumPoolSize, long keepAliveTime, TimeUnit unit,
                          BlockingQueue<Runnable> workQueue, ThreadFactory threadFactory, RejectedExecutionHandler handler) {

    if (corePoolSize < 0 || maximumPoolSize <= 0 || maximumPoolSize < corePoolSize || keepAliveTime < 0)
        throw new IllegalArgumentException();

    if (workQueue == null || threadFactory == null || handler == null)
        throw new NullPointerException();

    this.corePoolSize = corePoolSize;
    this.maximumPoolSize = maximumPoolSize;
    this.workQueue = workQueue;
    this.keepAliveTime = unit.toNanos(keepAliveTime);
    this.threadFactory = threadFactory;
    this.handler = handler;
}

其中蹦误,corePoolSize表示線程池中所保存的核心線程數(shù),包括空閑線程猾瘸;maximumPoolSize表示池中允許的最大線程數(shù)勉盅;keepAliveTime表示線程池中的空閑線程所能持續(xù)的最長(zhǎng)時(shí)間;unit表示時(shí)間的單位牺氨;workQueue表示任務(wù)執(zhí)行前保存任務(wù)的隊(duì)列狡耻,僅保存由execute 方法提交的Runnable任務(wù)墩剖;threadFactory表示線程創(chuàng)建的工廠,指定線程的特性夷狰,比如前面代碼中設(shè)置音頻播放線程為守護(hù)線程岭皂;handler表示隊(duì)列容量滿之后的處理方法。

ThreadPoolExecutor對(duì)于傳入的任務(wù)Runnable有如下處理流程:

  1. 如果線程池中的線程數(shù)量少于corePoolSize沼头,即使線程池中有空閑線程爷绘,也會(huì)創(chuàng)建一個(gè)新的線程來執(zhí)行新添加的任務(wù);
  2. 如果線程池中的線程數(shù)量大于等于corePoolSize进倍,但緩沖隊(duì)列workQueue 未滿揉阎,則將新添加的任務(wù)放到 workQueue中,按照 FIFO 的原則依次等待執(zhí)行(線程池中有線程空閑出來后依次將緩沖隊(duì)列中的任務(wù)交付給空閑的線程執(zhí)行)背捌;
  3. 如果線程池中的線程數(shù)量大于等于 corePoolSize毙籽,且緩沖隊(duì)列 workQueue 已滿,但線程池中的線程數(shù)量小于maximumPoolSize毡庆,則會(huì)創(chuàng)建新的線程來處理被添加的任務(wù)坑赡;
  4. 如果線程池中的線程數(shù)量等于了maximumPoolSize,交由RejectedExecutionHandler handler處理么抗。

ThreadPoolExecutor主要用于某些特定場(chǎng)合毅否,即上述工廠方法無法滿足的時(shí)候,自定義線程池使用蝇刀。本文使用了三種特性的線程池工廠方法:newCachedThreadPool()螟加、newScheduledThreadPool(int)newSingleThreadExecutor

首先吞琐,對(duì)于錄音線程捆探,由于對(duì)講機(jī)用戶大部分時(shí)間可能是在聽,而不是說站粟。錄音線程可能長(zhǎng)時(shí)間不用黍图,應(yīng)該讓其超時(shí)回收,所以錄音線程宜使用CachedThreadPool奴烙;
其次助被,對(duì)于發(fā)現(xiàn)局域網(wǎng)內(nèi)的其它用戶的功能,該功能需要不斷循環(huán)執(zhí)行切诀,相當(dāng)于循環(huán)的向局域網(wǎng)內(nèi)發(fā)送心跳信號(hào)揩环,因此宜使用ScheduledThreadPool
最后幅虑,對(duì)于音頻播放線程丰滑,該線程需要一直在后臺(tái)執(zhí)行,且播放需要串行執(zhí)行翘单,因此使用SingleThreadExecutor吨枉,并設(shè)置為守護(hù)線程蹦渣,在UI線程(主線程是最后一個(gè)用戶線程)結(jié)束之后結(jié)束。

// 設(shè)置音頻播放線程為守護(hù)線程
private ExecutorService outputService = Executors.newSingleThreadExecutor(new ThreadFactory() {
    @Override
    public Thread newThread(@NonNull Runnable r) {
        Thread thread = Executors.defaultThreadFactory().newThread(r);
        thread.setDaemon(true);
        return thread;
    }
});

以上貌亭。詳細(xì)代碼請(qǐng)移步github:intercom 柬唯。

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
  • 序言:七十年代末,一起剝皮案震驚了整個(gè)濱河市圃庭,隨后出現(xiàn)的幾起案子锄奢,更是在濱河造成了極大的恐慌,老刑警劉巖剧腻,帶你破解...
    沈念sama閱讀 216,997評(píng)論 6 502
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件拘央,死亡現(xiàn)場(chǎng)離奇詭異,居然都是意外死亡书在,警方通過查閱死者的電腦和手機(jī)灰伟,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 92,603評(píng)論 3 392
  • 文/潘曉璐 我一進(jìn)店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來儒旬,“玉大人栏账,你說我怎么就攤上這事≌辉矗” “怎么了挡爵?”我有些...
    開封第一講書人閱讀 163,359評(píng)論 0 353
  • 文/不壞的土叔 我叫張陵,是天一觀的道長(zhǎng)甚垦。 經(jīng)常有香客問我茶鹃,道長(zhǎng),這世上最難降的妖魔是什么艰亮? 我笑而不...
    開封第一講書人閱讀 58,309評(píng)論 1 292
  • 正文 為了忘掉前任闭翩,我火速辦了婚禮,結(jié)果婚禮上垃杖,老公的妹妹穿的比我還像新娘男杈。我一直安慰自己丈屹,他們只是感情好调俘,可當(dāng)我...
    茶點(diǎn)故事閱讀 67,346評(píng)論 6 390
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著旺垒,像睡著了一般彩库。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發(fā)上先蒋,一...
    開封第一講書人閱讀 51,258評(píng)論 1 300
  • 那天骇钦,我揣著相機(jī)與錄音,去河邊找鬼竞漾。 笑死眯搭,一個(gè)胖子當(dāng)著我的面吹牛窥翩,可吹牛的內(nèi)容都是我干的。 我是一名探鬼主播鳞仙,決...
    沈念sama閱讀 40,122評(píng)論 3 418
  • 文/蒼蘭香墨 我猛地睜開眼寇蚊,長(zhǎng)吁一口氣:“原來是場(chǎng)噩夢(mèng)啊……” “哼!你這毒婦竟也來了棍好?” 一聲冷哼從身側(cè)響起仗岸,我...
    開封第一講書人閱讀 38,970評(píng)論 0 275
  • 序言:老撾萬榮一對(duì)情侶失蹤,失蹤者是張志新(化名)和其女友劉穎借笙,沒想到半個(gè)月后扒怖,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體,經(jīng)...
    沈念sama閱讀 45,403評(píng)論 1 313
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡业稼,尸身上長(zhǎng)有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 37,596評(píng)論 3 334
  • 正文 我和宋清朗相戀三年盗痒,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片低散。...
    茶點(diǎn)故事閱讀 39,769評(píng)論 1 348
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡积糯,死狀恐怖,靈堂內(nèi)的尸體忽然破棺而出谦纱,到底是詐尸還是另有隱情看成,我是刑警寧澤,帶...
    沈念sama閱讀 35,464評(píng)論 5 344
  • 正文 年R本政府宣布跨嘉,位于F島的核電站川慌,受9級(jí)特大地震影響,放射性物質(zhì)發(fā)生泄漏祠乃。R本人自食惡果不足惜梦重,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 41,075評(píng)論 3 327
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望亮瓷。 院中可真熱鬧琴拧,春花似錦、人聲如沸嘱支。這莊子的主人今日做“春日...
    開封第一講書人閱讀 31,705評(píng)論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽除师。三九已至沛膳,卻和暖如春,著一層夾襖步出監(jiān)牢的瞬間汛聚,已是汗流浹背锹安。 一陣腳步聲響...
    開封第一講書人閱讀 32,848評(píng)論 1 269
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留,地道東北人叹哭。 一個(gè)月前我還...
    沈念sama閱讀 47,831評(píng)論 2 370
  • 正文 我出身青樓忍宋,卻偏偏與公主長(zhǎng)得像,于是被迫代替她去往敵國和親风罩。 傳聞我的和親對(duì)象是個(gè)殘疾皇子讶踪,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 44,678評(píng)論 2 354

推薦閱讀更多精彩內(nèi)容

  • Android 自定義View的各種姿勢(shì)1 Activity的顯示之ViewRootImpl詳解 Activity...
    passiontim閱讀 172,095評(píng)論 25 707
  • Spring Cloud為開發(fā)人員提供了快速構(gòu)建分布式系統(tǒng)中一些常見模式的工具(例如配置管理,服務(wù)發(fā)現(xiàn)泊交,斷路器乳讥,智...
    卡卡羅2017閱讀 134,654評(píng)論 18 139
  • 你若盛開,蝴蝶自來
    補(bǔ)釘閱讀 171評(píng)論 0 0
  • 今天,咱先講一個(gè)故事研乒。 話說汹忠,一個(gè)商人賺了一大筆錢,正騎著馬行駛在歸家的途中雹熬。離家不遠(yuǎn)了宽菜,這時(shí)他的仆人發(fā)現(xiàn)馬的后掌...
    我是銀璃閱讀 624評(píng)論 0 1