这里只是功能实现(个人非android开发),可能有很多更好的实现方式,该功能的开发是之前看到过阿里的实时语音转文字的接口,当时就想把这个功能做到手机上,自己又是java开发,就百度了点基础的android知识做了个简单的实现。
主方法(手机端),主要任务采集声音,流形式发送到后台
package com.hht.myapplication;
import android.Manifest;
import android.text.ClipboardManager;
import android.content.Context;
import android.content.pm.PackageManager;
import android.media.AudioFormat;
import android.media.AudioRecord;
import android.media.MediaRecorder;
import android.os.Bundle;
import android.os.Handler;
import android.support.v4.app.ActivityCompat;
import android.support.v4.content.ContextCompat;
import android.support.v7.app.AppCompatActivity;
import android.text.method.ScrollingMovementMethod;
import android.view.View;
import android.widget.TextView;
import android.widget.Toast;
import java.io.DataInputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.net.Socket;
/**
* 语音小助手
* hht
*/
public class MainActivity extends AppCompatActivity {
//接收转换的文字流
DataInputStream dis;
//音频流上传通道
OutputStream ous;
String serverIp = "101.201.XXX.XXX";
int serverPort = 5555;
private TextView realText;
private TextView finalText;
//音频相关
AudioRecord audioRecord=null;
int bufferSize=0;//最小缓冲区大小
int sampleRateInHz = 16000;//采样率
int channelConfig = AudioFormat.CHANNEL_IN_DEFAULT; //单声道
int audioFormat = AudioFormat.ENCODING_PCM_16BIT; //量化位数
private boolean isRecording = true;
private Handler handler=null;
private String content ;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
realText = (TextView) findViewById(R.id.textView2);
finalText = (TextView) findViewById(R.id.textView);
finalText.setMovementMethod(ScrollingMovementMethod.getInstance()) ;
handler = new Handler();
if (ContextCompat.checkSelfPermission(this, Manifest.permission.CALL_PHONE) != PackageManager.PERMISSION_GRANTED) {
ActivityCompat.requestPermissions(this, new String[]{Manifest.permission.RECORD_AUDIO},1);
} else {
init();
}
}
public void onClickCopy(View v) {
// 从API11开始android推荐使用android.content.ClipboardManager
// 为了兼容低版本我们这里使用旧版的android.text.ClipboardManager,虽然提示deprecated,但不影响使用。
ClipboardManager cm = (ClipboardManager) getSystemService(Context.CLIPBOARD_SERVICE);
// 将文本内容放到系统剪贴板里。
cm.setText(finalText.getText());
Toast.makeText(this, "复制成功,可以发给朋友们了。", Toast.LENGTH_LONG).show();
}
public void init(){
System.out.println("初始化录音");
bufferSize = AudioRecord.getMinBufferSize(sampleRateInHz,channelConfig, audioFormat)+1000;//计算最小缓冲区
try{
audioRecord = new AudioRecord(MediaRecorder.AudioSource.MIC,sampleRateInHz,channelConfig, audioFormat, bufferSize);//创建AudioRecorder对象
Runnable startRunnable = new Runnable(){
@Override
public void run() {
connect();
new sendDataThread().start();
new getDataThread().start();
}
};
new Thread(startRunnable).start();
}catch (Exception e){
e.printStackTrace();
}
System.out.println("初始化录音成功");
}
public void connect(){
try{
Socket socket = new Socket(serverIp,serverPort);
InputStream is=socket.getInputStream();
ous = socket.getOutputStream();
dis=new DataInputStream(is);
}catch(IOException e){
e.printStackTrace();
}
}
//发送数据
class sendDataThread extends Thread {
public void run() {
byte[] buffer = new byte[bufferSize];
audioRecord.startRecording();//开始录音
int r = 0;
try {
while (isRecording&&audioRecord.read(buffer,0,bufferSize)>0) {
ous.write(buffer);
ous.flush();
}
audioRecord.stop();//停止录音
} catch (IOException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
}
}
//接收数据
class getDataThread extends Thread {
public void run() {
while (true) {
String msg;
try {
msg = dis.readUTF();
if (!"status".equals(msg)&&!"".equals(msg)) {//status 为心跳检测
content=msg;
handler.post(runnableUi);
}
} catch (IOException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
}
}
}
// 构建Runnable对象,在runnable中更新界面
Runnable runnableUi=new Runnable(){
@Override
public void run() {
//更新界面
realText.setText(content);
finalText.append(content);
//跳转到底部
int offset=finalText.getLineCount()*finalText.getLineHeight();
if(finalText.getLineCount()>15 && offset>finalText.getHeight()){
finalText.scrollTo(0,offset-finalText.getHeight());
}
}
};
@Override
public void onRequestPermissionsResult(int requestCode, String permissions[], int[] grantResults) {
switch (requestCode) {
case 1: {
if (grantResults.length > 0 && grantResults[0] == PackageManager.PERMISSION_GRANTED) {
// 权限被用户同意,可以去放肆了。
init();
} else {
// 权限被用户拒绝了,洗洗睡吧。
}
return;
}
}
}
}
服务器端,接收流信息上传阿里获取实时转换结果,这里应该有自己的控制策略,我这里只做了实现,策略没有哦
这部分代码是接收和处理客户端请求
package com.hmkx.freezingapi.rest.jkjweb;
import java.io.Closeable;
import java.io.DataOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.net.ServerSocket;
import java.net.Socket;
import java.util.ArrayList;
import java.util.List;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import com.hmkx.freezingapi.util.ali.RealtimeAsrDemo;
/**
* 语音转换服务 监听
*
* @author hht
* @since 2018-06-12
*/
public class AudioChangeServerThread extends Thread {
private static Logger log = LoggerFactory.getLogger(AudioChangeServerThread.class);
static int cCount = 0;//当前连接数
static int maxCount = 10;//最大连接数
static ServerSocket server = null;
static List<DataOutputStream>socketsOut = new ArrayList<DataOutputStream>();
static List<Socket>sockets = new ArrayList<Socket>();
public static boolean runStatus = false;//标记状态 关闭后不再接受连接
public void run(){
runStatus = true;
if(server==null){//语音转换服务监听
try {
server = new ServerSocket(5555);
} catch (IOException e) {
e.printStackTrace();
}
while (runStatus) {
//每接受一个连接,清理半关闭的连接
validateSocket();
if(cCount<=maxCount){
try {
Socket temp = server.accept();
sockets.add(temp);
OutputStream os=temp.getOutputStream();
DataOutputStream dos=new DataOutputStream(os);
InputStream ins = temp.getInputStream();
RealtimeAsrDemo lun = new RealtimeAsrDemo(ins,dos);
log.error("audio change start ....");
new Thread(lun).start();
cCount++;
} catch (IOException e) {
log.error("has connected io exception");
}
log.error("has connected "+cCount);
}
}
}
}
/**
* 清楚已经断开的连接
* @param i
*/
public static void removeSocket(int i){
Aclose(sockets.get(i));
Aclose(socketsOut.get(i));
sockets.remove(i);
socketsOut.remove(i);
cCount--;
}
/**
* 清楚已经断开的连接
* @param i
*/
public void validateSocket(){
for(int i=0;i<socketsOut.size();i++){
try {
DataOutputStream temp = socketsOut.get(i);
temp.writeUTF("status");
temp.flush();
} catch (Exception e) {
removeSocket(i);
}
}
}
/**
* 连接关闭方法
* @param o
*/
public static void Aclose(Object...o){
for(int i=0;i<o.length;i++){
if(o[i] instanceof Closeable){
try {
Closeable c = (Closeable)o[i];
c.close();
} catch (IOException e) {
e.printStackTrace();
}
}
if(o[i] instanceof ServerSocket){
try {
ServerSocket c = (ServerSocket)o[i];
c.close();
} catch (IOException e) {
e.printStackTrace();
}
}
if(o[i] instanceof Socket){
try {
Socket c = (Socket)o[i];
c.close();
} catch (IOException e) {
e.printStackTrace();
}
}
}
}
}
下面这部分代码是处理流和阿里云服务接口的交互,用到阿里的包这个自行引入,(最近科大讯飞也提供了类似的接口,貌似效果更好)可以替换这部分逻辑就可以
package com.hmkx.freezingapi.util.ali;
import java.io.DataOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.PrintWriter;
import java.io.StringWriter;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import com.alibaba.fastjson.JSON;
import com.alibaba.idst.nls.realtime.NlsClient;
import com.alibaba.idst.nls.realtime.NlsFuture;
import com.alibaba.idst.nls.realtime.event.NlsEvent;
import com.alibaba.idst.nls.realtime.event.NlsListener;
import com.alibaba.idst.nls.realtime.protocol.NlsRequest;
import com.alibaba.idst.nls.realtime.protocol.NlsResponse;
import com.hmkx.freezingapi.rest.jkjweb.AudioChangeServerThread;
/**
* 语音实时转文字工具类
*
* @author hht
*
*/
public class RealtimeAsrDemo implements NlsListener,Runnable{
protected NlsClient client = new NlsClient();
protected static final String asrSC = "pcm";
static Logger logger = LoggerFactory.getLogger(RealtimeAsrDemo.class);
public String appKey = "XXX";
protected String ak_id = "XXX";
protected String ak_secret = "XXX";
private InputStream fis = null;
private DataOutputStream dos = null;
public RealtimeAsrDemo(InputStream fis, DataOutputStream dos) {
this.fis = fis;
this.dos = dos;
}
public void shutDown() {
logger.error("close NLS client manually!");
client.close();
logger.error("demo done");
}
public void init() {
logger.error("init Nls client...");
client.init();
}
public void process() {
logger.error("open audio file...");
if (fis != null) {
logger.error("create NLS future");
process(fis);
logger.error("calling NLS service end");
}
}
public void process(InputStream ins) {
try {
NlsRequest req = buildRequest();
NlsFuture future = client.createNlsFuture(req, this);
logger.error("call NLS service");
byte[] b = new byte[8000];
int len = 0;
while (AudioChangeServerThread.runStatus && (len = ins.read(b)) > 0 ) {
future.sendVoice(b, 0, len);
}
logger.error("send finish signal!");
future.sendFinishSignal();
logger.error("main thread enter waiting .");
future.await(100000);
} catch (Exception e) {
StringWriter sw = new StringWriter();
e.printStackTrace(new PrintWriter(sw));
logger.error(sw.toString());
}
}
protected NlsRequest buildRequest() {
NlsRequest req = new NlsRequest();
req.setAppkey(appKey);
req.setFormat(asrSC);
req.setResponseMode("streaming");
req.setSampleRate(16000);
// 用户根据[热词文档](~~49179~~) 设置自定义热词。
// 通过设置VocabularyId调用热词。
// req.setVocabularyId("");
// 设置关键词库ID 使用时请修改为自定义的词库ID
// req.setKeyWordListId("c1391f1c1f1b4002936893c6d97592f3");
// the id and the id secret
req.authorize(ak_id, ak_secret);
return req;
}
@Override
public void onMessageReceived(NlsEvent e) {
NlsResponse response = e.getResponse();
response.getFinish();
if (response.result != null) {
logger.error(response.getResult().toString());
if(response.getResult().getStatus_code()==0&&!"".equals(response.getText())){
String content = response.getText();
logger.error(content);
try {
dos.writeUTF(content);
dos.flush();
} catch (IOException e1) {
// TODO Auto-generated catch block
e1.printStackTrace();
}
}
} else {
logger.error(JSON.toJSONString(response));
}
}
@Override
public void onOperationFailed(NlsEvent e) {
logger.error("status code is {}, on operation failed: {}"+ e.getResponse().getStatusCode()+e.getErrorMessage());
}
@Override
public void onChannelClosed(NlsEvent e) {
logger.error("on websocket closed.");
}
@Override
public void run() {
init();
process();
shutDown();
}
}
这样就可以了,先启动服务端,然后启动app就可以实现语音是说转文字了哦
补充代码
<?xml version="1.0" encoding="utf-8"?>
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
android:id="@+id/activity_main"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:paddingBottom="@dimen/activity_vertical_margin"
android:paddingLeft="@dimen/activity_horizontal_margin"
android:paddingRight="@dimen/activity_horizontal_margin"
android:paddingTop="@dimen/activity_vertical_margin"
tools:context="com.hht.myapplication.MainActivity">
<TextView
android:text=""
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:id="@+id/textView2"
android:maxLines="5"
android:layout_alignParentTop="true"
android:layout_alignParentStart="true" />
<TextView
android:text=""
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:textIsSelectable="true"
android:focusable="true"
android:layout_centerVertical="true"
android:layout_alignParentStart="true"
android:scrollbars="vertical"
android:maxLines="15"
android:fadeScrollbars="false"
android:id="@+id/textView" />
<Button
android:layout_width="fill_parent"
android:layout_height="wrap_content"
android:onClick="onClickCopy"
android:text="复制上面的文本内容"
android:id="@+id/button"
android:layout_marginBottom="12dp"
android:layout_alignParentBottom="true"
android:layout_alignParentEnd="true"
android:layout_marginEnd="7dp" />
</RelativeLayout>