Twelve: Add audio format information

Change-Id: I0ef54b1019e2c0952dcdfab1c62b08e3fcc724ab
diff --git a/app/src/main/java/org/lineageos/twelve/ext/Player.kt b/app/src/main/java/org/lineageos/twelve/ext/Player.kt
index 18f42db..9ba4891 100644
--- a/app/src/main/java/org/lineageos/twelve/ext/Player.kt
+++ b/app/src/main/java/org/lineageos/twelve/ext/Player.kt
@@ -10,6 +10,7 @@
 import androidx.media3.common.MediaMetadata
 import androidx.media3.common.PlaybackParameters
 import androidx.media3.common.Player
+import androidx.media3.common.Tracks
 import androidx.media3.common.util.UnstableApi
 import kotlinx.coroutines.channels.awaitClose
 import org.lineageos.twelve.models.RepeatMode
@@ -120,6 +121,21 @@
     }
 }
 
+fun Player.tracksFlow() = conflatedCallbackFlow {
+    val listener = object : Player.Listener {
+        override fun onTracksChanged(tracks: Tracks) {
+            trySend(tracks)
+        }
+    }
+
+    addListener(listener)
+    trySend(currentTracks)
+
+    awaitClose {
+        removeListener(listener)
+    }
+}
+
 var Player.typedRepeatMode: RepeatMode
     get() = when (repeatMode) {
         Player.REPEAT_MODE_OFF -> RepeatMode.NONE
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 18990ad..0f06f79 100644
--- a/app/src/main/java/org/lineageos/twelve/fragments/NowPlayingFragment.kt
+++ b/app/src/main/java/org/lineageos/twelve/fragments/NowPlayingFragment.kt
@@ -100,6 +100,12 @@
             findNavController().navigateUp()
         }
 
+        fileTypeMaterialCardView.setOnClickListener {
+            findNavController().navigate(
+                R.id.action_nowPlayingFragment_to_fragment_now_playing_stats_dialog
+            )
+        }
+
         // Audio informations
         audioTitleTextView.isSelected = true
         artistNameTextView.isSelected = true
@@ -178,16 +184,6 @@
 
                 launch {
                     viewModel.mediaItem.collectLatest { mediaItem ->
-                        mediaItem?.localConfiguration?.mimeType
-                            ?.takeIf { mimeType -> mimeType.contains('/') }
-                            ?.substringAfterLast('/')
-                            ?.also { fileType ->
-                                fileTypeTextView.text = fileType
-                                fileTypeMaterialCardView.isVisible = true
-                            } ?: run {
-                            fileTypeMaterialCardView.isVisible = false
-                        }
-
                         addOrRemoveFromPlaylistsMaterialButton.setOnClickListener {
                             mediaItem?.localConfiguration?.uri?.let { uri ->
                                 findNavController().navigate(
@@ -261,6 +257,17 @@
                 }
 
                 launch {
+                    viewModel.displayFileType.collectLatest {
+                        it?.let { displayFileType ->
+                            fileTypeTextView.text = displayFileType
+                            fileTypeMaterialCardView.isVisible = true
+                        } ?: run {
+                            fileTypeMaterialCardView.isVisible = false
+                        }
+                    }
+                }
+
+                launch {
                     viewModel.repeatMode.collectLatest {
                         repeatMaterialButton.setIconResource(
                             when (it) {
diff --git a/app/src/main/java/org/lineageos/twelve/fragments/NowPlayingStatsDialogFragment.kt b/app/src/main/java/org/lineageos/twelve/fragments/NowPlayingStatsDialogFragment.kt
new file mode 100644
index 0000000..0a38685
--- /dev/null
+++ b/app/src/main/java/org/lineageos/twelve/fragments/NowPlayingStatsDialogFragment.kt
@@ -0,0 +1,187 @@
+/*
+ * SPDX-FileCopyrightText: 2024 The LineageOS Project
+ * SPDX-License-Identifier: Apache-2.0
+ */
+
+package org.lineageos.twelve.fragments
+
+import android.icu.text.DecimalFormat
+import android.icu.text.DecimalFormatSymbols
+import android.os.Bundle
+import android.view.View
+import android.widget.LinearLayout
+import androidx.core.view.isVisible
+import androidx.fragment.app.DialogFragment
+import androidx.fragment.app.viewModels
+import androidx.lifecycle.Lifecycle
+import androidx.lifecycle.lifecycleScope
+import androidx.lifecycle.repeatOnLifecycle
+import com.google.android.material.dialog.MaterialAlertDialogBuilder
+import kotlinx.coroutines.flow.collectLatest
+import kotlinx.coroutines.launch
+import org.lineageos.twelve.R
+import org.lineageos.twelve.ext.getViewProperty
+import org.lineageos.twelve.ui.views.ListItem
+import org.lineageos.twelve.viewmodels.NowPlayingStatsViewModel
+import java.util.Locale
+
+/**
+ * A fragment showing playback statistics for nerds and audiophiles thinking that audio files
+ * with a sample rate higher than 48 kHz is better.
+ */
+class NowPlayingStatsDialogFragment : DialogFragment(R.layout.fragment_now_playing_stats_dialog) {
+    // View models
+    private val viewModel by viewModels<NowPlayingStatsViewModel>()
+
+    // Views
+    private val outputChannelCountListItem by getViewProperty<ListItem>(R.id.outputChannelCountListItem)
+    private val outputEncodingListItem by getViewProperty<ListItem>(R.id.outputEncodingListItem)
+    private val outputHeaderListItem by getViewProperty<ListItem>(R.id.outputHeaderListItem)
+    private val outputItemsLinearLayout by getViewProperty<LinearLayout>(R.id.outputItemsLinearLayout)
+    private val outputSampleRateListItem by getViewProperty<ListItem>(R.id.outputSampleRateListItem)
+    private val sourceChannelCountListItem by getViewProperty<ListItem>(R.id.sourceChannelCountListItem)
+    private val sourceEncodingListItem by getViewProperty<ListItem>(R.id.sourceEncodingListItem)
+    private val sourceFileTypeListItem by getViewProperty<ListItem>(R.id.sourceFileTypeListItem)
+    private val sourceSampleRateListItem by getViewProperty<ListItem>(R.id.sourceSampleRateListItem)
+    private val transcodingBitrateListItem by getViewProperty<ListItem>(R.id.transcodingBitrateListItem)
+    private val transcodingEncodingListItem by getViewProperty<ListItem>(R.id.transcodingEncodingListItem)
+    private val transcodingFloatModeEnabledListItem by getViewProperty<ListItem>(R.id.transcodingFloatModeEnabledListItem)
+    private val transcodingOutputModeListItem by getViewProperty<ListItem>(R.id.transcodingOutputModeListItem)
+
+    override fun onCreateDialog(savedInstanceState: Bundle?) = MaterialAlertDialogBuilder(
+        requireContext()
+    ).show()!!
+
+    override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
+        super.onViewCreated(view, savedInstanceState)
+
+        viewLifecycleOwner.lifecycleScope.launch {
+            viewLifecycleOwner.repeatOnLifecycle(Lifecycle.State.STARTED) {
+                launch {
+                    viewModel.mimeType.collectLatest {
+                        it?.let {
+                            sourceFileTypeListItem.supportingText = it
+                        } ?: sourceFileTypeListItem.setSupportingText(
+                            R.string.audio_file_type_unknown
+                        )
+                    }
+                }
+
+                launch {
+                    viewModel.sourceAudioStreamInformation.collectLatest {
+                        it?.sampleRate?.also { sampleRate ->
+                            sourceSampleRateListItem.setSupportingText(
+                                R.string.audio_sample_rate_format,
+                                decimalFormatter.format(sampleRate.toFloat() / 1000)
+                            )
+                        } ?: sourceSampleRateListItem.setSupportingText(
+                            R.string.audio_sample_rate_unknown
+                        )
+
+                        it?.channelCount?.let { channelCount ->
+                            sourceChannelCountListItem.supportingText = channelCount.toString()
+                        } ?: sourceChannelCountListItem.setSupportingText(
+                            R.string.audio_channel_count_unknown
+                        )
+
+                        it?.encoding?.also { encoding ->
+                            sourceEncodingListItem.supportingText = encoding.displayName
+                        } ?: sourceEncodingListItem.setSupportingText(
+                            R.string.audio_encoding_unknown
+                        )
+                    }
+                }
+
+                launch {
+                    viewModel.transcodingFloatModeEnabled.collectLatest {
+                        transcodingFloatModeEnabledListItem.setSupportingText(
+                            when (it) {
+                                true -> R.string.audio_float_mode_enabled_true
+                                false -> R.string.audio_float_mode_enabled_false
+                                null -> R.string.audio_float_mode_enabled_unknown
+                            }
+                        )
+                    }
+                }
+
+                launch {
+                    viewModel.transcodingEncoding.collectLatest {
+                        it?.let {
+                            transcodingEncodingListItem.supportingText = it.displayName
+                        } ?: transcodingEncodingListItem.setSupportingText(
+                            R.string.audio_encoding_unknown
+                        )
+                    }
+                }
+
+                launch {
+                    viewModel.transcodingOutputMode.collectLatest {
+                        it?.let {
+                            transcodingOutputModeListItem.supportingText = it.displayName
+                        } ?: transcodingOutputModeListItem.setSupportingText(
+                            R.string.audio_encoding_unknown
+                        )
+                    }
+                }
+
+                launch {
+                    viewModel.transcodingBitrate.collectLatest {
+                        it?.let {
+                            transcodingBitrateListItem.setSupportingText(
+                                R.string.audio_bitrate_format,
+                                decimalFormatter.format(it.toFloat() / 1000)
+                            )
+                        } ?: transcodingBitrateListItem.setSupportingText(
+                            R.string.audio_bitrate_unknown
+                        )
+                    }
+                }
+
+                launch {
+                    viewModel.hasOutputInformation.collectLatest {
+                        when (it) {
+                            false -> outputHeaderListItem.setSupportingText(
+                                R.string.audio_output_not_available_in_current_configuration
+                            )
+
+                            else -> outputHeaderListItem.supportingText = null
+                        }
+
+                        outputItemsLinearLayout.isVisible = it != false
+                    }
+                }
+
+                launch {
+                    viewModel.outputAudioStreamInformation.collectLatest {
+                        it?.sampleRate?.also { sampleRate ->
+                            outputSampleRateListItem.setSupportingText(
+                                R.string.audio_sample_rate_format,
+                                decimalFormatter.format(sampleRate.toFloat() / 1000)
+                            )
+                        } ?: outputSampleRateListItem.setSupportingText(
+                            R.string.audio_sample_rate_unknown
+                        )
+
+                        it?.channelCount?.let { channelCount ->
+                            outputChannelCountListItem.supportingText = channelCount.toString()
+                        } ?: outputChannelCountListItem.setSupportingText(
+                            R.string.audio_channel_count_unknown
+                        )
+
+                        it?.encoding?.let { encoding ->
+                            outputEncodingListItem.supportingText = encoding.displayName
+                        } ?: outputEncodingListItem.setSupportingText(
+                            R.string.audio_encoding_unknown
+                        )
+                    }
+                }
+            }
+        }
+    }
+
+    companion object {
+        private val decimalFormatSymbols = DecimalFormatSymbols(Locale.ROOT)
+
+        private val decimalFormatter = DecimalFormat("0.#", decimalFormatSymbols)
+    }
+}
diff --git a/app/src/main/java/org/lineageos/twelve/models/AudioOutputMode.kt b/app/src/main/java/org/lineageos/twelve/models/AudioOutputMode.kt
new file mode 100644
index 0000000..47e356e
--- /dev/null
+++ b/app/src/main/java/org/lineageos/twelve/models/AudioOutputMode.kt
@@ -0,0 +1,41 @@
+/*
+ * SPDX-FileCopyrightText: 2024 The LineageOS Project
+ * SPDX-License-Identifier: Apache-2.0
+ */
+
+package org.lineageos.twelve.models
+
+import androidx.media3.common.util.UnstableApi
+import androidx.media3.exoplayer.audio.DefaultAudioSink
+
+/**
+ * Audio output mode.
+ */
+@androidx.annotation.OptIn(UnstableApi::class)
+enum class AudioOutputMode(
+    val displayName: String,
+    val media3OutputMode: @DefaultAudioSink.OutputMode Int,
+) {
+    /**
+     * The audio sink plays PCM audio.
+     */
+    PCM("PCM", DefaultAudioSink.OUTPUT_MODE_PCM),
+
+    /**
+     * The audio sink plays encoded audio in offload.
+     */
+    OFFLOAD("Offload", DefaultAudioSink.OUTPUT_MODE_OFFLOAD),
+
+    /**
+     * The audio sink plays encoded audio in passthrough.
+     */
+    PASSTHROUGH("Passthrough", DefaultAudioSink.OUTPUT_MODE_PASSTHROUGH);
+
+    companion object {
+        fun fromMedia3OutputMode(
+            media3OutputMode: @DefaultAudioSink.OutputMode Int,
+        ) = entries.firstOrNull {
+            it.media3OutputMode == media3OutputMode
+        } ?: throw Exception("Unknown output mode: $media3OutputMode")
+    }
+}
diff --git a/app/src/main/java/org/lineageos/twelve/models/AudioStreamInformation.kt b/app/src/main/java/org/lineageos/twelve/models/AudioStreamInformation.kt
new file mode 100644
index 0000000..7d6a62a
--- /dev/null
+++ b/app/src/main/java/org/lineageos/twelve/models/AudioStreamInformation.kt
@@ -0,0 +1,19 @@
+/*
+ * SPDX-FileCopyrightText: 2024 The LineageOS Project
+ * SPDX-License-Identifier: Apache-2.0
+ */
+
+package org.lineageos.twelve.models
+
+/**
+ * Information regarding an audio stream.
+ *
+ * @param sampleRate The sample rate of the stream.
+ * @param channelCount The channel count of the stream.
+ * @param encoding The [Encoding] of the stream.
+ */
+data class AudioStreamInformation(
+    val sampleRate: Int?,
+    val channelCount: Int?,
+    val encoding: Encoding?,
+)
diff --git a/app/src/main/java/org/lineageos/twelve/models/Encoding.kt b/app/src/main/java/org/lineageos/twelve/models/Encoding.kt
new file mode 100644
index 0000000..57caf98
--- /dev/null
+++ b/app/src/main/java/org/lineageos/twelve/models/Encoding.kt
@@ -0,0 +1,58 @@
+/*
+ * SPDX-FileCopyrightText: 2024 The LineageOS Project
+ * SPDX-License-Identifier: Apache-2.0
+ */
+
+package org.lineageos.twelve.models
+
+import androidx.annotation.OptIn
+import androidx.media3.common.C
+import androidx.media3.common.Format
+import androidx.media3.common.util.UnstableApi
+
+/**
+ * Audio encoding formats.
+ */
+@OptIn(UnstableApi::class)
+enum class Encoding(
+    val displayName: String,
+    private val media3Encoding: @C.Encoding Int,
+) {
+    PCM_8BIT("PCM 8-bit", C.ENCODING_PCM_8BIT),
+    PCM_16BIT("PCM 16-bit", C.ENCODING_PCM_16BIT),
+    PCM_16_BIT_BIG_ENDIAN("PCM 16-bit (big endian)", C.ENCODING_PCM_16BIT_BIG_ENDIAN),
+    PCM_24BIT("PCM 24-bit", C.ENCODING_PCM_24BIT),
+    PCM_24_BIT_BIG_ENDIAN("PCM 24-bit (big endian)", C.ENCODING_PCM_24BIT_BIG_ENDIAN),
+    PCM_32BIT("PCM 32-bit", C.ENCODING_PCM_32BIT),
+    PCM_32_BIT_BIG_ENDIAN("PCM 32-bit (big endian)", C.ENCODING_PCM_32BIT_BIG_ENDIAN),
+    PCM_FLOAT("PCM 32-bit floating point", C.ENCODING_PCM_FLOAT),
+    MP3("MP3", C.ENCODING_MP3),
+    AAC_LC("Advanced Audio Coding Low Complexity (AAC-LC)", C.ENCODING_AAC_LC),
+    AAC_HE_V1("Advanced Audio Coding High-Efficiency v1 (AAC HE v1)", C.ENCODING_AAC_HE_V1),
+    AAC_HE_V2("Advanced Audio Coding High-Efficiency v2 (AAC HE v2)", C.ENCODING_AAC_HE_V2),
+    AAC_XHE("Advance Audio Coding Extended High-Efficiency (AAC xHE)", C.ENCODING_AAC_XHE),
+    AAC_ELD("Advance Audio Coding Enhanced Low Delay (AAC ELD)", C.ENCODING_AAC_ELD),
+    AAC_ER_BSAC(
+        "Advance Audio Coding Error Resilient Bit-Sliced Arithmetic Coding", C.ENCODING_AAC_ER_BSAC
+    ),
+    AC3("Dolby Digital (AC-3)", C.ENCODING_AC3),
+    E_AC3("Dolby Digital Plus (E-AC-3)", C.ENCODING_E_AC3),
+    E_AC3_JOC("Dolby Digital Plus with Dolby Atmos (E-AC-3-JOC)", C.ENCODING_E_AC3_JOC),
+    AC4("Dolby Audio Codec 4 (AC-4)", C.ENCODING_AC4),
+    DTS("DTS", C.ENCODING_DTS),
+    DTS_HD("DTS HD", C.ENCODING_DTS_HD),
+    DOLBY_TRUEHD("Dolby TrueHD", C.ENCODING_DOLBY_TRUEHD),
+    OPUS("Opus", C.ENCODING_OPUS),
+    DTS_UHD_P2("DTS UHD Profile-2", C.ENCODING_DTS_UHD_P2);
+
+    companion object {
+        fun fromMedia3Encoding(media3Encoding: @C.Encoding Int) = when (media3Encoding) {
+            Format.NO_VALUE,
+            C.ENCODING_INVALID -> null
+
+            else -> entries.firstOrNull {
+                media3Encoding == it.media3Encoding
+            } ?: throw Exception("Unknown encoding: $media3Encoding")
+        }
+    }
+}
diff --git a/app/src/main/java/org/lineageos/twelve/services/ProxyAudioProcessor.kt b/app/src/main/java/org/lineageos/twelve/services/ProxyAudioProcessor.kt
new file mode 100644
index 0000000..d97ed0f
--- /dev/null
+++ b/app/src/main/java/org/lineageos/twelve/services/ProxyAudioProcessor.kt
@@ -0,0 +1,66 @@
+/*
+ * SPDX-FileCopyrightText: 2024 The LineageOS Project
+ * SPDX-License-Identifier: Apache-2.0
+ */
+
+package org.lineageos.twelve.services
+
+import androidx.annotation.OptIn
+import androidx.media3.common.audio.AudioProcessor
+import androidx.media3.common.util.UnstableApi
+import kotlinx.coroutines.flow.MutableStateFlow
+import kotlinx.coroutines.flow.asStateFlow
+import java.nio.ByteBuffer
+
+/**
+ * [AudioProcessor] that does nothing other than exposing the [AudioProcessor.AudioFormat].
+ * Here the input is the output.
+ */
+@OptIn(UnstableApi::class)
+class ProxyAudioProcessor : AudioProcessor {
+    private var pendingAudioFormat = AudioProcessor.AudioFormat.NOT_SET
+    private var audioFormat = AudioProcessor.AudioFormat.NOT_SET
+        set(value) {
+            field = value
+            _audioFormatFlow.value = value.takeIf { it != AudioProcessor.AudioFormat.NOT_SET }
+        }
+    private var buffer = AudioProcessor.EMPTY_BUFFER
+    private var isEnded = true
+
+    override fun configure(inputAudioFormat: AudioProcessor.AudioFormat) = inputAudioFormat.also {
+        this.pendingAudioFormat = it
+    }
+
+    override fun isActive() = pendingAudioFormat !== AudioProcessor.AudioFormat.NOT_SET
+
+    override fun queueInput(inputBuffer: ByteBuffer) {
+        this.buffer = inputBuffer
+    }
+
+    override fun queueEndOfStream() {
+        isEnded = true
+    }
+
+    override fun getOutput() = buffer.also {
+        buffer = AudioProcessor.EMPTY_BUFFER
+    }
+
+    override fun isEnded() = isEnded && buffer === AudioProcessor.EMPTY_BUFFER
+
+    override fun flush() {
+        buffer = AudioProcessor.EMPTY_BUFFER
+        isEnded = false
+        audioFormat = pendingAudioFormat
+    }
+
+    override fun reset() {
+        flush()
+        pendingAudioFormat = AudioProcessor.AudioFormat.NOT_SET
+        audioFormat = AudioProcessor.AudioFormat.NOT_SET
+    }
+
+    companion object {
+        private val _audioFormatFlow = MutableStateFlow<AudioProcessor.AudioFormat?>(null)
+        val audioFormatFlow = _audioFormatFlow.asStateFlow()
+    }
+}
diff --git a/app/src/main/java/org/lineageos/twelve/services/ProxyDefaultAudioTrackBufferSizeProvider.kt b/app/src/main/java/org/lineageos/twelve/services/ProxyDefaultAudioTrackBufferSizeProvider.kt
new file mode 100644
index 0000000..e83d192
--- /dev/null
+++ b/app/src/main/java/org/lineageos/twelve/services/ProxyDefaultAudioTrackBufferSizeProvider.kt
@@ -0,0 +1,49 @@
+/*
+ * SPDX-FileCopyrightText: 2024 The LineageOS Project
+ * SPDX-License-Identifier: Apache-2.0
+ */
+
+package org.lineageos.twelve.services
+
+import androidx.media3.common.C
+import androidx.media3.common.Format
+import androidx.media3.common.util.UnstableApi
+import androidx.media3.exoplayer.audio.DefaultAudioSink
+import kotlinx.coroutines.flow.MutableStateFlow
+import kotlinx.coroutines.flow.asStateFlow
+
+@androidx.annotation.OptIn(UnstableApi::class)
+object ProxyDefaultAudioTrackBufferSizeProvider : DefaultAudioSink.AudioTrackBufferSizeProvider {
+    private val delegate = DefaultAudioSink.AudioTrackBufferSizeProvider.DEFAULT
+
+    private val _encodingFlow = MutableStateFlow<@C.Encoding Int?>(null)
+    val encodingFlow = _encodingFlow.asStateFlow()
+
+    private val _outputModeFlow = MutableStateFlow<@DefaultAudioSink.OutputMode Int?>(null)
+    val outputModeFlow = _outputModeFlow.asStateFlow()
+
+    private val _bitrateFlow = MutableStateFlow<Int?>(null)
+    val bitrateFlow = _bitrateFlow.asStateFlow()
+
+    override fun getBufferSizeInBytes(
+        minBufferSizeInBytes: Int,
+        encoding: @C.Encoding Int,
+        outputMode: @DefaultAudioSink.OutputMode Int,
+        pcmFrameSize: Int,
+        sampleRate: Int,
+        bitrate: Int,
+        maxAudioTrackPlaybackSpeed: Double
+    ) = delegate.getBufferSizeInBytes(
+        minBufferSizeInBytes,
+        encoding,
+        outputMode,
+        pcmFrameSize,
+        sampleRate,
+        bitrate,
+        maxAudioTrackPlaybackSpeed
+    ).also {
+        _encodingFlow.value = encoding
+        _outputModeFlow.value = outputMode
+        _bitrateFlow.value = bitrate.takeIf { it != Format.NO_VALUE }
+    }
+}
diff --git a/app/src/main/java/org/lineageos/twelve/services/TurntableRenderersFactory.kt b/app/src/main/java/org/lineageos/twelve/services/TurntableRenderersFactory.kt
index c809270..bd3a9c8 100644
--- a/app/src/main/java/org/lineageos/twelve/services/TurntableRenderersFactory.kt
+++ b/app/src/main/java/org/lineageos/twelve/services/TurntableRenderersFactory.kt
@@ -26,5 +26,7 @@
     ) = DefaultAudioSink.Builder(context)
         .setEnableFloatOutput(enableFloatOutput)
         .setEnableAudioTrackPlaybackParams(enableAudioTrackPlaybackParams)
+        .setAudioProcessors(arrayOf(ProxyAudioProcessor()))
+        .setAudioTrackBufferSizeProvider(ProxyDefaultAudioTrackBufferSizeProvider)
         .build()
 }
diff --git a/app/src/main/java/org/lineageos/twelve/viewmodels/NowPlayingStatsViewModel.kt b/app/src/main/java/org/lineageos/twelve/viewmodels/NowPlayingStatsViewModel.kt
new file mode 100644
index 0000000..8c28d66
--- /dev/null
+++ b/app/src/main/java/org/lineageos/twelve/viewmodels/NowPlayingStatsViewModel.kt
@@ -0,0 +1,146 @@
+/*
+ * SPDX-FileCopyrightText: 2024 The LineageOS Project
+ * SPDX-License-Identifier: Apache-2.0
+ */
+
+package org.lineageos.twelve.viewmodels
+
+import android.app.Application
+import androidx.lifecycle.viewModelScope
+import androidx.media3.common.C
+import androidx.media3.common.MimeTypes
+import androidx.media3.common.util.UnstableApi
+import androidx.media3.exoplayer.audio.DefaultAudioSink
+import kotlinx.coroutines.Dispatchers
+import kotlinx.coroutines.ExperimentalCoroutinesApi
+import kotlinx.coroutines.flow.SharingStarted
+import kotlinx.coroutines.flow.combine
+import kotlinx.coroutines.flow.flowOn
+import kotlinx.coroutines.flow.mapLatest
+import kotlinx.coroutines.flow.stateIn
+import org.lineageos.twelve.models.AudioOutputMode
+import org.lineageos.twelve.models.AudioStreamInformation
+import org.lineageos.twelve.models.Encoding
+import org.lineageos.twelve.services.ProxyAudioProcessor
+import org.lineageos.twelve.services.ProxyDefaultAudioTrackBufferSizeProvider
+
+class NowPlayingStatsViewModel(application: Application) : TwelveViewModel(application) {
+    /**
+     * [AudioStreamInformation] parsed from the currently selected audio track returned by the
+     * player.
+     */
+    @androidx.annotation.OptIn(UnstableApi::class)
+    @OptIn(ExperimentalCoroutinesApi::class)
+    val sourceAudioStreamInformation = currentTrackFormat
+        .mapLatest { currentTrackFormat ->
+            currentTrackFormat?.let {
+                AudioStreamInformation(
+                    it.sampleRate,
+                    it.channelCount,
+                    it.sampleMimeType?.let { sampleMimeType ->
+                        Encoding.fromMedia3Encoding(
+                            MimeTypes.getEncoding(
+                                MimeTypes.normalizeMimeType(sampleMimeType),
+                                it.codecs
+                            )
+                        )
+                    } ?: Encoding.fromMedia3Encoding(it.pcmEncoding),
+                )
+            }
+        }
+        .flowOn(Dispatchers.IO)
+        .stateIn(
+            viewModelScope,
+            started = SharingStarted.WhileSubscribed(),
+            initialValue = null
+        )
+
+    /**
+     * Whether the output is in non-passthrough PCM float mode.
+     * This means the audio sink is ignoring all the processors.
+     */
+    @androidx.annotation.OptIn(UnstableApi::class)
+    val transcodingFloatModeEnabled = combine(
+        ProxyDefaultAudioTrackBufferSizeProvider.encodingFlow,
+        ProxyDefaultAudioTrackBufferSizeProvider.outputModeFlow,
+    ) { encoding, outputMode ->
+        outputMode == DefaultAudioSink.OUTPUT_MODE_PCM && encoding == C.ENCODING_PCM_FLOAT
+    }
+        .flowOn(Dispatchers.IO)
+        .stateIn(
+            viewModelScope,
+            started = SharingStarted.WhileSubscribed(),
+            initialValue = null
+        )
+
+    @OptIn(ExperimentalCoroutinesApi::class)
+    val transcodingEncoding = ProxyDefaultAudioTrackBufferSizeProvider.encodingFlow
+        .mapLatest {
+            it?.let { Encoding.fromMedia3Encoding(it) }
+        }
+        .flowOn(Dispatchers.IO)
+        .stateIn(
+            viewModelScope,
+            started = SharingStarted.WhileSubscribed(),
+            initialValue = null
+        )
+
+    @OptIn(ExperimentalCoroutinesApi::class)
+    val transcodingOutputMode = ProxyDefaultAudioTrackBufferSizeProvider.outputModeFlow
+        .mapLatest {
+            it?.let { AudioOutputMode.fromMedia3OutputMode(it) }
+        }
+        .flowOn(Dispatchers.IO)
+        .stateIn(
+            viewModelScope,
+            started = SharingStarted.WhileSubscribed(),
+            initialValue = null
+        )
+
+    val transcodingBitrate = ProxyDefaultAudioTrackBufferSizeProvider.bitrateFlow
+        .stateIn(
+            viewModelScope,
+            started = SharingStarted.WhileSubscribed(),
+            initialValue = null
+        )
+
+    /**
+     * Whether the output has valid information.
+     */
+    @androidx.annotation.OptIn(UnstableApi::class)
+    val hasOutputInformation = combine(
+        ProxyDefaultAudioTrackBufferSizeProvider.outputModeFlow,
+        transcodingFloatModeEnabled,
+    ) { outputMode, transcodingFloatModeEnabled ->
+        outputMode == DefaultAudioSink.OUTPUT_MODE_PCM && transcodingFloatModeEnabled != true
+    }
+        .flowOn(Dispatchers.IO)
+        .stateIn(
+            viewModelScope,
+            started = SharingStarted.WhileSubscribed(),
+            initialValue = null
+        )
+
+    /**
+     * The output audio stream information.
+     */
+    @androidx.annotation.OptIn(UnstableApi::class)
+    val outputAudioStreamInformation = combine(
+        ProxyAudioProcessor.audioFormatFlow,
+        hasOutputInformation,
+    ) { audioFormat, hasOutputInformation ->
+        audioFormat?.takeIf { hasOutputInformation != false }?.let {
+            AudioStreamInformation(
+                it.sampleRate,
+                it.channelCount,
+                Encoding.fromMedia3Encoding(it.encoding),
+            )
+        }
+    }
+        .flowOn(Dispatchers.IO)
+        .stateIn(
+            viewModelScope,
+            started = SharingStarted.WhileSubscribed(),
+            initialValue = null
+        )
+}
diff --git a/app/src/main/java/org/lineageos/twelve/viewmodels/TwelveViewModel.kt b/app/src/main/java/org/lineageos/twelve/viewmodels/TwelveViewModel.kt
index 62fde82..8831f7a 100644
--- a/app/src/main/java/org/lineageos/twelve/viewmodels/TwelveViewModel.kt
+++ b/app/src/main/java/org/lineageos/twelve/viewmodels/TwelveViewModel.kt
@@ -11,6 +11,8 @@
 import androidx.lifecycle.viewModelScope
 import androidx.media3.common.C
 import androidx.media3.common.MediaMetadata
+import androidx.media3.common.MimeTypes
+import androidx.media3.common.util.UnstableApi
 import androidx.media3.session.MediaController
 import androidx.media3.session.SessionToken
 import kotlinx.coroutines.Dispatchers
@@ -19,10 +21,12 @@
 import kotlinx.coroutines.delay
 import kotlinx.coroutines.flow.SharingStarted
 import kotlinx.coroutines.flow.channelFlow
+import kotlinx.coroutines.flow.combine
 import kotlinx.coroutines.flow.filterNotNull
 import kotlinx.coroutines.flow.flatMapLatest
 import kotlinx.coroutines.flow.flow
 import kotlinx.coroutines.flow.flowOn
+import kotlinx.coroutines.flow.mapLatest
 import kotlinx.coroutines.flow.stateIn
 import kotlinx.coroutines.guava.await
 import org.lineageos.twelve.TwelveApplication
@@ -34,6 +38,7 @@
 import org.lineageos.twelve.ext.playbackParametersFlow
 import org.lineageos.twelve.ext.repeatModeFlow
 import org.lineageos.twelve.ext.shuffleModeFlow
+import org.lineageos.twelve.ext.tracksFlow
 import org.lineageos.twelve.ext.typedRepeatMode
 import org.lineageos.twelve.models.Audio
 import org.lineageos.twelve.models.RepeatMode
@@ -136,6 +141,66 @@
             initialValue = null
         )
 
+    @androidx.annotation.OptIn(UnstableApi::class)
+    @OptIn(ExperimentalCoroutinesApi::class)
+    val currentTrackFormat = mediaController.filterNotNull()
+        .flatMapLatest { it.tracksFlow() }
+        .flowOn(Dispatchers.Main)
+        .mapLatest { tracks ->
+            val groups = tracks.groups.filter { group ->
+                group.type == C.TRACK_TYPE_AUDIO && group.isSelected
+            }
+
+            require(groups.size <= 1) { "More than one audio track selected" }
+
+            groups.firstOrNull()?.let { group ->
+                (0..group.length).firstNotNullOfOrNull { i ->
+                    when (group.isTrackSelected(i)) {
+                        true -> group.getTrackFormat(i)
+                        false -> null
+                    }
+                }
+            }
+        }
+        .flowOn(Dispatchers.IO)
+        .stateIn(
+            viewModelScope,
+            started = SharingStarted.WhileSubscribed(),
+            initialValue = null
+        )
+
+    val mimeType =
+        combine(currentTrackFormat, mediaItem) { format, mediaItem ->
+            format?.sampleMimeType
+                ?: format?.containerMimeType
+                ?: mediaItem?.localConfiguration?.mimeType
+        }
+            .flowOn(Dispatchers.IO)
+            .stateIn(
+                viewModelScope,
+                started = SharingStarted.WhileSubscribed(),
+                initialValue = null
+            )
+
+    @androidx.annotation.OptIn(UnstableApi::class)
+    @OptIn(ExperimentalCoroutinesApi::class)
+    val displayFileType = mimeType
+        .mapLatest { mimeType ->
+            mimeType?.let {
+                MimeTypes.normalizeMimeType(it)
+            }?.let {
+                it.takeIf { it.contains('/') }
+                    ?.substringAfterLast('/')
+                    ?.uppercase()
+            }
+        }
+        .flowOn(Dispatchers.IO)
+        .stateIn(
+            viewModelScope,
+            started = SharingStarted.WhileSubscribed(),
+            initialValue = null
+        )
+
     @OptIn(ExperimentalCoroutinesApi::class)
     fun availableCommands() = mediaController.filterNotNull()
         .flatMapLatest { it.availableCommandsFlow() }
diff --git a/app/src/main/res/drawable/ic_audio_file.xml b/app/src/main/res/drawable/ic_audio_file.xml
new file mode 100644
index 0000000..92f71a3
--- /dev/null
+++ b/app/src/main/res/drawable/ic_audio_file.xml
@@ -0,0 +1,17 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!--
+     SPDX-FileCopyrightText: Material Design Authors / Google LLC
+     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="960"
+    android:viewportHeight="960">
+
+    <path
+        android:fillColor="@android:color/white"
+        android:pathData="M430,760Q468,760 494,734Q520,708 520,670L520,520L640,520L640,440L480,440L480,595Q469,587 456.5,583.5Q444,580 430,580Q392,580 366,606Q340,632 340,670Q340,708 366,734Q392,760 430,760ZM240,880Q207,880 183.5,856.5Q160,833 160,800L160,160Q160,127 183.5,103.5Q207,80 240,80L560,80L800,320L800,800Q800,833 776.5,856.5Q753,880 720,880L240,880ZM520,360L520,160L240,160Q240,160 240,160Q240,160 240,160L240,800Q240,800 240,800Q240,800 240,800L720,800Q720,800 720,800Q720,800 720,800L720,360L520,360ZM240,160L240,160L240,360L240,360L240,160L240,360L240,360L240,800Q240,800 240,800Q240,800 240,800L240,800Q240,800 240,800Q240,800 240,800L240,160Q240,160 240,160Q240,160 240,160Z" />
+
+</vector>
diff --git a/app/src/main/res/drawable/ic_conversion_path.xml b/app/src/main/res/drawable/ic_conversion_path.xml
new file mode 100644
index 0000000..560a308
--- /dev/null
+++ b/app/src/main/res/drawable/ic_conversion_path.xml
@@ -0,0 +1,17 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!--
+     SPDX-FileCopyrightText: Material Design Authors / Google LLC
+     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="960"
+    android:viewportHeight="960">
+
+    <path
+        android:fillColor="@android:color/white"
+        android:pathData="M760,840Q721,840 690,817.5Q659,795 647,760L440,760Q374,760 327,713Q280,666 280,600Q280,534 327,487Q374,440 440,440L520,440Q553,440 576.5,416.5Q600,393 600,360Q600,327 576.5,303.5Q553,280 520,280L313,280Q300,315 269.5,337.5Q239,360 200,360Q150,360 115,325Q80,290 80,240Q80,190 115,155Q150,120 200,120Q239,120 269.5,142.5Q300,165 313,200L520,200Q586,200 633,247Q680,294 680,360Q680,426 633,473Q586,520 520,520L440,520Q407,520 383.5,543.5Q360,567 360,600Q360,633 383.5,656.5Q407,680 440,680L647,680Q660,645 690.5,622.5Q721,600 760,600Q810,600 845,635Q880,670 880,720Q880,770 845,805Q810,840 760,840ZM200,280Q217,280 228.5,268.5Q240,257 240,240Q240,223 228.5,211.5Q217,200 200,200Q183,200 171.5,211.5Q160,223 160,240Q160,257 171.5,268.5Q183,280 200,280Z" />
+
+</vector>
diff --git a/app/src/main/res/drawable/ic_media_output.xml b/app/src/main/res/drawable/ic_media_output.xml
new file mode 100644
index 0000000..eb669c6
--- /dev/null
+++ b/app/src/main/res/drawable/ic_media_output.xml
@@ -0,0 +1,17 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!--
+     SPDX-FileCopyrightText: Material Design Authors / Google LLC
+     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="960"
+    android:viewportHeight="960">
+
+    <path
+        android:fillColor="@android:color/white"
+        android:pathData="M340,440L340,440Q340,440 340,440Q340,440 340,440L340,440Q340,440 340,440Q340,440 340,440L340,440Q340,440 340,440Q340,440 340,440L340,440Q340,440 340,440Q340,440 340,440ZM160,800Q127,800 103.5,776.5Q80,753 80,720L80,160Q80,127 103.5,103.5Q127,80 160,80L520,80Q553,80 576.5,103.5Q600,127 600,160L600,322Q579,324 559,329.5Q539,335 520,343L520,160Q520,160 520,160Q520,160 520,160L160,160Q160,160 160,160Q160,160 160,160L160,720Q160,720 160,720Q160,720 160,720L320,720L320,800L160,800ZM320,679L320,640Q320,629 320.5,618.5Q321,608 323,597Q304,592 292,576Q280,560 280,540Q280,515 297.5,497.5Q315,480 340,480Q345,480 350,481Q355,482 360,484Q370,466 382.5,449.5Q395,433 409,418Q394,409 376.5,404.5Q359,400 340,400Q282,400 241,441Q200,482 200,540Q200,594 234.5,632.5Q269,671 320,679ZM340,340Q365,340 382.5,322.5Q400,305 400,280Q400,255 382.5,237.5Q365,220 340,220Q315,220 297.5,237.5Q280,255 280,280Q280,305 297.5,322.5Q315,340 340,340ZM560,880L480,880Q447,880 423.5,856.5Q400,833 400,800L400,640Q400,540 470,470Q540,400 640,400Q740,400 810,470Q880,540 880,640L880,800Q880,833 856.5,856.5Q833,880 800,880L720,880L720,680L820,680L820,640Q820,565 767.5,512.5Q715,460 640,460Q565,460 512.5,512.5Q460,565 460,640L460,680L560,680L560,880Z" />
+
+</vector>
diff --git a/app/src/main/res/layout/fragment_now_playing.xml b/app/src/main/res/layout/fragment_now_playing.xml
index bb09278..0f81781 100644
--- a/app/src/main/res/layout/fragment_now_playing.xml
+++ b/app/src/main/res/layout/fragment_now_playing.xml
@@ -45,10 +45,9 @@
                     android:layout_height="24dp"
                     android:gravity="center"
                     android:textAlignment="gravity"
-                    android:textAllCaps="true"
                     android:textAppearance="?attr/textAppearanceLabelMedium"
                     android:textColor="?attr/colorOnPrimaryContainer"
-                    tools:text="flac" />
+                    tools:text="FLAC" />
 
             </com.google.android.material.card.MaterialCardView>
 
diff --git a/app/src/main/res/layout/fragment_now_playing_stats_dialog.xml b/app/src/main/res/layout/fragment_now_playing_stats_dialog.xml
new file mode 100644
index 0000000..29c3f48
--- /dev/null
+++ b/app/src/main/res/layout/fragment_now_playing_stats_dialog.xml
@@ -0,0 +1,124 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+     SPDX-FileCopyrightText: 2024 The LineageOS Project
+     SPDX-License-Identifier: Apache-2.0
+-->
+<androidx.core.widget.NestedScrollView xmlns:android="http://schemas.android.com/apk/res/android"
+    xmlns:app="http://schemas.android.com/apk/res-auto"
+    android:layout_width="match_parent"
+    android:layout_height="match_parent">
+
+    <LinearLayout
+        android:layout_width="match_parent"
+        android:layout_height="match_parent"
+        android:background="?attr/colorSurfaceContainer"
+        android:paddingVertical="8dp"
+        android:orientation="vertical">
+
+        <org.lineageos.twelve.ui.views.ListItem
+            android:layout_width="match_parent"
+            android:layout_height="wrap_content"
+            app:headlineText="@string/audio_source"
+            app:leadingIconImage="@drawable/ic_audio_file" />
+
+        <org.lineageos.twelve.ui.views.ListItem
+            android:id="@+id/sourceFileTypeListItem"
+            android:layout_width="match_parent"
+            android:layout_height="wrap_content"
+            app:headlineText="@string/audio_file_type"
+            app:supportingText="@string/audio_file_type_unknown" />
+
+        <org.lineageos.twelve.ui.views.ListItem
+            android:id="@+id/sourceSampleRateListItem"
+            android:layout_width="match_parent"
+            android:layout_height="wrap_content"
+            app:headlineText="@string/audio_sample_rate"
+            app:supportingText="@string/audio_sample_rate_unknown" />
+
+        <org.lineageos.twelve.ui.views.ListItem
+            android:id="@+id/sourceChannelCountListItem"
+            android:layout_width="match_parent"
+            android:layout_height="wrap_content"
+            app:headlineText="@string/audio_channel_count"
+            app:supportingText="@string/audio_channel_count_unknown" />
+
+        <org.lineageos.twelve.ui.views.ListItem
+            android:id="@+id/sourceEncodingListItem"
+            android:layout_width="match_parent"
+            android:layout_height="wrap_content"
+            app:headlineText="@string/audio_encoding"
+            app:supportingText="@string/audio_encoding_unknown" />
+
+        <org.lineageos.twelve.ui.views.ListItem
+            android:layout_width="match_parent"
+            android:layout_height="wrap_content"
+            app:headlineText="@string/audio_transcoding"
+            app:leadingIconImage="@drawable/ic_conversion_path" />
+
+        <org.lineageos.twelve.ui.views.ListItem
+            android:id="@+id/transcodingFloatModeEnabledListItem"
+            android:layout_width="match_parent"
+            android:layout_height="wrap_content"
+            app:headlineText="@string/audio_float_mode_enabled"
+            app:supportingText="@string/audio_float_mode_enabled_unknown" />
+
+        <org.lineageos.twelve.ui.views.ListItem
+            android:id="@+id/transcodingEncodingListItem"
+            android:layout_width="match_parent"
+            android:layout_height="wrap_content"
+            app:headlineText="@string/audio_encoding"
+            app:supportingText="@string/audio_encoding_unknown" />
+
+        <org.lineageos.twelve.ui.views.ListItem
+            android:id="@+id/transcodingOutputModeListItem"
+            android:layout_width="match_parent"
+            android:layout_height="wrap_content"
+            app:headlineText="@string/audio_output_mode"
+            app:supportingText="@string/audio_output_mode_unknown" />
+
+        <org.lineageos.twelve.ui.views.ListItem
+            android:id="@+id/transcodingBitrateListItem"
+            android:layout_width="match_parent"
+            android:layout_height="wrap_content"
+            app:headlineText="@string/audio_bitrate"
+            app:supportingText="@string/audio_bitrate_unknown" />
+
+        <org.lineageos.twelve.ui.views.ListItem
+            android:id="@+id/outputHeaderListItem"
+            android:layout_width="match_parent"
+            android:layout_height="wrap_content"
+            app:headlineText="@string/audio_output"
+            app:leadingIconImage="@drawable/ic_media_output" />
+
+        <LinearLayout
+            android:id="@+id/outputItemsLinearLayout"
+            android:layout_width="match_parent"
+            android:layout_height="wrap_content"
+            android:orientation="vertical">
+
+            <org.lineageos.twelve.ui.views.ListItem
+                android:id="@+id/outputSampleRateListItem"
+                android:layout_width="match_parent"
+                android:layout_height="wrap_content"
+                app:headlineText="@string/audio_sample_rate"
+                app:supportingText="@string/audio_sample_rate_unknown" />
+
+            <org.lineageos.twelve.ui.views.ListItem
+                android:id="@+id/outputChannelCountListItem"
+                android:layout_width="match_parent"
+                android:layout_height="wrap_content"
+                app:headlineText="@string/audio_channel_count"
+                app:supportingText="@string/audio_channel_count_unknown" />
+
+            <org.lineageos.twelve.ui.views.ListItem
+                android:id="@+id/outputEncodingListItem"
+                android:layout_width="match_parent"
+                android:layout_height="wrap_content"
+                app:headlineText="@string/audio_encoding"
+                app:supportingText="@string/audio_encoding_unknown" />
+
+        </LinearLayout>
+
+    </LinearLayout>
+
+</androidx.core.widget.NestedScrollView>
diff --git a/app/src/main/res/navigation/fragment_main.xml b/app/src/main/res/navigation/fragment_main.xml
index a40b0a4..e279c3d 100644
--- a/app/src/main/res/navigation/fragment_main.xml
+++ b/app/src/main/res/navigation/fragment_main.xml
@@ -14,6 +14,7 @@
     <include app:graph="@navigation/fragment_artist" />
     <include app:graph="@navigation/fragment_audio_bottom_sheet_dialog" />
     <include app:graph="@navigation/fragment_now_playing" />
+    <include app:graph="@navigation/fragment_now_playing_stats_dialog" />
     <include app:graph="@navigation/fragment_playlist" />
 
     <fragment
diff --git a/app/src/main/res/navigation/fragment_now_playing.xml b/app/src/main/res/navigation/fragment_now_playing.xml
index f71cf97..b7736ef 100644
--- a/app/src/main/res/navigation/fragment_now_playing.xml
+++ b/app/src/main/res/navigation/fragment_now_playing.xml
@@ -22,6 +22,14 @@
             app:popEnterAnim="@anim/nav_default_pop_enter_anim"
             app:popExitAnim="@anim/nav_default_pop_exit_anim" />
 
+        <action
+            android:id="@+id/action_nowPlayingFragment_to_fragment_now_playing_stats_dialog"
+            app:destination="@+id/fragment_now_playing_stats_dialog"
+            app:enterAnim="@anim/nav_default_enter_anim"
+            app:exitAnim="@anim/nav_default_exit_anim"
+            app:popEnterAnim="@anim/nav_default_pop_enter_anim"
+            app:popExitAnim="@anim/nav_default_pop_exit_anim" />
+
     </fragment>
 
 </navigation>
diff --git a/app/src/main/res/navigation/fragment_now_playing_stats_dialog.xml b/app/src/main/res/navigation/fragment_now_playing_stats_dialog.xml
new file mode 100644
index 0000000..b3bbe50
--- /dev/null
+++ b/app/src/main/res/navigation/fragment_now_playing_stats_dialog.xml
@@ -0,0 +1,17 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+     SPDX-FileCopyrightText: 2024 The LineageOS Project
+     SPDX-License-Identifier: Apache-2.0
+-->
+<navigation xmlns:android="http://schemas.android.com/apk/res/android"
+    xmlns:app="http://schemas.android.com/apk/res-auto"
+    xmlns:tools="http://schemas.android.com/tools"
+    android:id="@+id/fragment_now_playing_stats_dialog"
+    app:startDestination="@id/nowPlayingStatsDialogFragment">
+
+    <dialog
+        android:id="@+id/nowPlayingStatsDialogFragment"
+        android:name="org.lineageos.twelve.fragments.NowPlayingStatsDialogFragment"
+        tools:layout="@layout/fragment_now_playing_stats_dialog" />
+
+</navigation>
diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml
index 8b6ad9e..996d66a 100644
--- a/app/src/main/res/values/strings.xml
+++ b/app/src/main/res/values/strings.xml
@@ -76,4 +76,31 @@
 
     <!-- Now playing fragment -->
     <string name="playback_speed_format">%1$s×</string>
+
+    <!-- Now playing stats dialog fragment -->
+    <string name="audio_source">Source</string>
+    <string name="audio_transcoding">Transcoding</string>
+    <string name="audio_output">Output</string>
+    <string name="audio_output_not_available_in_current_configuration">Output information are not available in current configuration</string>
+    <string name="audio_file_type">File type</string>
+    <string name="audio_file_type_unknown">Unknown</string>
+    <string name="audio_sample_rate">Sample rate</string>
+    <string name="audio_sample_rate_format">%1$s kHz</string>
+    <string name="audio_sample_rate_unknown">Unknown</string>
+    <string name="audio_channel_count">Channel count</string>
+    <string name="audio_channel_count_unknown">Unknown</string>
+    <string name="audio_encoding">Encoding</string>
+    <string name="audio_encoding_unknown">Unknown</string>
+    <string name="audio_float_mode_enabled">PCM float mode enabled</string>
+    <string name="audio_float_mode_enabled_unknown">Unknown</string>
+    <string name="audio_float_mode_enabled_true">True</string>
+    <string name="audio_float_mode_enabled_false">False</string>
+    <string name="audio_output_mode">Output mode</string>
+    <string name="audio_output_mode_unknown">Unknown</string>
+    <string name="audio_output_mode_pcm">PCM</string>
+    <string name="audio_output_mode_offload">Offload</string>
+    <string name="audio_output_mode_passthrough">Passthrough</string>
+    <string name="audio_bitrate">Bitrate</string>
+    <string name="audio_bitrate_unknown">Unknown</string>
+    <string name="audio_bitrate_format">%1$s kbps</string>
 </resources>