Skip to content
Original file line number Diff line number Diff line change
Expand Up @@ -102,7 +102,7 @@ internal fun PresentationSentenceAnalysisResponse.toDomain(): SentenceAnalysisDe
status = WordAnalysisStatus.from(value = status),
mainFeedback = mainFeedback,
subFeedback = subFeedback,
guideScript = guideScript,
guideScript = guideScript.orEmpty(),
accuracy = accuracy,
startTimeMs = startTimeMs,
endTimeMs = endTimeMs,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,7 @@ fun PrezelButtonArea(
isVertical: Boolean = true,
isStrongStrength: Boolean = true,
showBackground: Boolean = false,
showDivider: Boolean = showBackground,
isNested: Boolean = false,
config: PrezelButtonAreaDefault = PrezelButtonAreaDefaults.getDefault(),
mainButton: @Composable (Modifier) -> Unit,
Expand All @@ -53,7 +54,7 @@ fun PrezelButtonArea(
.fillMaxWidth()
.then(if (showBackground) Modifier.background(config.backgroundColor) else Modifier),
) {
if (showBackground) {
if (showDivider) {
PrezelHorizontalDivider(type = PrezelDividerType.THICK, color = config.borderColor)
}

Expand Down
Original file line number Diff line number Diff line change
@@ -1,8 +1,6 @@
package com.team.prezel.core.designsystem.component.voice

import androidx.annotation.FloatRange
import androidx.compose.animation.core.animateFloatAsState
import androidx.compose.animation.core.tween
import androidx.compose.foundation.clickable
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.Row
Expand Down Expand Up @@ -35,6 +33,9 @@ import kotlinx.collections.immutable.persistentListOf
import kotlinx.collections.immutable.toImmutableList
import kotlin.math.roundToInt

private const val MIN_REACTIVE_VOLUME = 0.12f
private const val MIN_WAVE_VOLUME = 0.1f

@Composable
fun PrezelVoiceChromeWave(
modifier: Modifier = Modifier,
Expand All @@ -48,33 +49,23 @@ fun PrezelVoiceChromeWave(
VoiceChromeStatus.LISTENING,
VoiceChromeStatus.WAITING,
-> {
val clippedVolumes = volumes.map { volume ->
volume.coerceIn(
minimumValue = 0.1f,
maximumValue = 1f,
)
}
val clippedVolumes = volumes
.filter { volume -> volume > MIN_REACTIVE_VOLUME }
.map { volume ->
volume.coerceIn(
minimumValue = MIN_WAVE_VOLUME,
maximumValue = 1f,
)
}

clippedVolumes.toImmutableList()
}
}
val activationProgress by animateFloatAsState(
targetValue = if (status == VoiceChromeStatus.IDLE) 0f else 1f,
animationSpec = tween(durationMillis = 6400),
label = "VoiceChromeWaveActivationProgress",
)
val volumeProgress by animateFloatAsState(
targetValue = if (status == VoiceChromeStatus.IDLE) 0f else 1f,
animationSpec = tween(durationMillis = 440),
label = "VoiceChromeWaveVolumeProgress",
)

Spacer(
modifier = modifier.drawVoiceChromeWave(
status = status,
volumes = adjustedVolumes,
activationProgress = activationProgress,
volumeProgress = volumeProgress,
showBaseline = showBaseline,
),
)
Expand All @@ -84,8 +75,6 @@ fun PrezelVoiceChromeWave(
private fun Modifier.drawVoiceChromeWave(
status: VoiceChromeStatus,
volumes: ImmutableList<Float>,
activationProgress: Float,
volumeProgress: Float,
showBaseline: Boolean,
): Modifier {
val colors = PrezelTheme.colors
Expand Down Expand Up @@ -122,8 +111,6 @@ private fun Modifier.drawVoiceChromeWave(
status = status,
volumes = volumes,
config = drawConfig,
activationProgress = activationProgress,
volumeProgress = volumeProgress,
showBaseline = showBaseline,
baselineColor = colors.borderRegular,
)
Expand All @@ -147,35 +134,14 @@ private fun DrawScope.drawVoiceChromeWaveContent(
status: VoiceChromeStatus,
volumes: ImmutableList<Float>,
config: VoiceChromeWaveDrawConfig,
activationProgress: Float,
volumeProgress: Float,
showBaseline: Boolean,
baselineColor: Color,
) {
if (status == VoiceChromeStatus.LISTENING && activationProgress < 1f) {
drawVoiceChromeWaveBars(
status = VoiceChromeStatus.IDLE,
volumes = persistentListOf(),
config = config,
xOffset = -size.width * activationProgress,
volumeProgress = 0f,
)
drawVoiceChromeWaveBars(
status = VoiceChromeStatus.LISTENING,
volumes = volumes,
config = config,
xOffset = size.width * (1f - activationProgress),
volumeProgress = volumeProgress,
)
} else {
drawVoiceChromeWaveBars(
status = status,
volumes = volumes,
config = config,
xOffset = 0f,
volumeProgress = volumeProgress,
)
}
drawVoiceChromeWaveBars(
status = status,
volumes = volumes,
config = config,
)

drawVoiceChromeWaveBaseline(
visible = showBaseline,
Expand All @@ -188,10 +154,8 @@ private fun DrawScope.drawVoiceChromeWaveBars(
status: VoiceChromeStatus,
volumes: ImmutableList<Float>,
config: VoiceChromeWaveDrawConfig,
xOffset: Float,
volumeProgress: Float,
) {
var barX = -config.barWidth + xOffset
var barX = -config.barWidth
var barIndex = 0
val barCount = (size.width / config.barSpacing).roundToInt() + 1

Expand All @@ -200,10 +164,7 @@ private fun DrawScope.drawVoiceChromeWaveBars(
index = barIndex,
sampleCount = barCount,
)
val barHeight = config.volumeToBarHeight(
volume = volume,
progress = volumeProgress,
)
val barHeight = config.volumeToBarHeight(volume)
val barTop = (size.height - barHeight) / 2f
val topLeft = Offset(x = barX, y = barTop)
val barSize = Size(width = config.barWidth, height = barHeight)
Expand Down Expand Up @@ -251,25 +212,22 @@ private fun DrawScope.drawVoiceChromeWaveBaseline(
)
}

private fun VoiceChromeWaveDrawConfig.volumeToBarHeight(
volume: Float,
progress: Float,
): Float {
val volumeProgress = (volume - 0.1f) / (1f - 0.1f)
val targetHeight = minBarHeight + volumeProgress * (maxBarHeight - minBarHeight)
private fun VoiceChromeWaveDrawConfig.volumeToBarHeight(volume: Float): Float {
val volumeProgress = (volume - MIN_WAVE_VOLUME) / (1f - MIN_WAVE_VOLUME)

return minBarHeight + (targetHeight - minBarHeight) * progress
return minBarHeight + volumeProgress * (maxBarHeight - minBarHeight)
}

private fun ImmutableList<Float>.sampleVolume(
index: Int,
sampleCount: Int,
): Float {
if (isEmpty()) return 0.1f
if (size == 1 || sampleCount <= 1) return first()
if (isEmpty() || sampleCount <= 0) return MIN_WAVE_VOLUME

val firstVisibleIndex = (size - sampleCount).coerceAtLeast(0)
val sampleIndex = firstVisibleIndex + index

val sampleIndex = (index * (lastIndex.toFloat() / (sampleCount - 1))).roundToInt()
return get(sampleIndex.coerceIn(indices))
return getOrElse(sampleIndex) { MIN_WAVE_VOLUME }
}

@LargeDevicePreview
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,7 @@ data class PresentationSentenceAnalysisResponse(
@SerialName("subFeedback")
val subFeedback: String,
@SerialName("guideScript")
val guideScript: String = "",
val guideScript: String?,
@SerialName("accuracy")
val accuracy: Double,
@SerialName("startTimeMs")
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -170,7 +170,6 @@ private fun VoiceRecordingFeedback.toChromeUi(): VoiceRecordingChromeUi =
titleResId = R.string.feature_analysis_impl_voice_recording_ready_to_continue_feedback,
status = VoiceChromeStatus.LISTENING,
gradient = VoiceChromeGradient.NONE,
hideWaveform = true,
)

VoiceRecordingFeedback.SPEAK_LOUDER -> VoiceRecordingChromeUi(
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -67,6 +67,8 @@ private fun IdleRecordingButtonArea(
) {
PrezelButtonArea(
modifier = modifier,
showBackground = true,
showDivider = false,
mainButton = { buttonModifier ->
RecordingIconButton(
iconResId = recordingState.actionIconResId,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.Spacer
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.height
import androidx.compose.foundation.layout.heightIn
Expand Down Expand Up @@ -35,13 +36,15 @@ import androidx.compose.ui.text.style.TextAlign
import androidx.compose.ui.text.withStyle
import androidx.compose.ui.unit.dp
import com.team.prezel.core.audio.AudioSessionState
import com.team.prezel.core.audio.AudioSource
import com.team.prezel.core.designsystem.component.actions.button.PrezelIconButton
import com.team.prezel.core.designsystem.component.actions.button.config.ButtonHierarchy
import com.team.prezel.core.designsystem.component.actions.button.config.ButtonSize
import com.team.prezel.core.designsystem.component.actions.button.config.ButtonType
import com.team.prezel.core.designsystem.component.actions.button.config.PrezelButtonDefaults
import com.team.prezel.core.designsystem.component.voice.PrezelVoiceChromeWave
import com.team.prezel.core.designsystem.icon.PrezelIcons
import com.team.prezel.core.designsystem.preview.BasicPreview
import com.team.prezel.core.designsystem.theme.PrezelTheme
import com.team.prezel.core.ui.util.noRippleClickable
import com.team.prezel.feature.analysis.impl.R
Expand All @@ -65,15 +68,15 @@ internal fun VoiceRecordingContent(
modifier = modifier
.fillMaxWidth()
.background(PrezelTheme.colors.bgRegular)
.padding(vertical = PrezelTheme.spacing.V16),
.padding(vertical = PrezelTheme.spacing.V4),
horizontalAlignment = Alignment.CenterHorizontally,
) {
if (!recordingState.isCompleted || isScriptExpanded) {
VoiceRecordingScriptHeader(
isScriptExpanded = isScriptExpanded,
onToggleScriptExpanded = onToggleScriptExpanded,
)
Spacer(modifier = Modifier.height(PrezelTheme.spacing.V16))
Spacer(modifier = Modifier.height(PrezelTheme.spacing.V4))
}

VoiceRecordingScriptBody(
Expand Down Expand Up @@ -104,7 +107,7 @@ private fun VoiceRecordingScriptHeader(
Box(
modifier = Modifier
.fillMaxWidth()
.height(20.dp)
.height(48.dp)
.padding(horizontal = PrezelTheme.spacing.V20),
) {
Text(
Expand Down Expand Up @@ -240,6 +243,7 @@ private fun VoiceRecordingStatusArea(
modifier = Modifier.fillMaxWidth(),
)
}
Spacer(modifier = Modifier.height(PrezelTheme.spacing.V12))
}

@Composable
Expand All @@ -262,7 +266,7 @@ private fun ScriptZoomButton(
),
contentDescription = null,
modifier = Modifier.size(24.dp),
tint = PrezelTheme.colors.iconRegular,
tint = PrezelTheme.colors.iconDisabled,
)
}
}
Expand All @@ -275,14 +279,10 @@ private fun RecordingWaveform(
modifier: Modifier = Modifier,
) {
val playbackProgress = recordingState.playbackProgress()
val visibleVolumes = if (voiceChromeUi?.hideWaveform == true) {
persistentListOf()
} else {
recordingState.visibleRecordingVolumes(
recordingVolumes = recordingVolumes,
playbackProgress = playbackProgress,
)
}
val visibleVolumes = recordingState.visibleRecordingVolumes(
recordingVolumes = recordingVolumes,
playbackProgress = playbackProgress,
)

PrezelVoiceChromeWave(
status = voiceChromeUi?.status ?: recordingState.toVoiceChromeStatus(),
Expand Down Expand Up @@ -447,3 +447,58 @@ private fun RecordingRoundIconButton(
onClick = onClick,
)
}

@BasicPreview
@Composable
private fun VoiceRecordingContentIdlePreview() {
VoiceRecordingContentPreview(recordingState = AudioSessionState.Idle)
}

@BasicPreview
@Composable
private fun VoiceRecordingContentEmptyScriptPreview() {
VoiceRecordingContentPreview(
recordingState = AudioSessionState.Idle,
script = "",
useMinimumScriptHeight = true,
)
}

@BasicPreview
@Composable
private fun VoiceRecordingContentRecordingPreview() {
VoiceRecordingContentPreview(
recordingState = AudioSessionState.Recording(elapsedSeconds = 12),
)
}

@BasicPreview
@Composable
private fun VoiceRecordingContentCompletedPreview() {
VoiceRecordingContentPreview(
recordingState = AudioSessionState.ReadyToPlay(
source = AudioSource.RecordedFile(filePath = "preview.m4a"),
durationSeconds = 75,
),
)
}

@Composable
private fun VoiceRecordingContentPreview(
recordingState: AudioSessionState,
script: String = "한 번쯤 발표하면서 긴장하신 경험 있으시죠. 오늘도 다들 긴장되는 마음으로 오셨을 것 같습니다.",
useMinimumScriptHeight: Boolean = false,
) {
PrezelTheme {
VoiceRecordingContent(
script = script,
recordingState = recordingState,
recordingVolumes = persistentListOf(0.2f, 0.45f, 0.7f, 0.35f, 0.8f, 0.55f),
isScriptExpanded = false,
onToggleScriptExpanded = {},
onClickRecordingControl = {},
modifier = if (useMinimumScriptHeight) Modifier.fillMaxWidth() else Modifier.fillMaxSize(),
useMinimumScriptHeight = useMinimumScriptHeight,
)
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,6 @@ internal data class VoiceRecordingChromeUi(
@param:StringRes val titleResId: Int? = null,
val status: VoiceChromeStatus? = null,
val gradient: VoiceChromeGradient? = null,
val hideWaveform: Boolean = false,
)

internal val AudioSessionState.currentSeconds: Int
Expand Down
Loading