feat(audio): add 10-band equalizer and audio presets

This commit is contained in:
2026-04-06 13:27:53 +02:00
parent 0d0980a1ef
commit 0d35770375
11 changed files with 949 additions and 214 deletions
+453 -141
View File
@@ -1,208 +1,520 @@
#include <jni.h>
#include <string>
#include <vector>
#include <cmath>
#include <algorithm>
#include <android/log.h>
#include <complex>
#include <array>
// --- DSP Classes ---
#if defined(__ARM_NEON)
#include <arm_neon.h>
#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<float>(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<float, NUM_EQ_BANDS> 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<float, NUM_EQ_BANDS> a0{}, a1{}, a2{}, b1{}, b2{};
// State variables
alignas(16) std::array<float, NUM_EQ_BANDS> 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<float>(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<float>(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;i<count;i++) data[i]=process(data[i]); return; }
// Scalar feedback for stability
for(int i=0;i<count;i++){
float x = data[i];
float y = a0*x + z1;
z1 = a1*x + z2 - b1*y + DENORMAL_OFFSET;
z2 = a2*x - b2*y;
if(y>1.2f) y=1.2f;
else if(y<-1.2f) y=-1.2f;
data[i] = y;
}
#else
for(int i=0;i<count;i++) data[i]=process(data[i]);
#endif
}
void setLowShelf(float sr,float f,float g,float q){
active=std::abs(g)>0.01f;
if(!active) return;
float A=powf(10.0f,g/40.0f);
float w=2.0f*static_cast<float>(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<int SIZE>
struct CircularBuffer {
alignas(16) std::array<float, SIZE> 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<CircularBuffer<1116>, 4> combs;
std::array<CircularBuffer<556>, 2> allpasses;
std::array<float, 4> 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<float> 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<Biquad> 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<float, 4096> gLeftBuf;
alignas(16) std::array<float, 4096> gRightBuf;
alignas(16) std::array<float, 256> gFFTData;
alignas(16) std::array<std::complex<float>, FFT_SIZE> gFFTWork;
// Fast FFT - iterative Cooley-Tukey
inline void fastFFT(std::complex<float>* __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<float>(M_PI) / static_cast<float>(len);
// Pre-compute wlen - critical for performance
std::complex<float> wlen(cosf(ang), sinf(ang));
for (int i = 0; i < n; i += len) {
std::complex<float> w(1.0f);
for (int j = 0; j < len / 2; j++) {
std::complex<float> u = data[i + j];
std::complex<float> 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<float>(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<size_t>(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<jshort*>(env->GetDirectBufferAddress(byteBuffer));
if (!buffer) return;
std::vector<float> floatBuf(static_cast<size_t>(size));
for (int i = 0; i < size; ++i) floatBuf[static_cast<size_t>(i)] = static_cast<float>(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<float>(buffer[i * 2]) * INV_32768;
gRightBuf[i] = static_cast<float>(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<size_t>(i)] = band.process(floatBuf[static_cast<size_t>(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<size_t>(i)] = gBassBoost.process(floatBuf[static_cast<size_t>(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<size_t>(i)] = gReverb.process(floatBuf[static_cast<size_t>(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<size_t>(i)]));
buffer[i] = static_cast<jshort>(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<float>(gLeftBuf[k], 0.0f) : std::complex<float>(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<jshort>(fastSoftClip(gLeftBuf[k]) * 32767.0f);
buffer[k * 2 + 1] = static_cast<jshort>(fastSoftClip(gRightBuf[k]) * 32767.0f);
}
}
} // extern "C"
} // extern "C"
@@ -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<SeekBarPreference>(Keys.PREF_EQ_LOW)?.value = 0
findPreference<SeekBarPreference>(Keys.PREF_EQ_MID)?.value = 0
findPreference<SeekBarPreference>(Keys.PREF_EQ_HIGH)?.value = 0
for (key in eqKeys) {
findPreference<SeekBarPreference>(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
}
@@ -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
@@ -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()
}
}
@@ -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<Preference>(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<Preference>(Keys.PREF_BASS_BOOST)?.isEnabled = !isPresetSelected
// Update Reverb
findPreference<Preference>(Keys.PREF_REVERB)?.isEnabled = !isPresetSelected
// Update DRC
findPreference<Preference>(Keys.PREF_DRC)?.isEnabled = !isPresetSelected
// Update Equalizer with proper key
val preferenceEqualizer = findPreference<Preference>(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,
@@ -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<android.widget.RadioGroup>(R.id.preset_radio_group)
val radioNone = view.findViewById<RadioButton>(R.id.radio_preset_none)
val radioRock = view.findViewById<RadioButton>(R.id.radio_preset_rock)
val radioPop = view.findViewById<RadioButton>(R.id.radio_preset_pop)
val radioJazz = view.findViewById<RadioButton>(R.id.radio_preset_jazz)
val radioFlat = view.findViewById<RadioButton>(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()
}
}
@@ -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)
}
}
@@ -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() {
@@ -0,0 +1,65 @@
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="vertical"
android:padding="16dp">
<TextView
android:id="@+id/dialog_title"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:text="@string/pref_preset_selection_title"
android:textSize="18sp"
android:textStyle="bold"
android:paddingBottom="12dp" />
<RadioGroup
android:id="@+id/preset_radio_group"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="vertical">
<RadioButton
android:id="@+id/radio_preset_none"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:text="@string/pref_preset_none"
android:textSize="16sp"
android:padding="8dp"/>
<RadioButton
android:id="@+id/radio_preset_rock"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:text="@string/pref_preset_rock"
android:textSize="16sp"
android:padding="8dp"/>
<RadioButton
android:id="@+id/radio_preset_pop"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:text="@string/pref_preset_pop"
android:textSize="16sp"
android:padding="8dp"/>
<RadioButton
android:id="@+id/radio_preset_jazz"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:text="@string/pref_preset_jazz"
android:textSize="16sp"
android:padding="8dp"/>
<RadioButton
android:id="@+id/radio_preset_flat"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:text="@string/pref_preset_flat"
android:textSize="16sp"
android:padding="8dp"/>
</RadioGroup>
</LinearLayout>
+18 -3
View File
@@ -55,7 +55,6 @@
<string name="pref_update_collection_summary">Die neueste Version aller Senderinformationen herunterladen.</string>
<string name="dialog_yes_no_message_update_collection">Die neueste Version aller Senderinformationen herunterladen?</string>
<string name="dialog_yes_no_positive_button_update_collection">Aktualisieren</string>
<string name="pref_eq_high_title">Equalizer: Höhen</string>
<string name="pref_advanced_title">Erweitert</string>
<string name="pref_app_version_summary">Version</string>
<string name="pref_app_version_title">App-Version</string>
@@ -125,9 +124,25 @@
<string name="pref_reverb_summary">Reverb-Mix anpassen.</string>
<string name="pref_drc_title">Dynamikkompression</string>
<string name="pref_drc_summary">Den Dynamikbereich für eine gleichbleibende Lautstärke komprimieren.</string>
<string name="pref_eq_low_title">Equalizer: Bass</string>
<string name="pref_eq_mid_title">Equalizer: Mitten</string>
<string name="pref_eq_low_title">Equalizer: 31 Hz</string>
<string name="pref_eq_mid_title">Equalizer: 2 kHz</string>
<string name="pref_eq_high_title">Equalizer: 16 kHz</string>
<string name="pref_eq_band_1_title">Equalizer: 62 Hz</string>
<string name="pref_eq_band_2_title">Equalizer: 125 Hz</string>
<string name="pref_eq_band_3_title">Equalizer: 250 Hz</string>
<string name="pref_eq_band_4_title">Equalizer: 500 Hz</string>
<string name="pref_eq_band_5_title">Equalizer: 1 kHz</string>
<string name="pref_eq_band_6_title">Equalizer: 4 kHz</string>
<string name="pref_eq_band_7_title">Equalizer: 8 kHz</string>
<string name="pref_equalizer_title">Equalizer</string>
<string name="pref_equalizer_summary">Passen Sie die Audio-Frequenzen an.</string>
<string name="pref_equalizer_summary_off">Das Anpassen von den Audio-Frequenzen ist deaktiviert.</string>
<string name="pref_equalizer_reset_title">Equalizer zurücksetzen</string>
<string name="pref_preset_selection_title">Preset auswählen</string>
<string name="pref_preset_selection_summary">Wählen Sie ein Klangprofil</string>
<string name="pref_preset_none">Keines (Manuell)</string>
<string name="pref_preset_rock">Rock</string>
<string name="pref_preset_pop">Pop</string>
<string name="pref_preset_jazz">Jazz</string>
<string name="pref_preset_flat">Flach</string>
</resources>
+18 -3
View File
@@ -70,12 +70,27 @@
<string name="pref_reverb_summary">Adjust reverb mix.</string>
<string name="pref_drc_title">Dynamic Range Compression</string>
<string name="pref_drc_summary">Compress dynamic range for consistent volume.</string>
<string name="pref_eq_low_title">Equalizer: Low</string>
<string name="pref_eq_mid_title">Equalizer: Mid</string>
<string name="pref_eq_high_title">Equalizer: High</string>
<string name="pref_eq_low_title">Equalizer: 31 Hz</string>
<string name="pref_eq_mid_title">Equalizer: 125 Hz</string>
<string name="pref_eq_high_title">Equalizer: 4 kHz</string>
<string name="pref_eq_band_1_title">Equalizer: 62 Hz</string>
<string name="pref_eq_band_2_title">Equalizer: 250 Hz</string>
<string name="pref_eq_band_3_title">Equalizer: 500 Hz</string>
<string name="pref_eq_band_4_title">Equalizer: 1 kHz</string>
<string name="pref_eq_band_5_title">Equalizer: 2 kHz</string>
<string name="pref_eq_band_6_title">Equalizer: 8 kHz</string>
<string name="pref_eq_band_7_title">Equalizer: 16 kHz</string>
<string name="pref_equalizer_title">Equalizer</string>
<string name="pref_equalizer_summary">Adjust audio frequencies</string>
<string name="pref_equalizer_summary_off">Adjust audio frequencies is off.</string>
<string name="pref_equalizer_reset_title">Reset Equalizer</string>
<string name="pref_preset_selection_title">Select Preset</string>
<string name="pref_preset_selection_summary">Choose an audio preset</string>
<string name="pref_preset_none">None (Manual)</string>
<string name="pref_preset_rock">Rock</string>
<string name="pref_preset_pop">Pop</string>
<string name="pref_preset_jazz">Jazz</string>
<string name="pref_preset_flat">Flat</string>
<string name="pref_advanced_title">Advanced</string>
<string name="pref_app_version_summary">Version</string>
<string name="pref_app_version_title">App Version</string>