• 讯飞离线语音合成新版(Aikit)-android sdk合成 demo(Java版本)


    前言:科大讯飞的新版离线语音合成,由于官网demo是kt语言开发的,咱也看不懂kt,搜遍了全网也没看到一个java版的新版离线语音demo,现记录下,留给有缘人参考!!!!!毕竟咱在这上面遇到了不少的坑。如果能留言指正,那就更好了。

    第一步:

    ​ 官网注册账号---》实名认证---》点击语音合成---》离线语音合成(新版)---》android sdk下载

    ​ sdk:下载的sdk是和当前账号绑定的,文档上方有appkey,secret等等

    第二步:

    ​ 安卓项目中设置以下权限,在AndroidManifest.xml中

        
        "android.permission.INTERNET"/>
        
        "android.permission.RECORD_AUDIO"/>
        
        "android.permission.ACCESS_NETWORK_STATE"/>
        
        "android.permission.ACCESS_WIFI_STATE"/>
        
        "android.permission.CHANGE_NETWORK_STATE"/>
        
        "android.permission.READ_PHONE_STATE"/>
        
        "android.permission.READ_CONTACTS"/>
        
        "android.permission.WRITE_EXTERNAL_STORAGE"/>
        
        "android.permission.READ_EXTERNAL_STORAGE"/>
        
        
        "android.permission.ACCESS_FINE_LOCATION"/>
        
        "android.permission.CAMERA" />
    
        
        "android.permission.WRITE_SETTINGS"
            tools:ignore="ProtectedPermissions" />
        android:requestLegacyExternalStorage="true"
    

    第三步:

    ​ 获取设备外部存储权限,后续需要把发音人的音频文件拷贝到设备中

        /**
         * 查看当前设备是否有存储权限:
         *      没有:请求获取权限
         *      有:复制当前项目assets下的xtts文件夹到设备根目录下(语音合成所必须的文件)
         * @param context
         */
        private void requestStoragePermission(Context context) {
            if (ContextCompat.checkSelfPermission(context, Manifest.permission.WRITE_EXTERNAL_STORAGE)
                    != PackageManager.PERMISSION_GRANTED) {
                ActivityCompat.requestPermissions(this,
                        new String[]{Manifest.permission.WRITE_EXTERNAL_STORAGE},
                        REQUEST_STORAGE_PERMISSION);
            }
        }
    
        /**
         * 请求获取存储权限
         * @param requestCode
         * @param permissions
         * @param grantResults
         */
        @Override
        public void onRequestPermissionsResult(int requestCode, String[] permissions, int[] grantResults) {
            super.onRequestPermissionsResult(requestCode, permissions, grantResults);
            if (requestCode == REQUEST_STORAGE_PERMISSION) {
                if (grantResults.length > 0 && grantResults[0] == PackageManager.PERMISSION_GRANTED) {
                    Log.i(TAG, "onRequestPermissionsResult: permission granted");
                    //再次判断存储权限是否已授予
                    boolean permission = FileUtils.hasStoragePermission(getApplicationContext());
                    if (!permission) {
                        Toast.makeText(getApplicationContext(), "没有存储权限,请重新获取!", Toast.LENGTH_SHORT).show();
                        return;
                    }
                    // 应用具有存储权限
                    Log.i(TAG,"成功获取存储权限!");
                    //判断xtts文件是否存在,不存在则复制,存在则忽略
                    FileUtils.createXttsDirAndCopyFile(getApplicationContext());
                } else {
                    Log.i(TAG, "onRequestPermissionsResult: permission denied");
                    Toast.makeText(this, "You Denied Permission", Toast.LENGTH_SHORT).show();
                }
            }
        }
    

    第四步:

    ​ 拷贝五个发音人的资源文件到当前设备的xtts文件目录下。这个文件在官方的demo文件中:

    package com.epean.store.utils;
    
    import android.Manifest;
    import android.content.Context;
    import android.content.pm.PackageManager;
    import android.content.res.AssetManager;
    import android.os.Build;
    import android.os.Environment;
    import android.util.Log;
    
    
    import com.epean.store.R;
    
    import java.io.File;
    import java.io.FileOutputStream;
    import java.io.IOException;
    import java.io.InputStream;
    import java.io.OutputStream;
    
    /**
     * 讯飞语音合成文件复制公共功能
     *	以下五个文件:
     *		e3fe94474_1.0.0_xTTS_CnCn_xiaoyan_2018_arm.irf
    *		e4b08c6f3_1.0.0_xTTS_CnCn_xiaofeng_2018_fix_arm.dat
    *		e4caee636_1.0.2_xTTS_CnCn_front_Emb_arm_2017.irf
    *		e05d571cc_1.0.0_xTTS_CnCn_xiaoyan_2018_fix_arm.dat
    *		ebdbd61ae_1.0.0_xTTS_CnCn_xiaofeng_2018_arm.irf
     */
    public class FileUtils {
        private static final String TAG = "FileUtils";
        // 获取外部存储路径
        public static String getExternalStoragePath() {
            return Environment.getExternalStorageDirectory().getAbsolutePath();
        }
    
        // 创建xtts目录
        public static void createDirectory(String directoryPath) {
            File directory = new File(directoryPath);
            if (!directory.exists()) {
                if (directory.mkdirs()) {
                    Log.d(TAG, "Directory created: " + directoryPath);
                } else {
                    Log.e(TAG, "Failed to create directory: " + directoryPath);
                }
            } else {
                Log.d(TAG, "Directory already exists: " + directoryPath);
            }
        }
    
        // 判断目录是否为空
        public static boolean isDirectoryEmpty(String directoryPath) {
            File directory = new File(directoryPath);
            if (directory.exists() && directory.isDirectory()) {
                File[] files = directory.listFiles();
                return files == null || files.length == 0;
            }
            return true;
        }
    
        // 递归复制文件
        public static void copyFiles(Context context, String sourceDir, String destinationDir) throws IOException {
            AssetManager assetManager = context.getAssets();
            String[] files = assetManager.list(sourceDir);
            if (files != null && files.length > 0) {
                createDirectory(destinationDir);
                for (String fileName : files) {
                    String sourcePath = sourceDir + File.separator + fileName;
                    String destinationPath = destinationDir + File.separator + fileName;
    
                    if (assetManager.list(sourcePath).length > 0) {
                        // 如果是目录,递归复制目录
                        copyFiles(context, sourcePath, destinationPath);
                    } else {
                        // 如果是文件,复制文件
                        copyFile(context, sourcePath, destinationPath);
                    }
                }
            }
        }
    
        // 复制文件
        public static void copyFile(Context context, String sourcePath, String destinationPath) throws IOException {
            InputStream inputStream = null;
            OutputStream outputStream = null;
    
            try {
                inputStream = context.getAssets().open(sourcePath);
                outputStream = new FileOutputStream(destinationPath);
                byte[] buffer = new byte[4096];
                int length;
                while ((length = inputStream.read(buffer)) > 0) {
                    outputStream.write(buffer, 0, length);
                }
                Log.d(TAG, "File copied: " + destinationPath);
            } finally {
                if (inputStream != null) {
                    try {
                        inputStream.close();
                    } catch (IOException e) {
                        Log.e(TAG, "Failed to close input stream", e);
                    }
                }
                if (outputStream != null) {
                    try {
                        outputStream.close();
                    } catch (IOException e) {
                        Log.e(TAG, "Failed to close output stream", e);
                    }
                }
            }
        }
    
        /**
         * 创建讯飞语音合成所必须的目录:xtts并复制音频文件
         * @param context
         */
        public static void createXttsDirAndCopyFile(Context context){
            // 获取外部存储路径
            String externalStoragePath = FileUtils.getExternalStoragePath();
    
            String xttsFolderPath = externalStoragePath + File.separator + context.getString(R.string.dir);
            // 创建xtts文件夹
            FileUtils.createDirectory(xttsFolderPath);
            // 判断xtts文件夹是否为空
            if (FileUtils.isDirectoryEmpty(xttsFolderPath)) {
                // 复制assets目录下的xtts文件夹中的所有文件到外部存储的xtts文件夹中
                try {
                    FileUtils.copyFiles(context, context.getString(R.string.dir), xttsFolderPath);
                } catch (IOException e) {
                    Log.e(TAG, "文件复制失败"+e.getMessage());
                }
            } else {
                // xtts文件夹不为空
                Log.d(TAG, "xtts folder is not empty. Skipping the operation.");
            }
        }
    
        public static boolean hasStoragePermission(Context context) {
            if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
                int permissionResult = context.checkSelfPermission(Manifest.permission.WRITE_EXTERNAL_STORAGE);
                return permissionResult == PackageManager.PERMISSION_GRANTED;
            }
            return true;
        }
    
    }
    
    

    第五步:

    ​ 通用工具播放类,实现代码如下:

    ​ initSDK() :sdk整个项目只需要初始化一次

    ​ playAudio(String content, Context context):播放通用方法

    package com.epean.store.utils;
    
    import android.content.Context;
    import android.media.AudioFormat;
    import android.media.AudioManager;
    import android.media.AudioTrack;
    import android.os.Environment;
    import android.util.Log;
    
    import androidx.annotation.NonNull;
    
    import com.epean.store.R;
    import com.iflytek.aikit.core.AiEvent;
    import com.iflytek.aikit.core.AiHandle;
    import com.iflytek.aikit.core.AiHelper;
    import com.iflytek.aikit.core.AiListener;
    import com.iflytek.aikit.core.AiRequest;
    import com.iflytek.aikit.core.AiResponse;
    import com.iflytek.aikit.core.AiText;
    import com.iflytek.aikit.core.AuthListener;
    import com.iflytek.aikit.core.ErrType;
    
    
    import org.apache.commons.lang3.StringUtils;
    
    import java.io.File;
    
    import java.util.List;
    
    public class AudioPlayByKeyUtils {
        private static final String TAG = "AudioPlayByKeyUtils";
        private static int sampleRateInHz = 16000;
        private static int channelConfig = AudioFormat.CHANNEL_OUT_MONO;
        private static int audioFormat = AudioFormat.ENCODING_PCM_16BIT;
        private static int bufferSizeInBytes = AudioTrack.getMinBufferSize(sampleRateInHz, channelConfig, audioFormat);
        //语音合成文件缓存数组
        private static byte[] cacheArray;
        private static AiHandle handle;
        //播放组件
        private static AudioTrack audioTrack = new AudioTrack(AudioManager.STREAM_MUSIC, sampleRateInHz, channelConfig, audioFormat, bufferSizeInBytes, AudioTrack.MODE_STREAM);
        //SDK初始化
        public static void initSDK(Context context){
            try {
                //外部存储绝对路径
                File externalStorageDirectory = Environment.getExternalStorageDirectory();
                // 初始化参数构建
                AiHelper.Params params = AiHelper.Params.builder()
                        .appId(context.getString(R.string.appId))
                        .apiKey(context.getString(R.string.apiKey))
                        .apiSecret(context.getString(R.string.apiSecret))
                        .workDir(externalStorageDirectory.getAbsolutePath() +File.separator+ context.getString(R.string.dir))//SDK工作路径,这里为绝对路径
                        .authInterval(60*60*24) //授权更新间隔
                        .build();
                // 初始化
                AiHelper.getInst().init(context, params);
                // 注册SDK 初始化状态监听
                AiHelper.getInst().registerListener(coreListener);
                // 注册能力结果监听
                AiHelper.getInst().registerListener(context.getString(R.string.enginID), aiRespListener);
            }catch (Exception e){
                Log.e(TAG,"语音合成初始化出现异常"+e.getMessage());
            }
        }
    
        public static void playAudio(String content, Context context){
            if (StringUtils.isEmpty(content)){
                Log.e(TAG,"播报内容不能为空!");
                return;
            }
            //已初始化则略过
            initSDK(context);
            //避免脏数据
            cacheArray = null;
            //音量及播报人等参数设置
            AiRequest.Builder paramBuilder = audioParam();
    
            handle  = AiHelper.getInst().start(context.getString(R.string.enginID),paramBuilder.build(),null);
            if (!handle.isSuccess()) {
                Log.e(TAG, "ERROR::START | handle code:" + handle.getCode());
                return;
            }
    
            //自定义文本参数
            AiRequest.Builder dataBuilder = contentParame(content);
            //开始合成,合成结果可通过回调接口获取
            int ret = AiHelper.getInst().write(dataBuilder.build(), handle);
            //ret 值为0 写入成功;非0失败
            if (ret != 0) {
                String error = "start write failed" + ret;
                Log.e(TAG, error);
            }
        }
        /**
         * 封装自定义文本参数
         * @param content
         * @return
         */
        @NonNull
        private static AiRequest.Builder contentParame(String content) {
            AiRequest.Builder dataBuilder = AiRequest.builder();
            //输入文本数据
            AiText textData = AiText
                    .get("text")
                    .data(content) //输入文本
                    .valid();
            dataBuilder.payload(textData);
            return dataBuilder;
        }
    
        /**
         * 音量及播报人等参数设置
         */
        @NonNull
        private static AiRequest.Builder audioParam() {
            AiRequest.Builder paramBuilder = AiRequest.builder();
            paramBuilder.param("vcn", "xiaoyan");
            paramBuilder.param("language", 1);
            paramBuilder.param("pitch", 50);
            paramBuilder.param("volume", 50);
            paramBuilder.param("speed", 50);
            paramBuilder.param("reg", 0);
            paramBuilder.param("rdn", 0);
            paramBuilder.param("textEncoding", "UTF-8");
            return paramBuilder;
        }
    
        /**
         * SDK监听回调
         */
        private static AuthListener coreListener = new AuthListener() {
            @Override
            public void onAuthStateChange(final ErrType type, final int code) {
                Log.i(TAG,"core listener code:" + code);
                switch (type) {
                    case AUTH:
                        Log.i(TAG,"SDK状态:授权结果码" + code);
                        break;
                    case HTTP:
                        Log.i(TAG,"SDK状态:HTTP认证结果" + code);
                        break;
                    default:
                        Log.i(TAG,"SDK状态:其他错误");
                }
            }
        };
    
    
        /**
         *能力监听回调
         */
        private static AiListener aiRespListener = new AiListener() {
            //获取合成结果,封装到缓存数组中
            @Override
            public void onResult(int handleID, List outputData, Object usrContext) {
                if (outputData == null || outputData.isEmpty()) {
                    return;
                }
                if (null != outputData && outputData.size() > 0) {
                    for (int i = 0; i < outputData.size(); i++) {
                        byte[] bytes = outputData.get(i).getValue();
                        if (bytes == null) {
                            continue;
                        }else {
                            if (cacheArray == null) {
                                cacheArray = bytes;
                            } else {
                                byte[] resBytes = new byte[(cacheArray != null ? cacheArray.length : 0) + bytes.length];
                                if (cacheArray != null) {
                                    System.arraycopy(cacheArray, 0, resBytes, 0, cacheArray.length);
                                }
                                System.arraycopy(bytes, 0, resBytes, cacheArray != null ? cacheArray.length : 0, bytes.length);
                                cacheArray = resBytes;
                            }
                        }
                    }
                }
            }
    
            @Override
            public void onEvent(int handleID, int event, List eventData, Object usrContext){
                if (event == AiEvent.EVENT_UNKNOWN.getValue()){
                }
                if (event == AiEvent.EVENT_START.getValue()){
                }
                if (event == AiEvent.EVENT_END.getValue()){
                    if (handle != null){
                        int rets = AiHelper.getInst().end(handle);
                        if (rets != 0) {
                            String error = "end failed" + rets;
                            Log.e(TAG, error);
                        }
                    }
                    cacheArray = null;
                }
                if (event == AiEvent.EVENT_PROGRESS.getValue()){
                    if (cacheArray != null) {
                        audioTrack.write(cacheArray, 0, cacheArray.length);
                        audioTrack.play();
                    }
                }
            }
            @Override
            public void onError(int handleID, int err, String msg, Object usrContext){
                if (handle != null){
                    int rets = AiHelper.getInst().end(handle);
                    if (rets != 0) {
                        String error = "end failed" + rets;
                        Log.e(TAG, error);
                    }
                }
            }
        };
    
        /**
         * 释放资源
         */
        public static void destory(){
    //        AiHelper.getInst().unInit();
            cacheArray = null;
        }
    }
    
  • 相关阅读:
    web前端面试题附答案003-说一下你都用过那些格式的图片
    React 全栈体系(八)
    坦克大战①
    SAP Table function 执行报错 code CX_SQL_EXCEPTION feature not supported 该如何分析
    深入浅出带你走进 RocksDB
    GIS前端编程-地理事件动态模拟
    我在京东的第417天:陷入了情绪的泥沼
    24v转12v转9v转5v转4.2v降压电源芯片AH8788
    python自学入门(打卡十)2022-11-22
    近期 yyds 的 GitHub 项目
  • 原文地址:https://www.cnblogs.com/wpy188/p/17511786.html