The previous tutorial introduced
the @ operator to access the attributes of entities. In this tutorial
we will learn to use functions in a similar fashion. Just as @velocity = 0.3
sets the dynamics for all entities in the scope, the @ prefix
can be used to apply a function to all entities in the scope.
In the end of this tutorial, we will be able to produce a sequence of notes.
Table of Contents
• • • • • •
Each entity has a span attribute. It represents the duration in seconds.
function Main() {
@instrument = "violin";
@span = 0.5; //short note (half a second)
}function Main() {
@instrument = "violin";
@span = 4; //long note (four seconds)
}Next to span, each entity has a time property as well.
It determines when the entity will be played.
It is basically an offset from the song start in seconds.
time is rarely used directly in code. Most of the time,
functions like Split take care of setting it properly.
Split is the first function of D♭ you encounter.
It divides an entity into a sequence of shorter entities, so
that in sum they cover the whole span of the original entity.
function Main() {
@instrument = "violin";
@span = 4; //song length will be four seconds
@Split(4); //four short notes (one second each)
}Now we finally hear more than a single note,
but playing a shorter version of the input note four times is not very exciting.
Then can be called on the result of Split
in order to directly manipulate the produced entities.
For example, we may shorten them even more.
function Main() {
@instrument = "violin";
@span = 4;
@Split(4).Then( () => @span *= 0.25 ); //four short tones (quarter a second each)
}Then consumes a transformer function.
It is applied to all entities produced by the Split.
Note that as a result of the Split, the span values
of all four entities equal 1 second at the time of the compound multiplication *= .
The transformer may be also used with an Indexer parameter
that allows easy distinction of the produced entities. The indexer
has two basic data properties: Index and Max. The latter carries
the highest index number in this Split call.
function Main() {
@instrument = "violin";
@span = 6;
@Split(12).Then(idx => {
@velocity = idx.Index / (float)idx.Max; //float cast to avoid integer division
//@velocity = idx.Relative; //same as above
@span *= idx.IsLast ? 2 : 0.8; //make the last note long, otherwise avoid legato-like impression
});
}The Indexer also offers a few computed properties that come handy in
common situations: Relative represents the index progress in the range of [0, 1],
IsFirst, IsLast, IsEven and IsOdd are self-explanatory.
To keep the syntax simple, Indexer is implicitly cast to an int if
used without any of its properties. In the following example, this is demonstrated
by the expression 1 + idx. It shortens the span with increasing index. Note
the addition of one to avoid division by zero.
function Main() {
@span = 4;
@Split(4).Then(idx => {
@span /= (1 + idx);
});
}Instead of a regular division resulting in equally sized entities,
Split can also take an array of relative durations and distribute
the available time span proportionally. Alternatively, the durations
can be also passed as separate method parameters. The following sequence will
sound like a famous Morse code.
function Main() {
@instrument = "violin";
@octave = 5;
@span = 5;
@Split([1.0, 1.0, 1.0, 2.0, 2.0, 2.0, 1.0, 1.0, 1.0]).Then(() => @span -= 0.05 ); //using a collection expression (new in C#12)
//the variant above is useful if you have the array stored in a variable or computed as an expression
//the variant below is shorter to write for constant arrays which can be directly entered as function arguments
//@Split(1.0, 1.0, 1.0, 2.0, 2.0, 2.0, 1.0, 1.0, 1.0).Then(() => @span -= 0.05 );
}Create a cha-cha rhythm in 4/4: three quarter notes followed by two eighth notes. Keep the notes in sequence.
A little practice, a little music.
Edit the starter, run your code, and see how it sounds.
There is an even more advanced concept that involves three different sizing types
that can mix together: FILL, REL, ABS. So far the spans were just
relative to each other. This would correspond to the FILL mode. The FILL
mode involves normalization of the values so that they exactly fit the duration
of the input entity.
function Main() {
@instrument = "violin";
@span = 4;
@Split(FILL(1.0), FILL(1.0), FILL(0.5), FILL(1.0), FILL(1.0)).Then(() => @span -= 0.1);
//So the relative durations are in ratio 2:2:1:2:2
//That was in sum 4.5, so each was normalized by 4.0/4.5 to exactly cover the span of 4.0 seconds.
}REL has a higher priority than FILL. The values are relative to the duration of the input entity.
If their sum is > 1.0 (i.e. the input duration), normalization
is applied to the relative items to scale them down so that they exactly cover the input duration
and at the same time all fills are neglected. If the sum of REL items is < 1.0, the unoccupied
space is assigned to the FILL items. If there are no FILL items, then REL items are scaled up
to cover the whole input duration.
function Main() {
@instrument = "violin";
@span = 4;
@Split(FILL(1.0), FILL(1.0), REL(0.75), FILL(1.0), FILL(1.0)).Then(() => @span -= 0.1);
//The relative item took 0.75*4s = 3s
//Fills were in sum 4.0, so each was normalized by 1.0/4.0 to exactly cover the remaining span of 1s.
}ABS has the same priority like REL. The values are in absolute time units, i.e. in seconds.
If the sum of ABS and REL items is higher than the available span, normalization
is applied to scale them down and all fills are neglected. Otherwise, either fills are used to
cover the unoccupied space or, if no fills are present, then ABS and REL items are scaled up to match the input duration.
function Main() {
@instrument = "violin";
@span = 4;
@Split(ABS(1.0), FILL(1.0), REL(0.5), FILL(1.0), ABS(1.0)).Then(() => @span -= 0.1);
//The absolute items took 2*1s = 2s
//The relative item took 0.5*4s = 2s
//Relative and absolute items already cover the whole span of 4s. Hence, FILL items will be omitted.
}Advanced sizing modes FILL, REL and ABS are useful for higher structuring
of music pieces with complex structures where further layers of splitting follow.
The examples above with single notes are provided just as toy examples for easy demonstration.
D♭ functions are also called batch functions as they process all entities in the scope in a single batch. Without going into any formal details, note that there is a relation to L-Systems.
The following example demonstrates the power of batch processing
of the whole scope. The first Split divides
the span into parts that could correspond to measures.
In the spirit of batch processing the second Split
divides each of the measures into beats.
function Main() {
@span = 6;
@Split(4); //measures
@Split(4).Then(idx => { //beats
if (idx.IsEven)
{
@velocity = 0.6;
@instrument = "bass drum";
}
else
{
@velocity = 0.8;
@instrument = "snare";
}
});
}We can use both indexes to make the rhythm a bit more interesting. Every second measure will double the third beat.
function Main() {
@span = 6;
@Split(4).Then(idx => @measureIndex = idx);
@Split(4).Then(idx => {
if (idx.IsEven)
{
@velocity = 0.6;
@instrument = "bass drum";
}
else
{
@velocity = 0.8;
@instrument = "snare";
}
if (idx == 2 && @measureIndex.IsOdd)
@Split(3f, 1f);
});
}There are a few more built-in functions which will be introduced in this and in the following tutorials, but most of the functions you will design by yourself as custom functions.
Just like with attributes, you can define custom functions and
apply them to the entities. When calling custom functions they need
to be prefixed by @ just like the built-in commands. The following
example shows the custom function Bursts.
function Main() {
@span = 12;
@instrument = "french horn";
@Split(4).Then(idx => @measureIndex = idx);
@Burst();
}
function Burst() {
@octave = 2 + @measureIndex;
@Split(@measureIndex + 1);
}The usage of Bursts can be integrated into the Then transformer.
We can omit measureIndex as a helper attribute and make it directly
an argument of 'Bursts`.
function Main() {
@span = 12;
@instrument = "french horn";
@Split(4).Then(idx => @Burst(idx));
}
function Burst(int measureIndex) {
@octave = 2 + measureIndex;
@Split(measureIndex + 1);
}The usage of Bursts can be improved even more. It can be
directly passed as the transformer by replacing int measureIndex
by Indexer idx. Note that Bursts is passed as a function
reference, hence the @ prefix must be omitted.
Otherwise, it would be executed right away (i.e. its
result would be expected to be passed to Then)
which would result in an error.
function Main() {
@span = 12;
@instrument = "french horn";
@Split(4).Then(Burst); //Attention, no @ for Bursts
}
function Burst(Indexer idx) {
@octave = 2 + idx;
@Split(idx + 1);
}Watch out for invalid function names. Some identifiers are reserved for other data structures. Using them as custom function names would result in an error. These are names of helper structures like: Melody, Rhythm which will be introduced in some of the intermediate tutorials or built-in functions like Split or Rest. The following example demonstrates the situation when a custom function name conflicts with a helper structure resulting in an error.
var idea = [0, 1, 1, 0, -1];
function Main() {
@octave += 1;
@span = idea.Length * 0.2;
@Melody(idea);
}
//since melodies are addressed by the next tutorial, here we only change the octave
function Melody(int[] melody) => @Split(melody.Length).Then(i => @octave += melody[i]);
//in order to resolve the error change @Melody (Ln5) and Melody (Ln9)
//to something else like e.g. @Octave and Octave, respectivelyAll the entities we generated so far have produced sound. But music contains many rests that serve different purposes: they grant time to breathe, they convey rhythm, they switch off instruments to produce certain colors.
The Rest function simply converts entities to rests. They keep all attributes, but one:
chord, which is discussed in the harmony tutorial, gets
a special value to represent the rest. This is a much better practice
than a full removal of an entity, as only the original chord
information gets lost, but anything else is preserved.
function Main() {
@span = 12;
@instrument = "french horn";
@Split(4).Then(Burst);
}
function Burst(Indexer outerIdx) {
@octave = 2 + outerIdx;
@Split(outerIdx + 1).Then(innerIdx => {
if (innerIdx == 1) @Rest();
});
}As Split produces many entities, it is sometimes useful
to mark some of them terminals. That means that they are considered
to be in their final state and no more D♭ function or
attribute assignment can change them. As if they would be deactivated.
In the following example Done is used to deactivate bursts with
an odd number of notes. Done bursts will not be played by the French horn.
function Main() {
@span = 12;
@Split(4).Then(Burst);
@instrument = "french horn";
}
function Burst(Indexer outerIdx) {
@octave = 2 + outerIdx;
@Split(outerIdx + 1);
if (outerIdx.IsOdd) @Done();
}Use Done with caution, it can easily cause confusion in case you
forget about it or some one else working with your code misses it.
Usually there are better ways to deal with exclusion of a subset of entities.
Local temporary exclusion should be preferred over Done.
For example, the previous example can be rewritten using a local conditional.
function Main() {
@span = 12;
@Split(4).Then(Burst);
if (@outerIndex.IsEven)
@instrument = "french horn";
}
function Burst(Indexer outerIdx) {
@outerIndex = outerIdx;
@octave = 2 + outerIdx;
@Split(outerIdx + 1);
}In the next tutorial we will utilize the attributes and functions for producing simple melodies.
D♭ Tutorials — Basic
D♭ Tutorials — Intermediate
D♭ Reference
D♭ Examples
Links