public void Apply(InstrumentalArrangement arrangement, Action <string> Log) { var events = arrangement.Events; var width3events = events.Where(ev => ev.Code.Equals("w3", StringComparison.OrdinalIgnoreCase)).ToList(); foreach (var w3event in width3events) { var modifiedAnchors = arrangement.Levels .SelectMany(lvl => lvl.Anchors) .Where(a => a.Time == w3event.Time); foreach (var anchor in modifiedAnchors) { anchor.Width = 3; Log($"Changed width of anchor at {anchor.Time.TimeToString()} to 3."); } events.Remove(w3event); } }
public void Apply(InstrumentalArrangement arrangement, Action<string> Log) { const int ngPhraseId = 1; if (arrangement.Phrases[ngPhraseId].MaxDifficulty != 0) return; var ngPhrasesIterations = from pi in arrangement.PhraseIterations where pi.PhraseId == ngPhraseId select pi; if (!ngPhrasesIterations.Any()) { Log("Removed unnecessary noguitar phrase."); arrangement.Phrases.RemoveAt(ngPhraseId); // Set correct phrase IDs for phrase iterations foreach (var pi in arrangement.PhraseIterations) { if (pi.PhraseId > ngPhraseId) { --pi.PhraseId; } } // Set correct phrase IDs for NLDs foreach (var nld in arrangement.NewLinkedDiffs) { for (int i = 0; i < nld.PhraseCount; i++) { if (nld.PhraseIds[i] > ngPhraseId) { nld.PhraseIds[i]--; } } } } }
public void Apply(InstrumentalArrangement arrangement, Action <string> Log) { if (arrangement.MetaData.ArrangementProperties.UnpitchedSlides) { return; } // TODO: Could optimize by looking at hardest level only var notes = arrangement.Levels.SelectMany(l => l.Notes); if (notes.Any(n => n.IsUnpitchedSlide)) { arrangement.MetaData.ArrangementProperties.UnpitchedSlides = true; } else { foreach (var chord in arrangement.Levels.SelectMany(l => l.Chords)) { if (chord.ChordNotes is null) { continue; } if (chord.ChordNotes.Any(n => n.IsUnpitchedSlide)) { arrangement.MetaData.ArrangementProperties.UnpitchedSlides = true; break; } } } if (arrangement.MetaData.ArrangementProperties.UnpitchedSlides) { Log("Enabled unpitched slides in arrangement properties."); } }
public void Apply(InstrumentalArrangement arrangement, Action <string> Log) { // Increase the number attribute of any following noguitar sections foreach (var section in arrangement.Sections.Where(s => s.Name == "noguitar")) { ++section.Number; } // Add the removed noguitar section back var newFirstSection = new Section("noguitar", _firstNGSectionTime, 1); arrangement.Sections.Insert(0, newFirstSection); // Add a new NG phrase as the last phrase arrangement.Phrases.Add(new Phrase("NG", maxDifficulty: 0, PhraseMask.None)); // Recreate the removed phrase iteration (with the phrase id of the new NG phrase) var newNGPhraseIteration = new PhraseIteration(_firstNGSectionTime, arrangement.Phrases.Count - 1); // Add after the first phraseIteration (COUNT) arrangement.PhraseIterations.Insert(1, newNGPhraseIteration); Log("Restored first noguitar section."); }
/// <summary> /// Adds a second difficulty level to phrases that have only one level. /// </summary> public void Apply(InstrumentalArrangement arrangement, Action <string> Log) { Log("********** Begin one level phrase fix **********"); for (int phraseID = 0; phraseID < arrangement.Phrases.Count; phraseID++) { if (arrangement.Phrases[phraseID].MaxDifficulty == 0) { Log($"--Phrase #{phraseID} ({arrangement.Phrases[phraseID].Name}, {arrangement.PhraseIterations.First(pi => pi.PhraseId == phraseID).Time.TimeToString()}) has only one level."); var phraseIterations = from pi in arrangement.PhraseIterations where pi.PhraseId == phraseID select pi; foreach (var pi in phraseIterations) { int startTime = pi.Time; int piIndex = arrangement.PhraseIterations.IndexOf(pi); if (piIndex == 0 || piIndex == arrangement.PhraseIterations.Count - 1) { // Skip first phrase (COUNT) and last phrase (END) continue; } int endTime = arrangement.PhraseIterations[piIndex + 1].Time; var firstLevel = arrangement.Levels[0]; var secondLevel = arrangement.Levels[1]; var firstLevelNotes = (from note in firstLevel.Notes where note.Time >= startTime && note.Time < endTime select note).ToArray(); int notesInPhrase = firstLevelNotes.Length; if (notesInPhrase == 0) // Phrase is a noguitar phrase { Log("Skipping empty phrase."); break; } Log($"Phrase has {notesInPhrase} note{(notesInPhrase == 1 ? "" : "s")}."); // Make copies of current notes that will be added to the harder difficulty level var harderLevelNotes = new List <Note>(from note in firstLevelNotes select new Note(note)); int notesRemoved = 0; var removableSustainNotes = firstLevelNotes.Where(note => note.Sustain != 0 && !note.IsBend && !note.IsVibrato && !note.IsTremolo && !note.IsLinkNext && !note.IsSlide && !note.IsUnpitchedSlide); var firstLevelAnchors = firstLevel.Anchors; var secondLevelAnchors = secondLevel.Anchors; var anchorsToAddToSecondLevel = from anchor in firstLevelAnchors where anchor.Time >= startTime && anchor.Time < endTime select anchor; var newSecondLevelAnchors = new List <Anchor>(); newSecondLevelAnchors.AddRange(secondLevelAnchors.Union(anchorsToAddToSecondLevel).OrderBy(a => a.Time)); secondLevel.Anchors = newSecondLevelAnchors; if (notesInPhrase > 1) { foreach (var note in firstLevelNotes) { // Remove sliding notes and vibratos if (note.IsSlide || note.IsUnpitchedSlide || note.IsVibrato) { Log($"Removed note at time {note.Time.TimeToString()}"); firstLevel.Notes.Remove(note); notesRemoved++; } else if (note.IsLinkNext) // Remove linkNext from the note { Log($"Removed linkNext from note at time {note.Time.TimeToString()}"); note.IsLinkNext = false; if (note.Sustain < 500 && !note.IsBend) // Remove sustain if it is very short { note.Sustain = 0; Log($"Removed sustain from note at time {note.Time.TimeToString()}"); } } // If note has more than two bendValues, remove the rest if (note.IsBend && note.BendValues?.Count > 2) { // Remove bendValues var secondBendValue = note.BendValues[1]; note.BendValues.RemoveRange(2, note.BendValues.Count - 2); // Set sustain of the note to end at the second bendValue note.Sustain = secondBendValue.Time - note.Time; Log($"Removed bendvalues and adjusted the sustain of note at {note.Time.TimeToString()}"); } } } if (notesRemoved == 0 && removableSustainNotes.Any()) { foreach (var note in removableSustainNotes) { note.Sustain = 0; } Log($"Solution: Removed sustain from note{(notesInPhrase == 1 ? "" : "s")}."); } else if (notesRemoved == 0) { // TODO: One note phrase -> remove all techniques? Log("Solution: Kept phrase as is."); } pi.HeroLevels = new HeroLevels(easy: 0, medium: 1, hard: 1); // Find correct place where to insert the notes in the second difficulty level int lastNoteTime = harderLevelNotes.Last().Time; int noteAfterIndex = secondLevel.Notes.FindIndex(n => n.Time > lastNoteTime); if (noteAfterIndex == -1) // Add to the end { secondLevel.Notes.AddRange(harderLevelNotes); } else { secondLevel.Notes.InsertRange(noteAfterIndex, harderLevelNotes); } arrangement.Phrases[phraseID].MaxDifficulty = 1; } } } Log("********** End one level phrase fix **********"); }
public PostProcessorBlocksTests(ConfigurationFixture fixture) { XMLProcessor.Preferences = fixture.Configuration; testArrangement = InstrumentalArrangement.Load(@".\TestFiles\postTest_RS2.xml"); }
public PreProcessorBlocksTests() { testArrangement = InstrumentalArrangement.Load(@".\TestFiles\preTest_RS2.xml"); }
private static (IList <Note> notes, IList <Chord> chords) GetNotesAndChordsFromSong(InstrumentalArrangement song) { // Check if the song has DD levels if (song.Levels.Count != 1) { int transcriptionNoteCount = song.TranscriptionTrack?.Notes?.Count ?? 0; int transcriptionChordCount = song.TranscriptionTrack?.Chords?.Count ?? 0; if (transcriptionNoteCount == 0 && transcriptionChordCount == 0) { // Manual DD, no transcription track available return(GenerateTranscriptionTrack(song)); } else { // Use transcription track return(song.TranscriptionTrack.Notes, song.TranscriptionTrack.Chords); } } else // No DD, just grab all notes and chords { return(song.Levels[0].Notes, song.Levels[0].Chords); } }
private static (IList <Note> notes, IList <Chord> chords) GenerateTranscriptionTrack(InstrumentalArrangement song) { var phrases = song.Phrases; var notes = new List <Note>(); var chords = new List <Chord>(); // Ignore the last phrase iteration (END) for (int i = 0; i < song.PhraseIterations.Count - 1; i++) { var phraseIteration = song.PhraseIterations[i]; int phraseId = phraseIteration.PhraseId; int maxDifficulty = phrases[phraseId].MaxDifficulty; if (maxDifficulty == 0) { continue; } int phraseStartTime = phraseIteration.Time; int phraseEndTime = song.PhraseIterations[i + 1].Time; var highestLevelForPhrase = song.Levels[maxDifficulty]; var notesInPhraseIteration = highestLevelForPhrase.Notes .Where(n => n.Time >= phraseStartTime && n.Time < phraseEndTime); var chordsInPhraseIteration = highestLevelForPhrase.Chords .Where(c => c.Time >= phraseStartTime && c.Time < phraseEndTime); notes.AddRange(notesInPhraseIteration); chords.AddRange(chordsInPhraseIteration); } return(notes, chords); }
public void Apply(InstrumentalArrangement arrangement, Action <string> Log) { int notesRemoved = 0; foreach (var level in arrangement.Levels) { var notesToRemove = new List <Note>(); var notes = level.Notes; var chords = level.Chords; for (int i = 0; i < notes.Count; i++) { Note note = notes[i]; if (note.IsLinkNext) { // Find the next note on the same string int j = i + 1; while (j < notes.Count && notes[j].String != note.String) { j++; } if (j == notes.Count) { break; } // Remove the note if it has no sustain if (notes[j].Sustain == 0) { notesToRemove.Add(notes[j]); note.IsLinkNext = false; } } } for (int i = 0; i < chords.Count; i++) { Chord chord = chords[i]; if (chord.IsLinkNext && chord.ChordNotes?.Any(n => n.IsSlide) == true) { // Find the first note after the chord int noteIndex = notes.FindIndex(n => n.Time > chord.Time); if (noteIndex == -1) { continue; } // Get all the chord notes that have LinkNext slides var chordNotes = chord.ChordNotes.Where(n => n.IsLinkNext && n.IsSlide).ToDictionary(n => n.String); while (chordNotes.Count > 0 && noteIndex < notes.Count) { var note = notes[noteIndex]; sbyte nString = note.String; if (chordNotes.ContainsKey(nString)) { if (note.Sustain == 0) { chordNotes[nString].IsLinkNext = false; notesToRemove.Add(note); } chordNotes.Remove(nString); } ++noteIndex; } chord.IsLinkNext = chord.ChordNotes.Any(n => n.IsLinkNext); } } notesRemoved += notesToRemove.Count; foreach (var note in notesToRemove) { notes.Remove(note); } } if (notesRemoved > 0) { Log($"Removed {notesRemoved} anchor placeholder note{(notesRemoved == 1 ? "" : "s")}."); } }
public void Apply(InstrumentalArrangement arrangement, Action <string> Log) { foreach (var level in arrangement.Levels) { var handShapes = level.HandShapes; for (int i = 1; i < handShapes.Count; i++) { int followingStartTime = handShapes[i].StartTime; int followingEndTime = handShapes[i].EndTime; var precedingHandshape = handShapes[i - 1]; int precedingStartTime = precedingHandshape.StartTime; int precedingEndTime = precedingHandshape.EndTime; // Ignore nested handshapes if (precedingEndTime >= followingEndTime) { Log($"Skipped nested handshape starting at {precedingStartTime.TimeToString()}."); continue; } int beat1Index = arrangement.Ebeats.FindIndex(b => b.Time > precedingEndTime); var beat1 = arrangement.Ebeats[beat1Index - 1]; var beat2 = arrangement.Ebeats[beat1Index]; int note32nd = (beat2.Time - beat1.Time) / 8; bool shortenBy16thNote = false; // Check if the chord that starts the handshape is a LinkNext slide var startChord = level.Chords.FindByTime(precedingStartTime); if (startChord?.IsLinkNext == true && startChord?.ChordNotes?.Any(cn => cn.IsSlide) == true) { // Check if the handshape length is an 8th note or longer if (precedingEndTime - precedingStartTime > note32nd * 4) { shortenBy16thNote = true; } } int minDistance = shortenBy16thNote ? note32nd * 2 : note32nd; // Shorten the min. distance required for 32nd notes or smaller if (precedingEndTime - precedingStartTime <= note32nd) { minDistance = (beat2.Time - beat1.Time) / 12; } // Sometimes the following handshape may begin before the preceding one ends (floating point rounding error?) int currentDistance = followingStartTime - precedingEndTime; if (currentDistance < minDistance) { int newEndTime = followingStartTime - minDistance; int safetyCount = 0; // Shorten the distance for very small note values while (newEndTime <= precedingStartTime && ++safetyCount < 3) { minDistance /= 2; newEndTime = followingStartTime - minDistance; #if DEBUG Log("Reduced handshape min. distance by half."); #endif } handShapes[i - 1].EndTime = newEndTime; // Skip logging < 5ms adjustments if (minDistance - currentDistance > 5) { var message = $"Adjusted the length of handshape starting at {precedingHandshape.StartTime.TimeToString()} (Distance: {currentDistance}ms -> {minDistance}ms)"; if (shortenBy16thNote) { message += " (Chord slide)"; } Log(message); } } } } }
public void Apply(InstrumentalArrangement arrangement, Action <string> Log) { var events = arrangement.Events; var removeBeatsEvent = events.Find(ev => ev.Code.Equals("removebeats", StringComparison.OrdinalIgnoreCase)); if (removeBeatsEvent is not null) { arrangement.Ebeats.RemoveAll(b => b.Time >= removeBeatsEvent.Time); Log($"removebeats event found: Removed beats from {removeBeatsEvent.Time.TimeToString()} onward."); events.Remove(removeBeatsEvent); } var slideoutEvents = events.Where(ev => ev.Code.StartsWith("so", StringComparison.OrdinalIgnoreCase)).ToList(); foreach (var slideEvent in slideoutEvents) { int slideTime = slideEvent.Time; // Find the max level for the phrase the slide is in var phraseIter = arrangement.PhraseIterations.Last(pi => pi.Time <= slideTime); int diff = arrangement.Phrases[phraseIter.PhraseId].MaxDifficulty; var level = arrangement.Levels[diff]; // If a number was given after the event code, get the time of the chord or note that is right of the event by that number if (slideEvent.Code.Length > 2 && int.TryParse(slideEvent.Code.Substring(2), NumberStyles.Integer, NumberFormatInfo.InvariantInfo, out int rightBy)) { slideTime = TimeParser.FindTimeOfNthNoteFrom(level, slideTime, rightBy); } int noteIndex = level.Notes.FindIndexByTime(slideTime); int chordIndex = level.Chords.FindIndexByTime(slideTime); if (noteIndex == -1 && chordIndex == -1) { string error = $"Could not find the notes or chord for SlideOut event at {slideEvent.Time.TimeToString()}"; _statusMessages.Add(new ImproverMessage(error, MessageType.Error, slideEvent.Time)); Log(error); continue; } var notes = new List <Note>(); ChordTemplate originalChordTemplate; if (noteIndex != -1) // These are notes that follow a LinkNext chord { do { var note = level.Notes[noteIndex]; if (note.IsUnpitchedSlide) { notes.Add(note); } noteIndex++; } while (noteIndex < level.Notes.Count && level.Notes[noteIndex].Time == slideTime); // Find the chord that is linked to the slide, its template and handshape var linkNextChord = level.Chords.Last(c => c.Time < slideTime); var linkNextChordHs = level.HandShapes.Find(hs => hs.StartTime == linkNextChord.Time); originalChordTemplate = arrangement.ChordTemplates[linkNextChord.ChordId]; // Shorten handshapes that EOF has set to include the slide out // If chord notes is null here, there is an error in the XML file if (linkNextChordHs is not null && linkNextChordHs.EndTime > linkNextChord.Time + linkNextChord.ChordNotes ![0].Sustain)
public ProcessorContext(InstrumentalArrangement arrangement, Action <string> logAction) { this.arrangement = arrangement; this.logAction = logAction; }
public void Apply(InstrumentalArrangement arrangement, Action <string> Log) { var lastBeat = arrangement.Ebeats[^ 1];
public void Apply(InstrumentalArrangement arrangement, Action <string> Log) { int hiDensRemoved = 0; void removeHighDensity(Chord chord, bool removeChordNotes) { if (chord.IsHighDensity) { chord.IsHighDensity = false; if (removeChordNotes) { // Set the chord as ignored if it has any harmonics in it if (chord.ChordNotes?.Count > 0 && chord.ChordNotes.Any(cn => cn.IsHarmonic)) { chord.IsIgnore = true; } chord.ChordNotes = null; } hiDensRemoved++; } } // Make sure that the version of the XML file is 8 arrangement.Version = 8; foreach (var level in arrangement.Levels) { foreach (var hs in level.HandShapes) { var chordsInHs = from chord in level.Chords where chord.Time >= hs.StartTime && chord.Time < hs.EndTime select chord; bool startsWithMute = false; int chordNum = 0; foreach (var chord in chordsInHs) { chordNum++; // If the handshape starts with a fret hand mute, we need to be careful if (chordNum == 1 && chord.IsFretHandMute) { startsWithMute = true; // Frethand-muted chords without techniques should not have chord notes if (chord.ChordNotes?.All(cn => cn.Sustain == 0) == true) { chord.ChordNotes = null; } } else if (chordNum == 1) { // Do not remove the chord notes even if the first chord somehow has "high density" removeHighDensity(chord, false); continue; } if (startsWithMute && !chord.IsFretHandMute) { // Do not remove the chord notes on the first non-muted chord after muted chord(s) removeHighDensity(chord, false); startsWithMute = false; } else { removeHighDensity(chord, true); } } } } if (hiDensRemoved > 0) { Log($"{hiDensRemoved} high density statuses removed."); } }
public void Apply(InstrumentalArrangement arrangement, Action <string> Log) { var chordRenamed = new Dictionary <string, bool>(); Log("Processing chord names..."); for (int i = 0; i < arrangement.ChordTemplates.Count; i++) { var currentChordTemplate = arrangement.ChordTemplates[i]; string chordName = currentChordTemplate.Name; // One fret handshape fret moving if (chordName.StartsWith("OF")) { Match match = Regex.Match(chordName, @"\d+$"); if (match.Success) { sbyte newFretNumber = sbyte.Parse(match.Value, NumberFormatInfo.InvariantInfo); for (int fretIndex = 0; fretIndex < 6; fretIndex++) { if (currentChordTemplate.Frets[fretIndex] == 0) { // Remove unnecessary open string notes currentChordTemplate.Frets[fretIndex] = -1; } else if (currentChordTemplate.Frets[fretIndex] != -1) { currentChordTemplate.Frets[fretIndex] = newFretNumber; } } Log($"Adjusted fret number of one fret chord: {currentChordTemplate.Name}"); // Remove chord name currentChordTemplate.Name = ""; currentChordTemplate.DisplayName = ""; } else { const string errorMessage = "Unable to read fret value from OF chord."; _statusMessages.Add(new ImproverMessage(errorMessage, MessageType.Warning)); Log(errorMessage); } } string oldChordName = currentChordTemplate.Name; string oldDisplayName = currentChordTemplate.DisplayName; string newChordName = string.IsNullOrWhiteSpace(oldChordName) ? string.Empty : oldChordName.Replace("min", "m") .Replace("CONV", "") .Replace("-nop", "") .Replace("-arp", ""); string newDisplayName = string.IsNullOrWhiteSpace(oldDisplayName) ? string.Empty : oldDisplayName.Replace("min", "m") .Replace("CONV", "-arp"); // Log message for changed chord names that are not empty if (newChordName != oldChordName && newChordName.Length != 0 && !chordRenamed.ContainsKey(oldChordName)) { if (oldChordName.Contains("CONV") || oldChordName.Contains("-arp")) { Log($"--Converted {newChordName} handshape into an arpeggio."); } else { Log($"--Renamed \"{oldChordName}\" to \"{newChordName}\""); } // Display renamed chords with the same name only once chordRenamed[oldChordName] = true; } currentChordTemplate.Name = newChordName; currentChordTemplate.DisplayName = newDisplayName; } }
public void Apply(InstrumentalArrangement arrangement, Action <string> Log) { var ChordTemplates = arrangement.ChordTemplates; for (int chordId = 0; chordId < ChordTemplates.Count; chordId++) { var currentChordTemplate = ChordTemplates[chordId]; string chordName = currentChordTemplate.Name; if (chordName.Contains("FIXOPEN")) { // Remove open notes from the chord template for (int i = 0; i < 6; ++i) { if (currentChordTemplate.Frets[i] == 0) { currentChordTemplate.Frets[i] = -1; } } foreach (var level in arrangement.Levels) { var chordsToFix = from chord in level.Chords where chord.ChordId == chordId select chord; foreach (var chord in chordsToFix) { if (chord.ChordNotes is null) { Log("ERROR: FIXOPEN chord does not have chord notes at " + chord.Time.TimeToString()); continue; } var chordNotesToRemove = chord.ChordNotes.Where(cn => cn.Fret == 0).ToArray(); // Store sustain of chord int initSustain = chordNotesToRemove[0].Sustain; foreach (var chordNote in chordNotesToRemove) { int sustain = initSustain; var noteToRemove = (from note in level.Notes where note.Fret == 0 && note.String == chordNote.String && note.Time > chord.Time select note).FirstOrDefault(); while (noteToRemove is not null) { sustain += noteToRemove.Sustain; if (noteToRemove.IsLinkNext) { level.Notes.Remove(noteToRemove); int nextIndex = level.Notes.FindIndex(n => n.Time > chordNote.Time && n.String == chordNote.String); noteToRemove = nextIndex == -1 ? null : level.Notes[nextIndex]; } else { level.Notes.Remove(noteToRemove); break; } } // Add new note var newNote = new Note(chordNote) { Sustain = sustain, IsLinkNext = false }; int insertIndex = level.Notes.FindIndex(n => n.Time > chord.Time); if (insertIndex != -1) { level.Notes.Insert(insertIndex, newNote); } else { // Should not be able to get here level.Notes.Insert(0, newNote); } // Remove open notes from chord chord.ChordNotes.Remove(chordNote); } // Move chord and handshape forward 20 ms (hack for DDC) const int amountToMove = 20; var handShape = level.HandShapes.First(hs => hs.StartTime == chord.Time); handShape.StartTime += amountToMove; chord.Time += amountToMove; foreach (var cn in chord.ChordNotes) { cn.Time += amountToMove; // Reduce sustain by amount moved if (cn.Sustain >= amountToMove) { cn.Sustain -= amountToMove; } } Log($"Applied open note sustain fix to chord at {chord.Time.TimeToString()}"); } } // Set correct name currentChordTemplate.Name = currentChordTemplate.Name.Replace("FIXOPEN", ""); currentChordTemplate.DisplayName = currentChordTemplate.DisplayName.Replace("FIXOPEN", ""); } } }
public void Apply(InstrumentalArrangement arrangement, Action <string> Log) { var phrasesToMove = arrangement.Phrases .Where(p => p.Name.StartsWith("moveto", StringComparison.OrdinalIgnoreCase) || p.Name.StartsWith("moveR", StringComparison.OrdinalIgnoreCase)) .ToList(); if (phrasesToMove.Count > 0) { Log("Processing 'move' phrases:"); } else { return; } foreach (var phraseToMove in phrasesToMove) { // Find phrase iterations by matching phrase index int phraseId = arrangement.Phrases.IndexOf(phraseToMove); foreach (var phraseIterationToMove in arrangement.PhraseIterations.Where(pi => pi.PhraseId == phraseId)) { int phraseTime = phraseIterationToMove.Time; int movetoTime; string phraseToMoveName = phraseToMove.Name; // Relative phrase moving right if (phraseToMoveName.StartsWith("moveR", StringComparison.OrdinalIgnoreCase)) { if (int.TryParse(phraseToMoveName.Substring("moveR".Length), NumberStyles.Integer, NumberFormatInfo.InvariantInfo, out int moveRightBy)) { var level = arrangement.Levels[phraseToMove.MaxDifficulty]; movetoTime = TimeParser.FindTimeOfNthNoteFrom(level, phraseTime, moveRightBy); } else { string errorMessage = $"Unable to read value for 'moveR' phrase at {phraseTime.TimeToString()}. (Usage example: moveR2)"; _statusMessages.Add(new ImproverMessage(errorMessage, MessageType.Warning)); Log(errorMessage); continue; } } else // Parse the absolute time to move to from the phrase name { int?parsedTime = TimeParser.Parse(phraseToMoveName); if (parsedTime.HasValue) { movetoTime = parsedTime.Value; } else { MoveToParseFailure(phraseTime, Log); continue; } } // Check if anchor(s) should be moved foreach (var level in arrangement.Levels) { if (level.Difficulty > phraseToMove.MaxDifficulty) { break; } var anchors = level.Anchors; int originalAnchorIndex = anchors.FindIndexByTime(phraseTime); int movetoAnchorIndex = anchors.FindIndexByTime(movetoTime); // If there is an anchor at the original position, but not at the new position, move it if (originalAnchorIndex != -1 && movetoAnchorIndex == -1) { var originalAnchor = anchors[originalAnchorIndex]; anchors.Insert(originalAnchorIndex + 1, new Anchor(originalAnchor.Fret, movetoTime, originalAnchor.Width)); // Remove anchor at original phrase position if no note or chord present if (level.Notes.FindIndexByTime(phraseTime) == -1 && level.Chords.FindIndexByTime(phraseTime) == -1) { anchors.RemoveAt(originalAnchorIndex); Log($"--Moved anchor from {phraseTime.TimeToString()} to {movetoTime.TimeToString()}"); } else { Log($"--Added anchor at {movetoTime.TimeToString()}"); } } } // Move phraseIteration phraseIterationToMove.Time = movetoTime; // Move section (if present) var sectionToMove = arrangement.Sections.FindByTime(phraseTime); if (sectionToMove is not null) { sectionToMove.Time = movetoTime; Log($"--Moved phrase and section from {phraseTime.TimeToString()} to {movetoTime.TimeToString()}"); } else { Log($"--Moved phrase from {phraseTime.TimeToString()} to {movetoTime.TimeToString()}"); } // Add new temporary beat var beatToAdd = new Ebeat(movetoTime, XMLProcessor.TempMeasureNumber); var insertIndex = arrangement.Ebeats.FindIndex(b => b.Time > movetoTime); arrangement.Ebeats.Insert(insertIndex, beatToAdd); // Set the beat for later removal _addedBeats.Add(beatToAdd); } } }