#推荐
Cocos Creator麻将源代码开发教程第八篇——情怀麻将语音聊天源码分析

2024-10-03 7,895

语音聊天

  1. 语音聊天功能仅支持 Native 平台,包括 iOSAndroid
  2. 语音聊天的音频格式为 amr
  3. Native 平台中实现了语音的录制和播放,可以移植到其他项目中使用,相关代码位于 native 文件夹下。

录制流程

  1. 在按钮上通过代码监听 STARTEnded 事件,以控制录音的开始与结束操作;位于 Voice.js 文件中。
  2. VoiceMgr.js:封装了 AndroidiOS 平台的录制调用接口。
  3. cc.vv.voiceMgr.prepare("record.amr"):启动录音。
  • 1> 暂停所有游戏的音效和背景音乐。
  • 2> 清理掉之前录制的音频文件。
  • 3> 调用 native 平台的 start 函数进行录音操作。
  1. Ended 事件响应:
  • 1> 判断录音时长是否超过 10 秒,若未达到则取消录制。
  • 2> cc.vv.voiceMgr.cancel() –> 调用 native 平台的 cancel 操作。
  • 3> cc.vv.voiceMgr.release() –> 调用 native 平台接口,表示录制完成。
  • 4> getVoiceData:将录制好的二进制文件转换为文本编码格式。
  • 5> 通过 voice_msg 事件将音频文本数据发送给游戏服务器。

播放流程

  1. 服务器接收到 voice_msg 事件。
  2. 将语音数据在房间内通过 voice_msg_push 事件进行广播。
  3. GameNetMgr.js:接收到 voice_msg_push 事件,并分发 voice_msg 事件。
  4. MJRoom.js:调用 playVoice 函数播放语音。
  5. cc.vv.voiceMgr.writeVoice(msgfile, msgInfo.msg):将接收到的语音数据写入音频文件。
  6. 播放音频文件 cc.vv.voiceMgr.play(msgfile),调用本地播放接口。
  7. update 函数中检测播放时间是否结束,到达时调用 onPlayerOver,恢复之前暂停的游戏音效。
  8. 优点Native 平台下的录音和播放代码可直接复用。

改进建议

由于音频数据较大,建议不要通过游戏的长连接通道进行传输,以免阻塞游戏数据广播。可以改为使用独立的 HTTP 上传和下载方式,然后在房间内只广播“某某玩家正在讲话”。

Cocos Creator麻将源代码开发教程第八篇——情怀麻将语音聊天源码分析

1.在 Voice.js 中找到对应的按钮元素,并对其进行事件监听与处理操作。

Cocos Creator麻将源代码开发教程第八篇——情怀麻将语音聊天源码分析

1.btnVoice.on(cc.Node.EventType.TOUCH_START, function() {
用于监听按钮的按下事件,触发录音的开始操作。

2.btnVoice.on(cc.Node.EventType.TOUCH_END, function() {
用于监听按钮的松开事件,触发录音的结束操作。

3.btnVoice.on(cc.Node.EventType.TOUCH_CANCEL, function() {
用于监听按钮的取消操作,处理异常情况下的录音终止操作。

onLoad: function () {
        
        this._voice = cc.find("Canvas/voice");
        this._voice.active = false;
        
        this._voice_failed = cc.find("Canvas/voice/voice_failed");
        this._voice_failed.active = false;
        
        this._timeBar = cc.find("Canvas/voice/time");
        this._timeBar.scaleX = 0.0;
        
        this._volume = cc.find("Canvas/voice/volume");
        for(var i = 1; i < this._volume.children.length; ++i){
            this._volume.children[i].active = false;
        }
        
        var btnVoice = cc.find("Canvas/voice/voice_failed/btn_ok");
        if(btnVoice){
            cc.vv.utils.addClickEvent(btnVoice,this.node,"Voice","onBtnOKClicked");
        }
        
        var self = this;
        var btnVoice = cc.find("Canvas/btn_voice");
        if(btnVoice){
            btnVoice.on(cc.Node.EventType.TOUCH_START,function(){
                console.log("cc.Node.EventType.TOUCH_START");
                cc.vv.voiceMgr.prepare("record.amr");
                self._lastTouchTime = Date.now();
                self._voice.active = true;
                self._voice_failed.active = false;
            });
 
            btnVoice.on(cc.Node.EventType.TOUCH_MOVE,function(){
                console.log("cc.Node.EventType.TOUCH_MOVE");
            });
                        
            btnVoice.on(cc.Node.EventType.TOUCH_END,function(){
                console.log("cc.Node.EventType.TOUCH_END");
                if(Date.now() - self._lastTouchTime < 1000){
                    self._voice_failed.active = true;
                    cc.vv.voiceMgr.cancel();
                }
                else{
                    self.onVoiceOK();
                }
                self._lastTouchTime = null;
            });
            
            btnVoice.on(cc.Node.EventType.TOUCH_CANCEL,function(){
                console.log("cc.Node.EventType.TOUCH_CANCEL");
                cc.vv.voiceMgr.cancel();
                self._lastTouchTime = null;
                self._voice.active = false;
            });
        }
    },

Cocos Creator麻将源代码开发教程第八篇——情怀麻将语音聊天源码分析Cocos Creator麻将源代码开发教程第八篇——情怀麻将语音聊天源码分析

  1. 根据不同平台分别调用 AndroidiOS 平台的录音函数:

Cocos Creator麻将源代码开发教程第八篇——情怀麻将语音聊天源码分析

Cocos Creator麻将源代码开发教程第八篇——情怀麻将语音聊天源码分析

5如果录音时长小于 1 秒,或者在按钮外部松开,则取消本次录音操作。

Cocos Creator麻将源代码开发教程第八篇——情怀麻将语音聊天源码分析

//---------------------VoiceMgr.js----------------------------
var radix = 12;
var base = 128 - radix;
function crypto(value){
    value -= base;
    var h = Math.floor(value/radix) + base;
    var l = value%radix + base;
    return String.fromCharCode(h) + String.fromCharCode(l);
}
 
var encodermap = {}
var decodermap = {}
for(var i = 0; i < 256; ++i){
    var code = null;
    var v = i + 1;
    if(v >= base){
        code = crypto(v);
    }
    else{
        code = String.fromCharCode(v);    
    }
    
    encodermap[i] = code;
    decodermap[code] = i;
}
 
function encode(data){
    var content = "";
    var len = data.length;
    var a = (len >> 24) & 0xff;
    var b = (len >> 16) & 0xff;
    var c = (len >> 8) & 0xff;
    var d = len & 0xff;
    content += encodermap[a];
    content += encodermap[b];
    content += encodermap[c];
    content += encodermap[d];
    for(var i = 0; i < data.length; ++i){
        content += encodermap[data[i]];
    }
    return content;
}
 
function getCode(content,index){
    var c = content.charCodeAt(index);
    if(c >= base){
        c = content.charAt(index) + content.charAt(index + 1);
    }
    else{
        c = content.charAt(index);
    }
    return c;
}
function decode(content){
    var index = 0;
    var len = 0;
    for(var i = 0; i < 4; ++i){
        var c = getCode(content,index);
        index += c.length;
        var v = decodermap[c];
        len |= v << (3-i)*8;
    }
    
    var newData = new Uint8Array(len);
    var cnt = 0;
    while(index < content.length){
        var c = getCode(content,index);
        index += c.length;
        newData[cnt] = decodermap[c];
        cnt++;
    }
    return newData;
}
 
cc.Class({
    extends: cc.Component,
 
    properties: {
        // foo: {
        //    default: null,      // The default value will be used only when the component attaching
        //                           to a node for the first time
        //    url: cc.Texture2D,  // optional, default is typeof default
        //    serializable: true, // optional, default is true
        //    visible: true,      // optional, default is true
        //    displayName: 'Foo', // optional
        //    readonly: false,    // optional, default is false
        // },
        // ...
        onPlayCallback:null,
        _voiceMediaPath:null,
    },
 
    // use this for initialization
    init: function () {
        /*
        var url = cc.url.raw("resources/test.amr");
        var fileData = jsb.fileUtils.getDataFromFile(url);
        var content = "";
        var sep = "";
        for(var i = 0; i < fileData.length; ++i){
            content += sep + fileData[i];
            sep = ",";
        }
        
        var url = cc.url.raw("resources/test.txt");
        jsb.fileUtils.writeStringToFile(content,url);
        
        var url = cc.url.raw("resources/test2.amrs");
        var content = encode(fileData);
        jsb.fileUtils.writeStringToFile(content,url);
        
        var url = cc.url.raw("resources/test2.amr");
        jsb.fileUtils.writeDataToFile(decode(content),url);
        */
        
        if(cc.sys.isNative){
            this._voiceMediaPath = jsb.fileUtils.getWritablePath() + "/voicemsgs/";
            this.setStorageDir(this._voiceMediaPath);
        }
    },
    
    prepare:function(filename){
        if(!cc.sys.isNative){
            return;
        }
        cc.vv.audioMgr.pauseAll();
        this.clearCache(filename);
        if(cc.sys.os == cc.sys.OS_ANDROID){
            jsb.reflection.callStaticMethod("com/babykylin/VoiceRecorder", "prepare", "(Ljava/lang/String;)V",filename);
        }
        else if(cc.sys.os == cc.sys.OS_IOS){
            jsb.reflection.callStaticMethod("VoiceSDK", "prepareRecord:",filename);
        }
    },
    
    release:function(){
        if(!cc.sys.isNative){
            return;
        }
        cc.vv.audioMgr.resumeAll();
        if(cc.sys.os == cc.sys.OS_ANDROID){
            jsb.reflection.callStaticMethod("com/babykylin/VoiceRecorder", "release", "()V");
        }
        else if(cc.sys.os == cc.sys.OS_IOS){
            jsb.reflection.callStaticMethod("VoiceSDK", "finishRecord");
        }
    },
    
    cancel:function(){
        if(!cc.sys.isNative){
            return;
        }
        cc.vv.audioMgr.resumeAll();
        if(cc.sys.os == cc.sys.OS_ANDROID){
            jsb.reflection.callStaticMethod("com/babykylin/VoiceRecorder", "cancel", "()V");
        }
        else if(cc.sys.os == cc.sys.OS_IOS){
            jsb.reflection.callStaticMethod("VoiceSDK", "cancelRecord");
        }
    },
 
    writeVoice:function(filename,voiceData){
        if(!cc.sys.isNative){
            return;
        }
        if(voiceData && voiceData.length > 0){
            var fileData = decode(voiceData);
            var url = this._voiceMediaPath + filename;
            this.clearCache(filename);
            jsb.fileUtils.writeDataToFile(fileData,url); 
        }
    },
    
    clearCache:function(filename){
        if(cc.sys.isNative){
            var url = this._voiceMediaPath + filename;
            //console.log("check file:" + url);
            if(jsb.fileUtils.isFileExist(url)){
                //console.log("remove:" + url);
                jsb.fileUtils.removeFile(url);
            }
            if(jsb.fileUtils.isFileExist(url + ".wav")){
                //console.log("remove:" + url + ".wav");
                jsb.fileUtils.removeFile(url + ".wav");
            }   
        }
    },
    
    play:function(filename){
        if(!cc.sys.isNative){
            return;
        }
        cc.vv.audioMgr.pauseAll();
        if(cc.sys.os == cc.sys.OS_ANDROID){
            jsb.reflection.callStaticMethod("com/babykylin/VoicePlayer", "play", "(Ljava/lang/String;)V",filename); 
        }
        else if(cc.sys.os == cc.sys.OS_IOS){
            jsb.reflection.callStaticMethod("VoiceSDK", "play:",filename);
        }
        else{
        }
    },
    
    stop:function(){
        if(!cc.sys.isNative){
            return;
        }
        cc.vv.audioMgr.resumeAll();
        if(cc.sys.os == cc.sys.OS_ANDROID){
            jsb.reflection.callStaticMethod("com/babykylin/VoicePlayer", "stop", "()V"); 
        }
        else if(cc.sys.os == cc.sys.OS_IOS){
            jsb.reflection.callStaticMethod("VoiceSDK", "stopPlay");
        }
        else{
        }
    },
    
    getVoiceLevel:function(maxLevel){
        return Math.floor(Math.random() * maxLevel + 1);
        if(cc.sys.os == cc.sys.OS_ANDROID){ 
            return jsb.reflection.callStaticMethod("com/babykylin/VoiceRecorder", "getVoiceLevel", "(I)I",maxLevel);
        }
        else if(cc.sys.os == cc.sys.OS_IOS){
        }
        else{
            return Math.floor(Math.random() * maxLevel + 1);
        }
    },
    
    getVoiceData:function(filename){
        if(cc.sys.isNative){
            var url = this._voiceMediaPath + filename;
            console.log("getVoiceData:" + url);
            var fileData = jsb.fileUtils.getDataFromFile(url);
            if(fileData){
                var content = encode(fileData);
                return content;
            }
        }
        return "";
    },
    
    download:function(){
        
    },
    // called every frame, uncomment this function to activate update callback
    // update: function (dt) {
 
    // },
    
    setStorageDir:function(dir){
        if(!cc.sys.isNative){
            return;
        }
        if(cc.sys.os == cc.sys.OS_ANDROID){ 
            jsb.reflection.callStaticMethod("com/babykylin/VoiceRecorder", "setStorageDir", "(Ljava/lang/String;)V",dir);    
        }
        else if(cc.sys.os == cc.sys.OS_IOS){
            jsb.reflection.callStaticMethod("VoiceSDK", "setStorageDir:",dir);
            if(!jsb.fileUtils.isDirectoryExist(dir)){
                jsb.fileUtils.createDirectory(dir);
            }
        }
    }
});

将音频数据转换为文本格式后,发送至服务器进行广播推送。

 onVoiceOK:function(){
        if(this._lastTouchTime != null){
            cc.vv.voiceMgr.release();
            var time = Date.now() - this._lastTouchTime;
            var msg = cc.vv.voiceMgr.getVoiceData("record.amr");
            cc.vv.net.send("voice_msg",{msg:msg,time:time});
        }
        this._voice.active = false;
    },

服务器的语音传输使用长连接方式进行通信。Cocos Creator麻将源代码开发教程第八篇——情怀麻将语音聊天源码分析

接收到语音消息后,进行广播分发给所有客户端。

userMgr.broacastInRoom('voice_msg_push',{sender:socket.userId,content:data},socket.userId,true);

客户端接收到语音消息后,进行消息分发处理。

Cocos Creator麻将源代码开发教程第八篇——情怀麻将语音聊天源码分析

MJRoom.js 接收到消息后,将数据放入缓存队列中,并调用 playVoice() 进行播放。

Cocos Creator麻将源代码开发教程第八篇——情怀麻将语音聊天源码分析

playVoice: function():该函数用于播放语音文件,并控制播放的相关逻辑。

Cocos Creator麻将源代码开发教程第八篇——情怀麻将语音聊天源码分析

将接收到的文本格式的音频数据转换回二进制文件,再执行播放操作。

cc.vv.voiceMgr.writeVoice(msgfile,msgInfo.msg);
cc.vv.voiceMgr.play(msgfile);

Cocos Creator麻将源代码开发教程第八篇——情怀麻将语音聊天源码分析Cocos Creator麻将源代码开发教程第八篇——情怀麻将语音聊天源码分析Cocos Creator麻将源代码开发教程第八篇——情怀麻将语音聊天源码分析

play: function(filename)

先暂停所有背景音乐:cc.vv.audioMgr.pauseAll()

stop: function()

播放结束后,恢复所有暂停的音乐:cc.vv.audioMgr.resumeAll()

//------------------------Voice.js------------------------------
cc.Class({
    extends: cc.Component,
 
    properties: {
        // foo: {
        //    default: null,      // The default value will be used only when the component attaching
        //                           to a node for the first time
        //    url: cc.Texture2D,  // optional, default is typeof default
        //    serializable: true, // optional, default is true
        //    visible: true,      // optional, default is true
        //    displayName: 'Foo', // optional
        //    readonly: false,    // optional, default is false
        // },
        // ...
        _lastTouchTime:null,
        _voice:null,
        _volume:null,
        _voice_failed:null,
        _lastCheckTime:-1,
        _timeBar:null,
        MAX_TIME:15000,
    },
 
    // use this for initialization
    onLoad: function () {
        
        this._voice = cc.find("Canvas/voice");
        this._voice.active = false;
        
        this._voice_failed = cc.find("Canvas/voice/voice_failed");
        this._voice_failed.active = false;
        
        this._timeBar = cc.find("Canvas/voice/time");
        this._timeBar.scaleX = 0.0;
        
        this._volume = cc.find("Canvas/voice/volume");
        for(var i = 1; i < this._volume.children.length; ++i){
            this._volume.children[i].active = false;
        }
        
        var btnVoice = cc.find("Canvas/voice/voice_failed/btn_ok");
        if(btnVoice){
            cc.vv.utils.addClickEvent(btnVoice,this.node,"Voice","onBtnOKClicked");
        }
        
        var self = this;
        var btnVoice = cc.find("Canvas/btn_voice");
        if(btnVoice){
            btnVoice.on(cc.Node.EventType.TOUCH_START,function(){
                console.log("cc.Node.EventType.TOUCH_START");
                cc.vv.voiceMgr.prepare("record.amr");
                self._lastTouchTime = Date.now();
                self._voice.active = true;
                self._voice_failed.active = false;
            });
 
            btnVoice.on(cc.Node.EventType.TOUCH_MOVE,function(){
                console.log("cc.Node.EventType.TOUCH_MOVE");
            });
                        
            btnVoice.on(cc.Node.EventType.TOUCH_END,function(){
                console.log("cc.Node.EventType.TOUCH_END");
                if(Date.now() - self._lastTouchTime < 1000){
                    self._voice_failed.active = true;
                    cc.vv.voiceMgr.cancel();
                }
                else{
                    self.onVoiceOK();
                }
                self._lastTouchTime = null;
            });
            
            btnVoice.on(cc.Node.EventType.TOUCH_CANCEL,function(){
                console.log("cc.Node.EventType.TOUCH_CANCEL");
                cc.vv.voiceMgr.cancel();
                self._lastTouchTime = null;
                self._voice.active = false;
            });
        }
    },
    
    onVoiceOK:function(){
        if(this._lastTouchTime != null){
            cc.vv.voiceMgr.release();
            var time = Date.now() - this._lastTouchTime;
            var msg = cc.vv.voiceMgr.getVoiceData("record.amr");
            cc.vv.net.send("voice_msg",{msg:msg,time:time});
        }
        this._voice.active = false;
    },
    
    onBtnOKClicked:function(){
        this._voice.active = false;
    },
 
    // called every frame, uncomment this function to activate update callback
    update: function (dt) {
        if(this._voice.active == true && this._voice_failed.active == false){
            if(Date.now() - this._lastCheckTime > 300){
                for(var i = 0; i < this._volume.children.length; ++i){
                    this._volume.children[i].active = false;
                }
                var v = cc.vv.voiceMgr.getVoiceLevel(7);
                if(v >= 1 && v <= 7){
                    this._volume.children[v-1].active = true;   
                }
                this._lastCheckTime = Date.now();
            }
        }
        
        if(this._lastTouchTime){
            var time = Date.now() - this._lastTouchTime;
            if(time >= this.MAX_TIME){
                this.onVoiceOK();
                this._lastTouchTime = null;
            }
            else{
                var percent = time / this.MAX_TIME;
                this._timeBar.scaleX = 1 - percent;
            }
        }
    },
});

好的本篇文章先到这

收藏 打赏

感谢您的支持,我会继续努力的!

打开USDT(trc-20)扫一扫,即可进行扫码打赏哦,分享从这里开始,精彩与您同在
点赞 (0)

Ts:本站所有内容均为互联网收集整理和网友上传。仅限于学习研究,请必须在24小时内删除。否则由此引发的法律纠纷及连带责任本站概不承担。

如侵犯到您的合法权益,请联系我们删除侵权资源!

韩仔技术 搭建教程 Cocos Creator麻将源代码开发教程第八篇——情怀麻将语音聊天源码分析 https://www.hanzijs.com/dajian/2878.html

相关文章

发表评论
暂无评论