Unity-AndroidStudio学习笔记-尝试在unity里打开安卓原生麦克风

Unity-AndroidStudio学习笔记

  • 尝试打开麦克风

很久没写java,这次回来整这个是因为项目之前需要语音功能,公司对接的图灵只支持amr和opus的音频格式上传进行识别,但是unity自带的麦克风的功能目前据我所知只支持wav和mp3.所以之前的解决办法一直是先通过能识别wav格式的百度api去传音频数据解析返回语音的识别结果,然后把结果以文字的方式再回传给图灵的api接口,从而间接的实现语音对话的功能.但是显而易见这样做的弊端有很多,首先就是需要额外的工作量和精力去接入百度,其次就是一个语音功能需要两个不同平台的api去实现显然是不可取的,但迫于当时时间和个人能力的原因,只能以这种方式实现
现在在不断的学习和对代码逻辑思维的不断清晰下,加上之前使用百度的语音唤醒功能让我对安卓的代码有了更深的理解,所以想挑战通过安卓原生的麦克风去直接回传amr和opus格式的音频文件给图灵接口,这样就可以去掉通过百度识别语音的步骤了

尝试打开麦克风

首先是需要在as里新建一个项目,然后新建一个library
导入unity-classes.jar文件到libs里,然后引用一下
具体的麦克风打开的步骤可以参考下面的这个帖子,我基本就是照猫画虎

https://www.cnblogs.com/vir56k/archive/2012/11/29/2794226.html

这里需要注意一下
由于安卓的更新,在不同的移动端上,安卓的不同版本在授权上有不同的处理方式
安卓6.0以上的话需要动态的在程序运行的时候去申请权限
这里可以参考其他大佬动态申请权限的帖子,小弟因为隔了一天,突然找不到用的大佬的帖子的网址了
哦我想起来了,这个动态申请权限的方法是看之前使用百度语音唤醒时看的帖子里的,在那个大佬的代码里初始化语音唤醒和识别的时候就做了这个操作,然后我就发现了他在初始化动态申请权限的方法里形参里加了一个Context,对java不了解的我点进去看了一下,发现这个其实就是一个activity,然后就用了上面讲的思路,把unity这个activity传回给了aar包里的这个初始化打胎申请权限的方法
这里贴一下自己看了之后做了一些变动的逻辑
这里是java代码

public String startRecording() {

        mediaRecorder = new MediaRecorder();
        mediaRecorder.setAudioSource(MediaRecorder.AudioSource.MIC);
        mediaRecorder.setOutputFormat(MediaRecorder.OutputFormat.AMR_NB);
        mediaRecorder.setAudioEncoder(MediaRecorder.AudioEncoder.AMR_NB);
        mediaRecorder.setOutputFile(newFileName());
        try {
            mediaRecorder.prepare();
        } catch (Exception e) {
            Log.e("test", e.getMessage());
        }
        mediaRecorder.start();
        return "finied";
    }
    public String newFileName() {
        //String mFileName = Environment.getExternalStorageDirectory() + File.separator + Environment.DIRECTORY_DCIM + File.separator;
        String mFileName = Environment.getExternalStorageDirectory()
                .getAbsolutePath();
        String s = new SimpleDateFormat("yyyy-MM-dd hhmmss")
                .format(new Date());
        mFileName += "/rcd_" + s + ".amr";
        Log.d("test", mFileName);
        return mFileName;
    }
	/**
     * android 6.0 以上需要动态申请权限
     */
    public void initPermission(Context context) {
        String permissions[] = {Manifest.permission.RECORD_AUDIO,
                Manifest.permission.ACCESS_NETWORK_STATE,
                Manifest.permission.INTERNET,
                Manifest.permission.WRITE_EXTERNAL_STORAGE
        };

        PackageManager pm = getActivityByContext(context).getPackageManager();
        boolean permission_readStorage = (PackageManager.PERMISSION_GRANTED ==
                pm.checkPermission("android.permission.RECORD_AUDIO", "com.cientx.tianguo"));
        boolean permission_network_state = (PackageManager.PERMISSION_GRANTED ==
                pm.checkPermission("android.permission.ACCESS_NETWORK_STATE", "com.cientx.tianguo"));
        boolean permission_internet = (PackageManager.PERMISSION_GRANTED ==
                pm.checkPermission("android.permission.INTERNET", "com.cientx.tianguo"));
        boolean permission_writeStorage = (PackageManager.PERMISSION_GRANTED ==
                pm.checkPermission("android.permission.WRITE_EXTERNAL_STORAGE", "com.cientx.tianguo"));

        if (!(permission_readStorage && permission_writeStorage && permission_network_state && permission_internet)) {
            if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
                getActivityByContext(context).requestPermissions(permissions, 0x01);
            }
        }

    }
 	

不过还需要注意一下,这个大佬的帖子里使用到了activity,但是我们是在unity项目里面的,是不需要安卓里自己再做一个activity的,那应该怎么办,鸡贼的我想到了一个方法,因为之前百度的语音唤醒功能里也是没有独立的activity的,但是他们还是能通过和上面这个大佬一样的方式获取到activity,于是我去翻了一下百度语音唤醒帖子的源码,发现那个大佬的实现方式是通过在unity里调用AndroidJavaClass类,然后传unity的路径,把项目的activity作为activity,然后回传给aar包
此处我只能说nb,大佬还是大佬
Unity-AndroidStudio学习笔记-尝试在unity里打开安卓原生麦克风_第1张图片
把unity作为activity之后回传给aar包里初始化动态申请权限的方法就可以了

有关动态获得权限的方法这一个难点

第二个难点是有关存储文件的问题
如下图
Unity-AndroidStudio学习笔记-尝试在unity里打开安卓原生麦克风_第2张图片

在麦克风初始化的时候是需要设置文件储存路径的
而有关储存的权限,似乎单单通过上面动态申请权限的方法是会出现问题的
就比如我在使用了上面的一顿操作之后,发现一直无法正确的调用麦克风,debug发现是因为

/storage/emulated/0/rcd_2021-09-11 094229.amr: open failed: EPERM (Operation

这个异常导致的,在百度和思考了良久后,我发现是因为获取读写文件权限的方式在不同的安卓版本也有不同的处理方式(可真是离谱给离谱他妈开门-离谱到家了)
除了上述动态申请权限的方法外还需要在AndroidManifest.xml文件里添加下面的这个标签

 <application android:requestLegacyExternalStorage="true" />

详细的可以看这个帖子和评论(主要是评论)

https://blog.csdn.net/qq_34884729/article/details/53284274

打包有两种方式,一个是aar,一个是jar
注意这里只能打aar包(从小弟目前的能力来说)
这是因为打出jar包的话他是不会打出AndroidManifest.xml这些文件的,也就是说里面的获得权限的标签就都没了,所以这个方法是不可取的
jar包的话比较适合用在一些不需要权限的功能比较好(个人感觉)

将aar里的unity-classes.jar删掉然后导入unity(老生常谈的问题了)
unity里调用相应的方法即可

贴一下完整的代码吧~
这是一个单例(是之前百度语音唤醒的那个大佬的帖子里的)
他把所有的功能都封装在了这个单例里,然后都是直接通过这个单例去调用需要的方法的

import android.content.Context;

public class CientBaiDuVoiceMainActivity {

    public static CientBaiDuVoiceMainActivity _instance;


    public static CientBaiDuVoiceMainActivity getInstance() {
        if (_instance == null) {
            _instance = new CientBaiDuVoiceMainActivity();
        }
        return _instance;
    }
    MroTest mroTest;

    public void InitMroTest(Context context){
        try{
            mroTest = new MroTest();
        }catch (Exception e){
            System.out.println(e.getMessage());
        }
        mroTest.initPermission(context);
    }
}

这是我自己在整理和百度语音唤醒大佬的帖子和网上其他帖子之后自己整理的麦克风的管理类

import android.Manifest;
import android.content.Context;
import android.content.pm.PackageManager;
import android.media.MediaRecorder;
import android.os.Build;
import android.os.Environment;
import android.util.Log;
import java.text.SimpleDateFormat;
import java.util.Date;

import static com.pure.testmcro.GetActivity.getActivityByContext;

public class MroTest {

    public MediaRecorder mediaRecorder;

    public String startRecording() {

        mediaRecorder = new MediaRecorder();
        mediaRecorder.setAudioSource(MediaRecorder.AudioSource.MIC);
        mediaRecorder.setOutputFormat(MediaRecorder.OutputFormat.AMR_NB);
        mediaRecorder.setAudioEncoder(MediaRecorder.AudioEncoder.AMR_NB);
        mediaRecorder.setOutputFile(newFileName());
        try {
            mediaRecorder.prepare();
        } catch (Exception e) {
            Log.e("test", e.getMessage());
        }
        mediaRecorder.start();
        return "finied";
    }

    public String newFileName() {
        //String mFileName = Environment.getExternalStorageDirectory() + File.separator + Environment.DIRECTORY_DCIM + File.separator;
        String mFileName = Environment.getExternalStorageDirectory()
                .getAbsolutePath();
        String s = new SimpleDateFormat("yyyy-MM-dd hhmmss")
                .format(new Date());
        mFileName += "/rcd_" + s + ".amr";
        Log.d("test", mFileName);
        return mFileName;
    }

    public void stopRecording() {
        mediaRecorder.stop();
        mediaRecorder.release();
        mediaRecorder = null;
    }

    public void test() {
        Log.d("pure", "test: 测试");
    }
    /**
     * android 6.0 以上需要动态申请权限
     */
    public void initPermission(Context context) {
        String permissions[] = {Manifest.permission.RECORD_AUDIO,
                Manifest.permission.ACCESS_NETWORK_STATE,
                Manifest.permission.INTERNET,
                Manifest.permission.WRITE_EXTERNAL_STORAGE
        };

        PackageManager pm = getActivityByContext(context).getPackageManager();
        boolean permission_readStorage = (PackageManager.PERMISSION_GRANTED ==
                pm.checkPermission("android.permission.RECORD_AUDIO", "com.cientx.tianguo"));
        boolean permission_network_state = (PackageManager.PERMISSION_GRANTED ==
                pm.checkPermission("android.permission.ACCESS_NETWORK_STATE", "com.cientx.tianguo"));
        boolean permission_internet = (PackageManager.PERMISSION_GRANTED ==
                pm.checkPermission("android.permission.INTERNET", "com.cientx.tianguo"));
        boolean permission_writeStorage = (PackageManager.PERMISSION_GRANTED ==
                pm.checkPermission("android.permission.WRITE_EXTERNAL_STORAGE", "com.cientx.tianguo"));

        if (!(permission_readStorage && permission_writeStorage && permission_network_state && permission_internet)) {
            if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
                getActivityByContext(context).requestPermissions(permissions, 0x01);
            }
        }

    }
}

还有一个工具类


import android.app.Activity;
import android.content.Context;
import android.content.ContextWrapper;

public class GetActivity {
    public static Activity getActivityByContext(Context context){
        while(context instanceof ContextWrapper){
            if(context instanceof Activity){
                return (Activity) context;
            }
            context = ((ContextWrapper) context).getBaseContext();
        }
        return null;
    }
}

然后是Androidmanifest文件(这里也是用的百度语音唤醒大佬的,所以有的其实是不需要用到的,看着删减吧)

<manifest xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:tools="http://schemas.android.com/tools"
    package="com.pure.testmcro">
    <!-- 通用权限 -->
    <uses-permission android:name="android.permission.INTERNET" />
    <uses-permission android:name="android.permission.ACCESS_NETWORK_STATE" />
    <uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE" />

    <!-- 语音识别权限 -->
    <uses-permission android:name="android.permission.RECORD_AUDIO" />
    <uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE" />

    <!-- 语音合成权限 -->
    <uses-permission android:name="android.permission.MODIFY_AUDIO_SETTINGS" />
    <uses-permission android:name="android.permission.WRITE_SETTINGS"
        tools:ignore="ProtectedPermissions" />
    <uses-permission android:name="android.permission.ACCESS_WIFI_STATE" />
    <uses-permission android:name="android.permission.CHANGE_WIFI_STATE" />
    <application android:requestLegacyExternalStorage="true" />
</manifest>

打包aar和jar的代码我也贴上来吧(百度一搜其实也有),注意打包的代码是需要放在build,gradle里的

task CopyPlugin(type: Copy) {
    dependsOn assemble
    from('build/outputs/aar')
    into('../../Assets/Plugins/Android')
    include(project.name + '-release.aar')
}
//上面是打包成aar的
///--------------分割线---------------------
//下面是打包成jar的
task deleteOldJar(type: Delete) {
    delete 'release/AndroidPlugin.jar'
}

//task to export contents as jar
task exportJar(type: Copy) {
    from('build/intermediates/packaged-classes/release/') //从这个目录下取出默认jar包
    into('release/')
    include('classes.jar')
    ///Rename the jar
    rename('classes.jar', 'AndroidPlugin.jar')
}

exportJar.dependsOn(deleteOldJar, build)

然后是unity里的代码

using System.Collections;
using System.Collections.Generic;
using UnityEngine;

public class TextMcro : MonoBehaviour
{
    AndroidJavaObject m_AndroidPluginObj;
    AndroidJavaClass _androidJC;
    AndroidJavaObject m_Android;
    AndroidJavaClass jc;

    AndroidJavaObject test1;
    // Start is called before the first frame update
    void Start()
    {
        
    }

    // Update is called once per frame
    void Update()
    {
        
    }

    public void StartRecoding()
    {
        test1 = new AndroidJavaObject("com.pure.testmcro.MroTest");
        string result = test1.Call<string>("startRecording");
        Debug.Log(result);
    }

    public void EndRecoding()
    {
        test1 = new AndroidJavaObject("com.pure.testmcro.MroTest");
        test1.Call("stopRecording");
    }

    public void test()
    {
        test1 = new AndroidJavaObject("com.pure.testmcro.MroTest");
        test1.Call("test");
    }

    private void Awake()
    {
        _androidJC = new AndroidJavaClass("com.unity3d.player.UnityPlayer");
        if (_androidJC == null)
        {
            Debug.Log("JNI initialization failure.");
            return;
        }
        m_AndroidPluginObj = _androidJC.GetStatic<AndroidJavaObject>("currentActivity");
        jc = new AndroidJavaClass("com.pure.testmcro.CientBaiDuVoiceMainActivity");
        m_Android = jc.CallStatic<AndroidJavaObject>("getInstance");
        Debug.Log("和安卓交互成功!");
        if (m_Android == null)
        {
            Debug.Log("BaiduWake class ini failure");
        }
        InitREe();
    }

    public void InitREe()
    {
        if (m_Android != null)
        {
            m_Android.Call("InitMroTest", m_AndroidPluginObj);
            Debug.Log("唤醒初始化成功!");
        }
        else
            Debug.Log("AndroidPlugin is Null");
    }
}

把startrecording和stoprecording挂载到场景里的按钮然后打包就可以了

目前是可以完美调用startrecoding方法的,也能在内存中找到对应的文件,就是在stoprecording方法的时候会调用异常
在这里插入图片描述

这个问题目前还没解决,主要是怕前面的这些都给忘了,所以就先记录一下
更新一下,这个问题已解决
这个异常很显然是报了空指针的异常
所以仔细研究一下代码就知道了我unity里的生成了两个mrotest类,开始的方法一个,结束的方法一个,在as里就生成了两个一样的类,但是我们的麦克风是需要调用的一个类里的内容的,所以这个时候前面创建的单例就很有用了,我们只需要在单例里面再写个开启麦克风和关闭麦克风的方法就好了,这样就能保证用的是一个类里的同一个麦克风了.
在前面的CientBaiDuVoiceMainActivity类里直接添加下面的这两个方法就可以了
然后重新打包导入到unity

 public void startRecording(){
        try{
            mroTest.startRecording();
        }catch (Exception e){
            System.out.println(e.getMessage());
        }
    }

    public void stopRecording(){
        try{
            mroTest.stopRecording();
        }catch (Exception e){
            System.out.println(e.getMessage());
        }
    }

在unity里也需要修改一下代码
用awake里已经创建好的单例的m_Android这个对象去调用刚才写好的两个方法就可以了

 public void StartRecoding()
    {
        //test1 = new AndroidJavaObject("com.pure.testmcro.MroTest");
        //string result = test1.Call("startRecording");
        m_Android.Call("startRecording");
    }

    public void EndRecoding()
    {
        //test1 = new AndroidJavaObject("com.pure.testmcro.MroTest");
        //test1.Call("stopRecording");

        m_Android.Call("stopRecording");
    }

完美解决
然后就是看怎么把这个文件上传到图灵的接口里了~
加油呀

你可能感兴趣的:(java,unity,java,语音识别,unity3d)