package com.radio.codec2talkie; import android.bluetooth.BluetoothSocket; import android.media.AudioAttributes; import android.media.AudioFormat; import android.media.AudioRecord; import android.media.AudioTrack; import android.media.MediaRecorder; import android.os.Handler; import android.os.Message; import java.io.IOException; import java.io.InputStream; import java.io.OutputStream; import java.nio.BufferOverflowException; import java.nio.BufferUnderflowException; import java.nio.ByteBuffer; import com.hoho.android.usbserial.driver.UsbSerialPort; import com.radio.codec2talkie.kiss.KissCallback; import com.radio.codec2talkie.kiss.KissProcessor; import com.ustadmobile.codec2.Codec2; public class Codec2Player extends Thread { public static int PLAYER_DISCONNECT = 1; public static int PLAYER_LISTENING = 2; public static int PLAYER_RECORDING = 3; public static int PLAYER_PLAYING = 4; public static int PLAYER_RX_LEVEL = 5; public static int PLAYER_TX_LEVEL = 6; private static int AUDIO_MIN_LEVEL = -70; private static int AUDIO_HIGH_LEVEL = -15; private final int AUDIO_SAMPLE_SIZE = 8000; private final int SLEEP_IDLE_DELAY_MS = 20; private final int RX_TIMEOUT = 100; private final int TX_TIMEOUT = 2000; private final byte CSMA_PERSISTENCE = (byte)0xff; private final byte CSMA_SLOT_TIME = (byte)0x00; private final byte TX_TAIL_10MS_UNITS = (byte)20; // 200ms private final int RX_BUFFER_SIZE = 8192; private long _codec2Con; private BluetoothSocket _btSocket; private UsbSerialPort _usbPort; private int _audioBufferSize; private boolean _isRecording = false; private int _currentStatus = PLAYER_DISCONNECT; // input data, bt -> audio private InputStream _btInputStream; private final AudioTrack _audioPlayer; private short[] _playbackAudioBuffer; // output data., mic -> bt private OutputStream _btOutputStream; private final AudioRecord _audioRecorder; private final byte[] _rxDataBuffer; private short[] _recordAudioBuffer; private char[] _recordAudioEncodedBuffer; // loopback mode private boolean _isLoopbackMode; private ByteBuffer _loopbackBuffer; // callbacks private KissProcessor _kissProcessor; private final Handler _onPlayerStateChanged; public Codec2Player(Handler onPlayerStateChanged, int codec2Mode) { _onPlayerStateChanged = onPlayerStateChanged; _isLoopbackMode = false; _rxDataBuffer = new byte[RX_BUFFER_SIZE]; setCodecModeInternal(codec2Mode); int _audioRecorderMinBufferSize = AudioRecord.getMinBufferSize( AUDIO_SAMPLE_SIZE, AudioFormat.CHANNEL_IN_MONO, AudioFormat.ENCODING_PCM_16BIT); _audioRecorder = new AudioRecord( MediaRecorder.AudioSource.MIC, AUDIO_SAMPLE_SIZE, AudioFormat.CHANNEL_IN_MONO, AudioFormat.ENCODING_PCM_16BIT, 3 * _audioRecorderMinBufferSize); _audioRecorder.startRecording(); int _audioPlayerMinBufferSize = AudioTrack.getMinBufferSize( AUDIO_SAMPLE_SIZE, AudioFormat.CHANNEL_OUT_MONO, AudioFormat.ENCODING_PCM_16BIT); _audioPlayer = new AudioTrack.Builder() .setAudioAttributes(new AudioAttributes.Builder() .setUsage(AudioAttributes.USAGE_MEDIA) .setContentType(AudioAttributes.CONTENT_TYPE_MUSIC) .build()) .setAudioFormat(new AudioFormat.Builder() .setEncoding(AudioFormat.ENCODING_PCM_16BIT) .setSampleRate(AUDIO_SAMPLE_SIZE) .setChannelMask(AudioFormat.CHANNEL_OUT_MONO) .build()) .setTransferMode(AudioTrack.MODE_STREAM) .setBufferSizeInBytes(3 * _audioPlayerMinBufferSize) .build(); _audioPlayer.play(); } public void setSocket(BluetoothSocket btSocket) throws IOException { _btSocket = btSocket; _btInputStream = _btSocket.getInputStream(); _btOutputStream = _btSocket.getOutputStream(); } public void setUsbPort(UsbSerialPort port) { _usbPort = port; } public void setLoopbackMode(boolean isLoopbackMode) { _isLoopbackMode = isLoopbackMode; } public void setCodecMode(int codecMode) { Codec2.destroy(_codec2Con); setCodecModeInternal(codecMode); } public static int getAudioMinLevel() { return AUDIO_MIN_LEVEL; } public static int getAudioHighLevel() { return AUDIO_HIGH_LEVEL; } public void startPlayback() { _isRecording = false; } public void startRecording() { _isRecording = true; } private void setCodecModeInternal(int codecMode) { _codec2Con = Codec2.create(codecMode); _audioBufferSize = Codec2.getSamplesPerFrame(_codec2Con); int _audioEncodedBufferSize = Codec2.getBitsSize(_codec2Con); // returns number of bytes _recordAudioBuffer = new short[_audioBufferSize]; _recordAudioEncodedBuffer = new char[_audioEncodedBufferSize]; _playbackAudioBuffer = new short[_audioBufferSize]; _loopbackBuffer = ByteBuffer.allocateDirect(1024 * _audioEncodedBufferSize); _kissProcessor = new KissProcessor( _audioEncodedBufferSize, CSMA_PERSISTENCE, CSMA_SLOT_TIME, TX_TAIL_10MS_UNITS, _kissCallback); } private final KissCallback _kissCallback = new KissCallback() { @Override protected void sendData(byte[] kissPacket) throws IOException { if (_isLoopbackMode) { try { _loopbackBuffer.put(kissPacket); } catch (BufferOverflowException e) { e.printStackTrace(); } } else { if (_btOutputStream != null) _btOutputStream.write(kissPacket); if (_usbPort != null) { _usbPort.write(kissPacket, TX_TIMEOUT); } } } @Override protected void receiveFrame(byte[] frame) { notifyAudioLevel(_playbackAudioBuffer, false); Codec2.decode(_codec2Con, _playbackAudioBuffer, frame); _audioPlayer.write(_playbackAudioBuffer, 0, _audioBufferSize); } }; private void notifyAudioLevel(short [] audioSamples, boolean isTx) { double db = getAudioMinLevel(); if (audioSamples != null) { double acc = 0; for (short v : audioSamples) { acc += Math.abs(v); } double avg = acc / audioSamples.length; db = (20.0 * Math.log10(avg / 32768.0)); } Message msg = Message.obtain(); if (isTx) msg.what = PLAYER_TX_LEVEL; else msg.what = PLAYER_RX_LEVEL; msg.arg1 = (int)db; _onPlayerStateChanged.sendMessage(msg); } private void processRecording() throws IOException { _audioRecorder.read(_recordAudioBuffer, 0, _audioBufferSize); Codec2.encode(_codec2Con, _recordAudioBuffer, _recordAudioEncodedBuffer); notifyAudioLevel(_recordAudioBuffer, true); byte [] frame = new byte[_recordAudioEncodedBuffer.length]; for (int i = 0; i < _recordAudioEncodedBuffer.length; i++) { frame[i] = (byte)_recordAudioEncodedBuffer[i]; } _kissProcessor.sendFrame(frame); } private boolean processLoopbackPlayback() { try { byte b = _loopbackBuffer.get(); _kissProcessor.receiveByte(b); return true; } catch (BufferUnderflowException e) { return false; } } private boolean processPlayback() throws IOException { if (_isLoopbackMode) { return processLoopbackPlayback(); } int bytesRead = 0; if (_btInputStream != null) { bytesRead = _btInputStream.available(); if (bytesRead > 0) { bytesRead = _btInputStream.read(_rxDataBuffer); } } if (_usbPort != null) { bytesRead = _usbPort.read(_rxDataBuffer, RX_TIMEOUT); } if (bytesRead > 0) { for (int i = 0; i < bytesRead; i++) { _kissProcessor.receiveByte(_rxDataBuffer[i]); } return true; } return false; } private void processRecordPlaybackToggle() throws IOException { // playback -> recording if (_isRecording && _audioRecorder.getRecordingState() != AudioRecord.RECORDSTATE_RECORDING) { _audioRecorder.startRecording(); _audioPlayer.stop(); _loopbackBuffer.clear(); notifyAudioLevel(null, false); } // recording -> playback if (!_isRecording && _audioRecorder.getRecordingState() == AudioRecord.RECORDSTATE_RECORDING) { _audioRecorder.stop(); _audioPlayer.play(); _kissProcessor.flush(); _loopbackBuffer.flip(); notifyAudioLevel(null, true); } } private void cleanup() { try { _kissProcessor.flush(); } catch (IOException e) { e.printStackTrace(); } _audioRecorder.stop(); _audioRecorder.release(); _audioPlayer.stop(); _audioPlayer.release(); Codec2.destroy(_codec2Con); } private void setStatus(int status) { if (status != _currentStatus) { _currentStatus = status; Message msg = Message.obtain(); msg.what = status; _onPlayerStateChanged.sendMessage(msg); } } @Override public void run() { setPriority(Thread.MAX_PRIORITY); try { if (!_isLoopbackMode) { _kissProcessor.setupTnc(); } while (true) { processRecordPlaybackToggle(); // recording if (_audioRecorder.getRecordingState() == AudioRecord.RECORDSTATE_RECORDING) { processRecording(); setStatus(PLAYER_RECORDING); } else { // playback if (processPlayback()) { setStatus(PLAYER_PLAYING); // idling } else { try { Thread.sleep(SLEEP_IDLE_DELAY_MS); if (_currentStatus != PLAYER_LISTENING) { notifyAudioLevel(null, false); notifyAudioLevel(null, true); } setStatus(PLAYER_LISTENING); } catch (InterruptedException e) { e.printStackTrace(); } } } } } catch (IOException e) { e.printStackTrace(); } cleanup(); setStatus(PLAYER_DISCONNECT); } }