【Android App】实现在线语音合成功能(使用云知声平台和WebSocket 超详细 附源码)

news2025/2/23 6:57:28

需要源码和Jar包请点赞关注收藏后评论区留下QQ~~~

一、在线语音合成

虽然国产智能机大多集成了中文语音引擎,但是系统自带的语音工具无法满足商用要求,功能单一,所以势必引入第三方的语音引擎,依靠第三方提供的开发包统一支撑语音的交互操作

此处选用云知声引擎,对新生免费并且语音处理采用公开的WebSocket接口,无须引入额外的语音SDK,进入云知声网址后,在右上角找到AI开放平台,然后注册进入控制台创建应用即可

云知声官网

创建好后如下 要记住key和secret  后面要用v

云知声采用WebSocket接口交互,故而不管是语音合成还是语音识别,都需要定义WebSocket客户端的处理任务,云知声使用JSON字符串封装报文合成后的音频数据通过字节数组传回,具体合成过程如下

1:定义WebSocket客户端的语音合成任务 

实现以下几个功能

1:在请求报文中填写原始文本 音频格式和采样率等合成参数 再把JSON字符串传给WebSocket服务器

2:服务器分批返回字节数组形式的音频流 客户端需要将这些数据依次追加到存储卡中

3:在合成过程中,服务器还会数次返回JSON格式的应答报文 可能不止一个,只有报文中的end字段为true时才表示合成结束

2:把语音任务关联到WebSocket服务器 

此时要拼接完整的URL地址,包含之前在云知声平台的appkey和appsecret,填在SoundUtil这个类中

3:创建并启动语音合成任务

效果如下

 

 合成结束后效果如下 点击右上角的播放可以收听由文字转换的语音

 部分代码如下

需要源码请点赞关注收藏后评论区留下QQ~~~

Java类

package com.example.voice;

import android.media.AudioManager;
import android.media.MediaPlayer;
import android.os.Bundle;
import androidx.appcompat.app.AppCompatActivity;

import android.os.Environment;
import android.text.TextUtils;
import android.util.Log;
import android.view.View;
import android.widget.EditText;
import android.widget.TextView;
import android.widget.Toast;

import com.example.voice.constant.SoundConstant;
import com.example.voice.task.TtsClientEndpoint;
import com.example.voice.util.DateUtil;
import com.example.voice.util.SoundUtil;

public class VoiceComposeActivity extends AppCompatActivity {
    private final static String TAG = "VoiceComposeActivity";
    private TextView tv_option; // 声明一个文本视图对象
    private EditText et_compose_text; // 声明一个编辑框对象
    private TextView tv_result; // 声明一个文本视图对象
    private String mComposeFilePath; // 合成语音的文件路径
    private MediaPlayer mMediaPlayer = new MediaPlayer(); // 媒体播放器
    private boolean isPlaying = false; // 是否正在播音

    public void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_voice_compose);
        findViewById(R.id.iv_back).setOnClickListener(v -> finish());
        TextView tv_title = findViewById(R.id.tv_title);
        tv_title.setText("在线语音合成");
        tv_option = findViewById(R.id.tv_option);
        tv_option.setText("开始播放语音");
        tv_option.setVisibility(View.GONE);
        et_compose_text = findViewById(R.id.et_compose_text);
        tv_result = findViewById(R.id.tv_result);
        findViewById(R.id.btn_compose_voice).setOnClickListener(v -> {
            String text = et_compose_text.getText().toString();
            if (TextUtils.isEmpty(text)) {
                Toast.makeText(this, "请先输入待朗读的一段话", Toast.LENGTH_SHORT).show();
                return;
            }
            new Thread(() -> onlineCompose(text)).start(); // 启动在线合成语音的线程
        });
        tv_option.setOnClickListener(v -> {
            if (!isPlaying) { // 未在播音
                startPlay(); // 开始播音
            } else { // 正在播音
                stopPlay(); // 停止播音
            }
        });
    }

    // 在线合成语音
    private void onlineCompose(String text) {
        mComposeFilePath = String.format("%s/%s.mp3",
                getExternalFilesDir(Environment.DIRECTORY_DOWNLOADS).toString(),
                DateUtil.getNowDateTime());
        // 创建语音合成任务,并指定语音监听器
        TtsClientEndpoint task = new TtsClientEndpoint(this, mComposeFilePath, text, arg -> {
            if (Boolean.TRUE.equals(arg[0])) {
                Toast.makeText(this, "语音合成结束", Toast.LENGTH_SHORT).show();
                tv_result.setText("音频文件位于"+arg[2]);
                tv_option.setVisibility(View.VISIBLE);
            }
        });
        SoundUtil.startSoundTask(SoundConstant.URL_TTS, task); // 启动语音合成任务
    }

    // 开始播音
    private void startPlay() {
        isPlaying = !isPlaying;
        tv_option.setText("停止播放语音");
        mMediaPlayer.reset(); // 重置媒体播放器
        // 设置媒体播放器的完成监听器
        mMediaPlayer.setOnCompletionListener(mp -> stopPlay());
        mMediaPlayer.setAudioStreamType(AudioManager.STREAM_MUSIC); // 设置音频流的类型为音乐
        try {
            mMediaPlayer.setDataSource(mComposeFilePath); // 设置媒体数据的文件路径
            mMediaPlayer.prepare(); // 媒体播放器准备就绪
            mMediaPlayer.start(); // 媒体播放器开始播放
        } catch (Exception e) {
            e.printStackTrace();
        }
    }

    // 停止播音
    private void stopPlay() {
        tv_option.setText("开始播放语音");
        if (mMediaPlayer.isPlaying() || isPlaying) { // 如果正在播放
            isPlaying = !isPlaying;
            mMediaPlayer.stop(); // 停止播放
            Toast.makeText(this, "语音播放结束", Toast.LENGTH_LONG).show();
        }
    }

    @Override
    protected void onStop() {
        super.onStop();
        stopPlay(); // 停止播音
    }

    @Override
    protected void onDestroy() {
        super.onDestroy();
        mMediaPlayer.release(); // 释放媒体播放器
    }
}

SoundUtil类

package com.example.voice.util;

import android.util.Log;

import com.example.voice.constant.SoundConstant;

import java.net.URI;
import java.security.MessageDigest;

import javax.websocket.ContainerProvider;
import javax.websocket.Session;
import javax.websocket.WebSocketContainer;

public class SoundUtil {
    private final static String TAG = "SoundUtil";

    // 启动语音处理任务(语音识别或者语音合成)
    public static void startSoundTask(String url, Object task) {
        long time = System.currentTimeMillis();
        StringBuilder paramBuilder = new StringBuilder();
        // 填写该应用在开放平台上申请的密钥和密码
        paramBuilder.append(SoundConstant.APP_KEY).append(time).
                append(SoundConstant.APP_SECRET);
        String sign = getSHA256Digest(paramBuilder.toString());

        StringBuilder param = new StringBuilder();
        param.append("appkey=azkk2kwv5f22m5z4iebchxsetodz3y677chtzniz").append(SoundConstant.APP_KEY).append("&")
                .append("time=").append(time).append("&")
                .append("sign=").append(sign).append("&").append("appsecret=6d6f4426e005e6b7f9a7fee2a9fdda44");
        String fullUrl = url + param.toString();
        Log.d(TAG, "fullUrl="+fullUrl);
        // 获取WebSocket容器
        WebSocketContainer container = ContainerProvider.getWebSocketContainer();
        try {
            URI uri = new URI(fullUrl); // 创建一个URI对象
            // 连接WebSocket服务器,并关联语音处理任务获得连接会话
            Session session = container.connectToServer(task, uri);
            // 设置文本消息的最大缓存大小
            session.setMaxTextMessageBufferSize(1024 * 1024 * 10);
            // 设置二进制消息的最大缓存大小
            session.setMaxBinaryMessageBufferSize(1024 * 1024 * 10);
        } catch (Exception e) {
            e.printStackTrace();
        }
    }

    // 获得SHA摘要
    private static String getSHA256Digest(String data) {
        String digest = null;
        try {
            MessageDigest md = MessageDigest.getInstance("SHA-256");
            byte[] bytes = md.digest(data.getBytes("UTF-8"));
            digest = byte2hex(bytes);
        } catch (Exception e) {
            e.printStackTrace();
        }
        return digest;
    }

    // 二进制转十六进制字符串
    private static String byte2hex(byte[] bytes) {
        StringBuilder sign = new StringBuilder();
        for (int i = 0; i < bytes.length; i++) {
            String hex = Integer.toHexString(bytes[i] & 0xFF);
            if (hex.length() == 1) {
                sign.append("0");
            }
            sign.append(hex.toUpperCase());
        }
        return sign.toString();
    }

}

创作不易 觉得有帮助请 点赞关注收藏~~~

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/40905.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

【新知实验室】——腾讯云音视频TRTC体验

腾讯实时音视频 TRTC 是什么&#xff1f; 腾讯实时音视频&#xff08;Tencent Real-Time Communication&#xff0c;TRTC&#xff09;将腾讯21年来在网络与音视频技术上的深度积累&#xff0c;以多人音视频通话和低延时互动直播两大场景化方案&#xff0c;通过腾讯云服务向开发…

clickHouse基础语法

clichouse数据类型 整形 lnt8 8bit,1字节 &#xff08;-128-127&#xff09;lnt16 16bitlnt32 32bitlnt64 64bit 无符号整型 相比于上面&#xff0c;就是把负数部分挪到正数部分 Ulnt8 &#xff08;0-255&#xff09;Ulnt16Ulnt32Ulnt64 浮点型 Float32 也就是floatFloa…

Instant Neural Graphics Primitives with a Multiresolution Hash Encoding以及源码浅析

背景 现存的一些新视图合成的训练过程和渲染速度都比较慢&#xff0c;其原因是因为query point需要使用MLP编码&#xff0c;而且在一个采样空间中&#xff0c;存在很多无效的query point也要计算其density和color&#xff0c;从而出现很多冗余计算。 作者针对这个问题&#x…

MAUI 中使用 DI 及 MVVM

MAUI 中使用 DI 及 MVVM为什么要使用 依赖注入 和 MVVM如何在 MAUI 中使用依赖注入如何使用 MVVM不使用框架或组件定义一个 BaseViewModelMainViewModel 的实现MainPage 中进行 Binding使用组件优化前面的 ViewModel 代码基项目的效果为什么要使用 依赖注入 和 MVVM MVVM 和 依…

1535_TriCore编译器Tasking使用_汇编分区、内置函数以及伪指令

全部学习汇总&#xff1a; GreyZhang/TriCore_Tasking_Compiler_Skills: Some skills for Tasking compiler on AURIX platform. Happy hacking! (github.com) 看了一下这个章节的内容&#xff0c;原本看着页数很多拆分成了两次学习。后面发现剩下的这部分内容主要并不是框架性…

项目实战——项目上线

ps : 项目要在云服务器上部署&#xff0c;博主自己是用的腾讯云&#xff0c;大家可以选择购买合适的服务器进行部署 目录 一、AC终端操 1、ssh登录服务器 2、创建新用户 3、分配用户 sudo 权限 4、配置免密登录&#xff08;SSH&#xff09; 5、传递祖传文件给服务器 6、安…

JetpackCompose从入门到实战学习笔记2——Modifier的简单使用

JetpackCompose从入门到实战学习笔记2——Modifier的简单使用 1.Image的使用&#xff1a; Composable fun Image(modifier: Modifier) {Row {Image(painterResource(id R.mipmap.iv_pic),contentDescription stringResource(R.string.description),modifier modifier.size…

阿里P8熬了一个月肝出这份32W字Java面试手册,传到Git上目前star数达到了30K+

互联网行业竞争越来越严峻&#xff0c;面试也是越来越难&#xff0c;一直以来我都想整理一套完美的面试宝典&#xff0c;奈何难抽出时间&#xff0c;这套1000道的Java面试手册我整理了整整1个月&#xff0c;上传到Git上目前star数达到了30K这套互联网Java工程师面试题包括了&am…

【iOS】UITableView的动态Cell高度(Masonry)

动态cell高度评论长度不同引出的问题实现评论长度不同引出的问题 对于之前写的项目的评论部分&#xff0c;由于评论文字字数的不同会导致label高度不同&#xff0c;所以需要设定不同的cell高度来展示。 一开始使用了 CGSize labelSize [label.text boundingRectWithSize:CG…

数据结构与算法_AVL平衡二叉树_四种旋转,插入和删除

1 AVL平衡二叉树的概念 平衡二叉树在BST树基础上加了平衡操作。 BST树特点 &#xff1a;在BST树的基础上&#xff0c;引入了节点“平衡”的概念&#xff0c;任意一个节点的左右子树高度差不超过 1 &#xff0c;为了维持节点的平衡&#xff0c;引入了四种旋转操作&#xff0c;如…

MySQL的时区引起的前后端数据交互不畅的问题解决

MySQL的时区问题 一、问题起源 在使用swagger2进行代码测试时&#xff0c;执行完成后显示的时间与国内时间少了8个小时 强迫症的原因&#xff0c;就手贱了如下操作 ① 修改MySQL内的时间 set global time_zone 8:00; flush privileges;② show variables like “%time_zone%…

整数除法不用除号

给定两个整数 a 和 b &#xff0c;求它们的除法的商 a/b &#xff0c;要求不得使用乘号 *、除号 / 以及求余符号 % 。 注意&#xff1a; 整数除法的结果应当截去&#xff08;truncate&#xff09;其小数部分&#xff0c;例如&#xff1a;truncate(8.345) 8 以及 truncate(-2…

【路径规划】(2) A* 算法求解最短路,附python完整代码

大家好&#xff0c;今天和各位分享一下机器人路径规划中非常经典的 A* 算法&#xff0c;感兴趣的点个关注&#xff0c;文末有 python 代码&#xff0c;那我么开始吧。 1. 算法介绍 A* 算法是 1968 年 P.E.Hart[1]等人所提出的在全局地图环境中所有已知情形下求解最短路径问题的…

部分gcc预定义宏和函数栈帧的内存分布

本文简单基于树莓派8&#xff0c;linux4.4.50版本&#xff0c;32位arm cpu 尝试了解函数调用栈的内存分布的形态。使用gcc内置的宏 __builtin_frame_address 来打印栈帧内存上的信息&#xff0c;以及了解一下常用的gcc 内置的宏的输出。 针对 __builtin_frame_address 在gcc官网…

猴子也能学会的jQuery第十二期——jQuery遍历(上)

&#x1f4da;系列文章—目录&#x1f525; 猴子也能学会的jQuery第一期——什么是jQuery 猴子也能学会的jQuery第二期——引用jQuery 猴子也能学会的jQuery第三期——使用jQuery 猴子也能学会的jQuery第四期——jQuery选择器大全 猴子也能学会的jQuery第五期——jQuery样式操作…

PIC单片机3——外部中断

//RB2&#xff08;INT2&#xff09;作为外中断 #include <p18cxxx.h>/*18F系列单片机头文件*/ void PIC18F_High_isr(void);/*中断服务函数声明*/ void PIC18F_Low_isr(void); #pragma code high_vector_section0x8 /*高优先级中断响应时&#xff0c;会自动跳转到0x8处…

基于三相坐标系状态方程的感应电动机起动动态计算matlab程序

基于三相坐标系状态方程的感应电动机起动动态计算matlab程序 1 异步电动机动态数学模型的性质 电磁耦合是机电能量转换的必要条件&#xff0c;电流与磁通的乘积产生转矩&#xff0c;转速与磁通的乘积得到感应电动势。无论是直流电动机&#xff0c;还是交流电动机均如此。 交、直…

二十七、CANdelaStudio深入-编辑技巧(一致性检查)

本专栏将由浅入深的展开诊断实际开发与测试的数据库编辑,包含大量实际开发过程中的步骤、使用技巧与少量对Autosar标准的解读。希望能对大家有所帮助,与大家共同成长,早日成为一名车载诊断、通信全栈工程师。 本文介绍CANdelaStudio软件的一致性检查,欢迎各位朋友订阅、评论…

『LeetCode|每日一题』---->最小路径和

目录 1.每日一句 2.作者简介 『LeetCode|每日一题』最小路径和 1.每日一题 2.解题思路 2.1 思路分析 2.2 核心代码 2.3 完整代码 2.4 运行结果 1.每日一句 希望冬天的风能吹散一年里所有的遗憾 2.作者简介 &#x1f3e1;个人主页&#xff1a;XiaoXiaoChen-2716 &#x1f…

Vue3框架中CompositionAPI的基本使用(第十课)

1.Setup函数 理解&#xff1a;Vue3.0中一个新的配置项&#xff0c;值为一个函数。 setup是所有Composition API&#xff08;组合API&#xff09;“ 表演的舞台 ”。 组件中所用到的&#xff1a;数据、方法等等&#xff0c;均要配置在setup中。 setup函数的两种返回值&#x…