mirror of
https://github.com/Michatec/Radio.git
synced 2026-05-31 00:52:40 +02:00
Compare commits
7 Commits
0d0980a1ef
...
9d47684f13
| Author | SHA1 | Date | |
|---|---|---|---|
| 9d47684f13 | |||
| 82993d7c97 | |||
| 487195b716 | |||
| 12445a3918 | |||
| bc38742eae | |||
| 99499ad174 | |||
| 0d35770375 |
@@ -18,21 +18,25 @@ project("radio")
|
||||
# Gradle automatically packages shared libraries with your APK.
|
||||
#
|
||||
# In this top level CMakeLists.txt, ${CMAKE_PROJECT_NAME} is used to define
|
||||
# the target library name; in the sub-module's CMakeLists.txt, ${PROJECT_NAME}
|
||||
# the target library name; in the sub-module's CMakeLists.txt, the project name
|
||||
# is preferred for the same purpose.
|
||||
#
|
||||
# In order to load a library into your app from Java/Kotlin, you must call
|
||||
# System.loadLibrary() and pass the name of the library defined here;
|
||||
# for GameActivity/NativeActivity derived applications, the same library name must be
|
||||
# used in the AndroidManifest.xml file.
|
||||
add_library(${CMAKE_PROJECT_NAME} SHARED
|
||||
# List C/C++ source files with relative paths to this CMakeLists.txt.
|
||||
radio.cpp)
|
||||
add_library(dsp SHARED
|
||||
dsp.cpp)
|
||||
|
||||
add_library(extra SHARED
|
||||
extra.cpp)
|
||||
|
||||
# Specifies libraries CMake should link to your target library. You
|
||||
# can link libraries from various origins, such as libraries defined in this
|
||||
# build script, prebuilt third-party libraries, or Android system libraries.
|
||||
target_link_libraries(${CMAKE_PROJECT_NAME}
|
||||
# List libraries link to the target library
|
||||
target_link_libraries(dsp
|
||||
android
|
||||
log)
|
||||
target_link_libraries(extra
|
||||
android
|
||||
log)
|
||||
@@ -0,0 +1,287 @@
|
||||
#include <jni.h>
|
||||
#include <vector>
|
||||
#include <cmath>
|
||||
#include <complex>
|
||||
#include <array>
|
||||
|
||||
#if defined(__ARM_NEON)
|
||||
#include <arm_neon.h>
|
||||
#endif
|
||||
|
||||
#ifndef M_PI
|
||||
#define M_PI 3.14159265358979323846
|
||||
#endif
|
||||
|
||||
static constexpr int FFT_SIZE = 512;
|
||||
static constexpr int NUM_EQ_BANDS = 10;
|
||||
static constexpr float INV_32768 = 1.0f / 32768.0f;
|
||||
static constexpr float SQRT_2_INV = 0.70710678f;
|
||||
static constexpr float DENORMAL_OFFSET = 1e-18f;
|
||||
|
||||
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
|
||||
};
|
||||
|
||||
struct alignas(16) BiquadBank {
|
||||
alignas(16) std::array<float, NUM_EQ_BANDS> a0{}, a1{}, a2{}, b1{}, b2{};
|
||||
alignas(16) std::array<float, NUM_EQ_BANDS> z1{}, z2{};
|
||||
uint16_t activeMask = 0;
|
||||
|
||||
[[nodiscard]] inline bool hasActiveBands() const { return activeMask != 0; }
|
||||
|
||||
inline void setBandActive(int band, bool active) {
|
||||
if (active) activeMask |= (1 << band);
|
||||
else activeMask &= ~(1 << band);
|
||||
}
|
||||
|
||||
inline void processBlock(float* __restrict__ data, int count) {
|
||||
if (!this -> hasActiveBands()) return;
|
||||
for (int i = 0; i < count; i++) {
|
||||
float x = data[i];
|
||||
#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;
|
||||
}
|
||||
}
|
||||
|
||||
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;
|
||||
}
|
||||
};
|
||||
|
||||
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;
|
||||
}
|
||||
|
||||
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;
|
||||
}
|
||||
};
|
||||
|
||||
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 {
|
||||
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:
|
||||
inline void setMix(float m) { mix = m; }
|
||||
inline float process(float x) {
|
||||
if (mix < 0.01f) return x;
|
||||
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;
|
||||
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;
|
||||
}
|
||||
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]);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
class CompressorOptimized {
|
||||
public:
|
||||
float threshold = 0.3f, ratio = 4.0f, attack = 0.08f, release = 0.8f, sampleRate = 44100.0f;
|
||||
private:
|
||||
float envelopeL = 0.0f, envelopeR = 0.0f;
|
||||
float attackCoef = 0.0f, 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]);
|
||||
envelope = (absInput > envelope) ? attackCoef*envelope + (1-attackCoef)*absInput : releaseCoef*envelope + (1-releaseCoef)*absInput;
|
||||
float gain = (envelope>threshold)? (threshold + (envelope-threshold)/ratio)/(envelope+1e-9f) : 1.0f;
|
||||
buffer[i]*=gain;
|
||||
}
|
||||
}
|
||||
inline void process(float* __restrict__ left, float* __restrict__ right, int count) {
|
||||
processBlock(left, count, envelopeL);
|
||||
processBlock(right, count, envelopeR);
|
||||
}
|
||||
};
|
||||
|
||||
CompressorOptimized gCompressor;
|
||||
ReverbOptimized gReverbL, gReverbR;
|
||||
BiquadBank gEqL, gEqR;
|
||||
BassFilter gBassL, gBassR;
|
||||
bool gDrcEnabled = false, gEqEnabled = false, gBassBoostEnabled = false;
|
||||
float gStereoWidth = 1.0f;
|
||||
alignas(16) std::array<float, 4096> gLeftBuf, gRightBuf;
|
||||
alignas(16) std::array<float, 256> gFFTData;
|
||||
alignas(16) std::array<std::complex<float>, FFT_SIZE> gFFTWork;
|
||||
|
||||
inline void fastFFT(std::complex<float>* __restrict__ data, int n) {
|
||||
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]);
|
||||
}
|
||||
for (int len = 2; len <= n; len <<= 1) {
|
||||
float ang = -2.0f * static_cast<float>(M_PI) / static_cast<float>(len);
|
||||
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;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
inline float fastSoftClip(float x) {
|
||||
float ax = fabsf(x);
|
||||
float sign = x > 0 ? 1.0f : -1.0f;
|
||||
if (ax > 1.0f) return sign;
|
||||
return x * (1.5f - 0.5f * x * x);
|
||||
}
|
||||
|
||||
extern "C" {
|
||||
|
||||
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*, jobject, jfloat m) { gReverbL.setMix(m); gReverbR.setMix(m); }
|
||||
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*, 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_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;
|
||||
int numFrames = (size / 2) / 2;
|
||||
if (numFrames > 4096) numFrames = 4096;
|
||||
|
||||
for (int i = 0; i < numFrames; i++) {
|
||||
gLeftBuf[i] = static_cast<float>(buffer[i * 2]) * INV_32768;
|
||||
gRightBuf[i] = static_cast<float>(buffer[i * 2 + 1]) * INV_32768;
|
||||
}
|
||||
|
||||
if (gEqEnabled) { gEqL.processBlock(gLeftBuf.data(), numFrames); gEqR.processBlock(gRightBuf.data(), numFrames); }
|
||||
if (gBassBoostEnabled) {
|
||||
for(int i=0; i<numFrames; i++) { gLeftBuf[i] = gBassL.process(gLeftBuf[i]); gRightBuf[i] = gBassR.process(gRightBuf[i]); }
|
||||
}
|
||||
gReverbL.processBlock(gLeftBuf.data(), gRightBuf.data(), numFrames);
|
||||
|
||||
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;
|
||||
}
|
||||
}
|
||||
|
||||
if (gDrcEnabled) gCompressor.process(gLeftBuf.data(), gRightBuf.data(), numFrames);
|
||||
|
||||
// FFT for visualization
|
||||
for (int k = 0; k < FFT_SIZE; k++) {
|
||||
gFFTWork[k] = (k < 256 && k < numFrames) ? std::complex<float>(gLeftBuf[k], 0.0f) : std::complex<float>(0.0f, 0.0f);
|
||||
}
|
||||
fastFFT(gFFTWork.data(), FFT_SIZE);
|
||||
for (int k = 0; k < 256; k++) {
|
||||
gFFTData[k] = std::abs(gFFTWork[k]) * 0.5f; // Increased scale
|
||||
}
|
||||
|
||||
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);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,70 @@
|
||||
#include <jni.h>
|
||||
#include <android/native_window_jni.h>
|
||||
#include <android/native_window.h>
|
||||
#include <android/log.h>
|
||||
#include <vector>
|
||||
#include <algorithm>
|
||||
|
||||
|
||||
extern "C" {
|
||||
|
||||
JNIEXPORT void JNICALL
|
||||
Java_com_michatec_radio_helpers_ExtrasHelper_visualize(JNIEnv *env, jclass clazz, jobject surface, jfloatArray data) {
|
||||
if (!surface) return;
|
||||
ANativeWindow* window = ANativeWindow_fromSurface(env, surface);
|
||||
if (!window) return;
|
||||
|
||||
jsize len = env->GetArrayLength(data);
|
||||
if (len == 0) {
|
||||
ANativeWindow_release(window);
|
||||
return;
|
||||
}
|
||||
jfloat* body = env->GetFloatArrayElements(data, nullptr);
|
||||
|
||||
ANativeWindow_Buffer buffer;
|
||||
ANativeWindow_setBuffersGeometry(window, 0, 0, WINDOW_FORMAT_RGBA_8888);
|
||||
|
||||
if (ANativeWindow_lock(window, &buffer, nullptr) == 0) {
|
||||
auto* pixels = static_cast<uint32_t*>(buffer.bits);
|
||||
|
||||
// Clear background (Dark Grey)
|
||||
for (int y = 0; y < buffer.height; y++) {
|
||||
for (int x = 0; x < buffer.width; x++) {
|
||||
pixels[y * buffer.stride + x] = 0xFF121212;
|
||||
}
|
||||
}
|
||||
|
||||
// Draw bars
|
||||
int displayBins = std::min(static_cast<int>(len), 128);
|
||||
float barWidth = static_cast<float>(buffer.width) / static_cast<float>(displayBins);
|
||||
|
||||
for (int i = 0; i < displayBins; i++) {
|
||||
// Keep original order: bass (low freq) at left, treble (high freq) at right
|
||||
float val = body[i];
|
||||
float scaledVal = val * 5.0f;
|
||||
int barHeight = static_cast<int>(scaledVal * static_cast<float>(buffer.height));
|
||||
|
||||
if (barHeight > buffer.height) barHeight = buffer.height;
|
||||
if (barHeight < 12) barHeight = 12; // Min height
|
||||
|
||||
int startX = static_cast<int>(static_cast<float>(i) * barWidth);
|
||||
int endX = static_cast<int>(static_cast<float>(i + 1) * barWidth);
|
||||
int barBottom = buffer.height;
|
||||
int barTop = barBottom - barHeight;
|
||||
|
||||
for (int x = startX; x <= endX; x++) {
|
||||
if (x < 0 || x >= buffer.width) continue;
|
||||
for (int y = barTop; y < barBottom; y++) {
|
||||
pixels[y * buffer.stride + x] = 0xFFC5DA03;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
ANativeWindow_unlockAndPost(window);
|
||||
}
|
||||
|
||||
env->ReleaseFloatArrayElements(data, body, JNI_ABORT);
|
||||
ANativeWindow_release(window);
|
||||
}
|
||||
|
||||
} // extern "C"
|
||||
@@ -1,208 +0,0 @@
|
||||
#include <jni.h>
|
||||
#include <string>
|
||||
#include <vector>
|
||||
#include <cmath>
|
||||
#include <algorithm>
|
||||
#include <android/log.h>
|
||||
|
||||
// --- DSP Classes ---
|
||||
|
||||
/**
|
||||
* 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;
|
||||
|
||||
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));
|
||||
|
||||
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;
|
||||
}
|
||||
|
||||
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);
|
||||
|
||||
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;
|
||||
}
|
||||
|
||||
float process(float in) {
|
||||
float out = in * a0 + z1;
|
||||
z1 = in * a1 + z2 - b1 * out;
|
||||
z2 = in * a2 - b2 * out;
|
||||
return out;
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Dynamic Range Compressor
|
||||
*/
|
||||
class Compressor {
|
||||
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));
|
||||
|
||||
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;
|
||||
|
||||
if (envelope > threshold) {
|
||||
float gainReduction = threshold + (envelope - threshold) / ratio;
|
||||
buffer[i] *= (gainReduction / envelope);
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* 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;
|
||||
}
|
||||
};
|
||||
|
||||
// --- Global Engine State ---
|
||||
Compressor gCompressor;
|
||||
Reverb gReverb;
|
||||
std::vector<Biquad> gEqBands(10);
|
||||
Biquad gBassBoost;
|
||||
|
||||
bool gDrcEnabled = false;
|
||||
bool gReverbEnabled = false;
|
||||
bool gEqEnabled = false;
|
||||
bool gBassBoostEnabled = false;
|
||||
|
||||
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_setReverbMix(JNIEnv *env, jobject thiz, jfloat mix) {
|
||||
gReverb.mix = mix;
|
||||
gReverbEnabled = (mix > 0.01f);
|
||||
}
|
||||
|
||||
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_setBassBoost(JNIEnv *env, jobject thiz, jfloat gainDb) {
|
||||
if (gainDb > 0.0f) {
|
||||
gBassBoost.setLowShelf(44100.0f, 150.0f, gainDb, 0.707f);
|
||||
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);
|
||||
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;
|
||||
|
||||
// Apply EQ
|
||||
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)]);
|
||||
}
|
||||
}
|
||||
|
||||
// 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)]);
|
||||
}
|
||||
|
||||
// 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);
|
||||
}
|
||||
|
||||
// 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);
|
||||
}
|
||||
|
||||
env->ReleaseShortArrayElements(data, buffer, 0);
|
||||
}
|
||||
|
||||
} // 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
|
||||
}
|
||||
|
||||
@@ -27,6 +27,7 @@ object Keys {
|
||||
const val EXTRA_START_LAST_PLAYED_STATION: String = "START_LAST_PLAYED_STATION"
|
||||
const val EXTRA_SLEEP_TIMER_REMAINING: String = "SLEEP_TIMER_REMAINING"
|
||||
const val EXTRA_METADATA_HISTORY: String = "METADATA_HISTORY"
|
||||
const val EXTRA_VISUALIZER_DATA: String = "VISUALIZER_DATA"
|
||||
|
||||
// arguments
|
||||
const val ARG_UPDATE_COLLECTION: String = "ArgUpdateCollection"
|
||||
@@ -43,6 +44,7 @@ object Keys {
|
||||
const val CMD_PLAY_STREAM: String = "PLAY_STREAM"
|
||||
const val CMD_REQUEST_SLEEP_TIMER_REMAINING: String = "REQUEST_SLEEP_TIMER_REMAINING"
|
||||
const val CMD_REQUEST_METADATA_HISTORY: String = "REQUEST_METADATA_HISTORY"
|
||||
const val CMD_GET_VISUALIZER_DATA: String = "GET_VISUALIZER_DATA"
|
||||
|
||||
// preferences
|
||||
const val PREF_RADIO_BROWSER_API: String = "RADIO_BROWSER_API"
|
||||
@@ -66,6 +68,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()
|
||||
}
|
||||
}
|
||||
@@ -334,6 +375,7 @@ class PlayerService : MediaLibraryService(), SharedPreferences.OnSharedPreferenc
|
||||
builder.add(SessionCommand(Keys.CMD_CANCEL_SLEEP_TIMER, Bundle.EMPTY))
|
||||
builder.add(SessionCommand(Keys.CMD_REQUEST_SLEEP_TIMER_REMAINING, Bundle.EMPTY))
|
||||
builder.add(SessionCommand(Keys.CMD_REQUEST_METADATA_HISTORY, Bundle.EMPTY))
|
||||
builder.add(SessionCommand(Keys.CMD_GET_VISUALIZER_DATA, Bundle.EMPTY))
|
||||
return MediaSession.ConnectionResult.accept(builder.build(), connectionResult.availablePlayerCommands)
|
||||
}
|
||||
|
||||
@@ -420,6 +462,19 @@ class PlayerService : MediaLibraryService(), SharedPreferences.OnSharedPreferenc
|
||||
)
|
||||
)
|
||||
}
|
||||
Keys.CMD_GET_VISUALIZER_DATA -> {
|
||||
val resultBundle = Bundle()
|
||||
resultBundle.putFloatArray(
|
||||
Keys.EXTRA_VISUALIZER_DATA,
|
||||
nativeAudioProcessor.getVisualizer()
|
||||
)
|
||||
return Futures.immediateFuture(
|
||||
SessionResult(
|
||||
SessionResult.RESULT_SUCCESS,
|
||||
resultBundle
|
||||
)
|
||||
)
|
||||
}
|
||||
}
|
||||
return super.onCustomCommand(session, controller, customCommand, args)
|
||||
}
|
||||
|
||||
@@ -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,19 +212,53 @@ 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
|
||||
}
|
||||
|
||||
val preferenceVisualizer = Preference(context)
|
||||
preferenceVisualizer.title = getString(R.string.pref_visualizer_title)
|
||||
preferenceVisualizer.setIcon(R.drawable.ic_music_note_24dp)
|
||||
preferenceVisualizer.summary = getString(R.string.pref_visualizer_summary)
|
||||
preferenceVisualizer.setOnPreferenceClickListener {
|
||||
findNavController().navigate(R.id.action_settings_to_visualizer)
|
||||
return@setOnPreferenceClickListener true
|
||||
}
|
||||
|
||||
// set up "App Version" preference
|
||||
val preferenceAppVersion = Preference(context)
|
||||
preferenceAppVersion.title = getString(R.string.pref_app_version_title)
|
||||
@@ -268,7 +308,7 @@ class SettingsFragment : PreferenceFragmentCompat(), YesNoDialog.YesNoDialogList
|
||||
// set preference categories
|
||||
val preferenceCategoryGeneral = PreferenceCategory(activity as Context)
|
||||
preferenceCategoryGeneral.title = getString(R.string.pref_general_title)
|
||||
|
||||
|
||||
val preferenceCategoryAudioEffects = PreferenceCategory(context)
|
||||
preferenceCategoryAudioEffects.title = getString(R.string.pref_audio_effects_title)
|
||||
|
||||
@@ -295,7 +335,9 @@ class SettingsFragment : PreferenceFragmentCompat(), YesNoDialog.YesNoDialogList
|
||||
preferenceCategoryAudioEffects.addPreference(preferenceBassBoost)
|
||||
preferenceCategoryAudioEffects.addPreference(preferenceReverb)
|
||||
preferenceCategoryAudioEffects.addPreference(preferenceDrc)
|
||||
preferenceCategoryAudioEffects.addPreference(preferencePresetSelection)
|
||||
preferenceCategoryAudioEffects.addPreference(preferenceEqualizer)
|
||||
preferenceCategoryAudioEffects.addPreference(preferenceVisualizer)
|
||||
|
||||
screen.addPreference(preferenceCategoryMaintenance)
|
||||
preferenceCategoryMaintenance.addPreference(preferenceUpdateStationImages)
|
||||
@@ -340,6 +382,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,122 @@
|
||||
package com.michatec.radio
|
||||
|
||||
import android.content.ComponentName
|
||||
import android.os.Bundle
|
||||
import android.os.Handler
|
||||
import android.os.Looper
|
||||
import android.util.Log
|
||||
import android.view.View
|
||||
import androidx.annotation.OptIn
|
||||
import androidx.appcompat.app.AppCompatActivity
|
||||
import androidx.media3.common.util.UnstableApi
|
||||
import androidx.media3.session.MediaController
|
||||
import androidx.media3.session.SessionToken
|
||||
import androidx.preference.PreferenceFragmentCompat
|
||||
import com.google.common.util.concurrent.ListenableFuture
|
||||
import com.google.common.util.concurrent.MoreExecutors
|
||||
import com.michatec.radio.extensions.requestVisualizerData
|
||||
import com.michatec.radio.helpers.ExtrasHelper
|
||||
|
||||
/*
|
||||
* VisualizerFragment class: Handles audio visualization
|
||||
*/
|
||||
@OptIn(UnstableApi::class)
|
||||
class VisualizerFragment : PreferenceFragmentCompat() {
|
||||
|
||||
private val TAG = "VisualizerFragment"
|
||||
private lateinit var controllerFuture: ListenableFuture<MediaController>
|
||||
private val controller: MediaController?
|
||||
get() = if (this::controllerFuture.isInitialized && controllerFuture.isDone) {
|
||||
try { controllerFuture.get() } catch (_: Exception) { null }
|
||||
} else null
|
||||
|
||||
private var visualizerPref: ExtrasHelper.VisualizerPreference? = null
|
||||
private val handler = Handler(Looper.getMainLooper())
|
||||
|
||||
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
|
||||
super.onViewCreated(view, savedInstanceState)
|
||||
(activity as AppCompatActivity).supportActionBar?.title = getString(R.string.pref_visualizer_title)
|
||||
(activity as AppCompatActivity).supportActionBar?.show()
|
||||
}
|
||||
|
||||
override fun onCreatePreferences(savedInstanceState: Bundle?, rootKey: String?) {
|
||||
val context = preferenceManager.context
|
||||
val screen = preferenceManager.createPreferenceScreen(context)
|
||||
|
||||
visualizerPref = ExtrasHelper.VisualizerPreference(context)
|
||||
visualizerPref?.key = "visualizer_key"
|
||||
screen.addPreference(visualizerPref!!)
|
||||
|
||||
preferenceScreen = screen
|
||||
}
|
||||
|
||||
override fun onStart() {
|
||||
super.onStart()
|
||||
initializeController()
|
||||
}
|
||||
|
||||
override fun onStop() {
|
||||
super.onStop()
|
||||
releaseController()
|
||||
}
|
||||
|
||||
override fun onResume() {
|
||||
super.onResume()
|
||||
startPolling()
|
||||
}
|
||||
|
||||
override fun onPause() {
|
||||
super.onPause()
|
||||
stopPolling()
|
||||
}
|
||||
|
||||
private fun initializeController() {
|
||||
controllerFuture = MediaController.Builder(
|
||||
requireContext(),
|
||||
SessionToken(requireContext(), ComponentName(requireContext(), PlayerService::class.java))
|
||||
).buildAsync()
|
||||
controllerFuture.addListener({
|
||||
Log.d(TAG, "MediaController connected: ${controller != null}")
|
||||
}, MoreExecutors.directExecutor())
|
||||
}
|
||||
|
||||
private fun releaseController() {
|
||||
if (this::controllerFuture.isInitialized) {
|
||||
MediaController.releaseFuture(controllerFuture)
|
||||
}
|
||||
}
|
||||
|
||||
private val pollRunnable = object : Runnable {
|
||||
override fun run() {
|
||||
val c = controller
|
||||
if (c != null && c.isPlaying) {
|
||||
val resultFuture = c.requestVisualizerData()
|
||||
resultFuture.addListener({
|
||||
try {
|
||||
val result = resultFuture.get()
|
||||
if (result.resultCode == androidx.media3.session.SessionResult.RESULT_SUCCESS) {
|
||||
val data = result.extras.getFloatArray(Keys.EXTRA_VISUALIZER_DATA)
|
||||
if (data != null && data.isNotEmpty()) {
|
||||
visualizerPref?.update(data)
|
||||
}
|
||||
} else {
|
||||
Log.e(TAG, "Custom command failed with result code: ${result.resultCode}")
|
||||
}
|
||||
} catch (e: Exception) {
|
||||
Log.e(TAG, "Error fetching visualizer data", e)
|
||||
}
|
||||
}, MoreExecutors.directExecutor())
|
||||
}
|
||||
handler.postDelayed(this, 18) // ~60 FPS
|
||||
}
|
||||
}
|
||||
|
||||
private fun startPolling() {
|
||||
handler.removeCallbacks(pollRunnable)
|
||||
handler.post(pollRunnable)
|
||||
}
|
||||
|
||||
private fun stopPolling() {
|
||||
handler.removeCallbacks(pollRunnable)
|
||||
}
|
||||
}
|
||||
@@ -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()
|
||||
}
|
||||
}
|
||||
@@ -35,7 +35,7 @@ fun MediaController.requestSleepTimerRemaining(): ListenableFuture<SessionResult
|
||||
}
|
||||
|
||||
|
||||
/* Request sleep timer remaining */
|
||||
/* Request metadata history */
|
||||
fun MediaController.requestMetadataHistory(): ListenableFuture<SessionResult> {
|
||||
return sendCustomCommand(
|
||||
SessionCommand(Keys.CMD_REQUEST_METADATA_HISTORY, Bundle.EMPTY),
|
||||
@@ -43,6 +43,14 @@ fun MediaController.requestMetadataHistory(): ListenableFuture<SessionResult> {
|
||||
)
|
||||
}
|
||||
|
||||
/* Request visualizer data */
|
||||
fun MediaController.requestVisualizerData(): ListenableFuture<SessionResult> {
|
||||
return sendCustomCommand(
|
||||
SessionCommand(Keys.CMD_GET_VISUALIZER_DATA, Bundle.EMPTY),
|
||||
Bundle.EMPTY
|
||||
)
|
||||
}
|
||||
|
||||
|
||||
/* Starts playback with a new media item */
|
||||
fun MediaController.play(context: Context, station: Station) {
|
||||
|
||||
@@ -0,0 +1,117 @@
|
||||
package com.michatec.radio.helpers
|
||||
|
||||
import android.content.Context
|
||||
import android.util.AttributeSet
|
||||
import android.util.Log
|
||||
import android.view.Surface
|
||||
import android.view.SurfaceHolder
|
||||
import android.view.SurfaceView
|
||||
import android.view.ViewGroup
|
||||
import android.widget.FrameLayout
|
||||
import androidx.preference.Preference
|
||||
import androidx.preference.PreferenceViewHolder
|
||||
import com.michatec.radio.R
|
||||
|
||||
class ExtrasHelper {
|
||||
companion object {
|
||||
private const val TAG = "ExtrasHelper"
|
||||
init {
|
||||
try {
|
||||
System.loadLibrary("extra")
|
||||
} catch (e: Exception) {
|
||||
Log.e(TAG, "Failed to load extra library", e)
|
||||
}
|
||||
}
|
||||
|
||||
@JvmStatic
|
||||
private external fun visualize(surface: Surface, data: FloatArray)
|
||||
|
||||
fun render(surface: Surface, data: FloatArray) {
|
||||
if (!surface.isValid) return
|
||||
try {
|
||||
visualize(surface, data)
|
||||
} catch (e: Exception) {
|
||||
Log.e(TAG, "Native visualize failed", e)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
class VisualizerPreference(context: Context, attrs: AttributeSet? = null) : Preference(context, attrs) {
|
||||
private var visualizerView: VisualizerView? = null
|
||||
|
||||
init {
|
||||
// We can use a standard layout and inject our view
|
||||
layoutResource = R.layout.preference_visualizer
|
||||
}
|
||||
|
||||
override fun onBindViewHolder(holder: PreferenceViewHolder) {
|
||||
super.onBindViewHolder(holder)
|
||||
|
||||
// Try to find the container in the inflated layout
|
||||
var container = holder.findViewById(R.id.visualizer_container) as? ViewGroup
|
||||
|
||||
// Fallback: If not found by ID, maybe the root is the container?
|
||||
if (container == null && holder.itemView is ViewGroup) {
|
||||
container = holder.itemView as ViewGroup
|
||||
}
|
||||
|
||||
if (container != null) {
|
||||
if (visualizerView == null) {
|
||||
visualizerView = VisualizerView(context).apply {
|
||||
layoutParams = ViewGroup.LayoutParams(
|
||||
ViewGroup.LayoutParams.MATCH_PARENT,
|
||||
ViewGroup.LayoutParams.MATCH_PARENT
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
val currentParent = visualizerView?.parent as? ViewGroup
|
||||
if (currentParent != container) {
|
||||
currentParent?.removeView(visualizerView)
|
||||
// If we injected into a standard preference, don't clear everything, just add
|
||||
if (container is FrameLayout || container.childCount == 0) {
|
||||
container.removeAllViews()
|
||||
}
|
||||
container.addView(visualizerView)
|
||||
}
|
||||
} else {
|
||||
Log.e("VisualizerPreference", "Could not find any container to attach VisualizerView!")
|
||||
}
|
||||
}
|
||||
|
||||
fun update(data: FloatArray) {
|
||||
visualizerView?.update(data)
|
||||
}
|
||||
}
|
||||
|
||||
class VisualizerView @JvmOverloads constructor(
|
||||
context: Context, attrs: AttributeSet? = null, defStyleAttr: Int = 0
|
||||
) : SurfaceView(context, attrs, defStyleAttr), SurfaceHolder.Callback {
|
||||
|
||||
private var surface: Surface? = null
|
||||
|
||||
init {
|
||||
Log.d("VisualizerView", "VisualizerView initialized")
|
||||
holder.addCallback(this)
|
||||
}
|
||||
|
||||
fun update(data: FloatArray) {
|
||||
val s = surface
|
||||
if (s != null && s.isValid) {
|
||||
render(s, data)
|
||||
}
|
||||
}
|
||||
|
||||
override fun surfaceCreated(holder: SurfaceHolder) {
|
||||
surface = holder.surface
|
||||
}
|
||||
|
||||
override fun surfaceChanged(holder: SurfaceHolder, format: Int, width: Int, height: Int) {
|
||||
surface = holder.surface
|
||||
}
|
||||
|
||||
override fun surfaceDestroyed(holder: SurfaceHolder) {
|
||||
surface = null
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,5 +1,6 @@
|
||||
package com.michatec.radio.helpers
|
||||
|
||||
import android.util.Log
|
||||
import androidx.annotation.OptIn
|
||||
import androidx.media3.common.C
|
||||
import androidx.media3.common.audio.AudioProcessor
|
||||
@@ -13,46 +14,117 @@ import java.nio.ByteOrder
|
||||
class NativeAudioProcessor : BaseAudioProcessor() {
|
||||
|
||||
companion object {
|
||||
private const val TAG = "NativeAudioProcessor"
|
||||
init {
|
||||
System.loadLibrary("radio")
|
||||
try {
|
||||
System.loadLibrary("dsp")
|
||||
} catch (e: Exception) {
|
||||
Log.e(TAG, "Failed to load dsp library", e)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// JNI Methods
|
||||
private var directBuffer: ByteBuffer? = null
|
||||
|
||||
// ===== 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)
|
||||
|
||||
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 {
|
||||
// Always try to support the input format if it is PCM 16-bit
|
||||
if (inputAudioFormat.encoding != C.ENCODING_PCM_16BIT) {
|
||||
Log.e(TAG, "Unsupported encoding: ${inputAudioFormat.encoding}")
|
||||
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)
|
||||
// Always ensure we have a direct buffer for JNI
|
||||
if (directBuffer == null || directBuffer!!.capacity() < size) {
|
||||
directBuffer = ByteBuffer.allocateDirect(size).order(ByteOrder.nativeOrder())
|
||||
}
|
||||
|
||||
directBuffer!!.clear()
|
||||
inputBuffer.position()
|
||||
directBuffer!!.put(inputBuffer)
|
||||
|
||||
directBuffer!!.flip()
|
||||
|
||||
// Process audio in JNI
|
||||
processAudioDirect(directBuffer!!, size)
|
||||
|
||||
inputBuffer.order(ByteOrder.nativeOrder()).asShortBuffer().get(shortArray)
|
||||
// Copy processed data back to output
|
||||
val out = replaceOutputBuffer(size)
|
||||
out.order(ByteOrder.nativeOrder())
|
||||
|
||||
directBuffer!!.position(0)
|
||||
out.put(directBuffer!!)
|
||||
out.flip()
|
||||
|
||||
processAudio(shortArray, shortArraySize)
|
||||
}
|
||||
|
||||
val outputBuffer = replaceOutputBuffer(remaining)
|
||||
outputBuffer.asShortBuffer().put(shortArray)
|
||||
outputBuffer.limit(remaining)
|
||||
override fun onReset() {
|
||||
super.onReset()
|
||||
directBuffer = null
|
||||
}
|
||||
|
||||
inputBuffer.position(inputBuffer.limit())
|
||||
// ===== Presets =====
|
||||
fun setPresetRock() {
|
||||
enableDrc(true)
|
||||
setReverb(0.10f)
|
||||
setWidth(1.1f)
|
||||
setEqAll(floatArrayOf(2f, 1f, 0f, -1f, -1f, 0f, 1f, 2f, 2f, 3f))
|
||||
enableBassBoost(0.6f)
|
||||
}
|
||||
|
||||
fun setPresetPop() {
|
||||
enableDrc(true)
|
||||
setReverb(0.15f)
|
||||
setWidth(1.05f)
|
||||
setEqAll(floatArrayOf(0f, 1f, 1f, 1f, 0f, 0f, 1f, 2f, 2f, 1f))
|
||||
enableBassBoost(0.5f)
|
||||
}
|
||||
|
||||
fun setPresetJazz() {
|
||||
enableDrc(false)
|
||||
setReverb(0.15f)
|
||||
setWidth(1.0f)
|
||||
setEqAll(floatArrayOf(0f, 0f, 1f, 1f, 0f, 0f, 1f, 1f, 0f, 0f))
|
||||
enableBassBoost(0.2f)
|
||||
}
|
||||
|
||||
fun setPresetFlat() {
|
||||
enableDrc(false)
|
||||
setReverb(0f)
|
||||
setWidth(1f)
|
||||
setEqAll(FloatArray(10))
|
||||
enableBassBoost(0f)
|
||||
}
|
||||
}
|
||||
@@ -257,13 +257,13 @@ 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)) 0.4f else 0.0f
|
||||
}
|
||||
|
||||
|
||||
/* Loads Reverb mix */
|
||||
fun loadReverb(): Float {
|
||||
return if (sharedPreferences.getBoolean(Keys.PREF_REVERB, false)) 0.3f else 0.0f
|
||||
return if (sharedPreferences.getBoolean(Keys.PREF_REVERB, false)) 0.2f 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,75 @@
|
||||
<?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="24dp">
|
||||
|
||||
<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="24sp"
|
||||
android:textStyle="bold"
|
||||
android:paddingBottom="16dp" />
|
||||
|
||||
<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="20sp"
|
||||
android:padding="12dp"
|
||||
android:focusable="true"
|
||||
android:clickable="true" />
|
||||
|
||||
<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="20sp"
|
||||
android:padding="12dp"
|
||||
android:focusable="true"
|
||||
android:clickable="true" />
|
||||
|
||||
<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="20sp"
|
||||
android:padding="12dp"
|
||||
android:focusable="true"
|
||||
android:clickable="true" />
|
||||
|
||||
<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="20sp"
|
||||
android:padding="12dp"
|
||||
android:focusable="true"
|
||||
android:clickable="true" />
|
||||
|
||||
<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="20sp"
|
||||
android:padding="12dp"
|
||||
android:focusable="true"
|
||||
android:clickable="true" />
|
||||
|
||||
</RadioGroup>
|
||||
|
||||
</LinearLayout>
|
||||
@@ -0,0 +1,14 @@
|
||||
<?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="match_parent"
|
||||
android:orientation="vertical">
|
||||
|
||||
<FrameLayout
|
||||
android:id="@+id/visualizer_container"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent"
|
||||
android:background="#FF000000"
|
||||
android:focusable="true" />
|
||||
|
||||
</LinearLayout>
|
||||
@@ -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>
|
||||
@@ -0,0 +1,13 @@
|
||||
<?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="match_parent"
|
||||
android:orientation="vertical">
|
||||
|
||||
<FrameLayout
|
||||
android:id="@+id/visualizer_container"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent"
|
||||
android:background="#FF000000" />
|
||||
|
||||
</LinearLayout>
|
||||
@@ -23,6 +23,9 @@
|
||||
<action
|
||||
android:id="@+id/action_settings_to_equalizer"
|
||||
app:destination="@id/equalizer_destination" />
|
||||
<action
|
||||
android:id="@+id/action_settings_to_visualizer"
|
||||
app:destination="@id/visualizer_destination" />
|
||||
</fragment>
|
||||
|
||||
<!-- EQUALIZER -->
|
||||
@@ -31,4 +34,9 @@
|
||||
android:name="com.michatec.radio.EqualizerFragment"
|
||||
android:label="Equalizer" />
|
||||
|
||||
<!-- VISUALIZER -->
|
||||
<fragment
|
||||
android:id="@+id/visualizer_destination"
|
||||
android:name="com.michatec.radio.VisualizerFragment"
|
||||
android:label="Visualizer" />
|
||||
</navigation>
|
||||
|
||||
@@ -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,27 @@
|
||||
<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>
|
||||
<string name="pref_visualizer_title">Spektrumanzeige</string>
|
||||
<string name="pref_visualizer_summary">Sehe die Spektrumanzeige.</string>
|
||||
</resources>
|
||||
|
||||
@@ -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>
|
||||
@@ -158,4 +173,6 @@
|
||||
<!-- Extras -->
|
||||
<string name="loading">Loading...</string>
|
||||
<string name="media_route_menu_title">Cast</string>
|
||||
<string name="pref_visualizer_title">Spectrum Analyzer</string>
|
||||
<string name="pref_visualizer_summary">Show the Spectrum Analyzer.</string>
|
||||
</resources>
|
||||
|
||||
Reference in New Issue
Block a user