From cebbda72ecb6cf23387e8da8705248ba6a198e65 Mon Sep 17 00:00:00 2001 From: Armel FAUVEAU Date: Thu, 26 Mar 2026 04:58:49 +0100 Subject: [PATCH] Add audio scope feature just for fun --- App/CMakeLists.txt | 1 + App/app/app.c | 13 +++++- App/ui/main.c | 100 ++++++++++++++++++++++++++++++++++++++++++++- App/ui/main.h | 7 ++++ CMakePresets.json | 2 + 5 files changed, 120 insertions(+), 3 deletions(-) diff --git a/App/CMakeLists.txt b/App/CMakeLists.txt index 697fbeda..97673365 100644 --- a/App/CMakeLists.txt +++ b/App/CMakeLists.txt @@ -161,6 +161,7 @@ enable_feature(ENABLE_SQUELCH_MORE_SENSITIVE) enable_feature(ENABLE_FASTER_CHANNEL_SCAN) enable_feature(ENABLE_RSSI_BAR) enable_feature(ENABLE_AUDIO_BAR) +enable_feature(ENABLE_FEAT_F4HWN_AUDIO_SCOPE) enable_feature(ENABLE_COPY_CHAN_TO_VFO) enable_feature(ENABLE_REDUCE_LOW_MID_TX_POWER) enable_feature(ENABLE_BYP_RAW_DEMODULATORS) diff --git a/App/app/app.c b/App/app/app.c index 12a8777b..b5ed912f 100644 --- a/App/app/app.c +++ b/App/app/app.c @@ -1364,12 +1364,23 @@ void APP_TimeSlice10ms(void) if (gCurrentFunction == FUNCTION_TRANSMIT) { // transmitting -#ifdef ENABLE_AUDIO_BAR +#if defined(ENABLE_AUDIO_BAR) && !defined(ENABLE_FEAT_F4HWN_AUDIO_SCOPE) if (gSetting_mic_bar && (gFlashLightBlinkCounter % (150 / 10)) == 0) // once every 150ms UI_DisplayAudioBar(); #endif } +#ifdef ENABLE_FEAT_F4HWN_AUDIO_SCOPE + // Sample audio amplitude every 30ms + if ((gFlashLightBlinkCounter % 3) == 0) + UI_AudioScope_AddSample(); + // Refresh display every 30ms during TX only (FM RX has no usable audio register) + if ((gFlashLightBlinkCounter % 3) == 0 && + gScreenToDisplay == DISPLAY_MAIN && + gCurrentFunction == FUNCTION_TRANSMIT) + UI_DisplayAudioScope(); +#endif + bool gUpdateDisplayCurrent = gUpdateDisplay; bool gUpdateStatusCurrent = gUpdateStatus; diff --git a/App/ui/main.c b/App/ui/main.c index e49a8a6a..1aa29248 100644 --- a/App/ui/main.c +++ b/App/ui/main.c @@ -158,15 +158,17 @@ static void DrawLevelBar(uint8_t xpos, uint8_t line, uint8_t level, uint8_t bars #endif #ifdef ENABLE_AUDIO_BAR - // Approximation of a logarithmic scale using integer arithmetic -uint8_t log2_approx(unsigned int value) { +static uint8_t log2_approx(unsigned int value) { uint8_t log = 0; while (value >>= 1) { log++; } return log; } +#endif + +#ifdef ENABLE_AUDIO_BAR void UI_DisplayAudioBar(void) { @@ -231,6 +233,92 @@ void UI_DisplayAudioBar(void) } #endif +#ifdef ENABLE_FEAT_F4HWN_AUDIO_SCOPE + +#define SCOPE_SAMPLES 42 + +static uint16_t g_scope_buf[SCOPE_SAMPLES]; +static uint8_t g_scope_write = 0; +static uint16_t g_scope_floor = 0; // persistent floor: snaps down fast, rises slowly + +void UI_AudioScope_AddSample(void) +{ + // REG_64 (VoiceAmplitudeOut) is only meaningful in TX (mic input). + // FM RX audio is frequency-encoded — no register gives the instantaneous waveform. + if (gCurrentFunction != FUNCTION_TRANSMIT) + return; + + const uint16_t value = BK4819_GetVoiceAmplitudeOut(); + g_scope_buf[g_scope_write] = value; + g_scope_write = (g_scope_write + 1) % SCOPE_SAMPLES; +} + +void UI_DisplayAudioScope(void) +{ + if (gLowBattery && !gLowBatteryConfirmed) + return; + +#ifdef ENABLE_FEAT_F4HWN + RxBlinkLed = 0; + RxBlinkLedCounter = 0; + BK4819_ToggleGpioOut(BK4819_GPIO6_PIN2_GREEN, false); + const unsigned int line = isMainOnly() ? 5 : 3; +#else + const unsigned int line = 3; +#endif + + if (gScreenToDisplay != DISPLAY_MAIN +#ifdef ENABLE_DTMF_CALLING + || gDTMF_CallState != DTMF_CALL_STATE_NONE +#endif + ) + return; + +#if defined(ENABLE_ALARM) || defined(ENABLE_TX1750) + if (gAlarmState != ALARM_STATE_OFF) + return; +#endif + + uint8_t *p_line = gFrameBuffer[line]; + memset(p_line, 0, LCD_WIDTH); + + // Find min and max across current buffer + uint16_t min_val = g_scope_buf[0]; + uint16_t max_val = g_scope_buf[0]; + for (uint8_t i = 1; i < SCOPE_SAMPLES; i++) { + if (g_scope_buf[i] < min_val) min_val = g_scope_buf[i]; + if (g_scope_buf[i] > max_val) max_val = g_scope_buf[i]; + } + + // Floor tracks buffer minimum with asymmetric IIR: + // - drops toward min smoothly (~500ms), avoiding instant-snap ghost + // - rises slowly (+2/frame ≈ +67/s) to handle loud constant voice + if (g_scope_floor > min_val) + g_scope_floor -= ((g_scope_floor - min_val) >> 3) + 1u; + else + g_scope_floor += 2u; + + const uint16_t range = (max_val > g_scope_floor) ? (max_val - g_scope_floor) : 0; + + for (uint8_t i = 0; i < SCOPE_SAMPLES; i++) { + const uint8_t idx = (g_scope_write + i) % SCOPE_SAMPLES; + uint8_t height = 0; + if (range >= 50u) { + const uint16_t v = (g_scope_buf[idx] > g_scope_floor) ? (g_scope_buf[idx] - g_scope_floor) : 0u; + height = (uint8_t)((uint32_t)v * 7u / range); + } + // Filled column using bits 6..0 only (bit 7 always off to avoid overlap with text below) + // At silence (height 0): single pixel at bit 6 (baseline) + const uint8_t mask = (height > 0u) ? (uint8_t)((0x7Fu << (7u - height)) & 0x7Fu) : 0x40u; + // 2px column + 1px gap per sample + p_line[i * 3u] |= mask; + p_line[i * 3u + 1u] |= mask; + } + + ST7565_BlitLine(line); +} +#endif // ENABLE_FEAT_F4HWN_AUDIO_SCOPE + void DisplayRSSIBar(const bool now) { #if defined(ENABLE_RSSI_BAR) @@ -1474,6 +1562,14 @@ void UI_DisplayMain(void) const bool rx = FUNCTION_IsRx(); +#ifdef ENABLE_FEAT_F4HWN_AUDIO_SCOPE + if (gCurrentFunction == FUNCTION_TRANSMIT) { + // Reserve the line so no other element overwrites it. + // Actual drawing is handled exclusively by the app.c timeslice. + center_line = CENTER_LINE_AUDIO_SCOPE; + } + else +#endif #ifdef ENABLE_AUDIO_BAR if (gSetting_mic_bar && gCurrentFunction == FUNCTION_TRANSMIT) { center_line = CENTER_LINE_AUDIO_BAR; diff --git a/App/ui/main.h b/App/ui/main.h index 90fbb9fe..8dd98f47 100644 --- a/App/ui/main.h +++ b/App/ui/main.h @@ -17,10 +17,13 @@ #ifndef UI_MAIN_H #define UI_MAIN_H +#include + enum center_line_t { CENTER_LINE_NONE = 0, CENTER_LINE_IN_USE, CENTER_LINE_AUDIO_BAR, + CENTER_LINE_AUDIO_SCOPE, CENTER_LINE_RSSI, CENTER_LINE_AM_FIX_DATA, CENTER_LINE_DTMF_DEC, @@ -38,6 +41,10 @@ typedef enum center_line_t center_line_t; extern center_line_t center_line; void UI_DisplayAudioBar(void); +#ifdef ENABLE_FEAT_F4HWN_AUDIO_SCOPE +void UI_AudioScope_AddSample(void); +void UI_DisplayAudioScope(void); +#endif void UI_MAIN_TimeSlice500ms(void); void UI_DisplayMain(void); diff --git a/CMakePresets.json b/CMakePresets.json index e82784d7..c19fd039 100644 --- a/CMakePresets.json +++ b/CMakePresets.json @@ -60,6 +60,7 @@ "ENABLE_FEAT_F4HWN_RESCUE_OPS": false, "ENABLE_FEAT_F4HWN_VOL": false, "ENABLE_FEAT_F4HWN_AUDIO": false, + "ENABLE_FEAT_F4HWN_AUDIO_SCOPE": false, "ENABLE_FEAT_F4HWN_RESET_VFO": true, "ENABLE_FEAT_F4HWN_PMR": false, "ENABLE_FEAT_F4HWN_GMRS_FRS_MURS": false, @@ -189,6 +190,7 @@ "ENABLE_FEAT_F4HWN_RESCUE_OPS": true, "ENABLE_FEAT_F4HWN_VOL": true, "ENABLE_FEAT_F4HWN_AUDIO": true, + "ENABLE_FEAT_F4HWN_AUDIO_SCOPE": true, "ENABLE_SWD": true, "EDITION_STRING": "Fusion", "TARGET": "f4hwn.fusion"