Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions OpenUtau/Strings/Strings.axaml
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@
<system:String x:Key="context.part.merge">Merge parts</system:String>
<system:String x:Key="context.part.rename">Rename part</system:String>
<system:String x:Key="context.part.replaceaudio">Reselect audio file</system:String>
<system:String x:Key="context.part.split">Split part at playhead</system:String>
<system:String x:Key="context.part.transcribe">Transcribe audio to create a note part</system:String>
<system:String x:Key="context.part.transcribing">Transcribing</system:String>
<system:String x:Key="context.pitch.easein">Ease in</system:String>
Expand Down Expand Up @@ -44,6 +45,9 @@ OpenUtau aims to be an open source editing environment for UTAU community, with
<system:String x:Key="dialogs.installdll.message">Installing </system:String>
<system:String x:Key="dialogs.merge.caption">Merging Parts</system:String>
<system:String x:Key="dialogs.merge.multitracks">Parts on different tracks cannot be merged.</system:String>
<system:String x:Key="dialogs.splitpart.caption">Splitting Part</system:String>
<system:String x:Key="dialogs.splitpart.intheway">There are one or more note(s) overlapping with the playhead.
Do you want to continue by splitting at the nearest position after current playhead with no notes in the way?</system:String>
<system:String x:Key="dialogs.messagebox.cancel">Cancel</system:String>
<system:String x:Key="dialogs.messagebox.copy">Copy error to clipboard</system:String>
<system:String x:Key="dialogs.messagebox.no">No</system:String>
Expand Down
1 change: 1 addition & 0 deletions OpenUtau/ViewModels/MainWindowViewModel.cs
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@ public class PartsContextMenuArgs {
public ReactiveCommand<UPart, Unit>? PartReplaceAudioCommand { get; set; }
public ReactiveCommand<UPart, Unit>? PartTranscribeCommand { get; set; }
public ReactiveCommand<UPart, Unit>? PartMergeCommand { get; set; }
public ReactiveCommand<UPart, Unit>? PartSplitCommand { get; set; }
}

public class RecentFileInfo {
Expand Down
4 changes: 4 additions & 0 deletions OpenUtau/Views/MainWindow.axaml
Original file line number Diff line number Diff line change
Expand Up @@ -370,6 +370,10 @@
IsVisible="{Binding IsVoicePart}"
Command="{Binding PartMergeCommand}"
CommandParameter="{Binding Part}"/>
<MenuItem Header="{DynamicResource context.part.split}"
IsVisible="{Binding IsVoicePart}"
Command="{Binding PartSplitCommand}"
CommandParameter="{Binding Part}"/>
</ContextMenu>
</c:PartsCanvas.ContextMenu>
</c:PartsCanvas>
Expand Down
65 changes: 65 additions & 0 deletions OpenUtau/Views/MainWindow.axaml.cs
Original file line number Diff line number Diff line change
Expand Up @@ -49,6 +49,7 @@ public partial class MainWindow : Window, ICmdSubscriber {
private readonly ReactiveCommand<UPart, Unit> PartReplaceAudioCommand;
private readonly ReactiveCommand<UPart, Unit> PartTranscribeCommand;
private readonly ReactiveCommand<UPart, Unit> PartMergeCommand;
private readonly ReactiveCommand<UPart, Unit> PartSplitCommand;

public MainWindow() {
Log.Information("Creating main window.");
Expand Down Expand Up @@ -82,6 +83,7 @@ public MainWindow() {
PartReplaceAudioCommand = ReactiveCommand.Create<UPart>(part => ReplaceAudio(part));
PartTranscribeCommand = ReactiveCommand.Create<UPart>(part => Transcribe(part));
PartMergeCommand = ReactiveCommand.Create<UPart>(part => MergePart(part));
PartSplitCommand = ReactiveCommand.Create<UPart>(async part => await SplitPart(part));

AddHandler(DragDrop.DropEvent, OnDrop);

Expand Down Expand Up @@ -1081,6 +1083,7 @@ public void PartsCanvasPointerPressed(object sender, PointerPressedEventArgs arg
PartRenameCommand = PartRenameCommand,
PartTranscribeCommand = PartTranscribeCommand,
PartMergeCommand = PartMergeCommand,
PartSplitCommand = PartSplitCommand
};
shouldOpenPartsContextMenu = true;
}
Expand Down Expand Up @@ -1369,7 +1372,69 @@ void MergePart(UPart part) {
DocManager.Inst.ExecuteCmd(new AddPartCommand(DocManager.Inst.Project, mergedPart));
DocManager.Inst.EndUndoGroup();
}
async Task SplitPart(UPart part) {
int tick = DocManager.Inst.playPosTick;
if (part.position >= tick || part.End <= tick) return;
if (part is not UVoicePart vp) return;
var notesInTheWay = vp.notes.Where(n => (n.position < tick - vp.position) && (n.End > tick - vp.position));
if (notesInTheWay.Any()) {
var res = await MessageBox.Show(
this,
ThemeManager.GetString("dialogs.splitpart.intheway"),
ThemeManager.GetString("dialogs.splitpart.caption"),
MessageBox.MessageBoxButtons.YesNo);
if (res == MessageBox.MessageBoxResult.No) { return; }
do {
tick = vp.position + notesInTheWay.Max(n => n.End);
notesInTheWay = vp.notes.Where(n => (n.position < tick - vp.position) && (n.End > tick - vp.position));
} while (notesInTheWay.Any());
}

static SortedSet<UNote> GetNotes(IEnumerable<UNote> notes, int relTick, bool after) => after
? [.. notes.Where(n => n.position >= relTick).Select(n => { var m = n.Clone(); m.position -= relTick; return m; })]
: [.. notes.Where(n => n.position < relTick).Select(n => n.Clone())];
static List<UCurve> GetCurves(IEnumerable<UCurve> curves, int relTick, bool after) =>
curves.Select(c => {
var cloned = c.Clone();
var zipped = cloned.xs.Zip(cloned.ys, (x, y) => (x, y));
var filtered = after
? zipped.Where(z => z.x >= relTick).Select(z => (x: z.x - relTick, z.y))
: zipped.Where(z => z.x < relTick);
cloned.xs = [.. filtered.Select(z => z.x)];
cloned.ys = [.. filtered.Select(z => z.y)];
return cloned;
}).ToList();

var notesAfter = GetNotes(vp.notes, tick - vp.position, after: true);
var notesBefore = GetNotes(vp.notes, tick - vp.position, after: false);
var curvesAfter = GetCurves(vp.curves, tick - vp.position, after: true);
var curvesBefore = GetCurves(vp.curves, tick - vp.position, after: false);

var firstPart = new UVoicePart {
name = vp.name + "-1",
comment = vp.comment,
trackNo = vp.trackNo,
position = vp.position,
notes = notesBefore,
curves = curvesBefore,
Duration = tick - vp.position
};
var secondPart = new UVoicePart {
name = vp.name + "-2",
comment = vp.comment,
trackNo = vp.trackNo,
position = tick,
notes = notesAfter,
curves = curvesAfter,
Duration = vp.End - tick
};

DocManager.Inst.StartUndoGroup();
DocManager.Inst.ExecuteCmd(new RemovePartCommand(DocManager.Inst.Project, vp));
DocManager.Inst.ExecuteCmd(new AddPartCommand(DocManager.Inst.Project, firstPart));
DocManager.Inst.ExecuteCmd(new AddPartCommand(DocManager.Inst.Project, secondPart));
DocManager.Inst.EndUndoGroup();
}
public async void OnWelcomeRecent(object sender, PointerPressedEventArgs args) {
if (sender is StackPanel panel &&
panel.DataContext is RecentFileInfo fileInfo) {
Expand Down
Loading