mirror of
https://github.com/Michatec/Radio.git
synced 2026-05-31 04:22:40 +02:00
Compare commits
8 Commits
9d47684f13
...
2e8cc9b243
| Author | SHA1 | Date | |
|---|---|---|---|
| 2e8cc9b243 | |||
| 9e1219549e | |||
| 17ba1c268a | |||
| b328af5c3a | |||
| d31d476cb5 | |||
| 6bb34cd707 | |||
| 883a4443e9 | |||
| 8ba22a4c09 |
@@ -12,6 +12,7 @@ env:
|
|||||||
ANDROID_HOME: /usr/local/lib/android/sdk/
|
ANDROID_HOME: /usr/local/lib/android/sdk/
|
||||||
APK_PATH: app/build/outputs/apk/release/Radio.apk
|
APK_PATH: app/build/outputs/apk/release/Radio.apk
|
||||||
APKSIGNER: /usr/local/lib/android/sdk/build-tools/34.0.0/apksigner
|
APKSIGNER: /usr/local/lib/android/sdk/build-tools/34.0.0/apksigner
|
||||||
|
ZIPALIGN: /usr/local/lib/android/sdk/build-tools/34.0.0/zipalign
|
||||||
|
|
||||||
jobs:
|
jobs:
|
||||||
build:
|
build:
|
||||||
@@ -42,7 +43,7 @@ jobs:
|
|||||||
#if: steps.cache-android-sdk.outputs.cache-hit != 'true'
|
#if: steps.cache-android-sdk.outputs.cache-hit != 'true'
|
||||||
uses: android-actions/setup-android@v4
|
uses: android-actions/setup-android@v4
|
||||||
with:
|
with:
|
||||||
packages: ''
|
packages: 'ndk;29.0.14206865'
|
||||||
|
|
||||||
- name: Grant execute permission for gradlew
|
- name: Grant execute permission for gradlew
|
||||||
run: chmod +x ./gradlew
|
run: chmod +x ./gradlew
|
||||||
@@ -55,7 +56,7 @@ jobs:
|
|||||||
|
|
||||||
- name: Zipalign APK
|
- name: Zipalign APK
|
||||||
run: |
|
run: |
|
||||||
/usr/local/lib/android/sdk/build-tools/34.0.0/zipalign -v -p 4 ${{ env.APK_PATH }} app-release-aligned.apk
|
${{ env.ZIPALIGN }} -v -p 4 ${{ env.APK_PATH }} app-release-aligned.apk
|
||||||
|
|
||||||
- name: Sign APK
|
- name: Sign APK
|
||||||
env:
|
env:
|
||||||
|
|||||||
Vendored
+16
@@ -0,0 +1,16 @@
|
|||||||
|
{
|
||||||
|
"configurations": [
|
||||||
|
{
|
||||||
|
"name": "Android",
|
||||||
|
"includePath": [
|
||||||
|
"${workspaceFolder}/**"
|
||||||
|
],
|
||||||
|
"defines": [
|
||||||
|
"_DEBUG",
|
||||||
|
"UNICODE",
|
||||||
|
"_UNICODE"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"version": 4
|
||||||
|
}
|
||||||
Vendored
+3
@@ -0,0 +1,3 @@
|
|||||||
|
{
|
||||||
|
"C_Cpp.default.compilerPath": ""
|
||||||
|
}
|
||||||
@@ -9,7 +9,7 @@
|
|||||||
### ℹ️ About Radio
|
### ℹ️ About Radio
|
||||||
**Radio is an application with a minimalist approach to listening to radio over the Internet.** <br>
|
**Radio is an application with a minimalist approach to listening to radio over the Internet.** <br>
|
||||||
**Radio only offers a very basic search option, and it imports audio streaming links when you tap them in a web browser.** <br>
|
**Radio only offers a very basic search option, and it imports audio streaming links when you tap them in a web browser.** <br>
|
||||||
**Radio now also supports Android TV (Beta).** <br>
|
**Radio now also supports Android TV (Beta) and Cast to Devices (Beta).** <br>
|
||||||
**Pull request are welcome at any time.**<br>
|
**Pull request are welcome at any time.**<br>
|
||||||
|
|
||||||
**Radio is free software. It is released under the [GPLv3 open source license](https://opensource.org/licenses/GPL-3.0).**
|
**Radio is free software. It is released under the [GPLv3 open source license](https://opensource.org/licenses/GPL-3.0).**
|
||||||
|
|||||||
@@ -54,6 +54,7 @@ android {
|
|||||||
proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro'
|
proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro'
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
ndkVersion "29.0.14206865"
|
||||||
externalNativeBuild {
|
externalNativeBuild {
|
||||||
cmake {
|
cmake {
|
||||||
path file('src/main/cpp/CMakeLists.txt')
|
path file('src/main/cpp/CMakeLists.txt')
|
||||||
|
|||||||
+237
-79
@@ -3,6 +3,7 @@
|
|||||||
#include <cmath>
|
#include <cmath>
|
||||||
#include <complex>
|
#include <complex>
|
||||||
#include <array>
|
#include <array>
|
||||||
|
#include <atomic>
|
||||||
|
|
||||||
#if defined(__ARM_NEON)
|
#if defined(__ARM_NEON)
|
||||||
#include <arm_neon.h>
|
#include <arm_neon.h>
|
||||||
@@ -12,72 +13,89 @@
|
|||||||
#define M_PI 3.14159265358979323846
|
#define M_PI 3.14159265358979323846
|
||||||
#endif
|
#endif
|
||||||
|
|
||||||
static constexpr int FFT_SIZE = 512;
|
static std::atomic<float> gSampleRate(44100.0f);
|
||||||
|
static constexpr int FFT_SIZE = 2048;
|
||||||
static constexpr int NUM_EQ_BANDS = 10;
|
static constexpr int NUM_EQ_BANDS = 10;
|
||||||
static constexpr float INV_32768 = 1.0f / 32768.0f;
|
static constexpr float INV_32768 = 1.0f / 32768.0f;
|
||||||
static constexpr float SQRT_2_INV = 0.70710678f;
|
static constexpr float SQRT_2_INV = 0.70710678f;
|
||||||
static constexpr float DENORMAL_OFFSET = 1e-18f;
|
static constexpr float DENORMAL_OFFSET = 1e-18f;
|
||||||
|
static constexpr float INTERPOLATION_SPEED = 0.1f;
|
||||||
|
|
||||||
static constexpr std::array<float, NUM_EQ_BANDS> EQ_FREQUENCIES = {
|
static constexpr std::array<float, NUM_EQ_BANDS> EQ_FREQUENCIES = {
|
||||||
31.25f, 62.5f, 125.0f, 250.0f, 500.0f,
|
31.25f, 62.5f, 125.0f, 250.0f, 500.0f,
|
||||||
1000.0f, 2000.0f, 4000.0f, 8000.0f, 16000.0f
|
1000.0f, 2000.0f, 4000.0f, 8000.0f, 16000.0f
|
||||||
};
|
};
|
||||||
|
|
||||||
struct alignas(16) BiquadBank {
|
struct alignas(16) EqBandInterpolator {
|
||||||
alignas(16) std::array<float, NUM_EQ_BANDS> a0{}, a1{}, a2{}, b1{}, b2{};
|
std::atomic<float> targetGain{0.0f};
|
||||||
alignas(16) std::array<float, NUM_EQ_BANDS> z1{}, z2{};
|
std::atomic<float> currentGain{0.0f};
|
||||||
uint16_t activeMask = 0;
|
float a0 = 1.0f, a1 = 0.0f, a2 = 0.0f, b1 = 0.0f, b2 = 0.0f;
|
||||||
|
float z1 = 0.0f, z2 = 0.0f;
|
||||||
|
bool active = false;
|
||||||
|
|
||||||
[[nodiscard]] inline bool hasActiveBands() const { return activeMask != 0; }
|
inline void setTargetGain(float g) { targetGain.store(g, std::memory_order_release); }
|
||||||
|
|
||||||
inline void setBandActive(int band, bool active) {
|
inline void updateInterpolation() {
|
||||||
if (active) activeMask |= (1 << band);
|
float target = targetGain.load(std::memory_order_acquire);
|
||||||
else activeMask &= ~(1 << band);
|
float current = currentGain.load(std::memory_order_relaxed);
|
||||||
}
|
float diff = target - current;
|
||||||
|
if (std::abs(diff) > 0.001f) {
|
||||||
inline void processBlock(float* __restrict__ data, int count) {
|
currentGain.store(current + diff * INTERPOLATION_SPEED, std::memory_order_release);
|
||||||
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) {
|
inline float process(float x) {
|
||||||
if (band < 0 || band >= NUM_EQ_BANDS) return;
|
if (!active) return x;
|
||||||
const bool active = std::abs(g) > 0.1f;
|
updateInterpolation();
|
||||||
setBandActive(band, active);
|
float g = currentGain.load(std::memory_order_acquire);
|
||||||
if (!active) return;
|
if (std::abs(g) < 0.01f) return x;
|
||||||
|
float y = x * a0 + z1;
|
||||||
|
z1 = x * a1 + z2 - b1 * y + DENORMAL_OFFSET;
|
||||||
|
z2 = x * a2 - b2 * y;
|
||||||
|
return y;
|
||||||
|
}
|
||||||
|
|
||||||
|
inline void setCoefficients(float sr, float f, float g, float bw) {
|
||||||
|
const bool isActive = std::abs(g) > 0.1f;
|
||||||
|
active = isActive;
|
||||||
|
if (!isActive) return;
|
||||||
const float A = powf(10.0f, g / 40.0f);
|
const float A = powf(10.0f, g / 40.0f);
|
||||||
const float w = 2.0f * static_cast<float>(M_PI) * f / sr;
|
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 alpha = sinf(w) * sinhf(logf(2.0f) / 2.0f * bw * w / sinf(w));
|
||||||
const float c = cosf(w);
|
const float c = cosf(w);
|
||||||
const float a0_raw = 1.0f + alpha / A;
|
const float a0_raw = 1.0f + alpha / A;
|
||||||
const float invA0 = 1.0f / a0_raw;
|
const float invA0 = 1.0f / a0_raw;
|
||||||
a0[band] = (1.0f + alpha * A) * invA0;
|
a0 = (1.0f + alpha * A) * invA0;
|
||||||
a1[band] = (-2.0f * c) * invA0;
|
a1 = (-2.0f * c) * invA0;
|
||||||
a2[band] = (1.0f - alpha * A) * invA0;
|
a2 = (1.0f - alpha * A) * invA0;
|
||||||
b1[band] = (-2.0f * c) * invA0;
|
b1 = (-2.0f * c) * invA0;
|
||||||
b2[band] = (1.0f - alpha / A) * invA0;
|
b2 = (1.0f - alpha / A) * invA0;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
inline void clearState() { z1 = 0.0f; z2 = 0.0f; }
|
||||||
};
|
};
|
||||||
|
|
||||||
struct alignas(16) BassFilter {
|
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 a0 = 1.2f, a1 = 1.2f, a2 = 1.2f, b1 = 0.0f, b2 = 0.0f;
|
||||||
alignas(16) float z1 = 0.0f, z2 = 0.0f;
|
alignas(16) float z1 = 0.0f, z2 = 0.0f;
|
||||||
bool active = false;
|
std::atomic<bool> active{false};
|
||||||
|
std::atomic<float> targetGain{0.0f};
|
||||||
|
std::atomic<float> currentGain{0.0f};
|
||||||
|
|
||||||
|
inline void updateInterpolation() {
|
||||||
|
float target = targetGain.load(std::memory_order_acquire);
|
||||||
|
float current = currentGain.load(std::memory_order_relaxed);
|
||||||
|
float diff = target - current;
|
||||||
|
if (std::abs(diff) > 0.001f) {
|
||||||
|
currentGain.store(current + diff * INTERPOLATION_SPEED, std::memory_order_release);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
inline float process(float x) {
|
inline float process(float x) {
|
||||||
if (!active) return x;
|
if (!active.load(std::memory_order_acquire)) return x;
|
||||||
|
updateInterpolation();
|
||||||
|
float g = currentGain.load(std::memory_order_acquire);
|
||||||
|
if (std::abs(g) < 0.01f) return x;
|
||||||
float y = x * a0 + z1;
|
float y = x * a0 + z1;
|
||||||
z1 = x * a1 + z2 - b1 * y + DENORMAL_OFFSET;
|
z1 = x * a1 + z2 - b1 * y + DENORMAL_OFFSET;
|
||||||
z2 = x * a2 - b2 * y;
|
z2 = x * a2 - b2 * y;
|
||||||
@@ -85,9 +103,7 @@ struct alignas(16) BassFilter {
|
|||||||
return y;
|
return y;
|
||||||
}
|
}
|
||||||
|
|
||||||
void setLowShelf(float sr,float f,float g,float q){
|
void setCoefficients(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 A=powf(10.0f,g/40.0f);
|
||||||
float w=2.0f*static_cast<float>(M_PI)*f/sr;
|
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 alpha=sinf(w)/2.0f*sqrtf((A+1.0f/A)*(1.0f/q-1.0f)+2.0f);
|
||||||
@@ -100,6 +116,11 @@ struct alignas(16) BassFilter {
|
|||||||
b1=-2.0f*((A-1.0f)+(A+1.0f)*c)*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;
|
b2=((A+1.0f)+(A-1.0f)*c-2.0f*sqrtA*alpha)*invA0;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
void applyGain(float sr) {
|
||||||
|
float g = currentGain.load(std::memory_order_acquire);
|
||||||
|
setCoefficients(sr, 150.0f, g, SQRT_2_INV);
|
||||||
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
template<int SIZE>
|
template<int SIZE>
|
||||||
@@ -115,11 +136,11 @@ class ReverbOptimized {
|
|||||||
std::array<CircularBuffer<1116>, 4> combs;
|
std::array<CircularBuffer<1116>, 4> combs;
|
||||||
std::array<CircularBuffer<556>, 2> allpasses;
|
std::array<CircularBuffer<556>, 2> allpasses;
|
||||||
std::array<float, 4> combFeedback = {0.841f, 0.815f, 0.796f, 0.771f};
|
std::array<float, 4> combFeedback = {0.841f, 0.815f, 0.796f, 0.771f};
|
||||||
float mix = 0.0f;
|
|
||||||
public:
|
public:
|
||||||
inline void setMix(float m) { mix = m; }
|
std::atomic<float> mix{0.0f};
|
||||||
inline float process(float x) {
|
inline float process(float x) {
|
||||||
if (mix < 0.01f) return x;
|
float m = mix.load(std::memory_order_acquire);
|
||||||
|
if (m < 0.01f) return x;
|
||||||
float out = 0.0f;
|
float out = 0.0f;
|
||||||
#pragma GCC unroll 4
|
#pragma GCC unroll 4
|
||||||
for (int i = 0; i < 4; i++) {
|
for (int i = 0; i < 4; i++) {
|
||||||
@@ -136,10 +157,11 @@ public:
|
|||||||
allpasses[i].advance();
|
allpasses[i].advance();
|
||||||
out = xOut;
|
out = xOut;
|
||||||
}
|
}
|
||||||
return x * (1.0f - mix) + out * mix;
|
return x * (1.0f - m) + out * m;
|
||||||
}
|
}
|
||||||
inline void processBlock(float* __restrict__ left, float* __restrict__ right, int count) {
|
inline void processBlock(float* __restrict__ left, float* __restrict__ right, int count) {
|
||||||
if (mix < 0.01f) return;
|
float m = mix.load(std::memory_order_acquire);
|
||||||
|
if (m < 0.01f) return;
|
||||||
for (int i = 0; i < count; i++) {
|
for (int i = 0; i < count; i++) {
|
||||||
left[i] = process(left[i]);
|
left[i] = process(left[i]);
|
||||||
right[i] = process(right[i]);
|
right[i] = process(right[i]);
|
||||||
@@ -149,7 +171,9 @@ public:
|
|||||||
|
|
||||||
class CompressorOptimized {
|
class CompressorOptimized {
|
||||||
public:
|
public:
|
||||||
float threshold = 0.3f, ratio = 4.0f, attack = 0.08f, release = 0.8f, sampleRate = 44100.0f;
|
std::atomic<float> threshold{0.3f}, ratio{4.0f}, attack{0.08f}, release{0.8f};
|
||||||
|
std::atomic<float> sampleRate{44100.0f};
|
||||||
|
std::atomic<bool> enabled{false};
|
||||||
private:
|
private:
|
||||||
float envelopeL = 0.0f, envelopeR = 0.0f;
|
float envelopeL = 0.0f, envelopeR = 0.0f;
|
||||||
float attackCoef = 0.0f, releaseCoef = 0.0f;
|
float attackCoef = 0.0f, releaseCoef = 0.0f;
|
||||||
@@ -157,34 +181,35 @@ private:
|
|||||||
public:
|
public:
|
||||||
inline void updateCoefficients() {
|
inline void updateCoefficients() {
|
||||||
if (coefficientsValid) return;
|
if (coefficientsValid) return;
|
||||||
attackCoef = expf(-1.0f / (attack * sampleRate));
|
float a = attack.load(std::memory_order_acquire);
|
||||||
releaseCoef = expf(-1.0f / (release * sampleRate));
|
float r = release.load(std::memory_order_acquire);
|
||||||
|
float sr = sampleRate.load(std::memory_order_acquire);
|
||||||
|
attackCoef = expf(-1.0f / (a * sr));
|
||||||
|
releaseCoef = expf(-1.0f / (r * sr));
|
||||||
coefficientsValid = true;
|
coefficientsValid = true;
|
||||||
}
|
}
|
||||||
inline void processBlock(float* __restrict__ buffer, int count, float& envelope) {
|
inline void processBlock(float* __restrict__ buffer, int count, float& envelope) {
|
||||||
updateCoefficients();
|
updateCoefficients();
|
||||||
|
float th = threshold.load(std::memory_order_acquire);
|
||||||
|
float rt = ratio.load(std::memory_order_acquire);
|
||||||
for(int i=0; i<count; i++){
|
for(int i=0; i<count; i++){
|
||||||
float absInput = fabsf(buffer[i]);
|
float absInput = fabsf(buffer[i]);
|
||||||
envelope = (absInput > envelope) ? attackCoef*envelope + (1-attackCoef)*absInput : releaseCoef*envelope + (1-releaseCoef)*absInput;
|
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;
|
float gain = (envelope>th)? (th + (envelope-th)/rt)/(envelope+1e-9f) : 1.0f;
|
||||||
buffer[i]*=gain;
|
buffer[i]*=gain;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
inline void process(float* __restrict__ left, float* __restrict__ right, int count) {
|
inline void process(float* __restrict__ left, float* __restrict__ right, int count) {
|
||||||
|
if (!enabled.load(std::memory_order_acquire)) return;
|
||||||
processBlock(left, count, envelopeL);
|
processBlock(left, count, envelopeL);
|
||||||
processBlock(right, count, envelopeR);
|
processBlock(right, count, envelopeR);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
CompressorOptimized gCompressor;
|
static std::atomic<bool> gEqEnabled{false};
|
||||||
ReverbOptimized gReverbL, gReverbR;
|
static std::atomic<float> gStereoWidth{1.0f};
|
||||||
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, 4096> gLeftBuf, gRightBuf;
|
||||||
alignas(16) std::array<float, 256> gFFTData;
|
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) {
|
inline void fastFFT(std::complex<float>* __restrict__ data, int n) {
|
||||||
for (int i = 1, j = 0; i < n; i++) {
|
for (int i = 1, j = 0; i < n; i++) {
|
||||||
@@ -209,6 +234,20 @@ inline void fastFFT(std::complex<float>* __restrict__ data, int n) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
inline void applyHannWindow(float* __restrict__ data, int size) {
|
||||||
|
for (int i = 0; i < size; i++) {
|
||||||
|
float window = 0.5f * (1.0f - cosf(2.0f * static_cast<float>(M_PI) * i / (size - 1)));
|
||||||
|
data[i] *= window;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
inline void applyHannWindowToReal(std::complex<float>* __restrict__ data, int size) {
|
||||||
|
for (int i = 0; i < size; i++) {
|
||||||
|
float window = 0.5f * (1.0f - cosf(2.0f * static_cast<float>(M_PI) * i / (size - 1)));
|
||||||
|
data[i] = std::complex<float>(data[i].real() * window, data[i].imag());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
inline float fastSoftClip(float x) {
|
inline float fastSoftClip(float x) {
|
||||||
float ax = fabsf(x);
|
float ax = fabsf(x);
|
||||||
float sign = x > 0 ? 1.0f : -1.0f;
|
float sign = x > 0 ? 1.0f : -1.0f;
|
||||||
@@ -216,25 +255,86 @@ inline float fastSoftClip(float x) {
|
|||||||
return x * (1.5f - 0.5f * x * x);
|
return x * (1.5f - 0.5f * x * x);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
static EqBandInterpolator gEqL[NUM_EQ_BANDS];
|
||||||
|
static EqBandInterpolator gEqR[NUM_EQ_BANDS];
|
||||||
|
static BassFilter gBassL, gBassR;
|
||||||
|
static CompressorOptimized gCompressor;
|
||||||
|
static ReverbOptimized gReverbL, gReverbR;
|
||||||
|
static alignas(16) std::array<std::complex<float>, FFT_SIZE> gFFTWork;
|
||||||
|
static int gEqUpdateCounter = 0;
|
||||||
|
|
||||||
|
inline void updateAllEqBands() {
|
||||||
|
float sr = gSampleRate.load(std::memory_order_acquire);
|
||||||
|
for (int b = 0; b < NUM_EQ_BANDS; b++) {
|
||||||
|
float g = gEqL[b].targetGain.load(std::memory_order_acquire);
|
||||||
|
gEqL[b].setCoefficients(sr, EQ_FREQUENCIES[b], g, 1.0f);
|
||||||
|
gEqR[b].setCoefficients(sr, EQ_FREQUENCIES[b], g, 1.0f);
|
||||||
|
}
|
||||||
|
bool anyActive = false;
|
||||||
|
for (int b = 0; b < NUM_EQ_BANDS; b++) {
|
||||||
|
if (std::abs(gEqL[b].targetGain.load(std::memory_order_acquire)) > 0.1f) {
|
||||||
|
anyActive = true;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
gEqEnabled.store(anyActive, std::memory_order_release);
|
||||||
|
}
|
||||||
|
|
||||||
extern "C" {
|
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_setSampleRate(JNIEnv*, jobject, jfloat sr) {
|
||||||
JNIEXPORT void JNICALL Java_com_michatec_radio_helpers_NativeAudioProcessor_setReverbMix(JNIEnv*, jobject, jfloat m) { gReverbL.setMix(m); gReverbR.setMix(m); }
|
gSampleRate.store(sr, std::memory_order_release);
|
||||||
|
gCompressor.sampleRate.store(sr, std::memory_order_release);
|
||||||
|
gBassL.applyGain(sr);
|
||||||
|
gBassR.applyGain(sr);
|
||||||
|
gEqUpdateCounter = 1;
|
||||||
|
}
|
||||||
|
JNIEXPORT void JNICALL Java_com_michatec_radio_helpers_NativeAudioProcessor_setDrcEnabled(JNIEnv*, jobject, jboolean e) {
|
||||||
|
gCompressor.enabled.store(e == JNI_TRUE, std::memory_order_release);
|
||||||
|
}
|
||||||
|
JNIEXPORT void JNICALL Java_com_michatec_radio_helpers_NativeAudioProcessor_setReverbMix(JNIEnv*, jobject, jfloat m) {
|
||||||
|
gReverbL.mix.store(m, std::memory_order_release);
|
||||||
|
gReverbR.mix.store(m, std::memory_order_release);
|
||||||
|
}
|
||||||
JNIEXPORT void JNICALL Java_com_michatec_radio_helpers_NativeAudioProcessor_setEqBand(JNIEnv*, jobject, jint b, jfloat g) {
|
JNIEXPORT void JNICALL Java_com_michatec_radio_helpers_NativeAudioProcessor_setEqBand(JNIEnv*, jobject, jint b, jfloat g) {
|
||||||
if (b >= 0 && b < NUM_EQ_BANDS) {
|
if (b >= 0 && b < NUM_EQ_BANDS) {
|
||||||
gEqL.setPeakingEQ(b, 44100.0f, EQ_FREQUENCIES[b], g, 1.0f);
|
gEqL[b].setTargetGain(g);
|
||||||
gEqR.setPeakingEQ(b, 44100.0f, EQ_FREQUENCIES[b], g, 1.0f);
|
gEqR[b].setTargetGain(g);
|
||||||
|
gEqUpdateCounter = 1;
|
||||||
}
|
}
|
||||||
gEqEnabled = gEqL.hasActiveBands();
|
}
|
||||||
|
JNIEXPORT void JNICALL Java_com_michatec_radio_helpers_NativeAudioProcessor_setEqFull(JNIEnv* env, jobject thiz, jfloatArray gains) {
|
||||||
|
if (!gains) return;
|
||||||
|
|
||||||
|
jsize len = env->GetArrayLength(gains);
|
||||||
|
int bandsToUpdate = std::min(static_cast<int>(len), NUM_EQ_BANDS);
|
||||||
|
|
||||||
|
jfloat* gainsPtr = env->GetFloatArrayElements(gains, nullptr);
|
||||||
|
if (!gainsPtr) return;
|
||||||
|
|
||||||
|
for (int b = 0; b < bandsToUpdate; b++) {
|
||||||
|
gEqL[b].setTargetGain(gainsPtr[b]);
|
||||||
|
gEqR[b].setTargetGain(gainsPtr[b]);
|
||||||
|
}
|
||||||
|
|
||||||
|
gEqUpdateCounter = 1;
|
||||||
|
|
||||||
|
env->ReleaseFloatArrayElements(gains, gainsPtr, JNI_ABORT);
|
||||||
}
|
}
|
||||||
JNIEXPORT void JNICALL Java_com_michatec_radio_helpers_NativeAudioProcessor_setBassBoost(JNIEnv*, jobject, jfloat g) {
|
JNIEXPORT void JNICALL Java_com_michatec_radio_helpers_NativeAudioProcessor_setBassBoost(JNIEnv*, jobject, jfloat g) {
|
||||||
if (g > 0.01f) {
|
gBassL.targetGain.store(g, std::memory_order_release);
|
||||||
gBassL.setLowShelf(44100.0f, 150.0f, g, SQRT_2_INV);
|
gBassR.targetGain.store(g, std::memory_order_release);
|
||||||
gBassR.setLowShelf(44100.0f, 150.0f, g, SQRT_2_INV);
|
if (std::abs(g) > 0.01f) {
|
||||||
gBassBoostEnabled = true;
|
gBassL.active.store(true, std::memory_order_release);
|
||||||
} else { gBassBoostEnabled = false; }
|
gBassR.active.store(true, std::memory_order_release);
|
||||||
|
} else {
|
||||||
|
gBassL.active.store(false, std::memory_order_release);
|
||||||
|
gBassR.active.store(false, std::memory_order_release);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
JNIEXPORT void JNICALL Java_com_michatec_radio_helpers_NativeAudioProcessor_setStereoWidth(JNIEnv*, jobject, jfloat w) {
|
||||||
|
gStereoWidth.store(fmaxf(0.0f, fminf(w, 2.0f)), std::memory_order_release);
|
||||||
}
|
}
|
||||||
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) {
|
JNIEXPORT jfloatArray JNICALL Java_com_michatec_radio_helpers_NativeAudioProcessor_getFftData(JNIEnv* env, jobject) {
|
||||||
jfloatArray arr = env->NewFloatArray(256);
|
jfloatArray arr = env->NewFloatArray(256);
|
||||||
@@ -242,42 +342,100 @@ JNIEXPORT jfloatArray JNICALL Java_com_michatec_radio_helpers_NativeAudioProcess
|
|||||||
return arr;
|
return arr;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
inline void computeLogarithmicFFT(float* output, const std::complex<float>* input, int inputSize) {
|
||||||
|
float sr = gSampleRate.load(std::memory_order_acquire);
|
||||||
|
float binWidth = sr / (2.0f * inputSize);
|
||||||
|
constexpr int NUM_BANDS = 256;
|
||||||
|
constexpr float MIN_FREQ = 20.0f;
|
||||||
|
constexpr float MAX_FREQ = 20000.0f;
|
||||||
|
float logMin = logf(MIN_FREQ);
|
||||||
|
float logMax = logf(MAX_FREQ);
|
||||||
|
float logRange = logMax - logMin;
|
||||||
|
|
||||||
|
for (int b = 0; b < NUM_BANDS; b++) {
|
||||||
|
float f1 = expf(logMin + (logRange * b / NUM_BANDS));
|
||||||
|
float f2 = expf(logMin + (logRange * (b + 1) / NUM_BANDS));
|
||||||
|
int idx1 = static_cast<int>(f1 / binWidth);
|
||||||
|
int idx2 = static_cast<int>(f2 / binWidth);
|
||||||
|
idx1 = std::max(0, std::min(idx1, inputSize - 1));
|
||||||
|
idx2 = std::max(0, std::min(idx2, inputSize - 1));
|
||||||
|
|
||||||
|
float sum = 0.0f;
|
||||||
|
int count = idx2 - idx1 + 1;
|
||||||
|
for (int i = idx1; i <= idx2 && i < inputSize; i++) {
|
||||||
|
sum += std::abs(input[i]);
|
||||||
|
}
|
||||||
|
float avg = (count > 0) ? sum / static_cast<float>(count) : 0.0f;
|
||||||
|
output[b] = avg * 0.5f;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
JNIEXPORT void JNICALL Java_com_michatec_radio_helpers_NativeAudioProcessor_processAudioDirect(JNIEnv* env, jobject, jobject byteBuffer, jint size) {
|
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));
|
auto* buffer = static_cast<jshort*>(env->GetDirectBufferAddress(byteBuffer));
|
||||||
if (!buffer) return;
|
if (!buffer) return;
|
||||||
int numFrames = (size / 2) / 2;
|
int numFrames = (size / 2) / 2;
|
||||||
if (numFrames > 4096) numFrames = 4096;
|
if (numFrames > 4096) numFrames = 4096;
|
||||||
|
|
||||||
|
if (gEqUpdateCounter > 0) {
|
||||||
|
updateAllEqBands();
|
||||||
|
gEqUpdateCounter--;
|
||||||
|
}
|
||||||
|
|
||||||
for (int i = 0; i < numFrames; i++) {
|
for (int i = 0; i < numFrames; i++) {
|
||||||
gLeftBuf[i] = static_cast<float>(buffer[i * 2]) * INV_32768;
|
gLeftBuf[i] = static_cast<float>(buffer[i * 2]) * INV_32768;
|
||||||
gRightBuf[i] = static_cast<float>(buffer[i * 2 + 1]) * 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); }
|
bool eqEnabled = gEqEnabled.load(std::memory_order_acquire);
|
||||||
if (gBassBoostEnabled) {
|
if (eqEnabled) {
|
||||||
for(int i=0; i<numFrames; i++) { gLeftBuf[i] = gBassL.process(gLeftBuf[i]); gRightBuf[i] = gBassR.process(gRightBuf[i]); }
|
for (int i = 0; i < numFrames; i++) {
|
||||||
|
float xL = gLeftBuf[i];
|
||||||
|
float xR = gRightBuf[i];
|
||||||
|
for (int b = 0; b < NUM_EQ_BANDS; b++) {
|
||||||
|
xL = gEqL[b].process(xL);
|
||||||
|
xR = gEqR[b].process(xR);
|
||||||
}
|
}
|
||||||
|
gLeftBuf[i] = xL;
|
||||||
|
gRightBuf[i] = xR;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
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);
|
gReverbL.processBlock(gLeftBuf.data(), gRightBuf.data(), numFrames);
|
||||||
|
|
||||||
if (gStereoWidth != 1.0f) {
|
float stereoWidth = gStereoWidth.load(std::memory_order_acquire);
|
||||||
float halfWidth = gStereoWidth * 0.5f;
|
if (stereoWidth != 1.0f) {
|
||||||
|
float halfWidth = stereoWidth * 0.5f;
|
||||||
for (int j = 0; j < numFrames; j++) {
|
for (int j = 0; j < numFrames; j++) {
|
||||||
float mid = (gLeftBuf[j] + gRightBuf[j]) * 0.5f;
|
float mid = (gLeftBuf[j] + gRightBuf[j]) * 0.5f;
|
||||||
float side = (gLeftBuf[j] - gRightBuf[j]) * halfWidth;
|
float side = (gLeftBuf[j] - gRightBuf[j]) * halfWidth;
|
||||||
gLeftBuf[j] = mid + side; gRightBuf[j] = mid - side;
|
gLeftBuf[j] = mid + side;
|
||||||
|
gRightBuf[j] = mid - side;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (gDrcEnabled) gCompressor.process(gLeftBuf.data(), gRightBuf.data(), numFrames);
|
gCompressor.process(gLeftBuf.data(), gRightBuf.data(), numFrames);
|
||||||
|
|
||||||
// FFT for visualization
|
if (numFrames >= FFT_SIZE) {
|
||||||
for (int k = 0; k < FFT_SIZE; k++) {
|
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);
|
gFFTWork[k] = std::complex<float>(gLeftBuf[k], 0.0f);
|
||||||
}
|
}
|
||||||
|
} else {
|
||||||
|
for (int k = 0; k < numFrames; k++) {
|
||||||
|
gFFTWork[k] = std::complex<float>(gLeftBuf[k], 0.0f);
|
||||||
|
}
|
||||||
|
for (int k = numFrames; k < FFT_SIZE; k++) {
|
||||||
|
gFFTWork[k] = std::complex<float>(0.0f, 0.0f);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
applyHannWindowToReal(gFFTWork.data(), FFT_SIZE);
|
||||||
fastFFT(gFFTWork.data(), FFT_SIZE);
|
fastFFT(gFFTWork.data(), FFT_SIZE);
|
||||||
for (int k = 0; k < 256; k++) {
|
computeLogarithmicFFT(gFFTData.data(), gFFTWork.data(), FFT_SIZE / 2);
|
||||||
gFFTData[k] = std::abs(gFFTWork[k]) * 0.5f; // Increased scale
|
|
||||||
}
|
|
||||||
|
|
||||||
for (int k = 0; k < numFrames; k++) {
|
for (int k = 0; k < numFrames; k++) {
|
||||||
buffer[k * 2] = static_cast<jshort>(fastSoftClip(gLeftBuf[k]) * 32767.0f);
|
buffer[k * 2] = static_cast<jshort>(fastSoftClip(gLeftBuf[k]) * 32767.0f);
|
||||||
|
|||||||
@@ -27,9 +27,11 @@ class NativeAudioProcessor : BaseAudioProcessor() {
|
|||||||
private var directBuffer: ByteBuffer? = null
|
private var directBuffer: ByteBuffer? = null
|
||||||
|
|
||||||
// ===== JNI =====
|
// ===== JNI =====
|
||||||
|
private external fun setSampleRate(sampleRate: Float)
|
||||||
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 setEqFull(gains: FloatArray)
|
||||||
private external fun setBassBoost(gainDb: Float)
|
private external fun setBassBoost(gainDb: Float)
|
||||||
private external fun setStereoWidth(width: Float)
|
private external fun setStereoWidth(width: Float)
|
||||||
private external fun processAudioDirect(buf: ByteBuffer, size: Int)
|
private external fun processAudioDirect(buf: ByteBuffer, size: Int)
|
||||||
@@ -39,9 +41,7 @@ class NativeAudioProcessor : BaseAudioProcessor() {
|
|||||||
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) {
|
fun setEqAll(gains: FloatArray) = setEqFull(gains)
|
||||||
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 setWidth(width: Float) = setStereoWidth(width)
|
||||||
|
|
||||||
@@ -59,6 +59,8 @@ class NativeAudioProcessor : BaseAudioProcessor() {
|
|||||||
Log.e(TAG, "Unsupported encoding: ${inputAudioFormat.encoding}")
|
Log.e(TAG, "Unsupported encoding: ${inputAudioFormat.encoding}")
|
||||||
throw AudioProcessor.UnhandledAudioFormatException(inputAudioFormat)
|
throw AudioProcessor.UnhandledAudioFormatException(inputAudioFormat)
|
||||||
}
|
}
|
||||||
|
// Pass the actual sample rate to native
|
||||||
|
setSampleRate(inputAudioFormat.sampleRate.toFloat())
|
||||||
return inputAudioFormat
|
return inputAudioFormat
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -66,28 +68,27 @@ class NativeAudioProcessor : BaseAudioProcessor() {
|
|||||||
val size = inputBuffer.remaining()
|
val size = inputBuffer.remaining()
|
||||||
if (size == 0) return
|
if (size == 0) return
|
||||||
|
|
||||||
// Always ensure we have a direct buffer for JNI
|
val bufferToProcess: ByteBuffer
|
||||||
|
if (inputBuffer.isDirect) {
|
||||||
|
bufferToProcess = inputBuffer
|
||||||
|
} else {
|
||||||
if (directBuffer == null || directBuffer!!.capacity() < size) {
|
if (directBuffer == null || directBuffer!!.capacity() < size) {
|
||||||
directBuffer = ByteBuffer.allocateDirect(size).order(ByteOrder.nativeOrder())
|
directBuffer = ByteBuffer.allocateDirect(size).order(ByteOrder.nativeOrder())
|
||||||
}
|
}
|
||||||
|
|
||||||
directBuffer!!.clear()
|
directBuffer!!.clear()
|
||||||
inputBuffer.position()
|
inputBuffer.position()
|
||||||
directBuffer!!.put(inputBuffer)
|
directBuffer!!.put(inputBuffer)
|
||||||
|
|
||||||
directBuffer!!.flip()
|
directBuffer!!.flip()
|
||||||
|
bufferToProcess = directBuffer!!
|
||||||
|
}
|
||||||
|
|
||||||
// Process audio in JNI
|
processAudioDirect(bufferToProcess, size)
|
||||||
processAudioDirect(directBuffer!!, size)
|
|
||||||
|
|
||||||
// Copy processed data back to output
|
|
||||||
val out = replaceOutputBuffer(size)
|
val out = replaceOutputBuffer(size)
|
||||||
out.order(ByteOrder.nativeOrder())
|
out.order(ByteOrder.nativeOrder())
|
||||||
|
bufferToProcess.position(0)
|
||||||
directBuffer!!.position(0)
|
out.put(bufferToProcess)
|
||||||
out.put(directBuffer!!)
|
|
||||||
out.flip()
|
out.flip()
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onReset() {
|
override fun onReset() {
|
||||||
|
|||||||
@@ -257,7 +257,7 @@ 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)) 0.4f else 0.0f
|
return if (sharedPreferences.getBoolean(Keys.PREF_BASS_BOOST, false)) 0.6f else 0.0f
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user