Skip to content

Commit

Permalink
Fix UTAU pitch conversion for multiple key points that are close to e…
Browse files Browse the repository at this point in the history
…ach other (#180)

* Fix UTAU/OpenUtau pitch import for very close pitch points

* Fix resampling during ustx pitch importing
  • Loading branch information
sdercolin authored Jul 29, 2024
1 parent 9cad76e commit 17de7cb
Show file tree
Hide file tree
Showing 3 changed files with 32 additions and 4 deletions.
24 changes: 21 additions & 3 deletions core/src/main/kotlin/core/process/pitch/OpenUtauPitchConversion.kt
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,10 @@ import kotlin.math.roundToInt

private const val SAMPLING_INTERVAL_TICK = 5L

// Not smaller than the sampling interval of the pitch data used in pitch generation targeting any format.
// This is used for placing two key points far enough to ensure they are not merged by the resampling process.
private const val SAFE_SAMPLING_INTERVAL_TICK = 5L

data class OpenUtauNotePitchData(
val points: List<Point>,
val vibrato: UtauNoteVibratoParams,
Expand Down Expand Up @@ -44,12 +48,17 @@ fun pitchFromUstxPart(notes: List<Note>, pitchData: OpenUtauPartPitchData, tempo
// Extract pitch points from notes
val notePointsList = mutableListOf<List<Pair<Long, Double>>>()
val tickTimeTransformer = TickTimeTransformer(tempos)
var lastKeyPos = -SAFE_SAMPLING_INTERVAL_TICK
for ((note, notePitch) in notes.zip(pitchData.notes)) {
val points = mutableListOf<Pair<Long, Double>>()
var lastPointShape = OpenUtauNotePitchData.Shape.EaseInOut
val noteStartInMillis = tickTimeTransformer.tickToMilliSec(note.tickOn)
val keyPointPositions = mutableListOf<Long>()
for (rawPoint in notePitch.points) {
val x = tickTimeTransformer.milliSecToTick(noteStartInMillis + rawPoint.x)
.coerceAtLeast(lastKeyPos + SAFE_SAMPLING_INTERVAL_TICK)
lastKeyPos = x
keyPointPositions.add(x)
val y = rawPoint.y / 10
val thisPoint = x to y
val lastPoint = points.lastOrNull()
Expand All @@ -67,7 +76,8 @@ fun pitchFromUstxPart(notes: List<Note>, pitchData: OpenUtauPartPitchData, tempo
val pointsInNoteWithVibrato = pointsIn
.appendUtauNoteVibrato(notePitch.vibrato, note, tickTimeTransformer, SAMPLING_INTERVAL_TICK)
val pointsWithVibrato = pointsBefore + pointsInNoteWithVibrato + pointsAfter
notePointsList.add(pointsWithVibrato.resampled(SAMPLING_INTERVAL_TICK))
val pointsResampled = pointsWithVibrato.resampled(SAMPLING_INTERVAL_TICK, keyPointPositions)
notePointsList.add(pointsResampled)
}

// Divide note with points into sections,
Expand Down Expand Up @@ -243,10 +253,18 @@ private fun MutableList<Pair<Long, Double>>.appendStartAndEndPoint(note: Note) {
}
}

private fun List<Pair<Long, Double>>.resampled(interval: Long): List<Pair<Long, Double>> =
private fun List<Pair<Long, Double>>.resampled(
interval: Long,
keyPointPositions: List<Long> = emptyList(),
): List<Pair<Long, Double>> =
groupBy { it.first / interval * interval }
.map { (mergedTick, points) ->
mergedTick to points.map { it.second }.average()
val keyPoint = points.find { it.first in keyPointPositions }
if (keyPoint != null) {
mergedTick to keyPoint.second
} else {
mergedTick to points.map { it.second }.average()
}
}
.sortedBy { it.first }
.fold(listOf()) { acc, point ->
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,11 @@ import core.util.runIf

private const val SAMPLING_INTERVAL_TICK = 4L

// Not smaller than the sampling interval of the pitch data used in pitch generation targeting any format.
// This is used for placing two key points far enough to ensure they are not merged by the resampling process.
// See also: core.process.pitch.OpenUtauPitchConversion.kt
private const val SAFE_SAMPLING_INTERVAL_TICK = 5L

data class UtauMode2TrackPitchData(
val notes: List<UtauMode2NotePitchData?>,
)
Expand Down Expand Up @@ -87,12 +92,15 @@ fun pitchFromUtauMode2Track(pitchData: UtauMode2TrackPitchData?, notes: List<Not
val pitchPoints = mutableListOf<Pair<Long, Double>>()
var lastNote: Note? = null
var pendingPitchPoints = listOf<Pair<Long, Double>>()
var lastKeyPos = -SAFE_SAMPLING_INTERVAL_TICK
for ((note, notePitch) in notePitches) {
val points = mutableListOf<Pair<Long, Double>>()
val noteStartInMillis = tickTimeTransformer.tickToMilliSec(note.tickOn)
if (notePitch?.start != null) {
var posInMillis = noteStartInMillis + notePitch.start
var tickPos = tickTimeTransformer.milliSecToTick(posInMillis)
.coerceAtLeast(lastKeyPos + SAFE_SAMPLING_INTERVAL_TICK)
lastKeyPos = tickPos
val startShift =
if (note.tickOn == lastNote?.tickOff) {
// always same value as the last note
Expand All @@ -108,6 +116,8 @@ fun pitchFromUtauMode2Track(pitchData: UtauMode2TrackPitchData?, notes: List<Not
val curveType = notePitch.curveTypes.getOrNull(index) ?: ""
posInMillis += width
tickPos = tickTimeTransformer.milliSecToTick(posInMillis)
.coerceAtLeast(lastKeyPos + SAFE_SAMPLING_INTERVAL_TICK)
lastKeyPos = tickPos
val thisPoint = tickPos to (shift / 10)
val lastPoint = points.last()
if (thisPoint.second != lastPoint.second) {
Expand Down
2 changes: 1 addition & 1 deletion src/jsMain/kotlin/Main.kt
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ import ui.strings.Language
import ui.strings.initializeI18n

const val APP_NAME = "UtaFormatix"
const val APP_VERSION = "3.22"
const val APP_VERSION = "3.22.1"

suspend fun main() {
initializeI18n(Language.English)
Expand Down

0 comments on commit 17de7cb

Please sign in to comment.