7 Commits

21 changed files with 1275 additions and 289 deletions
+10 -6
View File
@@ -18,21 +18,25 @@ project("radio")
# Gradle automatically packages shared libraries with your APK. # Gradle automatically packages shared libraries with your APK.
# #
# In this top level CMakeLists.txt, ${CMAKE_PROJECT_NAME} is used to define # 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. # is preferred for the same purpose.
# #
# In order to load a library into your app from Java/Kotlin, you must call # 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; # System.loadLibrary() and pass the name of the library defined here;
# for GameActivity/NativeActivity derived applications, the same library name must be # for GameActivity/NativeActivity derived applications, the same library name must be
# used in the AndroidManifest.xml file. # used in the AndroidManifest.xml file.
add_library(${CMAKE_PROJECT_NAME} SHARED add_library(dsp SHARED
# List C/C++ source files with relative paths to this CMakeLists.txt. dsp.cpp)
radio.cpp)
add_library(extra SHARED
extra.cpp)
# Specifies libraries CMake should link to your target library. You # Specifies libraries CMake should link to your target library. You
# can link libraries from various origins, such as libraries defined in this # can link libraries from various origins, such as libraries defined in this
# build script, prebuilt third-party libraries, or Android system libraries. # build script, prebuilt third-party libraries, or Android system libraries.
target_link_libraries(${CMAKE_PROJECT_NAME} target_link_libraries(dsp
# List libraries link to the target library
android android
log) log)
target_link_libraries(extra
android
log)
+287
View File
@@ -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);
}
}
}
+70
View File
@@ -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"
-208
View File
@@ -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 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() { 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?) { override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState) super.onViewCreated(view, savedInstanceState)
(activity as AppCompatActivity).supportActionBar?.title = getString(R.string.pref_equalizer_title) (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.setIcon(R.drawable.ic_refresh_24dp)
resetPreference.setOnPreferenceClickListener { resetPreference.setOnPreferenceClickListener {
PreferencesHelper.resetEqualizer() PreferencesHelper.resetEqualizer()
// Manually update SeekBars to 0 for (key in eqKeys) {
findPreference<SeekBarPreference>(Keys.PREF_EQ_LOW)?.value = 0 findPreference<SeekBarPreference>(key)?.value = 0
findPreference<SeekBarPreference>(Keys.PREF_EQ_MID)?.value = 0 }
findPreference<SeekBarPreference>(Keys.PREF_EQ_HIGH)?.value = 0
return@setOnPreferenceClickListener true return@setOnPreferenceClickListener true
} }
screen.addPreference(resetPreference) screen.addPreference(resetPreference)
// EQ Low // Create 10-band EQ
val eqLow = SeekBarPreference(context) for (i in eqKeys.indices) {
eqLow.title = getString(R.string.pref_eq_low_title) val eqBand = SeekBarPreference(context)
eqLow.key = Keys.PREF_EQ_LOW eqBand.title = "Equalizer: ${eqFrequencies[i]}"
eqLow.setIcon(R.drawable.ic_music_note_24dp) eqBand.key = eqKeys[i]
eqLow.min = -12 eqBand.setIcon(R.drawable.ic_music_note_24dp)
eqLow.max = 12 eqBand.min = -12
eqLow.showSeekBarValue = true eqBand.max = 12
eqLow.setDefaultValue(0) eqBand.showSeekBarValue = true
screen.addPreference(eqLow) eqBand.setDefaultValue(0)
screen.addPreference(eqBand)
// 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)
preferenceScreen = screen preferenceScreen = screen
} }
@@ -27,6 +27,7 @@ object Keys {
const val EXTRA_START_LAST_PLAYED_STATION: String = "START_LAST_PLAYED_STATION" 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_SLEEP_TIMER_REMAINING: String = "SLEEP_TIMER_REMAINING"
const val EXTRA_METADATA_HISTORY: String = "METADATA_HISTORY" const val EXTRA_METADATA_HISTORY: String = "METADATA_HISTORY"
const val EXTRA_VISUALIZER_DATA: String = "VISUALIZER_DATA"
// arguments // arguments
const val ARG_UPDATE_COLLECTION: String = "ArgUpdateCollection" const val ARG_UPDATE_COLLECTION: String = "ArgUpdateCollection"
@@ -43,6 +44,7 @@ object Keys {
const val CMD_PLAY_STREAM: String = "PLAY_STREAM" const val CMD_PLAY_STREAM: String = "PLAY_STREAM"
const val CMD_REQUEST_SLEEP_TIMER_REMAINING: String = "REQUEST_SLEEP_TIMER_REMAINING" 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_REQUEST_METADATA_HISTORY: String = "REQUEST_METADATA_HISTORY"
const val CMD_GET_VISUALIZER_DATA: String = "GET_VISUALIZER_DATA"
// preferences // preferences
const val PREF_RADIO_BROWSER_API: String = "RADIO_BROWSER_API" 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_LOW: String = "EQ_LOW"
const val PREF_EQ_MID: String = "EQ_MID" const val PREF_EQ_MID: String = "EQ_MID"
const val PREF_EQ_HIGH: String = "EQ_HIGH" 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 // default const values
const val DEFAULT_SIZE_OF_METADATA_HISTORY: Int = 25 const val DEFAULT_SIZE_OF_METADATA_HISTORY: Int = 25
@@ -286,20 +286,61 @@ class PlayerService : MediaLibraryService(), SharedPreferences.OnSharedPreferenc
/* Applies audio effects based on preferences */ /* Applies audio effects based on preferences */
private fun applyAudioEffects() { private fun applyAudioEffects() {
nativeAudioProcessor.enableBassBoost(PreferencesHelper.loadBassBoost()) val selectedPreset = PreferencesHelper.loadSelectedPreset()
nativeAudioProcessor.setReverb(PreferencesHelper.loadReverb()) if (selectedPreset.isNotEmpty()) {
nativeAudioProcessor.enableDrc(PreferencesHelper.loadDrcEnabled()) applyPreset(selectedPreset)
nativeAudioProcessor.setEq(0, PreferencesHelper.loadEqLow()) } else {
nativeAudioProcessor.setEq(1, PreferencesHelper.loadEqMid()) // Apply manual settings
nativeAudioProcessor.setEq(2, PreferencesHelper.loadEqHigh()) 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 */ /* Overrides onSharedPreferenceChanged from SharedPreferences.OnSharedPreferenceChangeListener */
override fun onSharedPreferenceChanged(sharedPreferences: SharedPreferences?, key: String?) { override fun onSharedPreferenceChanged(sharedPreferences: SharedPreferences?, key: String?) {
when (key) { when (key) {
Keys.PREF_BASS_BOOST, Keys.PREF_REVERB, Keys.PREF_DRC, 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_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() 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_CANCEL_SLEEP_TIMER, Bundle.EMPTY))
builder.add(SessionCommand(Keys.CMD_REQUEST_SLEEP_TIMER_REMAINING, 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_REQUEST_METADATA_HISTORY, Bundle.EMPTY))
builder.add(SessionCommand(Keys.CMD_GET_VISUALIZER_DATA, Bundle.EMPTY))
return MediaSession.ConnectionResult.accept(builder.build(), connectionResult.availablePlayerCommands) 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) return super.onCustomCommand(session, controller, customCommand, args)
} }
@@ -17,6 +17,7 @@ import androidx.navigation.fragment.findNavController
import androidx.preference.* import androidx.preference.*
import com.google.android.material.snackbar.Snackbar import com.google.android.material.snackbar.Snackbar
import com.michatec.radio.dialogs.ErrorDialog import com.michatec.radio.dialogs.ErrorDialog
import com.michatec.radio.dialogs.PresetSelectionDialog
import com.michatec.radio.dialogs.ThemeSelectionDialog import com.michatec.radio.dialogs.ThemeSelectionDialog
import com.michatec.radio.dialogs.YesNoDialog import com.michatec.radio.dialogs.YesNoDialog
import com.michatec.radio.helpers.* import com.michatec.radio.helpers.*
@@ -30,7 +31,7 @@ import java.util.*
/* /*
* SettingsFragment class * SettingsFragment class
*/ */
class SettingsFragment : PreferenceFragmentCompat(), YesNoDialog.YesNoDialogListener, ThemeSelectionDialog.ThemeSelectionDialogListener { class SettingsFragment : PreferenceFragmentCompat(), YesNoDialog.YesNoDialogListener, ThemeSelectionDialog.ThemeSelectionDialogListener, PresetSelectionDialog.PresetSelectionDialogListener {
/* Define log tag */ /* Define log tag */
@@ -52,6 +53,9 @@ class SettingsFragment : PreferenceFragmentCompat(), YesNoDialog.YesNoDialogList
val context = preferenceManager.context val context = preferenceManager.context
val screen = preferenceManager.createPreferenceScreen(context) val screen = preferenceManager.createPreferenceScreen(context)
// Load current preset once
val currentPreset = PreferencesHelper.loadSelectedPreset()
// set up "App Theme" preference // set up "App Theme" preference
val preferenceThemeSelection = Preference(activity as Context) val preferenceThemeSelection = Preference(activity as Context)
preferenceThemeSelection.title = getString(R.string.pref_theme_selection_title) 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.title = getString(R.string.pref_bass_boost_title)
preferenceBassBoost.setIcon(R.drawable.ic_music_note_24dp) preferenceBassBoost.setIcon(R.drawable.ic_music_note_24dp)
preferenceBassBoost.key = Keys.PREF_BASS_BOOST preferenceBassBoost.key = Keys.PREF_BASS_BOOST
preferenceBassBoost.isEnabled = currentPreset.isEmpty()
preferenceBassBoost.summary = getString(R.string.pref_bass_boost_summary) preferenceBassBoost.summary = getString(R.string.pref_bass_boost_summary)
preferenceBassBoost.setDefaultValue(false) preferenceBassBoost.setDefaultValue(false)
@@ -198,6 +203,7 @@ class SettingsFragment : PreferenceFragmentCompat(), YesNoDialog.YesNoDialogList
preferenceReverb.title = getString(R.string.pref_reverb_title) preferenceReverb.title = getString(R.string.pref_reverb_title)
preferenceReverb.setIcon(R.drawable.ic_music_note_24dp) preferenceReverb.setIcon(R.drawable.ic_music_note_24dp)
preferenceReverb.key = Keys.PREF_REVERB preferenceReverb.key = Keys.PREF_REVERB
preferenceReverb.isEnabled = currentPreset.isEmpty()
preferenceReverb.summary = getString(R.string.pref_reverb_summary) preferenceReverb.summary = getString(R.string.pref_reverb_summary)
preferenceReverb.setDefaultValue(false) preferenceReverb.setDefaultValue(false)
@@ -206,19 +212,53 @@ class SettingsFragment : PreferenceFragmentCompat(), YesNoDialog.YesNoDialogList
preferenceDrc.title = getString(R.string.pref_drc_title) preferenceDrc.title = getString(R.string.pref_drc_title)
preferenceDrc.setIcon(R.drawable.ic_music_note_24dp) preferenceDrc.setIcon(R.drawable.ic_music_note_24dp)
preferenceDrc.key = Keys.PREF_DRC preferenceDrc.key = Keys.PREF_DRC
preferenceDrc.isEnabled = currentPreset.isEmpty()
preferenceDrc.summary = getString(R.string.pref_drc_summary) preferenceDrc.summary = getString(R.string.pref_drc_summary)
preferenceDrc.setDefaultValue(true) 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 // set up "Equalizer" preference entry
val preferenceEqualizer = Preference(context) val preferenceEqualizer = Preference(context)
preferenceEqualizer.title = getString(R.string.pref_equalizer_title) preferenceEqualizer.title = getString(R.string.pref_equalizer_title)
preferenceEqualizer.setIcon(R.drawable.ic_music_note_24dp) 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 { preferenceEqualizer.setOnPreferenceClickListener {
findNavController().navigate(R.id.action_settings_to_equalizer) findNavController().navigate(R.id.action_settings_to_equalizer)
return@setOnPreferenceClickListener true 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 // set up "App Version" preference
val preferenceAppVersion = Preference(context) val preferenceAppVersion = Preference(context)
preferenceAppVersion.title = getString(R.string.pref_app_version_title) preferenceAppVersion.title = getString(R.string.pref_app_version_title)
@@ -268,7 +308,7 @@ class SettingsFragment : PreferenceFragmentCompat(), YesNoDialog.YesNoDialogList
// set preference categories // set preference categories
val preferenceCategoryGeneral = PreferenceCategory(activity as Context) val preferenceCategoryGeneral = PreferenceCategory(activity as Context)
preferenceCategoryGeneral.title = getString(R.string.pref_general_title) preferenceCategoryGeneral.title = getString(R.string.pref_general_title)
val preferenceCategoryAudioEffects = PreferenceCategory(context) val preferenceCategoryAudioEffects = PreferenceCategory(context)
preferenceCategoryAudioEffects.title = getString(R.string.pref_audio_effects_title) preferenceCategoryAudioEffects.title = getString(R.string.pref_audio_effects_title)
@@ -295,7 +335,9 @@ class SettingsFragment : PreferenceFragmentCompat(), YesNoDialog.YesNoDialogList
preferenceCategoryAudioEffects.addPreference(preferenceBassBoost) preferenceCategoryAudioEffects.addPreference(preferenceBassBoost)
preferenceCategoryAudioEffects.addPreference(preferenceReverb) preferenceCategoryAudioEffects.addPreference(preferenceReverb)
preferenceCategoryAudioEffects.addPreference(preferenceDrc) preferenceCategoryAudioEffects.addPreference(preferenceDrc)
preferenceCategoryAudioEffects.addPreference(preferencePresetSelection)
preferenceCategoryAudioEffects.addPreference(preferenceEqualizer) preferenceCategoryAudioEffects.addPreference(preferenceEqualizer)
preferenceCategoryAudioEffects.addPreference(preferenceVisualizer)
screen.addPreference(preferenceCategoryMaintenance) screen.addPreference(preferenceCategoryMaintenance)
preferenceCategoryMaintenance.addPreference(preferenceUpdateStationImages) 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 */ /* Overrides onYesNoDialog from YesNoDialogListener */
override fun onYesNoDialog( override fun onYesNoDialog(
type: Int, 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> { fun MediaController.requestMetadataHistory(): ListenableFuture<SessionResult> {
return sendCustomCommand( return sendCustomCommand(
SessionCommand(Keys.CMD_REQUEST_METADATA_HISTORY, Bundle.EMPTY), 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 */ /* Starts playback with a new media item */
fun MediaController.play(context: Context, station: Station) { 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 package com.michatec.radio.helpers
import android.util.Log
import androidx.annotation.OptIn import androidx.annotation.OptIn
import androidx.media3.common.C import androidx.media3.common.C
import androidx.media3.common.audio.AudioProcessor import androidx.media3.common.audio.AudioProcessor
@@ -13,46 +14,117 @@ import java.nio.ByteOrder
class NativeAudioProcessor : BaseAudioProcessor() { class NativeAudioProcessor : BaseAudioProcessor() {
companion object { companion object {
private const val TAG = "NativeAudioProcessor"
init { 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 setDrcEnabled(enabled: Boolean)
private external fun setReverbMix(mix: Float) private external fun setReverbMix(mix: Float)
private external fun setEqBand(band: Int, gainDb: Float) private external fun setEqBand(band: Int, gainDb: Float)
private external fun setBassBoost(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 enableDrc(enabled: Boolean) = setDrcEnabled(enabled)
fun setReverb(mix: Float) = setReverbMix(mix) fun setReverb(mix: Float) = setReverbMix(mix)
fun setEq(band: Int, gainDb: Float) = setEqBand(band, gainDb) 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 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 { 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) { if (inputAudioFormat.encoding != C.ENCODING_PCM_16BIT) {
Log.e(TAG, "Unsupported encoding: ${inputAudioFormat.encoding}")
throw AudioProcessor.UnhandledAudioFormatException(inputAudioFormat) throw AudioProcessor.UnhandledAudioFormatException(inputAudioFormat)
} }
return inputAudioFormat return inputAudioFormat
} }
override fun queueInput(inputBuffer: ByteBuffer) { override fun queueInput(inputBuffer: ByteBuffer) {
val remaining = inputBuffer.remaining() val size = inputBuffer.remaining()
if (remaining == 0) return if (size == 0) return
val shortArraySize = remaining / 2 // Always ensure we have a direct buffer for JNI
val shortArray = ShortArray(shortArraySize) 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) override fun onReset() {
outputBuffer.asShortBuffer().put(shortArray) super.onReset()
outputBuffer.limit(remaining) 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 */ /* Loads Bass Boost gain */
fun loadBassBoost(): Float { 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 */ /* Loads Reverb mix */
fun loadReverb(): Float { 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) return sharedPreferences.getBoolean(Keys.PREF_DRC, false)
} }
/* Loads EQ gains */ /* Loads all EQ bands (10 bands for full range) */
fun loadEqLow(): Float = sharedPreferences.getInt(Keys.PREF_EQ_LOW, 0).toFloat() fun loadEqBand(band: Int): Int {
fun loadEqMid(): Float = sharedPreferences.getInt(Keys.PREF_EQ_MID, 0).toFloat() return when (band) {
fun loadEqHigh(): Float = sharedPreferences.getInt(Keys.PREF_EQ_HIGH, 0).toFloat() 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 */ /* Resets Equalizer settings to default */
fun resetEqualizer() { 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 <action
android:id="@+id/action_settings_to_equalizer" android:id="@+id/action_settings_to_equalizer"
app:destination="@id/equalizer_destination" /> app:destination="@id/equalizer_destination" />
<action
android:id="@+id/action_settings_to_visualizer"
app:destination="@id/visualizer_destination" />
</fragment> </fragment>
<!-- EQUALIZER --> <!-- EQUALIZER -->
@@ -31,4 +34,9 @@
android:name="com.michatec.radio.EqualizerFragment" android:name="com.michatec.radio.EqualizerFragment"
android:label="Equalizer" /> android:label="Equalizer" />
<!-- VISUALIZER -->
<fragment
android:id="@+id/visualizer_destination"
android:name="com.michatec.radio.VisualizerFragment"
android:label="Visualizer" />
</navigation> </navigation>
+20 -3
View File
@@ -55,7 +55,6 @@
<string name="pref_update_collection_summary">Die neueste Version aller Senderinformationen herunterladen.</string> <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_message_update_collection">Die neueste Version aller Senderinformationen herunterladen?</string>
<string name="dialog_yes_no_positive_button_update_collection">Aktualisieren</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_advanced_title">Erweitert</string>
<string name="pref_app_version_summary">Version</string> <string name="pref_app_version_summary">Version</string>
<string name="pref_app_version_title">App-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_reverb_summary">Reverb-Mix anpassen.</string>
<string name="pref_drc_title">Dynamikkompression</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_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_low_title">Equalizer: 31 Hz</string>
<string name="pref_eq_mid_title">Equalizer: Mitten</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_title">Equalizer</string>
<string name="pref_equalizer_summary">Passen Sie die Audio-Frequenzen an.</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_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> </resources>
+20 -3
View File
@@ -70,12 +70,27 @@
<string name="pref_reverb_summary">Adjust reverb mix.</string> <string name="pref_reverb_summary">Adjust reverb mix.</string>
<string name="pref_drc_title">Dynamic Range Compression</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_drc_summary">Compress dynamic range for consistent volume.</string>
<string name="pref_eq_low_title">Equalizer: Low</string> <string name="pref_eq_low_title">Equalizer: 31 Hz</string>
<string name="pref_eq_mid_title">Equalizer: Mid</string> <string name="pref_eq_mid_title">Equalizer: 125 Hz</string>
<string name="pref_eq_high_title">Equalizer: High</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_title">Equalizer</string>
<string name="pref_equalizer_summary">Adjust audio frequencies</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_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_advanced_title">Advanced</string>
<string name="pref_app_version_summary">Version</string> <string name="pref_app_version_summary">Version</string>
<string name="pref_app_version_title">App Version</string> <string name="pref_app_version_title">App Version</string>
@@ -158,4 +173,6 @@
<!-- Extras --> <!-- Extras -->
<string name="loading">Loading...</string> <string name="loading">Loading...</string>
<string name="media_route_menu_title">Cast</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> </resources>