Skip to content

Custom Fine Tuning Presets UI

This guide explains the concepts and MSDK APIs that you will need to use in order to create your own custom Fine Tuning preset selection user interface.

It doesn't prescribe the UI components to be used - you are free to choose what representation you think best suits your design, rather it will outline how to get the necessary state to display and how to affect changes in response to user input.

Introduction

"Fine Tuning" is an MSDK feature which provides the user with a selection of possible personalization presets, from which they can select their preferred sound personalization experience.

In the standard Mimi Profile UI, the Fine Tuning feature is incorporated into the "Sound personalization" profile card.

The available Fine Tuning preset options may include:

  • Recommended
  • Richer
  • Softer

The available presets depend upon the user's hearing assessment, so not all presets options are available in all cases, as shown in these screenshots of Mimi Profile:

All Fine Tuning options Only recommended and richer Only recommended and softer Only recommended

Note

The term "Fine Tuning" is generally used when referring to the user-facing aspects of the feature, however in places the MSDK APIs use the term "Up Down" synonymously.

Understanding UpDown Presets

The UpDownPreset class contains the available fine-tuning personalization presets retrieved from the Mimi backend.

It is defined by io.mimi.sdk.core.model.personalization.updown.UpDownPreset, and contains up to three different presets.

  • default - the "Recommended" preset
  • up - the "Richer" preset
  • down - the "Softer" preset

If a preset component value is null, then that preset is not available for the current user.

The current state is available from the PersonalizationController.upDownPresets MimiObservable property.

Building a custom Fine Tuning UI

This section describes the individual steps required to create your own customized Fine Tuning UI experience.

It will cover the following topics:

  • Activate the ProcessingSession with PersonalizationModeConfiguration.FineTuning.
  • Display the available Fine Tuning preset options
  • Display the current Fine Tuning preset selection
  • Apply the preset payload to the preset processing parameter.

Activate the ProcessingSession with FineTuning Personalization

To support the Fine Tuning feature, you must must activate your app's ProcessingSession using PersonalizationModeConfiguration.FineTuning mode.

This is done by providing a PersonalizationModeConfiguration instance to the MimiCore.processingController.activate function.

val session = MimiCore.processingController.activate(
    MimiProcessingConfiguration(
        personalization = PersonalizationConfiguration(
            mode = FineTuning(Fitting(TODO("Fitting is defined by your Processing system")))
        )
    )
)

Read more in the Preset Parameter Data Sources guide.

Display the available Fine Tuning Preset options

As the available Fine Tuning presets can vary, your custom UI will need to adapt accordingly to only offer the appropriate presets for selection.

The PersonalizationController provides access to the current UpDownPreset values and their availability -

// Observe the changes in upDownPresets to determine availability - if `null`, then that preset type is not available.
coroutineScope.launch {
    MimiCore.personalizationController.upDownPresets.observe { upDownPresets: AsyncState<UpDownPreset?> ->
        // Extract the preset availability for each of the fine tuning options
        val isRecommendedAvailable = upDownPresets.value?.presets?.default != null
        val isRicherAvailable = upDownPresets.value?.presets?.up != null
        val isSofterAvailable = upDownPresets.value?.presets?.down != null

        // TODO - Update your UI to reflect the preset availability
    }
}

Note

The Mimi Profile screen does not show any Fine Tuning preset selection options if only the "Recommended" preset is available.

Display the current Fine Tuning preset selection

In addition to displaying only the available presets, your custom UI also needs to show the user which preset is currently in use.

This is done by comparing the currently applied preset value to the available UpDownPreset.

Note

Only a single preset can be selected at a time.

This requires observing both the preset ProcessingParameter and the upDownPreset values, as a change in either will affect the interpretation of the current selected preset type.

// Define an enum for the possible selection options
enum class PresetType {
    SOFTER,
    RECOMMENDED,
    RICHER
}
// Define a function which returns the selected `PresetType` given the current `UpDownPreset`and `preset` states. 
fun getSelectedPresetType(
        upDownPreset: UpDownPreset?,
        presetParameterPayload: String?
    ): PresetType? {
        if (upDownPreset == null || presetParameterPayload == null) return null

        return with(upDownPreset.presets) {
            if (default?.payload == presetParameterPayload) PresetType.RECOMMENDED
            else if (up?.payload == presetParameterPayload) PresetType.RICHER
            else if (down?.payload == presetParameterPayload) PresetType.SOFTER
            else null
        }
    }

Then observe changes to both the upDownPreset MimiObservable and the preset ProcessingParameter, invoking this function on each change and using the result to trigger an update to the UI state.

These two snippets show how this can be done for the preset and UpDownPreset changes respectively:

// Observe changes in the preset ProcessingParameter
coroutineScope.launch {
    val activeSession: ProcessingSession =
        requireNotNull(MimiCore.processingController.activeSession.state) { "No active ProcessingSession!" }

    activeSession.preset.observe { preset ->
        val currentUpDownPresets: UpDownPreset? =
            MimiCore.personalizationController.upDownPresets.state.value
        val selectedPresetType: PresetType? =
            getSelectedPresetType(currentUpDownPresets, preset?.payload)

        // TODO Update UI state with new selection: `selectedPresetType`
    }
}
// Observe changes in the UpDownPresets
coroutineScope.launch {
    MimiCore.personalizationController.upDownPresets.observe { upDownPresets: AsyncState<UpDownPreset?> ->
        val activeSession: ProcessingSession =
            requireNotNull(MimiCore.processingController.activeSession.state) { "No active ProcessingSession!" }

        val currentPresetPayload = activeSession.preset.value?.payload
        val selectedPresetType: PresetType? =
            getSelectedPresetType(upDownPresets.value, currentPresetPayload)

        // TODO Update UI state with new selection: `selectedPresetType`
    }
}

Apply the Preset in response to user selection

When the user selects to change the current Fine Tuning preset, you will need to apply that PersonalizationPreset to the preset ProcessingParameter in the active ProcessingSession.

This example, shows how to apply the Richer (Up) preset value to the preset ProcessingParameter.

// Based on the user selection, take the preset from the corresponding `UpDownPresets` property.
// In this example, we demonstrate the user selecting the "Richer" preset.
// For "recommended" use - `upDownPresets.state.value?.presets?.default?.payload`
// For "softer" use - `upDownPresets.state.value?.presets?.down?.payload`
val richerPayload: String =
    requireNotNull(MimiCore.personalizationController.upDownPresets.state.value?.presets?.up?.payload) { "User selected preset payload is not available!" }

// Convert to a `PersonalizationPreset` using the libcore helper function
// Note: As of 7.4.0, this is currently an experimental API, so it may change in future.
// You will need to opt-in to use it: @OptIn(MsdkInternalApi::class)
val richerPreset: Personalization.PersonalizationPreset =
    richerPayload.toPersonalizationPreset()

// Acquire the active `ProcessSession`
val activeSession: ProcessingSession =
    requireNotNull(MimiCore.processingController.activeSession.state) { "No active ProcessingSession!" }

// Apply the richer preset to the preset Parameter.
val result = coroutineScope.async {
    activeSession.preset.apply(richerPreset)
}

// Depending on your UI requirements, you may wish to handle the `result`, although an apply failure
// is also reported through the `preset` `ProcessingParameter` state.