Skip to content
Merged
62 changes: 40 additions & 22 deletions Assets/TestsEditMode/BeatmapBpmInfoTest.cs
Original file line number Diff line number Diff line change
Expand Up @@ -82,16 +82,16 @@ public void GetFromJson_V2BpmInfo()
[Test]
public void GetFromJson_V4AudioData()
{
var bpmInfo = V4AudioData.GetFromJson(JSON.Parse(audioDataJson));
var bpmInfo = V4AudioData.GetFromJson(JSON.Parse(audioDataJson));

AssertCommonGetFromJson(bpmInfo);
Assert.AreEqual("4.0.0", bpmInfo.Version);
AssertCommonGetFromJson(bpmInfo);

Assert.AreEqual("4.0.0", bpmInfo.Version);

Assert.AreEqual(1, bpmInfo.LufsRegions.Count);
Assert.AreEqual(0, bpmInfo.LufsRegions[0].StartSampleIndex);
Assert.AreEqual(882000, bpmInfo.LufsRegions[0].EndSampleIndex);
Assert.AreEqual(3.1f, bpmInfo.LufsRegions[0].Loudness);
Assert.AreEqual(1, bpmInfo.LufsRegions.Count);
Assert.AreEqual(0, bpmInfo.LufsRegions[0].StartSampleIndex);
Assert.AreEqual(882000, bpmInfo.LufsRegions[0].EndSampleIndex);
Assert.AreEqual(3.1f, bpmInfo.LufsRegions[0].Loudness);
}

private void AssertCommonGetFromJson(BaseBpmInfo bpmInfo)
Expand Down Expand Up @@ -162,13 +162,17 @@ public void GetBpmInfoRegions()
var songBpm = 60f;
var audioFrequency = 44100;
var audiosamples = 44100 * 20;
var bpmEvents = new List<BaseBpmEvent>
var difficulty = new BaseDifficulty
{
new() { JsonTime = 0, Bpm = 60 },
new() { JsonTime = 10, Bpm = 120 }
BpmEvents = new List<BaseBpmEvent>
{
new() { JsonTime = 0, Bpm = 60 },
new() { JsonTime = 10, Bpm = 120 }
}
};
difficulty.BootstrapBpmEvents(songBpm);

var regions = BaseBpmInfo.GetBpmInfoRegions(bpmEvents, songBpm, audiosamples, audioFrequency);
var regions = BaseBpmInfo.GetBpmInfoRegions(difficulty.BpmEvents, songBpm, audiosamples, audioFrequency);
Assert.AreEqual(2, regions.Count);

Assert.AreEqual(0f, regions[0].StartBeat);
Expand Down Expand Up @@ -205,27 +209,41 @@ public void ConversionDoesNotIntroduceDriftOverTime()
var songBpm = 60f;
var audioFrequency = 44100;
var audiosamples = 44100 * 20;
var initialBpmEvents = new List<BaseBpmEvent>

var initialDifficulty = new BaseDifficulty
{
new() { JsonTime = 0, Bpm = 60 },
new() { JsonTime = 10, Bpm = 120 }
BpmEvents = new List<BaseBpmEvent>
{
new() { JsonTime = 0, Bpm = 60 },
new() { JsonTime = 10, Bpm = 120 }
}
};
initialDifficulty.BootstrapBpmEvents(songBpm);

var initialBpmEvents = initialDifficulty.BpmEvents;
var initialRegions = BaseBpmInfo.GetBpmInfoRegions(initialBpmEvents, songBpm, audiosamples, audioFrequency);

// Loop conversion to and from a bunch of times
List<BaseBpmEvent> bpmEvents = new List<BaseBpmEvent>
var difficulty = new BaseDifficulty
{
new() { JsonTime = 0, Bpm = 60 },
new() { JsonTime = 10, Bpm = 120 }
BpmEvents = new List<BaseBpmEvent>
{
new() { JsonTime = 0, Bpm = 60 },
new() { JsonTime = 10, Bpm = 120 }
}
};
difficulty.BootstrapBpmEvents(songBpm);

List<BpmInfoBpmRegion> regions = new List<BpmInfoBpmRegion>();

for (var i = 0; i < 100; i++)
{
regions = BaseBpmInfo.GetBpmInfoRegions(bpmEvents, songBpm, audiosamples, audioFrequency);
bpmEvents = BaseBpmInfo.GetBpmEvents(regions, audioFrequency);
regions = BaseBpmInfo.GetBpmInfoRegions(difficulty.BpmEvents, songBpm, audiosamples, audioFrequency);
difficulty.BpmEvents = BaseBpmInfo.GetBpmEvents(regions, audioFrequency);
difficulty.BootstrapBpmEvents(songBpm);
}

var bpmEvents = difficulty.BpmEvents;

// Compare bpm events
Assert.AreEqual(initialBpmEvents.Count, bpmEvents.Count);

Expand Down
4 changes: 2 additions & 2 deletions Assets/__Scripts/Beatmap/Animations/ObjectAnimator.cs
Original file line number Diff line number Diff line change
Expand Up @@ -178,7 +178,7 @@ public void SetData(BaseGrid obj)
{
continue;
}
var bpmChangeGridContainer = BeatmapObjectContainerCollection.GetCollectionForType<BPMChangeGridContainer>(ObjectType.BpmChange);
var map = BeatSaberSongContainer.Instance.Map;
foreach (var ce in events.Where(ev => ev.Type == "AssignPathAnimation"))
{
foreach (var jprop in ce.Data)
Expand All @@ -196,7 +196,7 @@ public void SetData(BaseGrid obj)
TimeEnd = time_end,
};
if (p.Transition != 0) {
p.Transition = bpmChangeGridContainer.JsonTimeToSongBpmTime(ce.JsonTime + p.Transition) - ce.SongBpmTime;
p.Transition = (float)map.JsonTimeToSongBpmTime(ce.JsonTime + p.Transition) - ce.SongBpmTime;
}
AddPointDef(p, jprop.Key);
}
Expand Down
8 changes: 4 additions & 4 deletions Assets/__Scripts/Beatmap/Base/BaseArc.cs
Original file line number Diff line number Diff line change
Expand Up @@ -35,13 +35,13 @@ public BaseArc()

public BaseArc(BaseArc other)
{
SetTimes(other.JsonTime, other.SongBpmTime);
SetTimes(other.JsonTime);
Color = other.Color;
PosX = other.PosX;
PosY = other.PosY;
CutDirection = other.CutDirection;
HeadControlPointLengthMultiplier = other.HeadControlPointLengthMultiplier;
SetTailTimes(other.TailJsonTime, other.TailSongBpmTime);
SetTailTimes(other.TailJsonTime);
TailPosX = other.TailPosX;
TailPosY = other.TailPosY;
TailCutDirection = other.TailCutDirection;
Expand All @@ -52,13 +52,13 @@ public BaseArc(BaseArc other)

public BaseArc(BaseNote start, BaseNote end)
{
SetTimes(start.JsonTime, start.SongBpmTime);
SetTimes(start.JsonTime);
Color = start.Color;
PosX = start.PosX;
PosY = start.PosY;
CutDirection = start.CutDirection;
HeadControlPointLengthMultiplier = 1f;
SetTailTimes(end.JsonTime, end.SongBpmTime);
SetTailTimes(end.JsonTime);
TailPosX = end.PosX;
TailPosY = end.PosY;
TailCutDirection = end.CutDirection;
Expand Down
2 changes: 1 addition & 1 deletion Assets/__Scripts/Beatmap/Base/BaseBpmEvent.cs
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,7 @@ public BaseBpmEvent() {}

public BaseBpmEvent(BaseBpmEvent other)
{
SetTimes(other.JsonTime, other.SongBpmTime);
SetTimes(other.JsonTime);
Bpm = other.Bpm;
CustomData = other.CustomData.Clone();
}
Expand Down
8 changes: 4 additions & 4 deletions Assets/__Scripts/Beatmap/Base/BaseChain.cs
Original file line number Diff line number Diff line change
Expand Up @@ -33,12 +33,12 @@ public BaseChain()

public BaseChain(BaseChain other)
{
SetTimes(other.JsonTime, other.SongBpmTime);
SetTimes(other.JsonTime);
Color = other.Color;
PosX = other.PosX;
PosY = other.PosY;
CutDirection = other.CutDirection;
SetTailTimes(other.TailJsonTime, other.TailSongBpmTime);
SetTailTimes(other.TailJsonTime);
TailPosX = other.TailPosX;
TailPosY = other.TailPosY;
SliceCount = other.SliceCount;
Expand All @@ -48,12 +48,12 @@ public BaseChain(BaseChain other)

public BaseChain(BaseNote start, BaseNote end)
{
SetTimes(start.JsonTime, start.SongBpmTime);
SetTimes(start.JsonTime);
Color = start.Color;
PosX = start.PosX;
PosY = start.PosY;
CutDirection = start.CutDirection;
SetTailTimes(end.JsonTime, end.SongBpmTime);
SetTailTimes(end.JsonTime);
TailPosX = end.PosX;
TailPosY = end.PosY;
SliceCount = 5;
Expand Down
96 changes: 96 additions & 0 deletions Assets/__Scripts/Beatmap/Base/BaseDifficulty.cs
Original file line number Diff line number Diff line change
Expand Up @@ -93,6 +93,102 @@ public List<BaseLightTranslationEventBoxGroup<BaseLightTranslationEventBox>>
new List<BaseObject>(CustomEvents),
};

#region BPM Time Conversion Logic

private float? songBpm;

public void BootstrapBpmEvents(float songBpm)
{
this.songBpm = songBpm;

// remove invalid bpm events
BpmEvents.RemoveAll(x => x.JsonTime < 0);
BpmEvents.RemoveAll(x => x.Bpm < 0);

if (!BpmEvents.Any()) return;

BpmEvents.Sort();

// insert beat 0 bpm event if needed
if (BpmEvents.First().JsonTime > 0)
{
var newBpmEvent = new BaseBpmEvent(0, songBpm);
BpmEvents.Insert(0, newBpmEvent);
}

BaseBpmEvent lastBpmEvent = null;
foreach (var bpmEvent in BpmEvents)
{
if (lastBpmEvent is null)
{
bpmEvent.songBpmTime = bpmEvent.JsonTime;
}
else
{
bpmEvent.songBpmTime = lastBpmEvent.songBpmTime + (bpmEvent.JsonTime - lastBpmEvent.JsonTime) * (songBpm / lastBpmEvent.Bpm);
}

lastBpmEvent = bpmEvent;
}
}

public float? JsonTimeToSongBpmTime(float jsonTime)
{
if (songBpm is null) return null;
var lastBpmEvent = FindLastBpmEventByJsonTime(jsonTime, inclusive: false);
if (lastBpmEvent is null)
{
return jsonTime;
}
return lastBpmEvent.SongBpmTime + (jsonTime - lastBpmEvent.JsonTime) * (songBpm / lastBpmEvent.Bpm);
}

public float? SongBpmTimeToJsonTime(float songBpmTime)
{
if (songBpm is null) return null;
var lastBpmEvent = FindLastBpmEventBySongBpmTime(songBpmTime, inclusive: false);
if (lastBpmEvent is null)
{
return songBpmTime;
}
return lastBpmEvent.JsonTime + (songBpmTime - lastBpmEvent.SongBpmTime) * (lastBpmEvent.Bpm / songBpm);
}

public BaseBpmEvent FindLastBpmEventByJsonTime(float jsonTime, bool inclusive = false)
{
return BpmEvents.LastOrDefault(x => inclusive ? x.JsonTime <= jsonTime : x.JsonTime < jsonTime);
}

public BaseBpmEvent FindLastBpmEventBySongBpmTime(float songBpmTime, bool inclusive = false)
{
if (songBpm is null) return null;
return BpmEvents.LastOrDefault(x => inclusive ? x.SongBpmTime <= songBpmTime : x.SongBpmTime < songBpmTime);
}

public float? BpmAtJsonTime(float jsonTime)
{
return FindLastBpmEventByJsonTime(jsonTime, inclusive: true)?.Bpm ?? songBpm;
}

public float? BpmAtSongBpmTime(float songBpmTime)
{
return FindLastBpmEventBySongBpmTime(songBpmTime, inclusive: true)?.Bpm ?? songBpm;
}

public void RecomputeAllObjectSongBpmTimes()
{
foreach (var objList in AllBaseObjectProperties())
{
if (objList is null) continue;
foreach (var obj in objList)
{
obj.RecomputeSongBpmTime();
}
}
}

#endregion

public void ConvertCustomBpmToOfficial()
{
var songBpm = BeatSaberSongContainer.Instance.Info.BeatsPerMinute;
Expand Down
2 changes: 1 addition & 1 deletion Assets/__Scripts/Beatmap/Base/BaseEvent.cs
Original file line number Diff line number Diff line change
Expand Up @@ -37,7 +37,7 @@ public BaseEvent()

public BaseEvent(BaseEvent other)
{
SetTimes(other.JsonTime, other.SongBpmTime);
SetTimes(other.JsonTime);
Type = other.Type;
Value = other.Value;
FloatValue = other.FloatValue;
Expand Down
4 changes: 2 additions & 2 deletions Assets/__Scripts/Beatmap/Base/BaseGrid.cs
Original file line number Diff line number Diff line change
Expand Up @@ -53,8 +53,8 @@ protected BaseGrid(float jsonTime, float songBpmTime, int posX, int posY, JSONNo

public float EditorScale { get; private set; }

public virtual float SpawnSongBpmTime { get { return SongBpmTime - Hjd; } }
public virtual float DespawnSongBpmTime { get { return SongBpmTime + Hjd; } }
public virtual float SpawnSongBpmTime => SongBpmTime - Hjd;
public virtual float DespawnSongBpmTime => SongBpmTime + Hjd;

public virtual JSONNode CustomAnimation { get; set; }

Expand Down
2 changes: 1 addition & 1 deletion Assets/__Scripts/Beatmap/Base/BaseNJSEvent.cs
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,7 @@ public BaseNJSEvent()

public BaseNJSEvent(BaseNJSEvent other)
{
SetTimes(other.JsonTime, other.SongBpmTime);
SetTimes(other.JsonTime);
UsePrevious = other.UsePrevious;
Easing = other.Easing;
RelativeNJS = other.RelativeNJS;
Expand Down
2 changes: 1 addition & 1 deletion Assets/__Scripts/Beatmap/Base/BaseNote.cs
Original file line number Diff line number Diff line change
Expand Up @@ -36,7 +36,7 @@ public BaseNote()

public BaseNote(BaseNote other)
{
SetTimes(other.JsonTime, other.SongBpmTime);
SetTimes(other.JsonTime);
PosX = other.PosX;
PosY = other.PosY;
Color = other.Color;
Expand Down
26 changes: 10 additions & 16 deletions Assets/__Scripts/Beatmap/Base/BaseObject.cs
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@ public abstract class BaseObject : BaseItem, ICustomData, IHeckObject, IChromaOb
public virtual void Serialize(NetDataWriter writer)
{
writer.Put(jsonTime);
writer.Put(songBpmTime);
writer.Put((float)songBpmTime);
writer.Put(CustomData?.ToString());
}

Expand All @@ -25,6 +25,7 @@ public virtual void Deserialize(NetDataReader reader)

protected BaseObject()
{
JsonTime = 0; // needed to set songBpmTime
}

protected BaseObject(float time, JSONNode customData = null)
Expand All @@ -49,28 +50,21 @@ public float JsonTime
get => jsonTime;
set
{
var bpmChangeGridContainer = BeatmapObjectContainerCollection.GetCollectionForType<BPMChangeGridContainer>(ObjectType.BpmChange);
songBpmTime = bpmChangeGridContainer?.JsonTimeToSongBpmTime(value) ?? value;
var map = BeatSaberSongContainer.Instance != null ? BeatSaberSongContainer.Instance.Map : null;
songBpmTime = map?.JsonTimeToSongBpmTime(value);
jsonTime = value;
}
}

private float songBpmTime;
public float SongBpmTime
{
get => songBpmTime;
set
{
var bpmChangeGridContainer = BeatmapObjectContainerCollection.GetCollectionForType<BPMChangeGridContainer>(ObjectType.BpmChange);
jsonTime = bpmChangeGridContainer?.SongBpmTimeToJsonTime(value) ?? value;
songBpmTime = value;
}
}
// should only be set directly when initializing
// read from SongBpmTime instead, and write to JsonTime to update this
// really should be private but we need to set this from BaseDifficulty on init
internal float? songBpmTime;
public float SongBpmTime => (float)songBpmTime;

public void SetTimes(float jsonTime, float songBpmTime)
public void SetTimes(float jsonTime)
{
this.jsonTime = jsonTime;
this.songBpmTime = songBpmTime;
RecomputeSongBpmTime();
}

Expand Down
Loading