diff --git a/DryWetMidi.Tests/Multimedia/Playback/PlaybackTests.Asserts.cs b/DryWetMidi.Tests/Multimedia/Playback/PlaybackTests.Asserts.cs index 910795456..d5972c3b3 100644 --- a/DryWetMidi.Tests/Multimedia/Playback/PlaybackTests.Asserts.cs +++ b/DryWetMidi.Tests/Multimedia/Playback/PlaybackTests.Asserts.cs @@ -46,7 +46,8 @@ private void CheckPlayback( ICollection expectedReceivedEvents, Action setupPlayback = null, Action afterStart = null, - int? repeatsCount = null) + int? repeatsCount = null, + Action additionalChecks = null) { var outputDevice = useOutputDevice ? (IOutputDevice)OutputDevice.GetByName(SendReceiveUtilities.DeviceToTestOnName) @@ -113,6 +114,8 @@ private void CheckPlayback( sendReceiveTimeDelta: useOutputDevice ? SendReceiveUtilities.MaximumEventSendReceiveDelay : TimeSpan.FromMilliseconds(10)); + + additionalChecks?.Invoke(playback); } } } diff --git a/DryWetMidi.Tests/Multimedia/Playback/PlaybackTests.ObservableCollection.Add.cs b/DryWetMidi.Tests/Multimedia/Playback/PlaybackTests.ObservableCollection.Add.cs index 7f81a7c64..5d82b2c56 100644 --- a/DryWetMidi.Tests/Multimedia/Playback/PlaybackTests.ObservableCollection.Add.cs +++ b/DryWetMidi.Tests/Multimedia/Playback/PlaybackTests.ObservableCollection.Add.cs @@ -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 } } diff --git a/DryWetMidi.Tests/Multimedia/Playback/PlaybackTests.ObservableCollection.Common.cs b/DryWetMidi.Tests/Multimedia/Playback/PlaybackTests.ObservableCollection.Common.cs index 918a9a34f..2281edeb0 100644 --- a/DryWetMidi.Tests/Multimedia/Playback/PlaybackTests.ObservableCollection.Common.cs +++ b/DryWetMidi.Tests/Multimedia/Playback/PlaybackTests.ObservableCollection.Common.cs @@ -89,7 +89,8 @@ private void CheckPlaybackDataChangesOnTheFly( PlaybackChanger[] actions, ICollection expectedReceivedEvents, Action setupPlayback = null, - int? repeatsCount = null) + int? repeatsCount = null, + Action additionalChecks = null) { var collection = new ObservableTimedObjectsCollection(initialObjects); @@ -101,7 +102,8 @@ private void CheckPlaybackDataChangesOnTheFly( .ToArray(), expectedReceivedEvents: expectedReceivedEvents, setupPlayback: setupPlayback, - repeatsCount: repeatsCount); + repeatsCount: repeatsCount, + additionalChecks: additionalChecks); } private void CheckDuration( diff --git a/DryWetMidi/Interaction/TempoMap/Tempo.cs b/DryWetMidi/Interaction/TempoMap/Tempo.cs index db2e56d7c..81118e1d8 100644 --- a/DryWetMidi/Interaction/TempoMap/Tempo.cs +++ b/DryWetMidi/Interaction/TempoMap/Tempo.cs @@ -8,7 +8,7 @@ namespace Melanchall.DryWetMidi.Interaction /// Represents tempo expressed in microseconds per quarter note or beats per minute. /// /// - public sealed class Tempo + public sealed class Tempo : IEquatable { #region Constants @@ -238,6 +238,15 @@ public static Tempo FromBeatsPerMinute(double beatsPerMinute) #endregion + #region IEquatable + + public bool Equals(Tempo other) + { + return this == other; + } + + #endregion + #region Overrides /// @@ -256,7 +265,7 @@ public override string ToString() /// true if the specified object is equal to the current object; otherwise, false. public override bool Equals(object obj) { - return this == (obj as Tempo); + return Equals(obj as Tempo); } /// diff --git a/DryWetMidi/Interaction/TempoMap/TimeSignature.cs b/DryWetMidi/Interaction/TempoMap/TimeSignature.cs index b50734bf4..ee8bd1fbe 100644 --- a/DryWetMidi/Interaction/TempoMap/TimeSignature.cs +++ b/DryWetMidi/Interaction/TempoMap/TimeSignature.cs @@ -8,7 +8,7 @@ namespace Melanchall.DryWetMidi.Interaction /// Represents time signature which is number of beats of specified length. /// /// - public sealed class TimeSignature + public sealed class TimeSignature : IEquatable { #region Constants @@ -210,6 +210,15 @@ public TimeSignature(int numerator, int denominator) #endregion + #region IEquatable + + public bool Equals(TimeSignature other) + { + return this == other; + } + + #endregion + #region Overrides /// @@ -228,7 +237,7 @@ public override string ToString() /// true if the specified object is equal to the current object; otherwise, false. public override bool Equals(object obj) { - return this == (obj as TimeSignature); + return Equals(obj as TimeSignature); } /// diff --git a/DryWetMidi/Multimedia/Playback/Playback.DataManagement.cs b/DryWetMidi/Multimedia/Playback/Playback.DataManagement.cs index 48fd84da7..94d6a4f5d 100644 --- a/DryWetMidi/Multimedia/Playback/Playback.DataManagement.cs +++ b/DryWetMidi/Multimedia/Playback/Playback.DataManagement.cs @@ -44,6 +44,8 @@ private void OnObservableTimedObjectsCollectionChanged(object sender, Observable lock (_playbackLockObject) { + var isRunning = IsRunning; + var maxTime = TimeSpan.Zero; var maxTimeInTicks = 0L; @@ -90,7 +92,7 @@ private void OnObservableTimedObjectsCollectionChanged(object sender, Observable _beforeStart = true; } - if (IsRunning) + if (isRunning) { SendTrackedData(); StopStartNotes(); @@ -253,6 +255,20 @@ private void AddTimedObject( // _durationInTicks = e.RawTime; //} } + + // + + var timedEvent = timedObject as TimedEvent; + if (timedEvent != null) + { + TryAddSetTempoEvent(timedEvent); + + var timeSignatureEvent = timedEvent.Event as TimeSignatureEvent; + if (timeSignatureEvent != null) + { + + } + } } private NoteId GetNoteId(NoteEvent noteEvent) @@ -260,6 +276,91 @@ private NoteId GetNoteId(NoteEvent noteEvent) return new NoteId(noteEvent.Channel, noteEvent.NoteNumber); } + private bool TryAddSetTempoEvent(TimedEvent timedEvent) + { + if (timedEvent == null) + return false; + + var setTempoEvent = timedEvent.Event as SetTempoEvent; + if (setTempoEvent == null) + return false; + + var tempoChangeTime = (TimeSpan)timedEvent.TimeAs(TempoMap); + var firstNodeAfterTempoChange = _playbackEvents.GetLastNodeBelowThreshold(tempoChangeTime); + + while (true) + { + firstNodeAfterTempoChange = _playbackEvents.GetNextNode(firstNodeAfterTempoChange); + if (firstNodeAfterTempoChange == null || firstNodeAfterTempoChange.Key > tempoChangeTime) + break; + } + + if (firstNodeAfterTempoChange == null) + return false; + + // + + var oldTempo = TempoMap.TempoLine.GetValueAtTime(0); + TimeSpan? nextTempoTime = null; + + var valuesChanges = TempoMap.TempoLine.ToArray(); + + for (var i = 0; i < valuesChanges.Length; i++) + { + var valueChange = valuesChanges[i]; + if (valueChange.Time <= timedEvent.Time) + oldTempo = valueChange.Value; + else + { + nextTempoTime = TimeConverter.ConvertTo(valueChange.Time, TempoMap); + break; + } + } + + var newTempo = new Tempo(setTempoEvent.MicrosecondsPerQuarterNote); + if (oldTempo == newTempo) + return true; + + // + + var scaleFactor = oldTempo.MicrosecondsPerQuarterNote / (double)newTempo.MicrosecondsPerQuarterNote; + var shift = TimeSpan.Zero; + var node = firstNodeAfterTempoChange; + + do + { + if (nextTempoTime != null && node.Key > nextTempoTime) + node.Key -= shift; + else + { + var oldTime = node.Key; + var newTime = node.Key = TimeSpan.FromTicks(tempoChangeTime.Ticks + MathUtilities.RoundToLong((node.Key.Ticks - tempoChangeTime.Ticks) / scaleFactor)); + shift = oldTime - newTime; + } + + node.Value.Time.Time = node.Key; + } + while ((node = _playbackEvents.GetNextNode(node)) != null); + + // + + var currentTime = _clock.CurrentTime; + if (currentTime > tempoChangeTime) + { + if (nextTempoTime != null && currentTime > nextTempoTime) + currentTime -= shift; + else + currentTime = TimeSpan.FromTicks(tempoChangeTime.Ticks + MathUtilities.RoundToLong((currentTime.Ticks - tempoChangeTime.Ticks) / scaleFactor)); + + _clock.SetCurrentTime(currentTime); + } + + // + + TempoMap.TempoLine.SetValue(timedEvent.Time, newTempo); + return true; + } + private bool TryAddNoteEvent( ITimedObject timedObject, TempoMap tempoMap, @@ -369,31 +470,47 @@ private IEnumerable GetPlaybackEvents(Chord chord, TempoMap tempo private IEnumerable GetPlaybackEvents(Note note, TempoMap tempoMap, ITimedObject objectReference) { - TimeSpan noteStartTime = note.TimeAs(tempoMap); - TimeSpan noteEndTime = TimeConverter.ConvertTo(note.EndTime, tempoMap); + var noteStartTime = new PlaybackTime(note.TimeAs(tempoMap)); + var noteEndTime = new PlaybackTime(TimeConverter.ConvertTo(note.EndTime, tempoMap)); var noteMetadata = new NotePlaybackEventMetadata(note, noteStartTime, noteEndTime); - yield return GetPlaybackEventWithNoteMetadata(note.GetTimedNoteOnEvent(), tempoMap, noteMetadata, objectReference); - yield return GetPlaybackEventWithNoteMetadata(note.GetTimedNoteOffEvent(), tempoMap, noteMetadata, objectReference); + yield return GetPlaybackEventWithNoteMetadata( + note.GetTimedNoteOnEvent(), + noteStartTime, + tempoMap, + noteMetadata, + objectReference); + + yield return GetPlaybackEventWithNoteMetadata( + note.GetTimedNoteOffEvent(), + noteEndTime, + tempoMap, + noteMetadata, + objectReference); } private PlaybackEvent GetPlaybackEventWithNoteMetadata( TimedEvent timedEvent, + PlaybackTime time, TempoMap tempoMap, NotePlaybackEventMetadata noteMetadata, ITimedObject objectReference) { - var playbackEvent = CreatePlaybackEvent(timedEvent, tempoMap, objectReference); + var playbackEvent = CreatePlaybackEvent(timedEvent, tempoMap, objectReference, time); playbackEvent.Metadata.Note = noteMetadata; playbackEvent.Metadata.TimedEvent = new TimedEventPlaybackEventMetadata((timedEvent as IMetadata)?.Metadata); return playbackEvent; } - private PlaybackEvent CreatePlaybackEvent(TimedEvent timedEvent, TempoMap tempoMap, ITimedObject objectReference) + private PlaybackEvent CreatePlaybackEvent( + TimedEvent timedEvent, + TempoMap tempoMap, + ITimedObject objectReference, + PlaybackTime time = null) { return new PlaybackEvent( timedEvent.Event, - timedEvent.TimeAs(tempoMap), + time ?? new PlaybackTime(timedEvent.TimeAs(tempoMap)), timedEvent.Time, objectReference); } diff --git a/DryWetMidi/Multimedia/Playback/Playback.Misc.cs b/DryWetMidi/Multimedia/Playback/Playback.Misc.cs index c962d0169..17de3aa87 100644 --- a/DryWetMidi/Multimedia/Playback/Playback.Misc.cs +++ b/DryWetMidi/Multimedia/Playback/Playback.Misc.cs @@ -124,17 +124,17 @@ public Playback(IEnumerable timedObjects, TempoMap tempoMap, Playb playbackSettings = playbackSettings ?? new PlaybackSettings(); - TempoMap = tempoMap; + TempoMap = tempoMap.Clone(); InitializeDataTracking(); - InitializeData(timedObjects, tempoMap, playbackSettings.NoteDetectionSettings ?? new NoteDetectionSettings()); + InitializeData(timedObjects, TempoMap, playbackSettings.NoteDetectionSettings ?? new NoteDetectionSettings()); UpdateDuration(); var clockSettings = playbackSettings.ClockSettings ?? new MidiClockSettings(); _clock = new MidiClock(false, clockSettings.CreateTickGeneratorCallback(), ClockInterval); _clock.Ticked += OnClockTicked; - Snapping = new PlaybackSnapping(_playbackEvents, tempoMap); + Snapping = new PlaybackSnapping(_playbackEvents, TempoMap); } /// diff --git a/DryWetMidi/Multimedia/Playback/PlaybackEvent.cs b/DryWetMidi/Multimedia/Playback/PlaybackEvent.cs index 5e387ab59..061b24499 100644 --- a/DryWetMidi/Multimedia/Playback/PlaybackEvent.cs +++ b/DryWetMidi/Multimedia/Playback/PlaybackEvent.cs @@ -12,7 +12,7 @@ internal sealed class PlaybackEvent : IEquatable public PlaybackEvent( MidiEvent midiEvent, - TimeSpan time, + PlaybackTime time, long rawTime, ITimedObject objectReference) { @@ -28,7 +28,7 @@ public PlaybackEvent( public MidiEvent Event { get; } - public TimeSpan Time { get; } + public PlaybackTime Time { get; } public long RawTime { get; } diff --git a/DryWetMidi/Multimedia/Playback/PlaybackEventMetadata/NotePlaybackEventMetadata.cs b/DryWetMidi/Multimedia/Playback/PlaybackEventMetadata/NotePlaybackEventMetadata.cs index 519f66a17..345ef1d9c 100644 --- a/DryWetMidi/Multimedia/Playback/PlaybackEventMetadata/NotePlaybackEventMetadata.cs +++ b/DryWetMidi/Multimedia/Playback/PlaybackEventMetadata/NotePlaybackEventMetadata.cs @@ -7,7 +7,10 @@ internal sealed class NotePlaybackEventMetadata : IEquatable