diff --git a/OpenUtau.Core/Commands/ProjectCommands.cs b/OpenUtau.Core/Commands/ProjectCommands.cs index a61b8d8d4..f279c32c2 100644 --- a/OpenUtau.Core/Commands/ProjectCommands.cs +++ b/OpenUtau.Core/Commands/ProjectCommands.cs @@ -140,6 +140,18 @@ public override void Unexecute() { } } + public class KeyCommand : ProjectCommand{ + public readonly int oldKey; + public readonly int newKey; + public KeyCommand(UProject project, int key) : base(project) { + oldKey = project.key; + newKey = key; + } + public override string ToString() => $"Change key from {oldKey} to {newKey}"; + public override void Execute() => project.key = newKey; + public override void Unexecute() => project.key = oldKey; + } + public class ConfigureExpressionsCommand : ProjectCommand { readonly UExpressionDescriptor[] oldDescriptors; readonly UExpressionDescriptor[] newDescriptors; diff --git a/OpenUtau.Core/Ustx/UProject.cs b/OpenUtau.Core/Ustx/UProject.cs index 0bbf5193f..974fb00c7 100644 --- a/OpenUtau.Core/Ustx/UProject.cs +++ b/OpenUtau.Core/Ustx/UProject.cs @@ -48,6 +48,7 @@ public class UProject { public string[] expSelectors = new string[] { Format.Ustx.DYN, Format.Ustx.PITD, Format.Ustx.CLR, Format.Ustx.ENG, Format.Ustx.VEL }; public int expPrimary = 0; public int expSecondary = 1; + public int key = 0;//Music key of the project, 0 = C, 1 = C#, 2 = D, ..., 11 = B public List timeSignatures; public List tempos; public List tracks; diff --git a/OpenUtau.Core/Util/MusicMath.cs b/OpenUtau.Core/Util/MusicMath.cs index 4a4b85d60..f8123bd9a 100644 --- a/OpenUtau.Core/Util/MusicMath.cs +++ b/OpenUtau.Core/Util/MusicMath.cs @@ -32,6 +32,36 @@ public enum KeyColor { White, Black } { "B", 11 }, }; + public static readonly string[] Solfeges = { + "do", + "", + "re", + "", + "mi", + "fa", + "", + "sol", + "", + "la", + "", + "ti", + }; + + public static readonly string[] NumberedNotations = { + "1", + "", + "2", + "", + "3", + "4", + "", + "5", + "", + "6", + "", + "7", + }; + public static string GetToneName(int noteNum) { return noteNum < 0 ? string.Empty : KeysInOctave[noteNum % 12].Item1 + (noteNum / 12 - 1).ToString(); } diff --git a/OpenUtau.Core/Util/Preferences.cs b/OpenUtau.Core/Util/Preferences.cs index 20d667d36..f97c249ef 100644 --- a/OpenUtau.Core/Util/Preferences.cs +++ b/OpenUtau.Core/Util/Preferences.cs @@ -112,6 +112,7 @@ public class SerializablePreferences { public bool ShowPrefs = true; public bool ShowTips = true; public int Theme; + public int DegreeStyle; public bool UseTrackColor = false; public bool ClearCacheOnQuit = false; public bool PreRender = true; diff --git a/OpenUtau/Controls/TrackBackground.cs b/OpenUtau/Controls/TrackBackground.cs index e44606742..0b4a1e441 100644 --- a/OpenUtau/Controls/TrackBackground.cs +++ b/OpenUtau/Controls/TrackBackground.cs @@ -1,9 +1,11 @@ using System; +using System.Linq; using System.Reactive.Linq; using Avalonia; using Avalonia.Controls.Primitives; using Avalonia.Media; using OpenUtau.Core; +using OpenUtau.Core.Util; using ReactiveUI; namespace OpenUtau.App.Controls { @@ -65,12 +67,29 @@ protected override void OnPropertyChanged(AvaloniaPropertyChangedEventArgs chang } } + int mod(int a, int b){ + return (a % b + b) % b; + } + public override void Render(DrawingContext context) { if (TrackHeight == 0) { return; } int track = (int)TrackOffset; double top = TrackHeight * (track - TrackOffset); + int key = DocManager.Inst.Project == null ? 0 : DocManager.Inst.Project.key; + string[] degreeNames; + switch(Preferences.Default.DegreeStyle){ + case 1: + degreeNames = MusicMath.Solfeges; + break; + case 2: + degreeNames = MusicMath.NumberedNotations; + break; + default: + degreeNames = Enumerable.Repeat("", 12).ToArray(); + break; + } while (top < Bounds.Height) { bool isAltTrack = IsAltTrack(track) ^ (ThemeManager.IsDarkMode && !IsKeyboard); bool isCenterKey = IsKeyboard && IsCenterKey(track); @@ -85,11 +104,20 @@ public override void Render(DrawingContext context) { brush = isCenterKey ? ThemeManager.CenterKeyNameBrush : isAltTrack ? ThemeManager.BlackKeyNameBrush : ThemeManager.WhiteKeyNameBrush; - string toneName = MusicMath.GetToneName(ViewConstants.MaxTone - 1 - track); - var textLayout = TextLayoutCache.Get(toneName, brush, 12); - var textPosition = new Point(Bounds.Width - 4 - (int)textLayout.Width, (int)(top + (TrackHeight - textLayout.Height) / 2)); - using (var state = context.PushTransform(Matrix.CreateTranslation(textPosition))) { - textLayout.Draw(context, new Point()); + int tone = ViewConstants.MaxTone - 1 - track; + string toneName = MusicMath.GetToneName(tone); + var toneTextLayout = TextLayoutCache.Get(toneName, brush, 12); + var toneTextPosition = new Point(Bounds.Width - 4 - (int)toneTextLayout.Width, (int)(top + (TrackHeight - toneTextLayout.Height) / 2)); + using (var state = context.PushTransform(Matrix.CreateTranslation(toneTextPosition))) { + toneTextLayout.Draw(context, new Point()); + } + //scale degree display + int degree = mod(tone - key, 12); + string degreeName = degreeNames[degree]; + var degreeTextLayout = TextLayoutCache.Get(degreeName, brush, 12); + var degreeTextPosition = new Point(4, (int)(top + (TrackHeight - degreeTextLayout.Height) / 2)); + using (var state = context.PushTransform(Matrix.CreateTranslation(degreeTextPosition))) { + degreeTextLayout.Draw(context, new Point()); } } track++; diff --git a/OpenUtau/Strings/Strings.axaml b/OpenUtau/Strings/Strings.axaml index 519b27b3f..195597b30 100644 --- a/OpenUtau/Strings/Strings.axaml +++ b/OpenUtau/Strings/Strings.axaml @@ -36,6 +36,7 @@ Installing Installing phonemizer Installing + Key Cancel No Ok @@ -290,6 +291,10 @@ Stable vLabeler Path Appearance + Scale degree display style + Numbered (1 2 3 4 5 6 7) + Off + Solfège (do re mi fa sol la ti) Language Show other tracks' notes on piano roll Show portrait on piano roll diff --git a/OpenUtau/ViewModels/NotesViewModel.cs b/OpenUtau/ViewModels/NotesViewModel.cs index 64ee78259..3f119deb3 100644 --- a/OpenUtau/ViewModels/NotesViewModel.cs +++ b/OpenUtau/ViewModels/NotesViewModel.cs @@ -65,6 +65,7 @@ public class NotesViewModel : ViewModelBase, ICmdSubscriber { [Reactive] public bool ShowNoteParams { get; set; } [Reactive] public bool IsSnapOn { get; set; } [Reactive] public string SnapDivText { get; set; } + [Reactive] public string KeyText { get; set; } [Reactive] public Rect ExpBounds { get; set; } [Reactive] public string PrimaryKey { get; set; } [Reactive] public bool PrimaryKeyNotSupported { get; set; } @@ -85,8 +86,10 @@ public class NotesViewModel : ViewModelBase, ICmdSubscriber { public double VScrollBarMax => Math.Max(0, TrackCount - ViewportTracks); public UProject Project => DocManager.Inst.Project; [Reactive] public List SnapDivs { get; set; } + [Reactive] public List Keys { get; set; } public ReactiveCommand SetSnapUnitCommand { get; set; } + public ReactiveCommand SetKeyCommand { get; set; } // See the comments on TracksViewModel.playPosXToTickOffset private double playPosXToTickOffset => ViewportTicks / Bounds.Width; @@ -111,6 +114,14 @@ public NotesViewModel() { UpdateSnapDiv(); }); + Keys = new List(); + SetKeyCommand = ReactiveCommand.Create(key => { + DocManager.Inst.StartUndoGroup(); + DocManager.Inst.ExecuteCmd(new KeyCommand(Project, key)); + DocManager.Inst.EndUndoGroup(); + UpdateKey(); + }); + viewportTicks = this.WhenAnyValue(x => x.Bounds, x => x.TickWidth) .Select(v => v.Item1.Width / Math.Max(v.Item2, ViewConstants.TickWidthMin)) .ToProperty(this, x => x.ViewportTicks); @@ -173,6 +184,13 @@ public NotesViewModel() { Command = SetSnapUnitCommand, CommandParameter = div, })); + Keys.Clear(); + Keys.AddRange(MusicMath.KeysInOctave + .Select((key, index) => new MenuItemViewModel { + Header = $"1={key.Item1}", + Command = SetKeyCommand, + CommandParameter = index, + })); }); CursorTool = false; @@ -193,6 +211,7 @@ public NotesViewModel() { ShowTips = Preferences.Default.ShowTips; IsSnapOn = true; SnapDivText = string.Empty; + KeyText = string.Empty; PlayTone = Preferences.Default.PlayTone; this.WhenAnyValue(x => x.PlayTone) @@ -267,6 +286,11 @@ private void UpdateSnapDiv() { SnapDivText = $"(1/{div})"; } + private void UpdateKey(){ + int key = Project.key; + KeyText = "1="+MusicMath.KeysInOctave[key].Item1; + } + public void OnXZoomed(Point position, double delta) { bool recenter = true; if (TickOffset == 0 && position.X < 0.1) { @@ -378,6 +402,7 @@ private void LoadPart(UPart part, UProject project) { LoadPortrait(part, project); LoadWindowTitle(part, project); LoadTrackColor(part, project); + UpdateKey(); } //If PortraitHeight is 0, the default behaviour is resizing any image taller than 800px to 800px, diff --git a/OpenUtau/ViewModels/PlaybackViewModel.cs b/OpenUtau/ViewModels/PlaybackViewModel.cs index de6c4eadf..dcfd82a75 100644 --- a/OpenUtau/ViewModels/PlaybackViewModel.cs +++ b/OpenUtau/ViewModels/PlaybackViewModel.cs @@ -11,6 +11,8 @@ public class PlaybackViewModel : ViewModelBase, ICmdSubscriber { public int BeatPerBar => Project.timeSignatures[0].beatPerBar; public int BeatUnit => Project.timeSignatures[0].beatUnit; public double Bpm => Project.tempos[0].bpm; + public int Key => Project.key; + public string KeyName => MusicMath.KeysInOctave[Key].Item1; public int Resolution => Project.resolution; public int PlayPosTick => DocManager.Inst.playPosTick; public TimeSpan PlayPosTime => TimeSpan.FromMilliseconds((int)Project.timeAxis.TickPosToMsPos(DocManager.Inst.playPosTick)); @@ -61,6 +63,15 @@ public void SetBpm(double bpm) { DocManager.Inst.EndUndoGroup(); } + public void SetKey(int key) { + if (key == DocManager.Inst.Project.key) { + return; + } + DocManager.Inst.StartUndoGroup(); + DocManager.Inst.ExecuteCmd(new KeyCommand(Project, key)); + DocManager.Inst.EndUndoGroup(); + } + public void OnNext(UCommand cmd, bool isUndo) { if (cmd is BpmCommand || cmd is TimeSignatureCommand || @@ -68,10 +79,12 @@ cmd is AddTempoChangeCommand || cmd is DelTempoChangeCommand || cmd is AddTimeSigCommand || cmd is DelTimeSigCommand || + cmd is KeyCommand || cmd is LoadProjectNotification) { this.RaisePropertyChanged(nameof(BeatPerBar)); this.RaisePropertyChanged(nameof(BeatUnit)); this.RaisePropertyChanged(nameof(Bpm)); + this.RaisePropertyChanged(nameof(KeyName)); MessageBus.Current.SendMessage(new TimeAxisChangedEvent()); if (cmd is LoadProjectNotification) { DocManager.Inst.ExecuteCmd(new SetPlayPosTickNotification(0)); diff --git a/OpenUtau/ViewModels/PreferencesViewModel.cs b/OpenUtau/ViewModels/PreferencesViewModel.cs index 018834a27..8fa259eb1 100644 --- a/OpenUtau/ViewModels/PreferencesViewModel.cs +++ b/OpenUtau/ViewModels/PreferencesViewModel.cs @@ -43,6 +43,7 @@ public AudioOutputDevice? AudioOutputDevice { [Reactive] public int DiffsingerSpeedup { get; set; } [Reactive] public bool HighThreads { get; set; } [Reactive] public int Theme { get; set; } + [Reactive] public int DegreeStyle { get; set; } [Reactive] public bool UseTrackColor { get; set; } [Reactive] public bool ShowPortrait { get; set; } [Reactive] public bool ShowGhostNotes { get; set; } @@ -136,6 +137,7 @@ public PreferencesViewModel() { DiffSingerDepth = Preferences.Default.DiffSingerDepth; DiffsingerSpeedup = Preferences.Default.DiffsingerSpeedup; Theme = Preferences.Default.Theme; + DegreeStyle = Preferences.Default.DegreeStyle; UseTrackColor = Preferences.Default.UseTrackColor; ShowPortrait = Preferences.Default.ShowPortrait; ShowGhostNotes = Preferences.Default.ShowGhostNotes; @@ -207,6 +209,11 @@ public PreferencesViewModel() { Preferences.Save(); App.SetTheme(); }); + this.WhenAnyValue(vm => vm.DegreeStyle) + .Subscribe(degreeStyle => { + Preferences.Default.DegreeStyle = degreeStyle; + Preferences.Save(); + }); this.WhenAnyValue(vm => vm.UseTrackColor) .Subscribe(trackColor => { Preferences.Default.UseTrackColor = trackColor; diff --git a/OpenUtau/Views/PianoRollWindow.axaml b/OpenUtau/Views/PianoRollWindow.axaml index 9df063d89..4e4f6dfca 100644 --- a/OpenUtau/Views/PianoRollWindow.axaml +++ b/OpenUtau/Views/PianoRollWindow.axaml @@ -394,6 +394,20 @@ + diff --git a/OpenUtau/Views/PianoRollWindow.axaml.cs b/OpenUtau/Views/PianoRollWindow.axaml.cs index 9fd4b11ef..316573553 100644 --- a/OpenUtau/Views/PianoRollWindow.axaml.cs +++ b/OpenUtau/Views/PianoRollWindow.axaml.cs @@ -802,6 +802,19 @@ void OnSnapDivKeyDown(object sender, KeyEventArgs e) { } } + public void OnKeyMenuButton(object sender, RoutedEventArgs args) { + KeyMenu.PlacementTarget = sender as Button; + KeyMenu.Open(); + } + + void OnKeyKeyDown(object sender, KeyEventArgs e) { + if (e.Key == Key.Enter && e.KeyModifiers == KeyModifiers.None) { + if (sender is ContextMenu menu && menu.SelectedItem is MenuItemViewModel item) { + item.Command?.Execute(item.CommandParameter); + } + } + } + #region value tip void IValueTip.ShowValueTip() { diff --git a/OpenUtau/Views/PreferencesDialog.axaml b/OpenUtau/Views/PreferencesDialog.axaml index 9c9ccabdc..b0e5bc99a 100644 --- a/OpenUtau/Views/PreferencesDialog.axaml +++ b/OpenUtau/Views/PreferencesDialog.axaml @@ -166,6 +166,12 @@ + + + + + +