Skip to content

Commit

Permalink
Support on-the-fly tempo changes in Playback (#78)
Browse files Browse the repository at this point in the history
  • Loading branch information
melanchall committed Jan 31, 2025
1 parent 606d5ed commit 48b25d6
Show file tree
Hide file tree
Showing 11 changed files with 431 additions and 24 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -46,7 +46,8 @@ private void CheckPlayback(
ICollection<ReceivedEvent> expectedReceivedEvents,
Action<Playback> setupPlayback = null,
Action<Playback> afterStart = null,
int? repeatsCount = null)
int? repeatsCount = null,
Action<Playback> additionalChecks = null)
{
var outputDevice = useOutputDevice
? (IOutputDevice)OutputDevice.GetByName(SendReceiveUtilities.DeviceToTestOnName)
Expand Down Expand Up @@ -113,6 +114,8 @@ private void CheckPlayback(
sendReceiveTimeDelta: useOutputDevice
? SendReceiveUtilities.MaximumEventSendReceiveDelay
: TimeSpan.FromMilliseconds(10));

additionalChecks?.Invoke(playback);
}
}
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3062,6 +3062,230 @@ public void CheckPlaybackDataChangesOnTheFly_AddNoteEvents_11()
setupPlayback: playback => playback.TrackNotes = true);
}

[Retry(RetriesNumber)]
[Test]
public void CheckPlaybackDataChangesOnTheFly_Add_SetTempo_1()
{
var initialObjects = new ITimedObject[]
{
new TimedEvent(new TextEvent("START"))
.SetTime(new MetricTimeSpan(0, 0, 0, 200), TempoMap),
new TimedEvent(new TextEvent("MIDDLE"))
.SetTime(new MetricTimeSpan(0, 0, 0, 400), TempoMap),
new TimedEvent(new TextEvent("END"))
.SetTime(new MetricTimeSpan(0, 0, 0, 600), TempoMap),
};

var objectToAdd = new TimedEvent(new SetTempoEvent(SetTempoEvent.DefaultMicrosecondsPerQuarterNote / 2))
.SetTime(new MetricTimeSpan(0, 0, 0, 700), TempoMap);

CheckPlaybackDataChangesOnTheFly(
initialObjects: initialObjects,
actions: new[]
{
new PlaybackChanger(100,
(playback, collection) => collection.Add(objectToAdd)),
},
expectedReceivedEvents: new[]
{
new ReceivedEvent(new TextEvent("START"), TimeSpan.FromMilliseconds(200)),
new ReceivedEvent(new TextEvent("MIDDLE"), TimeSpan.FromMilliseconds(400)),
new ReceivedEvent(new TextEvent("END"), TimeSpan.FromMilliseconds(600)),
new ReceivedEvent(new SetTempoEvent(SetTempoEvent.DefaultMicrosecondsPerQuarterNote / 2), TimeSpan.FromMilliseconds(700)),
},
additionalChecks: playback =>
{
// TODO: check tempo map
});
}

[Retry(RetriesNumber)]
[Test]
public void CheckPlaybackDataChangesOnTheFly_Add_SetTempo_2()
{
var initialObjects = new ITimedObject[]
{
new TimedEvent(new TextEvent("START"))
.SetTime(new MetricTimeSpan(0, 0, 0, 200), TempoMap),
new TimedEvent(new TextEvent("MIDDLE"))
.SetTime(new MetricTimeSpan(0, 0, 0, 400), TempoMap),
new TimedEvent(new TextEvent("END"))
.SetTime(new MetricTimeSpan(0, 0, 0, 600), TempoMap),
};

var objectToAdd = new TimedEvent(new SetTempoEvent(SetTempoEvent.DefaultMicrosecondsPerQuarterNote / 2))
.SetTime(new MetricTimeSpan(0, 0, 0, 700), TempoMap);

CheckPlaybackDataChangesOnTheFly(
initialObjects: initialObjects,
actions: new[]
{
new PlaybackChanger(400,
(playback, collection) => collection.Add(objectToAdd)),
},
expectedReceivedEvents: new[]
{
new ReceivedEvent(new TextEvent("START"), TimeSpan.FromMilliseconds(200)),
new ReceivedEvent(new TextEvent("MIDDLE"), TimeSpan.FromMilliseconds(400)),
new ReceivedEvent(new TextEvent("END"), TimeSpan.FromMilliseconds(600)),
new ReceivedEvent(new SetTempoEvent(SetTempoEvent.DefaultMicrosecondsPerQuarterNote / 2), TimeSpan.FromMilliseconds(700)),
},
additionalChecks: playback =>
{
// TODO: check tempo map
});
}

[Retry(RetriesNumber)]
[Test]
public void CheckPlaybackDataChangesOnTheFly_Add_SetTempo_3()
{
var initialObjects = new ITimedObject[]
{
new TimedEvent(new TextEvent("START"))
.SetTime(new MetricTimeSpan(0, 0, 0, 200), TempoMap),
new TimedEvent(new TextEvent("MIDDLE"))
.SetTime(new MetricTimeSpan(0, 0, 0, 400), TempoMap),
new TimedEvent(new TextEvent("END"))
.SetTime(new MetricTimeSpan(0, 0, 0, 600), TempoMap),
};

var objectToAdd = new TimedEvent(new SetTempoEvent(SetTempoEvent.DefaultMicrosecondsPerQuarterNote / 2))
.SetTime(new MetricTimeSpan(0, 0, 0, 500), TempoMap);

CheckPlaybackDataChangesOnTheFly(
initialObjects: initialObjects,
actions: new[]
{
new PlaybackChanger(100,
(playback, collection) => collection.Add(objectToAdd)),
},
expectedReceivedEvents: new[]
{
new ReceivedEvent(new TextEvent("START"), TimeSpan.FromMilliseconds(200)),
new ReceivedEvent(new TextEvent("MIDDLE"), TimeSpan.FromMilliseconds(400)),
new ReceivedEvent(new SetTempoEvent(SetTempoEvent.DefaultMicrosecondsPerQuarterNote / 2), TimeSpan.FromMilliseconds(500)),
new ReceivedEvent(new TextEvent("END"), TimeSpan.FromMilliseconds(550)),
},
additionalChecks: playback =>
{
// TODO: check tempo map
});
}

[Retry(RetriesNumber)]
[Test]
public void CheckPlaybackDataChangesOnTheFly_Add_SetTempo_4()
{
var initialObjects = new ITimedObject[]
{
new TimedEvent(new TextEvent("START"))
.SetTime(new MetricTimeSpan(0, 0, 0, 200), TempoMap),
new TimedEvent(new TextEvent("MIDDLE"))
.SetTime(new MetricTimeSpan(0, 0, 0, 400), TempoMap),
new TimedEvent(new TextEvent("END"))
.SetTime(new MetricTimeSpan(0, 0, 0, 600), TempoMap),
};

var objectToAdd = new TimedEvent(new SetTempoEvent(SetTempoEvent.DefaultMicrosecondsPerQuarterNote / 2))
.SetTime(new MetricTimeSpan(0, 0, 0, 400), TempoMap);

CheckPlaybackDataChangesOnTheFly(
initialObjects: initialObjects,
actions: new[]
{
new PlaybackChanger(100,
(playback, collection) => collection.Add(objectToAdd)),
},
expectedReceivedEvents: new[]
{
new ReceivedEvent(new TextEvent("START"), TimeSpan.FromMilliseconds(200)),
new ReceivedEvent(new TextEvent("MIDDLE"), TimeSpan.FromMilliseconds(400)),
new ReceivedEvent(new SetTempoEvent(SetTempoEvent.DefaultMicrosecondsPerQuarterNote / 2), TimeSpan.FromMilliseconds(400)),
new ReceivedEvent(new TextEvent("END"), TimeSpan.FromMilliseconds(500)),
},
additionalChecks: playback =>
{
// TODO: check tempo map
});
}

[Retry(RetriesNumber)]
[Test]
public void CheckPlaybackDataChangesOnTheFly_Add_SetTempo_5()
{
var initialObjects = new ITimedObject[]
{
new TimedEvent(new TextEvent("START"))
.SetTime(new MetricTimeSpan(0, 0, 0, 200), TempoMap),
new TimedEvent(new TextEvent("MIDDLE"))
.SetTime(new MetricTimeSpan(0, 0, 0, 400), TempoMap),
new TimedEvent(new TextEvent("END"))
.SetTime(new MetricTimeSpan(0, 0, 0, 600), TempoMap),
};

var objectToAdd = new TimedEvent(new SetTempoEvent(SetTempoEvent.DefaultMicrosecondsPerQuarterNote * 2))
.SetTime(new MetricTimeSpan(0, 0, 0, 400), TempoMap);

CheckPlaybackDataChangesOnTheFly(
initialObjects: initialObjects,
actions: new[]
{
new PlaybackChanger(100,
(playback, collection) => collection.Add(objectToAdd)),
},
expectedReceivedEvents: new[]
{
new ReceivedEvent(new TextEvent("START"), TimeSpan.FromMilliseconds(200)),
new ReceivedEvent(new TextEvent("MIDDLE"), TimeSpan.FromMilliseconds(400)),
new ReceivedEvent(new SetTempoEvent(SetTempoEvent.DefaultMicrosecondsPerQuarterNote * 2), TimeSpan.FromMilliseconds(400)),
new ReceivedEvent(new TextEvent("END"), TimeSpan.FromMilliseconds(800)),
},
additionalChecks: playback =>
{
// TODO: check tempo map
});
}

[Retry(RetriesNumber)]
[Test]
public void CheckPlaybackDataChangesOnTheFly_Add_SetTempo_6()
{
var initialObjects = new ITimedObject[]
{
new TimedEvent(new TextEvent("START"))
.SetTime(new MetricTimeSpan(0, 0, 0, 200), TempoMap),
new TimedEvent(new TextEvent("MIDDLE"))
.SetTime(new MetricTimeSpan(0, 0, 0, 400), TempoMap),
new TimedEvent(new TextEvent("END"))
.SetTime(new MetricTimeSpan(0, 0, 0, 600), TempoMap),
};

var objectToAdd = new TimedEvent(new SetTempoEvent(SetTempoEvent.DefaultMicrosecondsPerQuarterNote / 2))
.SetTime(new MetricTimeSpan(0, 0, 0, 300), TempoMap);

CheckPlaybackDataChangesOnTheFly(
initialObjects: initialObjects,
actions: new[]
{
new PlaybackChanger(400, (playback, collection) =>
{
collection.Add(objectToAdd);
CheckCurrentTime(playback, TimeSpan.FromMilliseconds(350), "Invalid current time.");
}),
},
expectedReceivedEvents: new[]
{
new ReceivedEvent(new TextEvent("START"), TimeSpan.FromMilliseconds(200)),
new ReceivedEvent(new TextEvent("MIDDLE"), TimeSpan.FromMilliseconds(400)),
new ReceivedEvent(new TextEvent("END"), TimeSpan.FromMilliseconds(500)),
},
additionalChecks: playback =>
{
// TODO: check tempo map
});
}

#endregion
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -89,7 +89,8 @@ private void CheckPlaybackDataChangesOnTheFly(
PlaybackChanger[] actions,
ICollection<ReceivedEvent> expectedReceivedEvents,
Action<Playback> setupPlayback = null,
int? repeatsCount = null)
int? repeatsCount = null,
Action<Playback> additionalChecks = null)
{
var collection = new ObservableTimedObjectsCollection(initialObjects);

Expand All @@ -101,7 +102,8 @@ private void CheckPlaybackDataChangesOnTheFly(
.ToArray(),
expectedReceivedEvents: expectedReceivedEvents,
setupPlayback: setupPlayback,
repeatsCount: repeatsCount);
repeatsCount: repeatsCount,
additionalChecks: additionalChecks);
}

private void CheckDuration(
Expand Down
13 changes: 11 additions & 2 deletions DryWetMidi/Interaction/TempoMap/Tempo.cs
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ namespace Melanchall.DryWetMidi.Interaction
/// Represents tempo expressed in microseconds per quarter note or beats per minute.
/// </summary>
/// <seealso cref="TempoMap"/>
public sealed class Tempo
public sealed class Tempo : IEquatable<Tempo>
{
#region Constants

Expand Down Expand Up @@ -238,6 +238,15 @@ public static Tempo FromBeatsPerMinute(double beatsPerMinute)

#endregion

#region IEquatable<Tempo>

public bool Equals(Tempo other)
{
return this == other;
}

#endregion

#region Overrides

/// <summary>
Expand All @@ -256,7 +265,7 @@ public override string ToString()
/// <returns><c>true</c> if the specified object is equal to the current object; otherwise, <c>false</c>.</returns>
public override bool Equals(object obj)
{
return this == (obj as Tempo);
return Equals(obj as Tempo);
}

/// <summary>
Expand Down
13 changes: 11 additions & 2 deletions DryWetMidi/Interaction/TempoMap/TimeSignature.cs
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ namespace Melanchall.DryWetMidi.Interaction
/// Represents time signature which is number of beats of specified length.
/// </summary>
/// <seealso cref="TempoMap"/>
public sealed class TimeSignature
public sealed class TimeSignature : IEquatable<TimeSignature>
{
#region Constants

Expand Down Expand Up @@ -210,6 +210,15 @@ public TimeSignature(int numerator, int denominator)

#endregion

#region IEquatable<TimeSignature>

public bool Equals(TimeSignature other)
{
return this == other;
}

#endregion

#region Overrides

/// <summary>
Expand All @@ -228,7 +237,7 @@ public override string ToString()
/// <returns><c>true</c> if the specified object is equal to the current object; otherwise, <c>false</c>.</returns>
public override bool Equals(object obj)
{
return this == (obj as TimeSignature);
return Equals(obj as TimeSignature);
}

/// <summary>
Expand Down
Loading

0 comments on commit 48b25d6

Please sign in to comment.