内容简介:在一家专注于AI音频公司做了一年,最近正处于预离职状态,正好刚刚给客户写了个关于android音频方面的demo,花了我足足一天赶出来的,感觉挺全面的决定再努力一点写个总结。 公司虽小,是和中科院声学所合作,也和讯飞一样也有自己关于音频的一系列语音识别/语音转写等引擎,麻雀虽小五脏俱全的感觉。 Android 音频这块其实也没那么神秘,神秘的地方有专门的C++/算法工程师等为我们负责,大家都懂得,我只是搬搬砖。这里不讲TTS/STT底层原理,怎么实现的呆了这么久我也只是一点点,一点点而已,涉及人耳听声相关
在一家专注于AI音频公司做了一年,最近正处于预离职状态,正好刚刚给客户写了个关于android音频方面的demo,花了我足足一天赶出来的,感觉挺全面的决定再努力一点写个总结。 公司虽小,是和中科院声学所合作,也和讯飞一样也有自己关于音频的一系列语音识别/语音转写等引擎,麻雀虽小五脏俱全的感觉。 Android 音频这块其实也没那么神秘,神秘的地方有专门的C++/算法工程师等为我们负责,大家都懂得,我只是搬搬砖。
主要涉及3点
- SpeechToText(音频转文本:STT): AudioRecord 录制音频 并用本地和Socket2中方式上传 。
- TextToSpeech (文本转语音:TTS) API获取音频流并用AudioTrack 播放。
- Speex 加密
这里不讲TTS/STT底层原理,怎么实现的呆了这么久我也只是一点点,一点点而已,涉及人耳听声相关函数/声波/傅里叶分析/一系列复杂函数, 这里不敢班门弄斧了 感兴趣请大家自行Google 。,
AudioRecord 介绍
AudioRecord 过程是一个IPC过程,Java层通过JNI调用到native层的AudioRecord,后者通过IAudioRecord接口跨进程调用到 AudioFlinger,AudioFlinger负责启动录音线程,将从录音数据源里采集的音频数据填充到共享内存缓冲区,然后应用程序侧从其里面拷贝数据到自己的缓冲区。
public AudioRecord(int audioSource, //指定声音源 MediaRecorder.AudioSource.MIC; int sampleRateInHz,//指定采样率 这里8000 int channelConfig,//指定声道数,单声道 int audioFormat, //指定8/16pcm 这里16bit 模拟信号转化为数字信号时的量化单位 int bufferSizeInBytes)//缓冲区大小 根据采样率 通道 量化参数决定 复制代码
1. STT 之本地录完之后文件形式上传
第二步再与socket 上传比较 //参数初始化 // 音频输入-麦克风
public final static int AUDIO_INPUT = MediaRecorder.AudioSource.MIC; public final static int AUDIO_SAMPLE_RATE = 8000; // 44.1KHz,普遍使用的频率 public final static int CHANNEL_CONFIG = AudioFormat.CHANNEL_IN_MONO; public final static int AUDIO_FORMAT = AudioFormat.ENCODING_PCM_16BIT; private int bufferSizeInBytes = 0;//缓冲区字节大小 private AudioRecord audioRecord; private volatile boolean isRecord = false;// volatile 可见性 设置正在录制的状态 复制代码
//创建AudioRecord
private void creatAudioRecord() { // 获得缓冲区字节大小 bufferSizeInBytes = AudioRecord.getMinBufferSize(AudioFileUtils.AUDIO_SAMPLE_RATE, AudioFileUtils.CHANNEL_CONFIG, AudioFileUtils.AUDIO_FORMAT); // MONO单声道 audioRecord = new AudioRecord(AudioFileUtils.AUDIO_INPUT, AudioFileUtils.AUDIO_SAMPLE_RATE, AudioFileUtils.CHANNEL_CONFIG, AudioFileUtils.AUDIO_FORMAT, bufferSizeInBytes); } // @Override public boolean onTouch(View v, MotionEvent event) { AudioRecordUtils utils = AudioRecordUtils.getInstance(); switch (event.getAction()) { case MotionEvent.ACTION_DOWN: utils.startRecordAndFile(); break; case MotionEvent.ACTION_UP: utils.stopRecordAndFile(); Log.d(TAG, "stopRecordAndFile"); stt(); break; } return false; } //开始录音 public int startRecordAndFile() { Log.d("NLPService", "startRecordAndFile"); // 判断是否有外部存储设备sdcard if (AudioFileUtils.isSdcardExit()) { if (isRecord) { return ErrorCode.E_STATE_RECODING; } else { if (audioRecord == null) { creatAudioRecord(); } audioRecord.startRecording(); // 让录制状态为true isRecord = true; // 开启音频文件写入线程 new Thread(new AudioRecordThread()).start(); return ErrorCode.SUCCESS; } } else { return ErrorCode.E_NOSDCARD; } } //录音线程 class AudioRecordThread implements Runnable { @Override public void run() { writeDateTOFile();// 往文件中写入裸数据 AudioFileUtils.raw2Wav(mAudioRaw, mAudioWav, bufferSizeInBytes);// 给裸数据加上头文件 } } // 往文件中写入裸数据 private void writeDateTOFile() { Log.d("NLPService", "writeDateTOFile"); // new一个byte数组用来存一些字节数据,大小为缓冲区大小 byte[] audiodata = new byte[bufferSizeInBytes]; FileOutputStream fos = null; int readsize = 0; try { File file = new File(mAudioRaw); if (file.exists()) { file.delete(); } fos = new FileOutputStream(file);// 建立一个可存取字节的文件 } catch (Exception e) { e.printStackTrace(); } while (isRecord) { readsize = audioRecord.read(audiodata, 0, bufferSizeInBytes); if (AudioRecord.ERROR_INVALID_OPERATION != readsize && fos != null) { try { fos.write(audiodata); } catch (IOException e) { e.printStackTrace(); } } } try { if (fos != null) fos.close();// 关闭写入流 } catch (IOException e) { e.printStackTrace(); } } //add wav header public static void raw2Wav(String inFilename, String outFilename, int bufferSizeInBytes) { Log.d("NLPService", "raw2Wav"); FileInputStream in = null; RandomAccessFile out = null; byte[] data = new byte[bufferSizeInBytes]; try { in = new FileInputStream(inFilename); out = new RandomAccessFile(outFilename, "rw"); fixWavHeader(out, AUDIO_SAMPLE_RATE, 1, AudioFormat.ENCODING_PCM_16BIT); while (in.read(data) != -1) { out.write(data); } in.close(); out.close(); } catch (FileNotFoundException e) { e.printStackTrace(); } catch (IOException e) { e.printStackTrace(); } } private static void fixWavHeader(RandomAccessFile file, int rate, int channels, int format) { try { int blockAlign; if (format == AudioFormat.ENCODING_PCM_16BIT) blockAlign = channels * 2; else blockAlign = channels; int bitsPerSample; if (format == AudioFormat.ENCODING_PCM_16BIT) bitsPerSample = 16; else bitsPerSample = 8; long dataLen = file.length() - 44; // hard coding byte[] header = new byte[44]; header[0] = 'R'; // RIFF/WAVE header header[1] = 'I'; header[2] = 'F'; header[3] = 'F'; header[4] = (byte) ((dataLen + 36) & 0xff); header[5] = (byte) (((dataLen + 36) >> 8) & 0xff); header[6] = (byte) (((dataLen + 36) >> 16) & 0xff); header[7] = (byte) (((dataLen + 36) >> 24) & 0xff); header[8] = 'W'; header[9] = 'A'; header[10] = 'V'; header[11] = 'E'; header[12] = 'f'; // 'fmt ' chunk header[13] = 'm'; header[14] = 't'; header[15] = ' '; header[16] = 16; // 4 bytes: size of 'fmt ' chunk header[17] = 0; header[18] = 0; header[19] = 0; header[20] = 1; // format = 1 header[21] = 0; header[22] = (byte) channels; header[23] = 0; header[24] = (byte) (rate & 0xff); header[25] = (byte) ((rate >> 8) & 0xff); header[26] = (byte) ((rate >> 16) & 0xff); header[27] = (byte) ((rate >> 24) & 0xff); header[28] = (byte) ((rate * blockAlign) & 0xff); header[29] = (byte) (((rate * blockAlign) >> 8) & 0xff); header[30] = (byte) (((rate * blockAlign) >> 16) & 0xff); header[31] = (byte) (((rate * blockAlign) >> 24) & 0xff); header[32] = (byte) (blockAlign); // block align header[33] = 0; header[34] = (byte) bitsPerSample; // bits per sample header[35] = 0; header[36] = 'd'; header[37] = 'a'; header[38] = 't'; header[39] = 'a'; header[40] = (byte) (dataLen & 0xff); header[41] = (byte) ((dataLen >> 8) & 0xff); header[42] = (byte) ((dataLen >> 16) & 0xff); header[43] = (byte) ((dataLen >> 24) & 0xff); file.seek(0); file.write(header, 0, 44); } catch (Exception e) { } finally { } } //文件上传 结果回调 public void stt() { File voiceFile = new File(AudioFileUtils.getWavFilePath()); if (!voiceFile.exists()) { return; } RequestBody requestBody = RequestBody.create(MediaType.parse("multipart/form-data"), voiceFile); MultipartBody.Part file = MultipartBody.Part.createFormData("file", voiceFile.getName(), requestBody); NetRequest.sAPIClient.stt(RequestBodyUtil.getParams(), file) .observeOn(AndroidSchedulers.mainThread()) .subscribe(new Action1<STT>() { @Override public void call(STT result) { if (result != null && result.getCount() > 0) { sttTv.setText("结果: " + result.getSegments().get(0).getContent()); } } }); } //记得关闭AudioRecord private void stopRecordAndFile() { if (audioRecord != null) { isRecord = false;// 停止文件写入 audioRecord.stop(); audioRecord.release();// 释放资源 audioRecord = null; } } 复制代码
2. STT 之AudioRecord录制websocket 在线传输
WebSocket介绍: 我只记住一点点:它是应用层协议 ,就像http 也是,不过它是一种全双工通信, socket 只是TCP/IP 的封装,不算协议。websocket 第一次需要以http 接口建立长连接,就这么点了。
//MyWebSocketListener Websocket 回调
class MyWebSocketListener extends WebSocketListener { @Override public void onOpen(WebSocket webSocket, Response response) { output("onOpen: " + "webSocket connect success"); STTWebSocketActivity.this.webSocket = webSocket; startRecordAndFile(); //看清楚了开始录音函数在这里,原因由于涉及回调,当分离时候 处理逻辑复杂 //,而且第二次录音时候由于服务端WebSocket已经关闭 ,录音数据不能正常传输,需要重新建立连接 } @Override public void onMessage(WebSocket webSocket, final String text) { runOnUiThread(new Runnable() { @Override public void run() { sttTv.setText("Stt result:" + text); } }); output("onMessage1: " + text); } @Override public void onMessage(WebSocket webSocket, ByteString bytes) { output("onMessage2 byteString: " + bytes); } @Override public void onClosing(WebSocket webSocket, int code, String reason) { output("onClosing: " + code + "/" + reason); } @Override public void onClosed(WebSocket webSocket, int code, String reason) { output("onClosed: " + code + "/" + reason); } @Override public void onFailure(WebSocket webSocket, Throwable t, Response response) { output("onFailure: " + t.getMessage()); } private void output(String s) { Log.d("NLPService", s); } } 补充:AudioRecord创建与前面相同 // okhttp 创建websocket 并设置监听 private void createWebSocket() { Request request = new Request.Builder().url(sttApi).build(); NetRequest.getOkHttpClient().newWebSocket(request, socketListener); } class AudioRecordThread implements Runnable { @Override public void run() { //byteBuffer 缓冲区 (内存地址以数组形式排列,一个基本数据类型的数组) ByteBuffer audioBuffer = ByteBuffer.allocateDirect(bufferSizeInBytes).order(ByteOrder.LITTLE_ENDIAN);//小端模式 int readSize = 0; Log.d(TAG, "isRecord=" + isRecord); while (isRecord) { readSize = audioRecord.read(audioBuffer, audioBuffer.capacity()); if (readSize == AudioRecord.ERROR_INVALID_OPERATION || readSize == AudioRecord.ERROR_BAD_VALUE) { Log.d("NLPService", "Could not read audio data."); break; } boolean send = webSocket.send(ByteString.of(audioBuffer));//就这么简单哈哈 Log.d("NLPService", "send=" + send); audioBuffer.clear();//记住清空 } webSocket.send("close");//录制完之后发送约定字段。通知服务端关闭。 } } 复制代码
......然后呢,然后就有数据了 ,就是这么简单
......然后老司机就要说了。。。你这没有加密啊,效率很低啊。在此陈述一点,这里是转写引擎,每次就一句话 ,传输数据量本身不大,后端大神们说没必要加密,然后我就照办了...当然也可以一边加密一边传输
3.TTS 之AudioTrack 播放wav文件
这里就比较简单了,okhttp 调用API 传递text 获取response 然后用之AudioTrack 播放。这里是原始音频流,mediaplayer播放就有点大才小用了(我没试过),不过 mediaplayer播放也是IPC过程,底层最终也是调用AudioTrack 进行播放的。 直接上代码 :
public boolean request() { OkHttpClient client = NetRequest.getOkHttpClient(); Request request = new Request.Builder().url(NetRequest.BASE_URL + "api/tts?text=今天是星期三").build(); client.newCall(request).enqueue(new Callback() { @Override public void onFailure(Call call, IOException e) { } @Override public void onResponse(Call call, Response response) throws IOException { play(response.body().bytes()); } }); return true; } public void play( byte[] data) { try { Log.d(TAG, "audioTrack start "); AudioTrack audioTrack = new AudioTrack(mOutput, mSamplingRate, AudioFormat.CHANNEL_OUT_MONO, AudioFormat.ENCODING_PCM_16BIT, data.length, AudioTrack.MODE_STATIC); audioTrack.write(data, 0, data.length); audioTrack.play(); while (audioTrack.getPlaybackHeadPosition() < (data.length / 2)) { Thread.yield();//播放延迟处理...... } audioTrack.stop(); audioTrack.release(); } catch (IllegalArgumentException e) { } catch (IllegalStateException e) { } } 复制代码
4.speex 加密
speex 是一个开源免费的音频加密库,C++ 写的。demo里面是编译好的so 文件, ,我亲自编译了好久各种坑,最后没成功,只能借用了。-_-||。 下面有个speexDemo整个项目在工程里,音频加密解密都正常,亲测可用。学习这块时候CSDN下来的, 搬过来凑合数。
public static void raw2spx(String inFileName, String outFileName) { FileInputStream rawFileInputStream = null; FileOutputStream fileOutputStream = null; try { rawFileInputStream = new FileInputStream(inFileName); fileOutputStream = new FileOutputStream(outFileName); byte[] rawbyte = new byte[320]; byte[] encoded = new byte[160]; //将原数据转换成spx压缩的文件,speex只能编码160字节的数据,需要使用一个循环 int readedtotal = 0; int size = 0; int encodedtotal = 0; while ((size = rawFileInputStream.read(rawbyte, 0, 320)) != -1) { readedtotal = readedtotal + size; short[] rawdata = ShortByteUtil.byteArray2ShortArray(rawbyte); int encodesize = SpeexUtil.getInstance().encode(rawdata, 0, encoded, rawdata.length); fileOutputStream.write(encoded, 0, encodesize); encodedtotal = encodedtotal + encodesize; } fileOutputStream.close(); rawFileInputStream.close(); } catch (Exception e) { } } 复制代码
以上所述就是小编给大家介绍的《Android AudioRecord录音 并websocket实时传输,AudioTrack 播放wav 音频,Speex加密》,希望对大家有所帮助,如果大家有任何疑问请给我留言,小编会及时回复大家的。在此也非常感谢大家对 码农网 的支持!
猜你喜欢:- 8个PHP加密货币市场实时价格脚本
- LearningAVFoundation之拍摄+实时滤镜+实时写入
- 加密原理详解:对称式加密 VS 非对称式加密
- 编码、摘要和加密(三)——数据加密
- 基于实时计算(Flink)与高斯模型构建实时异常检测系统
- 聊聊对称加密与非对称加密
本站部分资源来源于网络,本站转载出于传递更多信息之目的,版权归原作者或者来源机构所有,如转载稿涉及版权问题,请联系我们。