咬合校正

原文链接(日语) 作者:SHINTA P


声明

  • 咬合校正是 UTAU 计算音符长度时非常重要的环节,虽然 Ameya 大大没有公布实际的校正算法,但是通过其他前辈的文档以及我自己的一些推导,在做了大量实验比较后,比较好地还原了校正过程。

为什么需要校正

  • 如果一个音符的先行声音长度太大,并且它的前驱音符长度比较短,在这种情况下,理论上就会比前驱音符先发声。(换句话说,前驱音符直接被缩没了)

  • 例如,当曲速为360时(四分音符的时长为167毫秒),有两个连续的四分音符(歌词分别为“あ”与“い”),如果后者的先行发声(设重叠为0)为200毫秒,那么理论上前者就会被全部被挤占。

  • 为了防止出现这种奇怪的情况,在UTAU中,当一个音符的先行声音使前一个音符缩短了一半以上的长度时,为了让前一个音符能存活一半长度(即用ticks计算的毫秒数除以2),该音符的先行声音、重叠和STP会被调整。

  • 在示例情况下,它实际的先行声音会被调整为约83毫秒,使得前驱的音符能存活一半。

  • 我(自作主张)地把这种调整称为“咬合校正”。

校正过程

辅音速度

  • 指定先行声音和重叠部分的伸缩倍率,可以在0到200的范围内指定,并且100不会扩展或收缩
  • 辅音速度值越大,速度越快,辅音部分被缩得越短。辅音速度0在时间轴上是两倍,200是一半
  • 设 k 是实际伸缩倍率,v 是辅音速度,则 k = 2^(1 - v / 100)

初始变量值

  • 初始的先行声音与重叠从属性面板中读取,如果属性面板为空,则从原音设定读取
  • 初始 STP 从属性面板读取,如果属性面板为空,则为 0
  • 音符理论时长(ms) = 音长(Ticks)/ 480 * 60 / 音符曲速 * 1000
  • 伸缩倍率 = 2^(1 - 辅音速度 / 100)
  • 先行声音 = 初始先行声音 * 伸缩倍率
  • 重叠 = 初始重叠 * 伸缩倍率

最大先行长度

  • 如果前驱音符是一般音符,最大先行长度 = 前驱音符理论时长 / 2
  • 如果前驱音符是休止符,最大先行长度 = 前驱音符理论时长

咬合校正的启动条件

  • 先行声音 - 重叠 > 最大先行长度

咬合校正率(系数)

  • 咬合校正率 = 最大先行长度 /(先行声音-重叠)

校正后的变量值

  • 校正后的先行声音 = 先行声音 * 咬合校正率
  • 校正后的重叠 = 重叠 * 咬合校正率
  • 校正后的 STP = 先行声音 - 校正后的先行声音 + 初始 STP

音符实际长度与二次校正

  • 末端偏移 = 后继音符的重叠(校正后) - 后继音符的先行声音(校正后)
  • 如果后继音符设置了一个非常大的重叠值,或者其他原因,使得末端偏移超过后继音符的理论时长,那么就取后继音符的理论时长
  • 实际长度 = 理论时长 + 先行声音(校正后)+ 末端偏移

UTAU 中的 Bug

  • 在测试变速曲的时候发现了一个严重的问题,这一问题告诉插件开发者,UTAU 提供给插件的校正后的先行/重叠/STP是有问题的。

  • 我们知道,音符长度(毫秒)= 音长(ticks) / 480 * 60 / 曲速 * 1000

  • UTAU在UI中执行当前音符的咬合校正计算时,用的是当前音符的曲速去计算前驱音符的长度,但是在传给引擎参数时又正确地用了前驱音符的曲速

  • 也就是说,它显示在UI上的,与它传给插件的校正结果都是有问题的!(幸好传给引擎的是对的)

  • 有以下三种情况

    1. 在前驱音符曲速与当前音符的相等时,不会引起什么问题。
    2. 在前驱音符曲速小于当前音符的时,那就意味着UI中计算的前驱音符长度变短了,有可能原本不用启动校正但是却被校正了。
    3. 在前驱音符曲速大于当前音符的时,那就意味着UI中计算的前驱音符长度变长了,有可能UI中的校正力度可能不够大,导致UI中计算的校正后的先行声音照样能吞掉前驱音符一半长度(或者吞掉前驱休止符的整个长度,导致合成器接收到的音符长度是负的,造成严重错误)。
  • 建议:所以在插件里,如果要使用校正后的先行/重叠/STP,最好还是自己算,或者在检测到前驱音符与当前音符曲速不相等时不使用UTAU提供的校正结果。

  • 在UTAU中手动修改的STP会直接与在校正后的STP相加作为新的STP传给引擎,校正后的先行声音和重叠不受影响,不过最好还是不要去改它。

  • UTAU 在传递给引擎数据的 先行声音/重叠/STP 都是经过校正的。

简易C++校正算法

// 先去除首位空格,判断余下部分是否为空或r或R,如果是就为真
bool isRestNoteLyric();

// 首先从UST中获取当前音符与前驱音符的所有音符属性
GetValueFromSectionNote();

// 判断过程如下
double CorrectRate = 1;
double velocityRate = pow(2, 1 - CurVelocity / 100);

CurPreUttr *= velocityRate;
CurOverlap *= velocityRate;

if (hasPrevNote){
  double PrevDuration = double(PrevLength) / 480 * 60 / PrevTempo * 1000;
  double MaxOccupy = isRestNoteLyric(PrevLyric) ? PrevDuration : (PrevDuration / 2);
  if (aCurPreUttr - aCurOverlap > MaxOccupy) {
    CorrectRate = MaxOccupy / (CurPreUttr - CurOverlap);
  }
}

double CorrecetPreUttr = CorrectRate * CurPreUttr;
double CorrecetOverlap = CorrectRate * CurOverlap;
double CorrecetSTPoint = CurPreUttr - CorrecetPreUttr;

// 追加用户设置的STP  
CorrectSTPoint += CurSTPoint;