Skip to content

Commit

Permalink
Implement phonemes conversion
Browse files Browse the repository at this point in the history
  • Loading branch information
sdercolin committed Feb 11, 2024
1 parent 544a4bf commit 1f78420
Show file tree
Hide file tree
Showing 9 changed files with 357 additions and 13 deletions.
3 changes: 2 additions & 1 deletion src/jsMain/kotlin/io/Svp.kt
Original file line number Diff line number Diff line change
Expand Up @@ -108,6 +108,7 @@ object Svp {
tickOn = tickOn,
tickOff = tickOn + note.duration / TICK_RATE,
lyric = note.lyrics.takeUnless { it.isNullOrBlank() } ?: DEFAULT_LYRIC,
phoneme = note.phonemes,
)
}

Expand Down Expand Up @@ -250,7 +251,7 @@ object Svp {
onset = it.tickOn * TICK_RATE,
duration = it.length * TICK_RATE,
lyrics = it.lyric,
phonemes = "",
phonemes = it.phoneme ?: "",
pitch = it.key,
attributes = Attributes(),
)
Expand Down
20 changes: 18 additions & 2 deletions src/jsMain/kotlin/io/Ustx.kt
Original file line number Diff line number Diff line change
Expand Up @@ -94,10 +94,20 @@ object Ustx {
val track = trackMap[trackId] ?: continue
val tickPrefix = voicePart.position
val notes = voicePart.notes.map {
val rawLyrics = it.lyric
// some phonemizers use `lyrics [phonemes]` format
val regex = Regex("\\s\\[([^\\[\\]]*)\\]$")
val match = regex.find(rawLyrics)
val (lyric, phoneme) = if (match == null) {
rawLyrics to null
} else {
rawLyrics.take(match.range.first) to rawLyrics.substring(match.range).trim().trim('[', ']')
}
Note(
id = 0,
key = it.tone,
lyric = it.lyric,
lyric = lyric,
phoneme = phoneme,
tickOn = it.position + tickPrefix,
tickOff = it.position + it.duration + tickPrefix,
)
Expand Down Expand Up @@ -265,12 +275,18 @@ object Ustx {
if (index == 0) datum.copy(y = firstPitchPointValue) else datum.copy()
}
val pitch = template.pitch.copy(data = pitchPoints)
val lyric = buildString {
append(thisNote.lyric)
if (thisNote.phoneme?.isNotBlank() == true) {
append(" [${thisNote.phoneme}]")
}
}
return Note(
position = thisNote.tickOn,
duration = thisNote.length,
tone = thisNote.key,
pitch = pitch,
lyric = thisNote.lyric,
lyric = lyric,
vibrato = template.vibrato.copy(),
)
}
Expand Down
5 changes: 5 additions & 0 deletions src/jsMain/kotlin/model/Feature.kt
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,11 @@ enum class Feature(val isAvailable: (Project) -> Boolean) {
),
SplitProject(
isAvailable = { true },
),
ConvertPhonemes(
isAvailable = { project ->
project.tracks.any { track -> track.notes.any { note -> note.phoneme != null } }
},
)
}

Expand Down
14 changes: 9 additions & 5 deletions src/jsMain/kotlin/model/Format.kt
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
package model

import io.VsqLike
import model.Feature.ConvertPhonemes
import model.Feature.ConvertPitch
import model.Feature.SplitProject
import model.JapaneseLyricsType.KanaCv
Expand Down Expand Up @@ -31,7 +32,7 @@ enum class Format(
io.Vsqx.generate(project, features)
},
possibleLyricsTypes = listOf(RomajiCv, KanaCv),
availableFeaturesForGeneration = listOf(ConvertPitch),
availableFeaturesForGeneration = listOf(ConvertPitch, ConvertPhonemes),
),
Vpr(
"vpr",
Expand All @@ -42,7 +43,7 @@ enum class Format(
io.Vpr.generate(project, features)
},
possibleLyricsTypes = listOf(RomajiCv, KanaCv),
availableFeaturesForGeneration = listOf(ConvertPitch),
availableFeaturesForGeneration = listOf(ConvertPitch, ConvertPhonemes),
),
Ust(
"ust",
Expand All @@ -66,7 +67,7 @@ enum class Format(
io.Ustx.generate(project, features)
},
possibleLyricsTypes = listOf(RomajiCv, RomajiVcv, KanaCv, KanaVcv),
availableFeaturesForGeneration = listOf(ConvertPitch),
availableFeaturesForGeneration = listOf(ConvertPitch, ConvertPhonemes),
),
Ccs(
"ccs",
Expand All @@ -88,7 +89,7 @@ enum class Format(
io.Svp.generate(project, features)
},
possibleLyricsTypes = listOf(RomajiCv, KanaCv),
availableFeaturesForGeneration = listOf(ConvertPitch, SplitProject),
availableFeaturesForGeneration = listOf(ConvertPitch, SplitProject, ConvertPhonemes),
),
S5p(
"s5p",
Expand Down Expand Up @@ -181,7 +182,7 @@ enum class Format(
io.UfData.generate(project, features)
},
possibleLyricsTypes = listOf(RomajiCv, RomajiVcv, KanaCv, KanaVcv),
availableFeaturesForGeneration = listOf(ConvertPitch),
availableFeaturesForGeneration = listOf(ConvertPitch, ConvertPhonemes),
);

private val allExtensions get() = listOf(extension) + otherExtensions
Expand Down Expand Up @@ -235,5 +236,8 @@ enum class Format(
StandardMid,
UfData,
)

val vocaloidFormats: List<Format>
get() = listOf(Vsq, Vsqx, VocaloidMid, Vpr)
}
}
2 changes: 1 addition & 1 deletion src/jsMain/kotlin/process/lyrics/LyricsMapping.kt
Original file line number Diff line number Diff line change
Expand Up @@ -44,7 +44,7 @@ fun Project.mapLyrics(request: LyricsMappingRequest) = copy(
tracks = tracks.map { it.replaceLyrics(request) },
)

fun Track.replaceLyrics(request: LyricsMappingRequest) = copy(
private fun Track.replaceLyrics(request: LyricsMappingRequest) = copy(
notes = notes.mapNotNull { note -> note.replaceLyrics(request).takeIf { it.lyric.isNotEmpty() } }
.validateNotes(),
)
Expand Down
9 changes: 5 additions & 4 deletions src/jsMain/kotlin/process/phonemes/PhonemesMapping.kt
Original file line number Diff line number Diff line change
Expand Up @@ -33,17 +33,18 @@ data class PhonemesMappingRequest(
}
}

fun Project.mapPhonemes(request: PhonemesMappingRequest) = copy(
fun Project.mapPhonemes(request: PhonemesMappingRequest?) = copy(
tracks = tracks.map { it.replacePhonemes(request) },
)

fun Track.replacePhonemes(request: PhonemesMappingRequest) = copy(
notes = notes.mapNotNull { note -> note.replacePhonemes(request).takeIf { it.lyric.isNotEmpty() } }
fun Track.replacePhonemes(request: PhonemesMappingRequest?) = copy(
notes = notes.mapNotNull { note -> note.replacePhonemes(request) }
.validateNotes(),
)

fun Note.replacePhonemes(request: PhonemesMappingRequest): Note {
fun Note.replacePhonemes(request: PhonemesMappingRequest?): Note {
val input = phoneme?.split(" ") ?: return this
if (request == null) return copy(phoneme = null)
val output = mutableListOf<String>()
var pos = 0
while (pos <= input.lastIndex) {
Expand Down
67 changes: 67 additions & 0 deletions src/jsMain/kotlin/ui/ConfigurationEditor.kt
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,8 @@ import process.lyrics.chinese.convertChineseLyricsToPinyin
import process.lyrics.japanese.convertJapaneseLyrics
import process.lyrics.mapLyrics
import process.lyrics.replaceLyrics
import process.phonemes.PhonemesMappingRequest
import process.phonemes.mapPhonemes
import process.projectZoomFactorOptions
import process.zoom
import react.ChildrenBuilder
Expand All @@ -50,6 +52,7 @@ import ui.configuration.ChinesePinyinConversionBlock
import ui.configuration.JapaneseLyricsConversionBlock
import ui.configuration.LyricsMappingBlock
import ui.configuration.LyricsReplacementBlock
import ui.configuration.PhonemesConversionBlock
import ui.configuration.PitchConversionBlock
import ui.configuration.ProjectSplitBlock
import ui.configuration.ProjectZoomBlock
Expand Down Expand Up @@ -170,6 +173,31 @@ val ConfigurationEditor = scopedFC<ConfigurationEditorProps> { props, scope ->
?: FeatureConfig.SplitProject.getDefault(props.outputFormat).maxTrackCount.toString(),
)
}
val (phonemesConversion, setPhonemesConversion) = useState {
currentConfigs?.phonemesConversion?.let {
return@useState it
}

val hasPhonemes = props.projects.any { Feature.ConvertPhonemes.isAvailable(it) }
val isAvailable = hasPhonemes &&
props.outputFormat.availableFeaturesForGeneration.contains(Feature.ConvertPhonemes)
// Enable for VOCALOID -> VOCALOID or UfData by default
val enabledFormats = Format.vocaloidFormats.plus(Format.UfData)
val isOn = isAvailable && (
(
props.projects.all { it.format in enabledFormats } &&
props.outputFormat in enabledFormats
) ||
props.projects.all { it.format == props.outputFormat }
)
PhonemesConversionState(
isAvailable = isAvailable,
isOn = isOn,
useMapping = false,
mappingPresetName = null,
mappingRequest = PhonemesMappingRequest(),
)
}
var dialogError by useState(DialogErrorState())

fun isReady() = listOf(
Expand Down Expand Up @@ -206,6 +234,10 @@ val ConfigurationEditor = scopedFC<ConfigurationEditorProps> { props, scope ->
initialState = lyricsMapping
submitState = setLyricsMapping
}
if (phonemesConversion.isAvailable) PhonemesConversionBlock {
initialState = phonemesConversion
submitState = setPhonemesConversion
}
SlightRestsFillingBlock {
initialState = slightRestsFilling
submitState = setSlightRestsFilling
Expand Down Expand Up @@ -235,6 +267,7 @@ val ConfigurationEditor = scopedFC<ConfigurationEditorProps> { props, scope ->
pitchConversion,
projectZoom,
projectSplit,
phonemesConversion,
setProgress = { progress = it },
onDialogError = { dialogError = it },
)
Expand All @@ -261,6 +294,7 @@ private fun ChildrenBuilder.buildNextButton(
pitchConversion: PitchConversionState,
projectZoom: ProjectZoomState,
projectSplit: ProjectSplitState,
phonemesConversion: PhonemesConversionState,
setProgress: (ProgressProps) -> Unit,
onDialogError: (DialogErrorState) -> Unit,
) {
Expand All @@ -284,6 +318,7 @@ private fun ChildrenBuilder.buildNextButton(
pitchConversion,
projectZoom,
projectSplit,
phonemesConversion,
setProgress,
onDialogError,
)
Expand All @@ -304,6 +339,7 @@ private fun process(
pitchConversion: PitchConversionState,
projectZoom: ProjectZoomState,
projectSplit: ProjectSplitState,
phonemesConversion: PhonemesConversionState,
setProgress: (ProgressProps) -> Unit,
onDialogError: (DialogErrorState) -> Unit,
) {
Expand Down Expand Up @@ -335,6 +371,7 @@ private fun process(
.runIf(projectZoom.isOn) {
zoom(projectZoom.factorValue)
}
.mapPhonemes(phonemesConversion.resolvedMappingRequest)

val featureConfigs = buildList {
if (pitchConversion.isOn) add(FeatureConfig.ConvertPitch)
Expand All @@ -358,6 +395,7 @@ private fun process(
pitchConversion,
projectZoom,
projectSplit,
phonemesConversion,
)
window.localStorage.setItem("currentConfigs", json.encodeToString(currentConfigs))

Expand Down Expand Up @@ -453,6 +491,34 @@ data class LyricsMappingState(
}
}

@Serializable
data class PhonemesConversionState(
val isAvailable: Boolean,
val isOn: Boolean,
val useMapping: Boolean,
val mappingPresetName: String?,
val mappingRequest: PhonemesMappingRequest,
) : SubState() {
override val isReady: Boolean get() = if (isAvailable && isOn && useMapping) mappingRequest.isValid else true

val resolvedMappingRequest: PhonemesMappingRequest?
get() = if (isOn) {
if (useMapping) {
mappingRequest
} else {
PhonemesMappingRequest("")
}
} else {
null
}

fun updatePresetName() = when {
mappingPresetName == null -> this
PhonemesMappingRequest.findPreset(mappingPresetName) == mappingRequest -> this
else -> copy(mappingPresetName = null)
}
}

@Serializable
data class SlightRestsFillingState(
val isOn: Boolean,
Expand Down Expand Up @@ -502,4 +568,5 @@ data class Configs(
val pitchConversion: PitchConversionState,
val projectZoom: ProjectZoomState,
val projectSplit: ProjectSplitState,
val phonemesConversion: PhonemesConversionState,
)
Loading

0 comments on commit 1f78420

Please sign in to comment.