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 Feb 7, 2025
1 parent 38eefed commit 879949b
Show file tree
Hide file tree
Showing 8 changed files with 141 additions and 73 deletions.
35 changes: 34 additions & 1 deletion DryWetMidi.Tests/Common/RedBlackTree/RedBlackTreeTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,6 @@
using System;
using Melanchall.DryWetMidi.Common;
using System.Drawing;
using Melanchall.DryWetMidi.Tests.Multimedia;

namespace Melanchall.DryWetMidi.Tests.Common
{
Expand All @@ -31,6 +30,8 @@ public bool Equals(KeyValue other) =>
public void Enumerate_Empty()
{
var tree = new RedBlackTree<int, int>();
Assert.AreEqual(0, tree.Count, "Invalid initial count.");

var enumerated = tree.ToArray();

CollectionAssert.IsEmpty(enumerated, "Enumerated collection is not empty.");
Expand All @@ -43,6 +44,8 @@ public void Enumerate([Values(0, 1, 2, 3, 4, 5, 10, 100, 1000, 10000)] int count
var data = Enumerable.Range(0, count).Select(_ => random.Next(1000)).ToArray();

var tree = new RedBlackTree<int, int>(data, d => d);
Assert.AreEqual(count, tree.Count, "Invalid initial count.");

var enumerated = tree.ToArray();

CollectionAssert.AreEqual(
Expand All @@ -56,10 +59,13 @@ public void InsertOne([Values(0, 10, 40, -2, 1000, 12, 2, 9, 11)] int value)
{
var data = new[] { 2, 3, 1, 1, 10, 100, 50, 45, 0 };
var tree = new RedBlackTree<int, int>(data, d => d);
Assert.AreEqual(data.Length, tree.Count, "Invalid initial count.");

CheckAscendingOrder(tree.ToArray());

tree.Add(value, value);
Assert.AreEqual(data.Length + 1, tree.Count, "Invalid count after add.");

CheckAscendingOrder(tree.ToArray());
}

Expand All @@ -68,14 +74,17 @@ public void InsertMultiple()
{
var data = new[] { 2, 3, 1, 1, 10, 100, 50, 45, 0 }.ToList();
var tree = new RedBlackTree<int, int>(data, d => d);
Assert.AreEqual(data.Count, tree.Count, "Invalid initial count.");

CheckAscendingOrder(tree.ToArray());

var dataToAdd = new[] { 0, 10, 40, -2, 1000, 12, 2, 9, 11 };
var count = data.Count;

foreach (var d in dataToAdd)
{
tree.Add(d, d);
Assert.AreEqual(++count, tree.Count, $"Invalid count after {d} addition.");
CheckAscendingOrder(tree.ToArray());
}
}
Expand All @@ -85,10 +94,13 @@ public void DeleteOne([Values(500, 100, 2, 4, 6, 9, 10, 700, 701, 702, 45, 44, 4
{
var data = Enumerable.Range(0, 1000).ToList();
var tree = new RedBlackTree<int, int>(data, d => d);
Assert.AreEqual(data.Count, tree.Count, "Invalid initial count.");

CheckAscendingOrder(tree.ToArray());

tree.Delete(tree.GetFirstNode(value));
Assert.AreEqual(data.Count - 1, tree.Count, "Invalid count after deletion.");

CheckAscendingOrder(tree.ToArray());
}

Expand All @@ -97,18 +109,39 @@ public void DeleteMultiple()
{
var data = Enumerable.Range(0, 1000).ToList();
var tree = new RedBlackTree<int, int>(data, d => d);
Assert.AreEqual(data.Count, tree.Count, "Invalid initial count.");

CheckAscendingOrder(tree.ToArray());

var dataToDelete = new[] { 500, 100, 2, 4, 6, 9, 10, 700, 701, 702, 45, 44, 43 };
var count = data.Count;

foreach (var d in dataToDelete)
{
tree.Delete(tree.GetFirstNode(d));
Assert.AreEqual(--count, tree.Count, $"Invalid count after {d} deletion.");

CheckAscendingOrder(tree.ToArray());
}
}

[Test]
public void DeleteNonExisting()
{
var data = Enumerable.Range(0, 1000).ToList();
var tree = new RedBlackTree<int, int>(data, d => d);
Assert.AreEqual(data.Count, tree.Count, "Invalid initial count.");

CheckAscendingOrder(tree.ToArray());

var nodeToDelete = tree.GetFirstNode(500);
tree.Delete(nodeToDelete);
Assert.AreEqual(data.Count - 1, tree.Count, $"Invalid count after first deletion.");

tree.Delete(nodeToDelete);
Assert.AreEqual(data.Count - 1, tree.Count, $"Invalid count after second deletion.");
}

[Test]
public void GetNextNode([Values(1, 2, 4, 8, 16, 32, 64, 128, 10, 100, 1000)] int count)
{
Expand Down
2 changes: 1 addition & 1 deletion DryWetMidi.Tests/Interaction/ValueLine/ValueLineTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -282,7 +282,7 @@ public void SetValue_NonDefault_AtStart_Default_AtStart(bool checkValueChangesFi
valueChangeTime1: 0,
value2: defaultValue,
valueChangeTime2: 0,
expectedValueChanges: new[] { new ValueChange<string>(0, defaultValue) },
expectedValueChanges: Array.Empty<ValueChange<string>>(),
expectedValueAtStart: defaultValue,
expectedValueBeforeFirstChange: defaultValue,
expectedValueAtFirstChange: defaultValue,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -48,7 +48,13 @@ public void EventPlayed_AllEventsTypes()
.Select(type => type == typeof(UnknownMetaEvent)
? new UnknownMetaEvent(0)
: (MidiEvent)Activator.CreateInstance(type, true))
.Select(midiEvent => new EventToSend(midiEvent, TimeSpan.FromMilliseconds(delay)))
.Select(midiEvent =>
{
if (midiEvent is SetTempoEvent)
midiEvent = new SetTempoEvent(500001);

return new EventToSend(midiEvent, TimeSpan.FromMilliseconds(delay));
})
.ToArray();

CheckEventPlayedEvent(
Expand Down
34 changes: 31 additions & 3 deletions DryWetMidi/Common/RedBlackTree/RedBlackTree.cs
Original file line number Diff line number Diff line change
Expand Up @@ -31,8 +31,20 @@ public RedBlackTree(IEnumerable<TValue> values, Func<TValue, TKey> keySelector)

#endregion

#region Properties

public int Count { get; private set; }

#endregion

#region Methods

// TODO: test
public void Clear()
{
_root = RedBlackTreeNode<TKey, TValue>.Void;
}

public RedBlackTreeNode<TKey, TValue> GetRoot()
{
return _root;
Expand Down Expand Up @@ -115,7 +127,7 @@ public RedBlackTreeNode<TKey, TValue> GetMinimumNode(RedBlackTreeNode<TKey, TVal
{
while (!IsVoid(node?.Left))
node = node.Left;
return node;
return NodeOrNull(node);
}

public RedBlackTreeNode<TKey, TValue> GetMaximumNode()
Expand All @@ -127,7 +139,7 @@ public RedBlackTreeNode<TKey, TValue> GetMaximumNode(RedBlackTreeNode<TKey, TVal
{
while (!IsVoid(node?.Right))
node = node.Right;
return node;
return NodeOrNull(node);
}

public RedBlackTreeNode<TKey, TValue> GetNextNode(RedBlackTreeNode<TKey, TValue> node)
Expand Down Expand Up @@ -185,7 +197,7 @@ public RedBlackTreeNode<TKey, TValue> Add(TKey key, TValue value)

public void Delete(RedBlackTreeNode<TKey, TValue> node)
{
if (IsVoid(node))
if (IsVoid(node) || !node.IsInTree)
return;

RedBlackTreeNode<TKey, TValue> x = null;
Expand Down Expand Up @@ -221,6 +233,9 @@ public void Delete(RedBlackTreeNode<TKey, TValue> node)
}
if (yOriginalColor == RedBlackTreeNodeColor.Black)
DeleteFixup(x);

node.IsInTree = false;
Count--;
}

public RedBlackTreeNode<TKey, TValue> GetLastNodeBelowThreshold(TKey threshold)
Expand Down Expand Up @@ -289,6 +304,11 @@ public RedBlackTreeNode<TKey, TValue> GetLastNodeBelowThreshold<TValueKey>(
}
}

private RedBlackTreeNode<TKey, TValue> NodeOrNull(RedBlackTreeNode<TKey, TValue> node)
{
return IsVoid(node) ? null : node;
}

private bool IsVoid(RedBlackTreeNode<TKey, TValue> node)
{
return node == null || node == RedBlackTreeNode<TKey, TValue>.Void;
Expand All @@ -312,6 +332,9 @@ private void DeleteFixup(RedBlackTreeNode<TKey, TValue> x)
if (x == x.Parent.Left)
{
var w = x.Parent.Right;
if (IsVoid(w))
break;

if (w.Color == RedBlackTreeNodeColor.Red)
{
w.Color = RedBlackTreeNodeColor.Black;
Expand Down Expand Up @@ -343,6 +366,9 @@ private void DeleteFixup(RedBlackTreeNode<TKey, TValue> x)
else
{
var w = x.Parent.Left;
if (IsVoid(w))
break;

if (w.Color == RedBlackTreeNodeColor.Red)
{
w.Color = RedBlackTreeNodeColor.Black;
Expand Down Expand Up @@ -398,6 +424,8 @@ private void Insert(RedBlackTreeNode<TKey, TValue> z)
z.Right = RedBlackTreeNode<TKey, TValue>.Void;
z.Color = RedBlackTreeNodeColor.Red;
InsertFixup(z);

Count++;
}

private void InsertFixup(RedBlackTreeNode<TKey, TValue> z)
Expand Down
2 changes: 2 additions & 0 deletions DryWetMidi/Common/RedBlackTree/RedBlackTreeNode.cs
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,8 @@ public RedBlackTreeNode(TKey key, TValue value, RedBlackTreeNode<TKey, TValue> p

public RedBlackTreeNodeColor Color { get; set; } = RedBlackTreeNodeColor.Black;

public bool IsInTree { get; set; } = true;

public RedBlackTreeNode<TKey, TValue> Clone()
{
if (this == Void)
Expand Down
13 changes: 11 additions & 2 deletions DryWetMidi/Interaction/ValueLine/ValueChange.cs
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ namespace Melanchall.DryWetMidi.Interaction
/// Represents a change of a parameter's value at some time.
/// </summary>
/// <typeparam name="TValue">Type of value.</typeparam>
public sealed class ValueChange<TValue> : ITimedObject
public sealed class ValueChange<TValue> : ITimedObject, IEquatable<ValueChange<TValue>>
{
#region Fields

Expand Down Expand Up @@ -106,6 +106,15 @@ public ITimedObject Clone()

#endregion

#region IEquatable<ValueChange<TValue>>

public bool Equals(ValueChange<TValue> other)
{
return this == other;
}

#endregion

#region Overrides

/// <summary>
Expand All @@ -124,7 +133,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 ValueChange<TValue>);
return Equals(this, obj as ValueChange<TValue>);
}

/// <summary>
Expand Down
Loading

0 comments on commit 879949b

Please sign in to comment.