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

news2025/1/15 19:49:17

在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.coloradmin.cn/o/1331811.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

【数据结构一】初始Java集合框架(前置知识)

Java中的数据结构 Java语言在设计之初有一个非常重要的理念便是&#xff1a;write once&#xff0c;run anywhere&#xff01;所以Java中的数据结构是已经被设计者封装好的了&#xff0c;我们只需要实例化出想使用的对象&#xff0c;便可以操作相应的数据结构了&#xff0c;本篇…

锯齿云服务器租赁使用教程

首先登陆锯齿云账号 网盘上传数据集与代码 随后我们需要做的是将所需要的数据集与代码上传到网盘&#xff08;也可以直接在租用服务器后将数据集与代码传到服务器的硬盘上&#xff0c;但这样做会消耗大量时间&#xff0c;造成资源浪费&#xff09; 点击工作空间&#xff1a;…

【Python】基于flaskMVT架构与session实现博客前台登录登出功能

目录 一、MVT说明 1.Model层 2.View层 3.Template层 二、功能说明 三、代码框架展示 四、具体代码实现 models.py 登录界面前端代码 博客界面前端代码&#xff08;profile.html&#xff09; main.py 一、MVT说明 MVT架构是Model-View-Template的缩写&#xff0c;是…

基于 Editor.js 开发富文本编辑器库

开始 Editor.js 提供了简单而直观的用户界面&#xff0c;根据需求可以灵活添加自定义的编辑工具&#xff0c;通过插件扩展功能 Editorjs 使用 js 开发&#xff0c;脱离框架依赖&#xff0c;因此可以基于它封装富文本编辑器&#xff0c;用于 Vue 和 React 项目 editor-js-com…

电化学仿真的基础知识笔记

1 概述 电化学反应是一种特殊的化学反应&#xff0c;其能量转移形式为化学能和电能之间互相转换。根据能量转换方向&#xff0c;可分为两类&#xff1a; 原电池&#xff08;Galvanic cells&#xff09;&#xff1a;将化学能转化为电能&#xff0c;对外放电电解槽&#xff08;…

2024 年 22 款顶级免费数据恢复软件比较 [Windows 和 Mac]

适用于 Windows 和 Mac 用户的最佳数据恢复软件下载列表和比较&#xff0c;可快速恢复丢失的数据、已删除的文件、照片或格式化的分区数据&#xff1a; 数据恢复软件是一种从任何存储介质恢复丢失文件的应用程序。它可以恢复由于病毒攻击、硬盘故障或任何其他原因而意外删除或…

Hadoop入门学习笔记——四、MapReduce的框架配置和YARN的部署

视频课程地址&#xff1a;https://www.bilibili.com/video/BV1WY4y197g7 课程资料链接&#xff1a;https://pan.baidu.com/s/15KpnWeKpvExpKmOC8xjmtQ?pwd5ay8 Hadoop入门学习笔记&#xff08;汇总&#xff09; 目录 四、MapReduce的框架配置和YARN的部署4.1. 配置MapReduce…

nn.LSTM个人记录

简介 nn.LSTM参数 torch.nn.lstm(input_size, "输入的嵌入向量维度&#xff0c;例如每个单词用50维向量表示&#xff0c;input_size就是50"hidden_size, "隐藏层节点数量,也是输出的嵌入向量维度"num_layers, "lstm 隐层的层数&#xff0c;默认…

02_算法分析

02_算法分析 0.1 算法的时间复杂度分析0.1.1 函数渐近增长概念&#xff1a;输入规模n>2时&#xff0c;算法A1的渐近增长小于算法B1 的渐近增长随着输入规模的增大&#xff0c;算法的常数操作可以忽略不计测试二&#xff1a;随着输入规模的增大&#xff0c;与最高次项相乘的常…

【计数DP】牛客小白月赛19

登录—专业IT笔试面试备考平台_牛客网 题意 思路 首先做法一定是计数 dp 然后状态设计&#xff0c;先设 dp[i] 然后看影响决策的因素&#xff1a;两边的火焰情况&#xff0c;那就 dp[i][0/1][0/1]表示 前 i 个&#xff0c;该位有无火焰&#xff0c;该位右边有无火焰的方案数…

单片机的RTC获取网络时间

理解网络同步校准RTC的原理需要考虑NTP、SNTP、RTC这三个关键组件的作用和交互。下面详细解释这个过程&#xff1a; 1. NTP&#xff08;Network Time Protocol&#xff09;&#xff1a; 协议目的&#xff1a;NTP是用于同步计算机和设备时钟的协议。它通过在网络上与时间服务器通…

为什么react call api in cDidMount

为什么react call api in cDM 首先&#xff0c;放到constructor或者cWillMount不是语法错误 参考1 参考2 根据上2个参考&#xff0c;总结为&#xff1a; 1、官网就是这么建议的&#xff1a; 2、17版本后的react 由于fiber的出现导致 cWM 会调用多次&#xff01; cWM 方法已…

【并发设计模式】聊聊两阶段终止模式如何优雅终止线程

在软件设计中&#xff0c;抽象出了23种设计模式&#xff0c;用以解决对象的创建、组合、使用三种场景。在并发编程中&#xff0c;针对线程的操作&#xff0c;也抽象出对应的并发设计模式。 两阶段终止模式- 优雅停止线程避免共享的设计模式- 只读、Copy-on-write、Thread-Spec…

LangChain 30 ChatGPT LLM将字符串作为输入并返回字符串Chat Model将消息列表作为输入并返回消息

LangChain系列文章 LangChain 实现给动物取名字&#xff0c;LangChain 2模块化prompt template并用streamlit生成网站 实现给动物取名字LangChain 3使用Agent访问Wikipedia和llm-math计算狗的平均年龄LangChain 4用向量数据库Faiss存储&#xff0c;读取YouTube的视频文本搜索I…

【线性代数】决定张成空间的最少向量线性无关吗?

答1&#xff1a; 是的&#xff0c;张成空间的最少向量是线性无关的。 在数学中&#xff0c;张成空间&#xff08;span space&#xff09;是一个向量空间&#xff0c;它由一组向量通过线性组合&#xff08;即每个向量乘以一个标量&#xff09;生成。如果这组向量是线性无关的&…

CV算法面试题学习

本文记录了CV算法题的学习。 CV算法面试题学习 点在多边形内&#xff08;point in polygon&#xff09;高斯滤波器 点在多边形内&#xff08;point in polygon&#xff09; 参考自文章1&#xff0c;其提供的代码没有考虑一些特殊情况&#xff0c;所以做了改进。 做法&#xff…

网络爬虫之多任务数据采集(多线程、多进程、协程)

进程&#xff1a;是操作系统中资源分配的基本单位 线程&#xff1a;使用进程分配的资源处理具体任务 一个进程中可以有多个线程&#xff1a;进程相当于一个公司&#xff0c;线程就是公司里面的员工。 一 多线程 多线程都是关于功能的并发执行。而异步编程是关于函数之间的非…

持续集成交付CICD:Jira 远程触发 Jenkins 实现更新 GitLab 分支

目录 一、实验 1.环境 2.GitLab 查看项目 3.Jira新建模块 4. Jira 通过Webhook 触发Jenkins流水线 3.Jira 远程触发 Jenkins 实现更新 GitLab 分支 二、问题 1.Jira 配置网络钩子失败 2. Jira 远程触发Jenkins 报错 一、实验 1.环境 &#xff08;1&#xff09;主机 …

uniapp自定义头部导航怎么实现?

一、在pages.json文件里边写上自定义属性 "navigationStyle": "custom" 二、在对应的index页面写上以下&#xff1a; <view :style"{ height: headheight px, backgroundColor: #24B7FF, zIndex: 99, position: fixed, top: 0px, width: 100% …

STM32——CAN协议

文章目录 一.CAN协议的基本特点1.1 特点1.2 电平标准1.3 基本的五个帧1.4 数据帧 二.数据帧解析2.1 帧起始和仲裁段2.2 控制段2.3 数据段和CRC段2.4 ACK段和帧结束 三.总线仲裁四.位时序五.STM32CAN控制器原理与配置5.1 STM32CAN控制器介绍5.2 CAN的模式5.3 CAN框图 六 手册寄存…