mirror of
https://github.com/Michatec/Radio.git
synced 2026-06-15 14:53:21 +02:00
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:
@@ -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,29 +29,43 @@ 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" -> entry.values.getOrNull(0)?.let { if (it.isNotEmpty()) title = it.trim() } // Title
|
||||||
"TIT2" -> title = entry.values.getOrNull(0) ?: "" // Title
|
"TPE1" -> entry.values.getOrNull(0)?.let { if (it.isNotEmpty()) artist = it.trim() } // Artist
|
||||||
"TPE1" -> artist = entry.values.getOrNull(0) ?: "" // Artist
|
"TALB" -> entry.values.getOrNull(0)?.let { if (it.isNotEmpty()) album = it.trim() } // Album
|
||||||
"TALB" -> album = entry.values.getOrNull(0) ?: "" // Album
|
|
||||||
}
|
|
||||||
}
|
|
||||||
else -> {
|
|
||||||
Log.d(TAG, "Unhandled ID3 frame: ${entry.javaClass.simpleName}")
|
|
||||||
}
|
}
|
||||||
|
} 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
|
// 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,12 +27,17 @@ 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()
|
||||||
if (!surface.isValid) return
|
|
||||||
try {
|
fun render(surface: Surface?, data: FloatArray) {
|
||||||
visualize(surface, data)
|
if (surface == null) return
|
||||||
} catch (e: Exception) {
|
synchronized(renderLock) {
|
||||||
Log.e(TAG, "Native visualize failed", e)
|
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) {
|
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) {
|
||||||
surface = holder.surface
|
synchronized(renderLock) {
|
||||||
|
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) {
|
||||||
surface = holder.surface
|
synchronized(renderLock) {
|
||||||
|
surface = holder.surface
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun surfaceDestroyed(holder: SurfaceHolder) {
|
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 = 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()
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user