From e0d1770a19ec45d52f69504c6af623090260c25c Mon Sep 17 00:00:00 2001 From: Michatec Date: Tue, 7 Apr 2026 11:26:14 +0200 Subject: [PATCH] feat(dsp): refine audio processing and visualizer rendering --- app/src/main/cpp/dsp.cpp | 93 ++++++++----------- app/src/main/cpp/extra.cpp | 38 +++++--- .../radio/helpers/NativeAudioProcessor.kt | 10 +- .../radio/helpers/PreferencesHelper.kt | 4 +- 4 files changed, 74 insertions(+), 71 deletions(-) diff --git a/app/src/main/cpp/dsp.cpp b/app/src/main/cpp/dsp.cpp index 317a766..5903a8c 100644 --- a/app/src/main/cpp/dsp.cpp +++ b/app/src/main/cpp/dsp.cpp @@ -71,8 +71,6 @@ struct alignas(16) EqBandInterpolator { b1 = (-2.0f * c) * invA0; b2 = (1.0f - alpha / A) * invA0; } - - inline void clearState() { z1 = 0.0f; z2 = 0.0f; } }; struct alignas(16) BassFilter { @@ -144,17 +142,17 @@ public: float out = 0.0f; #pragma GCC unroll 4 for (int i = 0; i < 4; i++) { - float delayed = combs[i].read(); + float delayed = combs[static_cast(i)].read(); out += delayed; - combs[i].write(x + delayed * combFeedback[i] + DENORMAL_OFFSET); - combs[i].advance(); + combs[static_cast(i)].write(x + delayed * combFeedback[static_cast(i)] + DENORMAL_OFFSET); + combs[static_cast(i)].advance(); } out *= 0.25f; for (int i = 0; i < 2; i++) { - float bufOut = allpasses[i].read(); + float bufOut = allpasses[static_cast(i)].read(); float xOut = -0.5f * out + bufOut; - allpasses[i].write(out + 0.5f * bufOut); - allpasses[i].advance(); + allpasses[static_cast(i)].write(out + 0.5f * bufOut); + allpasses[static_cast(i)].advance(); out = xOut; } return x * (1.0f - m) + out * m; @@ -194,7 +192,7 @@ public: float rt = ratio.load(std::memory_order_acquire); for(int i=0; i envelope) ? attackCoef*envelope + (1-attackCoef)*absInput : releaseCoef*envelope + (1-releaseCoef)*absInput; + envelope = (absInput > envelope) ? attackCoef*envelope + (1.0f-attackCoef)*absInput : releaseCoef*envelope + (1.0f-releaseCoef)*absInput; float gain = (envelope>th)? (th + (envelope-th)/rt)/(envelope+1e-9f) : 1.0f; buffer[i]*=gain; } @@ -234,23 +232,17 @@ inline void fastFFT(std::complex* __restrict__ data, int n) { } } -inline void applyHannWindow(float* __restrict__ data, int size) { - for (int i = 0; i < size; i++) { - float window = 0.5f * (1.0f - cosf(2.0f * static_cast(M_PI) * i / (size - 1))); - data[i] *= window; - } -} - inline void applyHannWindowToReal(std::complex* __restrict__ data, int size) { + const auto fSizeMinus1 = static_cast(size - 1); for (int i = 0; i < size; i++) { - float window = 0.5f * (1.0f - cosf(2.0f * static_cast(M_PI) * i / (size - 1))); + float window = 0.5f * (1.0f - cosf(2.0f * static_cast(M_PI) * static_cast(i) / fSizeMinus1)); data[i] = std::complex(data[i].real() * window, data[i].imag()); } } inline float fastSoftClip(float x) { float ax = fabsf(x); - float sign = x > 0 ? 1.0f : -1.0f; + float sign = x > 0.0f ? 1.0f : -1.0f; if (ax > 1.0f) return sign; return x * (1.5f - 0.5f * x * x); } @@ -260,19 +252,19 @@ static EqBandInterpolator gEqR[NUM_EQ_BANDS]; static BassFilter gBassL, gBassR; static CompressorOptimized gCompressor; static ReverbOptimized gReverbL, gReverbR; -static alignas(16) std::array, FFT_SIZE> gFFTWork; +static std::array, FFT_SIZE> gFFTWork; static int gEqUpdateCounter = 0; inline void updateAllEqBands() { float sr = gSampleRate.load(std::memory_order_acquire); for (int b = 0; b < NUM_EQ_BANDS; b++) { float g = gEqL[b].targetGain.load(std::memory_order_acquire); - gEqL[b].setCoefficients(sr, EQ_FREQUENCIES[b], g, 1.0f); - gEqR[b].setCoefficients(sr, EQ_FREQUENCIES[b], g, 1.0f); + gEqL[b].setCoefficients(sr, EQ_FREQUENCIES[static_cast(b)], g, 1.0f); + gEqR[b].setCoefficients(sr, EQ_FREQUENCIES[static_cast(b)], g, 1.0f); } bool anyActive = false; - for (int b = 0; b < NUM_EQ_BANDS; b++) { - if (std::abs(gEqL[b].targetGain.load(std::memory_order_acquire)) > 0.1f) { + for (auto const& band : gEqL) { + if (std::abs(band.targetGain.load(std::memory_order_acquire)) > 0.1f) { anyActive = true; break; } @@ -296,13 +288,6 @@ JNIEXPORT void JNICALL Java_com_michatec_radio_helpers_NativeAudioProcessor_setR gReverbL.mix.store(m, std::memory_order_release); gReverbR.mix.store(m, std::memory_order_release); } -JNIEXPORT void JNICALL Java_com_michatec_radio_helpers_NativeAudioProcessor_setEqBand(JNIEnv*, jobject, jint b, jfloat g) { - if (b >= 0 && b < NUM_EQ_BANDS) { - gEqL[b].setTargetGain(g); - gEqR[b].setTargetGain(g); - gEqUpdateCounter = 1; - } -} JNIEXPORT void JNICALL Java_com_michatec_radio_helpers_NativeAudioProcessor_setEqFull(JNIEnv* env, jobject thiz, jfloatArray gains) { if (!gains) return; @@ -322,8 +307,12 @@ JNIEXPORT void JNICALL Java_com_michatec_radio_helpers_NativeAudioProcessor_setE env->ReleaseFloatArrayElements(gains, gainsPtr, JNI_ABORT); } JNIEXPORT void JNICALL Java_com_michatec_radio_helpers_NativeAudioProcessor_setBassBoost(JNIEnv*, jobject, jfloat g) { - gBassL.targetGain.store(g, std::memory_order_release); - gBassR.targetGain.store(g, std::memory_order_release); + float scaledGain = g * 4.0f; + gBassL.targetGain.store(scaledGain, std::memory_order_release); + gBassR.targetGain.store(scaledGain, std::memory_order_release); + float sr = gSampleRate.load(std::memory_order_acquire); + gBassL.applyGain(sr); + gBassR.applyGain(sr); if (std::abs(g) > 0.01f) { gBassL.active.store(true, std::memory_order_release); gBassR.active.store(true, std::memory_order_release); @@ -344,7 +333,7 @@ JNIEXPORT jfloatArray JNICALL Java_com_michatec_radio_helpers_NativeAudioProcess inline void computeLogarithmicFFT(float* output, const std::complex* input, int inputSize) { float sr = gSampleRate.load(std::memory_order_acquire); - float binWidth = sr / (2.0f * inputSize); + float binWidth = sr / (2.0f * static_cast(inputSize)); constexpr int NUM_BANDS = 256; constexpr float MIN_FREQ = 20.0f; constexpr float MAX_FREQ = 20000.0f; @@ -353,8 +342,8 @@ inline void computeLogarithmicFFT(float* output, const std::complex* inpu float logRange = logMax - logMin; for (int b = 0; b < NUM_BANDS; b++) { - float f1 = expf(logMin + (logRange * b / NUM_BANDS)); - float f2 = expf(logMin + (logRange * (b + 1) / NUM_BANDS)); + float f1 = expf(logMin + (logRange * static_cast(b) / static_cast(NUM_BANDS))); + float f2 = expf(logMin + (logRange * static_cast(b + 1) / static_cast(NUM_BANDS))); int idx1 = static_cast(f1 / binWidth); int idx2 = static_cast(f2 / binWidth); idx1 = std::max(0, std::min(idx1, inputSize - 1)); @@ -382,27 +371,27 @@ JNIEXPORT void JNICALL Java_com_michatec_radio_helpers_NativeAudioProcessor_proc } for (int i = 0; i < numFrames; i++) { - gLeftBuf[i] = static_cast(buffer[i * 2]) * INV_32768; - gRightBuf[i] = static_cast(buffer[i * 2 + 1]) * INV_32768; + gLeftBuf[static_cast(i)] = static_cast(buffer[i * 2]) * INV_32768; + gRightBuf[static_cast(i)] = static_cast(buffer[i * 2 + 1]) * INV_32768; } bool eqEnabled = gEqEnabled.load(std::memory_order_acquire); if (eqEnabled) { for (int i = 0; i < numFrames; i++) { - float xL = gLeftBuf[i]; - float xR = gRightBuf[i]; + float xL = gLeftBuf[static_cast(i)]; + float xR = gRightBuf[static_cast(i)]; for (int b = 0; b < NUM_EQ_BANDS; b++) { xL = gEqL[b].process(xL); xR = gEqR[b].process(xR); } - gLeftBuf[i] = xL; - gRightBuf[i] = xR; + gLeftBuf[static_cast(i)] = xL; + gRightBuf[static_cast(i)] = xR; } } for(int i = 0; i < numFrames; i++) { - gLeftBuf[i] = gBassL.process(gLeftBuf[i]); - gRightBuf[i] = gBassR.process(gRightBuf[i]); + gLeftBuf[static_cast(i)] = gBassL.process(gLeftBuf[static_cast(i)]); + gRightBuf[static_cast(i)] = gBassR.process(gRightBuf[static_cast(i)]); } gReverbL.processBlock(gLeftBuf.data(), gRightBuf.data(), numFrames); @@ -411,10 +400,10 @@ JNIEXPORT void JNICALL Java_com_michatec_radio_helpers_NativeAudioProcessor_proc if (stereoWidth != 1.0f) { float halfWidth = stereoWidth * 0.5f; for (int j = 0; j < numFrames; j++) { - float mid = (gLeftBuf[j] + gRightBuf[j]) * 0.5f; - float side = (gLeftBuf[j] - gRightBuf[j]) * halfWidth; - gLeftBuf[j] = mid + side; - gRightBuf[j] = mid - side; + float mid = (gLeftBuf[static_cast(j)] + gRightBuf[static_cast(j)]) * 0.5f; + float side = (gLeftBuf[static_cast(j)] - gRightBuf[static_cast(j)]) * halfWidth; + gLeftBuf[static_cast(j)] = mid + side; + gRightBuf[static_cast(j)] = mid - side; } } @@ -422,14 +411,14 @@ JNIEXPORT void JNICALL Java_com_michatec_radio_helpers_NativeAudioProcessor_proc if (numFrames >= FFT_SIZE) { for (int k = 0; k < FFT_SIZE; k++) { - gFFTWork[k] = std::complex(gLeftBuf[k], 0.0f); + gFFTWork[static_cast(k)] = std::complex(gLeftBuf[static_cast(k)], 0.0f); } } else { for (int k = 0; k < numFrames; k++) { - gFFTWork[k] = std::complex(gLeftBuf[k], 0.0f); + gFFTWork[static_cast(k)] = std::complex(gLeftBuf[static_cast(k)], 0.0f); } for (int k = numFrames; k < FFT_SIZE; k++) { - gFFTWork[k] = std::complex(0.0f, 0.0f); + gFFTWork[static_cast(k)] = std::complex(0.0f, 0.0f); } } @@ -438,8 +427,8 @@ JNIEXPORT void JNICALL Java_com_michatec_radio_helpers_NativeAudioProcessor_proc computeLogarithmicFFT(gFFTData.data(), gFFTWork.data(), FFT_SIZE / 2); for (int k = 0; k < numFrames; k++) { - buffer[k * 2] = static_cast(fastSoftClip(gLeftBuf[k]) * 32767.0f); - buffer[k * 2 + 1] = static_cast(fastSoftClip(gRightBuf[k]) * 32767.0f); + buffer[k * 2] = static_cast(fastSoftClip(gLeftBuf[static_cast(k)]) * 32767.0f); + buffer[k * 2 + 1] = static_cast(fastSoftClip(gRightBuf[static_cast(k)]) * 32767.0f); } } } diff --git a/app/src/main/cpp/extra.cpp b/app/src/main/cpp/extra.cpp index e0c6ef2..48a05c9 100644 --- a/app/src/main/cpp/extra.cpp +++ b/app/src/main/cpp/extra.cpp @@ -4,7 +4,7 @@ #include #include #include - +#include extern "C" { @@ -29,32 +29,48 @@ Java_com_michatec_radio_helpers_ExtrasHelper_visualize(JNIEnv *env, jclass clazz // Clear background (Dark Grey) for (int y = 0; y < buffer.height; y++) { + uint32_t* row = pixels + (y * buffer.stride); for (int x = 0; x < buffer.width; x++) { - pixels[y * buffer.stride + x] = 0xFF121212; + row[x] = 0xFF121212; } } - // Draw bars - int displayBins = std::min(static_cast(len), 128); + // Draw bars - fewer bins = thicker bars + int displayBins = 40; float barWidth = static_cast(buffer.width) / static_cast(displayBins); + int padding = static_cast(barWidth * 0.2f); + if (padding < 1) padding = 1; for (int i = 0; i < displayBins; i++) { - // Keep original order: bass (low freq) at left, treble (high freq) at right - float val = body[i]; - float scaledVal = val * 5.0f; + // Map display bin to data index + int dataIdx = (i * len) / displayBins; + float val = body[dataIdx]; + + // Use square root to compress the range (so peaks don't hit the top too easily) + // and a lower multiplier (0.4f) to reduce overall height + float scaledVal = sqrtf(val) * 0.5f; int barHeight = static_cast(scaledVal * static_cast(buffer.height)); - if (barHeight > buffer.height) barHeight = buffer.height; - if (barHeight < 12) barHeight = 12; // Min height + // Cap height at 75% to leave some room at the top + int maxH = static_cast(static_cast(buffer.height) * 0.75f); + if (barHeight > maxH) barHeight = maxH; + if (barHeight < 4) barHeight = 4; // Minimal visible line int startX = static_cast(static_cast(i) * barWidth); int endX = static_cast(static_cast(i + 1) * barWidth); - int barBottom = buffer.height; + + int drawStartX = startX + padding; + int drawEndX = endX - padding; + if (drawEndX <= drawStartX) drawEndX = drawStartX + 1; + + int barBottom = buffer.height - 4; // Bottom margin int barTop = barBottom - barHeight; - for (int x = startX; x <= endX; x++) { + for (int x = drawStartX; x < drawEndX; x++) { if (x < 0 || x >= buffer.width) continue; for (int y = barTop; y < barBottom; y++) { + if (y < 0 || y >= buffer.height) continue; + // Using the same color, but now height is controlled pixels[y * buffer.stride + x] = 0xFFC5DA03; } } diff --git a/app/src/main/java/com/michatec/radio/helpers/NativeAudioProcessor.kt b/app/src/main/java/com/michatec/radio/helpers/NativeAudioProcessor.kt index eaac6ff..f88ce35 100644 --- a/app/src/main/java/com/michatec/radio/helpers/NativeAudioProcessor.kt +++ b/app/src/main/java/com/michatec/radio/helpers/NativeAudioProcessor.kt @@ -30,7 +30,6 @@ class NativeAudioProcessor : BaseAudioProcessor() { private external fun setSampleRate(sampleRate: Float) private external fun setDrcEnabled(enabled: Boolean) private external fun setReverbMix(mix: Float) - private external fun setEqBand(band: Int, gainDb: Float) private external fun setEqFull(gains: FloatArray) private external fun setBassBoost(gainDb: Float) private external fun setStereoWidth(width: Float) @@ -40,7 +39,6 @@ class NativeAudioProcessor : BaseAudioProcessor() { // ===== API ===== fun enableDrc(enabled: Boolean) = setDrcEnabled(enabled) fun setReverb(mix: Float) = setReverbMix(mix) - fun setEq(band: Int, gainDb: Float) = setEqBand(band, gainDb) fun setEqAll(gains: FloatArray) = setEqFull(gains) fun enableBassBoost(gainDb: Float) = setBassBoost(gainDb) fun setWidth(width: Float) = setStereoWidth(width) @@ -102,21 +100,21 @@ class NativeAudioProcessor : BaseAudioProcessor() { setReverb(0.10f) setWidth(1.1f) setEqAll(floatArrayOf(2f, 1f, 0f, -1f, -1f, 0f, 1f, 2f, 2f, 3f)) - enableBassBoost(0.6f) + enableBassBoost(0.9f) } fun setPresetPop() { enableDrc(true) - setReverb(0.15f) + setReverb(0.10f) setWidth(1.05f) setEqAll(floatArrayOf(0f, 1f, 1f, 1f, 0f, 0f, 1f, 2f, 2f, 1f)) - enableBassBoost(0.5f) + enableBassBoost(0.6f) } fun setPresetJazz() { enableDrc(false) setReverb(0.15f) - setWidth(1.0f) + setWidth(0.8f) setEqAll(floatArrayOf(0f, 0f, 1f, 1f, 0f, 0f, 1f, 1f, 0f, 0f)) enableBassBoost(0.2f) } diff --git a/app/src/main/java/com/michatec/radio/helpers/PreferencesHelper.kt b/app/src/main/java/com/michatec/radio/helpers/PreferencesHelper.kt index bbec2ad..7e811b1 100644 --- a/app/src/main/java/com/michatec/radio/helpers/PreferencesHelper.kt +++ b/app/src/main/java/com/michatec/radio/helpers/PreferencesHelper.kt @@ -257,13 +257,13 @@ object PreferencesHelper { /* Loads Bass Boost gain */ fun loadBassBoost(): Float { - return if (sharedPreferences.getBoolean(Keys.PREF_BASS_BOOST, false)) 0.6f else 0.0f + return if (sharedPreferences.getBoolean(Keys.PREF_BASS_BOOST, false)) 1f else 0.0f } /* Loads Reverb mix */ fun loadReverb(): Float { - return if (sharedPreferences.getBoolean(Keys.PREF_REVERB, false)) 0.2f else 0.0f + return if (sharedPreferences.getBoolean(Keys.PREF_REVERB, false)) 0.18f else 0.0f }