From 78e0a95adce113622fe1fd2a5f9c6a436ef4b199 Mon Sep 17 00:00:00 2001 From: Dmitry Kaukov Date: Thu, 2 Jan 2025 09:45:05 +1100 Subject: [PATCH 1/8] ADC bias injection - removes clipping - makes louder --- .../kv4p_ht_esp32_wroom_32/kv4p_ht_esp32_wroom_32.ino | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/microcontroller-src/kv4p_ht_esp32_wroom_32/kv4p_ht_esp32_wroom_32.ino b/microcontroller-src/kv4p_ht_esp32_wroom_32/kv4p_ht_esp32_wroom_32.ino index bfd8e21d..e483ee7b 100644 --- a/microcontroller-src/kv4p_ht_esp32_wroom_32/kv4p_ht_esp32_wroom_32.ino +++ b/microcontroller-src/kv4p_ht_esp32_wroom_32/kv4p_ht_esp32_wroom_32.ino @@ -22,6 +22,7 @@ along with this program. If not, see . #include #include #include +#include #include const byte FIRMWARE_VER[8] = {'0', '0', '0', '0', '0', '0', '0', '5'}; // Should be 8 characters representing a zero-padded version, like 00000001. @@ -168,7 +169,7 @@ void initI2SRx() { // Initialize ADC adc1_config_width(ADC_WIDTH_BIT_12); - adc1_config_channel_atten(ADC1_CHANNEL_6, ADC_ATTEN_DB_0); + adc1_config_channel_atten(ADC1_CHANNEL_6, ADC_ATTEN_DB_12); static const i2s_config_t i2sRxConfig = { .mode = (i2s_mode_t) (I2S_MODE_MASTER | I2S_MODE_RX | I2S_MODE_ADC_BUILT_IN), @@ -186,6 +187,8 @@ void initI2SRx() { ESP_ERROR_CHECK(i2s_driver_install(I2S_NUM_0, &i2sRxConfig, 0, NULL)); ESP_ERROR_CHECK(i2s_set_adc_mode(I2S_ADC_UNIT, I2S_ADC_CHANNEL)); + dac_output_enable(DAC_CHANNEL_2); // GPIO26 (DAC1) + dac_output_voltage(DAC_CHANNEL_2, 138); } void initI2STx() { From 58e936d3b61bbe5366ba8b3ad4fcbb1ba12b2ac9 Mon Sep 17 00:00:00 2001 From: Dmitry Kaukov Date: Thu, 2 Jan 2025 10:10:00 +1100 Subject: [PATCH 2/8] ADC auto DC removal 1) deals with ESP32 ADC reference voltage variation 2) 16 bit i2s transfers --- .../kv4p_ht_esp32_wroom_32.ino | 47 ++++++++++++++----- 1 file changed, 34 insertions(+), 13 deletions(-) diff --git a/microcontroller-src/kv4p_ht_esp32_wroom_32/kv4p_ht_esp32_wroom_32.ino b/microcontroller-src/kv4p_ht_esp32_wroom_32/kv4p_ht_esp32_wroom_32.ino index e483ee7b..765271e9 100644 --- a/microcontroller-src/kv4p_ht_esp32_wroom_32/kv4p_ht_esp32_wroom_32.ino +++ b/microcontroller-src/kv4p_ht_esp32_wroom_32/kv4p_ht_esp32_wroom_32.ino @@ -120,6 +120,7 @@ void initI2STx(); void tuneTo(float freqTx, float freqRx, int tone, int squelch, String bandwidth); void setMode(int newMode); void processTxAudio(uint8_t tempBuffer[], int bytesRead); +void iir_lowpass_reset(); void setup() { // Communication with Android via USB cable @@ -174,7 +175,7 @@ void initI2SRx() { static const i2s_config_t i2sRxConfig = { .mode = (i2s_mode_t) (I2S_MODE_MASTER | I2S_MODE_RX | I2S_MODE_ADC_BUILT_IN), .sample_rate = AUDIO_SAMPLE_RATE + SAMPLING_RATE_OFFSET, - .bits_per_sample = I2S_BITS_PER_SAMPLE_32BIT, + .bits_per_sample = I2S_BITS_PER_SAMPLE_16BIT, .channel_format = I2S_CHANNEL_FMT_ONLY_LEFT, .communication_format = i2s_comm_format_t(I2S_COMM_FORMAT_I2S | I2S_COMM_FORMAT_I2S_MSB), .intr_alloc_flags = ESP_INTR_FLAG_LEVEL1, @@ -189,6 +190,7 @@ void initI2SRx() { ESP_ERROR_CHECK(i2s_set_adc_mode(I2S_ADC_UNIT, I2S_ADC_CHANNEL)); dac_output_enable(DAC_CHANNEL_2); // GPIO26 (DAC1) dac_output_voltage(DAC_CHANNEL_2, 138); + iir_lowpass_reset(); } void initI2STx() { @@ -213,6 +215,31 @@ void initI2STx() { i2s_set_dac_mode(I2S_DAC_CHANNEL_RIGHT_EN); } +// Scale factor for fixed-point math (Q15 format) +#define FIXED_POINT_SHIFT 15 +#define DECAY_TIME 0.25 // seconds +#define ALPHA (1.0f - expf(-1.0f / (AUDIO_SAMPLE_RATE * (DECAY_TIME / logf(2.0f))))) + +static float prev_y = 0.0f; + +void iir_lowpass_reset() { + prev_y = 0.0f; +} + +// IIR Low-pass filter (float state) +int16_t iir_lowpass(int16_t x) { + float x_f = (float)x; + // IIR calculation: y[n] = α * x[n] + (1 - α) * y[n-1] + prev_y = ALPHA * x_f + (1.0f - ALPHA) * prev_y; + // Convert result back to int16 + return (int16_t)prev_y; +} + +// High-pass: x[n] - LPF(x[n]) +int16_t remove_dc(int16_t x) { + return x - iir_lowpass(x); +} + void loop() { try { if (mode == MODE_STOPPED) { @@ -433,11 +460,11 @@ void loop() { } size_t bytesRead = 0; - uint8_t buffer32[I2S_READ_LEN * 4] = {0}; - ESP_ERROR_CHECK(i2s_read(I2S_NUM_0, &buffer32, sizeof(buffer32), &bytesRead, 100)); - size_t samplesRead = bytesRead / 4; + uint16_t buffer16[I2S_READ_LEN] = {0}; + uint8_t buffer8[I2S_READ_LEN] = {0}; + ESP_ERROR_CHECK(i2s_read(I2S_NUM_0, &buffer16, sizeof(buffer16), &bytesRead, 100)); + size_t samplesRead = bytesRead / 2; - byte buffer8[I2S_READ_LEN] = {0}; bool squelched = (digitalRead(SQ_PIN) == HIGH); // Check for squelch status change @@ -457,11 +484,6 @@ void loop() { int attenuationIncrement = ATTENUATION_MAX / FADE_SAMPLES; for (int i = 0; i < samplesRead; i++) { - uint8_t sampleValue; - - // Extract 8-bit sample from 32-bit buffer - sampleValue = buffer32[i * 4 + 3] << 4; - sampleValue |= buffer32[i * 4 + 2] >> 4; // Adjust attenuation during fade if (fadeCounter > 0) { @@ -474,9 +496,8 @@ void loop() { } // Apply attenuation to the sample - int adjustedSample = (((int)sampleValue - 128) * attenuation) >> 8; - adjustedSample += 128; - buffer8[i] = (uint8_t)adjustedSample; + int16_t sample = (int32_t)remove_dc(((2048 - (buffer16[i] & 0xfff)) << 4)) * attenuation >> 8; + buffer8[i] = (sample >> 8) + 128; // Unsigned PCM8 } Serial.write(buffer8, samplesRead); From 32c903fc6cd930a6959f326a17d5b1d79442f382 Mon Sep 17 00:00:00 2001 From: Dmitry Kaukov Date: Thu, 2 Jan 2025 15:55:13 +1100 Subject: [PATCH 3/8] DRA module retries. --- .../kv4p_ht_esp32_wroom_32.ino | 23 ++++++++----------- 1 file changed, 10 insertions(+), 13 deletions(-) diff --git a/microcontroller-src/kv4p_ht_esp32_wroom_32/kv4p_ht_esp32_wroom_32.ino b/microcontroller-src/kv4p_ht_esp32_wroom_32/kv4p_ht_esp32_wroom_32.ino index 765271e9..cae721bb 100644 --- a/microcontroller-src/kv4p_ht_esp32_wroom_32/kv4p_ht_esp32_wroom_32.ino +++ b/microcontroller-src/kv4p_ht_esp32_wroom_32/kv4p_ht_esp32_wroom_32.ino @@ -216,7 +216,6 @@ void initI2STx() { } // Scale factor for fixed-point math (Q15 format) -#define FIXED_POINT_SHIFT 15 #define DECAY_TIME 0.25 // seconds #define ALPHA (1.0f - expf(-1.0f / (AUDIO_SAMPLE_RATE * (DECAY_TIME / logf(2.0f))))) @@ -334,7 +333,7 @@ void loop() { bool highpass = (paramsStr.charAt(1) == '1'); bool lowpass = (paramsStr.charAt(2) == '1'); - dra->filters(emphasis, highpass, lowpass); + while (!dra->filters(emphasis, highpass, lowpass)); } break; } @@ -368,8 +367,6 @@ void loop() { break; case COMMAND_TUNE_TO: { - setMode(MODE_RX); - // If we haven't received all the parameters needed for COMMAND_TUNE_TO, wait for them before continuing. // This can happen if ESP32 has pulled part of the command+params from the buffer before Android has completed // putting them in there. If so, we take byte-by-byte until we get the full params. @@ -460,8 +457,8 @@ void loop() { } size_t bytesRead = 0; - uint16_t buffer16[I2S_READ_LEN] = {0}; - uint8_t buffer8[I2S_READ_LEN] = {0}; + static uint16_t buffer16[I2S_READ_LEN]; + static uint8_t buffer8[I2S_READ_LEN]; ESP_ERROR_CHECK(i2s_read(I2S_NUM_0, &buffer16, sizeof(buffer16), &bytesRead, 100)); size_t samplesRead = bytesRead / 2; @@ -512,7 +509,7 @@ void loop() { // Check for incoming commands or audio from Android int bytesRead = 0; - uint8_t tempBuffer[TX_TEMP_AUDIO_BUFFER_SIZE]; + static uint8_t tempBuffer[TX_TEMP_AUDIO_BUFFER_SIZE]; int bytesAvailable = Serial.available(); if (bytesAvailable > 0) { bytesRead = Serial.readBytes(tempBuffer, bytesAvailable); @@ -616,14 +613,14 @@ void sendCmdToAndroid(byte cmdByte, const byte* params, size_t paramsLen) } void tuneTo(float freqTx, float freqRx, int tone, int squelch, String bandwidth) { - initI2SRx(); - // Tell radio module to tune int result = 0; - if (bandwidth.equals("W")) { - result = dra->group(DRA818_25K, freqTx, freqRx, tone, squelch, 0); - } else if (bandwidth.equals("N")) { - result = dra->group(DRA818_12K5, freqTx, freqRx, tone, squelch, 0); + while (!result) { + if (bandwidth.equals("W")) { + result = dra->group(DRA818_25K, freqTx, freqRx, tone, squelch, 0); + } else if (bandwidth.equals("N")) { + result = dra->group(DRA818_12K5, freqTx, freqRx, tone, squelch, 0); + } } // Serial.println("tuneTo: " + String(result)); } From 1cfd51ea61ae6b9d858b3d4cc0ec1ebccd174dc9 Mon Sep 17 00:00:00 2001 From: Dmitry Kaukov Date: Thu, 2 Jan 2025 16:36:21 +1100 Subject: [PATCH 4/8] Android: Switch AudioTrack to the AudioFormat.ENCODING_PCM_16BIT As some devices have broken PCM8 support --- .../kv4pht/radio/RadioAudioService.java | 20 ++++++++++++++++--- 1 file changed, 17 insertions(+), 3 deletions(-) diff --git a/android-src/KV4PHT/app/src/main/java/com/vagell/kv4pht/radio/RadioAudioService.java b/android-src/KV4PHT/app/src/main/java/com/vagell/kv4pht/radio/RadioAudioService.java index d450cc7b..381c1a8e 100644 --- a/android-src/KV4PHT/app/src/main/java/com/vagell/kv4pht/radio/RadioAudioService.java +++ b/android-src/KV4PHT/app/src/main/java/com/vagell/kv4pht/radio/RadioAudioService.java @@ -132,7 +132,7 @@ public class RadioAudioService extends Service { // For transmitting audio to ESP32 / radio public static final int AUDIO_SAMPLE_RATE = 22050; public static final int channelConfig = AudioFormat.CHANNEL_IN_MONO; - public static final int audioFormat = AudioFormat.ENCODING_PCM_8BIT; + public static final int audioFormat = AudioFormat.ENCODING_PCM_16BIT; public static final int minBufferSize = AudioRecord.getMinBufferSize(AUDIO_SAMPLE_RATE, channelConfig, audioFormat) * 4; private UsbManager usbManager; private UsbDevice esp32Device; @@ -1149,6 +1149,18 @@ public static UsbSerialPort getUsbSerialPort() { return serialPort; } + public static byte[] convert8BitTo16Bit(byte[] pcm8) { + byte[] pcm16 = new byte[pcm8.length * 2]; // 2 bytes per 16-bit sample + for (int i = 0; i < pcm8.length; i++) { + int unsignedSample = pcm8[i] & 0xFF; // Convert to unsigned + short sample16 = (short)((unsignedSample - 128) << 8); // Scale and shift + // Store as little-endian (least significant byte first) + pcm16[i * 2] = (byte)(sample16 & 0xFF); // LSB + pcm16[i * 2 + 1] = (byte)((sample16 >> 8) & 0xFF); // MSB + } + return pcm16; + } + private void handleESP32Data(byte[] data) { // Log.d("DEBUG", "Got bytes from ESP32: " + Arrays.toString(data)); /* try { @@ -1201,7 +1213,8 @@ private void handleESP32Data(byte[] data) { synchronized (audioTrack) { if (afskDemodulator != null) { // Avoid race condition at app start. // Play the audio. - audioTrack.write(data, 0, data.length); + byte[] pcm16 = convert8BitTo16Bit(data); + audioTrack.write(pcm16, 0, pcm16.length); // Add the audio samples to the AFSK demodulator. float[] audioAsFloats = convertPCM8ToFloatArray(data); @@ -1225,7 +1238,8 @@ private void handleESP32Data(byte[] data) { audioTrack.play(); } synchronized (audioTrack) { - audioTrack.write(rxBytesPrebuffer, 0, PRE_BUFFER_SIZE); + byte[] pcm16 = convert8BitTo16Bit(rxBytesPrebuffer); + audioTrack.write(pcm16, 0, pcm16.length); } } From 6ad9e312979169deceffb85e9a3d340a7f0c80c9 Mon Sep 17 00:00:00 2001 From: Dmitry Kaukov Date: Wed, 8 Jan 2025 10:27:49 +1100 Subject: [PATCH 5/8] Cleanup --- .../kv4p_ht_esp32_wroom_32/kv4p_ht_esp32_wroom_32.ino | 1 - 1 file changed, 1 deletion(-) diff --git a/microcontroller-src/kv4p_ht_esp32_wroom_32/kv4p_ht_esp32_wroom_32.ino b/microcontroller-src/kv4p_ht_esp32_wroom_32/kv4p_ht_esp32_wroom_32.ino index cae721bb..d6c2f19e 100644 --- a/microcontroller-src/kv4p_ht_esp32_wroom_32/kv4p_ht_esp32_wroom_32.ino +++ b/microcontroller-src/kv4p_ht_esp32_wroom_32/kv4p_ht_esp32_wroom_32.ino @@ -215,7 +215,6 @@ void initI2STx() { i2s_set_dac_mode(I2S_DAC_CHANNEL_RIGHT_EN); } -// Scale factor for fixed-point math (Q15 format) #define DECAY_TIME 0.25 // seconds #define ALPHA (1.0f - expf(-1.0f / (AUDIO_SAMPLE_RATE * (DECAY_TIME / logf(2.0f))))) From feed317dbace75109d0b371b451cb3d08814e0cb Mon Sep 17 00:00:00 2001 From: Dmitry Kaukov Date: Mon, 13 Jan 2025 07:47:04 +1100 Subject: [PATCH 6/8] Fixed merge confilict --- .../kv4p_ht_esp32_wroom_32/kv4p_ht_esp32_wroom_32.ino | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/microcontroller-src/kv4p_ht_esp32_wroom_32/kv4p_ht_esp32_wroom_32.ino b/microcontroller-src/kv4p_ht_esp32_wroom_32/kv4p_ht_esp32_wroom_32.ino index 6b879721..46e31cda 100644 --- a/microcontroller-src/kv4p_ht_esp32_wroom_32/kv4p_ht_esp32_wroom_32.ino +++ b/microcontroller-src/kv4p_ht_esp32_wroom_32/kv4p_ht_esp32_wroom_32.ino @@ -338,10 +338,8 @@ void loop() { while (!dra->filters(emphasis, highpass, lowpass)); } - break; - } + break; } - esp_task_wdt_reset(); return; } else if (mode == MODE_RX) { From 95af01cdcb2b87c79c3074fcfd160d7fb67c0c21 Mon Sep 17 00:00:00 2001 From: Dmitry Kaukov Date: Tue, 14 Jan 2025 17:13:12 +1100 Subject: [PATCH 7/8] signed pcm8 on wire --- .../main/java/com/vagell/kv4pht/radio/RadioAudioService.java | 2 +- .../kv4p_ht_esp32_wroom_32/kv4p_ht_esp32_wroom_32.ino | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/android-src/KV4PHT/app/src/main/java/com/vagell/kv4pht/radio/RadioAudioService.java b/android-src/KV4PHT/app/src/main/java/com/vagell/kv4pht/radio/RadioAudioService.java index 12197f17..d9c2d052 100644 --- a/android-src/KV4PHT/app/src/main/java/com/vagell/kv4pht/radio/RadioAudioService.java +++ b/android-src/KV4PHT/app/src/main/java/com/vagell/kv4pht/radio/RadioAudioService.java @@ -1155,7 +1155,7 @@ public static byte[] convert8BitTo16Bit(byte[] pcm8) { byte[] pcm16 = new byte[pcm8.length * 2]; // 2 bytes per 16-bit sample for (int i = 0; i < pcm8.length; i++) { int unsignedSample = pcm8[i] & 0xFF; // Convert to unsigned - short sample16 = (short)((unsignedSample - 128) << 8); // Scale and shift + short sample16 = (short)((unsignedSample) << 8); // Scale and shift // Store as little-endian (least significant byte first) pcm16[i * 2] = (byte)(sample16 & 0xFF); // LSB pcm16[i * 2 + 1] = (byte)((sample16 >> 8) & 0xFF); // MSB diff --git a/microcontroller-src/kv4p_ht_esp32_wroom_32/kv4p_ht_esp32_wroom_32.ino b/microcontroller-src/kv4p_ht_esp32_wroom_32/kv4p_ht_esp32_wroom_32.ino index 6cc40007..cd98fb34 100644 --- a/microcontroller-src/kv4p_ht_esp32_wroom_32/kv4p_ht_esp32_wroom_32.ino +++ b/microcontroller-src/kv4p_ht_esp32_wroom_32/kv4p_ht_esp32_wroom_32.ino @@ -506,7 +506,7 @@ void loop() { // Apply attenuation to the sample int16_t sample = (int32_t)remove_dc(((2048 - (buffer16[i] & 0xfff)) << 4)) * attenuation >> 8; - buffer8[i] = (sample >> 8) + 128; // Unsigned PCM8 + buffer8[i] = (sample >> 8); // Signed } Serial.write(buffer8, samplesRead); From 304c0899dbc9a76fd3f3e9ca009d03130937f7a3 Mon Sep 17 00:00:00 2001 From: Dmitry Kaukov Date: Mon, 20 Jan 2025 17:08:55 +1100 Subject: [PATCH 8/8] Fix for afskDemodulator --- .../java/com/vagell/kv4pht/radio/RadioAudioService.java | 9 +++------ 1 file changed, 3 insertions(+), 6 deletions(-) diff --git a/android-src/KV4PHT/app/src/main/java/com/vagell/kv4pht/radio/RadioAudioService.java b/android-src/KV4PHT/app/src/main/java/com/vagell/kv4pht/radio/RadioAudioService.java index 299f297d..304cc490 100644 --- a/android-src/KV4PHT/app/src/main/java/com/vagell/kv4pht/radio/RadioAudioService.java +++ b/android-src/KV4PHT/app/src/main/java/com/vagell/kv4pht/radio/RadioAudioService.java @@ -1229,7 +1229,7 @@ private void handleESP32Data(byte[] data) { audioTrack.write(pcm16, 0, pcm16.length); // Add the audio samples to the AFSK demodulator. - float[] audioAsFloats = convertPCM8ToFloatArray(data); + float[] audioAsFloats = convertPCM8SignedToFloatArray(data); afskDemodulator.addSamples(audioAsFloats, audioAsFloats.length); } @@ -1472,17 +1472,14 @@ private void handleParsedCommand(byte cmd, byte[] param) { } } - private float[] convertPCM8ToFloatArray(byte[] pcm8Data) { + private float[] convertPCM8SignedToFloatArray(byte[] pcm8Data) { // Create a float array of the same length as the input byte array float[] floatData = new float[pcm8Data.length]; // Iterate through the byte array and convert each sample for (int i = 0; i < pcm8Data.length; i++) { - // Convert unsigned 8-bit PCM to signed 8-bit value - int signedValue = (pcm8Data[i] & 0xFF) - 128; - // Normalize the signed 8-bit value to the range [-1.0, 1.0] - floatData[i] = signedValue / 128.0f; + floatData[i] = pcm8Data[i] / 128.0f; } return floatData;