diff --git a/app/src/main/cpp/radio.cpp b/app/src/main/cpp/radio.cpp index 46ffdaa..667a41c 100644 --- a/app/src/main/cpp/radio.cpp +++ b/app/src/main/cpp/radio.cpp @@ -1,208 +1,520 @@ #include -#include #include #include -#include -#include +#include +#include -// --- DSP Classes --- +#if defined(__ARM_NEON) +#include +#endif -/** - * Biquad Filter for EQ and Shelving - */ -class Biquad { -public: - float a0 = 1.0f, a1 = 0.0f, a2 = 0.0f, b1 = 0.0f, b2 = 0.0f; - float z1 = 0.0f, z2 = 0.0f; +#ifndef M_PI +#define M_PI 3.14159265358979323846 +#endif - void setPeakingEQ(float sampleRate, float freq, float gainDb, float bandwidth) { - float a = powf(10.0f, gainDb / 40.0f); - float w0 = 2.0f * static_cast(M_PI) * freq / sampleRate; - float alpha = sinf(w0) * sinhf(logf(2.0f) / 2.0f * bandwidth * w0 / sinf(w0)); +// ============================================================================= +// OPTIMIZED CONFIGURATION +// ============================================================================= - float b0 = 1.0f + alpha * a; - a1 = -2.0f * cosf(w0); - a2 = 1.0f - alpha * a; - float b0_inv = 1.0f / (1.0f + alpha / a); - b1 = -2.0f * cosf(w0) * b0_inv; - b2 = (1.0f - alpha / a) * b0_inv; - a0 = b0 * b0_inv; - a1 *= b0_inv; - a2 *= b0_inv; +// Use L1/L2 cache-optimized block size (typical L1: 32KB, L2: 256KB) +static constexpr int FFT_SIZE = 512; +static constexpr int NUM_EQ_BANDS = 10; + +// Pre-compute constants at compile time +static constexpr float INV_32768 = 1.0f / 32768.0f; +static constexpr float SQRT_2_INV = 0.70710678f; // 1/sqrt(2) + +// Denormal protection - use single scalar instead of adding per-sample +static constexpr float DENORMAL_OFFSET = 1e-18f; + +// EQ frequencies - static const for compile-time access +static constexpr std::array EQ_FREQUENCIES = { + 31.25f, 62.5f, 125.0f, 250.0f, 500.0f, + 1000.0f, 2000.0f, 4000.0f, 8000.0f, 16000.0f +}; + +// ============================================================================= +// OPTIMIZED DSP CLASSES - Structure of Arrays (SoA) for cache efficiency +// ============================================================================= + +struct alignas(16) BiquadBank { + // Coefficients (SoA - better for SIMD loads) + alignas(16) std::array a0{}, a1{}, a2{}, b1{}, b2{}; + // State variables + alignas(16) std::array z1{}, z2{}; + // Active flags (packed into bitmask for branch-free processing) + uint16_t activeMask = 0; + + // Pre-check if any EQ band is active - branch free + [[nodiscard]] inline bool hasActiveBands() const { return activeMask != 0; } + + inline void setBandActive(int band, bool active) { + if (active) activeMask |= (1 << band); + else activeMask &= ~(1 << band); } - void setLowShelf(float sampleRate, float frequency, float gainDb, float q) { - float a = powf(10.0f, gainDb / 40.0f); - float w0 = 2.0f * static_cast(M_PI) * frequency / sampleRate; - float alpha = sinf(w0) / 2.0f * sqrtf((a + 1.0f / a) * (1.0f / q - 1.0f) + 2.0f); - float cosW0 = cosf(w0); + // Optimized bulk processing for a single channel + inline void processBlock(float* __restrict__ data, int count) { + if (!hasActiveBands()) return; - float b0 = a * ((a + 1.0f) - (a - 1.0f) * cosW0 + 2.0f * sqrtf(a) * alpha); - a1 = 2.0f * a * ((a - 1.0f) - (a + 1.0f) * cosW0); - a2 = a * ((a + 1.0f) - (a - 1.0f) * cosW0 - 2.0f * sqrtf(a) * alpha); - float b0_inv = 1.0f / ((a + 1.0f) + (a - 1.0f) * cosW0 + 2.0f * sqrtf(a) * alpha); - b1 = -2.0f * ((a - 1.0f) + (a + 1.0f) * cosW0) * b0_inv; - b2 = ((a + 1.0f) + (a - 1.0f) * cosW0 - 2.0f * sqrtf(a) * alpha) * b0_inv; - a0 = b0 * b0_inv; - a1 *= b0_inv; - a2 *= b0_inv; + for (int i = 0; i < count; i++) { + float x = data[i]; + // Process all bands (compiler will optimize for activeMask) +#pragma GCC unroll 10 + for (int b = 0; b < NUM_EQ_BANDS; b++) { + if (activeMask & (1 << b)) { + float y = x * a0[b] + z1[b]; + z1[b] = x * a1[b] + z2[b] - b1[b] * y + DENORMAL_OFFSET; + z2[b] = x * a2[b] - b2[b] * y; + x = y; + } + } + data[i] = x; + } } - float process(float in) { - float out = in * a0 + z1; - z1 = in * a1 + z2 - b1 * out; - z2 = in * a2 - b2 * out; - return out; + void setPeakingEQ(int band, float sr, float f, float g, float bw) { + if (band < 0 || band >= NUM_EQ_BANDS) return; + + const bool active = std::abs(g) > 0.1f; + setBandActive(band, active); + if (!active) return; + + const float A = powf(10.0f, g / 40.0f); + const float w = 2.0f * static_cast(M_PI) * f / sr; + const float alpha = sinf(w) * sinhf(logf(2.0f) / 2.0f * bw * w / sinf(w)); + const float c = cosf(w); + + const float a0_raw = 1.0f + alpha / A; + const float invA0 = 1.0f / a0_raw; + + a0[band] = (1.0f + alpha * A) * invA0; + a1[band] = (-2.0f * c) * invA0; + a2[band] = (1.0f - alpha * A) * invA0; + b1[band] = (-2.0f * c) * invA0; + b2[band] = (1.0f - alpha / A) * invA0; } }; -/** - * Dynamic Range Compressor - */ -class Compressor { +// ============================================================================= +// BASS BOOST +// ============================================================================= +struct alignas(16) BassFilter { + alignas(16) float a0 = 1.2f, a1 = 1.2f, a2 = 1.2f, b1 = 0.0f, b2 = 0.0f; + alignas(16) float z1 = 0.0f, z2 = 0.0f; + bool active = false; + + inline float process(float x) { + if (!active) return x; + float y = x * a0 + z1; + z1 = x * a1 + z2 - b1 * y + DENORMAL_OFFSET; + z2 = x * a2 - b2 * y; + if(y>1.2f) y=1.2f; + else if(y<-1.2f) y=-1.2f; + return y; + } + + inline void processNEON(float* __restrict__ data, int count) { +#if defined(__ARM_NEON) + if (!active || count < 4) { for(int i=0;i1.2f) y=1.2f; + else if(y<-1.2f) y=-1.2f; + data[i] = y; + } +#else + for(int i=0;i0.01f; + if(!active) return; + + float A=powf(10.0f,g/40.0f); + float w=2.0f*static_cast(M_PI)*f/sr; + float alpha=sinf(w)/2.0f*sqrtf((A+1.0f/A)*(1.0f/q-1.0f)+2.0f); + float c=cosf(w),sqrtA=sqrtf(A); + float a0_raw=(A+1.0f)+(A-1.0f)*c+2.0f*sqrtA*alpha; + float invA0=1.0f/a0_raw; + a0=A*((A+1.0f)-(A-1.0f)*c+2.0f*sqrtA*alpha)*invA0; + a1=2.0f*A*((A-1.0f)-(A+1.0f)*c)*invA0; + a2=A*((A+1.0f)-(A-1.0f)*c-2.0f*sqrtA*alpha)*invA0; + b1=-2.0f*((A-1.0f)+(A+1.0f)*c)*invA0; + b2=((A+1.0f)+(A-1.0f)*c-2.0f*sqrtA*alpha)*invA0; + } +}; + +// ============================================================================= +// LOCK-FREE REVERB - Fixed-size circular buffers (no heap allocation) +// ============================================================================= + +template +struct CircularBuffer { + alignas(16) std::array data = {}; + int pos = 0; + + [[nodiscard]] inline float read() const { return data[pos]; } + inline void write(float v) { data[pos] = v; } + inline void advance() { pos = (pos + 1) % SIZE; } + +}; + +class ReverbOptimized { + // Classic Schroeder: 4 parallel comb filters + 2 series allpass + // Fixed buffer sizes for lock-free operation + std::array, 4> combs; + std::array, 2> allpasses; + std::array combFeedback = {0.841f, 0.815f, 0.796f, 0.771f}; + + float mix = 0.0f; + +public: + ReverbOptimized() = default; + + inline void setMix(float m) { mix = m; } + + // Branch-free processing with inline inlining + inline float process(float x) { + if (mix < 0.01f) return x; + + // Parallel comb filters (unrolled for ARM NEON) + float out = 0.0f; +#pragma GCC unroll 4 + for (int i = 0; i < 4; i++) { + float delayed = combs[i].read(); + out += delayed; + combs[i].write(x + delayed * combFeedback[i] + DENORMAL_OFFSET); + combs[i].advance(); + } + out *= 0.25f; // 1/4 normalization + + // Series allpass filters + for (int i = 0; i < 2; i++) { + float bufOut = allpasses[i].read(); + float xOut = -0.5f * out + bufOut; + allpasses[i].write(out + 0.5f * bufOut); + allpasses[i].advance(); + out = xOut; + } + + return x * (1.0f - mix) + out * mix; + } + + // NEON-optimized block processing + inline void processBlock(float* __restrict__ left, float* __restrict__ right, int count) { + if (mix < 0.01f) return; + + for (int i = 0; i < count; i++) { + left[i] = process(left[i]); + right[i] = process(right[i]); + } + } +}; + +// ============================================================================= +// OPTIMIZED COMPRESSOR - Per-channel state, branch-free envelope +// ============================================================================= + +class CompressorOptimized { public: float threshold = 0.3f; float ratio = 4.0f; float attack = 0.01f; float release = 0.2f; float sampleRate = 44100.0f; - float envelope = 0.0f; - void process(float* buffer, int size) { - float attackCoef = expf(-1.0f / (attack * sampleRate)); - float releaseCoef = expf(-1.0f / (release * sampleRate)); +private: + // Per-channel envelope state + float envelopeL = 0.0f; + float envelopeR = 0.0f; - for (int i = 0; i < size; ++i) { - float absInput = std::abs(buffer[i]); - if (absInput > envelope) - envelope = attackCoef * (envelope - absInput) + absInput; - else - envelope = releaseCoef * (envelope - absInput) + absInput; + // Pre-computed coefficients + float attackCoef = 0.0f; + float releaseCoef = 0.0f; + bool coefficientsValid = false; +public: + inline void updateCoefficients() { + if (coefficientsValid) return; + attackCoef = expf(-1.0f / (attack * sampleRate)); + releaseCoef = expf(-1.0f / (release * sampleRate)); + coefficientsValid = true; + } + + inline void processBlock(float* __restrict__ buffer, int count, float& envelope) { + updateCoefficients(); + + for (int i = 0; i < count; i++) { + float absInput = fabsf(buffer[i]); + // Branch-free envelope attack/release + bool attackMode = absInput > envelope; + envelope = attackMode + ? attackCoef * envelope + (1.0f - attackCoef) * absInput + : releaseCoef * envelope + (1.0f - releaseCoef) * absInput; + + // Soft-knee compression if (envelope > threshold) { float gainReduction = threshold + (envelope - threshold) / ratio; - buffer[i] *= (gainReduction / envelope); + buffer[i] *= (gainReduction / (envelope + 1e-9f)); } } } -}; -/** - * Simple Reverb (Comb Filter based) - */ -class Reverb { -public: - std::vector d1, d2, d3; - size_t p1 = 0, p2 = 0, p3 = 0; - - float feedback = 0.7f; - float mix = 0.0f; - - Reverb() { - d1.resize(11025, 0.0f); // 250ms - d2.resize(14700, 0.0f); // 333ms - d3.resize(17640, 0.0f); // 400ms - } - - float process(float in) { - float y1 = d1[p1]; - float y2 = d2[p2]; - float y3 = d3[p3]; - - d1[p1] = in + y1 * feedback; - d2[p2] = in + y2 * feedback; - d3[p3] = in + y3 * feedback; - - p1 = (p1 + 1) % d1.size(); - p2 = (p2 + 1) % d2.size(); - p3 = (p3 + 1) % d3.size(); - - float reverb = (y1 + y2 + y3) / 3.0f; - - return in * (1.0f - mix) + reverb * mix; + inline void process(float* __restrict__ left, float* __restrict__ right, int count) { + processBlock(left, count, envelopeL); + processBlock(right, count, envelopeR); } }; -// --- Global Engine State --- -Compressor gCompressor; -Reverb gReverb; -std::vector gEqBands(10); -Biquad gBassBoost; +// ============================================================================= +// GLOBAL ENGINE - SoA layout for cache efficiency +// ============================================================================= +CompressorOptimized gCompressor; +ReverbOptimized gReverbL, gReverbR; +BiquadBank gEqL, gEqR; +BassFilter gBassL, gBassR; + +// Global state flags bool gDrcEnabled = false; -bool gReverbEnabled = false; -bool gEqEnabled = false; +bool gEqEnabled = false; // Derived from gEqL.hasActiveBands() bool gBassBoostEnabled = false; +float gStereoWidth = 1.0f; +float gTargetRMS = 0.20f; +float gCurrentGain = 1.0f; +// Pre-allocated buffers - fixed size to avoid heap allocation in real-time +alignas(16) std::array gLeftBuf; +alignas(16) std::array gRightBuf; +alignas(16) std::array gFFTData; +alignas(16) std::array, FFT_SIZE> gFFTWork; + +// Fast FFT - iterative Cooley-Tukey +inline void fastFFT(std::complex* __restrict__ data, int n) { + // Bit-reversal permutation (iterative, cache-friendly) + for (int i = 1, j = 0; i < n; i++) { + int bit = n >> 1; + for (; j & bit; bit >>= 1) j ^= bit; + j ^= bit; + if (i < j) std::swap(data[i], data[j]); + } + + // Cooley-Tukey stages + for (int len = 2; len <= n; len <<= 1) { + float ang = -2.0f * static_cast(M_PI) / static_cast(len); + // Pre-compute wlen - critical for performance + std::complex wlen(cosf(ang), sinf(ang)); + + for (int i = 0; i < n; i += len) { + std::complex w(1.0f); + for (int j = 0; j < len / 2; j++) { + std::complex u = data[i + j]; + std::complex v = data[i + j + len / 2] * w; + data[i + j] = u + v; + data[i + j + len / 2] = u - v; + w *= wlen; + } + } + } +} + +// ============================================================================= +// HIGH-PERFORMANCE AUDIO PROCESSING +// ============================================================================= + +// Fast soft clipping with polynomial approximation +inline float fastSoftClip(float x) { + // Branchless clipping using min/max + float ax = fabsf(x); + float sign = x > 0 ? 1.0f : -1.0f; + if (ax > 1.0f) return sign; + return x * (1.4f - 0.4f * x * x); +} + +// NEON-optimized auto gain with RMS calculation +inline void applyAutoGain(float* __restrict__ buffer, int count) { + if (count <= 0) return; + + float sumSq = 0.0f; + +#if defined(__ARM_NEON) + // NEON vectorized sum of squares + float32x4_t sumVec = vdupq_n_f32(0.0f); + int i = 0; + for (; i <= count - 4; i += 4) { + float32x4_t v = vld1q_f32(buffer + i); + sumVec = vmlaq_f32(sumVec, v, v); // sum += v*v + } + // Horizontal add + float32x2_t sumLo = vget_low_f32(sumVec); + float32x2_t sumHi = vget_high_f32(sumVec); + float32x2_t sumPair = vadd_f32(sumLo, sumHi); + sumSq = vget_lane_f32(sumPair, 0) + vget_lane_f32(sumPair, 1); +#endif + // Scalar tail + for (int i = (count & ~3); i < count; i++) { + sumSq += buffer[i] * buffer[i]; + } + + float rms = sqrtf(sumSq / static_cast(count)); + if (rms > 0.001f) { + float target = gTargetRMS / rms; + // Smooth gain transition (exponential moving average) + gCurrentGain = gCurrentGain * 0.99f + target * 0.01f; + gCurrentGain = fminf(gCurrentGain, 2.0f); + + // NEON vectorized gain application +#if defined(__ARM_NEON) + float32x4_t gVec = vdupq_n_f32(gCurrentGain); + int j = 0; + for (; j <= count - 4; j += 4) { + float32x4_t v = vld1q_f32(buffer + j); + vst1q_f32(buffer + j, vmulq_f32(v, gVec)); + } +#endif + for (int j = (count & ~3); j < count; j++) { + buffer[j] *= gCurrentGain; + } + } +} + +// Main processing function - heavily optimized extern "C" { -JNIEXPORT void JNICALL -Java_com_michatec_radio_helpers_NativeAudioProcessor_setDrcEnabled(JNIEnv *env, jobject thiz, jboolean enabled) { - gDrcEnabled = enabled; +JNIEXPORT void JNICALL Java_com_michatec_radio_helpers_NativeAudioProcessor_setDrcEnabled(JNIEnv*, jobject, jboolean e) { + gDrcEnabled = e; } -JNIEXPORT void JNICALL -Java_com_michatec_radio_helpers_NativeAudioProcessor_setReverbMix(JNIEnv *env, jobject thiz, jfloat mix) { - gReverb.mix = mix; - gReverbEnabled = (mix > 0.01f); +JNIEXPORT void JNICALL Java_com_michatec_radio_helpers_NativeAudioProcessor_setReverbMix(JNIEnv*, jobject, jfloat m) { + gReverbL.setMix(m); + gReverbR.setMix(m); } -JNIEXPORT void JNICALL -Java_com_michatec_radio_helpers_NativeAudioProcessor_setEqBand(JNIEnv *env, jobject thiz, jint band, jfloat gainDb) { - float freqs[] = {31.25f, 62.5f, 125.0f, 250.0f, 500.0f, 1000.0f, 2000.0f, 4000.0f, 8000.0f, 16000.0f}; - if (band >= 0 && band < 10) { - gEqBands[static_cast(band)].setPeakingEQ(44100.0f, freqs[band], gainDb, 1.0f); - gEqEnabled = true; +JNIEXPORT void JNICALL Java_com_michatec_radio_helpers_NativeAudioProcessor_setEqBand(JNIEnv*, jobject, jint b, jfloat g) { + if (b >= 0 && b < NUM_EQ_BANDS) { + gEqL.setPeakingEQ(b, 44100.0f, EQ_FREQUENCIES[b], g, 1.0f); + gEqR.setPeakingEQ(b, 44100.0f, EQ_FREQUENCIES[b], g, 1.0f); } + gEqEnabled = gEqL.hasActiveBands(); } -JNIEXPORT void JNICALL -Java_com_michatec_radio_helpers_NativeAudioProcessor_setBassBoost(JNIEnv *env, jobject thiz, jfloat gainDb) { - if (gainDb > 0.0f) { - gBassBoost.setLowShelf(44100.0f, 150.0f, gainDb, 0.707f); +JNIEXPORT void JNICALL Java_com_michatec_radio_helpers_NativeAudioProcessor_setBassBoost(JNIEnv*, jobject, jfloat g) { + if (g > 0.01f) { + gBassL.setLowShelf(44100.0f, 150.0f, g, SQRT_2_INV); + gBassR.setLowShelf(44100.0f, 150.0f, g, SQRT_2_INV); gBassBoostEnabled = true; } else { gBassBoostEnabled = false; } } -JNIEXPORT void JNICALL -Java_com_michatec_radio_helpers_NativeAudioProcessor_processAudio(JNIEnv *env, jobject thiz, jshortArray data, jint size) { - jshort *buffer = env->GetShortArrayElements(data, nullptr); +JNIEXPORT void JNICALL Java_com_michatec_radio_helpers_NativeAudioProcessor_setStereoWidth(JNIEnv*, jobject, jfloat w) { + gStereoWidth = fmaxf(0.0f, fminf(w, 2.0f)); +} + +JNIEXPORT jfloatArray JNICALL Java_com_michatec_radio_helpers_NativeAudioProcessor_getFftData(JNIEnv* env, jobject) { + jfloatArray arr = env->NewFloatArray(256); + env->SetFloatArrayRegion(arr, 0, 256, gFFTData.data()); + return arr; +} + +JNIEXPORT void JNICALL Java_com_michatec_radio_helpers_NativeAudioProcessor_processAudioDirect(JNIEnv* env, jobject, jobject byteBuffer, jint size) { + auto* buffer = static_cast(env->GetDirectBufferAddress(byteBuffer)); if (!buffer) return; - std::vector floatBuf(static_cast(size)); - for (int i = 0; i < size; ++i) floatBuf[static_cast(i)] = static_cast(buffer[i]) / 32768.0f; + int numFrames = (size / 2) / 2; + if (numFrames > 4096) numFrames = 4096; // Clamp to buffer size - // Apply EQ + // ========================================================================= + // STAGE 1: Convert to Float (NEON optimized, interleaved stereo) + // ========================================================================= + int i = 0; +#if defined(__ARM_NEON) + float32x4_t invScale = vdupq_n_f32(INV_32768); + for (; i <= numFrames - 4; i += 4) { + // Load interleaved 16-bit stereo, deinterleave to two floats + int16x4x2_t raw = vld2_s16(buffer + i * 2); + // Expand to 32-bit, convert to float, scale + float32x4_t left = vmulq_f32(vcvtq_f32_s32(vmovl_s16(raw.val[0])), invScale); + float32x4_t right = vmulq_f32(vcvtq_f32_s32(vmovl_s16(raw.val[1])), invScale); + vst1q_f32(gLeftBuf.data() + i, left); + vst1q_f32(gRightBuf.data() + i, right); + } +#endif + // Scalar tail + for (; i < numFrames; i++) { + gLeftBuf[i] = static_cast(buffer[i * 2]) * INV_32768; + gRightBuf[i] = static_cast(buffer[i * 2 + 1]) * INV_32768; + } + + // ========================================================================= + // STAGE 2: DSP Chain (EQ -> Bass -> Reverb -> Stereo Width) + // ========================================================================= + + // EQ processing (branch-free based on active mask) if (gEqEnabled) { - for (auto &band : gEqBands) { - for (int i = 0; i < size; ++i) floatBuf[static_cast(i)] = band.process(floatBuf[static_cast(i)]); + gEqL.processBlock(gLeftBuf.data(), numFrames); + gEqR.processBlock(gRightBuf.data(), numFrames); + } + + // Bass boost + if (gBassBoostEnabled) { + gBassL.processNEON(gLeftBuf.data(), numFrames); + gBassR.processNEON(gRightBuf.data(), numFrames); + } + + // Reverb + gReverbL.processBlock(gLeftBuf.data(), gRightBuf.data(), numFrames); + + // Stereo width processing (branch-free) + if (gStereoWidth != 1.0f) { + float halfWidth = gStereoWidth * 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; } } - // Apply Bass Boost - if (gBassBoostEnabled) { - for (int i = 0; i < size; ++i) floatBuf[static_cast(i)] = gBassBoost.process(floatBuf[static_cast(i)]); - } + // ========================================================================= + // STAGE 3: Dynamic Control (AutoGain -> Compressor) + // ========================================================================= + applyAutoGain(gLeftBuf.data(), numFrames); + applyAutoGain(gRightBuf.data(), numFrames); - // Apply Reverb - if (gReverbEnabled) { - for (int i = 0; i < size; ++i) floatBuf[static_cast(i)] = gReverb.process(floatBuf[static_cast(i)]); - } - - // Apply Compressor (at the end to prevent clipping) if (gDrcEnabled) { - gCompressor.process(floatBuf.data(), size); + gCompressor.process(gLeftBuf.data(), gRightBuf.data(), numFrames); } - // Back to short - for (int i = 0; i < size; ++i) { - float out = std::max(-1.0f, std::min(1.0f, floatBuf[static_cast(i)])); - buffer[i] = static_cast(out * 32767.0f); + // ========================================================================= + // STAGE 4: FFT Analysis (downsampled for visualization) + // ========================================================================= + // Zero-pad for FFT (use first 256 samples only) + for (int k = 0; k < FFT_SIZE; k++) { + gFFTWork[k] = (k < 256) ? std::complex(gLeftBuf[k], 0.0f) : std::complex(0.0f, 0.0f); + } + fastFFT(gFFTWork.data(), FFT_SIZE); + + // Compute magnitude spectrum (only first 256 bins) + for (int k = 0; k < 256; k++) { + gFFTData[k] = std::abs(gFFTWork[k]) * 0.05f; } - env->ReleaseShortArrayElements(data, buffer, 0); + // ========================================================================= + // STAGE 5: Convert back to 16-bit with soft clipping + // ========================================================================= + 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); + } } -} // extern "C" +} // extern "C" \ No newline at end of file diff --git a/app/src/main/java/com/michatec/radio/EqualizerFragment.kt b/app/src/main/java/com/michatec/radio/EqualizerFragment.kt index 3b3808d..d8cdb1f 100644 --- a/app/src/main/java/com/michatec/radio/EqualizerFragment.kt +++ b/app/src/main/java/com/michatec/radio/EqualizerFragment.kt @@ -9,10 +9,25 @@ import androidx.preference.SeekBarPreference import com.michatec.radio.helpers.PreferencesHelper /* - * EqualizerFragment class: Handles audio frequency settings + * EqualizerFragment class: Handles audio frequency settings with 10-band EQ */ class EqualizerFragment : PreferenceFragmentCompat() { + // EQ band frequencies matching radio.cpp + private val eqFrequencies = arrayOf("31 Hz", "62 Hz", "125 Hz", "250 Hz", "500 Hz", "1 kHz", "2 kHz", "4 kHz", "8 kHz", "16 kHz") + private val eqKeys = arrayOf( + Keys.PREF_EQ_LOW, // Band 0: 31 Hz + Keys.PREF_EQ_BAND_1, // Band 1: 62 Hz + Keys.PREF_EQ_BAND_2, // Band 2: 125 Hz + Keys.PREF_EQ_BAND_3, // Band 3: 250 Hz + Keys.PREF_EQ_BAND_4, // Band 4: 500 Hz + Keys.PREF_EQ_BAND_5, // Band 5: 1 kHz + Keys.PREF_EQ_MID, // Band 6: 2 kHz + Keys.PREF_EQ_BAND_6, // Band 7: 4 kHz + Keys.PREF_EQ_BAND_7, // Band 8: 8 kHz + Keys.PREF_EQ_HIGH // Band 9: 16 kHz + ) + override fun onViewCreated(view: View, savedInstanceState: Bundle?) { super.onViewCreated(view, savedInstanceState) (activity as AppCompatActivity).supportActionBar?.title = getString(R.string.pref_equalizer_title) @@ -28,46 +43,25 @@ class EqualizerFragment : PreferenceFragmentCompat() { resetPreference.setIcon(R.drawable.ic_refresh_24dp) resetPreference.setOnPreferenceClickListener { PreferencesHelper.resetEqualizer() - // Manually update SeekBars to 0 - findPreference(Keys.PREF_EQ_LOW)?.value = 0 - findPreference(Keys.PREF_EQ_MID)?.value = 0 - findPreference(Keys.PREF_EQ_HIGH)?.value = 0 + for (key in eqKeys) { + findPreference(key)?.value = 0 + } return@setOnPreferenceClickListener true } screen.addPreference(resetPreference) - // EQ Low - val eqLow = SeekBarPreference(context) - eqLow.title = getString(R.string.pref_eq_low_title) - eqLow.key = Keys.PREF_EQ_LOW - eqLow.setIcon(R.drawable.ic_music_note_24dp) - eqLow.min = -12 - eqLow.max = 12 - eqLow.showSeekBarValue = true - eqLow.setDefaultValue(0) - screen.addPreference(eqLow) - - // EQ Mid - val eqMid = SeekBarPreference(context) - eqMid.title = getString(R.string.pref_eq_mid_title) - eqMid.key = Keys.PREF_EQ_MID - eqMid.setIcon(R.drawable.ic_music_note_24dp) - eqMid.min = -12 - eqMid.max = 12 - eqMid.showSeekBarValue = true - eqMid.setDefaultValue(0) - screen.addPreference(eqMid) - - // EQ High - val eqHigh = SeekBarPreference(context) - eqHigh.title = getString(R.string.pref_eq_high_title) - eqHigh.key = Keys.PREF_EQ_HIGH - eqHigh.setIcon(R.drawable.ic_music_note_24dp) - eqHigh.min = -12 - eqHigh.max = 12 - eqHigh.showSeekBarValue = true - eqHigh.setDefaultValue(0) - screen.addPreference(eqHigh) + // Create 10-band EQ + for (i in eqKeys.indices) { + val eqBand = SeekBarPreference(context) + eqBand.title = "Equalizer: ${eqFrequencies[i]}" + eqBand.key = eqKeys[i] + eqBand.setIcon(R.drawable.ic_music_note_24dp) + eqBand.min = -12 + eqBand.max = 12 + eqBand.showSeekBarValue = true + eqBand.setDefaultValue(0) + screen.addPreference(eqBand) + } preferenceScreen = screen } diff --git a/app/src/main/java/com/michatec/radio/Keys.kt b/app/src/main/java/com/michatec/radio/Keys.kt index f0edb8a..4fd9134 100644 --- a/app/src/main/java/com/michatec/radio/Keys.kt +++ b/app/src/main/java/com/michatec/radio/Keys.kt @@ -66,6 +66,30 @@ object Keys { const val PREF_EQ_LOW: String = "EQ_LOW" const val PREF_EQ_MID: String = "EQ_MID" const val PREF_EQ_HIGH: String = "EQ_HIGH" + const val PREF_EQUALIZER: String = "EQUALIZER_SETTINGS" + const val PREF_EQ_BAND_1: String = "EQ_BAND_1" + const val PREF_EQ_BAND_2: String = "EQ_BAND_2" + const val PREF_EQ_BAND_3: String = "EQ_BAND_3" + const val PREF_EQ_BAND_4: String = "EQ_BAND_4" + const val PREF_EQ_BAND_5: String = "EQ_BAND_5" + const val PREF_EQ_BAND_6: String = "EQ_BAND_6" + const val PREF_EQ_BAND_7: String = "EQ_BAND_7" + const val PREF_EQ_BAND_8: String = "EQ_BAND_8" + const val PREF_PRESET_SELECTED: String = "PRESET_SELECTED" + const val PREF_PRESET_EQ_BAND_0: String = "PRESET_EQ_BAND_0" + const val PREF_PRESET_EQ_BAND_1: String = "PRESET_EQ_BAND_1" + const val PREF_PRESET_EQ_BAND_2: String = "PRESET_EQ_BAND_2" + const val PREF_PRESET_EQ_BAND_3: String = "PRESET_EQ_BAND_3" + const val PREF_PRESET_EQ_BAND_4: String = "PRESET_EQ_BAND_4" + const val PREF_PRESET_EQ_BAND_5: String = "PRESET_EQ_BAND_5" + const val PREF_PRESET_EQ_BAND_6: String = "PRESET_EQ_BAND_6" + const val PREF_PRESET_EQ_BAND_7: String = "PRESET_EQ_BAND_7" + const val PREF_PRESET_EQ_BAND_8: String = "PRESET_EQ_BAND_8" + const val PREF_PRESET_EQ_BAND_9: String = "PRESET_EQ_BAND_9" + const val PREF_PRESET_BASS_BOOST: String = "PRESET_BASS_BOOST" + const val PREF_PRESET_REVERB: String = "PRESET_REVERB" + const val PREF_PRESET_DRC: String = "PRESET_DRC" + const val PREF_PRESET_STEREO_WIDTH: String = "PRESET_STEREO_WIDTH" // default const values const val DEFAULT_SIZE_OF_METADATA_HISTORY: Int = 25 diff --git a/app/src/main/java/com/michatec/radio/PlayerService.kt b/app/src/main/java/com/michatec/radio/PlayerService.kt index dadb0f8..fd5ae5e 100644 --- a/app/src/main/java/com/michatec/radio/PlayerService.kt +++ b/app/src/main/java/com/michatec/radio/PlayerService.kt @@ -286,20 +286,61 @@ class PlayerService : MediaLibraryService(), SharedPreferences.OnSharedPreferenc /* Applies audio effects based on preferences */ private fun applyAudioEffects() { - nativeAudioProcessor.enableBassBoost(PreferencesHelper.loadBassBoost()) - nativeAudioProcessor.setReverb(PreferencesHelper.loadReverb()) - nativeAudioProcessor.enableDrc(PreferencesHelper.loadDrcEnabled()) - nativeAudioProcessor.setEq(0, PreferencesHelper.loadEqLow()) - nativeAudioProcessor.setEq(1, PreferencesHelper.loadEqMid()) - nativeAudioProcessor.setEq(2, PreferencesHelper.loadEqHigh()) + val selectedPreset = PreferencesHelper.loadSelectedPreset() + if (selectedPreset.isNotEmpty()) { + applyPreset(selectedPreset) + } else { + // Apply manual settings + nativeAudioProcessor.enableBassBoost(PreferencesHelper.loadBassBoost()) + nativeAudioProcessor.setReverb(PreferencesHelper.loadReverb()) + nativeAudioProcessor.enableDrc(PreferencesHelper.loadDrcEnabled()) + nativeAudioProcessor.setWidth(1f) + // Apply all 10 EQ bands + val eqGains = FloatArray(10) + for (i in 0 until 10) { + eqGains[i] = PreferencesHelper.loadEqBand(i).toFloat() + } + nativeAudioProcessor.setEqAll(eqGains) + } } + /* Applies a saved preset */ + private fun applyPreset(presetName: String) { + when (presetName) { + getString(R.string.pref_preset_rock) -> nativeAudioProcessor.setPresetRock() + getString(R.string.pref_preset_pop) -> nativeAudioProcessor.setPresetPop() + getString(R.string.pref_preset_jazz) -> nativeAudioProcessor.setPresetJazz() + getString(R.string.pref_preset_flat) -> nativeAudioProcessor.setPresetFlat() + else -> { + // Custom preset - load from preferences + nativeAudioProcessor.enableDrc(PreferencesHelper.loadPresetDrc()) + nativeAudioProcessor.setReverb(PreferencesHelper.loadPresetReverb()) + nativeAudioProcessor.setWidth(PreferencesHelper.loadPresetStereoWidth()) + nativeAudioProcessor.enableBassBoost(PreferencesHelper.loadPresetBassBoost()) + val eqGains = FloatArray(10) + for (i in 0 until 10) { + eqGains[i] = PreferencesHelper.loadPresetEqBand(i).toFloat() + } + nativeAudioProcessor.setEqAll(eqGains) + } + } + } /* Overrides onSharedPreferenceChanged from SharedPreferences.OnSharedPreferenceChangeListener */ override fun onSharedPreferenceChanged(sharedPreferences: SharedPreferences?, key: String?) { when (key) { - Keys.PREF_BASS_BOOST, Keys.PREF_REVERB, Keys.PREF_DRC, - Keys.PREF_EQ_LOW, Keys.PREF_EQ_MID, Keys.PREF_EQ_HIGH -> { + Keys.PREF_BASS_BOOST, Keys.PREF_REVERB, Keys.PREF_DRC, + Keys.PREF_EQ_LOW, Keys.PREF_EQ_MID, Keys.PREF_EQ_HIGH, + Keys.PREF_EQ_BAND_1, Keys.PREF_EQ_BAND_2, Keys.PREF_EQ_BAND_3, + Keys.PREF_EQ_BAND_4, Keys.PREF_EQ_BAND_5, Keys.PREF_EQ_BAND_6, + Keys.PREF_EQ_BAND_7, Keys.PREF_EQ_BAND_8, + Keys.PREF_PRESET_SELECTED, + Keys.PREF_PRESET_EQ_BAND_0, Keys.PREF_PRESET_EQ_BAND_1, Keys.PREF_PRESET_EQ_BAND_2, + Keys.PREF_PRESET_EQ_BAND_3, Keys.PREF_PRESET_EQ_BAND_4, Keys.PREF_PRESET_EQ_BAND_5, + Keys.PREF_PRESET_EQ_BAND_6, Keys.PREF_PRESET_EQ_BAND_7, Keys.PREF_PRESET_EQ_BAND_8, + Keys.PREF_PRESET_EQ_BAND_9, + Keys.PREF_PRESET_BASS_BOOST, Keys.PREF_PRESET_REVERB, + Keys.PREF_PRESET_DRC, Keys.PREF_PRESET_STEREO_WIDTH -> { applyAudioEffects() } } diff --git a/app/src/main/java/com/michatec/radio/SettingsFragment.kt b/app/src/main/java/com/michatec/radio/SettingsFragment.kt index dd9e914..454248e 100644 --- a/app/src/main/java/com/michatec/radio/SettingsFragment.kt +++ b/app/src/main/java/com/michatec/radio/SettingsFragment.kt @@ -17,6 +17,7 @@ import androidx.navigation.fragment.findNavController import androidx.preference.* import com.google.android.material.snackbar.Snackbar import com.michatec.radio.dialogs.ErrorDialog +import com.michatec.radio.dialogs.PresetSelectionDialog import com.michatec.radio.dialogs.ThemeSelectionDialog import com.michatec.radio.dialogs.YesNoDialog import com.michatec.radio.helpers.* @@ -30,7 +31,7 @@ import java.util.* /* * SettingsFragment class */ -class SettingsFragment : PreferenceFragmentCompat(), YesNoDialog.YesNoDialogListener, ThemeSelectionDialog.ThemeSelectionDialogListener { +class SettingsFragment : PreferenceFragmentCompat(), YesNoDialog.YesNoDialogListener, ThemeSelectionDialog.ThemeSelectionDialogListener, PresetSelectionDialog.PresetSelectionDialogListener { /* Define log tag */ @@ -52,6 +53,9 @@ class SettingsFragment : PreferenceFragmentCompat(), YesNoDialog.YesNoDialogList val context = preferenceManager.context val screen = preferenceManager.createPreferenceScreen(context) + // Load current preset once + val currentPreset = PreferencesHelper.loadSelectedPreset() + // set up "App Theme" preference val preferenceThemeSelection = Preference(activity as Context) preferenceThemeSelection.title = getString(R.string.pref_theme_selection_title) @@ -190,6 +194,7 @@ class SettingsFragment : PreferenceFragmentCompat(), YesNoDialog.YesNoDialogList preferenceBassBoost.title = getString(R.string.pref_bass_boost_title) preferenceBassBoost.setIcon(R.drawable.ic_music_note_24dp) preferenceBassBoost.key = Keys.PREF_BASS_BOOST + preferenceBassBoost.isEnabled = currentPreset.isEmpty() preferenceBassBoost.summary = getString(R.string.pref_bass_boost_summary) preferenceBassBoost.setDefaultValue(false) @@ -198,6 +203,7 @@ class SettingsFragment : PreferenceFragmentCompat(), YesNoDialog.YesNoDialogList preferenceReverb.title = getString(R.string.pref_reverb_title) preferenceReverb.setIcon(R.drawable.ic_music_note_24dp) preferenceReverb.key = Keys.PREF_REVERB + preferenceReverb.isEnabled = currentPreset.isEmpty() preferenceReverb.summary = getString(R.string.pref_reverb_summary) preferenceReverb.setDefaultValue(false) @@ -206,14 +212,39 @@ class SettingsFragment : PreferenceFragmentCompat(), YesNoDialog.YesNoDialogList preferenceDrc.title = getString(R.string.pref_drc_title) preferenceDrc.setIcon(R.drawable.ic_music_note_24dp) preferenceDrc.key = Keys.PREF_DRC + preferenceDrc.isEnabled = currentPreset.isEmpty() preferenceDrc.summary = getString(R.string.pref_drc_summary) preferenceDrc.setDefaultValue(true) + // set up "Preset Selection" preference + val preferencePresetSelection = Preference(context) + preferencePresetSelection.title = getString(R.string.pref_preset_selection_title) + preferencePresetSelection.setIcon(R.drawable.ic_music_note_24dp) + preferencePresetSelection.key = Keys.PREF_PRESET_SELECTED + val presetSummary = currentPreset.ifEmpty { + getString(R.string.pref_preset_none) + } + preferencePresetSelection.summary = "${getString(R.string.pref_preset_selection_summary)}: $presetSummary" + preferencePresetSelection.setOnPreferenceClickListener { + PresetSelectionDialog(this).show(activity as Context) + return@setOnPreferenceClickListener true + } + + // Initialize EQ control states based on current preset + updateEqControlStates() + // set up "Equalizer" preference entry val preferenceEqualizer = Preference(context) preferenceEqualizer.title = getString(R.string.pref_equalizer_title) preferenceEqualizer.setIcon(R.drawable.ic_music_note_24dp) - preferenceEqualizer.summary = getString(R.string.pref_equalizer_summary) + preferenceEqualizer.key = Keys.PREF_EQUALIZER + if (currentPreset.isEmpty()) { + preferenceEqualizer.summary = getString(R.string.pref_equalizer_summary) + preferenceEqualizer.isEnabled = true + } else { + preferenceEqualizer.summary = getString(R.string.pref_equalizer_summary_off) + preferenceEqualizer.isEnabled = false + } preferenceEqualizer.setOnPreferenceClickListener { findNavController().navigate(R.id.action_settings_to_equalizer) return@setOnPreferenceClickListener true @@ -295,6 +326,7 @@ class SettingsFragment : PreferenceFragmentCompat(), YesNoDialog.YesNoDialogList preferenceCategoryAudioEffects.addPreference(preferenceBassBoost) preferenceCategoryAudioEffects.addPreference(preferenceReverb) preferenceCategoryAudioEffects.addPreference(preferenceDrc) + preferenceCategoryAudioEffects.addPreference(preferencePresetSelection) preferenceCategoryAudioEffects.addPreference(preferenceEqualizer) screen.addPreference(preferenceCategoryMaintenance) @@ -340,6 +372,49 @@ class SettingsFragment : PreferenceFragmentCompat(), YesNoDialog.YesNoDialogList } + /* Overrides onPresetSelectionDialog from PresetSelectionDialogListener */ + override fun onPresetSelectionDialog(dialogResult: Boolean, selectedPreset: String) { + if (dialogResult) { + // update summary + val presetPreference = findPreference(Keys.PREF_PRESET_SELECTED) + val presetSummary = selectedPreset.ifEmpty { + getString(R.string.pref_preset_none) + } + presetPreference?.summary = "${getString(R.string.pref_preset_selection_summary)}: $presetSummary" + + // Enable/disable manual EQ controls based on preset selection + updateEqControlStates() + } + } + + /* Updates the enabled/disabled state of EQ controls based on preset selection */ + private fun updateEqControlStates() { + val currentPreset = PreferencesHelper.loadSelectedPreset() + val isPresetSelected = currentPreset.isNotEmpty() + + // Update Bass Boost + findPreference(Keys.PREF_BASS_BOOST)?.isEnabled = !isPresetSelected + + // Update Reverb + findPreference(Keys.PREF_REVERB)?.isEnabled = !isPresetSelected + + // Update DRC + findPreference(Keys.PREF_DRC)?.isEnabled = !isPresetSelected + + // Update Equalizer with proper key + val preferenceEqualizer = findPreference(Keys.PREF_EQUALIZER) + if (preferenceEqualizer != null) { + if (isPresetSelected) { + preferenceEqualizer.summary = getString(R.string.pref_equalizer_summary_off) + preferenceEqualizer.isEnabled = false + } else { + preferenceEqualizer.summary = getString(R.string.pref_equalizer_summary) + preferenceEqualizer.isEnabled = true + } + } + } + + /* Overrides onYesNoDialog from YesNoDialogListener */ override fun onYesNoDialog( type: Int, diff --git a/app/src/main/java/com/michatec/radio/dialogs/PresetSelectionDialog.kt b/app/src/main/java/com/michatec/radio/dialogs/PresetSelectionDialog.kt new file mode 100644 index 0000000..b9265c5 --- /dev/null +++ b/app/src/main/java/com/michatec/radio/dialogs/PresetSelectionDialog.kt @@ -0,0 +1,86 @@ +package com.michatec.radio.dialogs + +import android.content.Context +import android.view.LayoutInflater +import android.widget.RadioButton +import androidx.appcompat.app.AlertDialog +import androidx.core.content.ContextCompat.getString +import com.google.android.material.dialog.MaterialAlertDialogBuilder +import com.michatec.radio.R +import com.michatec.radio.helpers.PreferencesHelper + + +/* + * PresetSelectionDialog class + */ +class PresetSelectionDialog(private var presetSelectionDialogListener: PresetSelectionDialogListener) { + + /* Interface used to communicate back to activity */ + interface PresetSelectionDialogListener { + fun onPresetSelectionDialog(dialogResult: Boolean, selectedPreset: String) + } + + + /* Main class variables */ + private lateinit var dialog: AlertDialog + + + /* Construct and show dialog */ + fun show(context: Context) { + // prepare dialog builder + val builder = MaterialAlertDialogBuilder(context) + + // inflate custom layout + val inflater = LayoutInflater.from(context) + val view = inflater.inflate(R.layout.dialog_preset_selection, null) + + // find radio buttons + val radioGroup = view.findViewById(R.id.preset_radio_group) + val radioNone = view.findViewById(R.id.radio_preset_none) + val radioRock = view.findViewById(R.id.radio_preset_rock) + val radioPop = view.findViewById(R.id.radio_preset_pop) + val radioJazz = view.findViewById(R.id.radio_preset_jazz) + val radioFlat = view.findViewById(R.id.radio_preset_flat) + + // set current selection + val currentPreset = PreferencesHelper.loadSelectedPreset() + when (currentPreset) { + "" -> radioNone.isChecked = true + getString(context, R.string.pref_preset_rock) -> radioRock.isChecked = true + getString(context, R.string.pref_preset_pop) -> radioPop.isChecked = true + getString(context, R.string.pref_preset_jazz) -> radioJazz.isChecked = true + getString(context, R.string.pref_preset_flat) -> radioFlat.isChecked = true + else -> radioNone.isChecked = true + } + + // set up radio group listener + radioGroup.setOnCheckedChangeListener { _, checkedId -> + val selectedPreset = when (checkedId) { + R.id.radio_preset_none -> "" + R.id.radio_preset_rock -> getString(context, R.string.pref_preset_rock) + R.id.radio_preset_pop -> getString(context, R.string.pref_preset_pop) + R.id.radio_preset_jazz -> getString(context, R.string.pref_preset_jazz) + R.id.radio_preset_flat -> getString(context, R.string.pref_preset_flat) + else -> "" + } + // save preset selection to preferences + PreferencesHelper.saveSelectedPreset(selectedPreset) + // notify listener + presetSelectionDialogListener.onPresetSelectionDialog(true, selectedPreset) + // dismiss dialog + dialog.dismiss() + } + + // set custom view + builder.setView(view) + + // handle outside-click as cancel + builder.setOnCancelListener { + presetSelectionDialogListener.onPresetSelectionDialog(false, "") + } + + // display dialog + dialog = builder.create() + dialog.show() + } +} \ No newline at end of file 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 d47ba14..121789b 100644 --- a/app/src/main/java/com/michatec/radio/helpers/NativeAudioProcessor.kt +++ b/app/src/main/java/com/michatec/radio/helpers/NativeAudioProcessor.kt @@ -18,41 +18,90 @@ class NativeAudioProcessor : BaseAudioProcessor() { } } - // JNI Methods + // ===== JNI ===== private external fun setDrcEnabled(enabled: Boolean) private external fun setReverbMix(mix: Float) private external fun setEqBand(band: Int, gainDb: Float) private external fun setBassBoost(gainDb: Float) - private external fun processAudio(data: ShortArray, size: Int) + private external fun setStereoWidth(width: Float) + private external fun processAudioDirect(buf: ByteBuffer, size: Int) + private external fun getFftData(): FloatArray - // Public API + // ===== 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) { + gains.forEachIndexed { i, g -> setEq(i, g) } + } fun enableBassBoost(gainDb: Float) = setBassBoost(gainDb) + fun setWidth(width: Float) = setStereoWidth(width) + @Suppress("unused") + fun getVisualizer(): FloatArray { + val raw = getFftData() + val out = FloatArray(raw.size) + for (i in raw.indices) out[i] = kotlin.math.log10(1f + raw[i]) + return out + } + + // ===== AudioProcessor Overrides ===== override fun onConfigure(inputAudioFormat: AudioFormat): AudioFormat { - if (inputAudioFormat.encoding != C.ENCODING_PCM_16BIT) { + if (inputAudioFormat.encoding != C.ENCODING_PCM_16BIT) throw AudioProcessor.UnhandledAudioFormatException(inputAudioFormat) - } return inputAudioFormat } override fun queueInput(inputBuffer: ByteBuffer) { - val remaining = inputBuffer.remaining() - if (remaining == 0) return + val size = inputBuffer.remaining() + if (size == 0) return - val shortArraySize = remaining / 2 - val shortArray = ShortArray(shortArraySize) + // Direct ByteBuffer -> JNI + inputBuffer.order(ByteOrder.nativeOrder()) + processAudioDirect(inputBuffer, size) - inputBuffer.order(ByteOrder.nativeOrder()).asShortBuffer().get(shortArray) + // Replace output buffer + val out = replaceOutputBuffer(size) + out.order(ByteOrder.nativeOrder()) - processAudio(shortArray, shortArraySize) + // Mark as processed and copy to output + val currentPos = inputBuffer.position() + out.put(inputBuffer) + inputBuffer.position(currentPos + size) - val outputBuffer = replaceOutputBuffer(remaining) - outputBuffer.asShortBuffer().put(shortArray) - outputBuffer.limit(remaining) + out.flip() + } - inputBuffer.position(inputBuffer.limit()) + // ===== Presets ===== + fun setPresetRock() { + enableDrc(true) + setReverb(0.2f) + setWidth(1.1f) + setEqAll(floatArrayOf(2f, 1f, 0f, -1f, -1f, 0f, 1f, 2f, 2f, 3f)) + enableBassBoost(1.5f) + } + + fun setPresetPop() { + enableDrc(true) + setReverb(0.15f) + setWidth(1.05f) + setEqAll(floatArrayOf(1f, 1f, 0f, 0f, 0f, 0f, 1f, 2f, 2f, 1f)) + enableBassBoost(1.0f) + } + + fun setPresetJazz() { + enableDrc(false) + setReverb(0.15f) + setWidth(1.0f) + setEqAll(floatArrayOf(0f, 0f, 1f, 1f, 0f, 0f, 1f, 1f, 0f, 0f)) + enableBassBoost(0.5f) + } + + fun setPresetFlat() { + enableDrc(false) + setReverb(0f) + setWidth(1f) + setEqAll(FloatArray(10)) + enableBassBoost(0f) } } \ No newline at end of file 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 435d46b..4388611 100644 --- a/app/src/main/java/com/michatec/radio/helpers/PreferencesHelper.kt +++ b/app/src/main/java/com/michatec/radio/helpers/PreferencesHelper.kt @@ -257,7 +257,7 @@ object PreferencesHelper { /* Loads Bass Boost gain */ fun loadBassBoost(): Float { - return if (sharedPreferences.getBoolean(Keys.PREF_BASS_BOOST, false)) 5.0f else 0.0f + return if (sharedPreferences.getBoolean(Keys.PREF_BASS_BOOST, false)) 1.5f else 0.0f } @@ -272,10 +272,69 @@ object PreferencesHelper { return sharedPreferences.getBoolean(Keys.PREF_DRC, false) } - /* Loads EQ gains */ - fun loadEqLow(): Float = sharedPreferences.getInt(Keys.PREF_EQ_LOW, 0).toFloat() - fun loadEqMid(): Float = sharedPreferences.getInt(Keys.PREF_EQ_MID, 0).toFloat() - fun loadEqHigh(): Float = sharedPreferences.getInt(Keys.PREF_EQ_HIGH, 0).toFloat() + /* Loads all EQ bands (10 bands for full range) */ + fun loadEqBand(band: Int): Int { + return when (band) { + 0 -> sharedPreferences.getInt(Keys.PREF_EQ_LOW, 0) + 1 -> sharedPreferences.getInt(Keys.PREF_EQ_BAND_1, 0) + 2 -> sharedPreferences.getInt(Keys.PREF_EQ_MID, 0) + 3 -> sharedPreferences.getInt(Keys.PREF_EQ_BAND_3, 0) + 4 -> sharedPreferences.getInt(Keys.PREF_EQ_BAND_4, 0) + 5 -> sharedPreferences.getInt(Keys.PREF_EQ_BAND_5, 0) + 6 -> sharedPreferences.getInt(Keys.PREF_EQ_BAND_6, 0) + 7 -> sharedPreferences.getInt(Keys.PREF_EQ_BAND_7, 0) + 8 -> sharedPreferences.getInt(Keys.PREF_EQ_BAND_8, 0) + 9 -> sharedPreferences.getInt(Keys.PREF_EQ_HIGH, 0) + else -> 0 + } + } + + /* Loads selected preset name */ + fun loadSelectedPreset(): String { + return sharedPreferences.getString(Keys.PREF_PRESET_SELECTED, "") ?: "" + } + + /* Saves selected preset name */ + fun saveSelectedPreset(preset: String) { + sharedPreferences.edit { putString(Keys.PREF_PRESET_SELECTED, preset) } + } + + /* Loads preset EQ band values */ + fun loadPresetEqBand(band: Int): Int { + return when (band) { + 0 -> sharedPreferences.getInt(Keys.PREF_PRESET_EQ_BAND_0, 0) + 1 -> sharedPreferences.getInt(Keys.PREF_PRESET_EQ_BAND_1, 0) + 2 -> sharedPreferences.getInt(Keys.PREF_PRESET_EQ_BAND_2, 0) + 3 -> sharedPreferences.getInt(Keys.PREF_PRESET_EQ_BAND_3, 0) + 4 -> sharedPreferences.getInt(Keys.PREF_PRESET_EQ_BAND_4, 0) + 5 -> sharedPreferences.getInt(Keys.PREF_PRESET_EQ_BAND_5, 0) + 6 -> sharedPreferences.getInt(Keys.PREF_PRESET_EQ_BAND_6, 0) + 7 -> sharedPreferences.getInt(Keys.PREF_PRESET_EQ_BAND_7, 0) + 8 -> sharedPreferences.getInt(Keys.PREF_PRESET_EQ_BAND_8, 0) + 9 -> sharedPreferences.getInt(Keys.PREF_PRESET_EQ_BAND_9, 0) + else -> 0 + } + } + + /* Loads preset Bass Boost */ + fun loadPresetBassBoost(): Float { + return sharedPreferences.getFloat(Keys.PREF_PRESET_BASS_BOOST, 0f) + } + + /* Loads preset Reverb */ + fun loadPresetReverb(): Float { + return sharedPreferences.getFloat(Keys.PREF_PRESET_REVERB, 0f) + } + + /* Loads preset DRC */ + fun loadPresetDrc(): Boolean { + return sharedPreferences.getBoolean(Keys.PREF_PRESET_DRC, false) + } + + /* Loads preset Stereo Width */ + fun loadPresetStereoWidth(): Float { + return sharedPreferences.getFloat(Keys.PREF_PRESET_STEREO_WIDTH, 1f) + } /* Resets Equalizer settings to default */ fun resetEqualizer() { diff --git a/app/src/main/res/layout/dialog_preset_selection.xml b/app/src/main/res/layout/dialog_preset_selection.xml new file mode 100644 index 0000000..a9c9d34 --- /dev/null +++ b/app/src/main/res/layout/dialog_preset_selection.xml @@ -0,0 +1,65 @@ + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/values-de/strings.xml b/app/src/main/res/values-de/strings.xml index f537ac0..599ec3d 100644 --- a/app/src/main/res/values-de/strings.xml +++ b/app/src/main/res/values-de/strings.xml @@ -55,7 +55,6 @@ Die neueste Version aller Senderinformationen herunterladen. Die neueste Version aller Senderinformationen herunterladen? Aktualisieren - Equalizer: Höhen Erweitert Version App-Version @@ -125,9 +124,25 @@ Reverb-Mix anpassen. Dynamikkompression Den Dynamikbereich für eine gleichbleibende Lautstärke komprimieren. - Equalizer: Bass - Equalizer: Mitten + Equalizer: 31 Hz + Equalizer: 2 kHz + Equalizer: 16 kHz + Equalizer: 62 Hz + Equalizer: 125 Hz + Equalizer: 250 Hz + Equalizer: 500 Hz + Equalizer: 1 kHz + Equalizer: 4 kHz + Equalizer: 8 kHz Equalizer Passen Sie die Audio-Frequenzen an. + Das Anpassen von den Audio-Frequenzen ist deaktiviert. Equalizer zurücksetzen + Preset auswählen + Wählen Sie ein Klangprofil + Keines (Manuell) + Rock + Pop + Jazz + Flach diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index b87e586..1000a6b 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -70,12 +70,27 @@ Adjust reverb mix. Dynamic Range Compression Compress dynamic range for consistent volume. - Equalizer: Low - Equalizer: Mid - Equalizer: High + Equalizer: 31 Hz + Equalizer: 125 Hz + Equalizer: 4 kHz + Equalizer: 62 Hz + Equalizer: 250 Hz + Equalizer: 500 Hz + Equalizer: 1 kHz + Equalizer: 2 kHz + Equalizer: 8 kHz + Equalizer: 16 kHz Equalizer Adjust audio frequencies + Adjust audio frequencies is off. Reset Equalizer + Select Preset + Choose an audio preset + None (Manual) + Rock + Pop + Jazz + Flat Advanced Version App Version