So I usually use UserView
s to control Pbind
s, but here the Pbind
is supposed to control the UserView
!
My concern was that both are kind of blackboxes, doing there own thing without communicating much with other parts of the software :
-
UserView
’s drawFunc
do not take custom arguments, but can access a variable that has been declared within it’s scope.
-
Pattern
s internally deal with their job, but won’t do more.
Fortunately, there’s a special pattern, Pfunc
, which allows to execute a Function
every time the Pbind
plays. More importantly, it takes the current Event
the Pbind
is playing as argument ! In other words, it allows the Pbind
to introspect itself, seeing it’s current parameters (i.e. freq, amp, etc), and to communicate with other parts of the software that way.
So the following code shows how to control an UserView
from within a Pbind
:
(
// Create a main window :
var win = Window();
// Create our custom view that will display the played note :
var display = UserView();
// The notes the Pbind can play :
var availableNotes = [58, 60, 61, 63, 65, 66, 68, 70].midicps;
// We need a variable that the drawFunc can access,
// which will hold the played note :
var playedNote = -1;
// This is our pbind !
// Here, I can create a Pfunc related to a non-existent symbol name (\foo),
// which is passed the current event the pbind has played.
// This in turn, allows me to reference the note inside the variable and to call the view refreshing.
var pbind = Pbind(
\instrument, \default,
\amp, 0.5,
\freq, Prand(availableNotes, inf),
// Here :
\foo, Pfunc({ |event|
// Pass the Pbind note to the variable
playedNote = event.freq;
// And call for the view refresh, we must use {}.defer because we don't have access to UI yet.
{ display.refresh }.defer;
0 // We must return a value so that the Pfunc keeps running
})
).play;
// Now, we'll just setup our display drawFunc, pretty simple for now,
// just write down the note that has been played :
display.drawFunc = { |view|
Pen.stringCenteredIn(
playedNote.asString,
Rect(
0, 0,
view.bounds.width, view.bounds.height
)
)
};
// Add the display to the window's view layout
win.layout_(
VLayout(display)
);
// And go !
win.front;
// This can be very handy, comment if needed :
CmdPeriod.doOnce({ win.close });
)
With this, if you’re in the DIY mood, I think it’s enough to figure out how to simulate the Galton box.
However, here is the simple version of the simulation (fully commented if you’d like to understand how it works) :
(
// Create a main window :
var win = Window();
// Create the view that will contain every counter :
var display = UserView().layout_(HLayout());
// The notes the Pbind can play :
var availableNotes = [58, 60, 61, 63, 65, 66, 68, 70].midicps;
// Create an array that will hold each note's occurence count :
var timesNotesWerePlayed = Array.fill(availableNotes.size, { 0 });
var myFavoriteColor = Color(1, 0.333, 0.333);
// Serialisation time !
// For each note that can be played, we'll create an UserView()
// that will display a rectangle,
// which height equals the number of time the note has been played
availableNotes.size.do({ |note, index|
var view = UserView();
view.drawFunc_({ |view|
Pen.fillColor_(myFavoriteColor);
Pen.fillRect(
Rect(
0,
view.bounds.height - timesNotesWerePlayed[index],
view.bounds.width,
timesNotesWerePlayed[index] // <- /!\ Here we access the variable /!\
)
);
});
// Then, we had it to the display layout :
display.layout.add(view);
});
// End of serialisation
// The pbind :
Pbind(
\instrument, \default,
\dur, 0.25,
\amp, 0.5,
\freq, Prand(availableNotes, inf),
\foo, Pfunc({ |event|
// Use .indexOf to find which note was played
var playedNote = availableNotes.indexOf(event.freq);
// Tell the software the note has been played once more
timesNotesWerePlayed[playedNote] = timesNotesWerePlayed[playedNote] + 1;
// We could only refresh the associated view
{ display.refresh }.defer;
0
})
).play;
// Add the display to the window's view layout
win.layout_(
VLayout(display)
);
// And go !
win.front;
// This can be very handy, comment if needed :
CmdPeriod.doOnce({ win.close });
)
It’s honest work, but it ain’t much. The following code is much more advanced (and evilly uncommented), and is a polish version of the previous code. It lacks a few optimizations, but I don’t think it matters that much :
(
var win = Window();
var display = UserView().layout_(HLayout().margins_(30));
var availableNotes = [58, 60, 61, 63, 65, 66, 68, 70].midicps;
var timesNotesWerePlayed = Array.fill(availableNotes.size, { 0 });
var timers = Array.fill(availableNotes.size, { 0 });
var myFavoriteColor = Color(1, 0.333, 0.333);
var mySecondFavoriteColor = Color(1, 1, 1);
var blinkTime = 0.25;
var displayRatio = 0.1;
var playedNote;
availableNotes.size.do({ |note, index|
var view = UserView();
var counterView = UserView().animate_(true).frameRate_(24);
var numberView = UserView().animate_(true);
var barSize = 0;
counterView.drawFunc_({ |view|
barSize = view.bounds.height * displayRatio * timesNotesWerePlayed[index];
Pen.fillColor_(
Color(
myFavoriteColor.red +
(mySecondFavoriteColor.red - myFavoriteColor.red * timers[index]),
myFavoriteColor.green +
(mySecondFavoriteColor.green - myFavoriteColor.green * timers[index]),
myFavoriteColor.blue +
(mySecondFavoriteColor.blue - myFavoriteColor.blue * timers[index]),
)
);
Pen.fillRect(
Rect(
0, view.bounds.height - barSize,
view.bounds.width, barSize
)
);
if(timers[index] > 0)
{ timers[index] = timers[index] - (blinkTime.reciprocal / view.frameRate) }
{ timers[index] = 0 };
});
numberView.drawFunc = { |view|
Pen.stringCenteredIn(
timesNotesWerePlayed[index].asString,
Rect(
0, 0,
view.bounds.width, view.bounds.height
),
Font.default.deepCopy.size_(view.bounds.height * 0.5),
Color.white
)
};
view.layout_(
VLayout(
[counterView, stretch: 6],
[numberView, stretch: 1]
).margins_(3)
);
display.layout.add(view);
});
Pbind(
\instrument, \default,
\dur, 0.25,
\amp, 0.5,
\freq, Prand(availableNotes, inf),
\foo, Pfunc({ |event|
playedNote = availableNotes.indexOf(event.freq);
timesNotesWerePlayed[playedNote] = timesNotesWerePlayed[playedNote] + 1;
if((timesNotesWerePlayed[playedNote] * displayRatio) >= 1)
{ displayRatio = displayRatio / 2 };
timers[playedNote] = 1;
0
})
).play;
display.drawFunc_({ |view|
Pen.fillColor_(myFavoriteColor);
Pen.fillRect(
Rect(
0, 0,
view.bounds.width, view.bounds.height
)
);
Pen.fillColor_(Color.black);
Pen.fillRect(
Rect(
5, 5,
view.bounds.width - 10, view.bounds.height - 10
)
);
});
win.layout_(
VLayout(display).margins_(0)
);
win.front;
CmdPeriod.doOnce({ win.close });
)
Do not hesitate if you have questions about the code,
<3 ,
Simon