Twelve: Add visualizer
Co-authored-by: Luca Stefani <luca.stefani.ge1@gmail.com>
Change-Id: Ib7630cdb939e16afa6e1edf9c215656cee99ff8f
diff --git a/app/Android.bp b/app/Android.bp
index dad3ac2..0b76895 100644
--- a/app/Android.bp
+++ b/app/Android.bp
@@ -56,6 +56,7 @@
"androidx.viewpager2_viewpager2",
"kotlinx_coroutines_guava",
"Twelve_com.google.android.material_material",
+ "Twelve_com.github.bogerchan_Nier-Visualizer",
],
optimize: {
diff --git a/app/build.gradle.kts b/app/build.gradle.kts
index 1ee5846..b287211 100644
--- a/app/build.gradle.kts
+++ b/app/build.gradle.kts
@@ -92,6 +92,9 @@
implementation(libs.androidx.viewpager2)
implementation(libs.kotlinx.coroutines.guava)
implementation(libs.material)
+ implementation(libs.nier.visualizer) {
+ exclude(group = "com.android.support")
+ }
}
configure<GenerateBpPluginExtension> {
diff --git a/app/libs/Android.bp b/app/libs/Android.bp
index dcde572..03ef205 100644
--- a/app/libs/Android.bp
+++ b/app/libs/Android.bp
@@ -409,6 +409,36 @@
java_version: "1.7",
}
+android_library_import {
+ name: "Twelve_com.github.bogerchan_Nier-Visualizer-nodeps",
+ aars: ["com/github/bogerchan/Nier-Visualizer/v0.1.3/Nier-Visualizer-v0.1.3.aar"],
+ sdk_version: "35",
+ min_sdk_version: "14",
+ apex_available: [
+ "//apex_available:platform",
+ "//apex_available:anyapex",
+ ],
+ static_libs: [
+ "org.jetbrains.kotlin_kotlin-stdlib-jre7",
+ ],
+}
+
+android_library {
+ name: "Twelve_com.github.bogerchan_Nier-Visualizer",
+ sdk_version: "35",
+ min_sdk_version: "14",
+ apex_available: [
+ "//apex_available:platform",
+ "//apex_available:anyapex",
+ ],
+ manifest: "com/github/bogerchan/Nier-Visualizer/v0.1.3/AndroidManifest.xml",
+ static_libs: [
+ "Twelve_com.github.bogerchan_Nier-Visualizer-nodeps",
+ "org.jetbrains.kotlin_kotlin-stdlib-jre7",
+ ],
+ java_version: "1.7",
+}
+
java_import {
name: "Twelve_com.github.philburk_jsyn-nodeps",
jars: ["com/github/philburk/jsyn/40a41092cbab558d7d410ec43d93bb1e4121e86a/jsyn-40a41092cbab558d7d410ec43d93bb1e4121e86a.jar"],
diff --git a/app/libs/com/github/bogerchan/Nier-Visualizer/v0.1.3/AndroidManifest.xml b/app/libs/com/github/bogerchan/Nier-Visualizer/v0.1.3/AndroidManifest.xml
new file mode 100644
index 0000000..9ea6a36
--- /dev/null
+++ b/app/libs/com/github/bogerchan/Nier-Visualizer/v0.1.3/AndroidManifest.xml
@@ -0,0 +1,11 @@
+<?xml version="1.0" encoding="utf-8"?>
+<manifest xmlns:android="http://schemas.android.com/apk/res/android"
+ package="me.bogerchan.niervisualizer.core"
+ android:versionCode="1"
+ android:versionName="1.0" >
+
+ <uses-sdk
+ android:minSdkVersion="19"
+ android:targetSdkVersion="26" />
+
+</manifest>
\ No newline at end of file
diff --git a/app/libs/com/github/bogerchan/Nier-Visualizer/v0.1.3/AndroidManifest.xml.license b/app/libs/com/github/bogerchan/Nier-Visualizer/v0.1.3/AndroidManifest.xml.license
new file mode 100644
index 0000000..ce7d22b
--- /dev/null
+++ b/app/libs/com/github/bogerchan/Nier-Visualizer/v0.1.3/AndroidManifest.xml.license
@@ -0,0 +1,2 @@
+SPDX-FileCopyrightText: 2017-2024 Boger Chan
+
diff --git a/app/libs/com/github/bogerchan/Nier-Visualizer/v0.1.3/Nier-Visualizer-v0.1.3.aar b/app/libs/com/github/bogerchan/Nier-Visualizer/v0.1.3/Nier-Visualizer-v0.1.3.aar
new file mode 100644
index 0000000..8523637
--- /dev/null
+++ b/app/libs/com/github/bogerchan/Nier-Visualizer/v0.1.3/Nier-Visualizer-v0.1.3.aar
Binary files differ
diff --git a/app/libs/com/github/bogerchan/Nier-Visualizer/v0.1.3/Nier-Visualizer-v0.1.3.aar.license b/app/libs/com/github/bogerchan/Nier-Visualizer/v0.1.3/Nier-Visualizer-v0.1.3.aar.license
new file mode 100644
index 0000000..ce7d22b
--- /dev/null
+++ b/app/libs/com/github/bogerchan/Nier-Visualizer/v0.1.3/Nier-Visualizer-v0.1.3.aar.license
@@ -0,0 +1,2 @@
+SPDX-FileCopyrightText: 2017-2024 Boger Chan
+
diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml
index e048618..112da23 100644
--- a/app/src/main/AndroidManifest.xml
+++ b/app/src/main/AndroidManifest.xml
@@ -17,6 +17,7 @@
android:name="android.permission.READ_EXTERNAL_STORAGE"
android:maxSdkVersion="32" />
<uses-permission android:name="android.permission.READ_MEDIA_AUDIO" />
+ <uses-permission android:name="android.permission.RECORD_AUDIO" />
<application
android:name=".TwelveApplication"
diff --git a/app/src/main/java/org/lineageos/twelve/fragments/NowPlayingFragment.kt b/app/src/main/java/org/lineageos/twelve/fragments/NowPlayingFragment.kt
index 092eb6b..041bd78 100644
--- a/app/src/main/java/org/lineageos/twelve/fragments/NowPlayingFragment.kt
+++ b/app/src/main/java/org/lineageos/twelve/fragments/NowPlayingFragment.kt
@@ -8,10 +8,12 @@
import android.animation.ValueAnimator
import android.content.Intent
import android.graphics.ImageDecoder
+import android.graphics.PixelFormat
import android.icu.text.DecimalFormat
import android.icu.text.DecimalFormatSymbols
import android.media.audiofx.AudioEffect
import android.os.Bundle
+import android.view.SurfaceView
import android.view.View
import android.view.animation.LinearInterpolator
import android.widget.ImageView
@@ -26,7 +28,9 @@
import androidx.core.widget.NestedScrollView
import androidx.fragment.app.Fragment
import androidx.fragment.app.viewModels
+import androidx.lifecycle.DefaultLifecycleObserver
import androidx.lifecycle.Lifecycle
+import androidx.lifecycle.LifecycleOwner
import androidx.lifecycle.lifecycleScope
import androidx.lifecycle.repeatOnLifecycle
import androidx.media3.common.Player
@@ -38,6 +42,7 @@
import com.google.android.material.slider.Slider
import kotlinx.coroutines.flow.collectLatest
import kotlinx.coroutines.launch
+import me.bogerchan.niervisualizer.NierVisualizerManager
import org.lineageos.twelve.R
import org.lineageos.twelve.TwelveApplication
import org.lineageos.twelve.ext.getViewProperty
@@ -62,7 +67,6 @@
private val albumTitleTextView by getViewProperty<TextView>(R.id.albumTitleTextView)
private val audioTitleTextView by getViewProperty<TextView>(R.id.audioTitleTextView)
private val artistNameTextView by getViewProperty<TextView>(R.id.artistNameTextView)
- private val castMaterialButton by getViewProperty<MaterialButton>(R.id.castMaterialButton)
private val currentTimestampTextView by getViewProperty<TextView>(R.id.currentTimestampTextView)
private val durationTimestampTextView by getViewProperty<TextView>(R.id.durationTimestampTextView)
private val equalizerMaterialButton by getViewProperty<MaterialButton>(R.id.equalizerMaterialButton)
@@ -81,17 +85,75 @@
private val shuffleMarkerImageView by getViewProperty<ImageView>(R.id.shuffleMarkerImageView)
private val shuffleMaterialButton by getViewProperty<MaterialButton>(R.id.shuffleMaterialButton)
private val toolbar by getViewProperty<MaterialToolbar>(R.id.toolbar)
+ private val visualizerMaterialButton by getViewProperty<MaterialButton>(R.id.visualizerMaterialButton)
+ private val visualizerSurfaceView by getViewProperty<SurfaceView>(R.id.visualizerSurfaceView)
// Progress slider state
private var isProgressSliderDragging = false
private var animator: ValueAnimator? = null
// AudioFX
+ private val audioSessionId: Int
+ get() = (requireActivity().application as TwelveApplication).audioSessionId
private val audioEffectsStartForResult =
registerForActivityResult(ActivityResultContracts.StartActivityForResult()) {
// Empty
}
+ // Visualizer
+ private val visualizerManager = NierVisualizerManager()
+ private val visualizerViewLifecycleObserver = object : DefaultLifecycleObserver {
+ private var isVisualizerInitialized = false
+ private var isVisualizerStarted = false
+
+ override fun onCreate(owner: LifecycleOwner) {
+ val initResult = visualizerManager.init(audioSessionId)
+ isVisualizerInitialized = initResult == NierVisualizerManager.SUCCESS
+
+ owner.lifecycleScope.launch {
+ owner.repeatOnLifecycle(Lifecycle.State.STARTED) {
+ viewModel.currentVisualizerType.collectLatest { currentVisualizerType ->
+ if (isVisualizerInitialized) {
+ currentVisualizerType.factory.invoke()?.let {
+ visualizerManager.start(visualizerSurfaceView, it)
+ isVisualizerStarted = true
+ } ?: run {
+ visualizerManager.stop()
+ isVisualizerStarted = false
+ }
+ }
+ }
+ }
+ }
+ }
+
+ override fun onResume(owner: LifecycleOwner) {
+ if (isVisualizerStarted) {
+ visualizerManager.resume()
+ }
+ }
+
+ override fun onPause(owner: LifecycleOwner) {
+ if (isVisualizerStarted) {
+ visualizerManager.pause()
+ }
+ }
+
+ override fun onStop(owner: LifecycleOwner) {
+ if (isVisualizerStarted) {
+ visualizerManager.stop()
+ }
+ isVisualizerStarted = false
+ }
+
+ override fun onDestroy(owner: LifecycleOwner) {
+ if (isVisualizerInitialized) {
+ visualizerManager.release()
+ }
+ isVisualizerInitialized = false
+ }
+ }
+
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
@@ -117,6 +179,12 @@
)
}
+ // Visualizer
+ visualizerSurfaceView.setZOrderOnTop(true)
+ visualizerSurfaceView.holder.setFormat(PixelFormat.TRANSPARENT)
+
+ viewLifecycleOwner.lifecycle.addObserver(visualizerViewLifecycleObserver)
+
// Audio informations
audioTitleTextView.isSelected = true
artistNameTextView.isSelected = true
@@ -166,22 +234,25 @@
}
equalizerMaterialButton.setOnClickListener {
- val activity = requireActivity()
-
// Open system equalizer
audioEffectsStartForResult.launch(
Intent(AudioEffect.ACTION_DISPLAY_AUDIO_EFFECT_CONTROL_PANEL).apply {
- putExtra(AudioEffect.EXTRA_PACKAGE_NAME, activity.packageName)
- putExtra(
- AudioEffect.EXTRA_AUDIO_SESSION,
- (activity.application as TwelveApplication).audioSessionId
- )
+ putExtra(AudioEffect.EXTRA_PACKAGE_NAME, requireContext().packageName)
+ putExtra(AudioEffect.EXTRA_AUDIO_SESSION, audioSessionId)
putExtra(AudioEffect.EXTRA_CONTENT_TYPE, AudioEffect.CONTENT_TYPE_MUSIC)
},
null
)
}
+ visualizerMaterialButton.setOnClickListener {
+ viewModel.nextVisualizerType()
+ }
+ visualizerMaterialButton.setOnLongClickListener {
+ viewModel.disableVisualizer()
+ true
+ }
+
viewLifecycleOwner.lifecycleScope.launch {
viewLifecycleOwner.repeatOnLifecycle(Lifecycle.State.STARTED) {
launch {
@@ -438,6 +509,13 @@
}
}
}
+
+ launch {
+ viewModel.currentVisualizerType.collectLatest {
+ visualizerSurfaceView.isVisible =
+ it != NowPlayingViewModel.VisualizerType.NONE
+ }
+ }
}
}
}
diff --git a/app/src/main/java/org/lineageos/twelve/utils/PermissionsUtils.kt b/app/src/main/java/org/lineageos/twelve/utils/PermissionsUtils.kt
index ce09b8c..5df9ad9 100644
--- a/app/src/main/java/org/lineageos/twelve/utils/PermissionsUtils.kt
+++ b/app/src/main/java/org/lineageos/twelve/utils/PermissionsUtils.kt
@@ -15,11 +15,13 @@
/**
* Permissions required to run the app
*/
- val mainPermissions = mutableListOf<String>().apply {
+ val mainPermissions = buildList {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) {
add(Manifest.permission.READ_MEDIA_AUDIO)
} else {
add(Manifest.permission.READ_EXTERNAL_STORAGE)
}
+
+ add(Manifest.permission.RECORD_AUDIO)
}.toTypedArray()
}
diff --git a/app/src/main/java/org/lineageos/twelve/viewmodels/NowPlayingViewModel.kt b/app/src/main/java/org/lineageos/twelve/viewmodels/NowPlayingViewModel.kt
index b91b54a..ebdc154 100644
--- a/app/src/main/java/org/lineageos/twelve/viewmodels/NowPlayingViewModel.kt
+++ b/app/src/main/java/org/lineageos/twelve/viewmodels/NowPlayingViewModel.kt
@@ -15,6 +15,7 @@
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.ExperimentalCoroutinesApi
import kotlinx.coroutines.delay
+import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.SharingStarted
import kotlinx.coroutines.flow.combine
import kotlinx.coroutines.flow.filterNotNull
@@ -23,6 +24,14 @@
import kotlinx.coroutines.flow.flowOn
import kotlinx.coroutines.flow.mapLatest
import kotlinx.coroutines.flow.stateIn
+import me.bogerchan.niervisualizer.renderer.IRenderer
+import me.bogerchan.niervisualizer.renderer.circle.CircleBarRenderer
+import me.bogerchan.niervisualizer.renderer.circle.CircleRenderer
+import me.bogerchan.niervisualizer.renderer.columnar.ColumnarType1Renderer
+import me.bogerchan.niervisualizer.renderer.columnar.ColumnarType2Renderer
+import me.bogerchan.niervisualizer.renderer.columnar.ColumnarType3Renderer
+import me.bogerchan.niervisualizer.renderer.columnar.ColumnarType4Renderer
+import me.bogerchan.niervisualizer.renderer.line.LineRenderer
import org.lineageos.twelve.ext.availableCommandsFlow
import org.lineageos.twelve.ext.isPlayingFlow
import org.lineageos.twelve.ext.mediaItemFlow
@@ -53,6 +62,17 @@
}
}
+ enum class VisualizerType(val factory: () -> Array<IRenderer>?) {
+ NONE({ null }),
+ TYPE_1({ arrayOf(ColumnarType1Renderer()) }),
+ TYPE_2({ arrayOf(ColumnarType2Renderer()) }),
+ TYPE_3({ arrayOf(ColumnarType3Renderer()) }),
+ TYPE_4({ arrayOf(ColumnarType4Renderer()) }),
+ LINE({ arrayOf(LineRenderer(true)) }),
+ CIRCLE_BAR({ arrayOf(CircleBarRenderer()) }),
+ CIRCLE({ arrayOf(CircleRenderer(true)) }),
+ }
+
@OptIn(ExperimentalCoroutinesApi::class)
val mediaMetadata = mediaController
.filterNotNull()
@@ -249,6 +269,20 @@
initialValue = Triple(null, null, 1f)
)
+ private val _currentVisualizerType = MutableStateFlow(VisualizerType.entries.first())
+ val currentVisualizerType = combine(
+ _currentVisualizerType,
+ isPlaying,
+ ) { currentVisualizerType, isPlaying ->
+ currentVisualizerType.takeIf { isPlaying } ?: VisualizerType.NONE
+ }
+ .flowOn(Dispatchers.IO)
+ .stateIn(
+ viewModelScope,
+ started = SharingStarted.WhileSubscribed(),
+ initialValue = VisualizerType.NONE
+ )
+
fun togglePlayPause() {
mediaController.value?.let {
if (it.isPlaying) {
@@ -301,4 +335,12 @@
it.setPlaybackSpeed(playbackSpeed.next().value)
}
}
+
+ fun nextVisualizerType() {
+ _currentVisualizerType.value = _currentVisualizerType.value.next()
+ }
+
+ fun disableVisualizer() {
+ _currentVisualizerType.value = VisualizerType.NONE
+ }
}
diff --git a/app/src/main/res/drawable/ic_audiofx.xml b/app/src/main/res/drawable/ic_audiofx.xml
new file mode 100644
index 0000000..ad6c2d2
--- /dev/null
+++ b/app/src/main/res/drawable/ic_audiofx.xml
@@ -0,0 +1,17 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+ SPDX-FileCopyrightText: 2024 The LineageOS Project
+ SPDX-License-Identifier: Apache-2.0
+-->
+<vector xmlns:android="http://schemas.android.com/apk/res/android"
+ android:width="24dp"
+ android:height="24dp"
+ android:tint="#000000"
+ android:viewportWidth="24"
+ android:viewportHeight="24">
+
+ <path
+ android:fillColor="@android:color/white"
+ android:pathData="M5.872,18.795C5.351,19.316 4.499,19.321 4.052,18.735C3.129,17.527 2.491,16.12 2.192,14.618C1.806,12.678 2.004,10.667 2.761,8.84C3.518,7.013 4.8,5.451 6.444,4.352C8.089,3.253 10.022,2.667 12,2.667C13.978,2.667 15.911,3.253 17.556,4.352C19.2,5.451 20.482,7.013 21.239,8.84C21.996,10.667 22.194,12.678 21.808,14.618C21.509,16.12 20.871,17.527 19.948,18.735C19.501,19.321 18.649,19.316 18.128,18.795L15.771,16.438C16.517,15.692 17.025,14.742 17.231,13.708C17.437,12.673 17.331,11.601 16.927,10.626L16.913,10.593C17.597,8.56 17.702,8.27 17.892,7.749L17.892,7.749L17.955,7.575C17.984,7.5 18,7.419 18,7.334C18,6.965 17.702,6.667 17.333,6.667C17.247,6.667 17.164,6.683 17.089,6.713L16.919,6.775C16.397,6.965 16.109,7.07 14.075,7.754C13.421,7.478 12.716,7.334 12,7.334C10.945,7.334 9.914,7.646 9.037,8.232C8.16,8.819 7.476,9.651 7.073,10.626C6.669,11.601 6.563,12.673 6.769,13.708C6.975,14.742 7.483,15.692 8.229,16.438L5.872,18.795ZM12,16.334C14.025,16.334 15.667,14.692 15.667,12.667C15.667,10.642 14.025,9 12,9C9.975,9 8.333,10.642 8.333,12.667C8.333,14.692 9.975,16.334 12,16.334Z" />
+
+</vector>
diff --git a/app/src/main/res/layout/fragment_now_playing.xml b/app/src/main/res/layout/fragment_now_playing.xml
index 09488ab..3e51829 100644
--- a/app/src/main/res/layout/fragment_now_playing.xml
+++ b/app/src/main/res/layout/fragment_now_playing.xml
@@ -104,6 +104,17 @@
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent" />
+ <SurfaceView
+ android:id="@+id/visualizerSurfaceView"
+ android:layout_width="0dp"
+ android:layout_height="0dp"
+ android:background="#55000000"
+ android:visibility="visible"
+ app:layout_constraintBottom_toBottomOf="@+id/albumArtImageView"
+ app:layout_constraintEnd_toEndOf="parent"
+ app:layout_constraintStart_toStartOf="parent"
+ app:layout_constraintTop_toTopOf="parent" />
+
</androidx.constraintlayout.widget.ConstraintLayout>
</com.google.android.material.card.MaterialCardView>
@@ -319,12 +330,12 @@
<com.google.android.material.button.MaterialButton
android:id="@+id/equalizerMaterialButton"
style="@style/Theme.Twelve.NowPlayingFragment.BottomBarButton"
- app:icon="@drawable/ic_graphic_eq" />
+ app:icon="@drawable/ic_audiofx" />
<com.google.android.material.button.MaterialButton
- android:id="@+id/castMaterialButton"
+ android:id="@+id/visualizerMaterialButton"
style="@style/Theme.Twelve.NowPlayingFragment.BottomBarButton"
- app:icon="@drawable/ic_cast" />
+ app:icon="@drawable/ic_graphic_eq" />
<com.google.android.material.button.MaterialButton
android:id="@+id/moreMaterialButton"
diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml
index c001c3b..fcd4667 100644
--- a/gradle/libs.versions.toml
+++ b/gradle/libs.versions.toml
@@ -16,6 +16,7 @@
material = "1.12.0"
media3 = "1.5.0-alpha01"
navigation = "2.8.0"
+nier-visualizer = "v0.1.3"
recyclerview = "1.3.2"
room = "2.6.1"
viewpager2 = "1.1.0"
@@ -41,6 +42,7 @@
androidx-viewpager2 = { group = "androidx.viewpager2", name = "viewpager2", version.ref = "viewpager2" }
kotlinx-coroutines-guava = { group = "org.jetbrains.kotlinx", name = "kotlinx-coroutines-guava", version.ref = "kotlinx-coroutines" }
material = { group = "com.google.android.material", name = "material", version.ref = "material" }
+nier-visualizer = { group = "com.github.bogerchan", name = "Nier-Visualizer", version.ref = "nier-visualizer" }
[plugins]
android-application = { id = "com.android.application", version.ref = "agp" }