Skip to content

Commit b82d95c

Browse files
committed
[Docs] Prepare docs for the release
1 parent 9b1283d commit b82d95c

File tree

64 files changed

+1148
-326
lines changed

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

64 files changed

+1148
-326
lines changed

Docs/articles/dev/Support.md

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -22,5 +22,9 @@ Things to pay attention for are:
2222
* on GitHub use [Markdown](https://docs.github.com/en/get-started/writing-on-github/getting-started-with-writing-and-formatting-on-github/basic-writing-and-formatting-syntax);
2323
* on emails use formatting provided by your mail client.
2424
9. Use the English language.
25+
10. Provide a short title and put all required info at the description.
2526

26-
Creating an issue or a discussion on the project's GitHub page, be sure to provide a short title and put all required info as the description.
27+
Examples of good issues:
28+
29+
* [Unity: Editor crash when (dis-)connecting input devices](https://github.com/melanchall/drywetmidi/issues/318)
30+
* [Playback Loop=True skips empty steps in the end of a Pattern](https://github.com/melanchall/drywetmidi/issues/298)
Lines changed: 354 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,354 @@
1+
---
2+
uid: a_playback_dynamic
3+
---
4+
5+
# Dynamic changes
6+
7+
Playback API allows you to modify objects during playback. So you can, for example, add or remove a note or chord or any other MIDI event, or change tempo at some point in time. With this feature it’s possible to even build a MIDI sequencer or piano roll like in your favorite DAW.
8+
9+
Following code is a small demonstration of what you can do:
10+
11+
```csharp
12+
using Melanchall.DryWetMidi.Common;
13+
using Melanchall.DryWetMidi.Core;
14+
using Melanchall.DryWetMidi.Interaction;
15+
using Melanchall.DryWetMidi.Multimedia;
16+
using Melanchall.DryWetMidi.MusicTheory;
17+
18+
namespace DynamicPlayback
19+
{
20+
internal class Program
21+
{
22+
private static MusicalTimeSpan NoteLength = MusicalTimeSpan.Quarter;
23+
private static MusicalTimeSpan GapStepLength = MusicalTimeSpan.ThirtySecond;
24+
25+
static void Main(string[] args)
26+
{
27+
var tempoMap = TempoMap.Default;
28+
29+
var observableCollection = new ObservableTimedObjectsCollection
30+
{
31+
new Scale(ScaleIntervals.Major, NoteName.C)
32+
.GetAscendingNotes(Octave.Middle.C)
33+
.Take(10)
34+
.Select((n, i) => new Melanchall.DryWetMidi.Interaction.Note(n.NoteNumber)
35+
.SetTime(NoteLength * i, tempoMap)
36+
.SetLength(NoteLength, tempoMap))
37+
};
38+
39+
var outputDevice = OutputDevice.GetByName("Microsoft GS Wavetable Synth");
40+
var playback = new Playback(observableCollection, tempoMap, outputDevice);
41+
playback.Loop = true;
42+
43+
Console.WriteLine("Press any key to start playback...");
44+
Console.ReadKey();
45+
46+
playback.Start();
47+
48+
Console.WriteLine("Press ↑ or ↓ to change program number");
49+
Console.WriteLine("Press ← or → to change gap between notes");
50+
Console.WriteLine("Press any other key to exit...");
51+
52+
HandleChanges(observableCollection, tempoMap);
53+
54+
playback.Dispose();
55+
outputDevice.Dispose();
56+
}
57+
58+
private static void HandleChanges(
59+
ObservableTimedObjectsCollection observableCollection,
60+
TempoMap tempoMap)
61+
{
62+
TimedEvent programChangeTimedEvent = null;
63+
64+
var currentGapSteps = 0;
65+
var currentProgram = SevenBitNumber.MinValue;
66+
67+
while (true)
68+
{
69+
var key = Console.ReadKey().Key;
70+
71+
switch (key)
72+
{
73+
case ConsoleKey.UpArrow:
74+
case ConsoleKey.DownArrow:
75+
HandleProgramChange(key, ref currentProgram, ref programChangeTimedEvent, observableCollection);
76+
break;
77+
case ConsoleKey.LeftArrow:
78+
case ConsoleKey.RightArrow:
79+
HandleGapChange(key, ref currentGapSteps, observableCollection, tempoMap);
80+
break;
81+
default:
82+
return;
83+
}
84+
}
85+
}
86+
87+
private static void HandleProgramChange(
88+
ConsoleKey key,
89+
ref SevenBitNumber program,
90+
ref TimedEvent programChangeTimedEvent,
91+
ObservableTimedObjectsCollection observableCollection)
92+
{
93+
program = key == ConsoleKey.UpArrow
94+
? (SevenBitNumber)Math.Min(SevenBitNumber.MaxValue, program + 1)
95+
: (SevenBitNumber)Math.Max(SevenBitNumber.MinValue, program - 1);
96+
97+
Console.WriteLine($"New program number: {program}");
98+
99+
if (programChangeTimedEvent == null)
100+
{
101+
programChangeTimedEvent = new TimedEvent(new ProgramChangeEvent(program), 0);
102+
observableCollection.Add(programChangeTimedEvent);
103+
}
104+
else
105+
{
106+
var programNumber = program;
107+
observableCollection.ChangeObject(
108+
programChangeTimedEvent,
109+
obj => ((ProgramChangeEvent)((TimedEvent)obj).Event).ProgramNumber = programNumber);
110+
}
111+
}
112+
113+
private static void HandleGapChange(
114+
ConsoleKey key,
115+
ref int gapSteps,
116+
ObservableTimedObjectsCollection observableCollection,
117+
TempoMap tempoMap)
118+
{
119+
gapSteps = key == ConsoleKey.LeftArrow
120+
? Math.Max(0, gapSteps - 1)
121+
: gapSteps + 1;
122+
123+
Console.WriteLine($"New gap between notes: {gapSteps}");
124+
125+
var gapStepsNumber = gapSteps;
126+
127+
var i = 0;
128+
129+
foreach (var note in observableCollection.OfType<Melanchall.DryWetMidi.Interaction.Note>())
130+
{
131+
observableCollection.ChangeObject(
132+
note,
133+
_ => note.SetTime((NoteLength + GapStepLength * gapStepsNumber) * i, tempoMap));
134+
135+
i++;
136+
}
137+
}
138+
}
139+
}
140+
```
141+
142+
What does this console application do? Well, first of all it creates a set of notes (which is 10 steps of major scale starting from _C_) and then starts playing them in a loop. Interesting thing here is we use [ObservableTimedObjectsCollection](xref:Melanchall.DryWetMidi.Interaction.ObservableTimedObjectsCollection) – a collection which allows us to modify those notes on the fly and even add new MIDI data to the current playback object:
143+
144+
* Press the **up** or **down** arrow key and the current program number will be **incremented** or **decremented**, so a new instrument will be used to make sound. To apply a program we need to add a [Program Change](xref:Melanchall.DryWetMidi.Core.ProgramChangeEvent) event. If it already exists, we’ll just modify its [ProgramNumber](xref:Melanchall.DryWetMidi.Core.ProgramChangeEvent.ProgramNumber) property.
145+
* Press the **right** or **left** arrow and the gap between notes will be **incremented** or **decremented**. So we’re modifying notes times here.
146+
147+
## IObservableTimedObjectsCollection
148+
149+
In fact, to enable tracking of data changes you need to create an instance of the [Playback](xref:Melanchall.DryWetMidi.Multimedia.Playback) passing to its constructor an object which type implements two interfaces:
150+
151+
1. `IEnumerable<ITimedObject>` (just because `Playback`’s constructor accepts an argument of this type);
152+
2. [IObservableTimedObjectsCollection](xref:Melanchall.DryWetMidi.Interaction.IObservableTimedObjectsCollection).
153+
154+
When you modify a collection, it will fire the [CollectionChanged](xref:Melanchall.DryWetMidi.Interaction.IObservableTimedObjectsCollection.CollectionChanged) event holding information about what objects were added, removed and changed.
155+
156+
[ObservableTimedObjectsCollection](xref:Melanchall.DryWetMidi.Interaction.ObservableTimedObjectsCollection) is a built-in type that meets two requirements above. It obviously has methods like [Add](xref:Melanchall.DryWetMidi.Interaction.ObservableTimedObjectsCollection.Add*) or [Remove](xref:Melanchall.DryWetMidi.Interaction.ObservableTimedObjectsCollection.Remove*), but also some special ones. First of all, in the example above we use the [ChangeObject](xref:Melanchall.DryWetMidi.Interaction.ObservableTimedObjectsCollection.ChangeObject*) method. It allows you to modify an object and tell the collection that it has been changed in some way.
157+
158+
```csharp
159+
collection.ChangeObject(chord, obj =>
160+
{
161+
var c = (Chord)obj;
162+
c.Notes.Remove(c.Notes.First());
163+
});
164+
165+
collection.ChangeObject(chord, _ =>
166+
{
167+
chord.Channel = (FourBitNumber)5;
168+
});
169+
```
170+
Sometimes your logic of collection modification can be complex and/or distributed between different parts of code. In this case it will be more efficient to fire [CollectionChanged](xref:Melanchall.DryWetMidi.Interaction.IObservableTimedObjectsCollection.CollectionChanged) one time when you’re done with the data instead of triggering the event each time you make a change. There is a method for that – [ChangeCollection](xref:Melanchall.DryWetMidi.Interaction.ObservableTimedObjectsCollection.ChangeCollection*):
171+
172+
```csharp
173+
var tempoMap = TempoMap.Create(Tempo.FromBeatsPerMinute(240));
174+
var collection = new ObservableTimedObjectsCollection();
175+
176+
collection.ChangeCollection(() =>
177+
{
178+
AddInitialObjects(collection, tempoMap);
179+
FilterObjects(collection);
180+
FixObjects(collection, tempoMap);
181+
});
182+
183+
private static void AddInitialObjects(
184+
ObservableTimedObjectsCollection collection,
185+
TempoMap tempoMap)
186+
{
187+
collection.Add(SevenBitNumber
188+
.Values
189+
.Select((noteNumber, i) => new Note(noteNumber) { Time = i * 100, Length = 200 }));
190+
191+
collection.Add(
192+
new TimedEvent(new ProgramChangeEvent((SevenBitNumber)7) { Channel = (FourBitNumber)4 })
193+
.SetTime(MusicalTimeSpan.Quarter, tempoMap),
194+
new Chord(
195+
new Note((SevenBitNumber)80),
196+
new Note((SevenBitNumber)90))
197+
{
198+
Channel = (FourBitNumber)4
199+
});
200+
}
201+
202+
private static void FilterObjects(
203+
ObservableTimedObjectsCollection collection)
204+
{
205+
var objectsToRemove = collection
206+
.Where(obj => obj is Note note && note.NoteNumber % 2 == 0)
207+
.ToList();
208+
209+
collection.Remove(objectsToRemove);
210+
}
211+
212+
private static void FixObjects(
213+
ObservableTimedObjectsCollection collection,
214+
TempoMap tempoMap)
215+
{
216+
new Quantizer().Quantize(
217+
collection,
218+
new SteppedGrid(new MetricTimeSpan(0, 0, 1)),
219+
tempoMap);
220+
}
221+
```
222+
223+
If you write just
224+
225+
```csharp
226+
AddInitialObjects(collection, tempoMap);
227+
FilterObjects(collection);
228+
FixObjects(collection, tempoMap);
229+
```
230+
231+
it will trigger playback’s logic of data tracking multiple times which will degrade performance (you may notice lags in playback).
232+
233+
Of course you can create your own class implementing `IObservableTimedObjectsCollection` (and `IEnumerable<ITimedObject>`) and pass this collection to `Playback`. As the `ObservableTimedObjectsCollection` described above, your class should fire `CollectionChanged` event which arguments object ([ObservableTimedObjectsCollectionChangedEventArgs](xref:Melanchall.DryWetMidi.Interaction.ObservableTimedObjectsCollectionChangedEventArgs)) contains following data:
234+
235+
* [AddedObjects](xref:Melanchall.DryWetMidi.Interaction.ObservableTimedObjectsCollectionChangedEventArgs.AddedObjects);
236+
* [RemovedObjects](xref:Melanchall.DryWetMidi.Interaction.ObservableTimedObjectsCollectionChangedEventArgs.RemovedObjects);
237+
* [ChangedObjects](xref:Melanchall.DryWetMidi.Interaction.ObservableTimedObjectsCollectionChangedEventArgs.ChangedObjects).
238+
239+
First and second properties provide a collection of [ITimedObject](xref:Melanchall.DryWetMidi.Interaction.ITimedObject) objects which have been added and removed. Third property contains instances of the [ChangedTimedObject](xref:Melanchall.DryWetMidi.Interaction.ChangedTimedObject) holding an object along with its old time (time before changing).
240+
241+
## Objects references
242+
243+
Note that you must use original objects references when you’re working with an observable collection. For example, with this code
244+
245+
```csharp
246+
var collection = new ObservableTimedObjectsCollection
247+
{
248+
new TimedEvent(new TextEvent("A"), 100)
249+
};
250+
251+
var removed = collection.Remove(new TimedEvent(new TextEvent("A"), 100));
252+
```
253+
254+
`removed` variable will have `false` value since we’re passing a new reference to the `Remove` method. Proper way is
255+
256+
```csharp
257+
var timedEvent = new TimedEvent(new TextEvent("A"), 100);
258+
var collection = new ObservableTimedObjectsCollection
259+
{
260+
timedEvent
261+
};
262+
263+
var removed = collection.Remove(timedEvent);
264+
```
265+
266+
or
267+
268+
```csharp
269+
var collection = new ObservableTimedObjectsCollection
270+
{
271+
new TimedEvent(new TextEvent("A"), 100)
272+
};
273+
274+
var removed = collection.Remove(collection.First());
275+
```
276+
277+
The same situation with the `ChangeObject` method.
278+
279+
## Orphaned Note On and Note Off events
280+
281+
Also we need to discuss a case when you add a [Note On](xref:Melanchall.DryWetMidi.Core.NoteOnEvent) or [Note Off](xref:Melanchall.DryWetMidi.Core.NoteOffEvent) event instead of a Note object. If you add a _Note On_ event, no note will be built internally until you add corresponding (with the same note number and channel) _Note Off_ event. The same is true if you add a _Note Off_ event – you need to add corresponding _Note On_ one to get the note and to have it played.
282+
283+
So with this code
284+
285+
```csharp
286+
collection.Add(new TimedEvent(
287+
new NoteOnEvent((SevenBitNumber)70, SevenBitNumber.MaxValue),
288+
100));
289+
```
290+
291+
you won’t hear the note until
292+
293+
```csharp
294+
collection.Add(new TimedEvent(
295+
new NoteOffEvent((SevenBitNumber)70, SevenBitNumber.MinValue),
296+
200));
297+
```
298+
299+
## Tempo map
300+
301+
Obviously you can add a new [Set Tempo](xref:Melanchall.DryWetMidi.Core.SetTempoEvent) event or modify an existing one. But what happens within playback when you do that?
302+
303+
First of all, the process of scaling events times begins. So it can take some time to process events after a tempo change. Of course, only data **after** a tempo change will be scaled.
304+
305+
For example, if we have this initial collection
306+
307+
```csharp
308+
var collection = new ObservableTimedObjectsCollection
309+
{
310+
new Note((SevenBitNumber)90) { Time = 300, Length = 500 },
311+
};
312+
```
313+
314+
and default tempo map (`500000` microseconds per quarter note or `120` BPM), then playback events are:
315+
316+
* _Note On_ at `300` ticks;
317+
* _Note Off_ at `800` ticks.
318+
319+
Now we want to add a _Set Tempo_ event:
320+
321+
```csharp
322+
collection.Add(new TimedEvent(new SetTempoEvent(250000), 100));
323+
```
324+
325+
This change makes tempo 2x faster and playback events will be:
326+
327+
* _Set Tempo_ at `100` ticks;
328+
* _Note On_ at `200` ticks;
329+
* _Note Off_ at `450` ticks.
330+
331+
So time spans between the tempo change and events become 2x shorter (so they will be played earlier making playback faster).
332+
333+
It's important to note that when you change the tempo map, some time-based things other than MIDI events within the playback will be scaled too. To explain this better, let's take a look at the current time of the playback which can be accessed via [GetCurrentTime](xref:Melanchall.DryWetMidi.Multimedia.Playback.GetCurrentTime*) methods. For example, we have such a playback state:
334+
335+
![BeforeTempoChange](images/BeforeTempoChange.png)
336+
337+
If we change the _Set Tempo_ event so that the tempo will be 180 BPM (instead of current 90 BPM), the data will be shrinked by two times:
338+
339+
![AfterTempoChange](images/AfterTempoChange.png)
340+
341+
As you can see, the current time has been scaled too. Initially it was at the middle of the note. Scaling of the current time ensures it remains at the middle of that note so the playback will be smooth and without unexpected time jumps.
342+
343+
To preserve relative positions, following properties will be scaled too:
344+
345+
* [PlaybackStart](xref:Melanchall.DryWetMidi.Multimedia.Playback.PlaybackStart);
346+
* [PlaybackEnd](xref:Melanchall.DryWetMidi.Multimedia.Playback.PlaybackEnd);
347+
* times of snap points.
348+
349+
When you use `Playback` created with an observable collection, you should use [TempoMap](xref:Melanchall.DryWetMidi.Multimedia.Playback.TempoMap) property of the `Playback` instance when you work with its objects. When you edit a tempo map (via adding, removing or changing _Set Tempo_ and _Time Signature_ events), this property will reflect these changes. So the property holds the actual [tempo map](xref:a_tempo_map) of the playback:
350+
351+
```csharp
352+
collection.Add(new TimedEvent(new TextEvent("A"))
353+
.SetTime(new MetricTimeSpan(0, 0, 1), playback.TempoMap));
354+
```

Docs/articles/playback/Overview.md

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -50,6 +50,8 @@ namespace SimplePlaybackApp
5050

5151
Please read [Tick generator](Tick-generator.md) article and [PlaybackSettings](xref:Melanchall.DryWetMidi.Multimedia.PlaybackSettings) class documentation to learn how you can adjust playback's internals.
5252

53+
Playback supports on-the-fly changes of the data being played. You can find detailed information on how to use this feature in the [Dynamic changes](xref:a_playback_dynamic) article.
54+
5355
If you call the [Start](xref:Melanchall.DryWetMidi.Multimedia.Playback.Start) method of the [Playback](xref:Melanchall.DryWetMidi.Multimedia.Playback), execution of the calling thread will continue immediately after the method is called. To stop playback use the [Stop](xref:Melanchall.DryWetMidi.Multimedia.Playback.Stop) method. Note that there is no any pausing method since it's useless. `Stop` leaves playback at the point where the method was called. To move to the start of the playback use the [MoveToStart](xref:Melanchall.DryWetMidi.Multimedia.Playback.MoveToStart) method.
5456

5557
> [!IMPORTANT]
@@ -71,4 +73,4 @@ If you call the [Start](xref:Melanchall.DryWetMidi.Multimedia.Playback.Start) me
7173
7274
There are constructors of [Playback](xref:Melanchall.DryWetMidi.Multimedia.Playback) that don't accept [IOutputDevice](xref:Melanchall.DryWetMidi.Multimedia.IOutputDevice) as an argument. It can be useful, for example, for notes visualization without sound. [Playback](xref:Melanchall.DryWetMidi.Multimedia.Playback) provides events that will be fired with or without an output device (see [Events](xref:Melanchall.DryWetMidi.Multimedia.Playback#events) section of the [Playback](xref:Melanchall.DryWetMidi.Multimedia.Playback) API page). Also all `GetPlayback` extensions methods have overloads without the `outputDevice` parameter.
7375
74-
Also if you don't specify an output device and use a [tick generator](Tick-generator.md) other than [HighPrecisionTickGenerator](xref:Melanchall.DryWetMidi.Multimedia.HighPrecisionTickGenerator), you can use `Playback` in a cross-platform app like Unity game that is supposed to be built for different platforms.
76+
Also if you don't specify an output device and use a [tick generator](Tick-generator.md) other than [HighPrecisionTickGenerator](xref:Melanchall.DryWetMidi.Multimedia.HighPrecisionTickGenerator), you can use `Playback` in a cross-platform app like Unity game that is supposed to be built for different platforms (you can find currently supported OS in the [Supported OS](xref:a_develop_supported_os) article).
43.7 KB
Loading
21.8 KB
Loading

Docs/articles/toc.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -60,6 +60,7 @@
6060

6161
# Playback
6262
## [Overview](playback/Overview.md)
63+
## [Dynamic changes](playback/Dynamic-changes.md)
6364
## [Tick generator](playback/Tick-generator.md)
6465
## [Current time watching](playback/Current-time-watching.md)
6566
## [Data tracking](playback/Data-tracking.md)

0 commit comments

Comments
 (0)