这里只是功能实现(我的非android开发),可能有不少更好的实现方式,该功能的开发是以前看到过阿里的实时语音转文字的接口,当时就想把这个功能作到手机上,本身又是java开发,就百度了点基础的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; } } } }
服务器端,接收流信息上传阿里获取实时转换结果,这里应该有本身的控制策略,我这里只作了实现,策略没有哦web
这部分代码是接收和处理客户端请求json
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(); } } } } }
下面这部分代码是处理流和阿里云服务接口的交互,用到阿里的包这个自行引入,(最近科大讯飞也提供了相似的接口,貌似效果更好)能够替换这部分逻辑就能够api
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就能够实现语音是说转文字了哦服务器
补充代码websocket
<?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>