diff --git a/DiffSingerDeclarations.cs b/DiffSingerDeclarations.cs index fe5ad45..9c5764c 100644 --- a/DiffSingerDeclarations.cs +++ b/DiffSingerDeclarations.cs @@ -24,6 +24,7 @@ public static class DiffSingerDeclarations public const string KeyMouthOpening = "shift_mouth_opening"; // SHMC alpha:键取偏移语义,给将来绝对口型轨留 "mouth_opening" public const string KeyExpressiveness = "expressiveness"; // pitch 表现力(PEXP):仅 dspitch use_expr 时暴露 public const string KeyToneShift = "tone_shift"; // 音区偏移(SHFC):仅 pitch_controllable 声码器时暴露 + public const string KeyVarianceMode = "variance_param_mode"; // part 属性:variance 参数模式(delta=偏移 / absolute=绝对值) public const string KeySpeaker = "speaker"; // legacy:part 默认说话人下拉 public const string KeyLanguage = "language"; public const string KeyModel = "model"; // manifest:模型下拉(part 属性) @@ -64,12 +65,27 @@ public static int MixSlots(PropertyObject partProperties) // 听感音高不变(声码器吃原始 f0)、声学按偏移后的 f0 取音色——「用另一个音区的嗓子唱这个音」。 public const double ToneShiftBaseline = 0, ToneShiftMin = -12, ToneShiftMax = 12; + // variance 参数模式 + public const string VarianceModeDelta = "delta"; // 偏移值编辑(当前行为) + public const string VarianceModeAbsolute = "absolute"; // 绝对值编辑(dB 分段轨) + + // 声库面包装记录:将命名空间级 VarianceSpec(纯数学)与 VoicebankConfig Use/Predict 选择器组合。 + // 所有数值元数据与 Delta 公式委托给纯数学规格,此处仅追加 SDK 依赖的 Use/Predict。 public readonly record struct VarianceSpec( - string Key, string Display, string Color, - Func Use, Func Predict, - double EditMin, double EditMax, double Neutral, - double AcousticMin, double AcousticMax, - Func Delta); + global::DiffSingerForTuneLab.VarianceSpec Math, + Func Use, + Func Predict) + { + public string Key => Math.Key; + public string Display => Math.Display; + public string Color => Math.Color; + public double EditMin => Math.EditMin; + public double EditMax => Math.EditMax; + public double Neutral => Math.Neutral; + public double AcousticMin => Math.AcousticMin; + public double AcousticMax => Math.AcousticMax; + public Func Delta => Math.Delta; + } // 编辑轨(delta 语义)归一化到小数:energy/breath/tension 中性 0、量程 [-1,1];voicing 中性 1、量程 [0,1.25]。 // Delta(x=预测声学值, y=用户归一化值) 系数随之 ×100:y=1 等价旧 y=100(energy/breath ±12dB、tension ±5)。 @@ -81,14 +97,13 @@ public readonly record struct VarianceSpec( // 上行(y>1)线性 +48(y−1):满偏 1.25 = +12dB(与 energy 正向满偏对齐、避开 0dBFS 过载区); // 中性线两侧斜率相等(48)、过线无顿挫(仅二阶导跳变)。 // 预测 < −72dB 的帧(本就近静音)下行中段轻微下越 −96,由合成期 clamp 兜住。详见 schema 文档 §14.2。 + // absolute 模式不经过 Delta:已编辑帧直接采用用户声学目标,未编辑 NaN 帧跟随预测。 public static readonly VarianceSpec[] Variances = { - new("energy", "Energy", "#E573A5", c => c.UseEnergyEmbed, c => c.PredictEnergy, -1, 1, 0, -96, 0, (x, y) => x + y * 12), - new("breathiness", "Breathiness", "#73E5C2", c => c.UseBreathinessEmbed, c => c.PredictBreathiness, -1, 1, 0, -96, 0, (x, y) => x + y * 12), - new("voicing", "Voicing", "#C2E573", c => c.UseVoicingEmbed, c => c.PredictVoicing, 0, 1.25, 1, -96, 0, - (x, y) => y > 1 ? x + 48 * (y - 1) - : x - 48 * (1 - y) / (2 - y) - (x + 72) * MathF.Pow(1 - y, 12)), - new("tension", "Tension", "#A573E5", c => c.UseTensionEmbed, c => c.PredictTension, -1, 1, 0, -10, 10, (x, y) => x + y * 5), + new(VarianceMath.Variances[0], c => c.UseEnergyEmbed, c => c.PredictEnergy), + new(VarianceMath.Variances[1], c => c.UseBreathinessEmbed, c => c.PredictBreathiness), + new(VarianceMath.Variances[2], c => c.UseVoicingEmbed, c => c.PredictVoicing), + new(VarianceMath.Variances[3], c => c.UseTensionEmbed, c => c.PredictTension), }; // manifest retake 三位(legacy → 全 false ⇒ 不暴露任何 seed 轨)。 @@ -99,9 +114,12 @@ public static (bool Acoustic, bool Pitch, bool Variance) RetakeOf(ResolvedVoice public static bool HasPhonemeMix(PartContext pc) => pc.Resolved.Manifest?.PhonemeMix ?? false; // —— 自动化轨(可编辑曲线)= 固定轨 + 已启用的说话人混合轨 —— + // absoluteMode=true 时 variance 轨为分段绝对轨(dB),否则为连续 delta 轨(归一化偏移)。 + // 由 part 属性 variance_param_mode 驱动("delta" | "absolute"),默认 delta。 public static OrderedMap BuildAutomationConfigs(PartContext pc, PropertyObject partProperties) { - var map = BuildFixedAutomationConfigs(pc.Config, RetakeOf(pc.Resolved)); + bool absoluteMode = VarianceModeOf(partProperties) == VarianceModeAbsolute; + var map = BuildFixedAutomationConfigs(pc.Config, RetakeOf(pc.Resolved), absoluteMode); var set = SpeakerSet.Compute(pc.Resolved); foreach (var (suffix, display, color) in SelectedMixTracks(set, partProperties)) map.Add((KeyMixPrefix + suffix, display), Continuous(color, 0, 0, 1)); // 混合权重归一化 [0,1](0=不混入) @@ -113,14 +131,21 @@ public static OrderedMap BuildAutomationConfigs(P return map; } - // 固定轨(与 part 属性无关):variance(按声库能力)+ Gender/Speed + seed(按 retake gating)。 + // 固定轨(与 part 属性无关):variance(按声库能力 + 参数模式)+ Gender/Speed + seed(按 retake gating)。 + // absoluteMode=true 时 variance 轨切换为分段绝对轨(dB 量程,DefaultValue=NaN,未编辑=跟随预测), + // 否则为连续 delta 轨(归一化偏移量,DefaultValue=Neutral)。 public static OrderedMap BuildFixedAutomationConfigs( - VoicebankConfig config, (bool Acoustic, bool Pitch, bool Variance) retake) + VoicebankConfig config, (bool Acoustic, bool Pitch, bool Variance) retake, bool absoluteMode = false) { var map = new OrderedMap(); foreach (var v in Variances) if (v.Use(config)) - map.Add((v.Key, L.Tr(v.Display)), Continuous(v.Color, v.Neutral, v.EditMin, v.EditMax)); + { + AutomationConfig cfg = absoluteMode + ? AbsoluteCurve(v) // 分段绝对轨:dB 量程,未编辑 NaN = 跟随预测 + : Continuous(v.Color, v.Neutral, v.EditMin, v.EditMax); // 连续 delta 轨 + map.Add((v.Key, L.Tr(v.Display) + (absoluteMode ? " (" + L.Tr("Absolute") + ")" : "")), cfg); + } if (config.UseKeyShiftEmbed) map.Add((KeyGender, L.Tr("Gender")), Continuous("#E5A573", GenderBaseline, GenderMin, GenderMax)); @@ -163,15 +188,21 @@ public static OrderedMap BuildFixedAutomationConf } // 只读回显轨:仅当声学接受该量为输入且方差器能产基线时——显示实参(预测 + 用户 delta 合成后)。 - public static OrderedMap BuildReadbackConfigs(VoicebankConfig config) + // 绝对值模式下用户轨即实参,回显轨冗余,跳过。 + public static OrderedMap BuildReadbackConfigs(VoicebankConfig config, bool absoluteMode = false) { var map = new OrderedMap(); + if (absoluteMode) return map; // 绝对值模式下不暴露回显轨 foreach (var v in Variances) if (v.Use(config) && v.Predict(config)) map.Add((v.Key, L.Tr(v.Display)), Piecewise(v.Color, v.AcousticMin, v.AcousticMax)); return map; } + // 绝对值模式下读取回显轨配置时也需要传入 mode(会话构造期已知当前模式) + public static OrderedMap BuildReadbackConfigs(PropertyObject partProperties, VoicebankConfig config) + => BuildReadbackConfigs(config, VarianceModeOf(partProperties) == VarianceModeAbsolute); + // part 级面板:manifest 暴露 model/version 下拉;legacy 暴露 speaker 下拉;两者皆可有混音容器 + 默认语言。 public static ObjectConfig BuildPartConfig(PartContext pc, PropertyObject mergedProps) { @@ -208,6 +239,19 @@ public static ObjectConfig BuildPartConfig(PartContext pc, PropertyObject merged properties.Add((KeyMixSlots, L.Tr("Phoneme mix slots")), DraggableNumberBoxConfig.Integer(0).WithRange(0, MaxMixSlots)); + // variance 参数模式(仅声库有 variance 参数时暴露):delta = 偏移值编辑;absolute = 绝对声学值编辑 + // (energy/breathiness/voicing 为 dB,tension 为模型声学单位)。 + if (pc.Config.UseEnergyEmbed || pc.Config.UseBreathinessEmbed || pc.Config.UseVoicingEmbed || pc.Config.UseTensionEmbed) + { + properties.Add((KeyVarianceMode, L.Tr("Variance param mode")), ComboBoxConfig + .Create(new List + { + new(PropertyValue.Create(VarianceModeDelta), L.Tr("Variance param mode (delta)")), + new(PropertyValue.Create(VarianceModeAbsolute), L.Tr("Variance param mode (absolute)")), + }) + .WithDefault(PropertyValue.Create(VarianceModeDelta))); + } + return ObjectConfig.Create(properties); } @@ -409,6 +453,20 @@ static AutomationConfig Piecewise(string color, double min, double max) // 连续轨:WithDefault 给实数基线 → 连续;按需 WithRandomizable。 static AutomationConfig Continuous(string color, double baseline, double min, double max, bool randomizable = false) => AutomationConfig.Create(min, max).WithDefault(baseline).WithColor(color).WithRandomizable(randomizable); + + // 绝对模式分段轨:NaN 基线(未编辑 = 跟随预测)。energy/breathiness/voicing 以 dB 标注; + // tension 是模型声学值([-10,10]),不是 dB,保持纯数值标签。 + static AutomationConfig AbsoluteCurve(VarianceSpec v) + { + var config = Piecewise(v.Color, v.AcousticMin, v.AcousticMax); + return v.Key == "tension" + ? config + : config.WithMinLabel($"{v.AcousticMin:0} dB").WithMaxLabel($"{v.AcousticMax:0} dB"); + } + + // part 属性 variance_param_mode → 当前模式(缺省 delta) + public static string VarianceModeOf(PropertyObject partProperties) + => partProperties.GetString(KeyVarianceMode, VarianceModeDelta); } // 一个 part 当前的“可用说话人集合”:驱动默认说话人、混音候选/轨、嵌入解析。 diff --git a/DiffSingerSynthesisSession.cs b/DiffSingerSynthesisSession.cs index 75f0a4d..99c214c 100644 --- a/DiffSingerSynthesisSession.cs +++ b/DiffSingerSynthesisSession.cs @@ -22,16 +22,11 @@ public sealed class DiffSingerSynthesisSession : IVoiceSynthesisSession readonly IVoiceSynthesisContext mContext; readonly string mVoiceId; readonly Func mResolve; // (model/version 选择) → 解析到具体物理包能力集;运行时支持换 model/version - readonly VoicebankConfig? mConfig; // 构造期解析包(驱动固定轨订阅/回显轨集合);运行时 Render 按 part 属性另行解析 readonly DiffSingerModelCache mModelCache; readonly int mSamplingSteps; readonly bool mTensorCache; // 张量缓存总开关(引擎设置 tensor_cache) readonly int mCacheMaxSizeMb; // 缓存体积上限(MB);0 = 不限制(引擎设置 cache_max_size_mb) - // 运行时复用的声明派生物(每会话固定,构造期据声库能力集算一次): - // 可编辑轨集合(构造期订阅其区间编辑)+ 回显轨集合(产物 SynthesizedParameters 按其 key 聚合)。 - readonly OrderedMap mReadbackConfigs; - // —— 调度状态(数据线程;按 note 间隙分块,账本式托管失效与产物)—— readonly IActionEvent mNoteFieldModified; // 「任一 note 任一字段变」聚合事件(宿主 WhenAnyItem,成员增删自动接线/退订) // 已订阅 RangeModified 的全部自动化轨(key → automation)。固定轨(variance/gender/speed)、seed、混音轨**皆**随 @@ -52,11 +47,6 @@ public DiffSingerSynthesisSession(string voiceId, IVoiceSynthesisContext context mTensorCache = tensorCache; mCacheMaxSizeMb = cacheMaxSizeMb; - // 构造期解析一次(驱动固定轨订阅与回显轨集合);运行时 Render 按 part 属性另行解析、支持 model/version 切换。 - var pc = resolve(PropsOf(context.PartProperties)); - mConfig = pc?.Config; - mReadbackConfigs = pc != null ? BuildReadbackConfigs(pc.Config) : new OrderedMap(); - // 变更接线(handler 只做廉价标脏;重活延迟到 Committed 重分块)——见 §5.9。 // note 字段变更:宿主 WhenAnyItem 把「每个现有/未来成员的这几个属性事件」聚合成一个带成员标识的事件, // 成员增删时自动接线/退订(取代旧的手工 SubscribeNote/UnsubscribeNote 簿记)。 @@ -206,6 +196,7 @@ public async Task SynthesizeNext(double startTime, double endTime, CancellationT return null; var config = pc.Config; var resolved = pc.Resolved; + bool absoluteVarianceMode = VarianceModeOf(snapshot.PartProperties) == VarianceModeAbsolute; // 声学-声码器参数一致性校验(对齐 OpenUtau DiffSingerRenderer.InvokeDiffsinger 入口校验)。 // 仅在声码器存在时校验(无声码器会在后续加载时报错,此处不重复)。 @@ -436,9 +427,11 @@ void AddF(string name, float[] data, int[] dims) AddF("blend", blendFlat, new[] { mixRows, nFrames }); AddF("f0", acousticF0, new[] { 1, nFrames }); // 声学吃移调 f0(无音区偏移时即原始 f0 同引用) - // —— variance:预测 + 用户 delta 合成喂声学,同一份实参产回显 —— - // 用户曲线按帧求值(连续轨:未编辑处=中性基线 → Delta 恒得纯预测;编辑处 → 叠加),clamp 到声学值域。 - // 回显(Use && Predict)= 实参(预测 + 用户 delta、clamp 后)——与 pitch 回显同语义:所见即喂给声学的值。 + // —— variance:预测 + 用户曲线合成喂声学 —— + // delta 模式(默认):用户曲线为归一化偏移量,CombineVariance 用 Delta(x, y) 合成; + // absolute 模式:用户曲线为声学绝对值(分段轨,NaN=未编辑);已编辑帧直接采用用户目标, + // 不受 delta 编辑量程限制。 + // 回显轨仅在 delta 模式暴露(absolute 模式下用户轨即实参,回显冗余)。 // mulaw 声库(voicing_domain)三明治:预测解码成 dB 进公式、出公式编码回线上域喂声学; // 公式/回显/clamp 恒 dB 语义(见 VoicingDomainCodec 与 schema §14.2)。db 声库 codec=null、路径不变。 var voicingCodec = VoicingDomainCodec.For(config.VoicingDomain, config.VoicingMu); @@ -450,14 +443,19 @@ void AddF(string name, float[] data, int[] dims) ? auto.Evaluator.Evaluate(frameTimes) : null; var codec = spec.Key == "voicing" ? voicingCodec : null; - var combined = CombineVariance(spec, predicted, user, nFrames, codec); + + bool absolute = absoluteVarianceMode && spec.Use(config); + var combined = absolute + ? CombineVarianceAbsolute(spec.Math, predicted, user, nFrames, codec) + : CombineVariance(spec.Math, predicted, user, nFrames, codec); if (ac.HasInput(spec.Key)) AddF(spec.Key, codec == null ? combined : Array.ConvertAll(combined, codec.DbToWire), new[] { 1, nFrames }); - if (spec.Use(config) && spec.Predict(config) && predicted != null) - varReadback[spec.Key] = BuildReadbackSegment(spec, combined, frameTimes, nFrames); + // 回显轨仅在 delta 模式 + 有预测时产生(absolute 模式下用户分段轨即实参、已自绘) + if (!absolute && spec.Use(config) && spec.Predict(config) && predicted != null) + varReadback[spec.Key] = BuildReadbackSegment(spec.Math, combined, frameTimes, nFrames); } // —— gender / velocity:纯用户曲线(无方差器基线),钳到声明量程后按帧 convert 喂声学 @@ -674,6 +672,23 @@ static float[] CombineVariance(VarianceSpec spec, float[]? predicted, double[]? return result; } + // 绝对值模式:用户轨存声学绝对值(分段形,NaN = 未编辑 = 跟随预测)。 + // 已编辑帧直接采用用户目标,不经过 delta 量程;否则 voicing 等参数会出现不可达目标。 + // codec 非空(mulaw voicing)时先把预测从线上域解码为 dB;返回值恒为 dB,调用方再编码回线上域。 + static float[] CombineVarianceAbsolute(VarianceSpec spec, float[]? predicted, double[]? user, int n, + VoicingDomainCodec? codec = null) + { + var result = new float[n]; + for (int f = 0; f < n; f++) + { + float x = predicted == null ? 0f : (f < predicted.Length ? predicted[f] : predicted[^1]); + if (codec != null) x = codec.WireToDb(x); + double target = user != null && f < user.Length ? user[f] : double.NaN; + result[f] = VarianceMath.CombineAbsoluteFrame(spec, x, target); + } + return result; + } + // 纯用户曲线 → 帧级声学输入:按帧求值用户轨,钳到声明量程 [min,max] 后逐帧 convert // (无轨 / NaN 自由区 → 中性基线)。钳位理由与逐帧实现见 DiffSingerCurveInput。 static float[] BuildCurveInput(VoiceSynthesisSnapshot snapshot, string key, double neutral, @@ -695,6 +710,10 @@ static ResolveProps PropsOf(IReadOnlyNotifiablePropertyObject p) static string LiveString(IReadOnlyNotifiablePropertyObject p, string key) => p.GetValue(key, PropertyValue.Create(string.Empty)).ToString(out var s) ? s : string.Empty; + // 实时只读外观下的 variance 模式判定(快照侧用 Declarations.VarianceModeOf;此处键缺省 ⇒ "" ⇒ delta)。 + static bool AbsoluteModeOf(IReadOnlyNotifiablePropertyObject p) + => LiveString(p, KeyVarianceMode) == VarianceModeAbsolute; + static string? NullIfEmpty(string s) => string.IsNullOrEmpty(s) ? null : s; // 回显段:实参(预测 + 用户 delta 合成后),clamp 到声学值域,逐帧 (全局秒, 值)。 @@ -738,7 +757,10 @@ public IReadOnlyMap SynthesizedParameters get { var map = new Map(); - foreach (var kvp in mReadbackConfigs) + if (mResolve(PropsOf(mContext.PartProperties)) is not { } pc) + return map; + bool absoluteMode = AbsoluteModeOf(mContext.PartProperties); + foreach (var kvp in BuildReadbackConfigs(pc.Config, absoluteMode)) { var segments = new List>(); foreach (var piece in mPieces) @@ -915,7 +937,8 @@ void SyncAutomationSubscriptions() // 当前应订的轨集 = 固定轨(已 gating) ∪ 全量候选混音轨;混音候选里未选中的不会 live,下方按 live 过滤。 var desired = new HashSet(StringComparer.Ordinal); - foreach (var key in BuildFixedAutomationConfigs(pc.Config, RetakeOf(pc.Resolved)).Keys) + bool absoluteMode = AbsoluteModeOf(mContext.PartProperties); + foreach (var key in BuildFixedAutomationConfigs(pc.Config, RetakeOf(pc.Resolved), absoluteMode).Keys) desired.Add(key.Id); foreach (var (key, _) in MixTrackKeys(pc.Resolved)) desired.Add(key); @@ -930,13 +953,22 @@ void SyncAutomationSubscriptions() automation.RangeModified.Subscribe(OnRangeModified); mSubscriptions[key] = automation; } - // 退订:已订阅但不再应有(gating 掉)、或宿主已撤下该轨(混音被取消选择)的。 + // 退订 / 刷新:已订阅但不再应有(gating 掉)、或宿主已撤下该轨、或同一 key 的 automation 对象已更换 + // (如 continuous→piecewise 切换时数据容器变更,对象引用不同但仍需 RangeModified 通知)。 foreach (var key in mSubscriptions.Keys.ToList()) - if (!desired.Contains(key) || !mContext.Automations.ContainsKey(key)) + { + if (!desired.Contains(key) || !mContext.Automations.TryGetValue(key, out var currentAuto)) { mSubscriptions[key].RangeModified.Unsubscribe(OnRangeModified); mSubscriptions.Remove(key); } + else if (!ReferenceEquals(mSubscriptions[key], currentAuto)) + { + mSubscriptions[key].RangeModified.Unsubscribe(OnRangeModified); + currentAuto.RangeModified.Subscribe(OnRangeModified); + mSubscriptions[key] = currentAuto; + } + } } void OnCommitted() diff --git a/DiffSingerVarianceMath.cs b/DiffSingerVarianceMath.cs new file mode 100644 index 0000000..0bf4faa --- /dev/null +++ b/DiffSingerVarianceMath.cs @@ -0,0 +1,36 @@ +using System; + +namespace DiffSingerForTuneLab; + +// Variance 参数的纯数学定义(零外部依赖):delta 公式、声学量程和 absolute 逐帧组合。 +// 供单测直接引用(无需 TuneLab SDK);引擎代码 via DiffSingerDeclarations 引用同一份定义。 + +// 一个 variance 参数的规格:Delta(x=预测声学值, y=用户归一化值) → 绝对声学值。 +public readonly record struct VarianceSpec( + string Key, string Display, string Color, + double EditMin, double EditMax, double Neutral, + double AcousticMin, double AcousticMax, + Func Delta); + +// 四个 variance 参数的数学定义(与 DiffSingerDeclarations.Variances 同源,此处不含 SDK 依赖)。 +public static class VarianceMath +{ + public static readonly VarianceSpec[] Variances = + { + new("energy", "Energy", "#E573A5", -1, 1, 0, -96, 0, + (x, y) => x + y * 12), + new("breathiness", "Breathiness", "#73E5C2", -1, 1, 0, -96, 0, + (x, y) => x + y * 12), + new("voicing", "Voicing", "#C2E573", 0, 1.25, 1, -96, 0, + (x, y) => y > 1 ? x + 48 * (y - 1) + : x - 48 * (1 - y) / (2 - y) - (x + 72) * MathF.Pow(1 - y, 12)), + new("tension", "Tension", "#A573E5", -1, 1, 0, -10, 10, + (x, y) => x + y * 5), + }; + + // Absolute 逐帧语义:NaN 表示未编辑,跟随预测;实数表示最终声学目标。 + // 两种路径都钳到参数声学量程,且不受 delta 编辑量程限制。 + public static float CombineAbsoluteFrame(VarianceSpec spec, float predicted, double target) + => (float)Math.Clamp(double.IsNaN(target) ? predicted : target, + spec.AcousticMin, spec.AcousticMax); +} diff --git a/DiffSingerVoiceEngine.cs b/DiffSingerVoiceEngine.cs index e0f3096..198289f 100644 --- a/DiffSingerVoiceEngine.cs +++ b/DiffSingerVoiceEngine.cs @@ -69,8 +69,11 @@ public IReadOnlyOrderedMap GetAutomationConfigs(I } public IReadOnlyOrderedMap GetSynthesizedParameterConfigs(IVoiceSynthesisPartPropertyContext context) - => CommonItems(SelectedContexts(context).Select(pc => - (IReadOnlyOrderedMap)DiffSingerDeclarations.BuildReadbackConfigs(pc.Config))); + { + var merged = context.Parts.Select(p => p.PartProperties).Merge(); + return CommonItems(SelectedContexts(context).Select(pc => + (IReadOnlyOrderedMap)DiffSingerDeclarations.BuildReadbackConfigs(merged, pc.Config))); + } public ObjectConfig GetPartPropertyConfig(IVoiceSynthesisPartPropertyContext context) { diff --git a/Localization.cs b/Localization.cs index af2eec7..d67c215 100644 --- a/Localization.cs +++ b/Localization.cs @@ -46,6 +46,10 @@ internal static class L ["Pitch seed"] = "音高种子", ["Variance seed"] = "参数种子", ["Timbre seed"] = "音色种子", + ["Variance param mode"] = "参数模式", + ["Variance param mode (delta)"] = "偏移值", + ["Variance param mode (absolute)"] = "绝对值", + ["Absolute"] = "绝对值", ["Synthesizing"] = "合成中", }, }; diff --git a/docs/tunelab-voicebank-schema.md b/docs/tunelab-voicebank-schema.md index 16138c1..0fd9900 100644 --- a/docs/tunelab-voicebank-schema.md +++ b/docs/tunelab-voicebank-schema.md @@ -450,4 +450,33 @@ variance 回显轨(`SynthesizedParameters`)是**实参(预测 + 用户 del 同样按 key 静默复用(值为字符串): - `language`:**建议**声库制作方用 ISO 639 / BCP-47 码(`zh`/`ja`/`en`)。插件作为桥**无法强制**,只能在 schema 建议 +(可选)加载时对非 ISO 形态打 warning。**不做**别名映射表(那是自造标准)。 -- `model` / `version` / `speaker`:受 ComboBox options 校验,查无回落默认 sentinel(`""`)——这类**受校验离散值天然安全**,不会因复用产生脏值。 \ No newline at end of file +- `model` / `version` / `speaker`:受 ComboBox options 校验,查无回落默认 sentinel(`""`)——这类**受校验离散值天然安全**,不会因复用产生脏值。 + +### 14.6 variance 参数模式(delta / absolute) + +variance 参数(energy / breathiness / voicing / tension)有两种编辑模式,由 part 属性 `variance_param_mode` 控制: + +- **`delta`**(默认):连续轨,归一化偏移量(energy/breathiness/tension `[-1,1]`,voicing `[0,1.25]`)。合成期 `Delta(predicted, userValue)` → 绝对声学值。 +- **`absolute`**:分段轨,使用回显轨相同的真实声学单位与范围(energy/breathiness/voicing 为 `[-96, 0]` dB,tension 为 `[-10, 10]`)。未编辑帧 `NaN` = 跟随模型预测值;已编辑帧存用户画的最终声学目标。 + +两种模式的数据**互不换算、互不迁移**: +- 切到 absolute 时,delta 连续轨数据保留在 `Automations` 容器中(孤儿数据,隐藏不删),absolute 分段轨从空开始画。 +- 切回 delta 时,absolute 分段轨数据保留在 `PiecewiseAutomations` 容器中(孤儿数据),delta 连续轨恢复为切换前的原样。 +- 这与 ACE Studio 的「包络/实参」选项卡单向数据流一致:delta → absolute 显示跟随(已有 readback 机制),absolute → delta 不反向传播。 + +**合成语义**:absolute 轨表示最终声学目标,不经过 delta 逆运算,也不受 delta 编辑量程限制。已编辑帧直接使用用户目标并钳到该参数的声学范围;未编辑的 `NaN` 帧使用模型预测值。对 `voicing_domain: mulaw` 声库,预测值先从线上域解码为 dB,完成上述组合后再编码回模型输入域。因此 `predicted=-40 dB, target=0 dB` 的最终目标就是 `0 dB`,不会被 delta 模式 `1.25` 的编辑上限截断。 + +**回显轨裁剪**:absolute 模式下用户轨本身就是实参(已含预测 + 编辑),`SynthesizedParameters` 对该参数不再暴露只读回显轨(避免两条轨同值重复)。 + +### 14.7 TuneLab 2.0 宿主要求 + +本功能依赖 TuneLab 的 voice synthesis 链能够消费 piecewise automation。`release/2.0.0`(提交 `2c21ab94843a50d8f2dccaa0f403f52f2aef7d47`)已经实现分段轨的编辑、显示、序列化与 `NaN` 段间语义,但其 `tests/PIECEWISE-AUTOMATION-TRACKS-TEST-CASES.md` 明确把“voice 分段轨参与合成”列为后续需求。**未应用下述宿主改动时,absolute 轨可以编辑和保存,但不会被可靠送入实际 voice 合成;因此该模式不能视为对原版 TuneLab 2.0 开箱即用。** + +TuneLab 2.0 需要补齐的宿主内部接线如下;不需要修改冻结的 `TuneLab.SDK` / `TuneLab.Foundation` 公共 ABI: + +1. `TuneLab/Data/Synthesis/VoiceSynthesisSnapshotFactory.cs`:按 `AutomationConfig.IsPiecewise` 选择 `MidiPart.PiecewiseAutomations` 或 `MidiPart.Automations`;piecewise 不存在时提供 `NaN` evaluator,并参考 `EffectSynthesisSnapshotFactory` 的现有实现,不叠加 continuous automation vibrato。 +2. `TuneLab/Data/Synthesis/VoiceSynthesisContext.cs`:同时 wire `PiecewiseAutomations` 的现有项及 `ItemAdded` / `ItemRemoved`,订阅每条分段轨的 `RangeModified`,使绘制后能标记对应合成区间失效。 +3. `TuneLab/Data/MidiPart.cs`(或等价的 host-internal evaluator 路径):live synthesis evaluator 必须按当前声明的 `IsPiecewise` 从正确容器求值;分段轨未覆盖区域保持 `NaN`,不能回退读取同 key 的旧连续 delta 轨。 +4. 模式切换后,同 key 从 continuous 变为 piecewise(或反向)时,voice context 必须刷新对应 evaluator/订阅对象,避免继续持有旧容器。 + +上述要求来自 TuneLab `release/2.0.0` 的 `docs/voice-sdk-design.md`(`DefaultValue=NaN` 表示 piecewise、段间求值为 `NaN`、`RangeModified` 为增量失效契约)以及官方 piecewise 测试用例文档。 \ No newline at end of file diff --git a/tests/DiffSinger.Tests/DiffSinger.Tests.csproj b/tests/DiffSinger.Tests/DiffSinger.Tests.csproj index 1d31512..3592f0b 100644 --- a/tests/DiffSinger.Tests/DiffSinger.Tests.csproj +++ b/tests/DiffSinger.Tests/DiffSinger.Tests.csproj @@ -14,12 +14,13 @@ - + diff --git a/tests/DiffSinger.Tests/VarianceAbsoluteTests.cs b/tests/DiffSinger.Tests/VarianceAbsoluteTests.cs new file mode 100644 index 0000000..7bc6c11 --- /dev/null +++ b/tests/DiffSinger.Tests/VarianceAbsoluteTests.cs @@ -0,0 +1,80 @@ +using System; +using DiffSingerForTuneLab; +using Xunit; + +namespace DiffSinger.Tests; + +// Absolute variance 的产品语义回归:实数是最终声学目标,NaN 才表示跟随预测。 +public class VarianceAbsoluteTests +{ + [Fact] + public void Voicing_FullRangeTarget_IsAppliedDirectly() + { + var spec = VarianceMath.Variances[2]; + + float result = VarianceMath.CombineAbsoluteFrame(spec, predicted: -40f, target: 0d); + + Assert.Equal(0f, result); + } + + [Theory] + [InlineData(0, -40f)] + [InlineData(1, -40f)] + [InlineData(2, -40f)] + [InlineData(3, 2f)] + public void NaN_FollowsPrediction(int specIndex, float predicted) + { + var spec = VarianceMath.Variances[specIndex]; + + float result = VarianceMath.CombineAbsoluteFrame(spec, predicted, double.NaN); + + Assert.Equal(predicted, result); + } + + [Theory] + [InlineData(0, -120d, -96f)] + [InlineData(0, 12d, 0f)] + [InlineData(1, -120d, -96f)] + [InlineData(1, 12d, 0f)] + [InlineData(2, -120d, -96f)] + [InlineData(2, 12d, 0f)] + [InlineData(3, -20d, -10f)] + [InlineData(3, 20d, 10f)] + public void EditedTarget_IsClampedToAcousticRange(int specIndex, double target, float expected) + { + var spec = VarianceMath.Variances[specIndex]; + + float result = VarianceMath.CombineAbsoluteFrame(spec, predicted: -40f, target); + + Assert.Equal(expected, result); + } + + [Theory] + [InlineData(0, -72d)] + [InlineData(1, -18.5d)] + [InlineData(2, -3d)] + [InlineData(3, 7.25d)] + public void EditedTarget_DoesNotDependOnPredictionOrDeltaRange(int specIndex, double target) + { + var spec = VarianceMath.Variances[specIndex]; + + float fromLowPrediction = VarianceMath.CombineAbsoluteFrame(spec, -90f, target); + float fromHighPrediction = VarianceMath.CombineAbsoluteFrame(spec, 10f, target); + + Assert.Equal((float)target, fromLowPrediction); + Assert.Equal((float)target, fromHighPrediction); + } + + [Theory] + [InlineData(0, -120f, -96f)] + [InlineData(2, 12f, 0f)] + [InlineData(3, 20f, 10f)] + public void NaNPrediction_IsClampedToAcousticRange(int specIndex, float predicted, float expected) + { + var spec = VarianceMath.Variances[specIndex]; + + float result = VarianceMath.CombineAbsoluteFrame(spec, predicted, double.NaN); + + Assert.Equal(expected, result); + } +}