小白安卓编程之旅(1)

0.目标

本地手机存储3-5首音乐,当外部设备播放某一音乐时,使用听歌识曲判断音乐名称,然后发送至手机,发送方式暂定为邮箱。软件为个人使用,很多安全性问题没有认真考虑,读者可自行修改。

1.环境构建

从网上下载好android studio,SDK,JDK

1.1安装软件

按照JDK-android studio-SDK的顺序安装好几个软件,再从SDK里下载好各类推荐软件,android studio打开后会自动加载并开始下载一些没有的软件

1.2需要gradle

从网上下载好gradle,解压到一个文件夹里,从setting里找到gradle配置,改好地址就行

1.3SDK搬家

发现SDK自动下载到C盘,所以搬家,说是存储路径不能有空格,所以放到D盘根目录

1.4模拟器

打算用android studio自带的,下载好后发现不行,我的CPU不支持什么VT-x,所以下载Genymotion,但是发现未响应,在查找问题的过程中发现可以使用真机模拟,所以又尝试真机,通过ADB驱动安装最终识别出来

2.界面

先使用原装界面和控件,后续再做优化

2.1加按钮

默认使用constraintlayout布局,加了button后要再加个

`tools:ignore="MissingConstraints"`

之后又随便改了一些名称显示之类的

3.代码

先实现录音

3.1权限

这里根据第一行代码里第八章的内容
权限如下,合并了一下

    val PERMISSION_ALL = arrayOf(
        Manifest.permission.RECORD_AUDIO,
        Manifest.permission.WRITE_EXTERNAL_STORAGE,
        Manifest.permission.READ_EXTERNAL_STORAGE
    )

在AndroidManifest里先声明

<uses-permission android:name="android.permission.RECORD_AUDIO" />
<uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE"/>
<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE"/>

在MainActivity里检验是否授权

            if (ContextCompat.checkSelfPermission(this,
                    PERMISSION_ALL.toString()
                ) != PackageManager.PERMISSION_GRANTED){
                ActivityCompat.requestPermissions(this,
                    PERMISSION_ALL,1)
            }
            else{
                record()

这里又重写了一下onRequestPermissionsResult(这里可能用来第一次提示授权的时候记录用户选择内容的,如果选了始终允许,后续不再出现,个人使用,只要能实现即可未深入测试)

    override fun onRequestPermissionsResult(
        requestCode: Int,
        permissions: Array<out String>,
        grantResults: IntArray
    ) {
        super.onRequestPermissionsResult(requestCode, permissions, grantResults)
        when (requestCode){
            1 ->{
                if(grantResults.isNotEmpty() &&
                       grantResults[0] == PackageManager.PERMISSION_GRANTED){
                    record()
                }else{
                    Toast.makeText(this, "不同意玩个锤子", Toast.LENGTH_SHORT).show()
                }

            }
        }
    }

3.2录音

对各类实现方法都有了了解,最终采纳Amoy阿磊-Android实现录音功能汇总的结果,使用MediaRecorder。
但一直未准备好,问题出在setOutputFile需要填写文件路径,已经尝试过Environment.getExternalStorageDirectory(),但是getExternalStorageDirectory这个函数不让用了;尝试过context.getExternalCacheDir()系列,是Context一直是红色不可用,不知为何。最后发现可以写file,如github大神写的一样,但是大神的

Namefile = "${externalCacheDir?.absolutePath}/audiorecordtest.3gp"
Pathfile= externalCacheDir?.absolutePath.toString()

这句无法准确得到文件位置放到setOutputFile里(可能是我不会),又查到jeffrey12138这位大神的kotlin-Android存储原理相关的内容很多但把代码写明白的就这个了,里面用到getExternalFilesDir(null)?.absolutePath可以直接使用,很方便。
为方便读写,设置了时长为15秒,然后发现保存的文件不能播放,找了半天原因发现没有主动stop,之前见有介绍MediaRecorder说到时间会自动保存,这里发现可能是有问题的,所以要主动停一下。综合几种定时器,最终选择了Young_cloud的CountDownTimer,当然也不是完全一样,如TODO(“not implemented”)需要删除、onTick和onFinish()都要重写,感谢qijingwangm0_46301460。同时发现有个提示说主进程干活太多了,Skipped 31 frames! The application may be doing too much work on its main thread.加上后面要判断分贝,所以可能要加上线程相关内容了。
至此,顺序实现录音,mp4格式,在本机可直接播放,不知道后面发送出去能不能不保存使用软件或邮箱直接播放,走一步看一步吧,代码如下:
不知道为啥,要把lateinit var Pathfile: String放在class MainActivity的外面。recorder = MediaRecorder()要放在fun recordStart()外面。没仔细研究,反正AS都有提示。

private val recorder = MediaRecorder()

    @RequiresApi(Build.VERSION_CODES.O)
    private fun recordStart(){
        try {
            recorder.setAudioSource(MediaRecorder.AudioSource.MIC)
            recorder.setOutputFormat(MediaRecorder.OutputFormat.MPEG_4)
            recorder.setAudioEncoder(MediaRecorder.AudioEncoder.AAC)
            Pathfile = getExternalFilesDir(null)?.absolutePath+"audiorecordtest.mp4"
            val file = File(Pathfile)
            recorder.setOutputFile(file)
            recorder.setMaxDuration(1000*15)
            Toast.makeText(this,"开始录音",Toast.LENGTH_SHORT).show()

            try {
                recorder.prepare()
                recorder.start()
                object : CountDownTimer(16000,1000){
                    override fun onFinish() {
                        recordStop()
                    }
                    override fun onTick(millisUntilFinished: Long) {
                    }
                }.start()
            } catch (e: Exception) {
                e.printStackTrace()
                Toast.makeText(this,"prepare() failed",Toast.LENGTH_SHORT).show()
            }
        }catch (e:SecurityException){
            e.printStackTrace()
            recordStop()
        }
    }

    private fun recordStop(){
        recorder.stop()
        recorder.release()
        Toast.makeText(this,"录好了",Toast.LENGTH_SHORT).show()
    }

3.3分贝

一开始想按照正规的分贝仪进行设计,发现好难啊,最终采用Android实时获取音量(单位:分贝)(这个文章好经典,6万阅读)里写的getMaxAmplitude方法,Android 录音getMaxAmplitude()这里提到要把getMaxAmplitude方法写到线程里,最终得到如下代码。

    private val BASE = 1
    private var db = 0.0// 分贝
    private fun fenbei() {
                    var ratio = recorder.maxAmplitude / BASE
                    if (ratio > 1)
                        db = 20 * log10(ratio.toDouble())
                    Log.d("db", db.toString())
                }

maxAmplitude要在线程里启用

                        override fun onTick(millisUntilFinished: Long) {
                            thread {
                                fenbei()
                           }

3.4自动循环录音

以为循环很容易,发现存在几个致命问题:
1.每次点击按钮录音结束后再点击录音就会闪退,日志都来不及写,最后发现是MediaRecorder()实例化后没有清除,找到一些代码发现可以用recorder = null来清除,但AS提示MediaRecorder()是非空类型,最后发现要用private var recorder: MediaRecorder? = null这种定义方法,才会有null;
2.自动循环用while每次执行recorder的时间不足每次记录15秒,用for首先要确定执行次数,我想的是一直执行,不停的那种,不过也可以随便写个比较大的数(事实上最后就是用这个比较大的数实现的循环,不过不是for),再就是无法延迟执行,这个问题同while;
3.想到用Handler和线程完成,但因为fenbei要用到线程,recorder无法再使用线程(好像是因为kotlin没有父子线程的原因,第一行代码在讲解协程的时间写到的是线程都是顶级的),又想尝试用每个结果位置加个线程传递参数,但发现fenbei里只要确定分贝>50就要传递参数,录音就无法完成,甚至不能播放,就又又想把分贝全写到数组里,但无奈没有找到可以收集动态创建的线程的结果的数组赋值方法,最终放弃(其实想想好像也不是不行,毕竟已经检测到大分贝了说明已经有提示了,可以进行下一步信息网络传递了,不需要录音文件);
4.又又又想着用延迟定时来实现recorder执行,如何在Kotlin中延迟后调用函数?里写到了两三种比较好的方法,但最终发现可能还是线程不父子的问题,无法实现。
最终,选择使用之前用过的定时器,把recorder定时执行一次得了,次数用的比较大的数。
(最终开窍了,只执行一次录音和分贝判断就行,无所谓录音时间长短)

            object : CountDownTimer(17000*3000, 17000) {
                override fun onFinish() {
                }
                override fun onTick(millisUntilFinished: Long) {
                    recordStart()
                }
            }.start()

3.5发送情况至对端

此处耗时耗精力最多,最一开始想最基本的实现邮件发送程序判断值班音乐名称,发现音乐指纹不好写,决定用语音通话方式掌握有情况后的值班室情况,再想调用qq或微信,发现真实的聊天没有接口调用,想用小程序,发现只有第一年免费而且要自建服务器,想自己写个IM系统,发现太难,注册了网易云发现要钱,在github上找成功的系统也没法有,因为没有服务器,找到了一个开源的IM SDK—MobileIMSDK,这里花费了一些时间,我会单独开帖记录这里,主要在学习如何搭建实时通讯系统,发现可能用不到这么高端的,毕竟只需要一个提醒、一个语音通话功能,利用SDK测试的服务器实现了登录、收发消息功能,至此很成功,但在做语音通话时发现自己不会做,也没有服务器,想尝试发送录音文件至接收端,研究了好久转码、UDP/TCP协议之类的,甚至修改了原SDK的jar包用来发送字节信息,最后发现SDK测试服务器不收这种,完犊子了,一朝回到解放前。
穷则思变,又开始寻找新的IM系统,发现声网的WebRTC可以用,免费,每月10000分钟,服务器可不建,接口调用方便,有消息、音视频、直播、KTV等服务可拓展,缺点很明显,无教程、无可用demo,代码全红但可以编译,没有任何提示,甚至代码都是错的,全靠自己,每一步都很艰难,把所有的文档看了一遍,各个功能学了一遍,感觉可以了,放弃死活用不成的demo,跟着文档廖廖几笔,写了自己的程序,把没用的都删除了,留下最核心的,发现可以用,当中也把所有能遇到的坑遇了个遍,比如gradle版本低、SDKjar包打不开、commons-codec不能加载、 Theme.AppCompat 、download dependencies and sync project (requires network)等问题,社区提问感觉好像没人回复一样,冷冷清清。最终还是实现了,哈哈,还是很欣慰的。

private var mRtcEngine: RtcEngine? = null // Tutorial Step 1,定义一个RtcEngine
   private var mRtcEventHandler = object : IRtcEngineEventHandler(){}//可以写一此用户离线之类的回调函数,没写(写了没成功)

   private fun initializeAgoraEngine() {//初始化
       try {//安全一点
           //调用create函数,加上上下文、appId,事件监听参数,其实内容是RTC的SDK在工作
           mRtcEngine = RtcEngine.create(MyApplication.context, "(这里写你的appId)", mRtcEventHandler)
           if (mRtcEngine !=null)//创建成功
               Log.d(LOG_TAG,"创建RtcEngine成功")//提示
       } catch (e: Exception) {//错误信息
           Log.e(LOG_TAG, Log.getStackTraceString(e))//提示并打印错误信息
       }
   }

   // Tutorial Step 2
   //从MailMe传入的Token参数
   fun joinChannel(tokenget:String) {
       initializeAgoraEngine()//初始化
       var accessToken = tokenget//其实没格式转换
       Log.d(LOG_TAG, accessToken)//提示
       val error1 = mRtcEngine?.setChannelProfile(Constants.CHANNEL_PROFILE_COMMUNICATION)//单/群聊规则,此处为单聊
       Log.d(LOG_TAG,"error1="+error1.toString())//打印错误代码
       //加入频道
       val error2= mRtcEngine?.joinChannel(accessToken, "WatchChannel1", "Extra Optional Data", 0) // if you do not specify the uid, we will generate the uid for you
       Log.d(LOG_TAG,"error2="+error2.toString())//打印错误代码
       mRtcEngine?.setEnableSpeakerphone(true)//打开扬声器
   }

3.6服务端生成Token

使用了开放的IM系统,最主要的就是要自己生成Token,但研究发现声网的生成密码过程好像是完全独立的,只是在用户登录的时候验证一下,所以也不管什么类型了直接kotlin搞起。这里一开始想做成单独的app,把用离线(微信)的方式生成的密码发送给录音端和接收端,我把复制的代码都写好了!但一想可以把密码直接集成到值班软件里,这样可以省略发送和录入的过程,就得想办法发送给接收端,发现集成了之后就无法直观看到再微信发送,所以就想到用邮箱把生成的密码发给接收端,正好接收端需要一个通话邀请,微信里邮箱来邮件的声音也很明显了,一举两得,很棒。

        var appId = "(这里写你的appId)"//声网WebRTC的appId
        var appCertificate = "(这里写你的app安全证书)"//声网WebRTC的app的安全证书一类的
        var channelName = "WatchChannel1"//加入语音聊天的频道,登录时是appId、频道、密码一样时登录
        var uid = 0//声网WebRTC的userId,用户ID,0的时候表示不验证用户ID,上面三个对就能进频道
        var expirationTimeInSeconds = 86400//密码有效时间,最长24小时,秒为单位记
        val token = RtcTokenBuilder()//初始化
        val timestamp =
            (System.currentTimeMillis() / 1000 + expirationTimeInSeconds).toInt()//计算时间参数,和有效时间、当前时间有关
        val result = token.buildTokenWithUid(//生成密码,参数有appId、安全证书、频道名、用户ID、用户发流权限,时间参数
            appId, appCertificate,
            channelName, uid, RtcTokenBuilder.Role.Role_Publisher, timestamp//(默认)用户有发流权限。
        )
        tokenmade=result.toString()//用于转换成string格式
        Log.d("Server", tokenmade)//打印密码

3.7邮件发送提醒

用邮件来实现通话邀请很简单,只要发送成功就能提醒到,所以先开工做邮件发送,在github里找到了开源的邮件项目,直接引用一句,就可以在代码里直接写了很好,十分推荐,这里应该是我写这个最顺利的一步了,哈哈,也真正体会到引用成熟第三方的好处。
但是很快就遇到问题了,上面服务端生成的Token不能直接赋值给邮件的标题或内容,转码转了一天都没成功,最后把两个函数合到一起发现竟然成功了,哈哈,说明还是基本功不扎实,不仅是这种函数之间变量互相使用,类与类之间函数的使用也很成问题,这还是在只有一个页面的基础上,所以知识的缺陷还是很大的,再接再厉吧。

        val mail = Mail().apply {
            mailServerHost = "smtp.qq.com"//邮件代理服务器,这个要在qq邮箱里开启服务的
            mailServerPort = "587"//示例代码里的,同时QQ邮箱也让用这个,能用
            fromAddress = "XXX@qq.com"//发送端
            password = "16位授权码"//开启IMTP后给个这个第三方许可一样的东西
            toAddress = arrayListOf("XXX@qq.com")//收文,可以发好几个邮箱
            subject = "有情况快回来"//标题
            //邮件内容,最难弄,试过很多转换,没想到合一起就成功了,没找到换行符,凑合用吧
            content = "$appId---------------------WatchChannel1--------------------------$result"
//            attachFiles = arrayListOf(file)//附件,没用上,功能其实很强大,打开原文件或Mail函数看看
        }
                //以上用于定义邮件参数
        MailSender.getInstance().sendMail(mail)//发送邮件,都是示例代码

3.7其他无关紧要的

比如屏幕常亮、全局变量等一并写下,方便下次使用,还学习了生成正式签名,嘿嘿。

        // 设置屏幕常亮,避免熄屏那一声嗒导致分贝判断出现误判,另android:keepScreenOn="true"没用
        window.addFlags(WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON)

    //需要先在AndroidManifest.xml中添加<application ...android:name=".MyApplication"
    //之后就可以用MyApplication.context代表全局context了
    companion object {
        @SuppressLint("StaticFieldLeak")
        lateinit var context: Context
    }

    override fun onCreate() {
        super.onCreate()
        context = applicationContext
    }

3.8代码合并

完整代码下载地址,因为和本文同步编写,有很多改动,但思路一致,可供参考
https://download.csdn.net/download/u011358978/15499235

4.总结

最终效果:自动值班,有情况发邮件提醒,使用浏览器语音聊天,掌握值班室情况,尽可能排除一些导致误判的问题。安装一个app即可。
应用层逻辑:通过录音实时持续监听–>通过声音检测判断是否有值班情况–>有情况就先发邮箱通知我–>再通过声网WebRTC进入频道。
下步改进:1.界面。2.误判后的重启。3.接收端随时开启语音观察值班室情况。

  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 1
    评论
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值