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.
This commit is contained in:
2026-06-07 13:22:11 +02:00
parent 1b9858fc4d
commit 5644c97c4c
4 changed files with 64 additions and 41 deletions
@@ -99,15 +99,15 @@ class VisualizerFragment : PreferenceFragmentCompat() {
if (data != null && data.isNotEmpty()) { if (data != null && data.isNotEmpty()) {
visualizerPref?.update(data) visualizerPref?.update(data)
} }
} else {
Log.e(TAG, "Custom command failed with result code: ${result.resultCode}")
} }
} catch (e: Exception) { } catch (_: Exception) {
Log.e(TAG, "Error fetching visualizer data", e) } finally {
handler.postDelayed(this, 20)
} }
}, MoreExecutors.directExecutor()) }, MoreExecutors.directExecutor())
} else {
handler.postDelayed(this, 100)
} }
handler.postDelayed(this, 18) // ~60 FPS
} }
} }
@@ -8,8 +8,9 @@ import androidx.media3.extractor.metadata.icy.IcyHeaders
import androidx.media3.extractor.metadata.icy.IcyInfo import androidx.media3.extractor.metadata.icy.IcyInfo
import androidx.media3.extractor.metadata.id3.Id3Frame import androidx.media3.extractor.metadata.id3.Id3Frame
import androidx.media3.extractor.metadata.id3.TextInformationFrame import androidx.media3.extractor.metadata.id3.TextInformationFrame
import androidx.media3.extractor.metadata.vorbis.VorbisComment
import com.michatec.radio.Keys import com.michatec.radio.Keys
import kotlin.math.min import java.util.Locale
/* /*
@@ -28,30 +29,44 @@ object AudioHelper {
var title = "" var title = ""
var artist = "" var artist = ""
var album = "" var album = ""
for (i in 0 until metadata.length()) { for (i in 0 until metadata.length()) {
// extract IceCast metadata // extract metadata
when (val entry = metadata.get(i)) { when (val entry = metadata.get(i)) {
is IcyInfo -> { 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 -> { is IcyHeaders -> {
Log.i(TAG, "icyHeaders:" + entry.name + " - " + entry.genre) Log.i(TAG, "icyHeaders: ${entry.name} - ${entry.genre}")
} }
is Id3Frame -> { is Id3Frame -> {
when (entry) { if (entry is TextInformationFrame) {
is TextInformationFrame -> {
when (entry.id) { when (entry.id) {
"TIT2" -> title = entry.values.getOrNull(0) ?: "" // Title "TIT2" -> entry.values.getOrNull(0)?.let { if (it.isNotEmpty()) title = it.trim() } // Title
"TPE1" -> artist = entry.values.getOrNull(0) ?: "" // Artist "TPE1" -> entry.values.getOrNull(0)?.let { if (it.isNotEmpty()) artist = it.trim() } // Artist
"TALB" -> album = entry.values.getOrNull(0) ?: "" // Album "TALB" -> entry.values.getOrNull(0)?.let { if (it.isNotEmpty()) album = it.trim() } // Album
} }
} } else {
else -> {
Log.d(TAG, "Unhandled ID3 frame: ${entry.javaClass.simpleName}") 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()
}
} }
else -> { else -> {
@@ -59,19 +74,21 @@ object AudioHelper {
} }
} }
} }
// Build metadata string // Build metadata string
var metadataString = title var metadataString = when {
if (artist.isNotEmpty() && title.isNotEmpty()) { artist.isNotEmpty() && title.isNotEmpty() -> "$artist - $title"
metadataString = "$artist - $title" artist.isNotEmpty() -> artist
title.isNotEmpty() -> title
else -> ""
} }
if (album.isNotEmpty() && metadataString.isNotEmpty()) { if (album.isNotEmpty() && metadataString.isNotEmpty()) {
metadataString += " ($album)" metadataString += " ($album)"
} }
// ensure a max length of the metadata string // ensure a max length of the metadata string
if (metadataString.isNotEmpty()) { return metadataString.take(Keys.DEFAULT_MAX_LENGTH_OF_METADATA_ENTRY)
metadataString = metadataString.take(min(metadataString.length, Keys.DEFAULT_MAX_LENGTH_OF_METADATA_ENTRY))
}
return metadataString
} }
@@ -27,7 +27,11 @@ class ExtrasHelper {
@JvmStatic @JvmStatic
private external fun visualize(surface: Surface, data: FloatArray) private external fun visualize(surface: Surface, data: FloatArray)
fun render(surface: Surface, data: FloatArray) { private val renderLock = Any()
fun render(surface: Surface?, data: FloatArray) {
if (surface == null) return
synchronized(renderLock) {
if (!surface.isValid) return if (!surface.isValid) return
try { try {
visualize(surface, data) visualize(surface, data)
@@ -36,6 +40,7 @@ class ExtrasHelper {
} }
} }
} }
}
class VisualizerPreference(context: Context, attrs: AttributeSet? = null) : Preference(context, attrs) { class VisualizerPreference(context: Context, attrs: AttributeSet? = null) : Preference(context, attrs) {
private var visualizerView: VisualizerView? = null private var visualizerView: VisualizerView? = null
@@ -97,22 +102,25 @@ class ExtrasHelper {
} }
fun update(data: FloatArray) { fun update(data: FloatArray) {
val s = surface render(surface, data)
if (s != null && s.isValid) {
render(s, data)
}
} }
override fun surfaceCreated(holder: SurfaceHolder) { override fun surfaceCreated(holder: SurfaceHolder) {
synchronized(renderLock) {
surface = holder.surface surface = holder.surface
} }
}
override fun surfaceChanged(holder: SurfaceHolder, format: Int, width: Int, height: Int) { override fun surfaceChanged(holder: SurfaceHolder, format: Int, width: Int, height: Int) {
synchronized(renderLock) {
surface = holder.surface surface = holder.surface
} }
}
override fun surfaceDestroyed(holder: SurfaceHolder) { override fun surfaceDestroyed(holder: SurfaceHolder) {
synchronized(renderLock) {
surface = null surface = null
} }
} }
}
} }
@@ -74,7 +74,6 @@ class NativeAudioProcessor : BaseAudioProcessor() {
directBuffer = ByteBuffer.allocateDirect(size).order(ByteOrder.nativeOrder()) directBuffer = ByteBuffer.allocateDirect(size).order(ByteOrder.nativeOrder())
} }
directBuffer!!.clear() directBuffer!!.clear()
inputBuffer.position()
directBuffer!!.put(inputBuffer) directBuffer!!.put(inputBuffer)
directBuffer!!.flip() directBuffer!!.flip()
bufferToProcess = directBuffer!! bufferToProcess = directBuffer!!
@@ -84,7 +83,6 @@ class NativeAudioProcessor : BaseAudioProcessor() {
val out = replaceOutputBuffer(size) val out = replaceOutputBuffer(size)
out.order(ByteOrder.nativeOrder()) out.order(ByteOrder.nativeOrder())
bufferToProcess.position(0)
out.put(bufferToProcess) out.put(bufferToProcess)
out.flip() out.flip()
} }