- 桌面应用
- 音频
【免费下载链接】OpenUtau
Open singing synthesis platform / Open source UTAU successor
本文围绕 OpenUtau 钢琴卷帘(Piano Roll)中的批量编辑系统展开,讲解官方在 OpenUtau.Core/Editing/README.md 中定义的批量编辑开发规范:如何以StartUndoGroup/ExecuteCmd/EndUndoGroup组织一次原子化的多步编辑,如何选择单音符或多音符命令变体,以及如何为编辑提供本地化名称。读完本文后,你将能够基于 BatchEdit.cs 接口自主编写可撤销、可本地化的批量编辑宏,并理解 DocManager.cs 中撤销组(Undo Group)的底层实现,以及 OpenUtau 内置的音符、歌词、重置三类批量编辑各自的工作原理。
批量编辑系统在 OpenUtau 中的定位
OpenUtau 的钢琴卷帘界面提供了大量针对选中音符的自动化操作,例如自动连音(Auto Legato)、量化对齐、添加换气音符、汉字转拼音、随机化时值等。这些操作并非散落在各个 UI 回调中,而是统一收敛为实现了BatchEdit接口的“编辑宏(Editing Macro)”对象,再由 PianoRoll.axaml.cs 在初始化时集中注册为菜单项。
从源码结构看,UI 层维护了四个独立的菜单集合(见 PianoRoll.axaml.cs 与 PianoRollViewModel.cs):
NoteBatchEdits:音符类批量编辑(量化、连音、变调、修复重叠等);LyricBatchEdits:歌词类批量编辑(罗马音转平假名、去音调后缀、插入滑音等);ResetBatchEdits:重置类批量编辑(重置弯音、表达式、颤音、时值、别名等);ExternalBatchEdits:由外部插件程序集动态发现并加载的批量编辑。
因此,任何希望进入 OpenUtau 菜单的批量编辑,最终都必须实现BatchEdit接口,这是整个系统的统一契约。
BatchEdit 接口:批量编辑的统一契约
接口定义位于 BatchEdit.cs:
public interface BatchEdit { string Name { get; } bool IsAsync => false; void Run(UProject project, UVoicePart part, List<UNote> selectedNotes, DocManager docManager); void RunAsync( UProject project, UVoicePart part, List<UNote> selectedNotes, DocManager docManager, Action<int, int> setProgressCallback, CancellationToken cancellationToken) { Run(project, part, selectedNotes, docManager); } }各成员的含义与设计意图如下:
| 成员 | 类型 | 说明 |
|---|---|---|
Name | string | 编辑的本地化名称键(localization key),UI 通过ThemeManager.GetString(edit.Name)渲染为菜单标题。这就是 README 中“Has a localized name”一条对应的属性。 |
IsAsync | bool(默认false) | 是否为异步编辑。异步编辑(如从渲染器加载音高曲线)需要长时间后台计算,UI 会弹出带进度条的对话框并支持取消。 |
Run | void | 同步执行入口,接受项目、声乐分部、选中音符列表与DocManager四个参数。 |
RunAsync | void | 异步执行入口,额外提供进度回调Action<int, int>(当前值、总数)与CancellationToken。接口默认实现直接转调Run,因此普通同步编辑无需实现它。 |
官方 README 定义的编写规范
README.md 明确指出一个合格的批量编辑应满足四点,这是开发者编写宏时必须遵守的硬性规范:
- 以
docManager.StartUndoGroup();开始:开启一个新的撤销组,使后续多次命令被合并为一次可撤销操作; - 通过
docManager.ExecuteCmd();执行修改:所有对文档的修改都必须包装为命令对象(UCommand子类)提交,绝不直接改动数据后绕过命令系统; - 优先使用多音符变体命令:部分命令同时提供单音符变体与多音符变体,例如
AddNoteCommand(UVoicePart part, UNote note)与AddNoteCommand(UVoicePart part, List<UNote> notes)。批量编辑场景应使用多音符变体(如 NoteCommands.cs 所示,多音符构造器同样会按最后一个音符的结束位置计算分部所需最短时长并扩展Part.Duration,但只需构造一次命令对象); - 以
docManager.EndUndoGroup();结束:提交撤销组,并触发一次验证(validate)与预渲染(pre-render)。
此外 README 还提到欢迎社区为编辑宏贡献力量,即开发者可以自行实现BatchEdit并通过外部插件机制接入。
撤销组机制:StartUndoGroup / ExecuteCmd / EndUndoGroup 的底层原理
README 规范的三步调用之所以如此设计,根源在于 DocManager.cs 中撤销组的实现。
StartUndoGroup:开启命令组
public void StartUndoGroup(string? nameKey = null, bool deferValidate = false) { if (undoGroup != null) { Log.Error("undoGroup already started"); EndUndoGroup(); } undoGroup = new UCommandGroup(nameKey, deferValidate); Log.Debug("undoGroup started"); }UCommandGroup(定义于 UCommand.cs)携带三个字段:NameKey(撤销记录中显示的名称键,如"command.batch.note")、DeferValidate(是否延迟校验)、以及命令列表Commands。若在已有未结束的组上再次调用StartUndoGroup,DocManager 会先强制结束旧组,保证组不会嵌套。
ExecuteCmd:命令必须经过命令系统
if (undoGroup == null) { Log.Error($"No active UndoGroup {cmd}"); return; } undoGroup.Commands.Add(cmd); lock (Project) { cmd.Execute(); }从 DocManager.cs 可见,ExecuteCmd要求当前必须存在激活的撤销组,随后把命令加入组内并执行cmd.Execute()。这带来三个关键保证:
- 可撤销性:命令对象同时实现
Execute()与Unexecute()(见 UCommand.cs),撤销组内全部命令构成一次可整体回滚的事务; - 线程安全:命令在
lock (Project)内执行,避免与渲染等并发流程冲突; - 校验与刷新:非延迟校验模式下,每条命令执行后都会令文档快照失效、排队校验并调度真实曲线(Real Curve)刷新。
值得注意的是:ExecuteCmd只能访问DocManager实例,因此Run方法签名中始终携带docManager参数。内置编辑中也存在少数通过DocManager.Inst单例调用的情况,例如CommonnotePaste(见 NoteBatchEdits.cs),此时撤销组键为"command.batch.note"。
EndUndoGroup:提交、校验与合并
public void EndUndoGroup() { if (undoGroup == null) { ... return; } FlushValidation(); if (undoGroup.Commands.Count > 0) { BumpRevision(); undoQueue.AddToBack(undoGroup); redoQueue.Clear(); } while (undoQueue.Count > Util.Preferences.Default.UndoLimit) { undoQueue.RemoveFromFront(); } if (undoGroup.DeferValidate) { InvalidateGroup(undoGroup.Commands); ValidateAndRefresh(); } undoGroup.Merge(); ScheduleRealCurveRefresh(undoGroup.Commands); undoGroup = null; ExecuteCmd(new PreRenderNotification()); }结合 DocManager.cs,EndUndoGroup依次完成:
- 冲刷排队的校验;
- 若组内有命令则提交:递增文档修订号、入撤销队列、清空重做队列,并按
UndoLimit配置裁剪撤销队列长度; - 若该组以
deferValidate: true创建(README 推荐的批量编辑均如此),则在此时一次性失效快照并整体校验,避免每条命令校验一次的性能开销; - 调用
UCommandGroup.Merge(),若组内最后一个命令实现了CanMerge,则将整组合并压缩为一条命令(见 UCommand.cs); - 调度真实曲线刷新、清空当前组,最后发出预渲染通知。
此外,EndUndoGroup会通过DescribeCommands以“Move 1 notes x85, Change 1 notes duration x3”的形式记录组内命令摘要日志(DocManager.cs),便于调试。
编写一个批量编辑宏:完整模板
综合上述规范与源码,一个标准的批量编辑实现骨架如下:
using System.Collections.Generic; using OpenUtau.Core.Ustx; namespace OpenUtau.Core.Editing { public class MyBatchEdit : BatchEdit { // 本地化名称键,对应 Strings.axaml 中的资源项 public string Name => "pianoroll.menu.notes.mybatchedit"; public void Run(UProject project, UVoicePart part, List<UNote> selectedNotes, DocManager docManager) { // 1. 未选中音符时,默认作用于整个分部 var notes = selectedNotes.Count > 0 ? selectedNotes : part.notes.ToList(); if (notes.Count == 0) { return; // 无音符时不做任何修改 } // 2. 开启撤销组;true 表示延迟校验,适合多条命令的批处理 docManager.StartUndoGroup("command.batch.note", true); // 3. 所有修改一律通过 ExecuteCmd 提交命令 foreach (var note in notes) { docManager.ExecuteCmd(new SomeCommand(part, note)); } // 4. 结束并提交撤销组 docManager.EndUndoGroup(); } } }要点解析:
- 选择范围兜底:
selectedNotes.Count > 0 ? selectedNotes : part.notes.ToList()是内置编辑的统一模式。若用户未选中任何音符,批量编辑默认作用于分部全部音符(见 NoteBatchEdits.cs 中几乎所有Run方法); - 空操作短路:若计算结果为空(如
AddTailNote发现无需追加音符),直接return,不开启撤销组,避免产生空操作记录(参考 NoteBatchEdits.cs); - 本地化:
Name返回的是资源键而非最终文案,实际标题在 UI 注册处通过ThemeManager.GetString(edit.Name)解析(见 PianoRoll.axaml.cs)。对应文案需添加到 Strings.axaml(如pianoroll.menu.notes.autolegato→ “Auto Legato”)。
内置批量编辑全览
音符类(NoteBatchEdits)
在 PianoRoll.axaml.cs 中注册的音符类编辑及其实现位置:
| 编辑 | 本地化键 | 功能说明 | 实现文件 |
|---|---|---|---|
LoadRenderedPitch | pianoroll.menu.notes.loadrenderedpitch | 从渲染器(如 DiffSinger)读取已渲染的音高,写入 PITD 曲线与 RPIT 掩码曲线。异步编辑,需渲染器支持SupportsRenderPitch,不支持时抛出本地化错误errors.editing.autopitch.unsupported | NoteBatchEdits.cs |
RefreshRealCurves | pianoroll.menu.notes.refreshrealcurves | 从渲染器加载真实曲线(Real Curve),通过MergedSetCurveCommand合并写回。异步编辑 | NoteBatchEdits.cs |
AddTailNote("-" / "R") | addtaildash/addtailrest | 在音符尾部追加 120 tick 的延音/休止音符,并继承原音符的音素表达式 | NoteBatchEdits.cs |
RemoveTailNote("-" / "R") | removetaildash/removetailrest | 移除尾部紧贴的延音/休止音符(note.Next.position > note.End才删除) | NoteBatchEdits.cs |
Transpose(12 / -12) | octaveup/octavedown | 通过MoveNoteCommand(part, note, 0, deltaNoteNum)整体升/降八度 | NoteBatchEdits.cs |
AutoLegato | autolegato | 按位置排序后逐个ResizeNoteCommand拉伸音符使其首尾相连 | NoteBatchEdits.cs |
CommonnoteCopy/CommonnotePaste | commonnotecopy/commonnotepaste | 通过 Commonnote.cs 复制/粘贴音符到剪贴板;粘贴时以播放指针为锚点,并自动扩展分部时长 | NoteBatchEdits.cs |
FixOverlap | fixoverlap | 修复重叠:同时起始的音符仅保留音高最高者;被后续音符覆盖的音符结尾截断到下一音符起点 | NoteBatchEdits.cs |
BakePitch | bakepitch | 把渲染音高烘焙为音符弯音(Pitch Point),使用 Ramer–Douglas–Peucker 算法简化曲线点数,并清除 PITD 曲线与颤音 | NoteBatchEdits.cs |
RandomizeTiming | randomizetiming | 随机化音符时值(±20 tick 内,按音素时长四分之一比例缩放) | NoteBatchEdits.cs |
RandomizePhonemeOffset | randomizeoffset | 随机化音素偏移(考虑 BPM 换算) | NoteBatchEdits.cs |
此外,AddBreathNote(添加换气音符)、QuantizeNotes(按网格量化)、RandomizeTuning(随机化调音)与LengthenCrossfade(延长交叉淡化)以带参数菜单项形式单独插入(见 PianoRoll.axaml.cs):
QuantizeNotes(int quantize):将音符起点与终点取整到指定网格(源码默认将每个 tick 四舍五入到quantize的倍数),仅在有差异时发出MoveNoteCommand/ResizeNoteCommand(NoteBatchEdits.cs);AddBreathNote(lyric):在音符前插入换气音,时长取 480 tick 或与前音符的间距(NoteBatchEdits.cs);LengthenCrossfade(ratio):对相连音素按maxOtoPreutter上限延长 preutter/overlap,实现更平滑的交叉淡化(NoteBatchEdits.cs)。
值得注意:LoadRenderedPitch与RefreshRealCurves均声明IsAsync => true。UI 侧对异步编辑使用MessageBox.ShowProcessing弹出“Running”对话框,并在回调中更新进度文本{name}: {current} / {total},同时支持取消令牌与异常后的本地化错误提示(PianoRoll.axaml.cs)。
歌词类(LyricBatchEdits)
歌词类编辑大多继承抽象基类SingleNoteLyricEdit,它把“逐音符歌词变换”封装为统一流程:开启"command.batch.lyric"撤销组 → 用ChangeNoteLyricCommand(part, notes, lyrics)批量提交 → 结束组(LyricBatchEdits.cs)。子类只需实现Transform(string lyric)单音符变换逻辑。注册列表见 PianoRoll.axaml.cs。
| 编辑 | 功能说明 |
|---|---|
RomajiToHiragana | 借助 WanaKanaNet 将罗马音歌词转为平假名,仅当结果含假名字符时才替换,保留标点映射 |
HiraganaToRomaji | 平假名转罗马音 |
JapaneseVCVtoCV | 对形如"a あ"、"- あ"的日文 VCV 歌词截取前两字符,还原为 CV |
HanziToPinyin | 调用BaseChinesePhonemizer.Romanize将汉字歌词转为拼音,按Enumerable.Zip逐音符提交ChangeNoteLyricCommand |
RemoveToneSuffix | 去除歌词尾部音调后缀(如C4、C#4、Cb4) |
RemoveLetterSuffix | 去除歌词尾部字母后缀,但保留R/r(休止标记) |
MoveSuffixToVoiceColor | 依据声库子库(Subbank)后缀把歌词中的触发字符迁移到声库颜色(CLR)表达式 |
AddPhoneticHints | 调用音素器为歌词生成音素提示[ka]形式;对非符号音素器回退为按音素边界拆分音符 |
RemovePhoneticHint | 去除[...]音素提示 |
DashToPlus | -转+(呼吸音标记) |
DashToPlusTilda | -转+~ |
InsertSlur | 在选中音符处插入滑音+~,并把后续音符歌词顺延(队列式滚动) |
其中AddPhoneticHints是较复杂的一个:若当前音素器实现了IG2pSymbols,则直接拼接note.lyric + " [" + string.Join(" ", phonemeList) + "]";否则遍历part.phonemes找到该音符的音素序列,在音素边界处用AddNoteCommand拆分音符,再分别赋别名(LyricBatchEdits.cs)。
重置类(ResetBatchEdits)
重置类编辑统一使用"command.batch.reset"撤销组,注册于 PianoRoll.axaml.cs,实现在 ResetBatchEdits.cs:
| 编辑 | 功能说明 |
|---|---|
ResetPitchBends | 用ResetPitchPointsCommand清除全部音符弯音点 |
ResetAllExpressions | 重置音素数值/选项表达式;对曲线表达式,全选时ClearCurveCommand清空,否则用PasteCurveCommand将选中区域写回默认值 |
ClearVibratos | 清除长度大于 0 的颤音 |
ResetVibratos | 用SetVibratoCommand写入默认颤音,并按NotePresets.Default.AutoVibratoToggle与AutoVibratoNoteDuration决定是否赋予默认颤音长度 |
ClearTimings | 仅当音符存在 offset/preutterDelta/overlapDelta 覆盖时才执行ClearPhonemeTimingCommand |
ResetAliases | 用ChangePhonemeAliasCommand(..., null)清除全部音素别名覆盖 |
ResetAll | 依次重置弯音、表达式、颤音、时值、别名与曲线,即上述全部重置的集合 |
这些编辑大量使用SelectionUtils辅助类:SimplifyTimeSelection将时间选区合并去重,SelectedTickRanges计算选中音符(含前置音素、首尾各扩 1 tick)的 tick 区间(ResetBatchEdits.cs),保证只重置选中区域而非整条曲线。
外部插件批量编辑(ExternalBatchEdits)
README 欢迎社区贡献编辑宏,而这一能力的落地正是插件发现机制:DocManager 在启动搜索插件时,对非内置程序集中所有可实例化(有无参构造函数、非抽象、非接口)且实现BatchEdit的类型,收集到ExternalBatchEditTypes(DocManager.cs);UI 层随后通过Activator.CreateInstance(type)实例化并注册为菜单项(PianoRoll.axaml.cs)。这意味着第三方开发者只需把BatchEdit实现类编译进独立插件程序集,即可无缝接入钢琴卷帘菜单。
批量编辑的最佳实践总结
结合 README 规范与源码实现,编写高质量的批量编辑宏应遵循以下实践:
- 始终包裹撤销组:
StartUndoGroup与EndUndoGroup成对出现,保证用户在菜单执行后可以一次 Ctrl+Z 完整回退;推荐以deferValidate: true开启延迟校验以降低批量编辑的校验开销; - 一切修改走命令:绝不直接操作
part.notes、note.lyric等数据,所有变更必须通过docManager.ExecuteCmd提交命令对象,从而获得撤销、校验、真实曲线刷新与快照失效的完整链路; - 优先批量命令变体:存在
List<UNote>变体时优先使用(如AddNoteCommand(part, notes)),减少命令对象数量与文档修订次数; - 提供本地化名称:
Name返回资源键,文案放入 Strings.axaml 等本地化资源文件,并遵守pianoroll.menu.*的键命名惯例; - 空操作尽早返回:没有实际改动时不要开启撤销组,避免产生空的撤销记录;
- 异步编辑实现
RunAsync:涉及渲染器调用等耗时任务时声明IsAsync => true,通过进度回调与取消令牌配合 UI 进度对话框。
遵循以上契约,即可在 OpenUtau 钢琴卷帘中集成可靠、可撤销、可本地化的批量编辑宏,与内置的 NoteBatchEdits.cs、LyricBatchEdits.cs、ResetBatchEdits.cs 保持同等的实现质量。
- 桌面应用
- 音频
【免费下载链接】OpenUtau
Open singing synthesis platform / Open source UTAU successor
相关推荐
PhotoPrism 照片批量编辑(Batch Edit)深入解析:`/api/v1/batch/photos/edit` 的架构、协议与源码实现
PhotoPrism 照片批量编辑(Batch Edit)深入解析: /api/v1/batch/photos/edit 的架构、协议与源码实现 PhotoPr
后端前端图像处理人工智能AI 应用FastAPI数据库连接池:提升性能的终极配置指南
FastAPI数据库连接池:提升性能的终极配置指南 在现代Web应用开发中,数据库连接管理是影响性能的关键因素之一。FastAPI作为一款高性能的Python
后端Web框架API设计VitePress 编辑链接(Edit Link)完全指南:配置、定制与源码解析
VitePress 编辑链接(Edit Link)完全指南:配置、定制与源码解析 导读 本文基于 VitePress 默认主题的 themeConfig.edi
前端文档
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考