Skip to content

Commit

Permalink
Add new settings to control rx and tx sampling rates, as well as an r…
Browse files Browse the repository at this point in the history
…x multipler (to account for varying ESP32 dev module ADC performance / clocks). Implements issue #138.
  • Loading branch information
VanceVagell committed Dec 14, 2024
1 parent 3aea45a commit b3433b7
Show file tree
Hide file tree
Showing 9 changed files with 937 additions and 505 deletions.
4 changes: 2 additions & 2 deletions android-src/KV4PHT/.idea/deploymentTargetSelector.xml

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

4 changes: 2 additions & 2 deletions android-src/KV4PHT/app/src/main/AndroidManifest.xml
Original file line number Diff line number Diff line change
Expand Up @@ -35,7 +35,7 @@
android:name=".ui.SettingsActivity"
android:exported="true"
android:theme="@style/Theme.KV4PHT"
android:windowSoftInputMode="stateVisible">
android:windowSoftInputMode="adjustResize">
<intent-filter>
<action android:name="com.vagell.kv4pht.SETTINGS_ACTION" />

Expand All @@ -46,7 +46,7 @@
android:name=".ui.AddEditMemoryActivity"
android:exported="true"
android:theme="@style/Theme.KV4PHT"
android:windowSoftInputMode="stateVisible|adjustResize"> <!-- There's an issue where fields are not scrollable, likely caused by an Android bug not honoring adjustResize on fullscreen apps, see similar bug in another project: https://github.com/dotnet/maui/issues/14281 -->
android:windowSoftInputMode="adjustResize">
<intent-filter>
<action android:name="com.vagell.kv4pht.ADD_MEMORY_ACTION" />
<action android:name="com.vagell.kv4pht.EDIT_MEMORY_ACTION" />
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -58,8 +58,7 @@ public APRSPacket( String source, String destination, ArrayList<Digipeater> digi
this.dti = (char)body[0];
this.aprsInformation = new InformationField(body);
}



/**
* @param callsign
* @return String
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -107,66 +107,18 @@ public class RadioAudioService extends Service {
public static final int MODE_FLASHING = 4;
private int mode = MODE_STARTUP;
private int messageNumber = 0;

public enum ESP32Command {
PTT_DOWN((byte) 1),
PTT_UP((byte) 2),
TUNE_TO((byte) 3), // paramsStr contains freq, offset, tone details
FILTERS((byte) 4), // paramStr contains emphasis, highpass, lowpass (each 0/1)
STOP((byte) 5),
GET_FIRMWARE_VER((byte) 6);

private byte commandByte;
ESP32Command(byte commandByte) {
this.commandByte = commandByte;
}

public byte getByte() {
return commandByte;
}
}

public enum MicGainBoost {
NONE,
LOW,
MED,
HIGH;

public static MicGainBoost parseMicGainBoost(String str) {
if (str.equals("High")) {
return MicGainBoost.HIGH;
} else if (str.equals("Med")) {
return MicGainBoost.MED;
} else if (str.equals("Low")) {
return MicGainBoost.LOW;
}

return MicGainBoost.NONE;
}

public static float toFloat(MicGainBoost micGainBoost) {
if (micGainBoost == LOW) {
return 1.5f;
} else if (micGainBoost == MED) {
return 2.0f;
} else if (micGainBoost == HIGH) {
return 2.5f;
}

return 1.0f;
}
}

public static final byte SILENT_BYTE = -128;

// Callbacks to the Activity that started us
private RadioAudioServiceCallbacks callbacks = null;

// For transmitting audio to ESP32 / radio
public static final int AUDIO_SAMPLE_RATE = 44100;
public static SampleRate rxSampleRate = SampleRate.SAMPLE_RATE_44;
public static SampleRate txSampleRate = SampleRate.SAMPLE_RATE_44;
public static float rxSampleRateMult = 1.00494f;
public static final int channelConfig = AudioFormat.CHANNEL_IN_MONO;
public static final int audioFormat = AudioFormat.ENCODING_PCM_8BIT;
public static final int minBufferSize = AudioRecord.getMinBufferSize(AUDIO_SAMPLE_RATE, channelConfig, audioFormat) * 2;
public static final int rxMinBufferSize = AudioRecord.getMinBufferSize(SampleRate.toInt(rxSampleRate), channelConfig, audioFormat) * 2;
private UsbManager usbManager;
private UsbDevice esp32Device;
private static UsbSerialPort serialPort;
Expand Down Expand Up @@ -213,6 +165,90 @@ public static float toFloat(MicGainBoost micGainBoost) {

private ThreadPoolExecutor threadPoolExecutor = null;

public enum ESP32Command {
PTT_DOWN((byte) 1),
PTT_UP((byte) 2),
TUNE_TO((byte) 3), // paramsStr contains freq, offset, tone details
FILTERS((byte) 4), // paramStr contains emphasis, highpass, lowpass (each 0/1)
STOP((byte) 5),
GET_FIRMWARE_VER((byte) 6);

private byte commandByte;
ESP32Command(byte commandByte) {
this.commandByte = commandByte;
}

public byte getByte() {
return commandByte;
}
}

public enum MicGainBoost {
NONE,
LOW,
MED,
HIGH;

public static MicGainBoost parse(String str) {
if (str.equals("High")) {
return HIGH;
} else if (str.equals("Med")) {
return MED;
} else if (str.equals("Low")) {
return LOW;
}

return NONE;
}

public static float toFloat(MicGainBoost micGainBoost) {
if (micGainBoost == LOW) {
return 1.5f;
} else if (micGainBoost == MED) {
return 2.0f;
} else if (micGainBoost == HIGH) {
return 2.5f;
}

return 1.0f;
}
}

public enum SampleRate {
SAMPLE_RATE_44,
SAMPLE_RATE_16;

public static SampleRate parse(String str) {
if (str.equals("44.1kHz")) {
return SAMPLE_RATE_44;
} else if (str.equals("16kHz")) {
return SAMPLE_RATE_16;
}

return SAMPLE_RATE_44;
}

public static int toInt(SampleRate sampleRate) {
if (sampleRate == SAMPLE_RATE_44) {
return 44100;
} else if (sampleRate == SAMPLE_RATE_16) {
return 16000;
}

return 44100;
}

public static String encode(SampleRate sampleRate) {
if (sampleRate == SAMPLE_RATE_44) {
return "0";
} else if (sampleRate == SAMPLE_RATE_16) {
return "1";
}

return "0";
}
}

/**
* Class used for the client Binder. This service always runs in the same process as its clients.
*/
Expand Down Expand Up @@ -244,7 +280,27 @@ public void setFilters(boolean emphasis, boolean highpass, boolean lowpass) {
}

public void setMicGainBoost(String micGainBoost) {
this.micGainBoost = MicGainBoost.parseMicGainBoost(micGainBoost);
this.micGainBoost = MicGainBoost.parse(micGainBoost);
}

public void setRxSampleRate(String rxSampleRate) {
this.rxSampleRate = SampleRate.parse(rxSampleRate);
initAudioTrack();
initAFSKModem();
}

public void setTxSampleRate(String txSampleRate) {
this.txSampleRate = SampleRate.parse(txSampleRate);
initAFSKModem();
// Recording sample rate is set in MainActivity (which owns the recording object).
}

public void setRxSampleRateMult(String rxSampleRateMult) {
try {
this.rxSampleRateMult = Float.parseFloat(rxSampleRateMult);
} catch (Exception e) {
this.rxSampleRateMult = 1.00494f; // Use default if some bad data in the setting.
}
}

public static void setMaxFreq(int newMaxFreq) {
Expand Down Expand Up @@ -444,7 +500,10 @@ public void tuneToFreq(String frequencyStr, int squelchLevel, boolean forceTune)
squelch = squelchLevel;

if (serialPort != null) {
sendCommandToESP32(ESP32Command.TUNE_TO, makeSafe2MFreq(activeFrequencyStr) + makeSafe2MFreq(activeFrequencyStr) + "00" + squelchLevel);
sendCommandToESP32(ESP32Command.TUNE_TO, makeSafe2MFreq(activeFrequencyStr) +
makeSafe2MFreq(activeFrequencyStr) + "00" + squelchLevel +
String.format("%05d", (int) (SampleRate.toInt(rxSampleRate) * rxSampleRateMult)) + // e.g. "44100"
String.format("%05d", SampleRate.toInt(txSampleRate))); // e.g. "44100"
}

// Reset audio prebuffer
Expand Down Expand Up @@ -531,7 +590,11 @@ public void tuneToMemory(ChannelMemory memory, int squelchLevel, boolean forceTu

if (serialPort != null) {
sendCommandToESP32(ESP32Command.TUNE_TO,
getTxFreq(memory.frequency, memory.offset) + makeSafe2MFreq(memory.frequency) + getToneIdxStr(memory.tone) + squelchLevel);
getTxFreq(memory.frequency, memory.offset) +
makeSafe2MFreq(memory.frequency) +
getToneIdxStr(memory.tone) + squelchLevel +
String.format("%05d", (int) (SampleRate.toInt(rxSampleRate) * rxSampleRateMult)) + // e.g. "44100"
String.format("%05d", SampleRate.toInt(txSampleRate))); // e.g. "44100"
}

// Reset audio prebuffer
Expand Down Expand Up @@ -578,29 +641,31 @@ private void checkScanDueToSilence() {
// means there is no signal detected on the given frequency) even when there is. I did
// extensive debugging and even rewrote large portions of the DRA818v library to determine
// that this was the case. So in lieu of that, we scan using a timing/silence-based system.
if (consecutiveSilenceBytes >= (AUDIO_SAMPLE_RATE * SEC_BETWEEN_SCANS)) {
if (consecutiveSilenceBytes >= (SampleRate.toInt(rxSampleRate) * SEC_BETWEEN_SCANS)) {
consecutiveSilenceBytes = 0;
nextScan();
}
}

private void initAudioTrack() {
if (audioTrack != null) {
if (null != audioTrack) {
audioTrack.stop();
audioTrack.release();
audioTrack = null;
}

audioTrack = new AudioTrack.Builder()
.setAudioAttributes(new AudioAttributes.Builder()
.setUsage(AudioAttributes.USAGE_MEDIA)
.setContentType(AudioAttributes.CONTENT_TYPE_SPEECH)
.build())
.setAudioFormat(new AudioFormat.Builder()
.setEncoding(audioFormat)
.setSampleRate(AUDIO_SAMPLE_RATE)
.setSampleRate(SampleRate.toInt(rxSampleRate))
.setChannelMask(AudioFormat.CHANNEL_OUT_MONO)
.build())
.setTransferMode(AudioTrack.MODE_STREAM)
.setBufferSizeInBytes(minBufferSize)
.setBufferSizeInBytes(rxMinBufferSize)
.setSessionId(AudioManager.AUDIO_SESSION_ID_GENERATE)
.build();
audioTrack.setAuxEffectSendLevel(0.0f);
Expand Down Expand Up @@ -935,7 +1000,7 @@ public void sendAudioToESP32(byte[] audioBuffer, boolean dataMode) {
// as audio sampling. Otherwise, we'll overwhelm its DAC buffer and some audio will
// be lost.
final Handler handler = new Handler(Looper.getMainLooper());
final float msToSendOneChunk = (float) TX_AUDIO_CHUNK_SIZE / (float) AUDIO_SAMPLE_RATE * 1000f;
final float msToSendOneChunk = (float) TX_AUDIO_CHUNK_SIZE / (float) SampleRate.toInt(txSampleRate) * 1000f;
float nextSendDelay = 0f;
byte[] finalAudioBuffer = audioBuffer;
for (int i = 0; i < audioBuffer.length; i += TX_AUDIO_CHUNK_SIZE) {
Expand Down Expand Up @@ -1093,17 +1158,22 @@ private void handleESP32Data(byte[] data) {
if (mode == MODE_RX || mode == MODE_SCAN) {
if (prebufferComplete && audioTrack != null) {
synchronized (audioTrack) {
if (afskDemodulator != null) { // Avoid race condition at app start.
try {
if (afskDemodulator != null) { // Avoid race condition at app start.
// Add the audio samples to the AFSK demodulator.
float[] audioAsFloats = convertPCM8ToFloatArray(data);
afskDemodulator.addSamples(audioAsFloats, audioAsFloats.length);
}

// Play the audio.
audioTrack.write(data, 0, data.length);

// Add the audio samples to the AFSK demodulator.
float[] audioAsFloats = convertPCM8ToFloatArray(data);
afskDemodulator.addSamples(audioAsFloats, audioAsFloats.length);
}

if (audioTrack != null && audioTrack.getPlayState() != AudioTrack.PLAYSTATE_PLAYING) {
audioTrack.play();
if (audioTrack != null && audioTrack.getPlayState() != AudioTrack.PLAYSTATE_PLAYING) {
audioTrack.play();
}
} catch (Exception e) {
// Objects may cease to exist when user changes rx sample rate
return;
}
}
} else {
Expand All @@ -1115,11 +1185,16 @@ private void handleESP32Data(byte[] data) {
prebufferComplete = true;
// Log.d("DEBUG", "Rx prebuffer full, writing to audioTrack.");
if (audioTrack != null) {
if (audioTrack.getPlayState() != AudioTrack.PLAYSTATE_PLAYING) {
audioTrack.play();
}
synchronized (audioTrack) {
audioTrack.write(rxBytesPrebuffer, 0, PRE_BUFFER_SIZE);
try {
if (audioTrack.getPlayState() != AudioTrack.PLAYSTATE_PLAYING) {
audioTrack.play();
}
synchronized (audioTrack) {
audioTrack.write(rxBytesPrebuffer, 0, PRE_BUFFER_SIZE);
}
} catch (Exception e) {
// audioTrack may cease to exist when user changes rx sample rate
return;
}
}

Expand Down Expand Up @@ -1221,8 +1296,8 @@ public void handlePacket(byte[] data) {
};

try {
afskDemodulator = new Afsk1200MultiDemodulator(AUDIO_SAMPLE_RATE, packetHandler);
afskModulator = new Afsk1200Modulator(AUDIO_SAMPLE_RATE);
afskDemodulator = new Afsk1200MultiDemodulator(SampleRate.toInt(rxSampleRate), packetHandler);
afskModulator = new Afsk1200Modulator(SampleRate.toInt(txSampleRate));
} catch (Exception e) {
Log.d("DEBUG", "Unable to create AFSK modem objects.");
}
Expand Down Expand Up @@ -1298,8 +1373,8 @@ private void txAX25Packet(Packet ax25Packet) {
@Override
public void run() {
// Add some silence before and after the data.
int bytesOfLeadInDelay = (AUDIO_SAMPLE_RATE / 1000 * MS_SILENCE_BEFORE_DATA);
int bytesOfTailDelay = (AUDIO_SAMPLE_RATE / 1000 * MS_SILENCE_AFTER_DATA);
int bytesOfLeadInDelay = (SampleRate.toInt(txSampleRate) / 1000 * MS_SILENCE_BEFORE_DATA);
int bytesOfTailDelay = (SampleRate.toInt(txSampleRate) / 1000 * MS_SILENCE_AFTER_DATA);
byte[] combinedAudio = new byte[bytesOfLeadInDelay + simpleAudioBytes.length + bytesOfTailDelay];
for (int i = 0; i < bytesOfLeadInDelay; i++) {
combinedAudio[i] = SILENT_BYTE;
Expand Down
Loading

0 comments on commit b3433b7

Please sign in to comment.