GeoMaestro
Tutorial 4: a home-made piano-roll
Implementation
Usage (example)
A bit with the same spirit as the previous tutorial, we will see here how we can build from scratch a piano-roll representation within GeoMaestro. Again, this is not intended to compete with actual piano-roll softs, but better to demonstrate how any kind of geometrical set-up can be created and used with GeoMaestro, leaving you free to develop your own, according to your intuitions and preferences.
This tutorial is heavily programming-oriented, so you may skip it if you simply intend to use the system with its basic options... or if you don't like programming.
All code introduced here is already available in the file userlib/tut4_pianoroll.k, so actually you don't have to write anything. You can simply consider this tutorial as a manual for the provided pianoroll features, if you think they're interesting enough for you (they can be improved a lot). In this case, you can also directly go to the example part and skip the technical details.
Implementation
Basically, in GeoMaestro terms, a piano-roll is a very specific 2-dimensionnal display where the position of a note is precisely related to its pitch and time attributes; also, "projection" happen along an horizontal segment, without the distance from the note/event to the segment having any effect.
So, in order to create such a representation, we must decide which positions must be related to which notes, and then make sure that this relationship is always respected when we edit the score.
Note pitches range from 0 to 127; usually a score only use a few octaves, but since we don't know which range will be used, we will make all notes available, with the following options:
- PitMin and PitMax will be two integer variables defining a pitch range within which consecutive pitch values will be separated by 0.1 unit (the narrow grid). It will be the work area of the roll.
- outside this range, pitches will be separated by 0.01 unit; this will make it possible to display all notes, the overall heigth of the roll not being too large.
- it will be possible to change PitMin and PitMax on the fly, so that we can "zoom" on a part of the score, making it the work area
- PitMin will always be represented by the line y = 0
- the starting time (t = 0) will be represented by the line x = 0
This is already enough to have the formulas we must use to link Y position and pitch for a note in the roll:
the Y position py for a note of pitch p is:
if (p < PitMin) py = 0.01*(p - PitMin) # lower pitches
else if (p >= PitMin && p <= PitMax) py = 0.1*(p-PitMin) # work area
else if (p > PitMax) py = 0.01*(p-PitMax) + 0.1*(PitMax-PitMin) # higher pitches
and the other way round, the pitch p for a note in Y position py is:
if (py < 0) p = 100*py+PitMin
else if (py >= 0 && py <= 0.1*(PitMax-PitMin)) p = 10*py + PitMin
else if (py > 0.1*(PitMax-PitMin)) p = 100*py-9*PitMax+10*PitMin
We will use these formulas in two ways: first, to ensure that an event we draw inside the roll will be affected it's right pitch; second, so that an event we draw elsewhere in the graphic area can be automatically moved inside the roll, at the place corresponding to its nodur.
In the second case, the nodur needs a time attribute so that we know when this event is supposed to be played; while in the first case, the X position is the time information, since we are in the roll.
Here's the fully commented code for the basic function doing the job of organizing all events in the roll; let's call it UpdateRoll():
#---------------------------------------------------------
function UpdateRoll()
{
# upper line of the roll (p = 127):
RollTop = 0.1*(PitMax-PitMin)+0.01*(127-PitMax)
# lower one (p = 0):
RollBottom = -0.01*PitMin
# for all non-empty channels (see here for more)
for (ch = 1; ch <= NbCan; ch ++) if (Ev[ch][0] > 0)
# for all active events in those channels
for (n = 1; n <= Ev[ch][0]; n++) if (Ev[ch][n]["actif"] == 1)
{
# current position for event n in channel ch:
evy = Ev[ch][n]["y"]
evx = Ev[ch][n]["x"]
# CASE 1: the event is in the roll
# --> we check that its nodur is accorded to its position
if (evx > 0 && evy <= RollTop && evy >= RollBottom)
{
# this is the pitch it should have, due to its Y position:
p = RollGetP(evy)
# the event goes to its exact place:
if (p >= PitMin && p <= PitMax)
Ev[ch][n]["y"] = 0.1*round(10*evy)
else
Ev[ch][n]["y"] = 0.01*round(100*evy)
# * if the event is empty (nodur is ''), we create the nodur:
if (Ev[ch][n]["nodur"] == '')
Ev[ch][n]["nodur"] = makenote(p, RollDefDur, RollDefVol)
# * if the event already had a nodur, we keep its duration and volume
else
{
Ev[ch][n]["nodur"].pitch = p
Ev[ch][n]["nodur"].time = 0
}
}
# CASE 2: the event is outside the roll
# --> we move it inside the roll
else
{
nod = Ev[ch][n]["nodur"]
# its X position depends on its time attribute:
Ev[ch][n]["x"] = nod.time/float(CPCM)
# its Y position depends on its pitch:
Ev[ch][n]["y"] = RollGetY(nod.pitch)
# inside the roll, no time attribute is allowed:
Ev[ch][n]["nodur"].time = 0
Ev[ch][n]["nodur"].length = nod.dur
}
}
# we call the redraw() method of the GUI
LastGMGUI.redraw() # [REDRAW]
}
# function returning the right Y position for a given pitch:
function RollGetY(p)
{
if (p < PitMin) py = 0.01*(p - PitMin)
else if (p >= PitMin && p <= PitMax) py = 0.1*(p-PitMin)
else if (p > PitMax) py = 0.01*(p-PitMax) + 0.1*(PitMax-PitMin)
return(py)
}
# function returning the right pitch for a given Y position:
function RollGetP(py)
{
if (py < 0) p = 100*py+PitMin
else if (py >= 0 && py <= 0.1*(PitMax-PitMin)) p = 10*py + PitMin
else if (py > 0.1*(PitMax-PitMin)) p = 100*py-9*PitMax+10*PitMin
return(p)
}
#---------------------------------------------------------
As you can see in the code, two new parameters are required: RollDefDur and RollDefVol: they are the default values for duration and volume, used if an empty event is found in the roll. In other words, simply creating an empty event somewhere in the roll will lead to the creation of a note of duration RollDefDur and of volume RollDefVol, its pitch being set by its Y position.
On the other hand, if you create an event with a nodur inside the roll, only its pitch will be changed according to its position: volume and duration will stay the same.
Two other variables are also defined: RollTop and RollBottom, the Y values corresponding to the whole roll range.
Let's try it; first of all, type this at the console:
PitMin = 40
PitMax = 90
RollDefDur = 192
RollDefVol = 100
Snarf = ''
... now everything is initialized (you don't have to enter the code for UpdateRoll(), since it is provided in the GeoMaestro distribution); click on [grid] twice so that you have the fine rectangular grid on; using the "Snarf to event" mouse mode, click a couple of time not too far away from the center of the grid, like this:
... you just created a set of empty events (they're empty because Snarf = '', as we defined it at the console)
Now type this at the console:
UpdateRoll()
... then do a [REDRAW]; here's what you should see:
(use the [A] button to display the brown boxes)
What did happen ? The events have been slightly moved so that they exactly stick on the horizontal lines of the grid, and their nodurs have been set accordingly to their position. You can check that it's true either with the mouse "infos"mode or the "hear ev/seg" one.
All of these events were inside the roll; the problem is that we don't see the roll... so let's type this at the console:
STop = xyd(0, RollTop, 10000, RollTop)
SMax = xyd(0, RollGetY(PitMax), 10000,RollGetY(PitMax))
SBot = xyd(0, RollBottom, 10000, RollBottom)
... then [display] "STop, SMax, SBot". This is a way to visualize the limits of the roll, as you can see if you [zoom out] a bit:
... here are clearly visible the pitch range PitMin/PitMax (the inner ribbon) and the whole roll (the outer ribbon)
Let's play a bit with this by defining a new function RollMinMax() that will allow us to change the values of PitMin and PitMax on the fly:
#---------------------------------------------------------
function RollMinMax(min, max)
{
UpdateRoll()
for (ch = 1; ch <= NbCan; ch++) if (Ev[ch][0] > 0)
for (n = 1; n <= Ev[ch][0]; n++) if (Ev[ch][n]["actif"] == 1)
{
Ev[ch][n]["nodur"].time = Ev[ch][n]["x"]*CPCM
Ev[ch][n]["y"] = 2000
}
PitMin = min
PitMax = max
UpdateRoll()
RollLimits()
}
function RollLimits()
{
#define ROLL RegGUIbackgrounds["RollBackgroundDisplay"]=1;LastGMGUIf.redraw()
#define NoROLL RegGUIbackgrounds["RollBackgroundDisplay"]=0;LastGMGUIf.redraw()
ROLL
}
function RollBackgroundDisplay()
{
# the repeating of color() calls minimizes color leaking when FastRedraw is on...
color(COLOR_BLUE)
Geoline(xyDisp(0, RollTop, 10000, RollTop))
y = RollGetY(PitMax)
color(COLOR_BLUE)
Geoline(xyDisp(0, y,10000,y))
yg = yDisp(y)
if (Geoymin() < yg-Geoth() && Geoymax() > yg)
{
color(COLOR_BLUE)
Geotleft(string(PitMax),xy(Geoxmin()+1,yg,Geoxmax()-1,yg-Geoth()))
}
color(COLOR_BLUE)
Geoline(xyDisp(0, 0, 10000, 0))
yg0 = yDisp(0)
if (Geoymin() < yg0-Geoth() && Geoymax() > yg0)
{
color(COLOR_BLUE)
Geotleft(string(PitMin),xy(Geoxmin()+1,yg0,Geoxmax()-1,yg0-Geoth()))
}
color(COLOR_BLUE)
Geoline(xyDisp(0, RollBottom, 10000, RollBottom))
color(1)
}
#---------------------------------------------------------
The way RollMinMax() works is a bit tricky: first it calls UpdateRoll(), so that all active events in the scene are moved to their correct place into the roll, with their correct nodurs.
Then, for each event, it gives the nodur a time attribute corresponding to its X position, then sets the Y position to 2000, so that eventually all events are out of the roll, with their nodur complete so that the next call to UpdateRoll will put them back to their right place... get the idea ?
Then it sets the new value for PitMin and PitMax and call UpdateRoll() again. At this stage, all events are back in the "new" roll.
Eventually it calls the RollLimits() function which draw four segments displaying the limits of the roll; it also define two macros: NoROLL
and ROLL
that you can use to toggle off and on the limits display.
RollLimits() works by simply registering RollBackgroundDisplay(), the actual drawing function, into the RegGUIbackgrounds
array. Any function whose name is an index of this array is being called whenever the main GUI refreshes its display. This makes it quite easy to define custom backgrounds such as the roll one. As you can see by playing with the zoom buttons, the roll display is always automatically updated.
As for RollBackgroundDisplay(), it uses some of the display methods of the GUI (note how Geotleft
is used to display the values for PitMin and PitMax)
Don't worry if it's a bit (only a bit ?) obscure to you... We will soon see all of this in a comprehensive example !
But before, we need a few more functions:
- one to import a whole phrase (several notes, from maybe different channels) into the roll
- one to display comment flags so that we don't get confused in the differents lines of the grid
- one to initialize the system
- one to project the notes
Importing a whole phrase: RollImport()
#---------------------------------------------------------
function RollImport(ph)
{
num = 0
for (n in ph)
{
nod = n
nod.chan = 1
CreateNewEvent(0, 2000, n.chan, nod)
num++
}
UpdateRoll()
print("duration", latest(ph)/float(seconds(1)),"s.")
print(num, "notes")
}
#---------------------------------------------------------
... this is a pretty simple function: it simply takes each note from the phrase ph and create a new event for it. At first, all these events are located somewhere outside the roll, then UpdateRoll() is called so that they all go into their correct place.
The function also prints at the console the imported phrase size, in duration and number of notes/events.
Initializing the system: RollInit()
#---------------------------------------------------------
function RollInit()
{
RollDefDur = 192
RollDefVol = 100
RollMinMax(PitMin = 40, PitMax = 90)
for (ch = 1; ch <= NbCan; ch++)
{
Volume[ch] = "NoChanges"
Dur[ch] = "NoChanges"
Pit[ch] = "NoChanges"
Pan[ch] = "NoPan"
}
}
#---------------------------------------------------------
... this gives initial values to the required globale variables (you can of course change these values), and also sets all distortion functions so that they have no effect. The RollMinMax() call also makes the limit segments be displayed.
Projecting the notes from the roll: GetRoll(), HearRoll()
#---------------------------------------------------------
function GetRoll(t1, t2)
{
UpdateRoll()
print("calculating...")
rollph = Ecoute(xyd(t1/float(CPCM), 0),xyd(t2/float(CPCM), 0))["ph"]
print("... done")
return (rollph)
}
function HearRoll(s1, s2)
{
realmidi(GetRoll(seconds(s1),seconds(s2)))
}
#---------------------------------------------------------
... GetRoll() calls Ecoute() on a segment parallel to the roll, between two points corresponding to the times t1 and t2 (in clicks)
... HearRoll() is the function you would use to have sound from time s1 (in seconds) to s2
Commenting the piano roll: RollComments()
#---------------------------------------------------------
function RollComments(x1, x2, dx, dp)
{
if (nargs() < 3) dx = 1
if (nargs() < 4) dp = 10
Ev["comm"] = []
n = 0
for (x = x1; x <= x2; x+= dx)
for (p = 0; p<= 127; p+= dp)
Ev["comm"][++n] = ["x" = x, "y"= RollGetY(p), "text"= string(x)+","+string(p), "dx"= -100, "dy"=-20]
LastGMGUI.redraw() # [REDRAW]
}
#---------------------------------------------------------
... this function creates a set of comment flags (which are part of the Ev array, see here for details), from X position x1 to x2 (with an optional step dx)
For example,
RollComments(0, 10)
... gives birth to the following flags: (use the [C] button to turn them on and off)
When zooming, it becomes something like this:
... here the first number is the X-position, that is, if CPCM = seconds(1), the time in seconds.
The second number is the pitch corresponding to the Y position
OK, let's go to the second part and see all of this on a detailled example..
Example
If you didn't read the first part of this tutorial, it doesn't really matter (but it won't be as interesting as it could be, either)... everything is available from the distribution (in file lib/tut_pianoroll.k). We are going to see how GeoMaestro can emulate a basic piano roll.
First of all, let's start from a blank GUI: kill the current events (click [new] and say "y") and get rid of the displayed objects ([display] and "--")
Then initialize the piano roll system: at the console, type
RollInit()
Let's try it with a midi file: this is the Bach.mid file seen from within the Group Tool:
Import the file in GeoMaestro by typing this at the console:
RollImport(MIDIfile())
... and using the browser.
Then [zoom out] a couple of time, set the [grid] on and display the duration boxes with [A]: you should now see something like this:
The inner ribbon contains the notes whose pitch belong to the range PitMin-PitMax (40-90, according to RollInit()) . In this part of the roll, consecutive pitches are separated by 0.1 units
The two other ribbons (at the top and the bottom) would contain the notes of pitches 0 to 39 and 91 to 127 (there are none of these notes in the example file). There, separation is of 0.01
... so we have here a kind of "dilatation" on the 40-90 range of pitches values.
We can add comments with
FastRedraw = 1
RollComments(0,50,5)
... (this makes flags appear for x = 0 to x =50 every 5 units), and when zooming in we see this:
In this representation, the first number in the comment flags is the time (in seconds), the second one the pitch, while the brown boxes are the durations
The whole piece is about 50 seconds long, so we can save it in the phrase variable Bach by typing:
Bach = GetRoll(0, seconds(50))
or we can directly listen to it:
HearRoll(0, 50)
You can edit the score by moving the events, taking care that the ribbons are not at the same scale. For example, to transpose the whole score one octave up, select all events ("selection" mouse mode), choose the "move selection" mouse mode, click once in the graphic area then at the prompt enter "0, 1.2" (this is the way to define accurate motions). So far, the events still have their previous nodurs. Now type:
UpdateRoll()
HearRoll(0,50)
... that's it !
You can not do this again, since it will make some events cross the ribbons limits. In order to have more space in the inner ribbon, we can reconfigure the roll geometry like this:
RollMinMax(0,127)
RollComments(0,50,5)
This time there are no outer ribbons: every pitch is available in the main central one.
As I said before, editing an event's pitch and time position inside the roll is simply done by moving it (after editing, you will need to call UpdateRoll() ). Editing its volume and duration attributes is best done with the ModNodur plug-in:
... click on the event you want to edit and drag the mouse: the distance you cover will set the duration, the angle will change the volume, as you can see at the console.
Adding notes can be done in two very different ways:
- directly inside the roll: use the "Snarf to event" mode: only the volume and duration of Snarf will be taken into account, and if Snarf is '' they will be RollDefDur and RollDefVol.
- outside the roll: create a new event anywhere ("new event" mouse mode); it will be moved to its place in the roll, according to its pitch (Y position) and time attribute (X position)
In both cases, you need to finish with an UpdateRoll() call in order to have all events correctly defined and ordonned
The piano roll we have here has an interesting property: it is truly 2-dimensionnal...
If you still have the Bach score around, try this:
define
Oi5 = MPlusP(5,Oi)
... then select the projector "FractalAB" and call it with the following arguments:
Or, Oi5, 9, Oi5, -0.04*Pi, 1, 0, 1
... then click on [hear]... this is a brand new way of listening to Bach !
-- Back to the tutorials index--
-- Back --