Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Implement guitar note shuffle #193

Open
wants to merge 8 commits into
base: master
Choose a base branch
from
46 changes: 42 additions & 4 deletions YARG.Core/Chart/Notes/GuitarNote.cs
Original file line number Diff line number Diff line change
Expand Up @@ -7,8 +7,46 @@ public class GuitarNote : Note<GuitarNote>
private GuitarNoteFlags _guitarFlags;
public GuitarNoteFlags GuitarFlags;

public int Fret { get; }
public int DisjointMask { get; }
private int _fret;

public int Fret
{
get => _fret;
set
{
if (value == _fret)
return;

int mask = GetNoteMask(value);
int oldMask = GetNoteMask(_fret);

// If we're a child note, adjust our parent's mask to reflect the change
if (Parent != null)
{
if ((Parent.NoteMask & mask) != 0)
throw new InvalidOperationException($"Fret {value} already exists in the current chord!");

Parent.NoteMask &= ~oldMask;
Parent.NoteMask |= mask;

NoteMask = mask;
}
// Otherwise, adjust our own mask
else
{
if ((NoteMask & mask) != 0)
throw new InvalidOperationException($"Fret {value} already exists in the current chord!");

NoteMask &= ~oldMask;
NoteMask |= mask;
}

_fret = value;
DisjointMask = mask;
}
}

public int DisjointMask { get; private set; }
public int NoteMask { get; private set; }

public uint SustainTicksHeld;
Expand Down Expand Up @@ -40,7 +78,7 @@ public GuitarNote(int fret, GuitarNoteType noteType, GuitarNoteFlags guitarFlags
double time, double timeLength, uint tick, uint tickLength)
: base(flags, time, timeLength, tick, tickLength)
{
Fret = fret;
_fret = fret;
Type = noteType;

GuitarFlags = _guitarFlags = guitarFlags;
Expand All @@ -51,7 +89,7 @@ public GuitarNote(int fret, GuitarNoteType noteType, GuitarNoteFlags guitarFlags

public GuitarNote(GuitarNote other) : base(other)
{
Fret = other.Fret;
_fret = other._fret;
Type = other.Type;

GuitarFlags = _guitarFlags = other._guitarFlags;
Expand Down
9 changes: 8 additions & 1 deletion YARG.Core/Chart/Notes/Note.cs
Original file line number Diff line number Diff line change
Expand Up @@ -74,6 +74,13 @@ public virtual void AddChildNote(TNote note)
_childNotes.Add(note);
}

public virtual void RemoveChildNote(TNote note)
{
if (!_childNotes.Remove(note))
throw new InvalidOperationException("Child note being removed is not part of this note!");
note.Parent = null;
}

public IEnumerable<TNote> ChordEnumerator()
{
yield return (TNote) this;
Expand Down Expand Up @@ -171,7 +178,7 @@ public virtual void ResetNoteState()
}
}

protected static int GetNoteMask(int note)
public static int GetNoteMask(int note)
{
// Resulting shift is 1 too high, shifting down by 1 corrects this.
// Reason for not doing (note - 1) is this breaks open notes. (1 << (0 - 1) == 0x80000000)
Expand Down
6 changes: 6 additions & 0 deletions YARG.Core/Chart/Notes/VocalNote.cs
Original file line number Diff line number Diff line change
Expand Up @@ -182,6 +182,12 @@ public override void AddChildNote(VocalNote note)
TotalTickLength = _childNotes[^1].TickEnd - Tick;
}

public override void RemoveChildNote(VocalNote note)
{
// TODO
throw new NotImplementedException();
}

Comment on lines +185 to +190
Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Wasn't sure what to do here and didn't feel like going through the AddChildNote override at the moment, so left it as a TODO.

/// <summary>
/// Adds a child note to this vocal phrase.
/// Use <see cref="AddChildNote"/> instead if this is a note!
Expand Down
164 changes: 164 additions & 0 deletions YARG.Core/Chart/Tracks/InstrumentDifficultyExtensions.cs
Original file line number Diff line number Diff line change
@@ -1,4 +1,7 @@
using System;
using System.Collections.Generic;
using YARG.Core.Extensions;
using YARG.Core.Logging;

namespace YARG.Core.Chart
{
Expand Down Expand Up @@ -34,6 +37,167 @@ public static void ConvertFromTypeToType(this InstrumentDifficulty<GuitarNote> d
}
}

public static void ShuffleNotes(this InstrumentDifficulty<GuitarNote> difficulty, int seed)
{
var random = new Random(seed);
var activeNotes = new GuitarNote?[5 + 1]; // 5 frets, one open note

var currentNotes = new List<GuitarNote>();
var availableFrets = new List<int>();
var selectedFrets = new List<int>();

double lastNoteTime = 0;
int lastNoteCount = 0;
int lastOriginalMask = 0;
int lastShuffledMask = 0;

foreach (var parent in difficulty.Notes)
{
// Reset state
// Must be done first and not last, otherwise skipping a note won't reset state
currentNotes.Clear();
selectedFrets.Clear();
availableFrets.Clear();

// Clear no-longer-active notes
for (int i = 0; i < activeNotes.Length; i++)
{
if (activeNotes[i] is not {} activeNote)
continue; // Already inactive

bool isActive = activeNote.TickLength == 0
? activeNote.TickEnd == parent.Tick // If the note has no length, its end is inclusive
: activeNote.TickEnd > parent.Tick; // Otherwise, the end is exclusive

if (!isActive)
activeNotes[i] = null;
}

// Set up grab bag
if (activeNotes[1] == null) availableFrets.Add(1);
if (activeNotes[2] == null) availableFrets.Add(2);
if (activeNotes[3] == null) availableFrets.Add(3);
if (activeNotes[4] == null) availableFrets.Add(4);
if (activeNotes[5] == null) availableFrets.Add(5);
availableFrets.Shuffle(random);

// Retrieve notes to shuffle
foreach (var note in parent.ChordEnumerator())
{
// Don't shuffle open notes
if (note.Fret == 0)
continue;

currentNotes.Add(note);
}

static double Falloff(double x, double rate, double offset)
{
// Falloff is exponential with respect to `x` and `rate`:
// - Rate of 2^0 (1) = no falloff
// - Rate of 2^1 (2) = half-life reached at 1
// - Rate of 2^2 (4) = half-life reached at 0.5
// - Rate of 2^3 (8) = half-life reached at 0.333...
// - Rate of 2^4 (16) = half-life reached at 0.25
// https://www.desmos.com/calculator/ydezyo9tou
return Math.Pow(rate, -x + offset);
}

static double DistanceWeight(double deltaTime, int minChance, int maxChance)
{
const double offset = 0.1;
const int falloffPower = 16; // Half-life reached at 0.0625 (0.1625 after offset is applied)

double falloffRate = Math.Pow(2, falloffPower);
double falloff = Falloff(deltaTime, falloffRate, offset);
return YargMath.LerpClamped(minChance, maxChance, falloff);
}

int originalMask = parent.NoteMask;
double deltaTime = parent.Time - lastNoteTime;

if (currentNotes.Count == lastNoteCount && originalMask == lastOriginalMask)
{
// Prefer to make consecutive same-fret notes still be consecutive
if (random.Next(100) < DistanceWeight(deltaTime, 60, 100))
{
for (int i = 1; i <= 5; i++)
{
if ((lastShuffledMask & GuitarNote.GetNoteMask(i)) != 0)
selectedFrets.Add(i);
}
}
}
else
{
// Prefer to keep different single notes different
if (currentNotes.Count == 1 && lastShuffledMask.CountBits(5) == 1 &&
random.Next(100) <= DistanceWeight(deltaTime, 60, 90))
{
for (int i = 1; i <= 5; i++)
{
if ((lastShuffledMask & GuitarNote.GetNoteMask(i)) != 0)
availableFrets.Remove(i);
}
}
}

lastNoteTime = parent.Time;
lastNoteCount = currentNotes.Count;
lastOriginalMask = originalMask;

// Pick a set of random notes for the chord
for (int i = selectedFrets.Count; i < currentNotes.Count; i++)
{
if (availableFrets.Count < 1)
{
// Ignore un-shuffleable notes
var note = currentNotes[i];
YargLogger.LogFormatWarning("Cannot shuffle note at {0:0.000} ({1}), removing.", note.Time, note.Tick);
continue;
}

int randomFret = availableFrets.PopRandom(random);
selectedFrets.Add(randomFret);
}

// Remove any notes that didn't make the cut
for (int i = 0; i < parent.ChildNotes.Count; i++)
{
var child = parent.ChildNotes[i];
if (!currentNotes.Contains(child) && child.Fret != 0) // Don't remove open notes
parent.RemoveChildNote(child);
}

// Skip open notes and 5-note chords
if (currentNotes.Count < 1 || currentNotes.Count >= 5)
continue;

// Sort notes/frets to prepare for the next step
currentNotes.Sort((left, right) => left.Fret.CompareTo(right.Fret));
selectedFrets.Sort();

// Push all notes to the right, to prevent intermediate overlaps
for (int i = 0; i < currentNotes.Count; i++)
{
currentNotes[^(i + 1)].Fret = 5 - i;
}

// Apply shuffled frets
YargLogger.Assert(currentNotes.Count == selectedFrets.Count);
for (int i = 0; i < selectedFrets.Count; i++)
{
int fret = selectedFrets[i];
var note = currentNotes[i];

note.Fret = fret;
activeNotes[fret] = note;
}

lastShuffledMask = parent.NoteMask;
}
}

public static void RemoveKickDrumNotes(this InstrumentDifficulty<DrumNote> difficulty)
{
var kickDrumPadIndex = difficulty.Instrument switch
Expand Down
11 changes: 11 additions & 0 deletions YARG.Core/Extensions/CollectionExtensions.cs
Original file line number Diff line number Diff line change
Expand Up @@ -48,6 +48,17 @@ public static T PickRandom<T>(this List<T> list, Random random)
return list[random.Next(0, list.Count)];
}

/// <summary>
/// Picks and removes a random value from the list using the given random number generator.
/// </summary>
public static T PopRandom<T>(this List<T> list, Random random)
{
int index = random.Next(0, list.Count);
var value = list[index];
list.RemoveAt(index);
return value;
}

/// <summary>
/// Searches for an item in the list using the given search object and comparer function.
/// </summary>
Expand Down
Loading