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()) {
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
}
}
@@ -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)
}
@@ -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
}
}
}
}
@@ -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()
}