在Jetpack Compose中使用ExoPlayer实现直播流和音频均衡器

在Jetpack Compose中使用ExoPlayer实现直播流和音频均衡器

背景

ExoPlayer与Media3的能力结合,为Android应用程序播放多媒体内容提供了强大的解决方案。在本教程中,我们将介绍如何设置带有Media3的ExoPlayer来支持使用M3U8 URL进行直播流。此外,我们还将探讨如何集成音频均衡器,为用户提供个性化的音频体验。

使用ExoPlayer进行直播流涉及到利用ExoPlayer库的强大能力,在互联网上实时播放多媒体内容。

在这个探索中,我们将深入了解ExoPlayer如何实现无缝播放直播流URL的复杂性,为用户提供沉浸式体验。

ExoPlayer如何处理直播流?

使用ExoPlayer进行直播流主要围绕着有效地处理音频和视频内容的实时传输。该过程包括几个关键阶段:

  1. 内容源(Content Source):使用摄像机和麦克风捕获直播内容,然后将此直播流可用于流媒体。
  2. 编码(Encoding):捕获的内容被编码为适用于流媒体的数字格式。这涉及将原始音频和视频数据压缩并转换为与流媒体协议兼容的格式。
  3. 流媒体服务器(Streaming Server):编码数据发送到充当中央枢纽的流媒体服务器。该服务器通过向用户设备发送数据包来管理多个观众的直播内容分发。
  4. ExoPlayer集成(ExoPlayer Integration):将具有强大能力的ExoPlayer集成到应用程序中,以处理直播流的播放。应用程序获取直播流URL,并配置ExoPlayer以处理流媒体协议(例如HLS或DASH)。
  5. 观众设备(Viewer’s Device):用户通过各种设备访问直播流,包括智能手机、平板电脑、计算机或智能电视。观众设备上的ExoPlayer实例解码接收到的数据,使他们可以实时观看或收听直播内容。

设置ExoPlayer以进行直播流

要将ExoPlayer集成到我们的应用程序中以进行直播流,我们需要遵循以下关键步骤:

1.添加依赖项
在项目的build.gradle文件中包含必要的依赖项:

// Exoplayer dependencies
implementation("androidx.media3:media3-exoplayer:1.2.0")
implementation("androidx.media3:media3-ui:1.2.0")
implementation("androidx.media3:media3-exoplayer-hls:1.2.0")

这些依赖项确保应用程序可以利用ExoPlayer的功能进行直播流。

  1. 创建ExoPlayerManager
    我们将创建一个管理器类来处理ExoPlayer实例。这可以确保应用程序的整个生命周期中只有一个播放器实例。
object ExoPlayerManager {
    private var exoPlayer: ExoPlayer? = null

    fun getExoPlayer(context: Context): ExoPlayer {
        if (exoPlayer == null) {
            exoPlayer = ExoPlayer.Builder(context).build()
        }
        return exoPlayer!!
    }

    fun releaseExoPlayer() {
        exoPlayer?.release()
        exoPlayer = null
    }
}
  1. 初始化ExoPlayer
    在您的Composable函数中,使用示例HLS流URL初始化ExoPlayer:
@Composable
fun LiveStreamingScreen() {

    // Obtain the current context and lifecycle owner using LocalContext and LocalLifecycleOwner
    val context = LocalContext.current
    val lifecycleOwner = LocalLifecycleOwner.current

    // Remember the ExoPlayer instance to persist across recompositions
    val exoPlayer = remember { ExoPlayerManager.getExoPlayer(context) }

    // Launch an effect to initialize ExoPlayer and set up the media source
    LaunchedEffect(key1 = Unit) {

        // Create a data source factory for handling media requests
        val dataSourceFactory = DefaultHttpDataSource.Factory()

        // Define the URI for the sample HLS stream
        val uri = Uri.Builder()
            .encodedPath("http://sample.vodobox.net/skate_phantom_flex_4k/skate_phantom_flex_4k.m3u8")
            .build()
        val mediaItem = MediaItem.Builder().setUri(uri).build()

        // Create an HlsMediaSource from the media item for handling HTTP Live Streaming (HLS) content 
        val internetVideoSource =
            HlsMediaSource.Factory(dataSourceFactory).createMediaSource(mediaItem)

        exoPlayer.setMediaSource(internetVideoSource)
        exoPlayer.prepare()

        // Will be used in later implementation for Equalizer
        viewModel.onStart(exoPlayer.audioSessionId)
    }

    // ...
}
  1. 显示ExoPlayer视图

将ExoPlayer视图集成到您的Composable函数中

// ...
Box(modifier = Modifier.fillMaxSize()) {
    AndroidView(
        modifier =
            Modifier.fillMaxWidth()
                .aspectRatio(1.4f)
                .padding(top = 16.dp)
                .background(Color.Black),
        factory = {
            PlayerView(context).apply {
                // Connect the ExoPlayer instance to the PlayerView
                player = exoPlayer
                // Configure ExoPlayer settings
                exoPlayer.repeatMode = Player.REPEAT_MODE_ONE
                exoPlayer.playWhenReady = false
                useController = true
            }
        }
    )
}
// ...

  1. 观察生命周期事件并释放资源
    设置DisposableEffects来观察生命周期事件,并在组合函数被释放时释放ExoPlayer
// ...
// Observe lifecycle events (e.g., app resume and pause)
// and adjust ExoPlayer's playback state accordingly.
DisposableEffect(key1 = lifecycleOwner) {
    val observer = LifecycleEventObserver { _, event ->
        if (event == Lifecycle.Event.ON_RESUME) {
            exoPlayer.playWhenReady = true
        } else if (event == Lifecycle.Event.ON_PAUSE) {
            exoPlayer.playWhenReady = false
        }
    }
    lifecycleOwner.lifecycle.addObserver(observer)
    onDispose {
        lifecycleOwner.lifecycle.removeObserver(observer)
    }
}

// Release the ExoPlayer when the composable is disposed
// This helps in proper resource management
DisposableEffect(key1 = Unit) {
    onDispose { ExoPlayerManager.releaseExoPlayer() }
}
// ...

设置音频均衡器

现在,让我们探讨如何在使用Jetpack Compose的Exoplayer设置中集成音频均衡器。这将允许用户通过调整预设均衡器设置或创建自定义均衡器配置来自定义音频体验。

音频均衡器通过提供对音频输出的细粒度控制来增强用户体验。

  1. 添加依赖项
    在项目的build.gradle文件中包含必要的依赖项:
    implementation("com.google.dagger:hilt-android:2.48")
    kapt("com.google.dagger:hilt-android-compiler:2.47")
    implementation("androidx.hilt:hilt-navigation-compose:1.1.0")

    //Gson
    implementation("com.google.code.gson:gson:2.9.1")

以上是示例依赖项,您可以根据您的项目需要进行相应的更改。

这些依赖项确保您的应用程序可以利用Hilt进行依赖注入和Gson以高效地将复杂数据存储在首选项中。

  1. 定义均衡器预设和增益值
    我们定义了一组预设,例如Flat, Acoustic,Rock,每个预设都有相应的增益值来控制音频频率。这些预设将作为用户自定义其音频体验的起点。
// Equalizer presets and gain values
val effectType = arrayListOf(
    "Custom", "Flat", "Acoustic", "Dance",
    "Hip Hop", "Jazz", "Pop", "Rock", "Podcast"
)

// Constants for presets
const val PRESET_CUSTOM = 0
const val PRESET_FLAT = 1
const val PRESET_ACOUSTIC = 2
const val PRESET_DANCE_LOUNGE = 3
const val PRESET_HIP_HOP = 4
const val PRESET_JAZZ_BLUES = 5
const val PRESET_POP = 6
const val PRESET_ROCK = 7
const val PRESET_PODCAST = 8

// Gain values for each preset
val FLAT = arrayListOf(0.0, 0.0, 0.0, 0.0, 0.0)
val ACOUSTIC = arrayListOf(0.44, 0.12, 0.12, 0.34, 0.2)
val DANCE = arrayListOf(0.52, 0.08, 0.28, 0.48, 0.06)
val HIP_HOPE = arrayListOf(0.44, 0.06, -0.14, 0.1, 0.38)
val JAZZ = arrayListOf(0.32, 0.0, 0.22, 0.1, 0.2)
val POP = arrayListOf(-0.14, 0.28, 0.38, 0.22, -0.2)
val ROCK = arrayListOf(0.38, 0.2, -0.04, 0.02, 0.34)
val PODCAST = arrayListOf(-0.12, 0.26, 0.36, 0.16, -0.2)

在上面的代码中,我们定义了一个EqualizerConfiguration数据类,它包含了预设设置和自定义频段设置的列表。EqualizerPreset类表示一个均衡器预设,包括名称和增益值的列表。CustomEqualizerBand类表示自定义的均衡器频段,包括频率和增益值。通过使用这些数据结构,我们可以轻松地管理和应用均衡器配置。

  1. 创建音频效果数据类
    音频效果数据类包含了关于所选效果类型及其相应增益值的重要信息。这个数据类充当用户偏好与音频均衡器实际实现之间的桥梁。
// 表示音频效果配置的数据类
data class AudioEffects(
    var selectedEffectType: Int = 0,
    var gainValues: ArrayList<Double>
)

在这里,selectedEffectType表示所选的音频预设,而gainValues则存储了不同频段的自定义增益值。这个数据类封装了用户的音频偏好设置。

  1. 创建AppModule进行依赖注入
    为了实现清晰模块化的依赖注入,我们引入了AppModule。这个模块使用@InstallIn(SingletonComponent::class)进行标注,提供了诸如Gson和SharedPreferences等必要的依赖项。
@Module
@InstallIn(SingletonComponent::class)
class AppModule {
    @Provides
    @Singleton
    fun provideGson(): Gson {
        val gsonBuilder = GsonBuilder()
        return gsonBuilder.create()
    }

    @Named(AUDIO_EFFECT_PREFERENCES)
    @Provides
    fun provideAudioEffectPreferences(application: Application): SharedPreferences {
        return application.getSharedPreferences(AUDIO_EFFECT_PREFERENCES, Context.MODE_PRIVATE)
    }
}

在这个模块中,provideGson提供了一个Gson的单例实例,而provideAudioEffectPreferences则提供了一个特定用于音频效果偏好设置的SharedPreferences实例。这个模块对于管理整个应用程序的依赖项非常重要。

  1. 使用SharedPreferencesGson实现均衡器偏好设置
    为了提供无缝的用户体验,我们将利用SharedPreferences来持久化与音频均衡器相关的用户偏好设置。此外,我们使用Gson进行高效的数据序列化,使我们能够将复杂的数据结构转换为易于存储和检索的格式。通过创建一个EqualizerPreferences类,我们确保用户不必重复设置他们的均衡器偏好设置。
const val AUDIO_EFFECT_PREFERENCES = "audio_effect_preferences"

private const val AUDIO_EFFECT_IS_EQUALIZER_ENABLED = "is_equalizer_enabled"
private const val AUDIO_EFFECT_EQUALIZER_SETTING = "equalizer_audio_effect"
private const val AUDIO_EFFECT_LOWEST_BAND_LEVEL = "equalizer_lowest_band_level"

@Singleton
class EqualizerPreferences
@Inject constructor(
    @param:Named(AUDIO_EFFECT_PREFERENCES) private val sharedPreferences: SharedPreferences,
    private val gson: Gson
) {

    var isEqualizerEnabled: Boolean
        get() = sharedPreferences.getBoolean(AUDIO_EFFECT_IS_EQUALIZER_ENABLED, false)
        set(isEnable) = sharedPreferences.edit()
            .putBoolean(AUDIO_EFFECT_IS_EQUALIZER_ENABLED, isEnable).apply()

    // Getting and setting the user's audio preferences
    var audioEffects: AudioEffects?
        get() {
            val json = sharedPreferences.getString(AUDIO_EFFECT_EQUALIZER_SETTING, null)
            if (json != null) {
                try {
                    return gson.fromJson(json, AudioEffects::class.java)
                } catch (t: Throwable) {
                    t.printStackTrace()
                }
            }
            return null
        }
        set(audioEffects) {
            var json: String? = null
            if (audioEffects != null) {
                json = gson.toJson(audioEffects)
            }
            sharedPreferences.edit().putString(AUDIO_EFFECT_EQUALIZER_SETTING, json).apply()
        }

    var lowestBandLevel: Int
        get() = sharedPreferences.getInt(AUDIO_EFFECT_LOWEST_BAND_LEVEL, 0)
        set(value) = sharedPreferences.edit().putInt(AUDIO_EFFECT_LOWEST_BAND_LEVEL, value).apply()
}

上述代码展示了如何使用SharedPreferences和Gson来保存和加载音频效果配置。saveAudioEffects方法将AudioEffects对象转换为JSON字符串,并将其保存在SharedPreferences中。loadAudioEffects方法从SharedPreferences中获取JSON字符串,并将其转换回AudioEffects对象。通过使用EqualizerPreferences类,我们可以方便地管理和访问均衡器偏好设置。

在这里,Gson在将我们的AudioEffects数据类转换为JSON字符串以存储在SharedPreferences中方面起着至关重要的作用。这确保了一种无缝且高效的方式来存储和检索复杂的数据结构。

  1. 创建一个音频均衡器ViewModel
    创建一个强大的AudioEqualizerViewModel,负责管理音频均衡器逻辑。这个ViewModel初始化均衡器,处理预设选择,并根据用户交互更新设置。
@HiltViewModel
class AudioEqualizerViewModel @Inject constructor(
    private val equalizerPreferences: EqualizerPreferences
) : ViewModel() {

    // MutableStateFlow to observe and emit changes in audio effects
    val audioEffects = MutableStateFlow<AudioEffects?>(null)

    // Instance of the Equalizer class from the Android system library
    private var equalizer: Equalizer? = null

    // MutableStateFlow to observe and emit changes in the equalizer's enable/disable state
    val enableEqualizer = MutableStateFlow(false)

    // Unique audio session ID associated with the Exoplayer
    private var audioSessionId = 0

    init {
        // Retrieve and set the initial equalizer enable/disable state and audio effects from preferences
        enableEqualizer.value = equalizerPreferences.isEqualizerEnabled
        audioEffects.tryEmit(equalizerPreferences.audioEffects)

        if (audioEffects.value == null) {
            audioEffects.tryEmit(AudioEffects(PRESET_FLAT, FLAT))
        }
    }

    // Will be called when exoplayer instance is created and we have audioSessionId
    fun onStart(sessionId: Int) {
        audioSessionId = sessionId
        equalizer?.enabled = enableEqualizer.value
        equalizer = Equalizer(Int.MAX_VALUE, audioSessionId)
  
        // Set the lowest band level based on the equalizer's capabilities
        equalizerPreferences.lowestBandLevel = equalizer?.bandLevelRange?.get(0)?.toInt() ?: 0
        
        // Apply gain values to the equalizer based on the stored audio effects
        audioEffects.value?.gainValues?.forEachIndexed { index, value ->
            val bandLevel = (value * 1000).toInt().toShort()
            equalizer?.setBandLevel(index.toShort(), bandLevel)
        }
    }

    // Method called when a preset is selected
    fun onSelectPreset(presetPosition: Int) {
      
        // Return if no audio effects are available
        if (audioEffects.value == null) return

        // Determine the gain values based on the selected preset
        val gain = if (presetPosition == PRESET_CUSTOM) {
            ArrayList(audioEffects.value!!.gainValues)
        } else {
            ArrayList(getPresetGainValue(presetPosition))
        }

        // Update the audio effects with the selected preset and gain values
        audioEffects.tryEmit(AudioEffects(presetPosition, gain))
        equalizerPreferences.audioEffects = audioEffects.value

        // Apply the gain values to the equalizer
        equalizer?.apply {
            gain.forEachIndexed { index, value ->
                val bandLevel = (value * 1000).toInt().toShort()
                setBandLevel(index.toShort(), bandLevel)
            }
        }
    }

    // Method called when a specific band level is changed by the user
    fun onBandLevelChanged(changedBand: Int, newGainValue: Int) {
        // Retrieve the lowest band level from preferences
        val lowest = equalizerPreferences.lowestBandLevel

        // Calculate the new band level
        val bandLevel = newGainValue.plus(lowest)

        // Apply the new band level to the equalizer
        equalizer?.setBandLevel(changedBand.toShort(), bandLevel.toShort())
        val list = ArrayList(audioEffects.value!!.gainValues)
        list[changedBand] = (newGainValue.toDouble() / 1000)
        audioEffects.tryEmit(
            AudioEffects(
                PRESET_CUSTOM,
                list
            )
        )
        equalizerPreferences.audioEffects = audioEffects.value
    }

    // Method called to toggle the equalizer's enable/disable state
    fun toggleEqualizer() {
        enableEqualizer.tryEmit(!enableEqualizer.value)
        equalizer?.enabled = enableEqualizer.value
        equalizerPreferences.isEqualizerEnabled = enableEqualizer.value
        if (!enableEqualizer.value) {
            audioEffects.tryEmit(AudioEffects(PRESET_FLAT, FLAT))
            equalizerPreferences.audioEffects = audioEffects.value
        }
    }

    // Method to retrieve gain values for a specific preset
    private fun getPresetGainValue(index: Int): List<Double> {
        return when (index) {
            PRESET_FLAT -> FLAT
            PRESET_ACOUSTIC -> ACOUSTIC
            PRESET_DANCE_LOUNGE -> DANCE
            PRESET_HIP_HOP -> HIP_HOPE
            PRESET_JAZZ_BLUES -> JAZZ
            PRESET_POP -> POP
            PRESET_ROCK -> ROCK
            PRESET_PODCAST -> PODCAST
            else -> FLAT
        }
    }
}

这个ViewModel高效地管理音频均衡器的状态,处理用户交互,并确保使用SharedPreferences持久化用户偏好设置。

  1. 开发均衡器开关视图、预设视图和均衡器视图组件
    设计一个用户友好的均衡器开关、均衡器视图和预设视图组件,让用户可以可视化和调整均衡器设置。开关允许用户使用均衡器启用/禁用音频设置,而均衡器视图将包含不同频率带的滑块,提供高度可定制的音频体验。预设视图将包含一些预定义的效果类型,可以直接应用到均衡器上。

Switch View

Row(
    modifier = Modifier.fillMaxWidth(),
    horizontalArrangement = Arrangement.SpaceBetween,
    verticalAlignment = Alignment.CenterVertically
) {
    Text(
        text = stringResource(R.string.equalizer_title_text),
        fontSize = MaterialTheme.typography.titleLarge.fontSize,
        fontWeight = FontWeight.SemiBold,
        color = Color.White
    )

    Switch(
        checked = enableEqualizer,
        onCheckedChange = { 
            // Toggle the equalizer's enable/disable state
            viewModel.toggleEqualizer() 
        },
        colors =
            SwitchDefaults.colors(
                checkedTrackColor = Color.Black,
                checkedIconColor = Color.Black,
                uncheckedTrackColor = Color.White,
                uncheckedBorderColor = Color.Black,
            )
    )
}

Equalizer SwitchView
EqualizerView

@OptIn(ExperimentalMaterial3Api::class)
@Composable
fun EqualizerView(viewModel: AudioEqualizerViewModel) {

    // Frequency labels for the equalizer bands
    val xAxisLabels = listOf("60Hz", "230Hz", "910Hz", "3kHz", "14kHz")

    // Collect the current state of audio effects from the ViewModel
    val audioEffects by viewModel.audioEffects.collectAsState()

    // Column layout to arrange UI elements vertically
    Column(
        modifier = Modifier
            .fillMaxWidth()
            .fillMaxHeight()
            .graphicsLayer {
                // Rotate the entire column to display frequency labels/sliders vertically
                rotationZ = 270f
            },
        verticalArrangement = Arrangement.SpaceEvenly,
        horizontalAlignment = Alignment.CenterHorizontally
    ) {
        // Iterate through frequency labels and create corresponding UI elements
        for (index in xAxisLabels.indices) {
            Row(
                modifier = Modifier
                    .padding(top = 20.dp)
                    .width(220.dp)
            ) {
                // Each frequency label and its corresponding slider are placed in a Box
                Box {
                    // Display the frequency label with rotation
                    Text(
                        text = xAxisLabels[index],
                        modifier = Modifier
                            .wrapContentWidth()
                            .align(Alignment.CenterStart)
                            .rotate(90f),
                        color = Color.White,
                        fontSize = 8.sp,
                        textAlign = TextAlign.Start
                    )

                    // Slider component for adjusting the gain value of each frequency band
                    Slider(
                        modifier = Modifier
                            .offset(x = 20.dp),
                        // Bind the slider value to the corresponding gain value from the ViewModel
                        value = audioEffects!!.gainValues[index].times(1000f).toFloat()
                            .coerceIn(-3000f, 3000f),
                        onValueChange = {
                            // Notify the ViewModel when a slider value changes
                            viewModel.onBandLevelChanged(index, it.toInt())
                        },
                        valueRange = -3000f..3000f,
                        colors = SliderDefaults.colors(
                            thumbColor = Color.Black,
                            activeTrackColor = Color.Black,
                            inactiveTrackColor = Color.White
                        ),
                        thumb = {
                            // Customized appearance of the slider's thumb
                            Box(
                                modifier = Modifier
                                    .size(20.dp)
                                    .border(
                                        1.dp,
                                        Color.White,
                                        CircleShape
                                    )
                                    .clip(CircleShape)
                                    .background(Color.Black, CircleShape)
                            )
                        }
                    )
                }
            }
        }
    }
}

Equalizer View
PresetsView

@Composable
fun PresetsView(viewModel: AudioEqualizerViewModel) {
    // Collect the current state of audio effects from the ViewModel
    val audioEffects by viewModel.audioEffects.collectAsState()

    // Group the effect types into chunks of 4 for layout
    val groupedList = effectType.chunked(4)

    // Row containing the title and dividers
    Row(
        verticalAlignment = Alignment.CenterVertically
    ) {
        Divider(
            modifier = Modifier
                .weight(1f)
                .height(4.dp)
                .clip(RoundedCornerShape(4.dp)),
            color = Color.White,
            thickness = 1.dp
        )

        // Title text
        Text(
            text = stringResource(R.string.presets_title_text),
            fontSize = MaterialTheme.typography.titleMedium.fontSize,
            fontWeight = FontWeight.Medium,
            color = Color.White,
            modifier = Modifier
                .wrapContentWidth()
                .weight(0.5f)
                .padding(4.dp)
                .zIndex(1f),
            textAlign = TextAlign.Center
        )

        Divider(
            modifier = Modifier
                .weight(1f)
                .height(4.dp)
                .clip(RoundedCornerShape(4.dp)),
            color = Color.White,
            thickness = 1.dp
        )
    }

    Spacer(modifier = Modifier.height(20.dp))

    // Iterate through grouped effect types and create UI elements
    for (itemList in groupedList) {
        BoxWithConstraints(
            modifier = Modifier
                .fillMaxWidth()
        ) {
            // Calculate padding and spacing based on screen width
            val horizontalPadding =
                if (maxWidth < 320.dp) 8.dp else if (maxWidth > 400.dp) 40.dp else 20.dp
            val horizontalSpacing = if (maxWidth > 400.dp) 24.dp else 16.dp

            // Row containing individual preset items
            Row(
                modifier = Modifier
                    .fillMaxWidth()
                    .padding(vertical = 8.dp),
                horizontalArrangement = Arrangement.spacedBy(
                    space = horizontalSpacing,
                    alignment = Alignment.CenterHorizontally
                ),
                verticalAlignment = Alignment.CenterVertically
            ) {
                for (item in itemList) {
                    // Get the index of the current item
                    val index by remember {
                        mutableIntStateOf(
                            effectType.indexOf(
                                item
                            )
                        )
                    }

                    // Create a clickable preset item
                    BoxWithConstraints(
                        modifier = Modifier
                            .wrapContentSize()
                            .border(
                                1.dp,
                                if (index == audioEffects?.selectedEffectType) Color.White else Color.Black,
                                RoundedCornerShape(40.dp)
                            )
                            .clip(RoundedCornerShape(40.dp))
                            .clickable {
                                // Notify the ViewModel when a preset is selected
                                viewModel.onSelectPreset(index)
                            }
                            .background(if (index == audioEffects?.selectedEffectType) Color.Black else Color.White),
                        contentAlignment = Alignment.Center
                    ) {
                        // Display the preset item text
                        Text(
                            text = item,
                            style = MaterialTheme.typography.bodySmall,
                            modifier = Modifier
                                .padding(
                                    horizontal = horizontalPadding,
                                    vertical = 12.dp
                                ),
                            fontSize = 14.sp,
                            color = if (index == audioEffects?.selectedEffectType) Color.White else Color.Black,
                            maxLines = 1,
                            overflow = TextOverflow.Ellipsis
                        )
                    }
                }
            }
        }
    }
}


现在,让我们使用AnimatedVisibility从父组件中调用上述函数!

@Composable
fun AudioEqualizerScreen() {
    val viewModel = hiltViewModel<AudioEqualizerViewModel>()

    val enableEqualizer by viewModel.enableEqualizer.collectAsState()

    Column {
        Row(
            modifier = Modifier.fillMaxWidth(),
            horizontalArrangement = Arrangement.SpaceBetween,
            verticalAlignment = Alignment.CenterVertically
        ) {
            Text(
                text = stringResource(R.string.equalizer_title_text),
                fontSize = MaterialTheme.typography.titleLarge.fontSize,
                fontWeight = FontWeight.SemiBold,
                color = Color.White
            )

            Switch(
                checked = enableEqualizer,
                onCheckedChange = { viewModel.toggleEqualizer() },
                colors =
                    SwitchDefaults.colors(
                        checkedTrackColor = Color.Black,
                        checkedIconColor = Color.Black,
                        uncheckedTrackColor = Color.White,
                        uncheckedBorderColor = Color.Black,
                    )
            )
        }

        AnimatedVisibility(
            visible = enableEqualizer,
            enter = fadeIn() + slideInVertically { fullHeight -> -fullHeight / 2 },
            exit = fadeOut() + slideOutVertically { fullHeight -> -fullHeight / 3 }
        ) {
            EqualizerView(viewModel = viewModel)
        }

        AnimatedVisibility(
            visible = enableEqualizer,
            enter = fadeIn() + slideInVertically { fullHeight -> -fullHeight / 2 },
            exit = fadeOut() + slideOutVertically { fullHeight -> -fullHeight / 2 }
        ) {
            PresetsView(viewModel)
        }
    }
}

Final View

结论

在本篇博客中,我们为在Jetpack Compose应用程序中设置ExoPlayer进行实时流式传输和集成音频均衡器打下了基础。这个组合为带有可定制均衡器设置的音频流提供了无缝的用户体验。

Github

https://github.com/cp-megh-l/audio-equalizer-compose

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.mfbz.cn/a/265768.html

如若内容造成侵权/违法违规/事实不符,请联系我们进行投诉反馈qq邮箱809451989@qq.com,一经查实,立即删除!

相关文章

html网页编写语言

html是一门语言&#xff0c;所有的网页都是用html这门语言编写出来的。 HTML&#xff08;HyperText Markup Language&#xff09;&#xff1a;超文本标记语言。 超文本&#xff1a;超越了文本的限制&#xff0c;比普通文本更强大。除了文字信息&#xff0c;还可以定义图片&…

Netty-1-编写网络应用程序的基本步骤

编写网络应用程序的基本步骤如下: 完成代码编写。复查代码。“临门一脚"。上线及反馈。 完成代码编写 编写网络应用程序的第一步是完成代码编写。 选择传输协议 对于普通的应用程序而言&#xff0c;经过需求分析、定义业务数据结构和实现业务逻辑之后&#xff0c;我…

研究论文 20231123-Genome Biology:零样本学习预测细基因表达顺式调控模式

Li, Yongge, et al. "CREaTor: zero-shot cis-regulatory pattern modeling with attention mechanisms." Genome Biology 24.1 (2023): 266. 2023年11月23日见刊 微信分享&#xff1a;Genome Biology | CREaTor: 零样本学习预测细胞类型特异的基因表达顺式调控模式…

算法与数据结构--哈夫曼树与哈夫曼编码

演示视频&#xff1a; 【1】数据结构——五分钟搞定哈夫曼树&#xff0c;会求WPL值&#xff0c;不会你打我_哔哩哔哩_bilibili 【2】哈夫曼树和哈夫曼编码_哔哩哔哩_bilibili 【3】哈夫曼树的构造的做题三步骤_哔哩哔哩_bilibili 求哈夫曼编码的步骤&#xff1a; 1.根据字符及…

HTML标签(下)

一、表格标签 1.1表格的主要作用 主要用于显示、展示数据 1.2表格的基本语法 <td>单元格中的文字</td> 如果是表头单元格的话&#xff0c;eg:姓名&#xff0c;年龄<th> 姓名</th>&#xff08;th是table head&#xff09;; 作用&#xff1a;表头会…

用Python处理PDF:拆分与合并PDF文档

PDF文档在信息共享和数据保存方面被广泛使用&#xff0c;处理PDF文档也成为常见需求。其中&#xff0c;合并和拆分PDF文档能够帮助我们更有效地管理PDF文档&#xff0c;使文档内容分布更合理。通过合并&#xff0c;可以将相关文档整合成一个文件&#xff0c;以便更好地组织和提…

深入了解C编译管道

文章目录 引言1. 预处理阶段2. 编译阶段3. 汇编阶段4. 链接阶段5.流程图结论 引言 C编译管道是软件开发中至关重要的工具&#xff0c;它负责将C语言源代码转换为可执行的机器代码。理解C编译管道的工作原理有助于提高代码的可读性、可维护性&#xff0c;并有助于优化生成的可执…

20231222给NanoPC-T4(RK3399)开发板的适配原厂Android10的挖掘机方案并跑通AP6398SV

20231222给NanoPC-T4(RK3399)开发板的适配原厂Android10的挖掘机方案并跑通AP6398SV 1、简略步骤&#xff1a;rootrootrootroot-X99-Turbo:~/3TB/3399-android10$ cat Rockchip_Android10.0_SDK_Release.tar.gz0* > Rockchip_Android10.0_SDK_Release.tar.gz rootrootrootro…

SUSE Linux服务器使用zypper安装nginx

SUSE Linux 的云服务器用户&#xff0c;不能yum,安装软件&#xff0c;可通过 zypper 快速安装软件。 使用 root 账号登录 openSUSE 操作系统的云服务器。 执行 zypper service-list 或 zypper sl 命令 列出软件源 安装软件包 执行 zypper search 或 zypper se 命令&#…

扫码展示多视频怎么做?视频的活码制作技巧

现在扫码看视频的应用场景越来越多&#xff0c;用这种方式不仅能够简单有效的低成本完成视频传播&#xff0c;而且也符合用户的习惯。那么当需要将视频制作二维码来展示内容时&#xff0c;多个视频文件生成二维码的制作方法是怎么操作的呢&#xff1f;下面教大家使用视频二维码…

运用ETL快速拉取吉客云平台订单信息

吉客云介绍 吉客云是一家中国的云计算服务提供商。它提供了包括云服务器、云数据库、云存储、云网络等各种云计算产品和解决方案&#xff0c;帮助企业和个人搭建高效、可靠、安全的云计算环境。 吉客云特点和优势&#xff1a; 大规模分布式架构&#xff1a;吉客云基于自主研发…

安装nodejs,配置环境变量并将npm设置淘宝镜像源

安装nodejs并将npm设置淘宝镜像源 1. 下载nodejs 个人不喜欢安装包&#xff0c;所以是下载zip包的方式。这里我下载的node 14解压包版本 下载地址如下&#xff1a;https://nodejs.org/dist/v14.15.1/node-v14.15.1-win-x64.zip 想要其他版本的小伙伴去https://nodejs.org/di…

【【迭代16次的CORDIC算法-verilog实现】】

迭代16次的CORDIC算法-verilog实现 -32位迭代16次verilog代码实现 CORDIC.v module cordic32#(parameter DATA_WIDTH 8d32 , // we set data widthparameter PIPELINE 5d16 // Optimize waveform)(input …

opencv入门到精通——改变颜色空间

目录 目标 改变颜色空间 对象追踪 如何找到要追踪的HSV值&#xff1f; 目标 在本教程中&#xff0c;你将学习如何将图像从一个色彩空间转换到另一个&#xff0c;像BGR↔灰色&#xff0c;BGR↔HSV等 除此之外&#xff0c;我们还将创建一个应用程序&#xff0c;以提取视频中的…

Mapmost Alpha上新啦!新增移动端的丝滑且强大功能!

本文目录 一、Mapmost Alpha 介绍1.1 Maopmost 数字孪生平台1.2 Mapmost 产品能力1.3 Mapmost Alpha 产品优势 二、移动端功能介绍三、Mapmost Alpha 总结 一、Mapmost Alpha 介绍 Hello&#xff0c;各位铁铁&#xff0c;今天给大家推荐一款好用的三维城市场景创建工具。 这款…

k8s部署elastic+kibana

1.软件版本说明 1.1软件版本说明 软件版本kubernetes1.23.17elasticsearch7.17.3kibana7.17.3 1.2硬件环境说明 宿主机使用windows10安装vmware17.5.0&#xff0c;虚拟机安装linux系统&#xff08;centos7.9&#xff09; 说明&#xff1a; elasticserch和kibana的版本尽量…

NTFS权限与文件系统:深入解析与实践指南

在当今的信息时代&#xff0c;数据安全和管理成为了每个组织和个人的重要议题。NTFS权限作为Windows操作系统中的一个核心功能&#xff0c;为文件和文件夹的安全管理提供了强大的支持。本文将深入解析NTFS权限的基本概念&#xff0c;并通过实际操作指导如何有效地利用这些权限来…

MySQL的hash索引

MySQL有BTree 索引及Hash索引等索引类型&#xff0c;BTree索引类型是MySQL采用最多的索引类型。Hash索引使用场景比较有限&#xff0c;文章将从Hash索引的底层结构出发&#xff0c;来分析Hash索引的利与弊。 1 hash数据结构 hash数据结构由键、哈希函数及哈希表组成。 键&am…

【3D生成与重建】SSDNeRF:单阶段Diffusion NeRF的三维生成和重建

系列文章目录 题目&#xff1a;Single-Stage Diffusion NeRF: A Unified Approach to 3D Generation and Reconstruction 论文&#xff1a;https://arxiv.org/pdf/2304.06714.pdf 任务&#xff1a;无条件3D生成&#xff08;如从噪音中&#xff0c;生成不同的车等&#xff09;、…

提前预测刚体移动轨迹 预测运动轨迹

提前预测刚体移动轨迹 预测运动轨迹 一、效果二、介绍三、脚本RigidbodyExtension.cs 计算工具类DrawLine.cs 画线工具类 四、资源分享 一、效果 二、介绍 通过计算Unity物理系统的运动方位来判断下一步移动的位置&#xff0c;主要用于物体运动的提前预测&#xff0c;通常使用…
最新文章