4 Commits

12 changed files with 400 additions and 49 deletions
+82 -37
View File
@@ -121,48 +121,90 @@ struct alignas(16) BassFilter {
} }
}; };
template<int SIZE> class ReverbOptimized {
struct CircularBuffer { struct DelayLine {
alignas(16) std::array<float, SIZE> data = {}; float buffer[48000]{};
int size = 48000;
int pos = 0; int pos = 0;
[[nodiscard]] inline float read() const { return data[pos]; }
inline void write(float v) { data[pos] = v; } inline float read(float delaySamples) {
inline void advance() { pos = (pos + 1) % SIZE; } 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
}; };
class ReverbOptimized {
std::array<CircularBuffer<1116>, 4> combs;
std::array<CircularBuffer<556>, 2> allpasses;
std::array<float, 4> combFeedback = {0.841f, 0.815f, 0.796f, 0.771f};
public: public:
std::atomic<float> mix{0.0f}; std::atomic<float> mix{0.0f};
inline float process(float x) {
float m = mix.load(std::memory_order_acquire); inline float processSample(float x) {
float m = mix.load(std::memory_order_relaxed);
if (m < 0.01f) return x; if (m < 0.01f) return x;
float out = 0.0f; float out = 0.0f;
#pragma GCC unroll 4
for (int i = 0; i < 4; i++) { #pragma GCC unroll 8
float delayed = combs[static_cast<size_t>(i)].read(); 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; out += delayed;
combs[static_cast<size_t>(i)].write(x + delayed * combFeedback[static_cast<size_t>(i)] + DENORMAL_OFFSET);
combs[static_cast<size_t>(i)].advance();
} }
out *= 0.25f;
for (int i = 0; i < 2; i++) { return x * (1.0f - m) + (out * 0.125f) * m;
float bufOut = allpasses[static_cast<size_t>(i)].read();
float xOut = -0.5f * out + bufOut;
allpasses[static_cast<size_t>(i)].write(out + 0.5f * bufOut);
allpasses[static_cast<size_t>(i)].advance();
out = xOut;
}
return x * (1.0f - m) + out * m;
} }
inline void processBlock(float* __restrict__ left, float* __restrict__ right, int count) { inline void processBlock(float* __restrict__ left, float* __restrict__ right, int count) {
float m = mix.load(std::memory_order_acquire); float m = mix.load(std::memory_order_relaxed);
if (m < 0.01f) return; if (m < 0.01f) return;
for (int i = 0; i < count; i++) { for (int i = 0; i < count; i++) {
left[i] = process(left[i]); float l = processSample(left[i]);
right[i] = process(right[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;
} }
} }
}; };
@@ -175,16 +217,14 @@ public:
private: private:
float envelopeL = 0.0f, envelopeR = 0.0f; float envelopeL = 0.0f, envelopeR = 0.0f;
float attackCoef = 0.0f, releaseCoef = 0.0f; float attackCoef = 0.0f, releaseCoef = 0.0f;
bool coefficientsValid = false;
public: public:
inline void updateCoefficients() { inline void updateCoefficients() {
if (coefficientsValid) return; float a = attack.load(std::memory_order_relaxed);
float a = attack.load(std::memory_order_acquire); float r = release.load(std::memory_order_relaxed);
float r = release.load(std::memory_order_acquire); float sr = sampleRate.load(std::memory_order_relaxed);
float sr = sampleRate.load(std::memory_order_acquire);
attackCoef = expf(-1.0f / (a * sr)); attackCoef = expf(-1.0f / (a * sr));
releaseCoef = expf(-1.0f / (r * sr)); releaseCoef = expf(-1.0f / (r * sr));
coefficientsValid = true;
} }
inline void processBlock(float* __restrict__ buffer, int count, float& envelope) { inline void processBlock(float* __restrict__ buffer, int count, float& envelope) {
updateCoefficients(); updateCoefficients();
@@ -375,15 +415,20 @@ JNIEXPORT void JNICALL Java_com_michatec_radio_helpers_NativeAudioProcessor_proc
gRightBuf[static_cast<size_t>(i)] = static_cast<float>(buffer[i * 2 + 1]) * INV_32768; gRightBuf[static_cast<size_t>(i)] = static_cast<float>(buffer[i * 2 + 1]) * INV_32768;
} }
bool eqEnabled = gEqEnabled.load(std::memory_order_acquire); bool eqEnabled = gEqEnabled.load(std::memory_order_relaxed);
if (eqEnabled) { if (eqEnabled) {
for (int i = 0; i < numFrames; i++) { for (int i = 0; i < numFrames; i++) {
float xL = gLeftBuf[static_cast<size_t>(i)]; float xL = gLeftBuf[static_cast<size_t>(i)];
float xR = gRightBuf[static_cast<size_t>(i)]; float xR = gRightBuf[static_cast<size_t>(i)];
for (int b = 0; b < NUM_EQ_BANDS; b++) { for (int b = 0; b < NUM_EQ_BANDS; b++) {
float g = gEqL[b].currentGain.load(std::memory_order_relaxed);
if (std::abs(g) < 0.01f) continue;
xL = gEqL[b].process(xL); xL = gEqL[b].process(xL);
xR = gEqR[b].process(xR); xR = gEqR[b].process(xR);
} }
gLeftBuf[static_cast<size_t>(i)] = xL; gLeftBuf[static_cast<size_t>(i)] = xL;
gRightBuf[static_cast<size_t>(i)] = xR; gRightBuf[static_cast<size_t>(i)] = xR;
} }
@@ -396,7 +441,7 @@ JNIEXPORT void JNICALL Java_com_michatec_radio_helpers_NativeAudioProcessor_proc
gReverbL.processBlock(gLeftBuf.data(), gRightBuf.data(), numFrames); gReverbL.processBlock(gLeftBuf.data(), gRightBuf.data(), numFrames);
float stereoWidth = gStereoWidth.load(std::memory_order_acquire); float stereoWidth = gStereoWidth.load(std::memory_order_relaxed);
if (stereoWidth != 1.0f) { if (stereoWidth != 1.0f) {
float halfWidth = stereoWidth * 0.5f; float halfWidth = stereoWidth * 0.5f;
for (int j = 0; j < numFrames; j++) { for (int j = 0; j < numFrames; j++) {
@@ -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
}
}
@@ -34,6 +34,7 @@ import androidx.media3.common.util.UnstableApi
import androidx.media3.session.MediaController import androidx.media3.session.MediaController
import androidx.media3.session.SessionResult import androidx.media3.session.SessionResult
import androidx.media3.session.SessionToken import androidx.media3.session.SessionToken
import androidx.navigation.fragment.findNavController
import androidx.recyclerview.widget.ItemTouchHelper import androidx.recyclerview.widget.ItemTouchHelper
import androidx.recyclerview.widget.RecyclerView import androidx.recyclerview.widget.RecyclerView
import com.android.volley.Request import com.android.volley.Request
@@ -342,8 +343,15 @@ class PlayerFragment : Fragment(),
/* Overrides onAddNewButtonTapped from CollectionAdapterListener */ /* Overrides onAddNewButtonTapped from CollectionAdapterListener */
override fun onAddNewButtonTapped() { override fun onAddNewButtonTapped() {
// 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() FindStationDialog(activity as Activity, this as FindStationDialog.FindStationDialogListener).show()
} }
}
/* Overrides onChangeImageButtonTapped from CollectionAdapterListener */ /* Overrides onChangeImageButtonTapped from CollectionAdapterListener */
@@ -625,6 +633,8 @@ class PlayerFragment : Fragment(),
} }
withContext(Main) { withContext(Main) {
if (stationList.isNotEmpty()) { 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() AddStationDialog(activity as Activity, stationList, this@PlayerFragment as AddStationDialog.AddStationDialogListener).show()
} else { } else {
// invalid address // invalid address
@@ -169,6 +169,9 @@ class CollectionAdapter(
addNewViewHolder.settingsButtonView.setOnClickListener { addNewViewHolder.settingsButtonView.setOnClickListener {
it.findNavController().navigate(R.id.settings_destination) it.findNavController().navigate(R.id.settings_destination)
} }
addNewViewHolder.visualizerButtonView.setOnClickListener {
it.findNavController().navigate(R.id.visualizer_destination)
}
} }
// CASE STATION CARD // CASE STATION CARD
is StationViewHolder -> { is StationViewHolder -> {
@@ -187,6 +190,8 @@ class CollectionAdapter(
setPlaybackProgress(stationViewHolder, station) setPlaybackProgress(stationViewHolder, station)
setDownloadProgress(stationViewHolder, station) setDownloadProgress(stationViewHolder, station)
stationViewHolder.playButtonView.isGone = true
// highlight if reordering // highlight if reordering
if (reorderStationUuid == station.uuid) { if (reorderStationUuid == station.uuid) {
stationViewHolder.stationCardView.setStrokeColor( stationViewHolder.stationCardView.setStrokeColor(
@@ -754,6 +759,8 @@ class CollectionAdapter(
listItemAddNewLayout.findViewById(R.id.card_add_new_station) listItemAddNewLayout.findViewById(R.id.card_add_new_station)
val settingsButtonView: ExtendedFloatingActionButton = val settingsButtonView: ExtendedFloatingActionButton =
listItemAddNewLayout.findViewById(R.id.card_settings) listItemAddNewLayout.findViewById(R.id.card_settings)
val visualizerButtonView: ExtendedFloatingActionButton =
listItemAddNewLayout.findViewById(R.id.card_visualizer)
} }
/* /*
* End of inner class * End of inner class
@@ -772,7 +779,6 @@ class CollectionAdapter(
val bufferingProgress: ProgressBar = stationCardLayout.findViewById(R.id.buffering_progress) val bufferingProgress: ProgressBar = stationCardLayout.findViewById(R.id.buffering_progress)
val downloadProgress: ProgressBar = stationCardLayout.findViewById(R.id.download_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 playButtonView: ImageView = stationCardLayout.findViewById(R.id.playback_button)
val editViews: Group = stationCardLayout.findViewById(R.id.default_edit_views) val editViews: Group = stationCardLayout.findViewById(R.id.default_edit_views)
val stationImageChangeView: ImageView = val stationImageChangeView: ImageView =
@@ -97,7 +97,7 @@ class NativeAudioProcessor : BaseAudioProcessor() {
// ===== Presets ===== // ===== Presets =====
fun setPresetRock() { fun setPresetRock() {
enableDrc(true) enableDrc(true)
setReverb(0.10f) setReverb(0.26f)
setWidth(1.1f) setWidth(1.1f)
setEqAll(floatArrayOf(2f, 1f, 0f, -1f, -1f, 0f, 1f, 2f, 2f, 3f)) setEqAll(floatArrayOf(2f, 1f, 0f, -1f, -1f, 0f, 1f, 2f, 2f, 3f))
enableBassBoost(0.9f) enableBassBoost(0.9f)
@@ -105,7 +105,7 @@ class NativeAudioProcessor : BaseAudioProcessor() {
fun setPresetPop() { fun setPresetPop() {
enableDrc(true) enableDrc(true)
setReverb(0.10f) setReverb(0.18f)
setWidth(1.05f) setWidth(1.05f)
setEqAll(floatArrayOf(0f, 1f, 1f, 1f, 0f, 0f, 1f, 2f, 2f, 1f)) setEqAll(floatArrayOf(0f, 1f, 1f, 1f, 0f, 0f, 1f, 2f, 2f, 1f))
enableBassBoost(0.6f) enableBassBoost(0.6f)
@@ -263,7 +263,7 @@ object PreferencesHelper {
/* Loads Reverb mix */ /* Loads Reverb mix */
fun loadReverb(): Float { fun loadReverb(): Float {
return if (sharedPreferences.getBoolean(Keys.PREF_REVERB, false)) 0.18f else 0.0f return if (sharedPreferences.getBoolean(Keys.PREF_REVERB, false)) 0.3f else 0.0f
} }
@@ -8,15 +8,21 @@ import android.view.LayoutInflater
import android.view.View import android.view.View
import android.view.ViewGroup import android.view.ViewGroup
import android.widget.Toast import android.widget.Toast
import androidx.annotation.OptIn
import androidx.appcompat.app.AppCompatActivity import androidx.appcompat.app.AppCompatActivity
import androidx.lifecycle.DefaultLifecycleObserver import androidx.lifecycle.DefaultLifecycleObserver
import androidx.lifecycle.LifecycleOwner 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.ExoPlayer
import androidx.media3.exoplayer.audio.AudioSink
import androidx.media3.exoplayer.audio.DefaultAudioSink
import androidx.recyclerview.widget.RecyclerView import androidx.recyclerview.widget.RecyclerView
import com.google.android.material.textview.MaterialTextView import com.google.android.material.textview.MaterialTextView
import com.michatec.radio.R import com.michatec.radio.R
import com.michatec.radio.core.Station 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 exoPlayer: ExoPlayer? = null
private var paused: Boolean = false private var paused: Boolean = false
private var isItemSelected: Boolean = false private var isItemSelected: Boolean = false
private var nativeAudioProcessor = NativeAudioProcessor()
/* Listener Interface */ /* Listener Interface */
interface SearchResultAdapterListener { interface SearchResultAdapterListener {
@@ -138,6 +145,7 @@ class SearchResultAdapter(
} }
@OptIn(UnstableApi::class)
private fun performPrePlayback(context: Context, streamUri: String) { private fun performPrePlayback(context: Context, streamUri: String) {
if (streamUri.contains(".m3u8")) { if (streamUri.contains(".m3u8")) {
// release previous player if it exists // release previous player if it exists
@@ -151,8 +159,30 @@ class SearchResultAdapter(
// release previous player if it exists // release previous player if it exists
stopPrePlayback() stopPrePlayback()
// create a new instance of ExoPlayer // set up audio attributes for the preview player
exoPlayer = ExoPlayer.Builder(context).build() 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 // create a MediaItem with the streamUri
val mediaItem = MediaItem.fromUri(streamUri) val mediaItem = MediaItem.fromUri(streamUri)
@@ -199,7 +229,7 @@ class SearchResultAdapter(
.setContentType(AudioAttributes.CONTENT_TYPE_MUSIC) .setContentType(AudioAttributes.CONTENT_TYPE_MUSIC)
.build() .build()
val focusRequest = AudioFocusRequest.Builder(AudioManager.AUDIOFOCUS_GAIN_TRANSIENT) val focusRequest = AudioFocusRequest.Builder(AudioManager.AUDIOFOCUS_GAIN)
.setAudioAttributes(audioAttributes) .setAudioAttributes(audioAttributes)
.build() .build()
@@ -11,6 +11,7 @@
android:layout_height="0dp" android:layout_height="0dp"
android:clipToPadding="false" android:clipToPadding="false"
android:paddingBottom="16dp" android:paddingBottom="16dp"
android:nextFocusRight="@id/dialog_negative_button"
app:layout_constraintBottom_toBottomOf="parent" app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toStartOf="@+id/guideline" app:layout_constraintEnd_toStartOf="@+id/guideline"
app:layout_constraintStart_toStartOf="parent" app:layout_constraintStart_toStartOf="parent"
@@ -39,6 +40,7 @@
style="@style/Widget.Material3.Button" style="@style/Widget.Material3.Button"
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:nextFocusLeft="@id/station_list"
android:text="@string/dialog_find_station_button_add" /> android:text="@string/dialog_find_station_button_add" />
<Button <Button
@@ -47,6 +49,7 @@
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:layout_marginTop="12dp" android:layout_marginTop="12dp"
android:nextFocusLeft="@id/station_list"
android:text="@string/dialog_generic_button_cancel" /> android:text="@string/dialog_generic_button_cancel" />
</LinearLayout> </LinearLayout>
@@ -9,6 +9,11 @@
android:id="@+id/station_search_box_view" android:id="@+id/station_search_box_view"
android:layout_width="0dp" android:layout_width="0dp"
android:layout_height="wrap_content" 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:iconifiedByDefault="false"
app:layout_constraintEnd_toStartOf="@+id/guideline" app:layout_constraintEnd_toStartOf="@+id/guideline"
app:layout_constraintStart_toStartOf="parent" app:layout_constraintStart_toStartOf="parent"
@@ -44,6 +49,8 @@
android:layout_marginTop="8dp" android:layout_marginTop="8dp"
android:clipToPadding="false" android:clipToPadding="false"
android:paddingBottom="16dp" android:paddingBottom="16dp"
android:nextFocusRight="@id/dialog_negative_button"
android:nextFocusUp="@id/station_search_box_view"
app:layout_constraintBottom_toBottomOf="parent" app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toStartOf="@+id/guideline" app:layout_constraintEnd_toStartOf="@+id/guideline"
app:layout_constraintStart_toStartOf="parent" app:layout_constraintStart_toStartOf="parent"
@@ -73,6 +80,8 @@
style="@style/Widget.Material3.Button" style="@style/Widget.Material3.Button"
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="wrap_content" 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" /> android:text="@string/dialog_find_station_button_add" />
<Button <Button
@@ -81,6 +90,7 @@
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:layout_marginTop="12dp" android:layout_marginTop="12dp"
android:nextFocusLeft="@id/station_search_result_list"
android:text="@string/dialog_generic_button_cancel" /> android:text="@string/dialog_generic_button_cancel" />
</LinearLayout> </LinearLayout>
@@ -7,7 +7,7 @@
android:layout_marginBottom="8dp" android:layout_marginBottom="8dp"
android:focusable="true" android:focusable="true"
android:clickable="true" android:clickable="true"
android:nextFocusRight="@+id/dialog_positive_button" android:nextFocusRight="@+id/dialog_negative_button"
android:background="@drawable/selector_search_result_item"> android:background="@drawable/selector_search_result_item">
<androidx.constraintlayout.widget.ConstraintLayout <androidx.constraintlayout.widget.ConstraintLayout
@@ -20,6 +20,7 @@
android:layout_width="0dp" android:layout_width="0dp"
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:ellipsize="marquee" android:ellipsize="marquee"
android:marqueeRepeatLimit="marquee_forever"
android:singleLine="true" android:singleLine="true"
android:textAppearance="@style/TextAppearance.Material3.TitleLarge" android:textAppearance="@style/TextAppearance.Material3.TitleLarge"
android:textColor="@color/text_default" android:textColor="@color/text_default"
@@ -35,6 +36,7 @@
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:layout_marginTop="4dp" android:layout_marginTop="4dp"
android:ellipsize="marquee" android:ellipsize="marquee"
android:marqueeRepeatLimit="marquee_forever"
android:singleLine="true" android:singleLine="true"
android:textAppearance="@style/TextAppearance.Material3.BodyLarge" android:textAppearance="@style/TextAppearance.Material3.BodyLarge"
android:textColor="@color/text_lightweight" android:textColor="@color/text_lightweight"
@@ -22,6 +22,24 @@
app:strokeColor="@color/list_card_stroke_background" app:strokeColor="@color/list_card_stroke_background"
app:strokeWidth="3dp" /> 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_music_note_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 <com.google.android.material.floatingactionbutton.ExtendedFloatingActionButton
android:id="@+id/card_settings" android:id="@+id/card_settings"
android:layout_width="wrap_content" android:layout_width="wrap_content"
@@ -13,6 +13,12 @@
<action <action
android:id="@+id/action_map_fragment_to_settings_fragment" android:id="@+id/action_map_fragment_to_settings_fragment"
app:destination="@id/settings_destination" /> 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> </fragment>
<!-- SETTINGS --> <!-- SETTINGS -->
@@ -39,4 +45,11 @@
android:id="@+id/visualizer_destination" android:id="@+id/visualizer_destination"
android:name="com.michatec.radio.VisualizerFragment" android:name="com.michatec.radio.VisualizerFragment"
android:label="Visualizer" /> 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> </navigation>