android:springboot后端+websocket+audioRecord+audioTrack實現(xiàn)語音聊天

需求:實現(xiàn)多人即時語音聊天室

實現(xiàn)方式:android端實現(xiàn)AudioRecord實時記錄原生pcm流的原始錄音數(shù)據(jù)战授,并通過websocket實時傳輸?shù)胶蠖搜菅担儆珊蠖藢崟r將pcm流數(shù)據(jù)分發(fā)給每個用戶,用戶接收到pcm流數(shù)據(jù)后通過AudioTrack進(jìn)行實時播放分唾。

注意事項:audioRecord的各種基礎(chǔ)配置需要和audioTrack的基礎(chǔ)配置項相對應(yīng)欲虚。

擴展:需要做聲音降噪,或者變聲處理的晚凿,可在拿到pcm錄音原始數(shù)據(jù)后進(jìn)行相關(guān)的處理

1.錄音端代碼

private Boolean doStart() {
        try {
            startRecordTime = System.currentTimeMillis();
            //創(chuàng)建MediaRecorder
            //計算AudioRecord內(nèi)部buffer最小緩沖區(qū)大小
            bufferSize = AudioRecord.getMinBufferSize(44100,
                    AudioFormat.CHANNEL_IN_MONO, AudioFormat.ENCODING_PCM_16BIT);
            audioRecord = new AudioRecord(MediaRecorder.AudioSource.MIC,
                    44100, AudioFormat.CHANNEL_IN_MONO,
                    AudioFormat.ENCODING_PCM_16BIT, Math.max(bufferSize,BUFFER_SIZE));
            //記錄開始錄音時間 用于統(tǒng)計時長 時間太短就是無效錄音
            stopRecordTime = System.currentTimeMillis();
            //開始錄音
            audioRecord.startRecording();
            //獲取錄音的二進(jìn)制數(shù)據(jù) 并使用websocket進(jìn)行數(shù)據(jù)傳輸亭罪;
            while(isStartRecord){
                if (null != audioRecord) {
                    int read = audioRecord.read(mBuffer,0,BUFFER_SIZE);
                    if (read == AudioRecord.ERROR_INVALID_OPERATION || read == AudioRecord.ERROR_BAD_VALUE) {
                        Log.d("NLPService", "Could not read audio data.");
                        break;
                    }
                    if (read != 0 && read != -1) {
                        //在此可以對錄制音頻的數(shù)據(jù)進(jìn)行二次處理 比如變聲,壓縮歼秽,降噪应役,增益等操作
                        //我們這里直接將pcm音頻原數(shù)據(jù)寫入文件 這里可以直接發(fā)送至服務(wù)器 對方采用AudioTrack進(jìn)行播放原數(shù)據(jù)
//                        dos.write(mBuffer, 0, read);
//                        dos.flush();
                        broadcastRoomService.send(ByteString.of(mBuffer));
                    } else {
                        break;
                    }
                }
            }
        }catch (RuntimeException e){
            e.printStackTrace();
            //捕獲異常避免閃退 返回false提醒用戶失敗
            return false;
        }
        return true;
    }

2.webSocketService端

public class WebSocketService extends Service {

    private static final String TAG = "websocket";
    private WebSocket webSocket;
    private WebSocketCallback webSocketCallback;
    private int reconnectTimeout = 2000;
    private boolean connected = false;
    private int limitConnect = 20;
    private int timeConnect = 0;

    private Handler handler = new Handler();

    public class LocalBinder extends Binder {
        public WebSocketService getService() {
            return WebSocketService.this;
        }
    }

    @Nullable
    @Override
    public IBinder onBind(Intent intent) {
        return new LocalBinder();
    }

    @Override
    public void onCreate() {
        super.onCreate();
        webSocket = connect();
        initConnectCount();
    }

    @Override
    public int onStartCommand(Intent intent, int flags, int startId) {
        return START_STICKY;
    }

    @Override
    public void onDestroy() {
        super.onDestroy();
//        if (webSocket != null) {
//            close();
//        }
        reconnect();
    }

    private WebSocket connect() {
        SharedPreferences userInfo = getSharedPreferences("userInfo", MODE_PRIVATE);
        String currentUserId = userInfo.getString("userId",null);
        OkHttpClient client = new OkHttpClient.Builder().build();
        Request request = new Request.Builder().url(GlobalVariable.getWsUrl()+"imserver/"+currentUserId).
                addHeader("Content-Type","application/json").build();
        return client.newWebSocket(request, new WebSocketHandler());
    }

    public void send(String text) {
        Log.d(TAG, "send " + text);
        if (webSocket != null) {
            webSocket.send(text);
        }
    }

    public void close() {
        if (webSocket != null) {
            boolean shutDownFlag = webSocket.close(1000, "manual close");
            Log.d(TAG, "shutDownFlag " + shutDownFlag);
            webSocket = null;
        }
    }

    public void initConnectCount(){
        limitConnect = 20;
        timeConnect = 0;
    }
    private void reconnect() {
        if(limitConnect>0) {
            limitConnect--;
            timeConnect++;
            handler.postDelayed(new Runnable() {
                @Override
                public void run() {
                    Log.d(TAG, "reconnect...第"+timeConnect+"次");
                    if (!connected) {
                        connect();
                    }
                }
            }, reconnectTimeout);
        }else {
            close();
        }
    }

    private class WebSocketHandler extends WebSocketListener {

        @Override
        public void onOpen(WebSocket webSocket, Response response) {
            Log.d(TAG, "onOpen");
            if (webSocketCallback != null) {
                webSocketCallback.onOpen();
            }
            connected = true;
        }

        @Override
        public void onMessage(WebSocket webSocket, String text) {
            Log.d(TAG, "onMessage " + text);
            if (webSocketCallback != null) {
                webSocketCallback.onMessage(text);
            }
        }

        @Override
        public void onClosed(WebSocket webSocket, int code, String reason) {
            Log.d(TAG, "onClosed");
            if (webSocketCallback != null) {
                webSocketCallback.onClosed();
            }
            connected = false;
            reconnect();
        }
        /**
         * Invoked when a web socket has been closed due to an error reading from or writing to the
         * network. Both outgoing and incoming messages may have been lost. No further calls to this
         * listener will be made.
         */
        @Override
        public void onFailure(WebSocket webSocket, Throwable t, Response response) {
            Log.d(TAG, "onFailure " + t.getMessage());
            connected = false;
            reconnect();
        }
    }

    /**
     * 只暴露需要的回調(diào)給頁面,onFailure 你給了頁面
     */
    public interface WebSocketCallback {
        void onMessage(String text);

        void onOpen();

        void onClosed();
    }

    public void setWebSocketCallback(WebSocketCallback webSocketCallback) {
        this.webSocketCallback = webSocketCallback;
    }

}

3.audioTrack播放接收到的二進(jìn)制流數(shù)據(jù)

    private ServiceConnection serviceConnection = new ServiceConnection() {
        @Override
        public void onServiceConnected(ComponentName name, IBinder service) {
            broadcastRoomService = ((BroadcastRoomService.LocalBinder) service).getService();
            broadcastRoomService.setBroadcastCallback(broadcastCallback);
        }

        @Override
        public void onServiceDisconnected(ComponentName name) {
            broadcastRoomService = null;
        }
    };
    private BroadcastRoomService.BroadcastCallback broadcastCallback = new BroadcastRoomService.BroadcastCallback() {
        @Override
        public void onMessage(final ByteString message) {
            //播放錄音
            executorService.submit(new Runnable() {
                @Override
                public void run() {
                    Log.e("message",message.toString());
                    if(!message.toString().contains("OK"))
                    voicePlayer.setDataSource(message.toByteArray());
                }
            });
        }

        @Override
        public void onOpen() {
            runOnUiThread(new Runnable() {
                @Override
                public void run() {
                    //可以做在線方面的
//                        tvMessage.setText("onOpen");
                    broadcastRoomService.initConnectCount();
                }
            });
        }

        @Override
        public void onClosed() {
            runOnUiThread(new Runnable() {
                @Override
                public void run() {
                    broadcastRoomService.close();
//                        tvMessage.setText("onClosed");
                }
            });
        }
    };




public class AudioParam {
    private int mFrequency; //采樣率
    private int mChannel; //聲道
    private int mSampleBit; //采樣精度

    public int getmFrequency() {
        return mFrequency;
    }

    public void setmFrequency(int mFrequency) {
        this.mFrequency = mFrequency;
    }

    public int getmChannel() {
        return mChannel;
    }

    public void setmChannel(int mChannel) {
        this.mChannel = mChannel;
    }

    public int getmSampleBit() {
        return mSampleBit;
    }

    public void setmSampleBit(int mSampleBit) {
        this.mSampleBit = mSampleBit;
    }
}

public class AudioUtil
{
    private static AudioUtil mInstance;
    private AudioRecord recorder;
    //聲音源
    private static int audioSource = MediaRecorder.AudioSource.MIC;
    //錄音的采樣頻率
    private static int audioRate = 44100;
    //錄音的聲道燥筷,單聲道
    private static int audioChannel = AudioFormat.CHANNEL_IN_STEREO;
    //量化的精度
    private static int audioFormat = AudioFormat.ENCODING_PCM_16BIT;
    //緩存的大小
    private static int bufferSize = AudioRecord.getMinBufferSize(audioRate , audioChannel , audioFormat);
    //記錄播放狀態(tài)
    private boolean isRecording = false;
    //數(shù)字信號數(shù)組
    private byte[] noteArray;
    //PCM文件
    private File pcmFile;
    //wav文件
    private File wavFile;
    //文件輸出流
    private OutputStream os;
    //文件根目錄
    private String basePath = Environment.getExternalStorageDirectory().getAbsolutePath() + "/record/";
    //wav文件目錄
    private String outFileName = basePath + "/encode.wav";
    //pcm文件目錄
    private String inFileName = basePath + "/encode.pcm";

    private AudioUtil()
    {
        //創(chuàng)建文件
        createFile();
        recorder = new AudioRecord(audioSource , audioRate ,
                audioChannel , audioFormat , bufferSize);
    }

    //創(chuàng)建文件夾,首先創(chuàng)建目錄箩祥,然后創(chuàng)建對應(yīng)的文件
    private void createFile()
    {
        File baseFile = new File(basePath);
        if (!baseFile.exists())
            baseFile.mkdirs();
        pcmFile = new File(basePath + "/encode.pcm");
        wavFile = new File(basePath + "/encode.wav");

        if (pcmFile.exists())
            pcmFile.delete();
        if (wavFile.exists())
            wavFile.delete();

        try
        {
            pcmFile.createNewFile();
            wavFile.createNewFile();
        }
        catch (IOException e)
        {
            e.printStackTrace();
        }
    }

    public synchronized static AudioUtil getInstance()
    {
        if (mInstance == null)
        {
            mInstance = new AudioUtil();
        }
        return mInstance;
    }

    //讀取錄音數(shù)字?jǐn)?shù)據(jù)線程
    class WriteThread implements Runnable
    {
        @Override
        public void run()
        {
            writeData();
        }
    }

    //錄音線程執(zhí)行體
    private void writeData()
    {
        noteArray = new byte[bufferSize];
        //建立文件輸出流
        try
        {
            os = new BufferedOutputStream(new FileOutputStream(pcmFile));
        }
        catch (FileNotFoundException e)
        {
            e.printStackTrace();
        }

        while (isRecording)
        {
            int recordSize = recorder.read(noteArray , 0 , bufferSize);
            if (recordSize > 0)
            {
                try
                {
                    os.write(noteArray);
                }
                catch (IOException e)
                {
                    e.printStackTrace();
                }
            }
        }

        if (os != null)
        {
            try
            {
                os.close();
            }
            catch (IOException e)
            {
                e.printStackTrace();
            }
        }
    }

    //開始錄音
    public void startRecord()
    {
        isRecording = true;
        recorder.startRecording();
    }

    //記錄數(shù)據(jù)
    public void recordData()
    {
        new Thread(new WriteThread()).start();
    }

    //停止錄音
    public void stopRecord()
    {
        if (recorder != null)
        {
            isRecording = false;
            recorder.stop();
            recorder.release();
        }
    }

    //將pcm文件轉(zhuǎn)換為wav文件
    public void convertWavFile()
    {
        FileInputStream in = null;
        FileOutputStream out = null;

        long totalAudioLen;
        long totalDataLen;
        long longSampleRate = AudioUtil.audioRate;
        int channels = 2;
        long byteRate = 16 * AudioUtil.audioRate * channels / 8;
        byte[] data = new byte[bufferSize];

        try
        {
            in = new FileInputStream(inFileName);
            out = new FileOutputStream(outFileName);
            totalAudioLen = in.getChannel().size();
            //由于不包括RIFF和WAV
            totalDataLen = totalAudioLen + 36;
            writeWaveFileHeader(out , totalAudioLen , totalDataLen , longSampleRate , channels , byteRate);
            while (in.read(data) != -1)
            {
                out.write(data);
            }

            in.close();
            out.close();
        }
        catch (IOException e)
        {
            e.printStackTrace();
        }
    }
}

public class VoicePlayer
{
    private AudioTrack mAudioTrack;      //AudioTrack對象
    private AudioPlayThread mAudioPlayThread = null;
    private AudioParam mAudioParam;      //音頻參數(shù)
    private int mPrimePlaySize = 0;      //較優(yōu)播放塊大小
    private static final int BUFFER_SIZE = 2048;

    //設(shè)置音頻參數(shù)
    public void setAudioParam(AudioParam audioParam)
    {
        mAudioParam = audioParam;
    }

    private void createAudioTrack()
    {
        if (mAudioTrack == null)
        {
            // 獲得構(gòu)建對象的最小緩沖區(qū)大小
            int minBufSize = AudioTrack.getMinBufferSize(mAudioParam.getmFrequency() ,
                    mAudioParam.getmChannel() , mAudioParam.getmSampleBit());
            mPrimePlaySize = minBufSize;
            mAudioTrack = new AudioTrack(AudioManager.STREAM_MUSIC , mAudioParam.getmFrequency() ,
                    mAudioParam.getmChannel() , mAudioParam.getmSampleBit() , Math.max(mPrimePlaySize,BUFFER_SIZE), AudioTrack.MODE_STREAM);
        }
    }

    private void releaseAudioTrack()
    {
        if (mAudioTrack != null)
        {
            mAudioTrack.stop();
            mAudioTrack.release();
            mAudioTrack = null;
        }
    }

    //就緒播放源
    public void prepare()
    {
        if (mAudioParam != null)
        {
            createAudioTrack();
        }
    }

    public void play()
    {
        if (mAudioPlayThread == null)
        {
            mAudioPlayThread = new AudioPlayThread();
            mAudioPlayThread.start();
            if (mAudioPlayThread.mPlayHandler == null)
            {
                try
                {
                    Thread.sleep(1000);
                }
                catch (InterruptedException e)
                {
                    e.printStackTrace();
                }
            }
        }
    }

    private void stop()
    {
        if (mAudioPlayThread != null)
        {
            mAudioPlayThread = null;
        }
    }

    public void release()
    {
        stop();
        releaseAudioTrack();
    }

    //設(shè)置音頻源
    public void setDataSource(byte[] data)
    {
        if (mAudioPlayThread.mPlayHandler != null)
        {
            Message message = mAudioPlayThread.mPlayHandler.obtainMessage();
            message.what = 0x123;
            message.obj = data;
            mAudioPlayThread.mPlayHandler.sendMessage(message);
        }
    }

    class AudioPlayThread extends Thread
    {
        private Handler mPlayHandler;
        @Override
        public void run()
        {
            mAudioTrack.play();
            Looper.prepare();
            mPlayHandler = new Handler()
            {
                @Override
                public void handleMessage(Message msg)
                {
                    if (msg.what == 0x123)
                    {
                        mAudioTrack.write((byte[]) msg.obj, 0, ((byte[]) msg.obj).length);
                    }
                }
            };
            Looper.loop();
        }
    }
}


?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末,一起剝皮案震驚了整個濱河市肆氓,隨后出現(xiàn)的幾起案子袍祖,更是在濱河造成了極大的恐慌,老刑警劉巖做院,帶你破解...
    沈念sama閱讀 218,036評論 6 506
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件盲泛,死亡現(xiàn)場離奇詭異濒持,居然都是意外死亡,警方通過查閱死者的電腦和手機寺滚,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 93,046評論 3 395
  • 文/潘曉璐 我一進(jìn)店門柑营,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人村视,你說我怎么就攤上這事官套。” “怎么了蚁孔?”我有些...
    開封第一講書人閱讀 164,411評論 0 354
  • 文/不壞的土叔 我叫張陵奶赔,是天一觀的道長。 經(jīng)常有香客問我杠氢,道長站刑,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 58,622評論 1 293
  • 正文 為了忘掉前任鼻百,我火速辦了婚禮绞旅,結(jié)果婚禮上,老公的妹妹穿的比我還像新娘温艇。我一直安慰自己因悲,他們只是感情好,可當(dāng)我...
    茶點故事閱讀 67,661評論 6 392
  • 文/花漫 我一把揭開白布勺爱。 她就那樣靜靜地躺著晃琳,像睡著了一般。 火紅的嫁衣襯著肌膚如雪琐鲁。 梳的紋絲不亂的頭發(fā)上卫旱,一...
    開封第一講書人閱讀 51,521評論 1 304
  • 那天,我揣著相機與錄音绣否,去河邊找鬼誊涯。 笑死,一個胖子當(dāng)著我的面吹牛蒜撮,可吹牛的內(nèi)容都是我干的。 我是一名探鬼主播跪呈,決...
    沈念sama閱讀 40,288評論 3 418
  • 文/蒼蘭香墨 我猛地睜開眼段磨,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了耗绿?” 一聲冷哼從身側(cè)響起苹支,我...
    開封第一講書人閱讀 39,200評論 0 276
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎误阻,沒想到半個月后债蜜,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體晴埂,經(jīng)...
    沈念sama閱讀 45,644評論 1 314
  • 正文 獨居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點故事閱讀 37,837評論 3 336
  • 正文 我和宋清朗相戀三年寻定,在試婚紗的時候發(fā)現(xiàn)自己被綠了儒洛。 大學(xué)時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片。...
    茶點故事閱讀 39,953評論 1 348
  • 序言:一個原本活蹦亂跳的男人離奇死亡狼速,死狀恐怖琅锻,靈堂內(nèi)的尸體忽然破棺而出,到底是詐尸還是另有隱情向胡,我是刑警寧澤恼蓬,帶...
    沈念sama閱讀 35,673評論 5 346
  • 正文 年R本政府宣布,位于F島的核電站僵芹,受9級特大地震影響处硬,放射性物質(zhì)發(fā)生泄漏。R本人自食惡果不足惜拇派,卻給世界環(huán)境...
    茶點故事閱讀 41,281評論 3 329
  • 文/蒙蒙 一荷辕、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧攀痊,春花似錦桐腌、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 31,889評論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至棘街,卻和暖如春蟆盐,著一層夾襖步出監(jiān)牢的瞬間,已是汗流浹背遭殉。 一陣腳步聲響...
    開封第一講書人閱讀 33,011評論 1 269
  • 我被黑心中介騙來泰國打工石挂, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留,地道東北人险污。 一個月前我還...
    沈念sama閱讀 48,119評論 3 370
  • 正文 我出身青樓痹愚,卻偏偏與公主長得像,于是被迫代替她去往敵國和親蛔糯。 傳聞我的和親對象是個殘疾皇子拯腮,可洞房花燭夜當(dāng)晚...
    茶點故事閱讀 44,901評論 2 355

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