mirror of
https://github.com/Michatec/Radio.git
synced 2026-05-31 05:12:41 +02:00
feat(audio): add native audio processing and Google Cast support
This commit is contained in:
@@ -0,0 +1,32 @@
|
||||
package com.michatec.radio
|
||||
|
||||
import android.content.Context
|
||||
import com.google.android.gms.cast.CastMediaControlIntent
|
||||
import com.google.android.gms.cast.framework.CastOptions
|
||||
import com.google.android.gms.cast.framework.OptionsProvider
|
||||
import com.google.android.gms.cast.framework.SessionProvider
|
||||
import com.google.android.gms.cast.framework.media.CastMediaOptions
|
||||
import com.google.android.gms.cast.framework.media.NotificationOptions
|
||||
|
||||
@Suppress("unused")
|
||||
class CastOptionsProvider : OptionsProvider {
|
||||
override fun getCastOptions(context: Context): CastOptions {
|
||||
val notificationOptions = NotificationOptions.Builder()
|
||||
.setTargetActivityClassName(MainActivity::class.java.name)
|
||||
.build()
|
||||
val mediaOptions = CastMediaOptions.Builder()
|
||||
.setNotificationOptions(notificationOptions)
|
||||
.build()
|
||||
|
||||
return CastOptions.Builder()
|
||||
.setReceiverApplicationId(CastMediaControlIntent.DEFAULT_MEDIA_RECEIVER_APPLICATION_ID)
|
||||
.setResumeSavedSession(true)
|
||||
.setStopReceiverApplicationWhenEndingSession(true)
|
||||
.setCastMediaOptions(mediaOptions)
|
||||
.build()
|
||||
}
|
||||
|
||||
override fun getAdditionalSessionProviders(context: Context): List<SessionProvider>? {
|
||||
return null
|
||||
}
|
||||
}
|
||||
@@ -60,6 +60,12 @@ object Keys {
|
||||
const val PREF_LARGE_BUFFER_SIZE: String = "LARGE_BUFFER_SIZE"
|
||||
const val PREF_EDIT_STATIONS: String = "EDIT_STATIONS"
|
||||
const val PREF_EDIT_STREAMS_URIS: String = "EDIT_STREAMS_URIS"
|
||||
const val PREF_BASS_BOOST: String = "BASS_BOOST"
|
||||
const val PREF_REVERB: String = "REVERB"
|
||||
const val PREF_DRC: String = "DRC"
|
||||
const val PREF_EQ_LOW: String = "EQ_LOW"
|
||||
const val PREF_EQ_MID: String = "EQ_MID"
|
||||
const val PREF_EQ_HIGH: String = "EQ_HIGH"
|
||||
|
||||
// default const values
|
||||
const val DEFAULT_SIZE_OF_METADATA_HISTORY: Int = 25
|
||||
@@ -84,7 +90,6 @@ object Keys {
|
||||
const val DIALOG_REMOVE_STATION: Int = 2
|
||||
const val DIALOG_UPDATE_STATION_IMAGES: Int = 4
|
||||
const val DIALOG_RESTORE_COLLECTION: Int = 5
|
||||
const val DIALOG_THEME_SELECTION: Int = 6
|
||||
|
||||
// dialog results
|
||||
const val DIALOG_EMPTY_PAYLOAD_STRING: String = ""
|
||||
|
||||
@@ -27,13 +27,12 @@ class MainActivity : AppCompatActivity() {
|
||||
/* Main class variables */
|
||||
private lateinit var appBarConfiguration: AppBarConfiguration
|
||||
|
||||
|
||||
/* Overrides onCreate from AppCompatActivity */
|
||||
override fun onCreate(savedInstanceState: Bundle?) {
|
||||
enableEdgeToEdge()
|
||||
setTheme(R.style.AppTheme)
|
||||
super.onCreate(savedInstanceState)
|
||||
|
||||
|
||||
// Free Android
|
||||
FreeDroidWarn.showWarningOnUpgrade(this, BuildConfig.VERSION_CODE)
|
||||
|
||||
@@ -46,7 +45,8 @@ class MainActivity : AppCompatActivity() {
|
||||
// set up action bar
|
||||
setSupportActionBar(findViewById(R.id.main_toolbar))
|
||||
val toolbar: Toolbar = findViewById(R.id.main_toolbar)
|
||||
val navHostFragment = supportFragmentManager.findFragmentById(R.id.main_host_container) as NavHostFragment
|
||||
val navHostFragment =
|
||||
supportFragmentManager.findFragmentById(R.id.main_host_container) as NavHostFragment
|
||||
val navController = navHostFragment.navController
|
||||
appBarConfiguration = AppBarConfiguration(navController.graph)
|
||||
NavigationUI.setupWithNavController(toolbar, navController, appBarConfiguration)
|
||||
@@ -69,9 +69,7 @@ class MainActivity : AppCompatActivity() {
|
||||
private fun hideLoadingOverlay() {
|
||||
findViewById<View>(R.id.loading_layout)?.let { overlay ->
|
||||
if (overlay.isVisible) {
|
||||
overlay.animate()
|
||||
.alpha(0f)
|
||||
.setDuration(500)
|
||||
overlay.animate().alpha(0f).setDuration(500)
|
||||
.withEndAction { overlay.visibility = View.GONE }
|
||||
}
|
||||
}
|
||||
@@ -90,7 +88,8 @@ class MainActivity : AppCompatActivity() {
|
||||
|
||||
/* Overrides onSupportNavigateUp from AppCompatActivity */
|
||||
override fun onSupportNavigateUp(): Boolean {
|
||||
val navHostFragment = supportFragmentManager.findFragmentById(R.id.main_host_container) as NavHostFragment
|
||||
val navHostFragment =
|
||||
supportFragmentManager.findFragmentById(R.id.main_host_container) as NavHostFragment
|
||||
val navController = navHostFragment.navController
|
||||
return navController.navigateUp(appBarConfiguration) || super.onSupportNavigateUp()
|
||||
}
|
||||
|
||||
@@ -809,7 +809,7 @@ class PlayerFragment : Fragment(),
|
||||
|
||||
|
||||
/*
|
||||
* Check for update on github
|
||||
* Check for update on GitHub
|
||||
*/
|
||||
private fun checkForUpdates() {
|
||||
val url = getString(R.string.snackbar_github_update_check_url)
|
||||
|
||||
@@ -14,8 +14,11 @@ import androidx.media3.common.*
|
||||
import androidx.media3.common.util.UnstableApi
|
||||
import androidx.media3.datasource.HttpDataSource
|
||||
import androidx.media3.exoplayer.DefaultLoadControl
|
||||
import androidx.media3.exoplayer.DefaultRenderersFactory
|
||||
import androidx.media3.exoplayer.ExoPlayer
|
||||
import androidx.media3.exoplayer.analytics.AnalyticsListener
|
||||
import androidx.media3.exoplayer.audio.AudioSink
|
||||
import androidx.media3.exoplayer.audio.DefaultAudioSink
|
||||
import androidx.media3.exoplayer.source.DefaultMediaSourceFactory
|
||||
import androidx.media3.exoplayer.upstream.DefaultAllocator
|
||||
import androidx.media3.exoplayer.upstream.DefaultLoadErrorHandlingPolicy
|
||||
@@ -25,10 +28,7 @@ import com.google.common.collect.ImmutableList
|
||||
import com.google.common.util.concurrent.Futures
|
||||
import com.google.common.util.concurrent.ListenableFuture
|
||||
import com.michatec.radio.core.Collection
|
||||
import com.michatec.radio.helpers.AudioHelper
|
||||
import com.michatec.radio.helpers.CollectionHelper
|
||||
import com.michatec.radio.helpers.FileHelper
|
||||
import com.michatec.radio.helpers.PreferencesHelper
|
||||
import com.michatec.radio.helpers.*
|
||||
import kotlinx.coroutines.*
|
||||
import kotlinx.coroutines.Dispatchers.Main
|
||||
import java.util.*
|
||||
@@ -38,7 +38,7 @@ import java.util.*
|
||||
* PlayerService class
|
||||
*/
|
||||
@UnstableApi
|
||||
class PlayerService : MediaLibraryService() {
|
||||
class PlayerService : MediaLibraryService(), SharedPreferences.OnSharedPreferenceChangeListener {
|
||||
|
||||
/* Define log tag */
|
||||
private val TAG: String = PlayerService::class.java.simpleName
|
||||
@@ -56,6 +56,9 @@ class PlayerService : MediaLibraryService() {
|
||||
private var playbackRestartCounter: Int = 0
|
||||
private var playLastStation: Boolean = false
|
||||
private var manuallyCancelledSleepTimer = false
|
||||
|
||||
// Native Audio Processor instance
|
||||
private val nativeAudioProcessor = NativeAudioProcessor()
|
||||
|
||||
|
||||
/* Overrides onCreate from Service */
|
||||
@@ -76,6 +79,11 @@ class PlayerService : MediaLibraryService() {
|
||||
setMediaNotificationProvider(notificationProvider)
|
||||
// fetch the metadata history
|
||||
metadataHistory = PreferencesHelper.loadMetadataHistory()
|
||||
|
||||
// register preference change listener
|
||||
PreferencesHelper.registerPreferenceChangeListener(this)
|
||||
// apply initial audio effects
|
||||
applyAudioEffects()
|
||||
}
|
||||
|
||||
|
||||
@@ -86,6 +94,8 @@ class PlayerService : MediaLibraryService() {
|
||||
player.removeListener(playerListener)
|
||||
player.release()
|
||||
mediaLibrarySession.release()
|
||||
// unregister preference change listener
|
||||
PreferencesHelper.unregisterPreferenceChangeListener(this)
|
||||
super.onDestroy()
|
||||
}
|
||||
|
||||
@@ -106,8 +116,26 @@ class PlayerService : MediaLibraryService() {
|
||||
|
||||
/* Initializes the ExoPlayer */
|
||||
private fun initializePlayer() {
|
||||
val exoPlayer: ExoPlayer = ExoPlayer.Builder(this).apply {
|
||||
setAudioAttributes(AudioAttributes.DEFAULT, true)
|
||||
val audioAttributes = AudioAttributes.Builder()
|
||||
.setContentType(C.AUDIO_CONTENT_TYPE_MUSIC)
|
||||
.setUsage(C.USAGE_MEDIA)
|
||||
.build()
|
||||
|
||||
// Create a RenderersFactory that injects the NativeAudioProcessor
|
||||
val renderersFactory = object : DefaultRenderersFactory(this) {
|
||||
override fun buildAudioSink(
|
||||
context: Context,
|
||||
enableFloatOutput: Boolean,
|
||||
enableAudioTrackPlaybackParams: Boolean
|
||||
): AudioSink? {
|
||||
return DefaultAudioSink.Builder(context)
|
||||
.setAudioProcessors(arrayOf(nativeAudioProcessor))
|
||||
.build()
|
||||
}
|
||||
}
|
||||
|
||||
val exoPlayer: ExoPlayer = ExoPlayer.Builder(this, renderersFactory).apply {
|
||||
setAudioAttributes(audioAttributes, true)
|
||||
setHandleAudioBecomingNoisy(true)
|
||||
setLoadControl(createDefaultLoadControl(bufferSizeMultiplier))
|
||||
setMediaSourceFactory(
|
||||
@@ -246,6 +274,28 @@ class PlayerService : MediaLibraryService() {
|
||||
}
|
||||
|
||||
|
||||
/* 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())
|
||||
}
|
||||
|
||||
|
||||
/* 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 -> {
|
||||
applyAudioEffects()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
/*
|
||||
* Custom MediaSession Callback that handles player commands
|
||||
*/
|
||||
@@ -268,7 +318,6 @@ class PlayerService : MediaLibraryService() {
|
||||
session: MediaSession,
|
||||
controller: MediaSession.ControllerInfo
|
||||
): MediaSession.ConnectionResult {
|
||||
// add custom commands
|
||||
val connectionResult: MediaSession.ConnectionResult = super.onConnect(session, controller)
|
||||
val builder: SessionCommands.Builder = connectionResult.availableSessionCommands.buildUpon()
|
||||
builder.add(SessionCommand(Keys.CMD_START_SLEEP_TIMER, Bundle.EMPTY))
|
||||
|
||||
@@ -186,6 +186,30 @@ class SettingsFragment : PreferenceFragmentCompat(), YesNoDialog.YesNoDialogList
|
||||
return@setOnPreferenceChangeListener true
|
||||
}
|
||||
|
||||
// set up "Bass Boost" preference
|
||||
val preferenceBassBoost = MarqueeSwitchPreference(context)
|
||||
preferenceBassBoost.title = getString(R.string.pref_bass_boost_title)
|
||||
preferenceBassBoost.setIcon(R.drawable.ic_music_note_24dp)
|
||||
preferenceBassBoost.key = Keys.PREF_BASS_BOOST
|
||||
preferenceBassBoost.summary = getString(R.string.pref_bass_boost_summary)
|
||||
preferenceBassBoost.setDefaultValue(false)
|
||||
|
||||
// set up "Reverb" preference
|
||||
val preferenceReverb = MarqueeSwitchPreference(context)
|
||||
preferenceReverb.title = getString(R.string.pref_reverb_title)
|
||||
preferenceReverb.setIcon(R.drawable.ic_music_note_24dp)
|
||||
preferenceReverb.key = Keys.PREF_REVERB
|
||||
preferenceReverb.summary = getString(R.string.pref_reverb_summary)
|
||||
preferenceReverb.setDefaultValue(false)
|
||||
|
||||
// set up "DRC" preference
|
||||
val preferenceDrc = MarqueeSwitchPreference(context)
|
||||
preferenceDrc.title = getString(R.string.pref_drc_title)
|
||||
preferenceDrc.setIcon(R.drawable.ic_music_note_24dp)
|
||||
preferenceDrc.key = Keys.PREF_DRC
|
||||
preferenceDrc.summary = getString(R.string.pref_drc_summary)
|
||||
preferenceDrc.setDefaultValue(true)
|
||||
|
||||
// set up "App Version" preference
|
||||
val preferenceAppVersion = Preference(context)
|
||||
preferenceAppVersion.title = getString(R.string.pref_app_version_title)
|
||||
@@ -238,51 +262,52 @@ class SettingsFragment : PreferenceFragmentCompat(), YesNoDialog.YesNoDialogList
|
||||
// set preference categories
|
||||
val preferenceCategoryGeneral = PreferenceCategory(activity as Context)
|
||||
preferenceCategoryGeneral.title = getString(R.string.pref_general_title)
|
||||
preferenceCategoryGeneral.contains(preferenceThemeSelection)
|
||||
|
||||
val preferenceCategoryAudioEffects = PreferenceCategory(context)
|
||||
preferenceCategoryAudioEffects.title = getString(R.string.pref_audio_effects_title)
|
||||
|
||||
val preferenceCategoryMaintenance = PreferenceCategory(activity as Context)
|
||||
preferenceCategoryMaintenance.title = getString(R.string.pref_maintenance_title)
|
||||
preferenceCategoryMaintenance.contains(preferenceUpdateStationImages)
|
||||
preferenceCategoryMaintenance.contains(preferenceUpdateCollection)
|
||||
|
||||
val preferenceCategoryImportExport = PreferenceCategory(activity as Context)
|
||||
preferenceCategoryImportExport.title = getString(R.string.pref_backup_import_export_title)
|
||||
preferenceCategoryImportExport.contains(preferenceM3uExport)
|
||||
preferenceCategoryImportExport.contains(preferencePlsExport)
|
||||
preferenceCategoryImportExport.contains(preferenceBackupCollection)
|
||||
preferenceCategoryImportExport.contains(preferenceRestoreCollection)
|
||||
|
||||
val preferenceCategoryAdvanced = PreferenceCategory(activity as Context)
|
||||
preferenceCategoryAdvanced.title = getString(R.string.pref_advanced_title)
|
||||
preferenceCategoryAdvanced.contains(preferenceBufferSize)
|
||||
preferenceCategoryAdvanced.contains(preferenceEnableEditingGeneral)
|
||||
preferenceCategoryAdvanced.contains(preferenceEnableEditingStreamUri)
|
||||
|
||||
val preferenceCategoryLinks = PreferenceCategory(context)
|
||||
preferenceCategoryLinks.title = getString(R.string.pref_links_title)
|
||||
preferenceCategoryLinks.contains(preferenceAppVersion)
|
||||
preferenceCategoryLinks.contains(preferenceGitHub)
|
||||
|
||||
|
||||
// setup preference screen
|
||||
screen.addPreference(preferenceAppVersion)
|
||||
screen.addPreference(preferenceLicense)
|
||||
screen.addPreference(preferenceCategoryGeneral)
|
||||
screen.addPreference(preferenceThemeSelection)
|
||||
preferenceCategoryGeneral.addPreference(preferenceThemeSelection)
|
||||
|
||||
screen.addPreference(preferenceCategoryAudioEffects)
|
||||
preferenceCategoryAudioEffects.addPreference(preferenceBassBoost)
|
||||
preferenceCategoryAudioEffects.addPreference(preferenceReverb)
|
||||
preferenceCategoryAudioEffects.addPreference(preferenceDrc)
|
||||
|
||||
screen.addPreference(preferenceCategoryMaintenance)
|
||||
screen.addPreference(preferenceUpdateStationImages)
|
||||
screen.addPreference(preferenceUpdateCollection)
|
||||
preferenceCategoryMaintenance.addPreference(preferenceUpdateStationImages)
|
||||
preferenceCategoryMaintenance.addPreference(preferenceUpdateCollection)
|
||||
|
||||
screen.addPreference(preferenceCategoryImportExport)
|
||||
screen.addPreference(preferenceM3uExport)
|
||||
screen.addPreference(preferencePlsExport)
|
||||
screen.addPreference(preferenceBackupCollection)
|
||||
screen.addPreference(preferenceRestoreCollection)
|
||||
preferenceCategoryImportExport.addPreference(preferenceM3uExport)
|
||||
preferenceCategoryImportExport.addPreference(preferencePlsExport)
|
||||
preferenceCategoryImportExport.addPreference(preferenceBackupCollection)
|
||||
preferenceCategoryImportExport.addPreference(preferenceRestoreCollection)
|
||||
|
||||
screen.addPreference(preferenceCategoryAdvanced)
|
||||
screen.addPreference(preferenceBufferSize)
|
||||
screen.addPreference(preferenceEnableEditingGeneral)
|
||||
screen.addPreference(preferenceEnableEditingStreamUri)
|
||||
preferenceCategoryAdvanced.addPreference(preferenceBufferSize)
|
||||
preferenceCategoryAdvanced.addPreference(preferenceEnableEditingGeneral)
|
||||
preferenceCategoryAdvanced.addPreference(preferenceEnableEditingStreamUri)
|
||||
|
||||
screen.addPreference(preferenceCategoryLinks)
|
||||
screen.addPreference(preferenceGitHub)
|
||||
preferenceCategoryLinks.addPreference(preferenceAppVersion)
|
||||
preferenceCategoryLinks.addPreference(preferenceGitHub)
|
||||
preferenceCategoryLinks.addPreference(preferenceLicense)
|
||||
|
||||
preferenceScreen = screen
|
||||
}
|
||||
|
||||
@@ -491,28 +516,6 @@ class SettingsFragment : PreferenceFragmentCompat(), YesNoDialog.YesNoDialogList
|
||||
}
|
||||
|
||||
|
||||
/* Opens up a file picker to select the save location */
|
||||
private fun openSavePlsDialog() {
|
||||
val intent = Intent(Intent.ACTION_CREATE_DOCUMENT).apply {
|
||||
addCategory(Intent.CATEGORY_OPENABLE)
|
||||
type = Keys.MIME_TYPE_PLS
|
||||
|
||||
val timeStamp: String
|
||||
val dateFormat = SimpleDateFormat("_yyyy-MM-dd'T'HH_mm", Locale.US)
|
||||
timeStamp = dateFormat.format(Date())
|
||||
|
||||
putExtra(Intent.EXTRA_TITLE, "collection$timeStamp.pls")
|
||||
}
|
||||
// file gets saved in the ActivityResult
|
||||
try {
|
||||
requestSavePlsLauncher.launch(intent)
|
||||
} catch (exception: Exception) {
|
||||
Log.e(TAG, "Unable to save PLS.\n$exception")
|
||||
Snackbar.make(requireView(), R.string.toastmessage_install_file_helper, Snackbar.LENGTH_LONG).show()
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
/* Opens up a file picker to select the backup location */
|
||||
private fun openBackupCollectionDialog() {
|
||||
val intent = Intent(Intent.ACTION_CREATE_DOCUMENT).apply {
|
||||
@@ -549,4 +552,24 @@ class SettingsFragment : PreferenceFragmentCompat(), YesNoDialog.YesNoDialogList
|
||||
Log.e(TAG, "Unable to open file picker for ZIP.\n$exception")
|
||||
}
|
||||
}
|
||||
|
||||
private fun openSavePlsDialog() {
|
||||
val intent = Intent(Intent.ACTION_CREATE_DOCUMENT).apply {
|
||||
addCategory(Intent.CATEGORY_OPENABLE)
|
||||
type = Keys.MIME_TYPE_PLS
|
||||
|
||||
val timeStamp: String
|
||||
val dateFormat = SimpleDateFormat("_yyyy-MM-dd'T'HH_mm", Locale.US)
|
||||
timeStamp = dateFormat.format(Date())
|
||||
|
||||
putExtra(Intent.EXTRA_TITLE, "collection$timeStamp.pls")
|
||||
}
|
||||
// file gets saved in the ActivityResult
|
||||
try {
|
||||
requestSavePlsLauncher.launch(intent)
|
||||
} catch (exception: Exception) {
|
||||
Log.e(TAG, "Unable to save PLS.\n$exception")
|
||||
Snackbar.make(requireView(), R.string.toastmessage_install_file_helper, Snackbar.LENGTH_LONG).show()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -640,12 +640,18 @@ object CollectionHelper {
|
||||
val mediaMetadata = MediaMetadata.Builder().apply {
|
||||
setArtist(station.name)
|
||||
//setTitle(station.name)
|
||||
|
||||
// Set artwork URI for casting (TV needs a public URL)
|
||||
val artworkUrl = station.remoteImageLocation.ifEmpty {
|
||||
// Placeholder PNG image for stations without remote image
|
||||
"https://raw.githubusercontent.com/google/material-design-icons/master/png/av/radio/materialicons/48dp/2x/baseline_radio_black_48dp.png"
|
||||
}
|
||||
setArtworkUri(artworkUrl.toUri())
|
||||
|
||||
/* check for "file://" prevents a crash when an old backup was restored */
|
||||
if (station.image.isNotEmpty() && station.image.startsWith("file://")) {
|
||||
//setArtworkUri(station.image.toUri())
|
||||
setArtworkData(station.image.toUri().toFile().readBytes(), MediaMetadata.PICTURE_TYPE_FRONT_COVER)
|
||||
} else {
|
||||
//setArtworkUri(Uri.parse(Keys.LOCATION_RESOURCES + R.raw.ic_default_station_image))
|
||||
setArtworkData(ImageHelper.getStationImageAsByteArray(context), MediaMetadata.PICTURE_TYPE_FRONT_COVER)
|
||||
}
|
||||
setIsBrowsable(false)
|
||||
|
||||
@@ -0,0 +1,25 @@
|
||||
package com.michatec.radio.helpers
|
||||
|
||||
import android.content.Context
|
||||
import android.text.TextUtils
|
||||
import android.widget.TextView
|
||||
import androidx.preference.PreferenceViewHolder
|
||||
import androidx.preference.SwitchPreferenceCompat
|
||||
|
||||
/*
|
||||
* Custom SwitchPreferenceCompat that enables marquee (scrolling text) for the title
|
||||
*/
|
||||
class MarqueeSwitchPreference(context: Context) : SwitchPreferenceCompat(context) {
|
||||
|
||||
override fun onBindViewHolder(holder: PreferenceViewHolder) {
|
||||
super.onBindViewHolder(holder)
|
||||
val title = holder.findViewById(android.R.id.title) as? TextView
|
||||
title?.apply {
|
||||
ellipsize = TextUtils.TruncateAt.MARQUEE
|
||||
setSingleLine(true)
|
||||
marqueeRepeatLimit = -1 // Repeat indefinitely
|
||||
isSelected = true // Required for marquee to start
|
||||
setHorizontallyScrolling(true)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,62 @@
|
||||
package com.michatec.radio.helpers
|
||||
|
||||
import androidx.annotation.OptIn
|
||||
import androidx.media3.common.C
|
||||
import androidx.media3.common.audio.AudioProcessor
|
||||
import androidx.media3.common.audio.AudioProcessor.AudioFormat
|
||||
import androidx.media3.common.audio.BaseAudioProcessor
|
||||
import androidx.media3.common.util.UnstableApi
|
||||
import java.nio.ByteBuffer
|
||||
import java.nio.ByteOrder
|
||||
|
||||
@OptIn(UnstableApi::class)
|
||||
class NativeAudioProcessor : BaseAudioProcessor() {
|
||||
|
||||
companion object {
|
||||
init {
|
||||
System.loadLibrary("radio")
|
||||
}
|
||||
}
|
||||
|
||||
// JNI Methods
|
||||
private external fun setDrcEnabled(enabled: Boolean)
|
||||
private external fun setReverbMix(mix: Float)
|
||||
private external fun setEqBand(band: Int, gainDb: Float)
|
||||
private external fun setBassBoost(gainDb: Float)
|
||||
private external fun processAudio(data: ShortArray, size: Int)
|
||||
|
||||
// Public API
|
||||
fun enableDrc(enabled: Boolean) = setDrcEnabled(enabled)
|
||||
fun setReverb(mix: Float) = setReverbMix(mix)
|
||||
fun setEq(band: Int, gainDb: Float) = setEqBand(band, gainDb)
|
||||
fun enableBassBoost(gainDb: Float) = setBassBoost(gainDb)
|
||||
|
||||
override fun onConfigure(inputAudioFormat: AudioFormat): AudioFormat {
|
||||
if (inputAudioFormat.encoding != C.ENCODING_PCM_16BIT) {
|
||||
throw AudioProcessor.UnhandledAudioFormatException(inputAudioFormat)
|
||||
}
|
||||
return inputAudioFormat
|
||||
}
|
||||
|
||||
override fun queueInput(inputBuffer: ByteBuffer) {
|
||||
val remaining = inputBuffer.remaining()
|
||||
if (remaining == 0) return
|
||||
|
||||
val shortArraySize = remaining / 2
|
||||
val shortArray = ShortArray(shortArraySize)
|
||||
|
||||
// Input-Daten lesen
|
||||
inputBuffer.order(ByteOrder.nativeOrder()).asShortBuffer().get(shortArray)
|
||||
|
||||
// Native Verarbeitung
|
||||
processAudio(shortArray, shortArraySize)
|
||||
|
||||
// Buffer der Basisklasse anfordern und befüllen
|
||||
val outputBuffer = replaceOutputBuffer(remaining)
|
||||
outputBuffer.asShortBuffer().put(shortArray)
|
||||
outputBuffer.limit(remaining) // Markiert das Ende der geschriebenen Daten
|
||||
|
||||
// Input-Buffer als verarbeitet markieren
|
||||
inputBuffer.position(inputBuffer.limit())
|
||||
}
|
||||
}
|
||||
@@ -254,4 +254,27 @@ object PreferencesHelper {
|
||||
)
|
||||
}
|
||||
|
||||
|
||||
/* Loads Bass Boost gain */
|
||||
fun loadBassBoost(): Float {
|
||||
return if (sharedPreferences.getBoolean(Keys.PREF_BASS_BOOST, false)) 5.0f else 0.0f
|
||||
}
|
||||
|
||||
|
||||
/* Loads Reverb mix */
|
||||
fun loadReverb(): Float {
|
||||
return if (sharedPreferences.getBoolean(Keys.PREF_REVERB, false)) 0.3f else 0.0f
|
||||
}
|
||||
|
||||
|
||||
/* Loads DRC enabled state */
|
||||
fun loadDrcEnabled(): Boolean {
|
||||
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()
|
||||
|
||||
}
|
||||
|
||||
@@ -19,9 +19,11 @@ import androidx.constraintlayout.widget.ConstraintLayout
|
||||
import androidx.constraintlayout.widget.Group
|
||||
import androidx.core.view.isGone
|
||||
import androidx.core.view.isVisible
|
||||
import androidx.mediarouter.app.MediaRouteButton
|
||||
import androidx.recyclerview.widget.DefaultItemAnimator
|
||||
import androidx.recyclerview.widget.LinearLayoutManager
|
||||
import androidx.recyclerview.widget.RecyclerView
|
||||
import com.google.android.gms.cast.framework.CastButtonFactory
|
||||
import com.google.android.material.bottomsheet.BottomSheetBehavior
|
||||
import com.google.android.material.snackbar.Snackbar
|
||||
import com.michatec.radio.Keys
|
||||
@@ -60,6 +62,7 @@ data class LayoutHolder(var rootView: View) {
|
||||
private var sheetNextMetadataView: ImageButton? = rootView.findViewById(R.id.sheet_next_metadata_button)
|
||||
private var sheetPreviousMetadataView: ImageButton? = rootView.findViewById(R.id.sheet_previous_metadata_button)
|
||||
private var sheetCopyMetadataButtonView: ImageButton? = rootView.findViewById(R.id.copy_station_metadata_button)
|
||||
private var mediaRouteButton: MediaRouteButton? = rootView.findViewById(R.id.media_route_button)
|
||||
private var sheetShareLinkButtonView: ImageView? = rootView.findViewById(R.id.sheet_share_link_button)
|
||||
private var sheetBitrateView: TextView? = rootView.findViewById(R.id.sheet_bitrate_view)
|
||||
var sheetSleepTimerStartButtonView: ImageButton? = rootView.findViewById(R.id.sleep_timer_start_button)
|
||||
@@ -115,6 +118,11 @@ data class LayoutHolder(var rootView: View) {
|
||||
return@setOnLongClickListener true
|
||||
}
|
||||
|
||||
// Set up MediaRouteButton (Google Cast)
|
||||
mediaRouteButton?.let {
|
||||
CastButtonFactory.setUpMediaRouteButton(rootView.context, it)
|
||||
}
|
||||
|
||||
// set layout for player
|
||||
setupBottomSheet()
|
||||
}
|
||||
@@ -123,8 +131,6 @@ data class LayoutHolder(var rootView: View) {
|
||||
/* Updates the player views */
|
||||
@SuppressLint("DefaultLocale")
|
||||
fun updatePlayerViews(context: Context, station: Station, isPlaying: Boolean) {
|
||||
|
||||
// set default metadata views, when playback has stopped
|
||||
if (!isPlaying) {
|
||||
metadataView?.text = station.name
|
||||
sheetMetadataHistoryView?.text = station.name
|
||||
|
||||
Reference in New Issue
Block a user