From 8053f5397f3096355cb9fc4009e1dd148ba369d0 Mon Sep 17 00:00:00 2001 From: Damontecres Date: Wed, 4 Mar 2026 19:45:28 -0500 Subject: [PATCH] More visualizer --- .../wholphin/services/MusicService.kt | 54 +--------------- .../ui/components/RecommendedMusic.kt | 3 + .../wholphin/ui/detail/music/BarVisualizer.kt | 2 +- .../ui/detail/music/NowPlayingPage.kt | 10 ++- .../ui/detail/music/NowPlayingViewModel.kt | 63 ++++++++++++++++--- 5 files changed, 69 insertions(+), 63 deletions(-) diff --git a/app/src/main/java/com/github/damontecres/wholphin/services/MusicService.kt b/app/src/main/java/com/github/damontecres/wholphin/services/MusicService.kt index c8371613..d8a43a7a 100644 --- a/app/src/main/java/com/github/damontecres/wholphin/services/MusicService.kt +++ b/app/src/main/java/com/github/damontecres/wholphin/services/MusicService.kt @@ -1,7 +1,6 @@ package com.github.damontecres.wholphin.services import android.content.Context -import android.media.audiofx.Visualizer import androidx.annotation.OptIn import androidx.compose.runtime.Composable import androidx.compose.runtime.Stable @@ -44,8 +43,6 @@ import timber.log.Timber import java.util.UUID import javax.inject.Inject import javax.inject.Singleton -import kotlin.math.abs -import kotlin.math.min @OptIn(UnstableApi::class) @Singleton @@ -57,12 +54,10 @@ class MusicService private val api: ApiClient, private val serverRepository: ServerRepository, private val imageUrlService: ImageUrlService, - ) : Visualizer.OnDataCaptureListener { + ) { private val _state = MutableStateFlow(MusicServiceState.EMPTY) val state: StateFlow = _state - val viz = MutableStateFlow(IntArray(0)) - val player: Player by lazy { ExoPlayer .Builder(context) @@ -76,12 +71,6 @@ class MusicService } } - private val visualizer by lazy { - Visualizer(player.audioSessionId).apply { - captureSize = Visualizer.getCaptureSizeRange()[1] - } - } - private val mutex = Mutex() var mediaSession: MediaSession? = null private set @@ -92,15 +81,6 @@ class MusicService if (mediaSession == null) { Timber.i("Starting music MediaSession") mediaSession = MediaSession.Builder(context, player).build() - onMain { - visualizer.setDataCaptureListener( - this@MusicService, - Visualizer.getMaxCaptureRate() / 2, - true, - false, - ) - visualizer.enabled = true - } } } } @@ -122,7 +102,6 @@ class MusicService player.stop() player.setMediaItems(emptyList()) } - visualizer.enabled = false _state.update { MusicServiceState.EMPTY } @@ -308,37 +287,6 @@ class MusicService _state.update { it.copy(loadingState = LoadingState.Success) } return result } - - override fun onFftDataCapture( - visualizer: Visualizer, - fft: ByteArray, - samplingRate: Int, - ) { - } - - override fun onWaveFormDataCapture( - visualizer: Visualizer, - waveform: ByteArray, - samplingRate: Int, - ) { - val resolution = 64 - val processed = IntArray(resolution) - val captureSize = - Visualizer.getCaptureSizeRange()[1] - val groupSize = captureSize / resolution -// val groupSize = captureSize / resolution.toFloat() - for (i in 0 until resolution) { - processed[i] = - waveform - .map { abs(it.toInt()) } -// .map { it.toInt() } - .subList(i * groupSize, min((i + 1) * groupSize, waveform.size)) - .average() - .toInt() -// processed[i] = abs(waveform[ceil(i * finder).toInt()].toInt()) - } - viz.update { processed } - } } @Stable diff --git a/app/src/main/java/com/github/damontecres/wholphin/ui/components/RecommendedMusic.kt b/app/src/main/java/com/github/damontecres/wholphin/ui/components/RecommendedMusic.kt index 007e46e5..87cc7c0d 100644 --- a/app/src/main/java/com/github/damontecres/wholphin/ui/components/RecommendedMusic.kt +++ b/app/src/main/java/com/github/damontecres/wholphin/ui/components/RecommendedMusic.kt @@ -15,6 +15,7 @@ import com.github.damontecres.wholphin.preferences.AppPreferences import com.github.damontecres.wholphin.preferences.UserPreferences import com.github.damontecres.wholphin.services.BackdropService import com.github.damontecres.wholphin.services.FavoriteWatchManager +import com.github.damontecres.wholphin.services.MediaManagementService import com.github.damontecres.wholphin.services.MediaReportService import com.github.damontecres.wholphin.services.NavigationManager import com.github.damontecres.wholphin.services.SuggestionService @@ -62,12 +63,14 @@ class RecommendedMusicViewModel favoriteWatchManager: FavoriteWatchManager, mediaReportService: MediaReportService, backdropService: BackdropService, + mediaManagementService: MediaManagementService, ) : RecommendedViewModel( context, navigationManager, favoriteWatchManager, mediaReportService, backdropService, + mediaManagementService, ) { @AssistedFactory interface Factory { diff --git a/app/src/main/java/com/github/damontecres/wholphin/ui/detail/music/BarVisualizer.kt b/app/src/main/java/com/github/damontecres/wholphin/ui/detail/music/BarVisualizer.kt index 5197146b..e707b93e 100644 --- a/app/src/main/java/com/github/damontecres/wholphin/ui/detail/music/BarVisualizer.kt +++ b/app/src/main/java/com/github/damontecres/wholphin/ui/detail/music/BarVisualizer.kt @@ -40,7 +40,7 @@ fun BoxScope.BarVisualizer( data.forEachIndexed { index, data -> val height by animateDpAsState( - targetValue = size.height * data / 128f / 1.5f, + targetValue = size.height * data / 128f, animationSpec = tween(easing = LinearEasing), ) Box( diff --git a/app/src/main/java/com/github/damontecres/wholphin/ui/detail/music/NowPlayingPage.kt b/app/src/main/java/com/github/damontecres/wholphin/ui/detail/music/NowPlayingPage.kt index af77b169..5d410e29 100644 --- a/app/src/main/java/com/github/damontecres/wholphin/ui/detail/music/NowPlayingPage.kt +++ b/app/src/main/java/com/github/damontecres/wholphin/ui/detail/music/NowPlayingPage.kt @@ -15,6 +15,7 @@ import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.Row import androidx.compose.foundation.layout.fillMaxHeight import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.width import androidx.compose.runtime.Composable @@ -166,12 +167,19 @@ fun NowPlayingPage( .fillMaxSize() .weight(1f), ) { - Box(modifier = Modifier.fillMaxSize(.7f)) { + Box( + contentAlignment = Alignment.Center, + modifier = + Modifier + .fillMaxWidth() + .fillMaxHeight(.75f), + ) { AsyncImage( contentDescription = null, model = current?.imageUrl, modifier = Modifier.fillMaxSize(), ) + BarVisualizer( data = viz, modifier = Modifier.fillMaxSize(), diff --git a/app/src/main/java/com/github/damontecres/wholphin/ui/detail/music/NowPlayingViewModel.kt b/app/src/main/java/com/github/damontecres/wholphin/ui/detail/music/NowPlayingViewModel.kt index 160b8230..0335e21f 100644 --- a/app/src/main/java/com/github/damontecres/wholphin/ui/detail/music/NowPlayingViewModel.kt +++ b/app/src/main/java/com/github/damontecres/wholphin/ui/detail/music/NowPlayingViewModel.kt @@ -1,15 +1,13 @@ package com.github.damontecres.wholphin.ui.detail.music import android.content.Context +import android.media.audiofx.Visualizer import androidx.lifecycle.ViewModel import androidx.lifecycle.viewModelScope import androidx.media3.common.MediaItem import androidx.media3.common.Player import com.github.damontecres.wholphin.data.model.AudioItem import com.github.damontecres.wholphin.preferences.AppPreference -import com.github.damontecres.wholphin.services.BackdropService -import com.github.damontecres.wholphin.services.FavoriteWatchManager -import com.github.damontecres.wholphin.services.ImageUrlService import com.github.damontecres.wholphin.services.MusicService import com.github.damontecres.wholphin.services.NavigationManager import com.github.damontecres.wholphin.services.UserPreferencesService @@ -35,6 +33,8 @@ import org.jellyfin.sdk.model.api.LyricDto import org.jellyfin.sdk.model.extensions.ticks import timber.log.Timber import java.util.UUID +import kotlin.math.abs +import kotlin.math.ceil import kotlin.time.Duration import kotlin.time.Duration.Companion.milliseconds @@ -45,18 +45,22 @@ class NowPlayingViewModel private val api: ApiClient, @param:ApplicationContext private val context: Context, val navigationManager: NavigationManager, - private val favoriteWatchManager: FavoriteWatchManager, - private val backdropService: BackdropService, - private val imageUrlService: ImageUrlService, private val musicService: MusicService, val userPreferencesService: UserPreferencesService, ) : ViewModel(), + Visualizer.OnDataCaptureListener, Player.Listener { @AssistedFactory interface Factory { fun create(): NowPlayingViewModel } + private val visualizer by lazy { + Visualizer(player.audioSessionId).apply { + captureSize = Visualizer.getCaptureSizeRange()[1] + } + } + val controllerViewState = ControllerViewState( AppPreference.Companion.ControllerTimeout.defaultValue, @@ -66,7 +70,7 @@ class NowPlayingViewModel val state = MutableStateFlow(NowPlayingState(musicService.state.value)) val player get() = musicService.player - val viz = musicService.viz + val viz = MutableStateFlow(IntArray(0)) private val lyricCache = InMemoryKache(20) { @@ -75,7 +79,18 @@ class NowPlayingViewModel init { player.addListener(this) - addCloseable { player.removeListener(this) } + visualizer.setDataCaptureListener( + this, + Visualizer.getMaxCaptureRate() / 2, + true, + false, + ) + visualizer.enabled = true + + addCloseable { + player.removeListener(this) + visualizer.release() + } viewModelScope.launchDefault { musicService.state.collectLatest { musicServiceState -> state.update { it.copy(musicServiceState = musicServiceState) } @@ -192,4 +207,36 @@ class NowPlayingViewModel navigationManager.goBack() } } + + override fun onFftDataCapture( + visualizer: Visualizer, + fft: ByteArray, + samplingRate: Int, + ) { + } + + override fun onWaveFormDataCapture( + visualizer: Visualizer, + waveform: ByteArray, + samplingRate: Int, + ) { + val resolution = 64 + val processed = IntArray(resolution) + val captureSize = + Visualizer.getCaptureSizeRange()[1] +// val groupSize = captureSize / resolution + val groupSize = captureSize / resolution.toFloat() + for (i in 0 until resolution) { +// processed[i] = +// waveform +// .map { abs(it.toInt()) } +// .map { it.toInt() } +// .subList(i * groupSize, min((i + 1) * groupSize, waveform.size)) +// .average() +// .toInt() + val v = abs(waveform[ceil(i * groupSize).toInt()].toInt()) + processed[i] = v * v / (128) + } + viz.update { processed } + } }