mirror of
https://github.com/Michatec/Radio.git
synced 2026-05-31 04:22:40 +02:00
Compare commits
4 Commits
e0d1770a19
...
cdf7668d43
| Author | SHA1 | Date | |
|---|---|---|---|
| cdf7668d43 | |||
| f755dc5173 | |||
| 8c7a8ce7c4 | |||
| d1cc340417 |
+82
-37
@@ -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>
|
||||||
|
|||||||
Reference in New Issue
Block a user