Refactor generic sound modem into base class

aprs
sh123 2022-08-12 10:10:04 +03:00
rodzic 8b8339b6d5
commit ea16069e52
5 zmienionych plików z 112 dodań i 253 usunięć

Wyświetl plik

@ -10,8 +10,8 @@ android {
applicationId "com.radio.codec2talkie" applicationId "com.radio.codec2talkie"
minSdkVersion 23 minSdkVersion 23
targetSdkVersion 30 targetSdkVersion 30
versionCode 135 versionCode 136
versionName "1.35" versionName "1.36"
testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner" testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
} }

Wyświetl plik

@ -22,46 +22,48 @@ import java.nio.ShortBuffer;
import java.util.Timer; import java.util.Timer;
import java.util.TimerTask; import java.util.TimerTask;
public class SoundModem implements Transport, Runnable { public class SoundModemBase implements Runnable {
private static final String TAG = SoundModem.class.getSimpleName(); private static final String TAG = SoundModemBase.class.getSimpleName();
private static final int SAMPLE_RATE = 8000; // TODO, need to get from freedv protected String _name;
protected Context _context;
protected SharedPreferences _sharedPreferences;
private String _name; protected AudioTrack _systemAudioPlayer;
protected AudioRecord _systemAudioRecorder;
private AudioTrack _systemAudioPlayer; protected boolean _isRunning = true;
private AudioRecord _systemAudioRecorder;
private boolean _isRunning = true;
private final RigCtl _rigCtl; private final RigCtl _rigCtl;
private Timer _pttOffTimer; private Timer _pttOffTimer;
private boolean _isPttOn; private boolean _isPttOn;
private final int _pttOffDelayMs; private final int _pttOffDelayMs;
private final ShortBuffer _recordAudioSampleBuffer; protected final ShortBuffer _recordAudioSampleBuffer;
private final boolean _isLoopback; protected final boolean _isLoopback;
public SoundModem(Context context) { public SoundModemBase(Context context, int sampleRate) {
_name = "SoundModem"; _name = "SoundModem";
_isPttOn = false; _isPttOn = false;
_context = context;
SharedPreferences sharedPreferences = PreferenceManager.getDefaultSharedPreferences(context); _sharedPreferences = PreferenceManager.getDefaultSharedPreferences(context);
boolean disableRx = sharedPreferences.getBoolean(PreferenceKeys.PORTS_SOUND_MODEM_DISABLE_RX, false); boolean disableRx = _sharedPreferences.getBoolean(PreferenceKeys.PORTS_SOUND_MODEM_DISABLE_RX, false);
_pttOffDelayMs = Integer.parseInt(sharedPreferences.getString(PreferenceKeys.PORTS_SOUND_MODEM_PTT_OFF_DELAY_MS, "1000")); _pttOffDelayMs = Integer.parseInt(_sharedPreferences.getString(PreferenceKeys.PORTS_SOUND_MODEM_PTT_OFF_DELAY_MS, "1000"));
_isLoopback = sharedPreferences.getBoolean(PreferenceKeys.PORTS_SOUND_MODEM_LOOPBACK, false); _isLoopback = _sharedPreferences.getBoolean(PreferenceKeys.PORTS_SOUND_MODEM_LOOPBACK, false);
if (_isLoopback) _name += "_"; if (_isLoopback) _name += "_";
constructSystemAudioDevices(disableRx); constructSystemAudioDevices(disableRx, sampleRate);
_rigCtl = RigCtlFactory.create(context); _rigCtl = RigCtlFactory.create(context);
try { try {
_rigCtl.initialize(TransportFactory.create(TransportFactory.TransportType.USB, context), context, null); _rigCtl.initialize(TransportFactory.create(TransportFactory.TransportType.USB, context), context, null);
} catch (IOException e) { } catch (IOException e) {
e.printStackTrace(); e.printStackTrace();
Log.e(TAG, "Failed to initialize RigCtl");
} }
_recordAudioSampleBuffer = ShortBuffer.allocate(_isLoopback ? 1024*100 : 1024*10); _recordAudioSampleBuffer = ShortBuffer.allocate(_isLoopback ? 1024*100 : 1024*10);
@ -70,22 +72,22 @@ public class SoundModem implements Transport, Runnable {
new Thread(this).start(); new Thread(this).start();
} }
private void constructSystemAudioDevices(boolean disableRx) { private void constructSystemAudioDevices(boolean disableRx, int sampleRate) {
int audioRecorderMinBufferSize = AudioRecord.getMinBufferSize( int audioRecorderMinBufferSize = AudioRecord.getMinBufferSize(
SAMPLE_RATE, sampleRate,
AudioFormat.CHANNEL_IN_MONO, AudioFormat.CHANNEL_IN_MONO,
AudioFormat.ENCODING_PCM_16BIT); AudioFormat.ENCODING_PCM_16BIT);
int audioSource = MediaRecorder.AudioSource.VOICE_RECOGNITION; int audioSource = MediaRecorder.AudioSource.VOICE_RECOGNITION;
_systemAudioRecorder = new AudioRecord( _systemAudioRecorder = new AudioRecord(
audioSource, audioSource,
SAMPLE_RATE, sampleRate,
AudioFormat.CHANNEL_IN_MONO, AudioFormat.CHANNEL_IN_MONO,
AudioFormat.ENCODING_PCM_16BIT, AudioFormat.ENCODING_PCM_16BIT,
10*audioRecorderMinBufferSize); 10*audioRecorderMinBufferSize);
int audioPlayerMinBufferSize = AudioTrack.getMinBufferSize( int audioPlayerMinBufferSize = AudioTrack.getMinBufferSize(
SAMPLE_RATE, sampleRate,
AudioFormat.CHANNEL_OUT_MONO, AudioFormat.CHANNEL_OUT_MONO,
AudioFormat.ENCODING_PCM_16BIT); AudioFormat.ENCODING_PCM_16BIT);
if (!disableRx) if (!disableRx)
@ -99,7 +101,7 @@ public class SoundModem implements Transport, Runnable {
.build()) .build())
.setAudioFormat(new AudioFormat.Builder() .setAudioFormat(new AudioFormat.Builder()
.setEncoding(AudioFormat.ENCODING_PCM_16BIT) .setEncoding(AudioFormat.ENCODING_PCM_16BIT)
.setSampleRate(SAMPLE_RATE) .setSampleRate(sampleRate)
.setChannelMask(AudioFormat.CHANNEL_OUT_MONO) .setChannelMask(AudioFormat.CHANNEL_OUT_MONO)
.build()) .build())
.setTransferMode(AudioTrack.MODE_STREAM) .setTransferMode(AudioTrack.MODE_STREAM)
@ -108,69 +110,18 @@ public class SoundModem implements Transport, Runnable {
_systemAudioPlayer.setVolume(AudioTrack.getMaxVolume()); _systemAudioPlayer.setVolume(AudioTrack.getMaxVolume());
} }
@Override protected int read(short[] sampleBuffer, int samplesToRead) {
public String name() {
return _name;
}
@Override
public int read(byte[] data) throws IOException {
return 0;
}
@Override
public int write(byte[] data) throws IOException {
return 0;
}
@Override
public int read(short[] audioSamples) throws IOException {
synchronized (_recordAudioSampleBuffer) { synchronized (_recordAudioSampleBuffer) {
if (_recordAudioSampleBuffer.position() >= audioSamples.length) { if (_recordAudioSampleBuffer.position() >= samplesToRead) {
_recordAudioSampleBuffer.flip(); _recordAudioSampleBuffer.flip();
_recordAudioSampleBuffer.get(audioSamples); _recordAudioSampleBuffer.get(sampleBuffer, 0, samplesToRead);
_recordAudioSampleBuffer.compact(); _recordAudioSampleBuffer.compact();
//Log.i(TAG, "read " + _recordAudioBuffer.position() + " " +audioSamples.length + " " + DebugTools.shortsToHex(audioSamples)); return samplesToRead;
return audioSamples.length;
} }
} }
return 0; return 0;
} }
@Override
public int write(short[] audioSamples) throws IOException {
pttOn();
if (_systemAudioPlayer.getPlayState() != AudioTrack.PLAYSTATE_PLAYING)
_systemAudioPlayer.play();
if (_isLoopback) {
synchronized (_recordAudioSampleBuffer) {
for (short sample : audioSamples) {
try {
_recordAudioSampleBuffer.put(sample);
} catch (BufferOverflowException e) {
// client is transmitting and cannot consume the buffer, just discard
_recordAudioSampleBuffer.clear();
}
}
}
} else {
_systemAudioPlayer.write(audioSamples, 0, audioSamples.length);
}
_systemAudioPlayer.stop();
pttOff();
return audioSamples.length;
}
@Override
public void close() throws IOException {
Log.i(TAG, "close()");
_isRunning = false;
_systemAudioRecorder.stop();
_systemAudioPlayer.stop();
_systemAudioRecorder.release();
_systemAudioPlayer.release();
}
@Override @Override
public void run() { public void run() {
Log.i(TAG, "Starting receive thread"); Log.i(TAG, "Starting receive thread");
@ -196,7 +147,16 @@ public class SoundModem implements Transport, Runnable {
} }
} }
private void pttOn() { protected void stop() {
Log.i(TAG, "stop()");
_isRunning = false;
_systemAudioRecorder.stop();
_systemAudioPlayer.stop();
_systemAudioRecorder.release();
_systemAudioPlayer.release();
}
protected void pttOn() {
if (_isPttOn) return; if (_isPttOn) return;
try { try {
@ -207,7 +167,7 @@ public class SoundModem implements Transport, Runnable {
} }
} }
private void pttOff() { protected void pttOff() {
if (!_isPttOn) return; if (!_isPttOn) return;
if (_pttOffTimer != null) { if (_pttOffTimer != null) {
_pttOffTimer.cancel(); _pttOffTimer.cancel();

Wyświetl plik

@ -1,33 +1,18 @@
package com.radio.codec2talkie.transport; package com.radio.codec2talkie.transport;
import android.content.Context; import android.content.Context;
import android.content.SharedPreferences;
import android.media.AudioAttributes;
import android.media.AudioFormat;
import android.media.AudioRecord;
import android.media.AudioTrack; import android.media.AudioTrack;
import android.media.MediaRecorder;
import android.os.Process;
import android.util.Log;
import androidx.preference.PreferenceManager;
import com.radio.codec2talkie.rigctl.RigCtl;
import com.radio.codec2talkie.rigctl.RigCtlFactory;
import com.radio.codec2talkie.settings.PreferenceKeys; import com.radio.codec2talkie.settings.PreferenceKeys;
import com.radio.codec2talkie.tools.BitTools; import com.radio.codec2talkie.tools.BitTools;
import com.ustadmobile.codec2.Codec2; import com.ustadmobile.codec2.Codec2;
import java.io.IOException; import java.io.IOException;
import java.nio.BufferOverflowException; import java.nio.BufferOverflowException;
import java.nio.BufferUnderflowException;
import java.nio.ByteBuffer; import java.nio.ByteBuffer;
import java.nio.ShortBuffer;
import java.util.Arrays; import java.util.Arrays;
import java.util.Timer;
import java.util.TimerTask;
public class SoundModemFsk implements Transport, Runnable { public class SoundModemFsk extends SoundModemBase implements Transport {
private static final String TAG = SoundModemFsk.class.getSimpleName(); private static final String TAG = SoundModemFsk.class.getSimpleName();
@ -35,48 +20,22 @@ public class SoundModemFsk implements Transport, Runnable {
public static final int SAMPLE_RATE = 19200; public static final int SAMPLE_RATE = 19200;
//public static final int SAMPLE_RATE = 48000; //public static final int SAMPLE_RATE = 48000;
private String _name;
private AudioTrack _systemAudioPlayer;
private AudioRecord _systemAudioRecorder;
private final short[] _recordAudioBuffer; private final short[] _recordAudioBuffer;
private final byte[] _recordBitBuffer; private final byte[] _recordBitBuffer;
private final short[] _playbackAudioBuffer; private final short[] _playbackAudioBuffer;
private final byte[] _playbackBitBuffer; private final byte[] _playbackBitBuffer;
private final int _samplesPerSymbol;
private final ByteBuffer _bitBuffer; private final ByteBuffer _bitBuffer;
private final Context _context;
private final SharedPreferences _sharedPreferences;
private boolean _isRunning = true;
private final ShortBuffer _recordAudioSampleBuffer;
private final ByteBuffer _sampleBuffer;
private boolean _isLoopback;
private final long _fskModem; private final long _fskModem;
private final RigCtl _rigCtl;
private Timer _pttOffTimer;
private boolean _isPttOn;
private final int _pttOffDelayMs;
private byte _prevBit; private byte _prevBit;
public SoundModemFsk(Context context) { public SoundModemFsk(Context context) {
_context = context; super(context, SAMPLE_RATE);
_isPttOn = false;
_prevBit = 0; _prevBit = 0;
_sharedPreferences = PreferenceManager.getDefaultSharedPreferences(_context);
boolean disableRx = _sharedPreferences.getBoolean(PreferenceKeys.PORTS_SOUND_MODEM_DISABLE_RX, false);
int bitRate = Integer.parseInt(_sharedPreferences.getString(PreferenceKeys.PORTS_SOUND_MODEM_TYPE, "1200")); int bitRate = Integer.parseInt(_sharedPreferences.getString(PreferenceKeys.PORTS_SOUND_MODEM_TYPE, "1200"));
int gain = Integer.parseInt(_sharedPreferences.getString(PreferenceKeys.PORTS_SOUND_MODEM_GAIN, "10000")); int gain = Integer.parseInt(_sharedPreferences.getString(PreferenceKeys.PORTS_SOUND_MODEM_GAIN, "10000"));
_pttOffDelayMs = Integer.parseInt(_sharedPreferences.getString(PreferenceKeys.PORTS_SOUND_MODEM_PTT_OFF_DELAY_MS, "1000"));
_isLoopback = _sharedPreferences.getBoolean(PreferenceKeys.PORTS_SOUND_MODEM_LOOPBACK, false);
_name = "FSK" + bitRate; _name = "FSK" + bitRate;
if (_isLoopback) _name += "_"; if (_isLoopback) _name += "_";
@ -92,62 +51,9 @@ public class SoundModemFsk implements Transport, Runnable {
_recordBitBuffer = new byte[Codec2.fskDemodBitsBufSize(_fskModem)]; _recordBitBuffer = new byte[Codec2.fskDemodBitsBufSize(_fskModem)];
_playbackAudioBuffer = new short[Codec2.fskModSamplesBufSize(_fskModem)]; _playbackAudioBuffer = new short[Codec2.fskModSamplesBufSize(_fskModem)];
_playbackBitBuffer = new byte[Codec2.fskModBitsBufSize(_fskModem)]; _playbackBitBuffer = new byte[Codec2.fskModBitsBufSize(_fskModem)];
_samplesPerSymbol = Codec2.fskSamplesPerSymbol(_fskModem);
_bitBuffer = ByteBuffer.allocate(100 * _recordBitBuffer.length); _bitBuffer = ByteBuffer.allocate(100 * _recordBitBuffer.length);
constructSystemAudioDevices(disableRx);
_sampleBuffer = ByteBuffer.allocate(_isLoopback ? 1024 * 100 : 0);
_recordAudioSampleBuffer = ShortBuffer.allocate(_isLoopback ? 1024*100 : 1024*100);
_rigCtl = RigCtlFactory.create(context);
try {
_rigCtl.initialize(TransportFactory.create(TransportFactory.TransportType.USB, context), context, null);
} catch (IOException e) {
e.printStackTrace();
}
if (!disableRx)
new Thread(this).start();
} }
private void constructSystemAudioDevices(boolean disableRx) {
int audioRecorderMinBufferSize = AudioRecord.getMinBufferSize(
SAMPLE_RATE,
AudioFormat.CHANNEL_IN_MONO,
AudioFormat.ENCODING_PCM_16BIT);
int audioSource = MediaRecorder.AudioSource.VOICE_RECOGNITION;
_systemAudioRecorder = new AudioRecord(
audioSource,
SAMPLE_RATE,
AudioFormat.CHANNEL_IN_MONO,
AudioFormat.ENCODING_PCM_16BIT,
10*audioRecorderMinBufferSize);
int audioPlayerMinBufferSize = AudioTrack.getMinBufferSize(
SAMPLE_RATE,
AudioFormat.CHANNEL_OUT_MONO,
AudioFormat.ENCODING_PCM_16BIT);
if (!disableRx)
_systemAudioRecorder.startRecording();
int usage = AudioAttributes.USAGE_MEDIA;
_systemAudioPlayer = new AudioTrack.Builder()
.setAudioAttributes(new AudioAttributes.Builder()
.setUsage(usage)
.setContentType(AudioAttributes.CONTENT_TYPE_MUSIC)
.build())
.setAudioFormat(new AudioFormat.Builder()
.setEncoding(AudioFormat.ENCODING_PCM_16BIT)
.setSampleRate(SAMPLE_RATE)
.setChannelMask(AudioFormat.CHANNEL_OUT_MONO)
.build())
.setTransferMode(AudioTrack.MODE_STREAM)
.setBufferSizeInBytes(10*audioPlayerMinBufferSize)
.build();
_systemAudioPlayer.setVolume(AudioTrack.getMaxVolume());
}
@Override @Override
public String name() { public String name() {
@ -157,19 +63,8 @@ public class SoundModemFsk implements Transport, Runnable {
@Override @Override
public int read(byte[] data) throws IOException { public int read(byte[] data) throws IOException {
int nin = Codec2.fskNin(_fskModem); int nin = Codec2.fskNin(_fskModem);
if (read(_recordAudioBuffer, nin) == 0) return 0;
synchronized (_recordAudioSampleBuffer) {
// read samples to record audio buffer if there is enough data
if (_recordAudioSampleBuffer.position() >= nin) {
_recordAudioSampleBuffer.flip();
_recordAudioSampleBuffer.get(_recordAudioBuffer, 0, nin);
_recordAudioSampleBuffer.compact();
//Log.i(TAG, "read " + _recordAudioBuffer.position() + " " +audioSamples.length + " " + DebugTools.shortsToHex(audioSamples));
// otherwise return void to the user
} else {
return 0;
}
}
//Log.v(TAG, "read audio power: " + AudioTools.getSampleLevelDb(Arrays.copyOf(_recordAudioBuffer, Codec2.fskNin(_fskModem)))); //Log.v(TAG, "read audio power: " + AudioTools.getSampleLevelDb(Arrays.copyOf(_recordAudioBuffer, Codec2.fskNin(_fskModem))));
//Log.v(TAG, readCnt + " " + _recordAudioBuffer.length + " " + Codec2.fskNin(_fskModem)); //Log.v(TAG, readCnt + " " + _recordAudioBuffer.length + " " + Codec2.fskNin(_fskModem));
Codec2.fskDemodulate(_fskModem, _recordAudioBuffer, _recordBitBuffer); Codec2.fskDemodulate(_fskModem, _recordAudioBuffer, _recordBitBuffer);
@ -264,69 +159,7 @@ public class SoundModemFsk implements Transport, Runnable {
@Override @Override
public void close() throws IOException { public void close() throws IOException {
Log.i(TAG, "close()"); stop();
_isRunning = false;
_systemAudioRecorder.stop();
_systemAudioPlayer.stop();
_systemAudioRecorder.release();
_systemAudioPlayer.release();
Codec2.fskDestroy(_fskModem); Codec2.fskDestroy(_fskModem);
} }
@Override
public void run() {
Log.i(TAG, "Starting receive thread");
android.os.Process.setThreadPriority(Process.THREAD_PRIORITY_URGENT_AUDIO);
int readSize = 32;
short[] sampleBuf = new short[readSize];
while (_isRunning) {
int readCnt = _systemAudioRecorder.read(sampleBuf, 0, readSize);
if (readCnt != readSize) {
Log.w(TAG, "" + readCnt + " != " + readSize);
continue;
}
synchronized (_recordAudioSampleBuffer) {
for (short sample : sampleBuf) {
try {
_recordAudioSampleBuffer.put(sample);
} catch (BufferOverflowException e) {
// user is probably transmitting and cannot consume, just discard
_recordAudioSampleBuffer.clear();
}
}
}
}
}
private void pttOn() {
if (_isPttOn) return;
try {
_rigCtl.pttOn();
_isPttOn = true;
} catch (IOException e) {
e.printStackTrace();
}
}
private void pttOff() {
if (!_isPttOn) return;
if (_pttOffTimer != null) {
_pttOffTimer.cancel();
_pttOffTimer.purge();
_pttOffTimer = null;
}
_pttOffTimer = new Timer();
_pttOffTimer.schedule(new TimerTask() {
@Override
public void run() {
try {
_rigCtl.pttOff();
_isPttOn = false;
} catch (IOException e) {
e.printStackTrace();
}
}
}, _pttOffDelayMs);
}
} }

Wyświetl plik

@ -0,0 +1,67 @@
package com.radio.codec2talkie.transport;
import android.content.Context;
import android.media.AudioTrack;
import java.io.IOException;
import java.nio.BufferOverflowException;
public class SoundModemRaw extends SoundModemBase implements Transport {
private static final String TAG = SoundModemRaw.class.getSimpleName();
private static final int SAMPLE_RATE = 8000; // TODO, need to get from freedv
public SoundModemRaw(Context context) {
super(context, SAMPLE_RATE);
}
@Override
public String name() {
return _name;
}
@Override
public int read(byte[] data) throws IOException {
return 0;
}
@Override
public int write(byte[] data) throws IOException {
return 0;
}
@Override
public int read(short[] audioSamples) throws IOException {
return read(audioSamples, audioSamples.length);
}
@Override
public int write(short[] audioSamples) throws IOException {
pttOn();
if (_systemAudioPlayer.getPlayState() != AudioTrack.PLAYSTATE_PLAYING)
_systemAudioPlayer.play();
if (_isLoopback) {
synchronized (_recordAudioSampleBuffer) {
for (short sample : audioSamples) {
try {
_recordAudioSampleBuffer.put(sample);
} catch (BufferOverflowException e) {
// client is transmitting and cannot consume the buffer, just discard
_recordAudioSampleBuffer.clear();
}
}
}
} else {
_systemAudioPlayer.write(audioSamples, 0, audioSamples.length);
}
_systemAudioPlayer.stop();
pttOff();
return audioSamples.length;
}
@Override
public void close() throws IOException {
stop();
}
}

Wyświetl plik

@ -9,7 +9,6 @@ import com.radio.codec2talkie.connect.BleHandler;
import com.radio.codec2talkie.connect.BluetoothSocketHandler; import com.radio.codec2talkie.connect.BluetoothSocketHandler;
import com.radio.codec2talkie.connect.TcpIpSocketHandler; import com.radio.codec2talkie.connect.TcpIpSocketHandler;
import com.radio.codec2talkie.connect.UsbPortHandler; import com.radio.codec2talkie.connect.UsbPortHandler;
import com.radio.codec2talkie.settings.PreferenceKeys;
import com.radio.codec2talkie.settings.SettingsWrapper; import com.radio.codec2talkie.settings.SettingsWrapper;
import java.io.IOException; import java.io.IOException;
@ -49,7 +48,7 @@ public class TransportFactory {
case BLE: case BLE:
return new Ble(BleHandler.getGatt(), BleHandler.getName()); return new Ble(BleHandler.getGatt(), BleHandler.getName());
case SOUND_MODEM: case SOUND_MODEM:
return SettingsWrapper.isFreeDvSoundModemModulation(sharedPreferences) ? new SoundModem(context) : new SoundModemFsk(context); return SettingsWrapper.isFreeDvSoundModemModulation(sharedPreferences) ? new SoundModemRaw(context) : new SoundModemFsk(context);
case LOOPBACK: case LOOPBACK:
default: default:
return new Loopback(); return new Loopback();