From 5644c97c4c18dc3285652519c64c8fe19dfa228b Mon Sep 17 00:00:00 2001 From: Michatec Date: Sun, 7 Jun 2026 13:22:11 +0200 Subject: [PATCH] feat: improve metadata parsing and visualizer stability - Enhance metadata extraction in `AudioHelper` by adding `VorbisComment` support and improving "Artist - Title" parsing for Icy streams. - Implement thread safety in `ExtrasHelper` using a synchronization lock for native visualization and surface lifecycle management. - Refactor visualizer update logic in `VisualizerFragment` to improve performance and error resilience. - Remove redundant buffer position calls in `NativeAudioProcessor`. - Clean up `AudioHelper` logic using Kotlin idiomatic patterns for string building and property access. --- .../com/michatec/radio/VisualizerFragment.kt | 10 ++-- .../com/michatec/radio/helpers/AudioHelper.kt | 59 ++++++++++++------- .../michatec/radio/helpers/ExtrasHelper.kt | 34 +++++++---- .../radio/helpers/NativeAudioProcessor.kt | 2 - 4 files changed, 64 insertions(+), 41 deletions(-) diff --git a/app/src/main/java/com/michatec/radio/VisualizerFragment.kt b/app/src/main/java/com/michatec/radio/VisualizerFragment.kt index 37d1076..cfdfd13 100644 --- a/app/src/main/java/com/michatec/radio/VisualizerFragment.kt +++ b/app/src/main/java/com/michatec/radio/VisualizerFragment.kt @@ -99,15 +99,15 @@ class VisualizerFragment : PreferenceFragmentCompat() { if (data != null && data.isNotEmpty()) { visualizerPref?.update(data) } - } else { - Log.e(TAG, "Custom command failed with result code: ${result.resultCode}") } - } catch (e: Exception) { - Log.e(TAG, "Error fetching visualizer data", e) + } catch (_: Exception) { + } finally { + handler.postDelayed(this, 20) } }, MoreExecutors.directExecutor()) + } else { + handler.postDelayed(this, 100) } - handler.postDelayed(this, 18) // ~60 FPS } } diff --git a/app/src/main/java/com/michatec/radio/helpers/AudioHelper.kt b/app/src/main/java/com/michatec/radio/helpers/AudioHelper.kt index d2474fb..00952f1 100644 --- a/app/src/main/java/com/michatec/radio/helpers/AudioHelper.kt +++ b/app/src/main/java/com/michatec/radio/helpers/AudioHelper.kt @@ -8,8 +8,9 @@ import androidx.media3.extractor.metadata.icy.IcyHeaders import androidx.media3.extractor.metadata.icy.IcyInfo import androidx.media3.extractor.metadata.id3.Id3Frame import androidx.media3.extractor.metadata.id3.TextInformationFrame +import androidx.media3.extractor.metadata.vorbis.VorbisComment import com.michatec.radio.Keys -import kotlin.math.min +import java.util.Locale /* @@ -28,29 +29,43 @@ object AudioHelper { var title = "" var artist = "" var album = "" + for (i in 0 until metadata.length()) { - // extract IceCast metadata + // extract metadata when (val entry = metadata.get(i)) { is IcyInfo -> { - title = entry.title.toString() + val streamTitle = entry.title + if (!streamTitle.isNullOrEmpty()) { + if (streamTitle.contains(" - ")) { + artist = streamTitle.substringBefore(" - ").trim() + title = streamTitle.substringAfter(" - ").trim() + } else { + title = streamTitle + } + } } is IcyHeaders -> { - Log.i(TAG, "icyHeaders:" + entry.name + " - " + entry.genre) + Log.i(TAG, "icyHeaders: ${entry.name} - ${entry.genre}") } is Id3Frame -> { - when (entry) { - is TextInformationFrame -> { - when (entry.id) { - "TIT2" -> title = entry.values.getOrNull(0) ?: "" // Title - "TPE1" -> artist = entry.values.getOrNull(0) ?: "" // Artist - "TALB" -> album = entry.values.getOrNull(0) ?: "" // Album - } - } - else -> { - Log.d(TAG, "Unhandled ID3 frame: ${entry.javaClass.simpleName}") + if (entry is TextInformationFrame) { + when (entry.id) { + "TIT2" -> entry.values.getOrNull(0)?.let { if (it.isNotEmpty()) title = it.trim() } // Title + "TPE1" -> entry.values.getOrNull(0)?.let { if (it.isNotEmpty()) artist = it.trim() } // Artist + "TALB" -> entry.values.getOrNull(0)?.let { if (it.isNotEmpty()) album = it.trim() } // Album } + } else { + Log.d(TAG, "Unhandled ID3 frame: ${entry.javaClass.simpleName}") + } + } + + is VorbisComment -> { + when (entry.key.uppercase(Locale.ROOT)) { + "TITLE" -> if (entry.value.isNotEmpty()) title = entry.value.trim() + "ARTIST" -> if (entry.value.isNotEmpty()) artist = entry.value.trim() + "ALBUM" -> if (entry.value.isNotEmpty()) album = entry.value.trim() } } @@ -59,19 +74,21 @@ object AudioHelper { } } } + // Build metadata string - var metadataString = title - if (artist.isNotEmpty() && title.isNotEmpty()) { - metadataString = "$artist - $title" + var metadataString = when { + artist.isNotEmpty() && title.isNotEmpty() -> "$artist - $title" + artist.isNotEmpty() -> artist + title.isNotEmpty() -> title + else -> "" } + if (album.isNotEmpty() && metadataString.isNotEmpty()) { metadataString += " ($album)" } + // ensure a max length of the metadata string - if (metadataString.isNotEmpty()) { - metadataString = metadataString.take(min(metadataString.length, Keys.DEFAULT_MAX_LENGTH_OF_METADATA_ENTRY)) - } - return metadataString + return metadataString.take(Keys.DEFAULT_MAX_LENGTH_OF_METADATA_ENTRY) } diff --git a/app/src/main/java/com/michatec/radio/helpers/ExtrasHelper.kt b/app/src/main/java/com/michatec/radio/helpers/ExtrasHelper.kt index a61ee95..1d0f3b3 100644 --- a/app/src/main/java/com/michatec/radio/helpers/ExtrasHelper.kt +++ b/app/src/main/java/com/michatec/radio/helpers/ExtrasHelper.kt @@ -27,12 +27,17 @@ class ExtrasHelper { @JvmStatic private external fun visualize(surface: Surface, data: FloatArray) - fun render(surface: Surface, data: FloatArray) { - if (!surface.isValid) return - try { - visualize(surface, data) - } catch (e: Exception) { - Log.e(TAG, "Native visualize failed", e) + private val renderLock = Any() + + fun render(surface: Surface?, data: FloatArray) { + if (surface == null) return + synchronized(renderLock) { + if (!surface.isValid) return + try { + visualize(surface, data) + } catch (e: Exception) { + Log.e(TAG, "Native visualize failed", e) + } } } } @@ -97,22 +102,25 @@ class ExtrasHelper { } fun update(data: FloatArray) { - val s = surface - if (s != null && s.isValid) { - render(s, data) - } + render(surface, data) } override fun surfaceCreated(holder: SurfaceHolder) { - surface = holder.surface + synchronized(renderLock) { + surface = holder.surface + } } override fun surfaceChanged(holder: SurfaceHolder, format: Int, width: Int, height: Int) { - surface = holder.surface + synchronized(renderLock) { + surface = holder.surface + } } override fun surfaceDestroyed(holder: SurfaceHolder) { - surface = null + synchronized(renderLock) { + surface = null + } } } } diff --git a/app/src/main/java/com/michatec/radio/helpers/NativeAudioProcessor.kt b/app/src/main/java/com/michatec/radio/helpers/NativeAudioProcessor.kt index f3b9ccc..12f446a 100644 --- a/app/src/main/java/com/michatec/radio/helpers/NativeAudioProcessor.kt +++ b/app/src/main/java/com/michatec/radio/helpers/NativeAudioProcessor.kt @@ -74,7 +74,6 @@ class NativeAudioProcessor : BaseAudioProcessor() { directBuffer = ByteBuffer.allocateDirect(size).order(ByteOrder.nativeOrder()) } directBuffer!!.clear() - inputBuffer.position() directBuffer!!.put(inputBuffer) directBuffer!!.flip() bufferToProcess = directBuffer!! @@ -84,7 +83,6 @@ class NativeAudioProcessor : BaseAudioProcessor() { val out = replaceOutputBuffer(size) out.order(ByteOrder.nativeOrder()) - bufferToProcess.position(0) out.put(bufferToProcess) out.flip() }