Music entities are implicitly stored in collections that can be processed further. So far, there has been always just a single collection called the scope — the one manipulated by batch functions. In this tutorial we introduce workflows with multiple collections that alternate in the role of the scope.
Table of Contents
• • • • • • •
Collections of D♭ entities can be stored in variables,
joined together, or manipulated in other ways,
and then passed to become the current scope. The term collection
applies to the versatile concept of moving and filtering music data
while the scope is the currently active collection to which the verbatim operator @ has access.
So if you read about the scope of a function, it refers to the collection it works with.
Each function operates on its own scope. The parent function passes its scope to
the function it invokes and once finished, the child function returns the updated scope back to the parent.
In this sense there is an implicit assignment (in pseudocode) scope = @Function(scope, arguments);.
Scope manipulation actually ocurred already in previous tutorials without
much of attention. For example, each time a Split or Slice creates new entities,
the chained Then modifier processes each of the generated entities in a
separate scope. At the end, all the scopes are merged to form the final scope which is
returned by the Split or Slice.
The second example is the if statement when the predicate features an
entity attribute. It partitions entities from the current scope into
two scopes, grouping positive and negative predicate results.
The if body is then applied to the former while the optional else branch to the latter.
All tutorials so far worked with the initial scope being processed and passed as the only scope. Every batch function invocation returns its local scope which implicitly replaces the current scope. But the resulting collection can also be rerouted to a custom variable instead of the scope. This is achieved simply by adding an assignment in front of the function call.
function Main() {
var A = @Split(4).Then(i => @degree = i);
//the split result has been stored in A,
//the scope remains unchanged with a single C4 note
}Any batch function can override the default return of its final scope.
An explicit return replaces it by the respective collection.
function Main() {
var A = @Split(4).Then(i => @degree = i);
return A; //the content stored in A is returned instead of the scope
}Extracting a scope would be useless without any means to process it further.
The most simple operations are the inverse operations to Split and Slice.
D♭ features them as overloaded binary operators to the entity collections.
Up until now the workflow presented was top-down.
Starting with a single entity representing the whole musical piece,
operations like Split and Slice divided it into more specific pieces down to single notes.
It could be also described in the sense of divide and conquer or as subtractive.
Concatenation + is the counterpart to Split. It places a pair of
entity collections after each other in time — producing a new concatenated collection.
It is the basic operation for an additive
bottom-up approach which connects small pieces of music into larger blocks.
function Main() {
var A = @Split(4).Then(i => @degree = i);
return A + A; //concatenation
}Overlay * is the counterpart to Slice. It puts several entity collections
on top of each other so that they will be played simultaneously.
function Main() {
@span = 4;
var A = @Split(4).Then(i => { @degree = i; @color = "orange"; });
var B = @Split(2).Then(i => @degree = -2 - i * 5);
return A * B; //overlay
}Multiple concatenations can be used to repeat a collection several times.
A collection of entities can be also multiplied * by an integer to achieve the same.
The following example compares top-down repetition with its bottom-up counterpart.
In A the Split(4) fits four ostinatos into the given time frame.
In B the Ostinato() * 4 extends the song duration by placing four ostinatos after each other.
function Main() => @A() + @B();
function A() {
@color = "orange";
@Split(4); //splitting the default span into 4 equal parts
@Ostinato(); //so each ostinato must fit into 0.25s
}
function B() => @Ostinato() * 4; //concatenation which repeats an ostinato 4x, each 1s long by default
function Ostinato() => @Split([1,1,2]).Then(i => @degree = i);Concatenation joins a pair of entity collections, repetition repeats a collection several times.
It is important to distinguish between functions and data at this moment.
Data: In the previous example the 4x repetition in B requires input data — a collection which is
provided by calling Ostinato() a single time. The obtained collection is then
repeated four times. The result of a repetition will always contain
exact copies of the input data.
Function: A different outcome would be achieved if the Ostinato() function would have been
called 4x and only then the four resulting collections would be concatenated.
The next example demonstrates both cases using a randomized ostinato.
In A (orange) the split creates four entities and
Ostinato is then evaluated for each of them as a separate command.
In B (grey) the Ostinato is evaluated only once and the resulting scope
with three random notes is repeated without further re-evaluation.
Therefore, the note sequences of the parts do not match anymore.
function Main() => @A() + @B();
function A() {
@span *= 4; //stretch the entity to sync with B()
@color = "orange";
@Split(4);
@Ostinato();
}
function B() => @Ostinato() * 4;
function Ostinato() => @Split([1,1,2]).Then(i => @degree = @Rnd.Choice(i, 2 * i, 3 * i));There are two ways how to overcome the limitation:
Splitting a stretched entity as shown in the previous example.
Concatenation + can be used instead of repetition *.
function Main() => @A() + @B();
function A() {
@span *= 4;
@color = "orange";
@Split(4);
@Ostinato();
}
//fixed count repetition with randomization
function B() => @Ostinato() + @Ostinato() + @Ostinato() + @Ostinato();
function Ostinato() => @Split([1,1,2]).Then(i => @degree = @Rnd.Choice(i, 2 * i, 3 * i));The second option B is not flexible in the number of repetitions,
but the following section shows a way how to fix it.
Functions are always applied to the scopes.
To apply a function to another collection, it must be wrapped
into a Scope command. It passes the provided collection
to the chained Then to become its scope.
The respective function is then applied to the scope.
.
function Main() {
@span = 12;
var A = @Split(4).Then(i => @degree = i);
//at this point the collection A still has a single entity
return @Scope(A).Then(Triad); //applying Triad to A
}
function Triad() {
@chord += [2, 4, 7];
@Split(4).Then(i => @inversion = i);
@Arpeggio(0.2);
}Using Scope without an assignment on the left-hand side works just
like any other D♭ function, it updates the scope. This is the only
way how to replace the scope by a collection from a variable. So the previous
example can actually omit the return for the Main function and
rely on the implicit return of the scope instead.
function Main() {
@span = 12;
var A = @Split(4).Then(i => @degree = i);
@Scope(A).Then(Triad); //no return needed
}
function Triad() {
@chord += [2, 4, 7];
@Split(4).Then(i => @inversion = i);
@Arpeggio(0.2);
}A parameterless Scope simply extracts the current scope for
further processing. The concatenation example can be also written as:
function Main() {
//before
//var A = @Split(4).Then(i => @degree = i);
//return A + A;
@Split(4).Then(i => @degree = i);
return @Scope() + @Scope();
}The parameterless Scope can be used for writing a flexible
repetition function C for the randomized repetition example.
function Main() => @A() + @B() + @C(4);
function A() {
@span *= 4;
@color = "orange";
@Split(4);
@Ostinato();
}
//fixed count repetition with randomization
function B() => @Ostinato() + @Ostinato() + @Ostinato() + @Ostinato();
//flexible count repetition with randomization
function C(int count)
{
@color = "violet";
var result = count == 0 ? @Scope() : @Ostinato();
for(int i = 1; i < count; ++i)
result += @Ostinato();
return result;
}
function Ostinato() => @Split([1,1,2]).Then(i => @degree = @Rnd.Choice(i, 2 * i, 3 * i));The Main function always starts with the default entity, but
for any other function the input scope is arbitrary.
It could be a single note but also a whole symphony.
No assumptions can be made.
If the purpose of a custom function is to generate a small building block, then it should start from the default entity. This is achieved by the Axiom command which creates a new scope with the default entity.
//task: fast short ascend, then slower descend
function Main() {
@Split(4).Then(i => @degree = i); //ascend
return @Scope() + @Descend();
}
function Descend() {
@Axiom(); //comment out this line and see what happens
@span *= 4;
@Split(8).Then(i => @degree += 4 - i); //descend
}Removing the Axiom command changes the result in two ways.
The initial entity spans 1s. After the first split in Main
the span which enters Descend is 0.25s, so after multiplying
it by 4 it becomes 1s right before the second split,
resulting in 0.125s entities in the descending part.
Since Descend is called after the first split, the input scope
has four entities and Descend will be called four times. Therefore,
four copies of the descending ostinato offset by 0.25s appear.
With Axiom the first issue does not occur because the default span
of 1s is multiplied by 4 resulting in 0.5s entities after
the second split. Also Axiom procudes a new scope with a single entity,
so despite four entities enter the Descend call, Split(8) is applied only once.
The previous example can be generalized and extended to form a parametrizable loop. For better understanding, the following example shows a slightly different formulation, but fully equivalent with the previous code without any parametrization so far.
function Main() {
@Split(4).Then(i => @degree = i); //ascend
@Descend();
}
function Descend() {
var descend = @Axiom().Then(() => {
@span *= 4;
@Split(8).Then(i => @degree += 4 - i);
});
return @Scope() + descend;
}The intended generalization includes two variables which help to form a loop. Feel free to experiment with their values: steps controls the number of consecutive notes while repeats determines the number of loop repetitions. Both variables can be also turned into entity attributes or input parameters.
The loop is formed by adding a small ascending part on the last line.
var steps = 4;
var repeats = 2;
function Main() {
@Split(steps).Then(i => @degree = i);
return @Loop() * repeats;
}
function Loop() {
var descend = @Axiom().Then(() => {
@span *= steps;
@Split(steps * 2).Then(i => @degree += steps - i);
});
//remember that scope is [0,1,2,3] for steps = 4 right now
//descend is [4,3,2,1,0,-1,-2,-3]
//the last scope transformation gives [-4, -3, -2, -1]
return @Scope() + descend + @Scope().Then(i => @degree = i - steps);
}If a concatenation or overlay for an unknown number of entity collections is necessary,
or the collections are stored in a list or array, it is possible to use the
Concat and Overlay commands. These are the first
global-level commands to be introduced. Using the prefix # they both
ignore the current scope and take just an array of entity collections as input.
Their output, however, implicitly replaces the scope just like any other batch function does.
In the following example pay attention to the Main function. It first
shuffles an array with three motives and then concatenates them. Using the Concat
command allows for flexibility with respect to the array length, which can be arbitrary.
Using the + operator would require to exactly list the element indexes,
e.g. shuffled[0] + shuffled[1] + shuffled[2], which requires a fixed length of three.
Another alternative would be to use the concatenation operator + in a loop (see previous examples).
var steps = 4;
var tempoScale = 0.3;
function Main() {
var shuffled = @Rnd.Shuffle([@A(), @B(), @C()]);
#Concat(shuffled);
}
function A() {
@Axiom().Then(() => {
@color = "green";
@span = steps * tempoScale;
//simple scale-like ascend
@Split(steps).Then(i => @degree = i);
});
}
function B() {
@Axiom().Then(() => {
@color = "cyan";
@span = steps * tempoScale * 2;
//every second note ascends
@Split(steps).Then(i => @degree = i * 2);
});
}
function C() {
@Axiom().Then(() => {
@color = "orange";
@span = steps * tempoScale * 0.5;
//scale-like descend
@Split(steps).Then(i => @degree = steps - i);
});
}The initial example on repetition of an entities collection presented a solution that copied
the same scope several times. This is because both Concat and Overlay take
collections as input not functions. If there is a function call involved, it gets evaluated once
and the result is passed to the Concat or Overlay. Of course the respective
+ and * operators work the same.
The best practice to produce each copy independently is to prepare
an array or a list and concatenate its members with the Concat
command instead of multiplying them.
function Main() => @A() + @B();
function A() {
@span *= 4;
@color = "orange";
@Split(4);
@Ostinato();
}
function B() {
@Axiom();
var list = new List<Music>();
for(int i = 0; i < 4; ++i)
list.Add(@Ostinato());
#Concat(list);
}
function Ostinato() => @Split([1,1,2]).Then(i => @degree = @Rnd.Choice(i, 2 * i, 3 * i));Sometimes, creating a long list of entity collections can be problematic.
For example, if there are too many of them. It is also possible to achieve
the same effect through iterative appending instead of list concatenation.
One approach has been already presented a few examples above. Here it is shown in B. The Axiom
call at the beginning assures default entity with just an altered color. C shows another approach
using the Append command instead. Since there are 3 notes in each ostinato,
a normal Append would be activated 3x in the first iteration,
9x in the second iteration and 27x in the third one.
Therefore, AppendOnce is necessary to apply the command only to the last entity of the scope.
C also presents a different approach to Axiom which is now attached
to each Ostinato call separately. Thus, setting color at the beginning of C
would have no effect.
var repeats = 4;
function Main() => @A() + @B() + @C();
function A() {
@span *= repeats;
@color = "orange";
@Split(repeats);
@Ostinato();
}
function B() {
@Axiom();
@color = "violet";
var result = @Ostinato(); //initial ostinato triplet
for(int i = 1; i < repeats; ++i) //starting from 1 as we already have the initial triplet
result += @Ostinato(); //each adds another ostinato triplet
return result;
}
function C() {
@Axiom().Then(Ostinato); //initial ostinato triplet
for(int i = 1; i < repeats; ++i) //starting from 1 as we already have the initial triplet
@AppendOnce(@Axiom().Then(Ostinato)); //each adds another ostinato triplet
}
function Ostinato() => @Split([1,1,2]).Then(i => @degree = @Rnd.Choice(i, 2 * i, 3 * i));Collections are vivid for working with segments of varying length. It can be just
the flexibility of interpretation, but also structural variation involved.
The following example shows the first case where random tempo per segment avoids
an easy determination of the overall span.
function Main()
{
@instrument = "piano";
@release = 0.2;
@baseDuration = @Rnd.Float(4..7);
@chordRatio = @Rnd.Float(4..8) * 0.1;
@chordDuration = @baseDuration * @chordRatio;
@arpeggioDuration = @baseDuration * (1 - @chordRatio);
return @A() + @B() + @A() + @B(finish: true);
}
function A()
{
@chordSequence = @Rnd.Shuffle([true, true, false]);
@span = 2 * @chordDuration + @arpeggioDuration;
@splitDurations = @chordSequence.Select(x => x ? @chordDuration : @arpeggioDuration).ToArray();
@inversion = @Rnd.Int(-1..0);
@Split(@splitDurations).Then(i => {
@harmony = i.Max - i;
if (@chordSequence[i])
{
@velocity = 0.2;
@release *= 4;
@Split(1,2).Then(i => {
if (i == 0) { --@octave; @release *= 4; }
else
{
@velocity -= 0.05;
@inversion += @Rnd.Int(-1..1);
if (@harmony == 0)
@chord += [2, 4, 7];
else
@chord += @Rnd.Choice([2, 4], [2, 4, 6]);
}
});
}
else
{
--@octave;
@release *= 6;
@inversion = 0;
@chord = @Rnd.Subset(@Rnd.Int(5,6), i.IsLast ? 0..5 : 0..6);
@Split(3).Then(i => @octave += i);
@Split().Then(i => @velocity = 0.3 + 0.03 * i.Complement);
}
});
}
function B(bool finish = false)
{
var harmony = [4, 5, 6, 7];
@octave -= 1;
@span = harmony.Length * @arpeggioDuration * 0.7;
@inversion = @Rnd.Int(-1..1);
@Split(harmony.Select(x => x + 6).ToArray()) .Then(i => {
@velocity = 0.2 + 0.1 * i;
@harmony = harmony[i];
if (i.IsLast && finish)
{
@chord += [2, 4, 7, 9, 11, 14];
@Arpeggio(@span * 0.5);
}
else
{
@chord += @Rnd.Choice([2, 5, 8, 10], [2, 4, 7, 9], [2, 4, 6, 8]);
@Arpeggio(0.2 + 0.02 * i);
}
if (i.IsLast)
@span *= 2;
});
}The next tutorial continues with the topic of data extraction from the scope, but selectively handles only certain attributes.
D♭ Tutorials — Basic
D♭ Tutorials — Intermediate
D♭ Reference
D♭ Examples
Links