GeoMaestro

-- Back to the tutorials index--

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:


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:

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: 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 --