58 Commits

Author SHA1 Message Date
Michatec f6208f5e5a style(ui): enable marquee for station metadata and playback controls 2026-04-21 20:04:34 +02:00
Michatec a1beb17b26 style(ui): make language selection dialog scrollable 2026-04-21 19:51:53 +02:00
Michatec 18c28170c5 style(ui): update preference and card icons for presets, equalizer, and visualizer 2026-04-21 19:44:46 +02:00
Michatec 48836334bb refactor(ui): remove unused LanguageSelectionDialog import 2026-04-21 19:26:02 +02:00
Michatec 133c56be4d feat(ui): update and expand translations for multiple languages 2026-04-21 19:15:37 +02:00
Michatec 63d85118a4 feat(ui): add manual language selection to settings 2026-04-21 18:58:53 +02:00
Michatec 4f150221b7 style(ui): add adaptive channel icon 2026-04-20 16:13:28 +02:00
Michatec 5fbf763bd6 style(ui): update banner and banner foreground assets 2026-04-20 14:32:38 +02:00
Michatec 0d3224214e style(ui): update application banner to ic_banner 2026-04-19 12:03:14 +02:00
Michatec 11610084f9 loc(de): add German translations for streaming and loading strings 2026-04-19 11:35:59 +02:00
Michatec 08f303997e refactor(ui): use Locale.ROOT for bitrate formatting 2026-04-19 11:27:54 +02:00
Michatec b60b8cdb7c docs: add SECURITY.md 2026-04-19 00:11:17 +02:00
Michatec b3de68050c refactor(collection): add TODO for notifyDataSetChanged cleanup 2026-04-18 23:35:27 +02:00
Michatec 5d99b2a113 chore: update vscode settings and gradlew reference 2026-04-18 23:29:36 +02:00
Michatec adac340925 style(ui): add tools:visibility to station icon in card layout 2026-04-18 20:23:30 +02:00
Michatec d6037dc0c2 chore(config): prune unsupported locales in locale config 2026-04-18 20:13:04 +02:00
Michatec 297310a784 feat(ui): show snackbar when content is copied to clipboard 2026-04-18 20:08:43 +02:00
Michatec abd9b5ecd9 fix(settings): conditionally enable stream URI editing based on station editing status 2026-04-18 18:39:59 +02:00
Michatec 744a650a91 feat(ui): use checkbox to indicate reordering station status 2026-04-18 17:53:52 +02:00
Michatec 1238a5fba2 style(ui): remove manual card stroke color reset 2026-04-18 16:47:19 +02:00
Michatec cc0b284a7f style(ui): update station card stroke color during reordering and remove play button constraint 2026-04-18 16:31:25 +02:00
Michachatz dfbbd8da33 Fix version formatting in README.md 2026-04-15 21:07:22 +02:00
Michachatz 7690ce5685 Update README to specify Android TV support version 2026-04-15 21:06:42 +02:00
Michachatz 3bbc9280b5 Refactor gradle-publish.yml 2026-04-15 07:18:42 +02:00
Michachatz 0fe32420f2 Merge pull request #61 from Michatec/renovate/agp
chore(deps): update agp to v9.1.1
2026-04-15 06:45:02 +02:00
Michachatz 1491a084f3 Merge pull request #62 from Michatec/renovate/freedroidwarn
fix(deps): update dependency com.github.woheller69:freedroidwarn to v1.11
2026-04-15 06:44:49 +02:00
renovate[bot] 244121edb1 fix(deps): update dependency com.github.woheller69:freedroidwarn to v1.11 2026-04-15 04:39:29 +00:00
renovate[bot] 5479ae25f2 chore(deps): update agp to v9.1.1 2026-04-15 04:39:25 +00:00
Michachatz c0ef50b5a9 Update gradle-publish.yml 2026-04-15 06:38:52 +02:00
Michatec 5fb775a373 ci: update gradle-publish workflow to version artifact with app version name 2026-04-09 16:02:41 +02:00
Michatec 901ae6b8ad build: migrate Gradle build files to Kotlin DSL (KTS) 2026-04-09 15:41:13 +02:00
Michatec 255f27bddf perf(dsp): optimize equalizer and update coefficients calculation 2026-04-08 11:48:56 +02:00
Michatec 45576e1577 refactor(player): remove eighth equalizer band preference 2026-04-08 11:08:41 +02:00
Michatec 1212fc61b5 fix(equalizer): update band mappings and reset all bands 2026-04-08 11:04:12 +02:00
Michatec cdf7668d43 refactor(dsp): improve reverb algorithm and update audio presets 2026-04-07 16:10:50 +02:00
Michatec f755dc5173 perf(dsp): optimize audio processing and atomic memory ordering 2026-04-07 15:55:16 +02:00
Michatec 8c7a8ce7c4 feat(ui): add dedicated station search fragment for television platforms 2026-04-07 12:59:33 +02:00
Michatec d1cc340417 feat(ui): add visualizer button to collection screen 2026-04-07 11:58:23 +02:00
Michatec e0d1770a19 feat(dsp): refine audio processing and visualizer rendering 2026-04-07 11:26:14 +02:00
Michachatz 2e8cc9b243 refactor(helpers): adjust the bass boost value 2026-04-07 01:09:13 +02:00
Michatec 9e1219549e docs(readme): add Cast to Devices support to the Android TV section 2026-04-06 23:33:25 +02:00
Michatec 17ba1c268a refactor(gradle): relocate ndkVersion to top-level build script 2026-04-06 23:21:45 +02:00
Michatec b328af5c3a build(gradle): set ndk version 29.0.14206865 in gradle build and ci workflow 2026-04-06 23:20:06 +02:00
Michatec d31d476cb5 ci: add ZIPALIGN env variable to gradle-publish workflow 2026-04-06 23:15:54 +02:00
Michatec 6bb34cd707 chore(config): add .vscode configuration folder 2026-04-06 23:11:30 +02:00
Michatec 883a4443e9 refactor(dsp): make DSP thread‑safe and add EQ interpolation 2026-04-06 22:58:45 +02:00
Michatec 8ba22a4c09 feat(audio): add runtime sample rate handling and thread safety 2026-04-06 22:44:57 +02:00
Michatec 9d47684f13 feat(ui): add television layouts and improve visualizer performance 2026-04-06 17:17:29 +02:00
Michatec 82993d7c97 feat(ui): add spectrum analyzer visualizer 2026-04-06 16:58:53 +02:00
Michatec 487195b716 build: rename target library to dsp in CMakeLists.txt 2026-04-06 15:20:20 +02:00
Michatec 12445a3918 build: rename native audio library from radio to dsp 2026-04-06 15:16:41 +02:00
Michatec bc38742eae perf(audio): adjust audio processing presets and limiters 2026-04-06 15:14:01 +02:00
Michatec 99499ad174 perf(audio): optimize signal processing with NEON and block-based gains 2026-04-06 14:29:08 +02:00
Michatec 0d35770375 feat(audio): add 10-band equalizer and audio presets 2026-04-06 13:27:53 +02:00
Michatec 0d0980a1ef refactor(radio): change Reverb indices to size_t 2026-04-05 19:14:57 +02:00
Michachatz ae215691ca Update radio.cpp 2026-04-05 19:09:43 +02:00
Michachatz 52f1a57de3 Update radio.cpp 2026-04-05 19:07:27 +02:00
Michatec 53abe918ca fix(player): improve cast player integration and service lifecycle management 2026-04-05 18:19:00 +02:00
78 changed files with 2844 additions and 469 deletions
+17 -10
View File
@@ -7,11 +7,14 @@ on:
pull_request:
branches: [ "main" ]
permissions:
contents: write
env:
ANDROID_HOME: /usr/local/lib/android/sdk/
APK_PATH: app/build/outputs/apk/release/Radio.apk
APK_PATH: app/build/outputs/apk/release/app-release-unsigned.apk
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:
build:
@@ -29,20 +32,24 @@ jobs:
distribution: adopt
cache: gradle
- name: Extract Build Metadata
id: meta
run: |
# Extract Version Name
VERSION_NAME=$(grep "versionName =" app/build.gradle.kts | awk -F\" '{print $2}')
echo "Found Version Name: $VERSION_NAME"
echo "VERSION_NAME=$VERSION_NAME" >> $GITHUB_ENV
- name: Cache Android SDK
#id: cache-android-sdk
uses: actions/cache@v5
with:
path: ${{ env.ANDROID_HOME }}
key: ${{ runner.os }}-android-sdk
- name: Setup Android SDK
## It is not necessary to check for cache hit as it
## will not download Android SDK again
#if: steps.cache-android-sdk.outputs.cache-hit != 'true'
uses: android-actions/setup-android@v4
with:
packages: ''
packages: 'ndk;29.0.14206865'
- name: Grant execute permission for gradlew
run: chmod +x ./gradlew
@@ -55,7 +62,7 @@ jobs:
- name: Zipalign APK
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
env:
@@ -66,10 +73,10 @@ jobs:
echo "$SIGN_KEY" | base64 -d > key.der
${{ env.APKSIGNER }} sign --key key.der --cert cert.der app-release-aligned.apk
rm cert.der key.der
mv app-release-aligned.apk app-release.apk
mv app-release-aligned.apk Radio_${{ env.VERSION_NAME }}-release.apk
- name: Upload artifact
uses: actions/upload-artifact@v7
with:
name: app-release
path: app-release.apk
name: Radio
path: Radio_${{ env.VERSION_NAME }}-release.apk
+16
View File
@@ -0,0 +1,16 @@
{
"configurations": [
{
"name": "Android",
"includePath": [
"${workspaceFolder}/**"
],
"defines": [
"_DEBUG",
"UNICODE",
"_UNICODE"
]
}
],
"version": 4
}
+4
View File
@@ -0,0 +1,4 @@
{
"C_Cpp.default.compilerPath": "",
"java.configuration.updateBuildConfiguration": "interactive"
}
+1 -1
View File
@@ -9,7 +9,7 @@
### ️ About Radio
**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 now also supports Android TV (Beta).** <br>
**Radio now also supports Android TV (Beta) and Cast to Devices (Beta) with the Version 14.5 and above.** <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).**
+59
View File
@@ -0,0 +1,59 @@
# Security Policy
## Reporting a Vulnerability
We take the security of Radio seriously. If you believe you have found a security vulnerability, please report it via **GitHub Issues** before disclosing it publicly.
**Create a new security issue**: [GitHub Issues — Security](https://github.com/michatec/Radio/issues/new?labels=security). We will respond within 48 hours acknowledging your report and work with you to understand and address the issue.
Include in your report:
- Description of the vulnerability
- Steps to reproduce
- Affected versions (if known)
- Potential impact
- Any suggested fixes (optional)
We appreciate responsible disclosure and will credit researchers who report valid security issues (unless you prefer to remain anonymous).
## Supported Versions
| Version | Supported |
| ------- | ------------------ |
| 14.5 | :white_check_mark: |
| < 14.5 | :x: |
Only the latest stable release receives security updates. Users on older versions are encouraged to update.
## Security Considerations
### Network Security
Radio streams audio over the internet and connects to the [radio-browser.info](https://www.radio-browser.info/) API for station search. The app allows cleartext HTTP traffic for radio stream compatibility (required for many legacy radio stations).
### Data Collection
- **Station data** (names, images, stream URLs) is fetched from radio-browser.info's public API
- **Station lists** are stored locally on the device only
- **No personal data** is collected or transmitted to michatec servers
- **Usage data** is not collected
### Permissions
Radio requests only the permissions necessary for core functionality:
- `INTERNET` — stream radio and fetch station metadata
- `ACCESS_NETWORK_STATE` — detect connectivity changes
- `FOREGROUND_SERVICE_MEDIA_PLAYBACK` — maintain playback when app is backgrounded
- `WAKE_LOCK` — prevent device from sleeping during playback
### Third-Party Dependencies
Radio uses several third-party libraries. Security issues in dependencies are monitored via Renovate bot for updates. Key dependencies include:
- AndroidX Media3 / ExoPlayer (media playback)
- Google Cast SDK (Chromecast support)
- Volley (HTTP requests)
### File Handling
The app can import M3U/PLS playlist files from external sources. Files are processed locally and stream URLs are validated before playback. Station images are downloaded from radio-browser.info and cached locally.
## Security Update Process
Security patches are delivered via normal app update channels (GitHub Releases, automated update notifications). Critical vulnerabilities may trigger an out-of-band security update.
## Contacts
- General issues: [GitHub Issues](https://github.com/michatec/Radio/issues)
- Project maintainer: [@michatec](https://github.com/michatec)
-94
View File
@@ -1,94 +0,0 @@
plugins {
alias libs.plugins.android.application
id 'kotlin-parcelize'
}
androidComponents {
onVariants(selector().all()) { variant ->
variant.outputs.forEach { output ->
output.outputFileName.set("Radio.apk")
}
}
}
android {
namespace 'com.michatec.radio'
compileSdk 36
defaultConfig {
applicationId 'com.michatec.radio'
minSdk 28
targetSdk 36
versionCode 145
versionName '14.5'
resourceConfigurations += ['en', 'de', 'el', 'nl', 'pl', 'ru','uk', 'ja', 'da', 'fr']
externalNativeBuild {
cmake {
cppFlags ''
}
}
}
compileOptions {
sourceCompatibility JavaVersion.VERSION_17
targetCompatibility JavaVersion.VERSION_17
}
buildFeatures {
buildConfig true
}
buildTypes {
debug {
minifyEnabled false
shrinkResources false
crunchPngs false
proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro'
applicationIdSuffix = ".debug"
}
release {
minifyEnabled true
shrinkResources true
crunchPngs true
proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro'
}
}
externalNativeBuild {
cmake {
path file('src/main/cpp/CMakeLists.txt')
version '3.22.1'
}
}
}
dependencies {
implementation fileTree(dir: 'libs', include: ['*.jar'])
// Google Stuff //
implementation libs.material
implementation libs.gson
implementation libs.play.services.cast.framework
// AndroidX Stuff //
implementation libs.core.ktx
implementation libs.activity.ktx
implementation libs.palette.ktx
implementation libs.preference.ktx
implementation libs.media
implementation libs.media3.exoplayer
implementation libs.media3.exoplayer.hls
implementation libs.media3.session
implementation libs.media3.cast
implementation libs.media3.datasource.okhttp
implementation libs.navigation.fragment.ktx
implementation libs.navigation.ui.ktx
implementation libs.work.runtime.ktx
implementation libs.leanback
implementation libs.freedroidwarn
// Volley HTTP request //
implementation libs.volley
implementation libs.material3
}
+81
View File
@@ -0,0 +1,81 @@
plugins {
alias(libs.plugins.android.application)
id("kotlin-parcelize")
}
android {
namespace = "com.michatec.radio"
compileSdk = 36
defaultConfig {
applicationId = "com.michatec.radio"
minSdk = 28
targetSdk = 36
versionCode = 145
versionName = "14.5"
}
compileOptions {
sourceCompatibility = JavaVersion.VERSION_17
targetCompatibility = JavaVersion.VERSION_17
}
buildFeatures {
buildConfig = true
}
buildTypes {
debug {
isMinifyEnabled = false
isShrinkResources = false
isCrunchPngs = false
proguardFiles.addAll(listOf(getDefaultProguardFile("proguard-android-optimize.txt"), file("proguard-rules.pro")))
applicationIdSuffix = ".debug"
}
release {
isMinifyEnabled = true
isShrinkResources = true
isCrunchPngs = true
proguardFiles.addAll(listOf(getDefaultProguardFile("proguard-android-optimize.txt"), file("proguard-rules.pro")))
}
}
ndkVersion = "29.0.14206865"
externalNativeBuild {
cmake {
path = file("src/main/cpp/CMakeLists.txt")
version = "3.22.1"
}
}
}
dependencies {
// Google Stuff //
implementation(libs.material)
implementation(libs.gson)
implementation(libs.play.services.cast.framework)
// AndroidX Stuff //
implementation(libs.core.ktx)
implementation(libs.activity.ktx)
implementation(libs.palette.ktx)
implementation(libs.preference.ktx)
implementation(libs.media)
implementation(libs.media3.exoplayer)
implementation(libs.media3.exoplayer.hls)
implementation(libs.media3.session)
implementation(libs.media3.cast)
implementation(libs.media3.datasource.okhttp)
implementation(libs.navigation.fragment.ktx)
implementation(libs.navigation.ui.ktx)
implementation(libs.work.runtime.ktx)
implementation(libs.leanback)
implementation(libs.freedroidwarn)
// Volley HTTP request //
implementation(libs.volley)
implementation(libs.material3)
}
+1 -1
View File
@@ -1,6 +1,6 @@
# Add project specific ProGuard rules here.
# You can control the set of applied configuration files using the
# proguardFiles setting in build.gradle.
# proguardFiles setting in build.gradle.kts.
#
# For more details, see
# http://developer.android.com/guide/developing/tools/proguard.html
+1 -1
View File
@@ -17,7 +17,7 @@
<application
android:name=".Radio"
android:allowBackup="true"
android:banner="@mipmap/ic_launcher"
android:banner="@mipmap/ic_banner"
android:dataExtractionRules="@xml/data_extraction_rules"
android:fullBackupContent="@xml/backup_rules"
android:icon="@mipmap/ic_launcher"
+10 -6
View File
@@ -18,21 +18,25 @@ project("radio")
# Gradle automatically packages shared libraries with your APK.
#
# In this top level CMakeLists.txt, ${CMAKE_PROJECT_NAME} is used to define
# the target library name; in the sub-module's CMakeLists.txt, ${PROJECT_NAME}
# the target library name; in the sub-module's CMakeLists.txt, the project name
# is preferred for the same purpose.
#
# In order to load a library into your app from Java/Kotlin, you must call
# System.loadLibrary() and pass the name of the library defined here;
# for GameActivity/NativeActivity derived applications, the same library name must be
# used in the AndroidManifest.xml file.
add_library(${CMAKE_PROJECT_NAME} SHARED
# List C/C++ source files with relative paths to this CMakeLists.txt.
radio.cpp)
add_library(dsp SHARED
dsp.cpp)
add_library(extra SHARED
extra.cpp)
# Specifies libraries CMake should link to your target library. You
# can link libraries from various origins, such as libraries defined in this
# build script, prebuilt third-party libraries, or Android system libraries.
target_link_libraries(${CMAKE_PROJECT_NAME}
# List libraries link to the target library
target_link_libraries(dsp
android
log)
target_link_libraries(extra
android
log)
+458
View File
@@ -0,0 +1,458 @@
#include <jni.h>
#include <vector>
#include <cmath>
#include <complex>
#include <array>
#include <atomic>
#if defined(__ARM_NEON)
#include <arm_neon.h>
#endif
#ifndef M_PI
#define M_PI 3.14159265358979323846
#endif
static std::atomic<float> gSampleRate(44100.0f);
static constexpr int FFT_SIZE = 2048;
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 float INTERPOLATION_SPEED = 0.5f;
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) EqBandInterpolator {
std::atomic<float> targetGain{0.0f};
std::atomic<float> currentGain{0.0f};
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;
inline void setTargetGain(float g) { targetGain.store(g, std::memory_order_release); }
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) {
if (!active) return x;
updateInterpolation();
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) {
active = true;
const float A = powf(10.0f, g / 60.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 = (1.0f + alpha * A) * invA0;
a1 = (-2.0f * c) * invA0;
a2 = (1.0f - alpha * A) * invA0;
b1 = (-2.0f * c) * invA0;
b2 = (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;
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) {
if (!active.load(std::memory_order_acquire)) return x;
updateInterpolation();
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 setCoefficients(float sr, float f, float g, float q){
float A=powf(10.0f,g/60.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;
}
void applyGain(float sr) {
float g = currentGain.load(std::memory_order_acquire);
setCoefficients(sr, 150.0f, g, SQRT_2_INV);
}
};
class ReverbOptimized {
struct DelayLine {
float buffer[48000]{};
int size = 48000;
int pos = 0;
inline float read(float delaySamples) {
float readPos = static_cast<float>(pos) - delaySamples;
if (readPos < 0.0f) readPos += static_cast<float>(size);
int i1 = static_cast<int>(readPos);
int i2 = (i1 + 1) % size;
float frac = readPos - static_cast<float>(i1);
return buffer[i1] * (1.0f - frac) + buffer[i2] * frac;
}
inline void write(float x) {
buffer[pos] = x;
pos++;
if (pos >= size) pos = 0;
}
};
DelayLine delays[8];
float feedback[8] = {
0.78f, 0.80f, 0.82f, 0.84f,
0.76f, 0.79f, 0.81f, 0.83f
};
float baseDelay[8] = {
1423.0f, 1557.0f, 1617.0f, 1789.0f,
1867.0f, 1999.0f, 2137.0f, 2251.0f
};
float modPhase[8] = {};
float modSpeed[8] = {
0.10f, 0.12f, 0.09f, 0.11f,
0.13f, 0.08f, 0.14f, 0.07f
};
public:
std::atomic<float> mix{0.0f};
inline float processSample(float x) {
float m = mix.load(std::memory_order_relaxed);
if (m < 0.01f) return x;
float out = 0.0f;
#pragma GCC unroll 8
for (int i = 0; i < 8; i++) {
modPhase[i] += modSpeed[i];
if (modPhase[i] > 2.0f * static_cast<float>(M_PI)) modPhase[i] -= 2.0f * static_cast<float>(M_PI);
float mod = sinf(modPhase[i]) * 5.0f;
float delayTime = baseDelay[i] + mod;
float delayed = delays[i].read(delayTime);
float input = x + delayed * feedback[i] + DENORMAL_OFFSET;
delays[i].write(input);
out += delayed;
}
return x * (1.0f - m) + (out * 0.125f) * m;
}
inline void processBlock(float* __restrict__ left, float* __restrict__ right, int count) {
float m = mix.load(std::memory_order_relaxed);
if (m < 0.01f) return;
for (int i = 0; i < count; i++) {
float l = processSample(left[i]);
float r = processSample(right[i]);
float wetL = l * 0.7f + r * 0.3f;
float wetR = r * 0.7f + l * 0.3f;
left[i] = wetL;
right[i] = wetR;
}
}
};
class CompressorOptimized {
public:
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:
float envelopeL = 0.0f, envelopeR = 0.0f;
float attackCoef = 0.0f, releaseCoef = 0.0f;
public:
inline void updateCoefficients() {
float a = attack.load(std::memory_order_relaxed);
float r = release.load(std::memory_order_relaxed);
float sr = sampleRate.load(std::memory_order_relaxed);
attackCoef = expf(-1.0f / (a * sr));
releaseCoef = expf(-1.0f / (r * sr));
}
inline void processBlock(float* __restrict__ buffer, int count, float& envelope) {
updateCoefficients();
float th = threshold.load(std::memory_order_acquire);
float rt = ratio.load(std::memory_order_acquire);
for(int i=0; i<count; i++){
float absInput = fabsf(buffer[i]);
envelope = (absInput > envelope) ? attackCoef*envelope + (1.0f-attackCoef)*absInput : releaseCoef*envelope + (1.0f-releaseCoef)*absInput;
float gain = (envelope>th)? (th + (envelope-th)/rt)/(envelope+1e-9f) : 1.0f;
buffer[i]*=gain;
}
}
inline void process(float* __restrict__ left, float* __restrict__ right, int count) {
if (!enabled.load(std::memory_order_acquire)) return;
processBlock(left, count, envelopeL);
processBlock(right, count, envelopeR);
}
};
static std::atomic<float> gStereoWidth{1.0f};
alignas(16) std::array<float, 4096> gLeftBuf, gRightBuf;
alignas(16) std::array<float, 256> gFFTData;
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 void applyHannWindowToReal(std::complex<float>* __restrict__ data, int size) {
const auto fSizeMinus1 = static_cast<float>(size - 1);
for (int i = 0; i < size; i++) {
float window = 0.5f * (1.0f - cosf(2.0f * static_cast<float>(M_PI) * static_cast<float>(i) / fSizeMinus1));
data[i] = std::complex<float>(data[i].real() * window, data[i].imag());
}
}
inline float fastSoftClip(float x) {
float ax = fabsf(x);
float sign = x > 0.0f ? 1.0f : -1.0f;
if (ax > 1.0f) return sign;
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 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[static_cast<size_t>(b)], g, 1.0f);
gEqR[b].setCoefficients(sr, EQ_FREQUENCIES[static_cast<size_t>(b)], g, 1.0f);
}
}
extern "C" {
JNIEXPORT void JNICALL Java_com_michatec_radio_helpers_NativeAudioProcessor_setSampleRate(JNIEnv*, jobject, jfloat sr) {
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_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) {
float scaledGain = g * 4.0f;
gBassL.targetGain.store(scaledGain, std::memory_order_release);
gBassR.targetGain.store(scaledGain, std::memory_order_release);
float sr = gSampleRate.load(std::memory_order_acquire);
gBassL.applyGain(sr);
gBassR.applyGain(sr);
if (std::abs(g) > 0.01f) {
gBassL.active.store(true, std::memory_order_release);
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 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;
}
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 * static_cast<float>(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 * static_cast<float>(b) / static_cast<float>(NUM_BANDS)));
float f2 = expf(logMin + (logRange * static_cast<float>(b + 1) / static_cast<float>(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) {
auto* buffer = static_cast<jshort*>(env->GetDirectBufferAddress(byteBuffer));
if (!buffer) return;
int numFrames = (size / 2) / 2;
if (numFrames > 4096) numFrames = 4096;
if (gEqUpdateCounter > 0) {
updateAllEqBands();
gEqUpdateCounter--;
}
for (int i = 0; i < numFrames; i++) {
gLeftBuf[static_cast<size_t>(i)] = static_cast<float>(buffer[i * 2]) * INV_32768;
gRightBuf[static_cast<size_t>(i)] = static_cast<float>(buffer[i * 2 + 1]) * INV_32768;
}
for (int i = 0; i < numFrames; i++) {
float xL = gLeftBuf[static_cast<size_t>(i)];
float xR = gRightBuf[static_cast<size_t>(i)];
for (int b = 0; b < NUM_EQ_BANDS; b++) {
xL = gEqL[b].process(xL);
xR = gEqR[b].process(xR);
}
gLeftBuf[static_cast<size_t>(i)] = xL;
gRightBuf[static_cast<size_t>(i)] = xR;
}
for(int i = 0; i < numFrames; i++) {
gLeftBuf[static_cast<size_t>(i)] = gBassL.process(gLeftBuf[static_cast<size_t>(i)]);
gRightBuf[static_cast<size_t>(i)] = gBassR.process(gRightBuf[static_cast<size_t>(i)]);
}
gReverbL.processBlock(gLeftBuf.data(), gRightBuf.data(), numFrames);
float stereoWidth = gStereoWidth.load(std::memory_order_relaxed);
if (stereoWidth != 1.0f) {
float halfWidth = stereoWidth * 0.5f;
for (int j = 0; j < numFrames; j++) {
float mid = (gLeftBuf[static_cast<size_t>(j)] + gRightBuf[static_cast<size_t>(j)]) * 0.5f;
float side = (gLeftBuf[static_cast<size_t>(j)] - gRightBuf[static_cast<size_t>(j)]) * halfWidth;
gLeftBuf[static_cast<size_t>(j)] = mid + side;
gRightBuf[static_cast<size_t>(j)] = mid - side;
}
}
gCompressor.process(gLeftBuf.data(), gRightBuf.data(), numFrames);
if (numFrames >= FFT_SIZE) {
for (int k = 0; k < FFT_SIZE; k++) {
gFFTWork[static_cast<size_t>(k)] = std::complex<float>(gLeftBuf[static_cast<size_t>(k)], 0.0f);
}
} else {
for (int k = 0; k < numFrames; k++) {
gFFTWork[static_cast<size_t>(k)] = std::complex<float>(gLeftBuf[static_cast<size_t>(k)], 0.0f);
}
for (int k = numFrames; k < FFT_SIZE; k++) {
gFFTWork[static_cast<size_t>(k)] = std::complex<float>(0.0f, 0.0f);
}
}
applyHannWindowToReal(gFFTWork.data(), FFT_SIZE);
fastFFT(gFFTWork.data(), FFT_SIZE);
computeLogarithmicFFT(gFFTData.data(), gFFTWork.data(), FFT_SIZE / 2);
for (int k = 0; k < numFrames; k++) {
buffer[k * 2] = static_cast<jshort>(fastSoftClip(gLeftBuf[static_cast<size_t>(k)]) * 32767.0f);
buffer[k * 2 + 1] = static_cast<jshort>(fastSoftClip(gRightBuf[static_cast<size_t>(k)]) * 32767.0f);
}
}
}
+86
View File
@@ -0,0 +1,86 @@
#include <jni.h>
#include <android/native_window_jni.h>
#include <android/native_window.h>
#include <android/log.h>
#include <vector>
#include <algorithm>
#include <cmath>
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++) {
uint32_t* row = pixels + (y * buffer.stride);
for (int x = 0; x < buffer.width; x++) {
row[x] = 0xFF121212;
}
}
// Draw bars - fewer bins = thicker bars
int displayBins = 40;
float barWidth = static_cast<float>(buffer.width) / static_cast<float>(displayBins);
int padding = static_cast<int>(barWidth * 0.2f);
if (padding < 1) padding = 1;
for (int i = 0; i < displayBins; i++) {
// Map display bin to data index
int dataIdx = (i * len) / displayBins;
float val = body[dataIdx];
// Use square root to compress the range (so peaks don't hit the top too easily)
// and a lower multiplier (0.4f) to reduce overall height
float scaledVal = sqrtf(val) * 0.5f;
int barHeight = static_cast<int>(scaledVal * static_cast<float>(buffer.height));
// Cap height at 75% to leave some room at the top
int maxH = static_cast<int>(static_cast<float>(buffer.height) * 0.75f);
if (barHeight > maxH) barHeight = maxH;
if (barHeight < 4) barHeight = 4; // Minimal visible line
int startX = static_cast<int>(static_cast<float>(i) * barWidth);
int endX = static_cast<int>(static_cast<float>(i + 1) * barWidth);
int drawStartX = startX + padding;
int drawEndX = endX - padding;
if (drawEndX <= drawStartX) drawEndX = drawStartX + 1;
int barBottom = buffer.height - 4; // Bottom margin
int barTop = barBottom - barHeight;
for (int x = drawStartX; x < drawEndX; x++) {
if (x < 0 || x >= buffer.width) continue;
for (int y = barTop; y < barBottom; y++) {
if (y < 0 || y >= buffer.height) continue;
// Using the same color, but now height is controlled
pixels[y * buffer.stride + x] = 0xFFC5DA03;
}
}
}
ANativeWindow_unlockAndPost(window);
}
env->ReleaseFloatArrayElements(data, body, JNI_ABORT);
ANativeWindow_release(window);
}
} // extern "C"
-192
View File
@@ -1,192 +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> delayLine;
int pos = 0;
float feedback = 0.4f;
float mix = 0.0f;
Reverb() { delayLine.resize(4410, 0.0f); } // ~100ms
float process(float in) {
float delayed = delayLine[static_cast<size_t>(pos)];
delayLine[static_cast<size_t>(pos)] = in + delayed * feedback;
pos = (pos + 1) % static_cast<int>(delayLine.size());
return in + delayed * 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"
@@ -0,0 +1,214 @@
package com.michatec.radio
import android.annotation.SuppressLint
import android.content.Context
import android.os.Bundle
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import android.view.inputmethod.InputMethodManager
import android.widget.Button
import android.widget.EditText
import android.widget.ProgressBar
import androidx.appcompat.widget.SearchView
import androidx.core.view.isGone
import androidx.core.view.isVisible
import androidx.fragment.app.Fragment
import androidx.lifecycle.ViewModelProvider
import androidx.navigation.fragment.findNavController
import androidx.recyclerview.widget.DefaultItemAnimator
import androidx.recyclerview.widget.LinearLayoutManager
import androidx.recyclerview.widget.RecyclerView
import com.google.android.material.textview.MaterialTextView
import com.michatec.radio.collection.CollectionViewModel
import com.michatec.radio.core.Station
import com.michatec.radio.helpers.CollectionHelper
import com.michatec.radio.helpers.NetworkHelper
import com.michatec.radio.search.DirectInputCheck
import com.michatec.radio.search.RadioBrowserResult
import com.michatec.radio.search.RadioBrowserSearch
import com.michatec.radio.search.SearchResultAdapter
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers.IO
import kotlinx.coroutines.Dispatchers.Main
import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext
class AddStationFragment : Fragment(),
SearchResultAdapter.SearchResultAdapterListener,
RadioBrowserSearch.RadioBrowserSearchListener,
DirectInputCheck.DirectInputCheckListener {
private lateinit var collectionViewModel: CollectionViewModel
private lateinit var stationSearchBoxView: SearchView
private lateinit var searchRequestProgressIndicator: ProgressBar
private lateinit var noSearchResultsTextView: MaterialTextView
private lateinit var stationSearchResultList: RecyclerView
private lateinit var positiveButton: Button
private lateinit var negativeButton: Button
private lateinit var searchResultAdapter: SearchResultAdapter
private lateinit var radioBrowserSearch: RadioBrowserSearch
private lateinit var directInputCheck: DirectInputCheck
private var station: Station = Station()
override fun onCreateView(
inflater: LayoutInflater,
container: ViewGroup?,
savedInstanceState: Bundle?
): View {
// We reuse the dialog layout as it's already optimized for TV in layout-television
val view = inflater.inflate(R.layout.dialog_find_station, container, false)
collectionViewModel = ViewModelProvider(requireActivity())[CollectionViewModel::class.java]
radioBrowserSearch = RadioBrowserSearch(this)
directInputCheck = DirectInputCheck(this)
stationSearchBoxView = view.findViewById(R.id.station_search_box_view)
searchRequestProgressIndicator = view.findViewById(R.id.search_request_progress_indicator)
stationSearchResultList = view.findViewById(R.id.station_search_result_list)
noSearchResultsTextView = view.findViewById(R.id.no_results_text_view)
positiveButton = view.findViewById(R.id.dialog_positive_button)
negativeButton = view.findViewById(R.id.dialog_negative_button)
setupRecyclerView()
setupSearchView()
positiveButton.setOnClickListener {
addStationAndExit()
}
negativeButton.setOnClickListener {
searchResultAdapter.stopPrePlayback()
findNavController().navigateUp()
}
stationSearchBoxView.setOnQueryTextListener(object : SearchView.OnQueryTextListener {
override fun onQueryTextChange(query: String): Boolean {
handleSearch(query)
return true
}
override fun onQueryTextSubmit(query: String): Boolean {
handleSearch(query)
return true
}
})
return view
}
override fun onDestroy() {
super.onDestroy()
// Stop playback when fragment is destroyed (e.g. via back button)
if (this::searchResultAdapter.isInitialized) {
searchResultAdapter.stopPrePlayback()
}
}
private fun setupRecyclerView() {
searchResultAdapter = SearchResultAdapter(this, listOf())
stationSearchResultList.adapter = searchResultAdapter
stationSearchResultList.layoutManager = LinearLayoutManager(context)
stationSearchResultList.itemAnimator = DefaultItemAnimator()
}
private fun setupSearchView() {
// TV specific: ensure keyboard opens when search view gets focus
stationSearchBoxView.setOnQueryTextFocusChangeListener { v, hasFocus ->
if (hasFocus) {
// Find the internal EditText of the SearchView
val searchEditText = v.findViewById<EditText>(androidx.appcompat.R.id.search_src_text)
if (searchEditText != null) {
searchEditText.requestFocus()
val imm = requireContext().getSystemService(Context.INPUT_METHOD_SERVICE) as InputMethodManager
imm.showSoftInput(searchEditText, InputMethodManager.SHOW_IMPLICIT)
}
}
}
// Make the SearchView always expanded and ready for input
stationSearchBoxView.isIconified = false
}
private fun handleSearch(query: String) {
if (query.isEmpty()) {
resetLayout(true)
return
}
showProgressIndicator()
if (query.startsWith("http")) {
directInputCheck.checkStationAddress(requireContext(), query)
} else {
radioBrowserSearch.searchStation(requireContext(), query, Keys.SEARCH_TYPE_BY_KEYWORD)
}
}
private fun addStationAndExit() {
searchResultAdapter.stopPrePlayback()
val currentCollection = collectionViewModel.collectionLiveData.value ?: return
if (station.streamContent.isNotEmpty() && station.streamContent != Keys.MIME_TYPE_UNSUPPORTED) {
CollectionHelper.addStation(requireContext(), currentCollection, station)
findNavController().navigateUp()
} else {
CoroutineScope(IO).launch {
val contentType = NetworkHelper.detectContentType(station.getStreamUri())
station.streamContent = contentType.type
withContext(Main) {
CollectionHelper.addStation(requireContext(), currentCollection, station)
findNavController().navigateUp()
}
}
}
}
override fun onSearchResultTapped(result: Station) {
station = result
activateAddButton()
}
override fun activateAddButton() {
positiveButton.isEnabled = true
}
override fun deactivateAddButton() {
positiveButton.isEnabled = false
}
@SuppressLint("NotifyDataSetChanged")
override fun onRadioBrowserSearchResults(results: Array<RadioBrowserResult>) {
if (results.isNotEmpty()) {
searchResultAdapter.searchResults = results.map { it.toStation() }
searchResultAdapter.notifyDataSetChanged()
resetLayout(false)
} else {
showNoResultsError()
}
}
@SuppressLint("NotifyDataSetChanged")
override fun onDirectInputCheck(stationList: MutableList<Station>) {
if (stationList.isNotEmpty()) {
searchResultAdapter.searchResults = stationList
searchResultAdapter.notifyDataSetChanged()
resetLayout(false)
} else {
showNoResultsError()
}
}
private fun resetLayout(clear: Boolean) {
positiveButton.isEnabled = false
searchRequestProgressIndicator.isGone = true
noSearchResultsTextView.isGone = true
if (clear) searchResultAdapter.resetSelection(true)
}
private fun showProgressIndicator() {
searchRequestProgressIndicator.isVisible = true
noSearchResultsTextView.isGone = true
}
private fun showNoResultsError() {
searchRequestProgressIndicator.isGone = true
noSearchResultsTextView.isVisible = true
}
}
@@ -9,10 +9,25 @@ import androidx.preference.SeekBarPreference
import com.michatec.radio.helpers.PreferencesHelper
/*
* EqualizerFragment class: Handles audio frequency settings
* EqualizerFragment class: Handles audio frequency settings with 10-band EQ
*/
class EqualizerFragment : PreferenceFragmentCompat() {
// EQ band frequencies matching radio.cpp
private val eqFrequencies = arrayOf("31 Hz", "62 Hz", "125 Hz", "250 Hz", "500 Hz", "1 kHz", "2 kHz", "4 kHz", "8 kHz", "16 kHz")
private val eqKeys = arrayOf(
Keys.PREF_EQ_LOW, // Band 0: 31 Hz
Keys.PREF_EQ_BAND_1, // Band 1: 62 Hz
Keys.PREF_EQ_BAND_2, // Band 2: 125 Hz
Keys.PREF_EQ_BAND_3, // Band 3: 250 Hz
Keys.PREF_EQ_BAND_4, // Band 4: 500 Hz
Keys.PREF_EQ_BAND_5, // Band 5: 1 kHz
Keys.PREF_EQ_MID, // Band 6: 2 kHz
Keys.PREF_EQ_BAND_6, // Band 7: 4 kHz
Keys.PREF_EQ_BAND_7, // Band 8: 8 kHz
Keys.PREF_EQ_HIGH // Band 9: 16 kHz
)
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
(activity as AppCompatActivity).supportActionBar?.title = getString(R.string.pref_equalizer_title)
@@ -28,46 +43,25 @@ class EqualizerFragment : PreferenceFragmentCompat() {
resetPreference.setIcon(R.drawable.ic_refresh_24dp)
resetPreference.setOnPreferenceClickListener {
PreferencesHelper.resetEqualizer()
// Manually update SeekBars to 0
findPreference<SeekBarPreference>(Keys.PREF_EQ_LOW)?.value = 0
findPreference<SeekBarPreference>(Keys.PREF_EQ_MID)?.value = 0
findPreference<SeekBarPreference>(Keys.PREF_EQ_HIGH)?.value = 0
for (key in eqKeys) {
findPreference<SeekBarPreference>(key)?.value = 0
}
return@setOnPreferenceClickListener true
}
screen.addPreference(resetPreference)
// EQ Low
val eqLow = SeekBarPreference(context)
eqLow.title = getString(R.string.pref_eq_low_title)
eqLow.key = Keys.PREF_EQ_LOW
eqLow.setIcon(R.drawable.ic_music_note_24dp)
eqLow.min = -12
eqLow.max = 12
eqLow.showSeekBarValue = true
eqLow.setDefaultValue(0)
screen.addPreference(eqLow)
// EQ Mid
val eqMid = SeekBarPreference(context)
eqMid.title = getString(R.string.pref_eq_mid_title)
eqMid.key = Keys.PREF_EQ_MID
eqMid.setIcon(R.drawable.ic_music_note_24dp)
eqMid.min = -12
eqMid.max = 12
eqMid.showSeekBarValue = true
eqMid.setDefaultValue(0)
screen.addPreference(eqMid)
// EQ High
val eqHigh = SeekBarPreference(context)
eqHigh.title = getString(R.string.pref_eq_high_title)
eqHigh.key = Keys.PREF_EQ_HIGH
eqHigh.setIcon(R.drawable.ic_music_note_24dp)
eqHigh.min = -12
eqHigh.max = 12
eqHigh.showSeekBarValue = true
eqHigh.setDefaultValue(0)
screen.addPreference(eqHigh)
// Create 10-band EQ
for (i in eqKeys.indices) {
val eqBand = SeekBarPreference(context)
eqBand.title = "Equalizer: ${eqFrequencies[i]}"
eqBand.key = eqKeys[i]
eqBand.setIcon(R.drawable.ic_music_note_24dp)
eqBand.min = -12
eqBand.max = 12
eqBand.showSeekBarValue = true
eqBand.setDefaultValue(0)
screen.addPreference(eqBand)
}
preferenceScreen = screen
}
@@ -1,14 +1,40 @@
package com.michatec.radio
import android.content.Context
import android.content.res.Configuration
import android.view.Menu
import com.google.android.gms.cast.framework.CastButtonFactory
import com.google.android.gms.cast.framework.media.widget.ExpandedControllerActivity
import com.michatec.radio.helpers.PreferencesHelper
import java.util.Locale
class ExpandedControllerActivity : ExpandedControllerActivity() {
override fun attachBaseContext(newBase: Context) {
val languageCode = PreferencesHelper.loadSelectedLanguage()
val context = if (languageCode.isEmpty() || languageCode == "system") {
// Use system default locale
newBase
} else {
val locale = Locale.forLanguageTag(languageCode)
val config = Configuration(newBase.resources.configuration)
config.setLocale(locale)
newBase.createConfigurationContext(config)
}
super.attachBaseContext(context)
}
override fun onCreateOptionsMenu(menu: Menu): Boolean {
super.onCreateOptionsMenu(menu)
menuInflater.inflate(R.menu.expanded_controller, menu)
CastButtonFactory.setUpMediaRouteButton(this, menu, R.id.media_route_menu_item)
return true
}
override fun onResume() {
try {
super.onResume()
} catch (_: ClassCastException) {
// Fix for lifecycle exception on some devices (e.g. Samsung)
}
}
}
@@ -27,6 +27,7 @@ object Keys {
const val EXTRA_START_LAST_PLAYED_STATION: String = "START_LAST_PLAYED_STATION"
const val EXTRA_SLEEP_TIMER_REMAINING: String = "SLEEP_TIMER_REMAINING"
const val EXTRA_METADATA_HISTORY: String = "METADATA_HISTORY"
const val EXTRA_VISUALIZER_DATA: String = "VISUALIZER_DATA"
// arguments
const val ARG_UPDATE_COLLECTION: String = "ArgUpdateCollection"
@@ -43,6 +44,7 @@ object Keys {
const val CMD_PLAY_STREAM: String = "PLAY_STREAM"
const val CMD_REQUEST_SLEEP_TIMER_REMAINING: String = "REQUEST_SLEEP_TIMER_REMAINING"
const val CMD_REQUEST_METADATA_HISTORY: String = "REQUEST_METADATA_HISTORY"
const val CMD_GET_VISUALIZER_DATA: String = "GET_VISUALIZER_DATA"
// preferences
const val PREF_RADIO_BROWSER_API: String = "RADIO_BROWSER_API"
@@ -66,6 +68,30 @@ object Keys {
const val PREF_EQ_LOW: String = "EQ_LOW"
const val PREF_EQ_MID: String = "EQ_MID"
const val PREF_EQ_HIGH: String = "EQ_HIGH"
const val PREF_EQUALIZER: String = "EQUALIZER_SETTINGS"
const val PREF_EQ_BAND_1: String = "EQ_BAND_1"
const val PREF_EQ_BAND_2: String = "EQ_BAND_2"
const val PREF_EQ_BAND_3: String = "EQ_BAND_3"
const val PREF_EQ_BAND_4: String = "EQ_BAND_4"
const val PREF_EQ_BAND_5: String = "EQ_BAND_5"
const val PREF_EQ_BAND_6: String = "EQ_BAND_6"
const val PREF_EQ_BAND_7: String = "EQ_BAND_7"
const val PREF_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"
const val PREF_LANGUAGE_SELECTED: String = "PRESET_LANGUAGE_SELECTED"
// default const values
const val DEFAULT_SIZE_OF_METADATA_HISTORY: Int = 25
@@ -1,7 +1,9 @@
package com.michatec.radio
import android.content.Context
import android.content.SharedPreferences
import android.content.pm.PackageManager
import android.content.res.Configuration
import android.os.Bundle
import android.os.Handler
import android.os.Looper
@@ -16,8 +18,10 @@ import androidx.navigation.ui.NavigationUI
import androidx.navigation.ui.navigateUp
import com.michatec.radio.helpers.AppThemeHelper
import com.michatec.radio.helpers.FileHelper
import com.michatec.radio.helpers.LanguageHelper
import com.michatec.radio.helpers.PreferencesHelper
import org.woheller69.freeDroidWarn.FreeDroidWarn
import java.util.Locale
/*
* MainActivity class
@@ -27,6 +31,21 @@ class MainActivity : AppCompatActivity() {
/* Main class variables */
private lateinit var appBarConfiguration: AppBarConfiguration
/* Overrides attachBaseContext from AppCompatActivity */
override fun attachBaseContext(newBase: Context) {
val languageCode = PreferencesHelper.loadSelectedLanguage()
val context = if (languageCode.isEmpty() || languageCode == "system") {
// Use system default locale
newBase
} else {
val locale = Locale.forLanguageTag(languageCode)
val config = Configuration(newBase.resources.configuration)
config.setLocale(locale)
newBase.createConfigurationContext(config)
}
super.attachBaseContext(context)
}
/* Overrides onCreate from AppCompatActivity */
override fun onCreate(savedInstanceState: Bundle?) {
enableEdgeToEdge()
@@ -112,6 +131,9 @@ class MainActivity : AppCompatActivity() {
Keys.PREF_THEME_SELECTION -> {
AppThemeHelper.setTheme(PreferencesHelper.loadThemeSelection())
}
Keys.PREF_LANGUAGE_SELECTED -> {
LanguageHelper.setLanguage(this, PreferencesHelper.loadSelectedLanguage())
}
}
}
/*
@@ -34,6 +34,7 @@ import androidx.media3.common.util.UnstableApi
import androidx.media3.session.MediaController
import androidx.media3.session.SessionResult
import androidx.media3.session.SessionToken
import androidx.navigation.fragment.findNavController
import androidx.recyclerview.widget.ItemTouchHelper
import androidx.recyclerview.widget.RecyclerView
import com.android.volley.Request
@@ -342,7 +343,14 @@ class PlayerFragment : Fragment(),
/* Overrides onAddNewButtonTapped from CollectionAdapterListener */
override fun onAddNewButtonTapped() {
FindStationDialog(activity as Activity, this as FindStationDialog.FindStationDialogListener).show()
// stop playback when adding a new station
controller?.stop()
if (activity?.packageManager?.hasSystemFeature(PackageManager.FEATURE_LEANBACK) == true) {
findNavController().navigate(R.id.action_map_fragment_to_player_to_add_station)
} else {
FindStationDialog(activity as Activity, this as FindStationDialog.FindStationDialogListener).show()
}
}
@@ -625,6 +633,8 @@ class PlayerFragment : Fragment(),
}
withContext(Main) {
if (stationList.isNotEmpty()) {
// stop playback when adding a new station via intent
controller?.stop()
AddStationDialog(activity as Activity, stationList, this@PlayerFragment as AddStationDialog.AddStationDialogListener).show()
} else {
// invalid address
@@ -156,7 +156,8 @@ class PlayerService : MediaLibraryService(), SharedPreferences.OnSharedPreferenc
castPlayer = CastPlayer.Builder(this).setLocalPlayer(exoPlayer).build()
// manually add seek to next and seek to previous since headphones issue them, and they are translated to next and previous station
player = object : ForwardingPlayer(exoPlayer) {
// IMPORTANT: Use castPlayer here instead of exoPlayer so the session controls both local and remote playback
player = object : ForwardingPlayer(castPlayer) {
override fun getAvailableCommands(): Player.Commands {
return super.getAvailableCommands().buildUpon().add(COMMAND_SEEK_TO_NEXT)
.add(COMMAND_SEEK_TO_PREVIOUS).build()
@@ -170,6 +171,7 @@ class PlayerService : MediaLibraryService(), SharedPreferences.OnSharedPreferenc
return C.TIME_UNSET // this will hide progress bar for HLS stations in the notification
}
}
player.addListener(playerListener)
}
@@ -284,20 +286,61 @@ class PlayerService : MediaLibraryService(), SharedPreferences.OnSharedPreferenc
/* Applies audio effects based on preferences */
private fun applyAudioEffects() {
nativeAudioProcessor.enableBassBoost(PreferencesHelper.loadBassBoost())
nativeAudioProcessor.setReverb(PreferencesHelper.loadReverb())
nativeAudioProcessor.enableDrc(PreferencesHelper.loadDrcEnabled())
nativeAudioProcessor.setEq(0, PreferencesHelper.loadEqLow())
nativeAudioProcessor.setEq(1, PreferencesHelper.loadEqMid())
nativeAudioProcessor.setEq(2, PreferencesHelper.loadEqHigh())
val selectedPreset = PreferencesHelper.loadSelectedPreset()
if (selectedPreset.isNotEmpty()) {
applyPreset(selectedPreset)
} else {
// Apply manual settings
nativeAudioProcessor.enableBassBoost(PreferencesHelper.loadBassBoost())
nativeAudioProcessor.setReverb(PreferencesHelper.loadReverb())
nativeAudioProcessor.enableDrc(PreferencesHelper.loadDrcEnabled())
nativeAudioProcessor.setWidth(1f)
// Apply all 10 EQ bands
val eqGains = FloatArray(10)
for (i in 0 until 10) {
eqGains[i] = PreferencesHelper.loadEqBand(i).toFloat()
}
nativeAudioProcessor.setEqAll(eqGains)
}
}
/* Applies a saved preset */
private fun applyPreset(presetName: String) {
when (presetName) {
getString(R.string.pref_preset_rock) -> nativeAudioProcessor.setPresetRock()
getString(R.string.pref_preset_pop) -> nativeAudioProcessor.setPresetPop()
getString(R.string.pref_preset_jazz) -> nativeAudioProcessor.setPresetJazz()
getString(R.string.pref_preset_flat) -> nativeAudioProcessor.setPresetFlat()
else -> {
// Custom preset - load from preferences
nativeAudioProcessor.enableDrc(PreferencesHelper.loadPresetDrc())
nativeAudioProcessor.setReverb(PreferencesHelper.loadPresetReverb())
nativeAudioProcessor.setWidth(PreferencesHelper.loadPresetStereoWidth())
nativeAudioProcessor.enableBassBoost(PreferencesHelper.loadPresetBassBoost())
val eqGains = FloatArray(10)
for (i in 0 until 10) {
eqGains[i] = PreferencesHelper.loadPresetEqBand(i).toFloat()
}
nativeAudioProcessor.setEqAll(eqGains)
}
}
}
/* Overrides onSharedPreferenceChanged from SharedPreferences.OnSharedPreferenceChangeListener */
override fun onSharedPreferenceChanged(sharedPreferences: SharedPreferences?, key: String?) {
when (key) {
Keys.PREF_BASS_BOOST, Keys.PREF_REVERB, Keys.PREF_DRC,
Keys.PREF_EQ_LOW, Keys.PREF_EQ_MID, Keys.PREF_EQ_HIGH -> {
Keys.PREF_BASS_BOOST, Keys.PREF_REVERB, Keys.PREF_DRC,
Keys.PREF_EQ_LOW, Keys.PREF_EQ_MID, Keys.PREF_EQ_HIGH,
Keys.PREF_EQ_BAND_1, Keys.PREF_EQ_BAND_2, Keys.PREF_EQ_BAND_3,
Keys.PREF_EQ_BAND_4, Keys.PREF_EQ_BAND_5, Keys.PREF_EQ_BAND_6,
Keys.PREF_EQ_BAND_7,
Keys.PREF_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()
}
}
@@ -332,6 +375,7 @@ class PlayerService : MediaLibraryService(), SharedPreferences.OnSharedPreferenc
builder.add(SessionCommand(Keys.CMD_CANCEL_SLEEP_TIMER, Bundle.EMPTY))
builder.add(SessionCommand(Keys.CMD_REQUEST_SLEEP_TIMER_REMAINING, Bundle.EMPTY))
builder.add(SessionCommand(Keys.CMD_REQUEST_METADATA_HISTORY, Bundle.EMPTY))
builder.add(SessionCommand(Keys.CMD_GET_VISUALIZER_DATA, Bundle.EMPTY))
return MediaSession.ConnectionResult.accept(builder.build(), connectionResult.availablePlayerCommands)
}
@@ -418,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)
}
@@ -483,35 +540,23 @@ class PlayerService : MediaLibraryService(), SharedPreferences.OnSharedPreferenc
isPlaying
)
if (isPlaying) {
// playback is active
} else {
if (!isPlaying) {
// cancel sleep timer
cancelSleepTimer()
// reset metadata
updateMetadata()
// playback is not active
// Not playing because playback is paused, ended, suppressed, or the player
// is buffering, stopped or failed. Check player.getPlayWhenReady,
// player.getPlaybackState, player.getPlaybackSuppressionReason and
// player.getPlaybackError for details.
// Check playback state to decide whether to stop the service
when (player.playbackState) {
// player is able to immediately play from its current position
Player.STATE_ENDED, Player.STATE_IDLE -> {
stopSelf()
}
Player.STATE_READY -> {
// Playback is paused. For radio, we can stop the service to remove the notification.
stopSelf()
}
// buffering - data needs to be loaded
Player.STATE_BUFFERING -> {
stopSelf()
}
// player finished playing all media
Player.STATE_ENDED -> {
stopSelf()
}
// initial state or player is stopped or playback failed
Player.STATE_IDLE -> {
stopSelf()
// DO NOT stop the service while buffering (especially important for Cast)
}
}
}
@@ -520,13 +565,9 @@ class PlayerService : MediaLibraryService(), SharedPreferences.OnSharedPreferenc
override fun onPlayWhenReadyChanged(playWhenReady: Boolean, reason: Int) {
super.onPlayWhenReadyChanged(playWhenReady, reason)
if (!playWhenReady) {
when (reason) {
Player.PLAY_WHEN_READY_CHANGE_REASON_END_OF_MEDIA_ITEM -> {
stopSelf()
}
else -> {
stopSelf()
}
// Only stop if not buffering and not ready to play (i.e. truly stopped/paused)
if (player.playbackState != Player.STATE_BUFFERING) {
stopSelf()
}
}
}
@@ -2,6 +2,7 @@ package com.michatec.radio
import android.app.Application
import com.michatec.radio.helpers.AppThemeHelper
import com.michatec.radio.helpers.LanguageHelper
import com.michatec.radio.helpers.PreferencesHelper
import com.michatec.radio.helpers.PreferencesHelper.initPreferences
@@ -18,6 +19,7 @@ class Radio : Application() {
initPreferences()
// set Dark / Light theme state
AppThemeHelper.setTheme(PreferencesHelper.loadThemeSelection())
LanguageHelper.setLanguage(this, PreferencesHelper.loadSelectedLanguage())
}
}
@@ -17,6 +17,8 @@ import androidx.navigation.fragment.findNavController
import androidx.preference.*
import com.google.android.material.snackbar.Snackbar
import com.michatec.radio.dialogs.ErrorDialog
import com.michatec.radio.dialogs.LanguageSelectionDialog
import com.michatec.radio.dialogs.PresetSelectionDialog
import com.michatec.radio.dialogs.ThemeSelectionDialog
import com.michatec.radio.dialogs.YesNoDialog
import com.michatec.radio.helpers.*
@@ -30,7 +32,7 @@ import java.util.*
/*
* SettingsFragment class
*/
class SettingsFragment : PreferenceFragmentCompat(), YesNoDialog.YesNoDialogListener, ThemeSelectionDialog.ThemeSelectionDialogListener {
class SettingsFragment : PreferenceFragmentCompat(), YesNoDialog.YesNoDialogListener, ThemeSelectionDialog.ThemeSelectionDialogListener, PresetSelectionDialog.PresetSelectionDialogListener, LanguageSelectionDialog.LanguageSelectionDialogListener {
/* Define log tag */
@@ -52,6 +54,9 @@ class SettingsFragment : PreferenceFragmentCompat(), YesNoDialog.YesNoDialogList
val context = preferenceManager.context
val screen = preferenceManager.createPreferenceScreen(context)
// Load current preset once
val currentPreset = PreferencesHelper.loadSelectedPreset()
// set up "App Theme" preference
val preferenceThemeSelection = Preference(activity as Context)
preferenceThemeSelection.title = getString(R.string.pref_theme_selection_title)
@@ -161,7 +166,11 @@ class SettingsFragment : PreferenceFragmentCompat(), YesNoDialog.YesNoDialogList
preferenceEnableEditingStreamUri.summaryOn = getString(R.string.pref_edit_station_stream_summary_enabled)
preferenceEnableEditingStreamUri.summaryOff = getString(R.string.pref_edit_station_stream_summary_disabled)
preferenceEnableEditingStreamUri.setDefaultValue(PreferencesHelper.loadEditStreamUrisEnabled(context))
preferenceEnableEditingStreamUri.isEnabled = PreferencesHelper.loadEditStreamUrisEnabled(context)
preferenceEnableEditingStreamUri.isEnabled = if (PreferencesHelper.loadEditStationsEnabled(context)) {
true
} else {
PreferencesHelper.loadEditStreamUrisEnabled(context)
}
// set up "Edit Stations" preference
@@ -190,6 +199,7 @@ class SettingsFragment : PreferenceFragmentCompat(), YesNoDialog.YesNoDialogList
preferenceBassBoost.title = getString(R.string.pref_bass_boost_title)
preferenceBassBoost.setIcon(R.drawable.ic_music_note_24dp)
preferenceBassBoost.key = Keys.PREF_BASS_BOOST
preferenceBassBoost.isEnabled = currentPreset.isEmpty()
preferenceBassBoost.summary = getString(R.string.pref_bass_boost_summary)
preferenceBassBoost.setDefaultValue(false)
@@ -198,6 +208,7 @@ class SettingsFragment : PreferenceFragmentCompat(), YesNoDialog.YesNoDialogList
preferenceReverb.title = getString(R.string.pref_reverb_title)
preferenceReverb.setIcon(R.drawable.ic_music_note_24dp)
preferenceReverb.key = Keys.PREF_REVERB
preferenceReverb.isEnabled = currentPreset.isEmpty()
preferenceReverb.summary = getString(R.string.pref_reverb_summary)
preferenceReverb.setDefaultValue(false)
@@ -206,19 +217,53 @@ class SettingsFragment : PreferenceFragmentCompat(), YesNoDialog.YesNoDialogList
preferenceDrc.title = getString(R.string.pref_drc_title)
preferenceDrc.setIcon(R.drawable.ic_music_note_24dp)
preferenceDrc.key = Keys.PREF_DRC
preferenceDrc.isEnabled = currentPreset.isEmpty()
preferenceDrc.summary = getString(R.string.pref_drc_summary)
preferenceDrc.setDefaultValue(true)
// set up "Preset Selection" preference
val preferencePresetSelection = Preference(context)
preferencePresetSelection.title = getString(R.string.pref_preset_selection_title)
preferencePresetSelection.setIcon(R.drawable.ic_presets_24dp)
preferencePresetSelection.key = Keys.PREF_PRESET_SELECTED
val presetSummary = currentPreset.ifEmpty {
getString(R.string.pref_preset_none)
}
preferencePresetSelection.summary = "${getString(R.string.pref_preset_selection_summary)}: $presetSummary"
preferencePresetSelection.setOnPreferenceClickListener {
PresetSelectionDialog(this).show(activity as Context)
return@setOnPreferenceClickListener true
}
// Initialize EQ control states based on current preset
updateEqControlStates()
// set up "Equalizer" preference entry
val preferenceEqualizer = Preference(context)
preferenceEqualizer.title = getString(R.string.pref_equalizer_title)
preferenceEqualizer.setIcon(R.drawable.ic_music_note_24dp)
preferenceEqualizer.summary = getString(R.string.pref_equalizer_summary)
preferenceEqualizer.setIcon(R.drawable.ic_equalizer_24dp)
preferenceEqualizer.key = Keys.PREF_EQUALIZER
if (currentPreset.isEmpty()) {
preferenceEqualizer.summary = getString(R.string.pref_equalizer_summary)
preferenceEqualizer.isEnabled = true
} else {
preferenceEqualizer.summary = getString(R.string.pref_equalizer_summary_off)
preferenceEqualizer.isEnabled = false
}
preferenceEqualizer.setOnPreferenceClickListener {
findNavController().navigate(R.id.action_settings_to_equalizer)
return@setOnPreferenceClickListener true
}
val preferenceVisualizer = Preference(context)
preferenceVisualizer.title = getString(R.string.pref_visualizer_title)
preferenceVisualizer.setIcon(R.drawable.ic_visualizer_24dp)
preferenceVisualizer.summary = getString(R.string.pref_visualizer_summary)
preferenceVisualizer.setOnPreferenceClickListener {
findNavController().navigate(R.id.action_settings_to_visualizer)
return@setOnPreferenceClickListener true
}
// set up "App Version" preference
val preferenceAppVersion = Preference(context)
preferenceAppVersion.title = getString(R.string.pref_app_version_title)
@@ -264,11 +309,23 @@ class SettingsFragment : PreferenceFragmentCompat(), YesNoDialog.YesNoDialogList
return@setOnPreferenceClickListener true
}
val preferenceLanguageSelection = Preference(context)
preferenceLanguageSelection.title = getString(R.string.pref_language_selection_title)
preferenceLanguageSelection.setIcon(R.drawable.ic_language_24dp)
preferenceLanguageSelection.key = Keys.PREF_LANGUAGE_SELECTED
preferenceLanguageSelection.summary = "${getString(R.string.pref_language_selection_summary)}: ${
LanguageHelper.getCurrentLanguage(activity as Context)
}"
preferenceLanguageSelection.setOnPreferenceClickListener {
LanguageSelectionDialog(this).show(activity as Context)
return@setOnPreferenceClickListener true
}
// set preference categories
val preferenceCategoryGeneral = PreferenceCategory(activity as Context)
preferenceCategoryGeneral.title = getString(R.string.pref_general_title)
val preferenceCategoryAudioEffects = PreferenceCategory(context)
preferenceCategoryAudioEffects.title = getString(R.string.pref_audio_effects_title)
@@ -290,12 +347,15 @@ class SettingsFragment : PreferenceFragmentCompat(), YesNoDialog.YesNoDialogList
screen.addPreference(preferenceCategoryGeneral)
preferenceCategoryGeneral.addPreference(preferenceThemeSelection)
preferenceCategoryGeneral.addPreference(preferenceLanguageSelection)
screen.addPreference(preferenceCategoryAudioEffects)
preferenceCategoryAudioEffects.addPreference(preferenceBassBoost)
preferenceCategoryAudioEffects.addPreference(preferenceReverb)
preferenceCategoryAudioEffects.addPreference(preferenceDrc)
preferenceCategoryAudioEffects.addPreference(preferencePresetSelection)
preferenceCategoryAudioEffects.addPreference(preferenceEqualizer)
preferenceCategoryAudioEffects.addPreference(preferenceVisualizer)
screen.addPreference(preferenceCategoryMaintenance)
preferenceCategoryMaintenance.addPreference(preferenceUpdateStationImages)
@@ -340,6 +400,63 @@ 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()
}
}
/* Overrides onLanguageSelectionDialog from LanguageSelectionDialogListener */
override fun onLanguageSelectionDialog(dialogResult: Boolean, selectedLanguage: String) {
if (dialogResult) {
// update summary
val languagePreference = findPreference<Preference>(Keys.PREF_LANGUAGE_SELECTED)
val languageSummary = if (selectedLanguage.isEmpty()) {
getString(R.string.pref_language_system)
} else {
LanguageHelper.getCurrentLanguage(activity as Context)
}
languagePreference?.summary = "${getString(R.string.pref_language_selection_summary)}: $languageSummary"
}
}
/* Updates the enabled/disabled state of EQ controls based on preset selection */
private fun updateEqControlStates() {
val currentPreset = PreferencesHelper.loadSelectedPreset()
val isPresetSelected = currentPreset.isNotEmpty()
// Update Bass Boost
findPreference<Preference>(Keys.PREF_BASS_BOOST)?.isEnabled = !isPresetSelected
// Update Reverb
findPreference<Preference>(Keys.PREF_REVERB)?.isEnabled = !isPresetSelected
// Update DRC
findPreference<Preference>(Keys.PREF_DRC)?.isEnabled = !isPresetSelected
// Update Equalizer with proper key
val preferenceEqualizer = findPreference<Preference>(Keys.PREF_EQUALIZER)
if (preferenceEqualizer != null) {
if (isPresetSelected) {
preferenceEqualizer.summary = getString(R.string.pref_equalizer_summary_off)
preferenceEqualizer.isEnabled = false
} else {
preferenceEqualizer.summary = getString(R.string.pref_equalizer_summary)
preferenceEqualizer.isEnabled = true
}
}
}
/* Overrides onYesNoDialog from YesNoDialogListener */
override fun onYesNoDialog(
type: Int,
@@ -0,0 +1,122 @@
package com.michatec.radio
import android.content.ComponentName
import android.os.Bundle
import android.os.Handler
import android.os.Looper
import android.util.Log
import android.view.View
import androidx.annotation.OptIn
import androidx.appcompat.app.AppCompatActivity
import androidx.media3.common.util.UnstableApi
import androidx.media3.session.MediaController
import androidx.media3.session.SessionToken
import androidx.preference.PreferenceFragmentCompat
import com.google.common.util.concurrent.ListenableFuture
import com.google.common.util.concurrent.MoreExecutors
import com.michatec.radio.extensions.requestVisualizerData
import com.michatec.radio.helpers.ExtrasHelper
/*
* VisualizerFragment class: Handles audio visualization
*/
@OptIn(UnstableApi::class)
class VisualizerFragment : PreferenceFragmentCompat() {
private val TAG = "VisualizerFragment"
private lateinit var controllerFuture: ListenableFuture<MediaController>
private val controller: MediaController?
get() = if (this::controllerFuture.isInitialized && controllerFuture.isDone) {
try { controllerFuture.get() } catch (_: Exception) { null }
} else null
private var visualizerPref: ExtrasHelper.VisualizerPreference? = null
private val handler = Handler(Looper.getMainLooper())
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
(activity as AppCompatActivity).supportActionBar?.title = getString(R.string.pref_visualizer_title)
(activity as AppCompatActivity).supportActionBar?.show()
}
override fun onCreatePreferences(savedInstanceState: Bundle?, rootKey: String?) {
val context = preferenceManager.context
val screen = preferenceManager.createPreferenceScreen(context)
visualizerPref = ExtrasHelper.VisualizerPreference(context)
visualizerPref?.key = "visualizer_key"
screen.addPreference(visualizerPref!!)
preferenceScreen = screen
}
override fun onStart() {
super.onStart()
initializeController()
}
override fun onStop() {
super.onStop()
releaseController()
}
override fun onResume() {
super.onResume()
startPolling()
}
override fun onPause() {
super.onPause()
stopPolling()
}
private fun initializeController() {
controllerFuture = MediaController.Builder(
requireContext(),
SessionToken(requireContext(), ComponentName(requireContext(), PlayerService::class.java))
).buildAsync()
controllerFuture.addListener({
Log.d(TAG, "MediaController connected: ${controller != null}")
}, MoreExecutors.directExecutor())
}
private fun releaseController() {
if (this::controllerFuture.isInitialized) {
MediaController.releaseFuture(controllerFuture)
}
}
private val pollRunnable = object : Runnable {
override fun run() {
val c = controller
if (c != null && c.isPlaying) {
val resultFuture = c.requestVisualizerData()
resultFuture.addListener({
try {
val result = resultFuture.get()
if (result.resultCode == androidx.media3.session.SessionResult.RESULT_SUCCESS) {
val data = result.extras.getFloatArray(Keys.EXTRA_VISUALIZER_DATA)
if (data != null && data.isNotEmpty()) {
visualizerPref?.update(data)
}
} else {
Log.e(TAG, "Custom command failed with result code: ${result.resultCode}")
}
} catch (e: Exception) {
Log.e(TAG, "Error fetching visualizer data", e)
}
}, MoreExecutors.directExecutor())
}
handler.postDelayed(this, 18) // ~60 FPS
}
}
private fun startPolling() {
handler.removeCallbacks(pollRunnable)
handler.post(pollRunnable)
}
private fun stopPolling() {
handler.removeCallbacks(pollRunnable)
}
}
@@ -3,7 +3,6 @@ package com.michatec.radio.collection
import android.annotation.SuppressLint
import android.content.Context
import android.content.SharedPreferences
import android.content.res.ColorStateList
import android.text.Editable
import android.text.TextWatcher
import android.view.KeyEvent
@@ -11,12 +10,12 @@ import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import android.view.inputmethod.EditorInfo
import android.widget.CheckBox
import android.widget.ImageView
import android.widget.ProgressBar
import android.widget.TextView
import androidx.appcompat.app.AppCompatActivity
import androidx.constraintlayout.widget.Group
import androidx.core.content.ContextCompat
import androidx.core.net.toUri
import androidx.core.view.isGone
import androidx.core.view.isVisible
@@ -169,6 +168,9 @@ class CollectionAdapter(
addNewViewHolder.settingsButtonView.setOnClickListener {
it.findNavController().navigate(R.id.settings_destination)
}
addNewViewHolder.visualizerButtonView.setOnClickListener {
it.findNavController().navigate(R.id.visualizer_destination)
}
}
// CASE STATION CARD
is StationViewHolder -> {
@@ -177,7 +179,6 @@ class CollectionAdapter(
// get reference to StationViewHolder
val stationViewHolder: StationViewHolder = holder
// set up station views
setStarredIcon(stationViewHolder, station)
setStationName(stationViewHolder, station)
@@ -187,13 +188,12 @@ class CollectionAdapter(
setPlaybackProgress(stationViewHolder, station)
setDownloadProgress(stationViewHolder, station)
// highlight if reordering
if (reorderStationUuid == station.uuid) {
stationViewHolder.stationCardView.setStrokeColor(
ColorStateList.valueOf(
ContextCompat.getColor(context, R.color.cardview_reordering)
)
)
stationViewHolder.reorderCheckbox.isVisible = true
stationViewHolder.reorderCheckbox.isChecked = true
} else {
stationViewHolder.reorderCheckbox.isGone = true
stationViewHolder.reorderCheckbox.isChecked = false
}
// show / hide edit views
@@ -339,6 +339,7 @@ class CollectionAdapter(
/* Shows / hides the edit view for a station */
/* TODO: Remove @SuppressLint("NotifyDataSetChanged"), remove NotifyDataSetChanged */
@SuppressLint("NotifyDataSetChanged")
private fun toggleEditViews(position: Int, stationUuid: String) {
when (stationUuid) {
@@ -754,6 +755,8 @@ class CollectionAdapter(
listItemAddNewLayout.findViewById(R.id.card_add_new_station)
val settingsButtonView: ExtendedFloatingActionButton =
listItemAddNewLayout.findViewById(R.id.card_settings)
val visualizerButtonView: ExtendedFloatingActionButton =
listItemAddNewLayout.findViewById(R.id.card_visualizer)
}
/*
* End of inner class
@@ -769,10 +772,10 @@ class CollectionAdapter(
val stationImageView: ImageView = stationCardLayout.findViewById(R.id.station_icon)
val stationNameView: TextView = stationCardLayout.findViewById(R.id.station_name)
val stationStarredView: ImageView = stationCardLayout.findViewById(R.id.starred_icon)
val reorderCheckbox: CheckBox = stationCardLayout.findViewById(R.id.reorder_checkbox)
val bufferingProgress: ProgressBar = stationCardLayout.findViewById(R.id.buffering_progress)
val downloadProgress: ProgressBar = stationCardLayout.findViewById(R.id.download_progress)
// val menuButtonView: ImageView = stationCardLayout.findViewById(R.id.menu_button)
val playButtonView: ImageView = stationCardLayout.findViewById(R.id.playback_button)
val editViews: Group = stationCardLayout.findViewById(R.id.default_edit_views)
val stationImageChangeView: ImageView =
@@ -73,6 +73,7 @@ class FindStationDialog (
/* Overrides onRadioBrowserSearchResults from RadioBrowserSearchListener */
/* TODO: Remove @SuppressLint("NotifyDataSetChanged"), remove NotifyDataSetChanged */
@SuppressLint("NotifyDataSetChanged")
override fun onRadioBrowserSearchResults(results: Array<RadioBrowserResult>) {
if (results.isNotEmpty()) {
@@ -0,0 +1,140 @@
package com.michatec.radio.dialogs
import android.content.Context
import android.view.LayoutInflater
import android.widget.RadioButton
import android.widget.RadioGroup
import androidx.appcompat.app.AlertDialog
import com.google.android.material.dialog.MaterialAlertDialogBuilder
import com.michatec.radio.R
import com.michatec.radio.helpers.PreferencesHelper
/*
* LanguageSelectionDialog class
*/
class LanguageSelectionDialog(private var languageSelectionDialogListener: LanguageSelectionDialogListener) {
/* Interface used to communicate back to activity */
interface LanguageSelectionDialogListener {
fun onLanguageSelectionDialog(dialogResult: Boolean, selectedLanguage: String)
}
/* Main class variables */
private lateinit var dialog: AlertDialog
/* Data class representing a supported language */
data class Language(
val code: String,
val nameResId: Int
)
/* List of supported languages - displayed in their own language */
private val supportedLanguages = listOf(
Language("system", R.string.pref_language_system),
Language("en", R.string.pref_language_en),
Language("de", R.string.pref_language_de),
Language("fr", R.string.pref_language_fr),
Language("ru", R.string.pref_language_ru),
Language("ja", R.string.pref_language_ja),
Language("nl", R.string.pref_language_nl),
Language("pl", R.string.pref_language_pl),
Language("el", R.string.pref_language_el),
Language("da", R.string.pref_language_da)
)
/* Counter for generating unique view IDs */
private var viewIdCounter = 0x7F010001 // Starting after android.R.id.home
/* 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_language_selection, null)
// find radio group
val radioGroup = view.findViewById<RadioGroup>(R.id.language_radio_group)
val currentLanguage = PreferencesHelper.loadSelectedLanguage()
// add radio buttons for each supported language
for (language in supportedLanguages) {
val radioButton = RadioButton(context).apply {
id = generateViewId()
tag = language.code
text = context.getString(language.nameResId)
textSize = if (isTelevision(context)) 20f else 16f
setPadding(dpToPx(context, 8), dpToPx(context, 16), dpToPx(context, 16), dpToPx(context, 16))
}
radioGroup.addView(radioButton)
}
// set current selection
for (i in 0 until radioGroup.childCount) {
val radioButton = radioGroup.getChildAt(i) as RadioButton
if (radioButton.tag == currentLanguage) {
radioButton.isChecked = true
break
}
}
// if no language is selected, check the first one (system)
if (radioGroup.checkedRadioButtonId == -1) {
val firstButton = radioGroup.getChildAt(0) as RadioButton
firstButton.isChecked = true
}
// set up radio group listener
radioGroup.setOnCheckedChangeListener { _, checkedId ->
val selectedButton = radioGroup.findViewById<RadioButton>(checkedId)
val selectedLanguageCode = selectedButton?.tag as? String ?: "system"
// save language selection to preferences
PreferencesHelper.saveSelectedLanguage(selectedLanguageCode)
// notify listener
languageSelectionDialogListener.onLanguageSelectionDialog(true, selectedLanguageCode)
// dismiss dialog
dialog.dismiss()
}
// set custom view
builder.setView(view)
// handle outside-click as cancel
builder.setOnCancelListener {
languageSelectionDialogListener.onLanguageSelectionDialog(false, "")
}
// display dialog
dialog = builder.create()
dialog.show()
}
/* Generate a unique view ID */
private fun generateViewId(): Int {
return viewIdCounter++
}
/* Helper function to check if device is a TV */
private fun isTelevision(context: Context): Boolean {
val uiMode = context.resources.configuration.uiMode
return (uiMode and android.content.res.Configuration.UI_MODE_TYPE_MASK) == android.content.res.Configuration.UI_MODE_TYPE_TELEVISION
}
/* Helper function to convert dp to pixels */
private fun dpToPx(context: Context, dp: Int): Int {
return (dp * context.resources.displayMetrics.density).toInt()
}
}
@@ -0,0 +1,86 @@
package com.michatec.radio.dialogs
import android.content.Context
import android.view.LayoutInflater
import android.widget.RadioButton
import androidx.appcompat.app.AlertDialog
import androidx.core.content.ContextCompat.getString
import com.google.android.material.dialog.MaterialAlertDialogBuilder
import com.michatec.radio.R
import com.michatec.radio.helpers.PreferencesHelper
/*
* PresetSelectionDialog class
*/
class PresetSelectionDialog(private var presetSelectionDialogListener: PresetSelectionDialogListener) {
/* Interface used to communicate back to activity */
interface PresetSelectionDialogListener {
fun onPresetSelectionDialog(dialogResult: Boolean, selectedPreset: String)
}
/* Main class variables */
private lateinit var dialog: AlertDialog
/* Construct and show dialog */
fun show(context: Context) {
// prepare dialog builder
val builder = MaterialAlertDialogBuilder(context)
// inflate custom layout
val inflater = LayoutInflater.from(context)
val view = inflater.inflate(R.layout.dialog_preset_selection, null)
// find radio buttons
val radioGroup = view.findViewById<android.widget.RadioGroup>(R.id.preset_radio_group)
val radioNone = view.findViewById<RadioButton>(R.id.radio_preset_none)
val radioRock = view.findViewById<RadioButton>(R.id.radio_preset_rock)
val radioPop = view.findViewById<RadioButton>(R.id.radio_preset_pop)
val radioJazz = view.findViewById<RadioButton>(R.id.radio_preset_jazz)
val radioFlat = view.findViewById<RadioButton>(R.id.radio_preset_flat)
// set current selection
val currentPreset = PreferencesHelper.loadSelectedPreset()
when (currentPreset) {
"" -> radioNone.isChecked = true
getString(context, R.string.pref_preset_rock) -> radioRock.isChecked = true
getString(context, R.string.pref_preset_pop) -> radioPop.isChecked = true
getString(context, R.string.pref_preset_jazz) -> radioJazz.isChecked = true
getString(context, R.string.pref_preset_flat) -> radioFlat.isChecked = true
else -> radioNone.isChecked = true
}
// set up radio group listener
radioGroup.setOnCheckedChangeListener { _, checkedId ->
val selectedPreset = when (checkedId) {
R.id.radio_preset_none -> ""
R.id.radio_preset_rock -> getString(context, R.string.pref_preset_rock)
R.id.radio_preset_pop -> getString(context, R.string.pref_preset_pop)
R.id.radio_preset_jazz -> getString(context, R.string.pref_preset_jazz)
R.id.radio_preset_flat -> getString(context, R.string.pref_preset_flat)
else -> ""
}
// save preset selection to preferences
PreferencesHelper.saveSelectedPreset(selectedPreset)
// notify listener
presetSelectionDialogListener.onPresetSelectionDialog(true, selectedPreset)
// dismiss dialog
dialog.dismiss()
}
// set custom view
builder.setView(view)
// handle outside-click as cancel
builder.setOnCancelListener {
presetSelectionDialogListener.onPresetSelectionDialog(false, "")
}
// display dialog
dialog = builder.create()
dialog.show()
}
}
@@ -35,7 +35,7 @@ fun MediaController.requestSleepTimerRemaining(): ListenableFuture<SessionResult
}
/* Request sleep timer remaining */
/* Request metadata history */
fun MediaController.requestMetadataHistory(): ListenableFuture<SessionResult> {
return sendCustomCommand(
SessionCommand(Keys.CMD_REQUEST_METADATA_HISTORY, Bundle.EMPTY),
@@ -43,6 +43,14 @@ fun MediaController.requestMetadataHistory(): ListenableFuture<SessionResult> {
)
}
/* Request visualizer data */
fun MediaController.requestVisualizerData(): ListenableFuture<SessionResult> {
return sendCustomCommand(
SessionCommand(Keys.CMD_GET_VISUALIZER_DATA, Bundle.EMPTY),
Bundle.EMPTY
)
}
/* Starts playback with a new media item */
fun MediaController.play(context: Context, station: Station) {
@@ -0,0 +1,117 @@
package com.michatec.radio.helpers
import android.content.Context
import android.util.AttributeSet
import android.util.Log
import android.view.Surface
import android.view.SurfaceHolder
import android.view.SurfaceView
import android.view.ViewGroup
import android.widget.FrameLayout
import androidx.preference.Preference
import androidx.preference.PreferenceViewHolder
import com.michatec.radio.R
class ExtrasHelper {
companion object {
private const val TAG = "ExtrasHelper"
init {
try {
System.loadLibrary("extra")
} catch (e: Exception) {
Log.e(TAG, "Failed to load extra library", e)
}
}
@JvmStatic
private external fun visualize(surface: Surface, data: FloatArray)
fun render(surface: Surface, data: FloatArray) {
if (!surface.isValid) return
try {
visualize(surface, data)
} catch (e: Exception) {
Log.e(TAG, "Native visualize failed", e)
}
}
}
class VisualizerPreference(context: Context, attrs: AttributeSet? = null) : Preference(context, attrs) {
private var visualizerView: VisualizerView? = null
init {
// We can use a standard layout and inject our view
layoutResource = R.layout.preference_visualizer
}
override fun onBindViewHolder(holder: PreferenceViewHolder) {
super.onBindViewHolder(holder)
// Try to find the container in the inflated layout
var container = holder.findViewById(R.id.visualizer_container) as? ViewGroup
// Fallback: If not found by ID, maybe the root is the container?
if (container == null && holder.itemView is ViewGroup) {
container = holder.itemView as ViewGroup
}
if (container != null) {
if (visualizerView == null) {
visualizerView = VisualizerView(context).apply {
layoutParams = ViewGroup.LayoutParams(
ViewGroup.LayoutParams.MATCH_PARENT,
ViewGroup.LayoutParams.MATCH_PARENT
)
}
}
val currentParent = visualizerView?.parent as? ViewGroup
if (currentParent != container) {
currentParent?.removeView(visualizerView)
// If we injected into a standard preference, don't clear everything, just add
if (container is FrameLayout || container.childCount == 0) {
container.removeAllViews()
}
container.addView(visualizerView)
}
} else {
Log.e("VisualizerPreference", "Could not find any container to attach VisualizerView!")
}
}
fun update(data: FloatArray) {
visualizerView?.update(data)
}
}
class VisualizerView @JvmOverloads constructor(
context: Context, attrs: AttributeSet? = null, defStyleAttr: Int = 0
) : SurfaceView(context, attrs, defStyleAttr), SurfaceHolder.Callback {
private var surface: Surface? = null
init {
Log.d("VisualizerView", "VisualizerView initialized")
holder.addCallback(this)
}
fun update(data: FloatArray) {
val s = surface
if (s != null && s.isValid) {
render(s, data)
}
}
override fun surfaceCreated(holder: SurfaceHolder) {
surface = holder.surface
}
override fun surfaceChanged(holder: SurfaceHolder, format: Int, width: Int, height: Int) {
surface = holder.surface
}
override fun surfaceDestroyed(holder: SurfaceHolder) {
surface = null
}
}
}
@@ -0,0 +1,64 @@
package com.michatec.radio.helpers
import android.app.Activity
import android.content.Context
import android.content.res.Configuration
import android.util.Log
import com.michatec.radio.R
import java.util.Locale
/*
* LanguageHelper object
*/
object LanguageHelper {
/* Define log tag */
private val TAG: String = LanguageHelper::class.java.simpleName
/* Sets the app language on the activity */
fun setLanguage(context: Context, languageCode: String): Boolean {
if (languageCode.isEmpty()) {
Log.i(TAG, "No language code provided, using system default")
return false
}
if (languageCode == "system") {
Log.i(TAG, "Reverting to system default locale")
if (context is Activity) {
context.recreate()
}
return true
}
val locale = Locale.forLanguageTag(languageCode)
Locale.setDefault(locale)
if (context is Activity) {
context.recreate()
}
Log.i(TAG, "Locale changed to: $languageCode")
return true
}
/* Returns a localized resources object */
fun getCurrentLanguage(context: Context): String {
return when (val languageCode = PreferencesHelper.loadSelectedLanguage()) {
"system" -> context.getString(R.string.pref_language_system)
"en" -> context.getString(R.string.pref_language_en)
"de" -> context.getString(R.string.pref_language_de)
"fr" -> context.getString(R.string.pref_language_fr)
"ru" -> context.getString(R.string.pref_language_ru)
"ja" -> context.getString(R.string.pref_language_ja)
"nl" -> context.getString(R.string.pref_language_nl)
"pl" -> context.getString(R.string.pref_language_pl)
"el" -> context.getString(R.string.pref_language_el)
"da" -> context.getString(R.string.pref_language_da)
else -> languageCode
}
}
}
@@ -1,5 +1,6 @@
package com.michatec.radio.helpers
import android.util.Log
import androidx.annotation.OptIn
import androidx.media3.common.C
import androidx.media3.common.audio.AudioProcessor
@@ -13,46 +14,116 @@ import java.nio.ByteOrder
class NativeAudioProcessor : BaseAudioProcessor() {
companion object {
private const val TAG = "NativeAudioProcessor"
init {
System.loadLibrary("radio")
try {
System.loadLibrary("dsp")
} catch (e: Exception) {
Log.e(TAG, "Failed to load dsp library", e)
}
}
}
// JNI Methods
private var directBuffer: ByteBuffer? = null
// ===== JNI =====
private external fun setSampleRate(sampleRate: Float)
private external fun setDrcEnabled(enabled: Boolean)
private external fun setReverbMix(mix: Float)
private external fun setEqBand(band: Int, gainDb: Float)
private external fun setEqFull(gains: FloatArray)
private external fun setBassBoost(gainDb: Float)
private external fun processAudio(data: ShortArray, size: Int)
private external fun setStereoWidth(width: Float)
private external fun processAudioDirect(buf: ByteBuffer, size: Int)
private external fun getFftData(): FloatArray
// Public API
// ===== API =====
fun enableDrc(enabled: Boolean) = setDrcEnabled(enabled)
fun setReverb(mix: Float) = setReverbMix(mix)
fun setEq(band: Int, gainDb: Float) = setEqBand(band, gainDb)
fun setEqAll(gains: FloatArray) = setEqFull(gains)
fun enableBassBoost(gainDb: Float) = setBassBoost(gainDb)
fun setWidth(width: Float) = setStereoWidth(width)
fun getVisualizer(): FloatArray {
val raw = getFftData()
val out = FloatArray(raw.size)
for (i in raw.indices) out[i] = kotlin.math.log10(1f + raw[i])
return out
}
// ===== AudioProcessor Overrides =====
override fun onConfigure(inputAudioFormat: AudioFormat): AudioFormat {
// Always try to support the input format if it is PCM 16-bit
if (inputAudioFormat.encoding != C.ENCODING_PCM_16BIT) {
Log.e(TAG, "Unsupported encoding: ${inputAudioFormat.encoding}")
throw AudioProcessor.UnhandledAudioFormatException(inputAudioFormat)
}
// Pass the actual sample rate to native
setSampleRate(inputAudioFormat.sampleRate.toFloat())
return inputAudioFormat
}
override fun queueInput(inputBuffer: ByteBuffer) {
val remaining = inputBuffer.remaining()
if (remaining == 0) return
val size = inputBuffer.remaining()
if (size == 0) return
val shortArraySize = remaining / 2
val shortArray = ShortArray(shortArraySize)
val bufferToProcess: ByteBuffer
if (inputBuffer.isDirect) {
bufferToProcess = inputBuffer
} else {
if (directBuffer == null || directBuffer!!.capacity() < size) {
directBuffer = ByteBuffer.allocateDirect(size).order(ByteOrder.nativeOrder())
}
directBuffer!!.clear()
inputBuffer.position()
directBuffer!!.put(inputBuffer)
directBuffer!!.flip()
bufferToProcess = directBuffer!!
}
inputBuffer.order(ByteOrder.nativeOrder()).asShortBuffer().get(shortArray)
processAudioDirect(bufferToProcess, size)
processAudio(shortArray, shortArraySize)
val out = replaceOutputBuffer(size)
out.order(ByteOrder.nativeOrder())
bufferToProcess.position(0)
out.put(bufferToProcess)
out.flip()
}
val outputBuffer = replaceOutputBuffer(remaining)
outputBuffer.asShortBuffer().put(shortArray)
outputBuffer.limit(remaining)
override fun onReset() {
super.onReset()
directBuffer = null
}
inputBuffer.position(inputBuffer.limit())
// ===== Presets =====
fun setPresetRock() {
enableDrc(true)
setReverb(0.26f)
setWidth(1.1f)
setEqAll(floatArrayOf(2f, 1f, 0f, -1f, -1f, 0f, 1f, 2f, 2f, 3f))
enableBassBoost(0.9f)
}
fun setPresetPop() {
enableDrc(true)
setReverb(0.18f)
setWidth(1.05f)
setEqAll(floatArrayOf(0f, 1f, 1f, 1f, 0f, 0f, 1f, 2f, 2f, 1f))
enableBassBoost(0.6f)
}
fun setPresetJazz() {
enableDrc(false)
setReverb(0.15f)
setWidth(0.8f)
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,7 +257,7 @@ object PreferencesHelper {
/* Loads Bass Boost gain */
fun loadBassBoost(): Float {
return if (sharedPreferences.getBoolean(Keys.PREF_BASS_BOOST, false)) 5.0f else 0.0f
return if (sharedPreferences.getBoolean(Keys.PREF_BASS_BOOST, false)) 1f else 0.0f
}
@@ -272,16 +272,92 @@ object PreferencesHelper {
return sharedPreferences.getBoolean(Keys.PREF_DRC, false)
}
/* Loads EQ gains */
fun loadEqLow(): Float = sharedPreferences.getInt(Keys.PREF_EQ_LOW, 0).toFloat()
fun loadEqMid(): Float = sharedPreferences.getInt(Keys.PREF_EQ_MID, 0).toFloat()
fun loadEqHigh(): Float = sharedPreferences.getInt(Keys.PREF_EQ_HIGH, 0).toFloat()
/* Loads all EQ bands (10 bands for full range) */
fun loadEqBand(band: Int): Int {
return when (band) {
0 -> sharedPreferences.getInt(Keys.PREF_EQ_LOW, 0)
1 -> sharedPreferences.getInt(Keys.PREF_EQ_BAND_1, 0)
2 -> sharedPreferences.getInt(Keys.PREF_EQ_BAND_2, 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_MID, 0)
7 -> sharedPreferences.getInt(Keys.PREF_EQ_BAND_6, 0)
8 -> sharedPreferences.getInt(Keys.PREF_EQ_BAND_7, 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 selected language */
fun loadSelectedLanguage(): String {
return sharedPreferences.getString(Keys.PREF_LANGUAGE_SELECTED, "system") ?: "system"
}
/* Saves selected language */
fun saveSelectedLanguage(language: String) {
sharedPreferences.edit { putString(Keys.PREF_LANGUAGE_SELECTED, language) }
}
/* Loads preset Bass Boost */
fun loadPresetBassBoost(): Float {
return sharedPreferences.getFloat(Keys.PREF_PRESET_BASS_BOOST, 0f)
}
/* Loads preset Reverb */
fun loadPresetReverb(): Float {
return sharedPreferences.getFloat(Keys.PREF_PRESET_REVERB, 0f)
}
/* Loads preset DRC */
fun loadPresetDrc(): Boolean {
return sharedPreferences.getBoolean(Keys.PREF_PRESET_DRC, false)
}
/* Loads preset Stereo Width */
fun loadPresetStereoWidth(): Float {
return sharedPreferences.getFloat(Keys.PREF_PRESET_STEREO_WIDTH, 1f)
}
/* Resets Equalizer settings to default */
fun resetEqualizer() {
sharedPreferences.edit {
putInt(Keys.PREF_EQ_LOW, 0)
putInt(Keys.PREF_EQ_BAND_1, 0)
putInt(Keys.PREF_EQ_BAND_2, 0)
putInt(Keys.PREF_EQ_BAND_3, 0)
putInt(Keys.PREF_EQ_BAND_4, 0)
putInt(Keys.PREF_EQ_BAND_5, 0)
putInt(Keys.PREF_EQ_MID, 0)
putInt(Keys.PREF_EQ_BAND_6, 0)
putInt(Keys.PREF_EQ_BAND_7, 0)
putInt(Keys.PREF_EQ_HIGH, 0)
}
}
@@ -8,15 +8,21 @@ import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import android.widget.Toast
import androidx.annotation.OptIn
import androidx.appcompat.app.AppCompatActivity
import androidx.lifecycle.DefaultLifecycleObserver
import androidx.lifecycle.LifecycleOwner
import androidx.media3.common.MediaItem
import androidx.media3.common.*
import androidx.media3.common.util.UnstableApi
import androidx.media3.exoplayer.DefaultRenderersFactory
import androidx.media3.exoplayer.ExoPlayer
import androidx.media3.exoplayer.audio.AudioSink
import androidx.media3.exoplayer.audio.DefaultAudioSink
import androidx.recyclerview.widget.RecyclerView
import com.google.android.material.textview.MaterialTextView
import com.michatec.radio.R
import com.michatec.radio.core.Station
import com.michatec.radio.helpers.NativeAudioProcessor
/*
@@ -31,6 +37,7 @@ class SearchResultAdapter(
private var exoPlayer: ExoPlayer? = null
private var paused: Boolean = false
private var isItemSelected: Boolean = false
private var nativeAudioProcessor = NativeAudioProcessor()
/* Listener Interface */
interface SearchResultAdapterListener {
@@ -138,6 +145,7 @@ class SearchResultAdapter(
}
@OptIn(UnstableApi::class)
private fun performPrePlayback(context: Context, streamUri: String) {
if (streamUri.contains(".m3u8")) {
// release previous player if it exists
@@ -151,8 +159,30 @@ class SearchResultAdapter(
// release previous player if it exists
stopPrePlayback()
// create a new instance of ExoPlayer
exoPlayer = ExoPlayer.Builder(context).build()
// set up audio attributes for the preview player
val audioAttributes = androidx.media3.common.AudioAttributes.Builder()
.setContentType(C.AUDIO_CONTENT_TYPE_MUSIC)
.setUsage(C.USAGE_MEDIA)
.build()
// Create a RenderersFactory that injects the NativeAudioProcessor
val renderersFactory = object : DefaultRenderersFactory(context) {
override fun buildAudioSink(
context: Context,
enableFloatOutput: Boolean,
enableAudioTrackPlaybackParams: Boolean
): AudioSink? {
return DefaultAudioSink.Builder(context)
.setAudioProcessors(arrayOf(nativeAudioProcessor))
.build()
}
}
// create a new instance of ExoPlayer with focus handling
exoPlayer = ExoPlayer.Builder(context, renderersFactory)
.setAudioAttributes(audioAttributes, true)
.setHandleAudioBecomingNoisy(true)
.build()
// create a MediaItem with the streamUri
val mediaItem = MediaItem.fromUri(streamUri)
@@ -199,7 +229,7 @@ class SearchResultAdapter(
.setContentType(AudioAttributes.CONTENT_TYPE_MUSIC)
.build()
val focusRequest = AudioFocusRequest.Builder(AudioManager.AUDIOFOCUS_GAIN_TRANSIENT)
val focusRequest = AudioFocusRequest.Builder(AudioManager.AUDIOFOCUS_GAIN)
.setAudioAttributes(audioAttributes)
.build()
@@ -1,13 +1,12 @@
package com.michatec.radio.ui
import android.annotation.SuppressLint
import android.content.ClipData
import android.content.ClipboardManager
import android.content.Context
import android.content.Intent
import android.content.pm.PackageManager
import android.graphics.drawable.AnimatedVectorDrawable
import android.os.Build
import java.util.Locale
import android.view.View
import android.view.animation.Animation
import android.view.animation.AnimationUtils
@@ -129,7 +128,6 @@ data class LayoutHolder(var rootView: View) {
/* Updates the player views */
@SuppressLint("DefaultLocale")
fun updatePlayerViews(context: Context, station: Station, isPlaying: Boolean) {
if (!isPlaying) {
metadataView?.text = station.name
@@ -162,9 +160,9 @@ data class LayoutHolder(var rootView: View) {
} else {
val kiloBytesPerSecond = station.bitrate / 8F
val dataRateString = if (kiloBytesPerSecond >= 1000) {
String.format("%.2f mb/s", kiloBytesPerSecond / 1000F)
String.format(Locale.ROOT, "%.2f mb/s", kiloBytesPerSecond / 1000F)
} else {
String.format("%.0f kb/s", kiloBytesPerSecond)
String.format(Locale.ROOT, "%.0f kb/s", kiloBytesPerSecond)
}
// show the bitrate and codec if the result is available in the radio-browser.info API
buildString {
@@ -190,36 +188,42 @@ data class LayoutHolder(var rootView: View) {
context,
sheetStreamingLinkView?.text ?: ""
)
Snackbar.make(rootView, R.string.toastmessage_copied_to_clipboard, Snackbar.LENGTH_LONG).show()
}
sheetStreamingLinkView?.setOnClickListener {
copyToClipboard(
context,
sheetStreamingLinkView?.text ?: ""
)
Snackbar.make(rootView, R.string.toastmessage_copied_to_clipboard, Snackbar.LENGTH_LONG).show()
}
sheetMetadataHistoryHeadline?.setOnClickListener {
copyToClipboard(
context,
sheetMetadataHistoryView?.text ?: ""
)
Snackbar.make(rootView, R.string.toastmessage_copied_to_clipboard, Snackbar.LENGTH_LONG).show()
}
sheetMetadataHistoryView?.setOnClickListener {
copyToClipboard(
context,
sheetMetadataHistoryView?.text ?: ""
)
Snackbar.make(rootView, R.string.toastmessage_copied_to_clipboard, Snackbar.LENGTH_LONG).show()
}
sheetCopyMetadataButtonView?.setOnClickListener {
copyToClipboard(
context,
sheetMetadataHistoryView?.text ?: ""
)
Snackbar.make(rootView, R.string.toastmessage_copied_to_clipboard, Snackbar.LENGTH_LONG).show()
}
sheetBitrateView?.setOnClickListener {
copyToClipboard(
context,
sheetBitrateView?.text ?: ""
)
Snackbar.make(rootView, R.string.toastmessage_copied_to_clipboard, Snackbar.LENGTH_LONG).show()
}
sheetShareLinkButtonView?.setOnClickListener {
val share = Intent.createChooser(Intent().apply {
@@ -238,10 +242,7 @@ data class LayoutHolder(var rootView: View) {
val clip: ClipData = ClipData.newPlainText("simple text", clipString)
val cm: ClipboardManager = context.getSystemService(Context.CLIPBOARD_SERVICE) as ClipboardManager
cm.setPrimaryClip(clip)
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.TIRAMISU) {
// since API 33 (TIRAMISU) the OS displays its own notification when content is copied to the clipboard
Snackbar.make(rootView, R.string.toastmessage_copied_to_clipboard, Snackbar.LENGTH_LONG).show()
}
Snackbar.make(rootView, R.string.toastmessage_copied_to_clipboard, Snackbar.LENGTH_LONG).show()
}
@@ -251,6 +252,7 @@ data class LayoutHolder(var rootView: View) {
val stringBuilder: StringBuilder = StringBuilder()
metadataHistory.forEach { stringBuilder.append("${it.trim()}\n") }
copyToClipboard(rootView.context, stringBuilder.toString())
Snackbar.make(rootView, R.string.toastmessage_copied_to_clipboard, Snackbar.LENGTH_LONG).show()
}
@@ -1,5 +1,7 @@
<?xml version="1.0" encoding="utf-8"?>
<selector xmlns:android="http://schemas.android.com/apk/res/android">
<!-- Focused (TV remote) states -->
<item android:color="@color/list_card_stroke_focused" android:state_focused="true" />
<!-- Default state -->
<item android:color="@color/list_card_stroke_background" />
</selector>
@@ -0,0 +1,9 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:height="24dp"
android:viewportHeight="960"
android:viewportWidth="960"
android:width="24dp">
<path
android:fillColor="@color/icon_default"
android:pathData="M200,840Q167,840 143.5,816.5Q120,793 120,760L120,200Q120,167 143.5,143.5Q167,120 200,120L760,120Q793,120 816.5,143.5Q840,167 840,200L840,760Q840,793 816.5,816.5Q793,840 760,840L200,840ZM240,503L400,343L560,503L720,343L760,383L760,200Q760,200 760,200Q760,200 760,200L200,200Q200,200 200,200Q200,200 200,200L200,463L240,503ZM200,760L760,760Q760,760 760,760Q760,760 760,760L760,496L720,456L560,616L400,456L240,616L200,576L200,760Q200,760 200,760Q200,760 200,760ZM200,760L200,760Q200,760 200,760Q200,760 200,760L200,496L200,576L200,463L200,383L200,200Q200,200 200,200Q200,200 200,200L200,200Q200,200 200,200Q200,200 200,200L200,463L200,463L200,576L200,576L200,760Q200,760 200,760Q200,760 200,760Z"/>
</vector>
@@ -0,0 +1,9 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:viewportWidth="960"
android:viewportHeight="960">
<path
android:fillColor="@color/icon_default"
android:pathData="M280,680L560,680L560,600L280,600L280,680ZM280,520L680,520L680,440L280,440L280,520ZM280,360L680,360L680,280L280,280L280,360ZM200,840Q167,840 143.5,816.5Q120,793 120,760L120,200Q120,167 143.5,143.5Q167,120 200,120L760,120Q793,120 816.5,143.5Q840,167 840,200L840,760Q840,793 816.5,816.5Q793,840 760,840L200,840ZM200,760L760,760Q760,760 760,760Q760,760 760,760L760,200Q760,200 760,200Q760,200 760,200L200,200Q200,200 200,200Q200,200 200,200L200,760Q200,760 200,760Q200,760 200,760ZM200,200L200,200Q200,200 200,200Q200,200 200,200L200,760Q200,760 200,760Q200,760 200,760L200,760Q200,760 200,760Q200,760 200,760L200,200Q200,200 200,200Q200,200 200,200Z" />
</vector>
@@ -0,0 +1,9 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:height="24dp"
android:viewportHeight="960"
android:viewportWidth="960"
android:width="24dp">
<path
android:fillColor="@color/icon_default"
android:pathData="M200,840Q167,840 143.5,816.5Q120,793 120,760L120,200Q120,167 143.5,143.5Q167,120 200,120L760,120Q793,120 816.5,143.5Q840,167 840,200L840,760Q840,793 816.5,816.5Q793,840 760,840L200,840ZM200,333L760,333L760,200Q760,200 760,200Q760,200 760,200L200,200Q200,200 200,200Q200,200 200,200L200,333ZM200,547L760,547L760,413L200,413L200,547ZM200,760L760,760Q760,760 760,760Q760,760 760,760L760,627L200,627L200,760Q200,760 200,760Q200,760 200,760ZM240,306L240,226L320,226L320,306L240,306ZM240,520L240,440L320,440L320,520L240,520ZM240,734L240,654L320,654L320,734L240,734Z"/>
</vector>
@@ -0,0 +1,9 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:height="24dp"
android:viewportHeight="960"
android:viewportWidth="960"
android:width="24dp">
<path
android:fillColor="@color/icon_default"
android:pathData="M640,800L640,520L800,520L800,800L640,800ZM400,800L400,160L560,160L560,800L400,800ZM160,800L160,360L320,360L320,800L160,800Z"/>
</vector>
@@ -78,6 +78,9 @@
android:layout_height="wrap_content"
android:layout_weight="1"
android:layout_marginHorizontal="16dp"
android:ellipsize="marquee"
android:marqueeRepeatLimit="marquee_forever"
android:singleLine="true"
android:textAlignment="center"
android:textAppearance="@style/TextAppearance.Material3.BodyMedium"
android:textColor="@color/player_sheet_text_main"
@@ -11,6 +11,7 @@
android:layout_height="0dp"
android:clipToPadding="false"
android:paddingBottom="16dp"
android:nextFocusRight="@id/dialog_negative_button"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toStartOf="@+id/guideline"
app:layout_constraintStart_toStartOf="parent"
@@ -39,6 +40,7 @@
style="@style/Widget.Material3.Button"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:nextFocusLeft="@id/station_list"
android:text="@string/dialog_find_station_button_add" />
<Button
@@ -47,6 +49,7 @@
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginTop="12dp"
android:nextFocusLeft="@id/station_list"
android:text="@string/dialog_generic_button_cancel" />
</LinearLayout>
@@ -9,6 +9,11 @@
android:id="@+id/station_search_box_view"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:focusable="true"
android:focusableInTouchMode="true"
android:clickable="true"
android:nextFocusRight="@id/dialog_negative_button"
android:nextFocusDown="@id/station_search_result_list"
app:iconifiedByDefault="false"
app:layout_constraintEnd_toStartOf="@+id/guideline"
app:layout_constraintStart_toStartOf="parent"
@@ -44,6 +49,8 @@
android:layout_marginTop="8dp"
android:clipToPadding="false"
android:paddingBottom="16dp"
android:nextFocusRight="@id/dialog_negative_button"
android:nextFocusUp="@id/station_search_box_view"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toStartOf="@+id/guideline"
app:layout_constraintStart_toStartOf="parent"
@@ -73,6 +80,8 @@
style="@style/Widget.Material3.Button"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:nextFocusUp="@id/station_search_box_view"
android:nextFocusLeft="@id/station_search_result_list"
android:text="@string/dialog_find_station_button_add" />
<Button
@@ -81,6 +90,7 @@
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginTop="12dp"
android:nextFocusLeft="@id/station_search_result_list"
android:text="@string/dialog_generic_button_cancel" />
</LinearLayout>
@@ -0,0 +1,32 @@
<?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_language_selection_title"
android:textSize="24sp"
android:textStyle="bold"
android:paddingBottom="16dp" />
<ScrollView
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:scrollbars="vertical">
<RadioGroup
android:id="@+id/language_radio_group"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="vertical">
</RadioGroup>
</ScrollView>
</LinearLayout>
@@ -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>
@@ -7,7 +7,7 @@
android:layout_marginBottom="8dp"
android:focusable="true"
android:clickable="true"
android:nextFocusRight="@+id/dialog_positive_button"
android:nextFocusRight="@+id/dialog_negative_button"
android:background="@drawable/selector_search_result_item">
<androidx.constraintlayout.widget.ConstraintLayout
@@ -20,6 +20,7 @@
android:layout_width="0dp"
android:layout_height="wrap_content"
android:ellipsize="marquee"
android:marqueeRepeatLimit="marquee_forever"
android:singleLine="true"
android:textAppearance="@style/TextAppearance.Material3.TitleLarge"
android:textColor="@color/text_default"
@@ -35,6 +36,7 @@
android:layout_height="wrap_content"
android:layout_marginTop="4dp"
android:ellipsize="marquee"
android:marqueeRepeatLimit="marquee_forever"
android:singleLine="true"
android:textAppearance="@style/TextAppearance.Material3.BodyLarge"
android:textColor="@color/text_lightweight"
@@ -72,10 +72,11 @@
<com.google.android.material.textview.MaterialTextView
android:id="@+id/player_station_metadata"
android:marqueeRepeatLimit="marquee_forever"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_marginTop="4dp"
android:ellipsize="end"
android:ellipsize="marquee"
android:singleLine="true"
android:textAlignment="center"
android:textAppearance="@style/TextAppearance.Material3.BodyLarge"
@@ -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>
@@ -56,7 +56,8 @@
android:layout_marginStart="12dp"
android:layout_marginEnd="12dp"
android:layout_marginBottom="8dp"
android:ellipsize="end"
android:ellipsize="marquee"
android:marqueeRepeatLimit="marquee_forever"
android:letterSpacing="0"
android:singleLine="true"
android:textAlignment="center"
@@ -22,6 +22,24 @@
app:strokeColor="@color/list_card_stroke_background"
app:strokeWidth="3dp" />
<com.google.android.material.floatingactionbutton.ExtendedFloatingActionButton
android:id="@+id/card_visualizer"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="end"
android:layout_marginTop="10dp"
android:layout_marginEnd="70dp"
android:layout_marginBottom="24dp"
android:focusable="true"
android:clickable="true"
android:stateListAnimator="@null"
app:backgroundTint="@color/list_card_background"
app:icon="@drawable/ic_visualizer_24dp"
app:iconTint="@color/icon_default"
app:rippleColor="@color/list_card_stroke_background"
app:strokeColor="@color/list_card_stroke_background"
app:strokeWidth="3dp" />
<com.google.android.material.floatingactionbutton.ExtendedFloatingActionButton
android:id="@+id/card_settings"
android:layout_width="wrap_content"
+12 -1
View File
@@ -61,6 +61,17 @@
app:shapeAppearanceOverlay="@style/RoundedCorners"
app:srcCompat="@drawable/ic_image_white_36dp" />
<CheckBox
android:id="@+id/reorder_checkbox"
android:layout_width="24dp"
android:layout_height="24dp"
android:foregroundTint="@color/icon_default"
android:visibility="gone"
app:layout_constraintBottom_toBottomOf="@+id/station_icon"
app:layout_constraintStart_toEndOf="@+id/starred_icon"
app:layout_constraintTop_toTopOf="@+id/station_icon"
tools:visibility="visible" />
<ImageView
android:id="@+id/starred_icon"
android:layout_width="wrap_content"
@@ -86,7 +97,7 @@
android:textColor="@color/text_lightweight"
app:layout_constraintBottom_toBottomOf="@+id/station_icon"
app:layout_constraintEnd_toStartOf="@+id/playback_button"
app:layout_constraintStart_toEndOf="@+id/starred_icon"
app:layout_constraintStart_toEndOf="@+id/reorder_checkbox"
app:layout_constraintTop_toTopOf="@+id/station_icon"
tools:text="@string/sample_text_station_name" />
@@ -0,0 +1,32 @@
<?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_language_selection_title"
android:textSize="18sp"
android:textStyle="bold"
android:paddingBottom="12dp" />
<ScrollView
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:scrollbars="vertical">
<RadioGroup
android:id="@+id/language_radio_group"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="vertical">
</RadioGroup>
</ScrollView>
</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>
@@ -0,0 +1,5 @@
<?xml version="1.0" encoding="utf-8"?>
<adaptive-icon xmlns:android="http://schemas.android.com/apk/res/android">
<background android:drawable="@color/ic_banner_background"/>
<foreground android:drawable="@mipmap/ic_banner_foreground"/>
</adaptive-icon>
@@ -0,0 +1,5 @@
<?xml version="1.0" encoding="utf-8"?>
<adaptive-icon xmlns:android="http://schemas.android.com/apk/res/android">
<background android:drawable="@color/ic_channel_background"/>
<foreground android:drawable="@mipmap/ic_channel_foreground"/>
</adaptive-icon>
Binary file not shown.

After

Width:  |  Height:  |  Size: 5.4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.3 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 9.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 36 KiB

@@ -13,6 +13,12 @@
<action
android:id="@+id/action_map_fragment_to_settings_fragment"
app:destination="@id/settings_destination" />
<action
android:id="@+id/action_map_fragment_to_visualizer_fragment"
app:destination="@id/visualizer_destination" />
<action
android:id="@+id/action_map_fragment_to_player_to_add_station"
app:destination="@id/add_station_destination" />
</fragment>
<!-- SETTINGS -->
@@ -23,6 +29,9 @@
<action
android:id="@+id/action_settings_to_equalizer"
app:destination="@id/equalizer_destination" />
<action
android:id="@+id/action_settings_to_visualizer"
app:destination="@id/visualizer_destination" />
</fragment>
<!-- EQUALIZER -->
@@ -31,4 +40,16 @@
android:name="com.michatec.radio.EqualizerFragment"
android:label="Equalizer" />
<!-- VISUALIZER -->
<fragment
android:id="@+id/visualizer_destination"
android:name="com.michatec.radio.VisualizerFragment"
android:label="Visualizer" />
<!-- ADD STATION (TV) -->
<fragment
android:id="@+id/add_station_destination"
android:name="com.michatec.radio.AddStationFragment"
android:label="Add Station"
tools:layout="@layout/dialog_find_station" />
</navigation>
+42
View File
@@ -60,6 +60,7 @@
<string name="pref_buffer_size_title">Brug større buffer</string>
<string name="pref_edit_station_stream_summary_disabled">Redigering af stream-links er deaktiveret.</string>
<string name="pref_edit_station_stream_summary_enabled">Redigering af stream-links er aktiveret. Sørg for at angive en korrekt streamadresse.</string>
<string name="pref_edit_station_stream_title">Rediger stream-links</string>
<string name="pref_edit_station_summary_disabled">Redigering af stationsoplysninger er deaktiveret.</string>
<string name="pref_edit_station_summary_enabled">Redigering er aktiveret. Langt tryk for at redigere.</string>
<string name="pref_edit_station_title">Rediger station</string>
@@ -110,4 +111,45 @@
<!-- Snackbars -->
<string name="snackbar_show">Vis</string>
<string name="snackbar_update_available">er tilgængelig!</string>
<!-- Language Selection -->
<string name="pref_language_selection_title">Sprog</string>
<string name="pref_language_selection_summary">Aktuelt sprog</string>
<string name="pref_language_system">🗺️ System</string>
<!-- Settings -->
<string name="pref_update_collection_title">Opdater stationer</string>
<string name="pref_update_collection_summary">Download den nyeste version af alle stationer.</string>
<string name="dialog_yes_no_message_update_collection">Download den nyeste version af alle stationer?</string>
<string name="dialog_yes_no_positive_button_update_collection">Opdater</string>
<string name="pref_audio_effects_title">Lydeffekter</string>
<string name="pref_bass_boost_title">Bas-forstærkning</string>
<string name="pref_bass_boost_summary">Øg basforstærkningen.</string>
<string name="pref_reverb_title">Hall</string>
<string name="pref_reverb_summary">Juster hall-blanding.</string>
<string name="pref_drc_title">Dynamisk rækkeviddekomprimering</string>
<string name="pref_drc_summary">Komprimer det dynamiske område for konsistent lydstyrke.</string>
<string name="pref_eq_low_title">Equalizer: 31 Hz</string>
<string name="pref_eq_mid_title">Equalizer: 125 Hz</string>
<string name="pref_eq_high_title">Equalizer: 4 kHz</string>
<string name="pref_eq_band_1_title">Equalizer: 62 Hz</string>
<string name="pref_eq_band_2_title">Equalizer: 250 Hz</string>
<string name="pref_eq_band_3_title">Equalizer: 500 Hz</string>
<string name="pref_eq_band_4_title">Equalizer: 1 kHz</string>
<string name="pref_eq_band_5_title">Equalizer: 2 kHz</string>
<string name="pref_eq_band_6_title">Equalizer: 8 kHz</string>
<string name="pref_eq_band_7_title">Equalizer: 16 kHz</string>
<string name="pref_equalizer_title">Equalizer</string>
<string name="pref_equalizer_summary">Juster lydfrekvenserne.</string>
<string name="pref_equalizer_summary_off">Justering af lydfrekvenserne er deaktiveret.</string>
<string name="pref_equalizer_reset_title">Nulstil equalizer</string>
<string name="pref_preset_selection_title">Vælg forudindstilling</string>
<string name="pref_preset_selection_summary">Vælg en lydforudindstilling</string>
<string name="pref_preset_none">Ingen (Manuel)</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">Flad</string>
<string name="loading">Indlæser…</string>
<string name="media_route_menu_title">Cast</string>
<string name="pref_visualizer_title">Spektrumanalysator</string>
<string name="pref_visualizer_summary">Vis spektrumanalysatoren.</string>
</resources>
+27 -3
View File
@@ -55,7 +55,6 @@
<string name="pref_update_collection_summary">Die neueste Version aller Senderinformationen herunterladen.</string>
<string name="dialog_yes_no_message_update_collection">Die neueste Version aller Senderinformationen herunterladen?</string>
<string name="dialog_yes_no_positive_button_update_collection">Aktualisieren</string>
<string name="pref_eq_high_title">Equalizer: Höhen</string>
<string name="pref_advanced_title">Erweitert</string>
<string name="pref_app_version_summary">Version</string>
<string name="pref_app_version_title">App-Version</string>
@@ -70,6 +69,11 @@
<string name="pref_edit_station_summary_enabled">Die Bearbeitung von Senderinformationen ist aktiviert. Drücke lange, um den Bearbeitungsmodus aufzurufen.</string>
<string name="pref_edit_station_title">Sender bearbeiten</string>
<string name="pref_general_title">Allgemein</string>
<!-- Language Selection -->
<string name="pref_language_selection_title">Sprache</string>
<string name="pref_language_selection_summary">Aktuelle Sprache</string>
<string name="pref_language_system">🗺️ System</string>
<string name="pref_language_en" translatable="false">🇬🇧 Englisch</string>
<string name="pref_license_title">Diese Anwendung ist Open Source</string>
<string name="pref_license_summary">Lizenziert unter der GPLv3 License</string>
<string name="pref_links_title">Links</string>
@@ -125,9 +129,29 @@
<string name="pref_reverb_summary">Reverb-Mix anpassen.</string>
<string name="pref_drc_title">Dynamikkompression</string>
<string name="pref_drc_summary">Den Dynamikbereich für eine gleichbleibende Lautstärke komprimieren.</string>
<string name="pref_eq_low_title">Equalizer: Bass</string>
<string name="pref_eq_mid_title">Equalizer: Mitten</string>
<string name="pref_eq_low_title">Equalizer: 31 Hz</string>
<string name="pref_eq_mid_title">Equalizer: 2 kHz</string>
<string name="pref_eq_high_title">Equalizer: 16 kHz</string>
<string name="pref_eq_band_1_title">Equalizer: 62 Hz</string>
<string name="pref_eq_band_2_title">Equalizer: 125 Hz</string>
<string name="pref_eq_band_3_title">Equalizer: 250 Hz</string>
<string name="pref_eq_band_4_title">Equalizer: 500 Hz</string>
<string name="pref_eq_band_5_title">Equalizer: 1 kHz</string>
<string name="pref_eq_band_6_title">Equalizer: 4 kHz</string>
<string name="pref_eq_band_7_title">Equalizer: 8 kHz</string>
<string name="pref_equalizer_title">Equalizer</string>
<string name="pref_equalizer_summary">Passen Sie die Audio-Frequenzen an.</string>
<string name="pref_equalizer_summary_off">Das Anpassen von den Audio-Frequenzen ist deaktiviert.</string>
<string name="pref_equalizer_reset_title">Equalizer zurücksetzen</string>
<string name="pref_preset_selection_title">Preset auswählen</string>
<string name="pref_preset_selection_summary">Wählen Sie ein Klangprofil</string>
<string name="pref_preset_none">Keines (Manuell)</string>
<string name="pref_preset_rock">Rock</string>
<string name="pref_preset_pop">Pop</string>
<string name="pref_preset_jazz">Jazz</string>
<string name="pref_preset_flat">Flach</string>
<string name="media_route_menu_title">Streamen</string>
<string name="pref_visualizer_title">Spektrumanzeige</string>
<string name="pref_visualizer_summary">Sehe die Spektrumanzeige.</string>
<string name="loading">Lade…</string>
</resources>
+45
View File
@@ -16,6 +16,7 @@
<string name="descr_player_playback_button">Αναπαραγωγή/Παύση</string>
<string name="descr_player_station_image">Εικόνα σταθμού</string>
<!-- Dialogs -->
<string name="dialog_add_station_title">Προσθήκη Σταθμού</string>
<string name="dialog_edit_station_name">Όνομα σταθμού</string>
<string name="dialog_edit_stream_uri">Διεύθυνση ροής</string>
<string name="dialog_error_message_default">Προέκυψε ένα σφάλμα</string>
@@ -29,6 +30,7 @@
<string name="dialog_generic_button_okay">ΟΚ</string>
<string name="dialog_generic_details_button">Εμφάνιση λεπτομερειών</string>
<string name="dialog_opml_import_details_default">Δεν υπάρχουν διαθέσιμες λεπτομέρειες</string>
<string name="dialog_restore_collection_replace_existing">Αντικατάσταση της τρέχουσας συλλογής ραδιοφωνικών σταθμών με τον ραδιοφωνικό σταθμό από το αντίγραφο ασφαλείας;</string>
<string name="dialog_yes_no_message_remove_station">Αφαίρεση αυτού του σταθμού;</string>
<string name="dialog_yes_no_message_update_station_images">Να κατεβεί η τελευταία έκδοση όλων των εικόνων σταθμών;</string>
<string name="dialog_yes_no_positive_button_default">Ναι</string>
@@ -43,6 +45,7 @@
<string name="notification_skip_to_previous">Προηγούμενο</string>
<string name="notification_skip_to_next">Επόμενο</string>
<!-- Onboarding -->
<string name="onboarding_app_description">Βυθιστείτε στον ήχο της επιλογής σας!</string>
<string name="onboarding_app_get_started">Ας ξεκινήσουμε</string>
<!-- Player -->
<string name="player_sheet_h2_station_metadata">Παίζεται τώρα</string>
@@ -91,6 +94,7 @@
<!-- Toasts -->
<string name="toastmessage_backed_up">έχει δημιουργηθεί επιτυχώς το αντίγραφο ασφαλείας.</string>
<string name="toastmessage_copied_to_clipboard">Αντιγράφηκε στο πρόχειρο.</string>
<string name="toastmessage_connection_failed">Η σύνδεση δεν μπόρεσε να δημιουργηθεί ή να επαναφερθεί.</string>
<string name="toastmessage_error_download_error">Σφάλμα λήψης</string>
<string name="toastmessage_error_restart_playback_failed">Αδυναμία εκκίνησης ή επανεκκίνησης της αναπαραγωγής.</string>
<string name="toastmessage_install_file_helper">Παρακαλώ εγκαταστήστε ένα πρόγραμμα διαχείρισης αρχείων.</string>
@@ -109,4 +113,45 @@
<!-- Snackbars -->
<string name="snackbar_show">Εμφάνισε</string>
<string name="snackbar_update_available">είναι διαθέσιμη!</string>
<!-- Language Selection -->
<string name="pref_language_selection_title">Γλώσσα</string>
<string name="pref_language_selection_summary">Τρέχουσα γλώσσα</string>
<string name="pref_language_system">🗺️ Σύστημα</string>
<!-- Settings -->
<string name="pref_update_collection_title">Ενημέρωση Σταθμών</string>
<string name="pref_update_collection_summary">Κατεβάστε την τελευταία έκδοση όλων των σταθμών.</string>
<string name="dialog_yes_no_message_update_collection">Κατεβάστε την τελευταία έκδοση όλων των σταθμών;</string>
<string name="dialog_yes_no_positive_button_update_collection">Ενημέρωση</string>
<string name="pref_audio_effects_title">Ηχητικά Εφέ</string>
<string name="pref_bass_boost_title">Ενίσχυση Μπάσων</string>
<string name="pref_bass_boost_summary">Αύξηση της ενίσχυσης μπάσων.</string>
<string name="pref_reverb_title">Αντήχηση</string>
<string name="pref_reverb_summary">Προσαρμογή μίξης αντήχησης.</string>
<string name="pref_drc_title">Συμπίεση Δυναμικού Εύρους</string>
<string name="pref_drc_summary">Συμπίεση δυναμικού εύρους για σταθερή ένταση.</string>
<string name="pref_eq_low_title">Ισοσταθμιστής: 31 Hz</string>
<string name="pref_eq_mid_title">Ισοσταθμιστής: 125 Hz</string>
<string name="pref_eq_high_title">Ισοσταθμιστής: 4 kHz</string>
<string name="pref_eq_band_1_title">Ισοσταθμιστής: 62 Hz</string>
<string name="pref_eq_band_2_title">Ισοσταθμιστής: 250 Hz</string>
<string name="pref_eq_band_3_title">Ισοσταθμιστής: 500 Hz</string>
<string name="pref_eq_band_4_title">Ισοσταθμιστής: 1 kHz</string>
<string name="pref_eq_band_5_title">Ισοσταθμιστής: 2 kHz</string>
<string name="pref_eq_band_6_title">Ισοσταθμιστής: 8 kHz</string>
<string name="pref_eq_band_7_title">Ισοσταθμιστής: 16 kHz</string>
<string name="pref_equalizer_title">Ισοσταθμιστής</string>
<string name="pref_equalizer_summary">Προσαρμογή ηχητικών συχνοτήτων.</string>
<string name="pref_equalizer_summary_off">Η προσαρμογή ηχητικών συχνοτήτων είναι απενεργοποιημένη.</string>
<string name="pref_equalizer_reset_title">Επαναφορά Ισοσταθμιστή</string>
<string name="pref_preset_selection_title">Επιλογή Προκαθορισμένου</string>
<string name="pref_preset_selection_summary">Επιλέξτε ένα ηχητικό προκαθορισμένο.</string>
<string name="pref_preset_none">Κανένα (Χειροκίνητο)</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">Επίπεδο</string>
<string name="loading">Φόρτωση…</string>
<string name="media_route_menu_title">Μετάδοση</string>
<string name="pref_visualizer_title">Αναλυτής Φάσματος</string>
<string name="pref_visualizer_summary">Εμφάνιση του Αναλυτή Φάσματος.</string>
</resources>
+42
View File
@@ -60,6 +60,7 @@
<string name="pref_buffer_size_title">Utiliser un tampon plus grand</string>
<string name="pref_edit_station_stream_summary_disabled">La modification des liens de streaming est désactivée.</string>
<string name="pref_edit_station_stream_summary_enabled">La modification des liens de streaming est activée. Assurez-vous dentrer une adresse de flux correcte.</string>
<string name="pref_edit_station_stream_title">Modifier les liens de streaming</string>
<string name="pref_edit_station_summary_disabled">La modification des informations de la station est désactivée.</string>
<string name="pref_edit_station_summary_enabled">La modification est activée. Maintenez appuyé pour éditer.</string>
<string name="pref_edit_station_title">Modifier la station</string>
@@ -110,4 +111,45 @@
<!-- Snackbars -->
<string name="snackbar_show">Afficher</string>
<string name="snackbar_update_available">est disponible !</string>
<!-- Language Selection -->
<string name="pref_language_selection_title">Langue</string>
<string name="pref_language_selection_summary">Langue actuelle</string>
<string name="pref_language_system">🗺️ Système</string>
<!-- Settings -->
<string name="pref_update_collection_title">Mettre à jour les stations</string>
<string name="pref_update_collection_summary">Téléchargez la dernière version de toutes les stations.</string>
<string name="dialog_yes_no_message_update_collection">Téléchargez la dernière version de toutes les stations ?</string>
<string name="dialog_yes_no_positive_button_update_collection">Mettre à jour</string>
<string name="pref_audio_effects_title">Effets Audio</string>
<string name="pref_bass_boost_title">Amplification des basses</string>
<string name="pref_bass_boost_summary">Augmenter l amplification des basses.</string>
<string name="pref_reverb_title">Réverbération</string>
<string name="pref_reverb_summary">Ajuster le mix de réverbération.</string>
<string name="pref_drc_title">Compression Dynamique</string>
<string name="pref_drc_summary">Compresser la plage dynamique pour un volume constant.</string>
<string name="pref_eq_low_title">Égaliseur : 31 Hz</string>
<string name="pref_eq_mid_title">Égaliseur : 125 Hz</string>
<string name="pref_eq_high_title">Égaliseur : 4 kHz</string>
<string name="pref_eq_band_1_title">Égaliseur : 62 Hz</string>
<string name="pref_eq_band_2_title">Égaliseur : 250 Hz</string>
<string name="pref_eq_band_3_title">Égaliseur : 500 Hz</string>
<string name="pref_eq_band_4_title">Égaliseur : 1 kHz</string>
<string name="pref_eq_band_5_title">Égaliseur : 2 kHz</string>
<string name="pref_eq_band_6_title">Égaliseur : 8 kHz</string>
<string name="pref_eq_band_7_title">Égaliseur : 16 kHz</string>
<string name="pref_equalizer_title">Égaliseur</string>
<string name="pref_equalizer_summary">Ajuster les fréquences audio.</string>
<string name="pref_equalizer_summary_off">L ajustement des fréquences audio est désactivé.</string>
<string name="pref_equalizer_reset_title">Réinitialiser l égaliseur</string>
<string name="pref_preset_selection_title">Sélectionner un préréglage</string>
<string name="pref_preset_selection_summary">Choisissez un préréglage audio.</string>
<string name="pref_preset_none">Aucun (Manuel)</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">Plat</string>
<string name="loading">Chargement…</string>
<string name="media_route_menu_title">Diffuser</string>
<string name="pref_visualizer_title">Analyseur de spectre</string>
<string name="pref_visualizer_summary">Afficher l analyseur de spectre.</string>
</resources>
+42
View File
@@ -60,6 +60,7 @@
<string name="pref_buffer_size_title">大きなバッファを使用</string>
<string name="pref_edit_station_stream_summary_disabled">ストリームリンクの編集は無効です。</string>
<string name="pref_edit_station_stream_summary_enabled">ストリームリンクの編集は有効です。正しいURLを入力してください。</string>
<string name="pref_edit_station_stream_title">ストリームリンクを編集</string>
<string name="pref_edit_station_summary_disabled">局情報の編集は無効です。</string>
<string name="pref_edit_station_summary_enabled">局情報の編集は有効です。長押しで編集モードに入ります。</string>
<string name="pref_edit_station_title">局を編集</string>
@@ -111,4 +112,45 @@
<!-- スナックバー -->
<string name="snackbar_show">表示</string>
<string name="snackbar_update_available">が利用可能です!</string>
<!-- 言語選択 -->
<string name="pref_language_selection_title">言語</string>
<string name="pref_language_selection_summary">現在の言語</string>
<string name="pref_language_system">🗺️ システム</string>
<!-- 設定 -->
<string name="pref_update_collection_title">局を更新</string>
<string name="pref_update_collection_summary">すべての局の最新バージョンをダウンロードします。</string>
<string name="dialog_yes_no_message_update_collection">すべての局の最新バージョンをダウンロードしますか?</string>
<string name="dialog_yes_no_positive_button_update_collection">更新</string>
<string name="pref_audio_effects_title">オーディオエフェクト</string>
<string name="pref_bass_boost_title">バスブースト</string>
<string name="pref_bass_boost_summary">低音を増強します。</string>
<string name="pref_reverb_title">リバーブ</string>
<string name="pref_reverb_summary">リバーブミスを調整します。</string>
<string name="pref_drc_title">ダイナミックレンジ圧縮</string>
<string name="pref_drc_summary">音量的一定のためダイナミックレンジを圧縮します。</string>
<string name="pref_eq_low_title">イコライザー:31 Hz</string>
<string name="pref_eq_mid_title">イコライザー:125 Hz</string>
<string name="pref_eq_high_title">イコライザー:4 kHz</string>
<string name="pref_eq_band_1_title">イコライザー:62 Hz</string>
<string name="pref_eq_band_2_title">イコライザー:250 Hz</string>
<string name="pref_eq_band_3_title">イコライザー:500 Hz</string>
<string name="pref_eq_band_4_title">イコライザー:1 kHz</string>
<string name="pref_eq_band_5_title">イコライザー:2 kHz</string>
<string name="pref_eq_band_6_title">イコライザー:8 kHz</string>
<string name="pref_eq_band_7_title">イコライザー:16 kHz</string>
<string name="pref_equalizer_title">イコライザー</string>
<string name="pref_equalizer_summary">オーディオ周波数を調整します。</string>
<string name="pref_equalizer_summary_off">オーディオ周波数の調整は無効です。</string>
<string name="pref_equalizer_reset_title">イコライザーリセット</string>
<string name="pref_preset_selection_title">プリセットを選択</string>
<string name="pref_preset_selection_summary">オーディオプリセットを選択</string>
<string name="pref_preset_none">なし(マニュアル)</string>
<string name="pref_preset_rock">ロック</string>
<string name="pref_preset_pop">ポップ</string>
<string name="pref_preset_jazz">ジャズ</string>
<string name="pref_preset_flat">フラット</string>
<string name="loading">読み込み中…</string>
<string name="media_route_menu_title">キャスト</string>
<string name="pref_visualizer_title">スペクトラムアナライザー</string>
<string name="pref_visualizer_summary">スペクトラムアナライザーを表示します。</string>
</resources>
+55
View File
@@ -16,6 +16,7 @@
<string name="descr_player_playback_button">Afspelen/pauzeren</string>
<string name="descr_player_station_image">Zenderafbeelding</string>
<!-- Dialogs -->
<string name="dialog_add_station_title">Zender Toevoegen</string>
<string name="dialog_edit_station_name">Zendernaam</string>
<string name="dialog_edit_stream_uri">Stream adres</string>
<string name="dialog_error_message_default">Er is een fout opgetreden</string>
@@ -29,6 +30,7 @@
<string name="dialog_generic_button_okay">OK</string>
<string name="dialog_generic_details_button">Toon details</string>
<string name="dialog_opml_import_details_default">Geen details beschikbaar</string>
<string name="dialog_restore_collection_replace_existing">Huidige collectie van radiozenders vervangen met de radiozender uit de back-up?</string>
<string name="dialog_yes_no_message_remove_station">Verwijder deze zender?</string>
<string name="dialog_yes_no_message_update_station_images">Laatste versie van alle zenderafbeeldingen downloaden?</string>
<string name="dialog_yes_no_positive_button_default">Ja</string>
@@ -43,6 +45,7 @@
<string name="notification_skip_to_previous">Vorige</string>
<string name="notification_skip_to_next">Volgende</string>
<!-- Onboarding -->
<string name="onboarding_app_description">Dompel jezelf onder in het geluid van je keuze!</string>
<string name="onboarding_app_get_started">Aan de slag</string>
<!-- Player -->
<string name="player_sheet_h2_station_metadata">Nu aan het afspelen</string>
@@ -64,6 +67,11 @@
<string name="pref_general_title">Algemeen</string>
<string name="pref_m3u_export_summary">Sla je radiozenders op in een M3U afspeellijstbestand dat in andere spelers kan worden geïmporteerd.</string>
<string name="pref_m3u_export_title">Exporteer M3U</string>
<string name="pref_pls_export_summary">Sla je radiozenders op in een PLS afspeellijstbestand dat in andere spelers kan worden geïmporteerd.</string>
<string name="pref_pls_export_title">Exporteer PLS</string>
<string name="pref_license_title">Deze applicatie is open source</string>
<string name="pref_license_summary">Gelicentieerd onder de GPLv3 licentie</string>
<string name="pref_links_title">Links</string>
<string name="pref_maintenance_title">Onderhoud</string>
<string name="pref_station_export_summary">Sla de gehele collectie radiozenders, inclusief afbeeldingen, in de apparaatopslag op.</string>
<string name="pref_station_export_title">Exporteer Zender</string>
@@ -86,6 +94,12 @@
<!-- Toasts -->
<string name="toastmessage_backed_up">back-up is succesvol gemaakt.</string>
<string name="toastmessage_copied_to_clipboard">Gekopieerd naar het klembord.</string>
<string name="toastmessage_connection_failed">De verbinding kon niet tot stand worden gebracht of hersteld.</string>
<string name="toastmessage_error_restart_playback_failed">Kan afspelen niet starten of herstarten.</string>
<string name="toastmessage_preview_playback_started">Voorbeeld-afspelen is gestart.</string>
<string name="toastmessage_preview_playback_failed">Kan voorbeeld niet afspelen.</string>
<string name="toastmessage_save_pls">Radiozenders opslaan als PLS…</string>
<string name="toastmessage_station_duplicate">Deze zender is een duplicaat.</string>
<string name="toastmessage_error_download_error">Download fout</string>
<string name="toastmessage_install_file_helper">Installeer alstublieft een bestandsbeheer applicatie.</string>
<string name="toastmessage_restored">Zenders zijn succesvol hersteld.</string>
@@ -99,4 +113,45 @@
<!-- Snackbars -->
<string name="snackbar_show">Weergeven</string>
<string name="snackbar_update_available">is beschikbaar!</string>
<!-- Language Selection -->
<string name="pref_language_selection_title">Taal</string>
<string name="pref_language_selection_summary">Huidige taal</string>
<string name="pref_language_system">🗺️ Systeem</string>
<!-- Settings -->
<string name="pref_update_collection_title">Zenders Bijwerken</string>
<string name="pref_update_collection_summary">Download de laatste versie van alle zenders.</string>
<string name="dialog_yes_no_message_update_collection">Download de laatste versie van alle zenders?</string>
<string name="dialog_yes_no_positive_button_update_collection">Bijwerken</string>
<string name="pref_audio_effects_title">Audio Effecten</string>
<string name="pref_bass_boost_title">Bass Boost</string>
<string name="pref_bass_boost_summary">Verhoog de bassversterking.</string>
<string name="pref_reverb_title">Reverb</string>
<string name="pref_reverb_summary">Pas de reverb mix aan.</string>
<string name="pref_drc_title">Dynamisch Bereik Compressie</string>
<string name="pref_drc_summary">Comprimeer het dynamisch bereik voor consistent volume.</string>
<string name="pref_eq_low_title">Equalizer: 31 Hz</string>
<string name="pref_eq_mid_title">Equalizer: 125 Hz</string>
<string name="pref_eq_high_title">Equalizer: 4 kHz</string>
<string name="pref_eq_band_1_title">Equalizer: 62 Hz</string>
<string name="pref_eq_band_2_title">Equalizer: 250 Hz</string>
<string name="pref_eq_band_3_title">Equalizer: 500 Hz</string>
<string name="pref_eq_band_4_title">Equalizer: 1 kHz</string>
<string name="pref_eq_band_5_title">Equalizer: 2 kHz</string>
<string name="pref_eq_band_6_title">Equalizer: 8 kHz</string>
<string name="pref_eq_band_7_title">Equalizer: 16 kHz</string>
<string name="pref_equalizer_title">Equalizer</string>
<string name="pref_equalizer_summary">Pas audio frequenties aan.</string>
<string name="pref_equalizer_summary_off">Het aanpassen van audio frequenties is uitgeschakeld.</string>
<string name="pref_equalizer_reset_title">Reset Equalizer</string>
<string name="pref_preset_selection_title">Selecteer Voorinstelling</string>
<string name="pref_preset_selection_summary">Kies een audio voorinstelling.</string>
<string name="pref_preset_none">Geen (Handmatig)</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">Plat</string>
<string name="loading">Laden…</string>
<string name="media_route_menu_title">Cast</string>
<string name="pref_visualizer_title">Spectrum Analyser</string>
<string name="pref_visualizer_summary">Toon de Spectrum Analyser.</string>
</resources>
+53
View File
@@ -16,6 +16,7 @@
<string name="descr_player_playback_button">Odtwórz/zatrzymaj</string>
<string name="descr_player_station_image">Ikona stacji</string>
<!-- Dialogs -->
<string name="dialog_add_station_title">Dodaj Stację</string>
<string name="dialog_edit_station_name">Nazwa stacji</string>
<string name="dialog_edit_stream_uri">Adres strumienia</string>
<string name="dialog_error_message_default">Wystąpił błąd</string>
@@ -29,6 +30,7 @@
<string name="dialog_generic_button_okay">OK</string>
<string name="dialog_generic_details_button">Pokaż szczegóły</string>
<string name="dialog_opml_import_details_default">Brak dostępnych szczegółów</string>
<string name="dialog_restore_collection_replace_existing">Zastąpić bieżącą kolekcję stacji radiowych stacją z kopii zapasowej?</string>
<string name="dialog_yes_no_message_remove_station">Usunąć stację?</string>
<string name="dialog_yes_no_message_update_station_images">Pobrać najnowszą wersję obrazów wszystkich stacji?</string>
<string name="dialog_yes_no_positive_button_default">Tak</string>
@@ -43,6 +45,7 @@
<string name="notification_skip_to_previous">Poprzedni</string>
<string name="notification_skip_to_next">Następny</string>
<!-- Onboarding -->
<string name="onboarding_app_description">Zanurz się w dźwięku swojego wyboru!</string>
<string name="onboarding_app_get_started">Zaczynamy</string>
<!-- Player -->
<string name="player_sheet_h2_station_metadata">Obecnie odtwarzane</string>
@@ -64,6 +67,11 @@
<string name="pref_general_title">Ogólne</string>
<string name="pref_m3u_export_summary">Zapisz swoje stacje radiowe w pliku listy odtwarzania M3U, który można zaimportować do innych odtwarzaczy.</string>
<string name="pref_m3u_export_title">Eksportuj M3U</string>
<string name="pref_pls_export_summary">Zapisz swoje stacje radiowe w pliku listy odtwarzania PLS, który można zaimportować do innych odtwarzaczy.</string>
<string name="pref_pls_export_title">Eksportuj PLS</string>
<string name="pref_license_title">Ta aplikacja jest open source</string>
<string name="pref_license_summary">Licencjonowane na licencji GPLv3</string>
<string name="pref_links_title">Linki</string>
<string name="pref_maintenance_title">Zarządzanie</string>
<string name="pref_station_export_summary">Zapisz do pamięci urządzenia całą kolekcję stacji radiowych, w tym obrazy.</string>
<string name="pref_station_export_title">Eksportuj stacje</string>
@@ -86,6 +94,10 @@
<!-- Toasts -->
<string name="toastmessage_backed_up">kopia zapasowa została wykonana.</string>
<string name="toastmessage_copied_to_clipboard">Skopiowano do schowka.</string>
<string name="toastmessage_connection_failed">Nie można nawiązać lub przywrócić połączenia.</string>
<string name="toastmessage_preview_playback_started">Rozpoczęto odtwarzanie podglądu.</string>
<string name="toastmessage_preview_playback_failed">Nie można odtworzyć podglądu.</string>
<string name="toastmessage_save_pls">Zapisywanie stacji radiowych jako PLS…</string>
<string name="toastmessage_error_download_error">Błąd pobierania</string>
<string name="toastmessage_error_restart_playback_failed">Nie można ponownie uruchomić odtwarzania.</string>
<string name="toastmessage_install_file_helper">Proszę zainstalować menedżer plików.</string>
@@ -101,4 +113,45 @@
<!-- Snackbars -->
<string name="snackbar_show">Wyświetl</string>
<string name="snackbar_update_available">jest dostępna!</string>
<!-- Language Selection -->
<string name="pref_language_selection_title">Język</string>
<string name="pref_language_selection_summary">Aktualny język</string>
<string name="pref_language_system">🗺️ System</string>
<!-- Settings -->
<string name="pref_update_collection_title">Aktualizacja Stacji</string>
<string name="pref_update_collection_summary">Pobierz najnowszą wersję wszystkich stacji.</string>
<string name="dialog_yes_no_message_update_collection">Pobrać najnowszą wersję wszystkich stacji?</string>
<string name="dialog_yes_no_positive_button_update_collection">Aktualizuj</string>
<string name="pref_audio_effects_title">Efekty Dźwiękowe</string>
<string name="pref_bass_boost_title">Wzmocnienie Basów</string>
<string name="pref_bass_boost_summary">Zwiększ wzmocnienie basów.</string>
<string name="pref_reverb_title">Pogłos</string>
<string name="pref_reverb_summary">Dostosuj miks pogłosu.</string>
<string name="pref_drc_title">Kompresja Zakresu Dynamiki</string>
<string name="pref_drc_summary">Kompresuj zakres dynamiki dla spójnej głośności.</string>
<string name="pref_eq_low_title">Korektor: 31 Hz</string>
<string name="pref_eq_mid_title">Korektor: 125 Hz</string>
<string name="pref_eq_high_title">Korektor: 4 kHz</string>
<string name="pref_eq_band_1_title">Korektor: 62 Hz</string>
<string name="pref_eq_band_2_title">Korektor: 250 Hz</string>
<string name="pref_eq_band_3_title">Korektor: 500 Hz</string>
<string name="pref_eq_band_4_title">Korektor: 1 kHz</string>
<string name="pref_eq_band_5_title">Korektor: 2 kHz</string>
<string name="pref_eq_band_6_title">Korektor: 8 kHz</string>
<string name="pref_eq_band_7_title">Korektor: 16 kHz</string>
<string name="pref_equalizer_title">Korektor</string>
<string name="pref_equalizer_summary">Dostosuj częstotliwości audio.</string>
<string name="pref_equalizer_summary_off">Dostosowanie częstotliwości audio jest wyłączone.</string>
<string name="pref_equalizer_reset_title">Resetuj Korektor</string>
<string name="pref_preset_selection_title">Wybierz Preset</string>
<string name="pref_preset_selection_summary">Wybierz preset audio.</string>
<string name="pref_preset_none">Brak (Ręczny)</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">Płaski</string>
<string name="loading">Ładowanie…</string>
<string name="media_route_menu_title">Przesyłanie</string>
<string name="pref_visualizer_title">Analizator Widma</string>
<string name="pref_visualizer_summary">Pokaż Analizator Widma.</string>
</resources>
+45
View File
@@ -16,6 +16,7 @@
<string name="descr_player_playback_button">Играть/пауза</string>
<string name="descr_player_station_image">Изображение станции</string>
<!-- Dialogs -->
<string name="dialog_add_station_title">Добавить станцию</string>
<string name="dialog_edit_station_name">Имя станции</string>
<string name="dialog_edit_stream_uri">Адрес потока</string>
<string name="dialog_error_message_default">Произошла ошибка</string>
@@ -29,6 +30,7 @@
<string name="dialog_generic_button_okay">ОК</string>
<string name="dialog_generic_details_button">Показать детали</string>
<string name="dialog_opml_import_details_default">Детали недоступны</string>
<string name="dialog_restore_collection_replace_existing">Заменить текущую коллекцию радиостанций радиостанцией из резервной копии?</string>
<string name="dialog_yes_no_message_remove_station">Удалить эту станцию?</string>
<string name="dialog_yes_no_message_update_station_images">Скачать последнюю версию всех изображений станций?</string>
<string name="dialog_yes_no_positive_button_default">Да</string>
@@ -43,6 +45,7 @@
<string name="notification_skip_to_previous">Предыдущий</string>
<string name="notification_skip_to_next">Следующий</string>
<!-- Onboarding -->
<string name="onboarding_app_description">Погрузитесь в звук по вашему выбору!</string>
<string name="onboarding_app_get_started">Начать</string>
<!-- Player -->
<string name="player_sheet_h2_station_metadata">Сейчас играет</string>
@@ -91,6 +94,7 @@
<!-- Toasts -->
<string name="toastmessage_backed_up">резервная копия создана.</string>
<string name="toastmessage_copied_to_clipboard">Скопировано в буфер обмена.</string>
<string name="toastmessage_connection_failed">Не удалось установить или восстановить соединение.</string>
<string name="toastmessage_error_download_error">Ошибка загрузки</string>
<string name="toastmessage_error_restart_playback_failed">Невозможно запустить или перезапустить воспроизведение.</string>
<string name="toastmessage_install_file_helper">Пожалуйста, установите файловый менеджер.</string>
@@ -109,4 +113,45 @@
<!-- Snackbars -->
<string name="snackbar_show">Показать</string>
<string name="snackbar_update_available">доступно!</string>
<!-- Language Selection -->
<string name="pref_language_selection_title">Язык</string>
<string name="pref_language_selection_summary">Текущий язык</string>
<string name="pref_language_system">🗺️ Система</string>
<!-- Settings -->
<string name="pref_update_collection_title">Обновление станций</string>
<string name="pref_update_collection_summary">Скачать последнюю версию всех станций.</string>
<string name="dialog_yes_no_message_update_collection">Скачать последнюю версию всех станций?</string>
<string name="dialog_yes_no_positive_button_update_collection">Обновить</string>
<string name="pref_audio_effects_title">Звуковые эффекты</string>
<string name="pref_bass_boost_title">Усиление басов</string>
<string name="pref_bass_boost_summary">Увеличить усиление басов.</string>
<string name="pref_reverb_title">Реверберация</string>
<string name="pref_reverb_summary">Настроить микс реверберации.</string>
<string name="pref_drc_title">Динамическое сжатие</string>
<string name="pref_drc_summary">Сжать динамический диапазон для постоянной громкости.</string>
<string name="pref_eq_low_title">Эквалайзер: 31 Гц</string>
<string name="pref_eq_mid_title">Эквалайзер: 125 Гц</string>
<string name="pref_eq_high_title">Эквалайзер: 4 кГц</string>
<string name="pref_eq_band_1_title">Эквалайзер: 62 Гц</string>
<string name="pref_eq_band_2_title">Эквалайзер: 250 Гц</string>
<string name="pref_eq_band_3_title">Эквалайзер: 500 Гц</string>
<string name="pref_eq_band_4_title">Эквалайзер: 1 кГц</string>
<string name="pref_eq_band_5_title">Эквалайзер: 2 кГц</string>
<string name="pref_eq_band_6_title">Эквалайзер: 8 кГц</string>
<string name="pref_eq_band_7_title">Эквалайзер: 16 кГц</string>
<string name="pref_equalizer_title">Эквалайзер</string>
<string name="pref_equalizer_summary">Настроить звуковые частоты.</string>
<string name="pref_equalizer_summary_off">Настройка звуковых частот отключена.</string>
<string name="pref_equalizer_reset_title">Сбросить эквалайзер</string>
<string name="pref_preset_selection_title">Выбрать пресет</string>
<string name="pref_preset_selection_summary">Выберите звуковой пресет.</string>
<string name="pref_preset_none">Нет (Ручной)</string>
<string name="pref_preset_rock">Рок</string>
<string name="pref_preset_pop">Поп</string>
<string name="pref_preset_jazz">Джаз</string>
<string name="pref_preset_flat">Плоский</string>
<string name="loading">Загрузка…</string>
<string name="media_route_menu_title">Трансляция</string>
<string name="pref_visualizer_title">Анализатор спектра</string>
<string name="pref_visualizer_summary">Показать анализатор спектра.</string>
</resources>
+44
View File
@@ -16,6 +16,7 @@
<string name="descr_player_playback_button">Відтворити/призупинити</string>
<string name="descr_player_station_image">Зображення станції</string>
<!-- Dialogs -->
<string name="dialog_add_station_title">Додати станцію</string>
<string name="dialog_edit_station_name">Назва станції</string>
<string name="dialog_edit_stream_uri">Адреса трансляції</string>
<string name="dialog_error_message_default">Виникла помилка</string>
@@ -29,6 +30,7 @@
<string name="dialog_generic_button_okay">Добре</string>
<string name="dialog_generic_details_button">Показати подробиці</string>
<string name="dialog_opml_import_details_default">Подробиці недоступні</string>
<string name="dialog_restore_collection_replace_existing">Замінити поточну колекцію радіостанцій радіостанцією з резервної копії?</string>
<string name="dialog_yes_no_message_remove_station">Видалити цю станцію?</string>
<string name="dialog_yes_no_message_update_station_images">Завантажити останню версію всіх зображень станцій?</string>
<string name="dialog_yes_no_positive_button_default">Так</string>
@@ -43,6 +45,7 @@
<string name="notification_skip_to_previous">Попередня</string>
<string name="notification_skip_to_next">Наступна</string>
<!-- Onboarding -->
<string name="onboarding_app_description">Пориньте у звук на ваш вибір!</string>
<string name="onboarding_app_get_started">Початок роботи</string>
<!-- Player -->
<string name="player_sheet_h2_station_metadata">Зараз грає</string>
@@ -110,4 +113,45 @@
<!-- Snackbars -->
<string name="snackbar_show">Показати</string>
<string name="snackbar_update_available">доступне!</string>
<!-- Language Selection -->
<string name="pref_language_selection_title">Мова</string>
<string name="pref_language_selection_summary">Поточна мова</string>
<string name="pref_language_system">🗺️ Система</string>
<!-- Settings -->
<string name="pref_update_collection_title">Оновлення станцій</string>
<string name="pref_update_collection_summary">Завантажити останню версію всіх станцій.</string>
<string name="dialog_yes_no_message_update_collection">Завантажити останню версію всіх станцій?</string>
<string name="dialog_yes_no_positive_button_update_collection">Оновити</string>
<string name="pref_audio_effects_title">Звукові ефекти</string>
<string name="pref_bass_boost_title">Підсилення басів</string>
<string name="pref_bass_boost_summary">Збільшити підсилення басів.</string>
<string name="pref_reverb_title">Реверберація</string>
<string name="pref_reverb_summary">Налаштувати мікс реверберації.</string>
<string name="pref_drc_title">Динамічна компресія</string>
<string name="pref_drc_summary">Стиснути динамічний діапазон для стабільної гучності.</string>
<string name="pref_eq_low_title">Еквалайзер: 31 Гц</string>
<string name="pref_eq_mid_title">Еквалайзер: 125 Гц</string>
<string name="pref_eq_high_title">Еквалайзер: 4 кГц</string>
<string name="pref_eq_band_1_title">Еквалайзер: 62 Гц</string>
<string name="pref_eq_band_2_title">Еквалайзер: 250 Гц</string>
<string name="pref_eq_band_3_title">Еквалайзер: 500 Гц</string>
<string name="pref_eq_band_4_title">Еквалайзер: 1 кГц</string>
<string name="pref_eq_band_5_title">Еквалайзер: 2 кГц</string>
<string name="pref_eq_band_6_title">Еквалайзер: 8 кГц</string>
<string name="pref_eq_band_7_title">Еквалайзер: 16 кГц</string>
<string name="pref_equalizer_title">Еквалайзер</string>
<string name="pref_equalizer_summary">Налаштувати звукові частоти.</string>
<string name="pref_equalizer_summary_off">Налаштування звукових частот вимкнено.</string>
<string name="pref_equalizer_reset_title">Скинути еквалайзер</string>
<string name="pref_preset_selection_title">Вибрати пресет</string>
<string name="pref_preset_selection_summary">Виберіть звуковий пресет.</string>
<string name="pref_preset_none">Немає (Ручний)</string>
<string name="pref_preset_rock">Рок</string>
<string name="pref_preset_pop">Поп</string>
<string name="pref_preset_jazz">Джаз</string>
<string name="pref_preset_flat">Плаский</string>
<string name="loading">Завантаження…</string>
<string name="media_route_menu_title">Трансляція</string>
<string name="pref_visualizer_title">Аналізатор спектру</string>
<string name="pref_visualizer_summary">Показати аналізатор спектру.</string>
</resources>
@@ -0,0 +1,4 @@
<?xml version="1.0" encoding="utf-8"?>
<resources>
<color name="ic_banner_background">#2C67E6</color>
</resources>
@@ -0,0 +1,4 @@
<?xml version="1.0" encoding="utf-8"?>
<resources>
<color name="ic_channel_background">#0A5DBC</color>
</resources>
+36 -4
View File
@@ -58,6 +58,21 @@
<string name="player_sheet_h2_station_metadata">Currently playing</string>
<string name="player_sheet_h2_stream_url">Streaming link</string>
<!-- Language Selection -->
<string name="pref_language_selection_title">Language</string>
<string name="pref_language_selection_summary">Current language</string>
<string name="pref_language_system">🗺️ System</string>
<string name="pref_language_en" translatable="false">🇬🇧 English</string>
<string name="pref_language_de" translatable="false">🇩🇪 Deutsch</string>
<string name="pref_language_fr" translatable="false">🇫🇷 Français</string>
<string name="pref_language_ru" translatable="false">🇷🇺 Русский</string>
<string name="pref_language_ja" translatable="false">🇯🇵 日本語</string>
<string name="pref_language_nl" translatable="false">🇳🇱 Nederlands</string>
<string name="pref_language_pl" translatable="false">🇵🇱 Polski</string>
<string name="pref_language_el" translatable="false">🇬🇷 Ελληνικά</string>
<string name="pref_language_da" translatable="false">🇩🇰 Dansk</string>
<!-- Settings -->
<string name="pref_update_collection_title">Update Stations</string>
<string name="pref_update_collection_summary">Download latest version of all station.</string>
@@ -70,12 +85,27 @@
<string name="pref_reverb_summary">Adjust reverb mix.</string>
<string name="pref_drc_title">Dynamic Range Compression</string>
<string name="pref_drc_summary">Compress dynamic range for consistent volume.</string>
<string name="pref_eq_low_title">Equalizer: Low</string>
<string name="pref_eq_mid_title">Equalizer: Mid</string>
<string name="pref_eq_high_title">Equalizer: High</string>
<string name="pref_eq_low_title">Equalizer: 31 Hz</string>
<string name="pref_eq_mid_title">Equalizer: 125 Hz</string>
<string name="pref_eq_high_title">Equalizer: 4 kHz</string>
<string name="pref_eq_band_1_title">Equalizer: 62 Hz</string>
<string name="pref_eq_band_2_title">Equalizer: 250 Hz</string>
<string name="pref_eq_band_3_title">Equalizer: 500 Hz</string>
<string name="pref_eq_band_4_title">Equalizer: 1 kHz</string>
<string name="pref_eq_band_5_title">Equalizer: 2 kHz</string>
<string name="pref_eq_band_6_title">Equalizer: 8 kHz</string>
<string name="pref_eq_band_7_title">Equalizer: 16 kHz</string>
<string name="pref_equalizer_title">Equalizer</string>
<string name="pref_equalizer_summary">Adjust audio frequencies</string>
<string name="pref_equalizer_summary_off">Adjust audio frequencies is off.</string>
<string name="pref_equalizer_reset_title">Reset Equalizer</string>
<string name="pref_preset_selection_title">Select Preset</string>
<string name="pref_preset_selection_summary">Choose an audio preset</string>
<string name="pref_preset_none">None (Manual)</string>
<string name="pref_preset_rock">Rock</string>
<string name="pref_preset_pop">Pop</string>
<string name="pref_preset_jazz">Jazz</string>
<string name="pref_preset_flat">Flat</string>
<string name="pref_advanced_title">Advanced</string>
<string name="pref_app_version_summary">Version</string>
<string name="pref_app_version_title">App Version</string>
@@ -156,6 +186,8 @@
<string name="icon_launcher" translatable="false">Icon launcher.</string>
<!-- Extras -->
<string name="loading">Loading...</string>
<string name="loading">Loading</string>
<string name="media_route_menu_title">Cast</string>
<string name="pref_visualizer_title">Spectrum Analyzer</string>
<string name="pref_visualizer_summary">Show the Spectrum Analyzer.</string>
</resources>
-9
View File
@@ -1,22 +1,13 @@
<?xml version="1.0" encoding="utf-8"?>
<locale-config xmlns:android="http://schemas.android.com/apk/res/android">
<locale android:name="en" />
<locale android:name="ar" />
<locale android:name="bg" />
<locale android:name="cs" />
<locale android:name="de" />
<locale android:name="el" />
<locale android:name="fr" />
<locale android:name="hu" />
<locale android:name="it" />
<locale android:name="nl" />
<locale android:name="pl" />
<locale android:name="pt" />
<locale android:name="ru" />
<locale android:name="ro" />
<locale android:name="tr" />
<locale android:name="uk" />
<locale android:name="zh-rCN" />
<locale android:name="da" />
<locale android:name="ja" />
</locale-config>
-11
View File
@@ -1,11 +0,0 @@
// Top-level build file where you can add configuration options common to all sub-projects/modules.
plugins {
alias libs.plugins.android.application apply false
alias libs.plugins.android.library apply false
alias libs.plugins.jetbrains.kotlin.android apply false
}
tasks.register('clean', Delete) {
delete layout.buildDirectory
}
+11
View File
@@ -0,0 +1,11 @@
// Top-level build file where you can add configuration options common to all subprojects/modules.
plugins {
alias(libs.plugins.android.application) apply false
alias(libs.plugins.android.library) apply false
alias(libs.plugins.jetbrains.kotlin.android) apply false
}
tasks.register<Delete>("clean") {
delete(layout.buildDirectory)
}
+3 -3
View File
@@ -1,8 +1,8 @@
[versions]
activityKtx = "1.13.0"
agp = "9.1.0"
agp = "9.1.1"
coreKtx = "1.18.0"
freedroidwarn = "V1.10"
freedroidwarn = "V1.11"
gson = "2.13.2"
kotlin = "2.3.20"
leanback = "1.2.0"
@@ -15,7 +15,7 @@ paletteKtx = "1.0.0"
preferenceKtx = "1.2.1"
volley = "1.2.1"
workRuntimeKtx = "2.11.2"
playServicesCastFramework = "22.3.0"
playServicesCastFramework = "22.3.1"
[libraries]
activity-ktx = { group = "androidx.activity", name = "activity-ktx", version.ref = "activityKtx" }
Vendored
+1 -1
View File
@@ -57,7 +57,7 @@
# Darwin, MinGW, and NonStop.
#
# (3) This script is generated from the Groovy template
# https://github.com/gradle/gradle/blob/b631911858264c0b6e4d6603d677ff5218766cee/platforms/jvm/plugins-application/src/main/resources/org/gradle/api/internal/plugins/unixStartScript.txt
# https://github.com/gradle/gradle/blob/2d6327017519d23b96af35865dc997fcb544fb40/platforms/jvm/plugins-application/src/main/resources/org/gradle/api/internal/plugins/unixStartScript.txt
# within the Gradle project.
#
# You can find Gradle at https://github.com/gradle/gradle/.
+8 -6
View File
@@ -3,20 +3,22 @@ pluginManagement {
google()
mavenCentral()
gradlePluginPortal()
maven { url 'https://jitpack.io' }
maven { url = uri("https://jitpack.io") }
}
}
plugins {
id 'org.gradle.toolchains.foojay-resolver-convention' version '1.0.0'
}
@Suppress("UnstableApiUsage")
dependencyResolutionManagement {
repositoriesMode.set(RepositoriesMode.FAIL_ON_PROJECT_REPOS)
repositories {
google()
mavenCentral()
maven { url 'https://jitpack.io' }
maven { url = uri("https://jitpack.io") }
}
}
include ':app'
plugins {
id("org.gradle.toolchains.foojay-resolver-convention") version "1.0.0"
}
include(":app")