MKSynthPatch
The MKSynthPatch
class is abstract; each
subclass of MKSynthPatch
describes a unique
strategy for creating a musical sound. It does this by implementing
methods that provide two things:
A patch specification. A patch is a configuration of DSP synthesis/processing elements.
A scheme for playing the patch. This consists of
defining the conditions in which the patch is turned on and off and
how MKNote
parameters are used to control it
while it's running.
Designing a patch is actually quite simple: The
MusicKit provides an object-oriented
interface to the DSP, thus protecting the
MKSynthPatch
designer from the rigors of
programming directly in MC56000 assembly code.
While concern for efficiency makes some knowledge of
DSP memory organization necessary,
MKSynthPatch
design makes greater demands of
your imagination in creating new sound-making schemes than of your
ability to examine and grasp the small print of signal
processing.
The MusicKit defines a number of
conventions for controlling a patch. Most of these conventions are
manifested as methods that are declared as subclass responsibilities
by the MKSynthPatch
class. Other conventions
are given as general guidelines that should be followed to maintain
consistency with the MKSynthPatch
subclasses
provided by the MusicKit.
MKSynthPatch
This section describes, by example, the basic steps for creating
a MKSynthPatch
subclass. The example
MKSynthPatch
produces a single sine wave (with
a settable frequency, amplitude, and bearing) for each
MKNote
it receives. The design is broken into
two parts: designing the patch specification, and playing the patch.
While the methodology shown for playing the patch introduces a number
of MKSynthPatch
design conventions, it lacks
some important features that enhance musical flexibility. These
features are shown in the more complex
MKSynthPatch
design demonstrated in
the Section called A Better MKSynthPatch
, later in this chapter.
exampleSynthPatch | |
---|---|
Please see the programming example
./Examples/MusicKit/exampleSynthPatch. It
illustrates a variety of |
Every MKSynthPatch
contains a recipe for
creating a patch. The ingredients of the patch are
MKUnitGenerator
and
MKSynthData
objects (collectively referred to
as synthElements):
Each MKUnitGenerator
subclass
represents a specific signal processing function. The MusicKit
supplies a number of MKUnitGenerator
subclasses
that perform functions such as creating and combining signals,
filtering, and adding the finished product to the output sample
stream.
MKSynthData
objects represent
data. These objects can be used for downloading information to the
DSP; for example, a MKWaveTable
is represented
on the DSP as a MKSynthData
. Another important
use of a MKSynthData
is to provide a location
through which one MKUnitGenerator
can send data
to another MKUnitGenerator
. This type of
MKSynthData
object is called a
patchpoint.
The list of synthElement specifications and instructions for
connecting these elements to each other are encapsulated in a
MKPatchTemplate
object. Every MKSynthPatch
subclass creates at least one MKPatchTemplate
―most create only
one. A MKPatchTemplate
is created and returned by the
MKSynthPatch
class method patchTemplateFor:, a subclass responsibility.
In the following example, a single sine wave
MKSynthPatch
is declared and its patchTemplateFor: method is implemented:
/* The following files must be imported. */ #import <MusicKit/MusicKit.h> #import <MusicKit/unitgenerators.h> /* We call our simple SynthPatch 'Simplicity'. */ @implementation Simplicity /* A static integer is created for each synthElement. */ static int osc, /* sine wave UnitGenerator */ stereoOut, /* sound output UnitGenerator */ outPatchpoint; /* SynthData */ + patchTemplateFor:aNote /* The argument is ignored in this implementation. */ { /* * Step 1: Create an instance of the PatchTemplate class. This * method is automatically invoked each time the MKSynthPatch * receives a Note. However, the PatchTemplate should only be * created the first time this method is invoked. If the object * has already been created, it's immediately returned. */ static id theTemplate = nil; if (theTemplate) return theTemplate; theTemplate = [PatchTemplate new]; /* * Step 2: Add synthElement specifications to the PatchTemplate. * The first two are UnitGenerators; the last is a SynthData * that's used as a patchpoint. */ osc = [theTemplate addUnitGenerator:[OscgUGxy class]]; stereoOut = [theTemplate addUnitGenerator:[Out2sumUGx class]]; outPatchpoint = [theTemplate addPatchpoint:MK_xPatch]; /* * Step 3: Specify the connections between synthElements. */ [theTemplate to:osc sel:@selector(setOutput:) arg:outPatchpoint]; /* Always return the PatchTemplate. */ return theTemplate; } |
After creating the MKPatchTemplate
instance (step 1 in the example), synthElement specifications are
added to it (step 2) using methods defined by the
MKPatchTemplate
class. There are three basic
methods to do this (a fourth method will be discussed later):
addUnitGenerator:
addSynthData:length:
addPatchpoint:
Each of these methods returns an integer value that's used as an
index to the added synthElement. Subsequent references to the
synthElements are always made through these indices. Since all
instances of a particular MKSynthPatch
subclass
use the same set of indices, the variables that store the values
returned by these methods must be declared statically and be made
global to the entire class.
Finally, instructions for connecting the synthElements are
specified by invoking MKPatchTemplate
's to:sel:arg: method (step 3). The arguments to
this method are the receiver, selector, and argument, respectively, of
a message that will be sent when a MKSynthPatch
instance is created. Simplicity specifies a single connection:
[theTemplate to:osc sel:@selector(setOutput:) arg:outPatchpoint] |
When an instance of Simplicity is created and played, the output
of the MKUnitGenerator
indexed by osc will be set to the
MKSynthData
indexed by outPatchpoint.
To understand the synthElements used in the example, you need to
be familiar with a simple detail of DSP memory organization. DSP
memory is divided into three sections, x, y, and p. x and y memory
are used for data; p memory is used for program code. Thus,
MKSynthData
objects represent data in either x
or y memory, while MKUnitGenerator
s represent
DSP functions that always reside in p memory. This is illustrated in
Figure 5-2.
The MusicKit further divides
DSP memory into logical segments, represented as
integer constants. In designing a
MKSynthPatch
, you only need to be concerned
with four of these segments:
MK_xPatch
is used for
patchpoints in x memory.
MK_yPatch
is for
patchpoints in y memory.
MK_xData
is for
non-patchpoint MKSynthData
objects in x
memory.
MK_yData
is for
non-patchpoint MKSynthData
objects in y
memory.
A MKSynthData
object is specified by its
segment. For example, Simplicity's patch contains a single
MKSynthData
(a patchpoint) that resides in x
memory, as set in the message
/* Add an x segment patchpoint. */ [theTemplate addPatchpoint:MK_xPatch] |
MKUnitGenerator
s also refer to x and y
memory in order to properly read and write data. Recall the first
MKUnitGenerator
added to Simplicity's
patch:
[theTemplate addUnitGenerator:[OscgUGxy class]] |
The “x” and “y” in the class name
OscgUGxy
refer to x and y memory spaces,
respectively. OscgUGxy
is a simple
MKUnitGenerator
that has a single input for
reading data and a single output for writing data. The order of these
data spigots, or memory arguments, is given in
the MKUnitGenerator
name as output followed by
input. Thus, OscgUGxy
's writes data to x
memory (output) and reads it from y memory (input). The
MusicKit provides a class for each memory
permutation: OscgUGyy
,
OscgUGyx
, OscgUGxy
, and
OscgUGxx
. These are called leaf
classes of the master class
OscgUG
. Aside from the differing memory references, the
leaf classes are exactly the same. Every
MKUnitGenerator
function provided by the
MusicKit is similarly organized into a
master class and a complete set of leaf classes.
When describing a
subclass of |
The Oscg
family of
MKUnitGenerator
s provides a general oscillator
function (the “g” in Oscg
stands
for “general”). An oscillator is a module that creates a
signal by cycling over a table of values, called a lookup
table, that represents a single period of a waveform. In a
general oscillator, the lookup table isn't part of the
MKUnitGenerator
. You can supply the oscillator
with a lookup table by using a MKWaveTable
object (this will be demonstrated in a subsequent example).
Alternatively, the oscillator can use the built-in sineROM, a
read-only section of y memory that contains a single period of a sine
wave. Simplicity's OscgUGxy
does the latter: It reads the
sineRom, therefore its input must read from y
memory. In the example, the connection between the sineROM and
OscgUGxy
is made by default―it needn't be specified
through the to:sel:arg:
method.
One of the conventions of designing a patch is to balance, as
much as possible, the use of x and y memory. Since Simplicity's
oscillator must read from y memory in order to read the sineROM, its
output is set to x memory. So of the four Oscg
leaf classes, OscgUGxy
is chosen.
The other MKUnitGenerator
in Simplicity's
patch, Out2sumUGx
, is a special MKUnitGenerator
that adds a stream of (two-channel) sample data to the stereo output
sample stream. The single memory argument (the “x” in
Out2sumUGx
) is the MKUnitGenerator
's input.
Simplicity uses the Out2sumUGx
leaf class so the
MKUnitGenerator
can read the
MK_xPatch
patchpoint that's written to by
OscgUGxy
. Figure 5-3 shows a diagram of the
complete patch, superimposed on the DSP memory
layout.
Notice that Figure 5-3 shows a connection
between the patchpoint and the input of Out2Sum, a connection that
isn't specified in Simplicity's MKPatchTemplate
. By convention, the
connection to the output MKUnitGenerator
is
implemented in a method that's invoked when the
MKSynthPatch
receives a noteOn. This method,
called noteOnSelf:, is examined in
the next section.
The illustration in Figure 5-3 introduces schematic conventions that will be used throughout this section: |
MKUnitGenerator
s are drawn as
half-circles (oscillators) or as inverted triangles (everything
else).
A MKUnitGenerator
's inputs are
at the top of the icon, its outputs are on the
bottom.
Patchpoints are drawn as diamonds. Other
MKSynthData
objects, including the predefined
MKSynthData
that represents the DSP sineROM,
are rectangles.
Data written to a MKSynthData
arrives at the left side of the icon. Data is read from the
right.
It's often convenient to represent a patch without including the
patchpoints and without superimposing the schematic on the DSP memory
diagram. Figure 5-4 shows Simplicity's
patch in an abbreviated form. A MKSynthData
's
memory space is indicated by an x or y inside the icon. The spaces
from which and to which a MKUnitGenerator
reads
and writes data is similarly indicated just to the right of each input
and output.
Keep in mind that a MKSynthPatch
object
is ordinarily created and controlled by an instance of
MKSynthInstrument
. During a MusicKit
performance, the MKSynthInstrument
distributes
the MKNote
s it receives to the various
MKSynthPatch
objects that it controls through
the methods noteOn:, noteUpdate:, and noteOff: (the
MKSynthInstrument
treats a noteDur as a noteOn
and manufactures a noteOff to balance it; also, the
MKSynthInstrument
normally suppresses mutes).
The design of a MKSynthPatch
subclass must
include a methodology to control the patch in response to these
messages. This is done by implementing the following methods:
noteOnSelf:
noteUpdateSelf:
noteOffSelf:
noteEndSelf
As their names imply, the first three of these methods are
invoked automatically when the MKSynthPatch
receives a noteOn:, noteUpdate:, or noteOff: message, respectively. noteEndSelf is automatically invoked when the
MKNote
is completely finished and is provided
to accommodate the release portion of the
MKSynthPatch
's
MKEnvelope
s.
While these four methods
aren't subclass responsibilities, the default implementations provided
by the |
Simplicity, our example MKSynthPatch
,
implements noteOnSelf: as
follows:
- noteOnSelf:aNote { /* Step 1: Read the parameters in the Note and apply them to the patch. */ [self applyParameters:aNote]; /* * Step 2: Turn on the patch by connecting the Out2sumUGx object * to the patchpoint and sending the run message to all the * synthElements. */ [[self synthElementAt: stereoOut] setInput: [self synthElementAt:outPatchpoint]]; [synthElements makeObjectsPerform:@selector(run)]; return self; } |
The first of the two steps, applying the
MKNote
parameters to the patch, is performed in
the applyParameters: method. The
implementation of this method is described in a later section.
The second step, turning on the patch, distinguishes the
noteOnSelf: method from the others.
The first message in step 2 sets the input of stereoOut (the Out2Sum
MKUnitGenerator
) to the patchpoint outPatchpoint (recall that this connection was
purposely left unspecified in the MKPatchTemplate
). The final message
sends run to each of the
MKSynthPatch
's synthElements. This causes the
MKUnitGenerator
s to begin operating.
While it isn't necessary
to send run to the patch's
patchpoint, it's convenient to send it to all synthElements as shown
in the example. |
The extremely important step of actually creating and connecting
the objects that make up Simplicity's patch is performed
automatically. As described in the next chapter, part of a
MKSynthInstrument
's duties when it receives a
MKNote
is to allocate an appropriate
MKSynthPatch
object to synthesize the
MKNote
. It does this by sending patchTemplateFor: to its
MKSynthPatch
subclass with the received
MKNote
as the argument. As we have seen, this
method returns a MKPatchTemplate
object. It then allocates a
MKSynthPatch
according to the specifications in
the MKPatchTemplate
and forwards the MKNote
to
the MKSynthPatch
through the noteOn: method. Thus, by the time the
MKSynthPatch
receives the noteOnSelf: message (which is sent by noteOn:) the patch has already been
created.
A MKSynthPatch
contains a
NSArray
of the objects that make up its patch
in its synthElements instance
variable. In the example above, a use of this instance variable is
given as the receiver of the message that causes the
MKUnitGenerator
s to start running:
[synthElements makeObjectsPerform: @selector(run)]; |
You can retrieve a particular object from the synthElements NSArray
by
invoking the synthElementAt: method,
passing the index of the synthElement as the argument. This is
demonstrated in the example above in the line
[[self synthElementAt:stereoOut] setInput:[self synthElementAt:outPatchpoint]]; |
synthElementAt:stereoOut
returns an instance of the object indexed by stereoOut. In other words, it returns an
instance of the Out2sumUGx
class, as specified in Simplicity's
patchTemplateFor: method. Similarly,
synthElementAt:outPatchpoint returns
Simplicity's patchpoint.
Finally, the return value of noteOnSelf: is significant: If the method
returns nil, the argument
MKNote
isn't synthesized. Simplicity's
implementation always returns self so
all noteOns that it receives are synthesized.
Simplicity's implementation of noteUpdateSelf: is straightforward; it simply applies its argument's parameters to the patch:
- noteUpdateSelf:aNote { [self applyParameters:aNote]; return self; } |
The value returned by noteUpdateSelf: is ignored.
The methods noteOffSelf: and
noteEndSelf work together to wind
down and deactivate a MKSynthPatch
. As
mentioned earlier, noteOffSelf: is
automatically sent when a noteOff is forwarded to the
MKSynthPatch
through the noteOff: method. When a noteOff arrives, the
MKSynthPatch
doesn't stop; rather, the noteOff
is taken as a signal to begin the release portions of any
MKEnvelope
s that are part of the patch. The
value returned by noteOffSelf: is
taken as the amount of time, in seconds, to wait before invoking
noteEnd; this value is usually the
release time of the MKSynthPatch
's amplitude
envelope. Since Simplicity doesn't have any
MKEnvelope
s, its implementation of noteOffSelf: always returns 0.0 (an example of
a MKSynthPatch
that uses
MKEnvelope
s is given later):
- (double)noteOffSelf:aNote { [self applyParameters:aNote]; /* No Envelopes, so no release time is needed. */ return 0.0; } |
Even though a noteOff is the beginning of the end of a
MKSynthPatch
's activity, the
MKNote
may contain some parameters; these
parameters are applied just as in the other methods, by invoking
applyParameters:.
After waiting the prescribed amount of time, the noteEnd message is sent. noteEnd invokes noteEndSelf, a method that deactivates the
MKSynthPatch
:
- noteEndSelf { /* Deactivate the SynthPatch by idling the output. */ [[self synthElementAt:stereoOut] idle]; return self; } |
The idle method is implemented
by all subclasses of MKUnitGenerator
. In its
implementation of idle, Out2sum
connects its input to a predefined patchpoint that always contains
zero data (the data in the patchpoint consists wholly of zeroes).
This effectively turns off the patch. Notice that the
MKSynthPatch
isn't freed, nor is its patch (as
specified in the MKPatchTemplate
) dismantled. An important convention
of MKSynthPatch
design is to perform the
minimum amount of work necessary when deactivating the object. This
makes both the deactivation itself and a subsequent reactivation (when
another noteOn arrives) as efficient as possible.
It should be noted that noteEnd
(and, thus, noteEndSelf) is also
invoked when the MKSynthPatch
is created,
thereby ensuring that the patch is silent until the first
MKNote
is received. This also explains why the
final connection to Out2sum
isn't specified in the
MKPatchTemplate
―if it was so specified, the connection would be
made only to be immediately severed upon reception of the noteEnd message (the patch is created before
noteEnd is sent).
The final step in our MKSynthPatch
design
is to supply it with parameter values. As mentioned earlier,
Simplicity has three settable attributes: frequency, amplitude, and
bearing. Simplicity implements the method applyParameters: to read the appropriate
parameters from its argument MKNote
and apply
their values to the patch:
- applyParameters:aNote { /* Retrieve and store the parameters. */ double myFreq = [aNote freq]; double myAmp = [aNote parAsDouble:MK_amp]; double myBearing = [aNote parAsDouble:MK_bearing]; /* Apply frequency if present. */ if ( !MKIsNoDVal(myFreq) ) [[self synthElementAt:osc] setFreq:myFreq]; /* Apply amplitude if present. */ if ( !MKIsNoDVal(myAmp) ) [[self synthElementAt:osc] setAmp:myAmp]; /* Apply bearing if present. */ if ( !MKIsNoDVal(myBearing) ) [[self synthElementAt:stereoOut] setBearing:myBearing]; } |
First, the parameters are retrieved from the argument
MKNote
. Notice that the freq method is used to retrieve frequency;
recall from the description of the MKNote
class
that this method returns the value of MK_freq or, in MK_freq's absence, a value converted from
MK_keyNum.
To apply a parameter value to the patch, you send a message to
the MKUnitGenerator
that controls that aspect
of the patch. The Oscg
MKUnitGenerator
controls frequency and amplitude, so setFreq: and setAmp: are sent to the patch's OscgUGxy
object. Out2sum
controls bearing, so it receives setBearing:. These methods are defined in the
MKUnitGenerator
s' master classes.
The complete source code for Simplicity is provided as an
example MKSynthPatch
in the files Simplicity.m and Simplicity.h in the directory ./Examples/MusicKit/exampsynthpatch.
MKSynthPatch
Build a better MKSynthPatch
and the world
will beat a path to your door. This section improves the
MKSynthPatch
design shown in the previous
sections. Of greatest significance is the envelope control that's
added to both frequency and amplitude. The patch specification is
accordingly more complex than in Simplicity. In addition, a number of
conventions are introduced in the methods that play the patch, not
only to accommodate envelope control but, more important, to make the
MKSynthPatch
more efficient and more adaptable
to the caprice of musical performance.
The MKSynthPatch
designed here is called
Envy. Like Simplicity, it produces a single sine wave with a settable
frequency, amplitude, and bearing.
The following example shows the implementation of Envy's patchTemplateFor: method:
/* Statically declare the synthElement indices. */
static int ampAsymp, /* amplitude envelope UG */
freqAsymp, /* frequency envelope UG */
osc, /* oscillator UG */
stereoOut, /* output UG */
ampPp, /* amplitude patchpoint */
freqPp, /* frequency patchpoint */
outPp; /* output patchpoint */
+ patchTemplateFor:aNote
{
/* Step 1: Create (or return) the PatchTemplate. */
static id theTemplate = nil;
if (theTemplate)
return theTemplate;
theTemplate = [PatchTemplate new];
/* Step 2: Add the SynthElement specifications. */
ampAsymp = [theTemplate addUnitGenerator:[AsympenvUGx class]];
freqAsymp = [theTemplate addUnitGenerator:[AsympenvUGy class]];
osc = [theTemplate addUnitGenerator:[OscgafiUGxxyy class]];
stereoOut = [theTemplate addUnitGenerator:[Out2sumUGx class]];
ampPp = [theTemplate addPatchpoint:MK_xPatch];
freqPp = [theTemplate addPatchpoint:MK_yPatch];
outPp = ampPp;
/* Step 3: Specify the connections. */
[theTemplate to:ampAsymp sel:@selector(setOutput:) arg:ampPp];
[theTemplate to:freqAsymp sel:@selector(setOutput:) arg:freqPp];
[theTemplate to:osc sel:@selector(setAmpInput:) arg:ampPp];
[theTemplate to:osc sel:@selector(setIncInput:) arg:freqPp];
[theTemplate to:osc sel:@selector(setOutput:) arg:outPp];
/* Return the |
The three-step design outline is the same as in Simplicity: The
MKPatchTemplate
is created, the synthElement specifications are added to
the MKPatchTemplate
, and the connections between SynthElements are
specified. However, two new MKUnitGenerator
families, Oscgafi
and Asymp, are introduced. These are examined in
the next sections; briefly, Oscgafi
is a general oscillator that
allows another MKUnitGenerator
to its amplitude
and frequency. Asymp is an envelope handler; it's used to apply
MKEnvelope
objects to the patch. Asympenv is a
variant of Asymp that is better-suited to interactive real-time
applications. The patch is illustrated in Figure 5-5.
Returning to the code example, notice that ampPp and outPp are given the same value:
ampPp = [theTemplate addPatchpoint:MK_xPatch]; . . . outPp = ampPp; |
When the patch is created, these two synthElement indices will
refer to the same object. In other words, the patchpoint that's used
in the connection between ampAsymp
and osc is reused in the connection
between osc and stereoOut. Reusing patchpoints makes the patch
smaller and more efficient. However, you can only reuse patchpoints
if the patch's MKUnitGenerator
s are executed in
a predictable order. Consider how Envy's
MKUnitGenerator
s use the shared
patchpoint:
ampAsymp writes to ampPp.
osc reads from ampPp and writes to outPp.
stereoOut reads from outPp.
For the shared patchpoint scheme to work, the
MKUnitGenerator
s must be executed in the order
given―chaos would reign should stereoOut read from outPp before osc writes to it. The order in which a
MKSynthPatch
's
MKUnitGenerator
s are executed is the order in
which their specifications are added to the
MKPatchTemplate
. Thus, Envy's
MKUnitGenerator
s are executed in the following
order:
ampAsymp
freqAsymp
osc
stereoOut
Since ampAsymp is executed before osc, and osc before stereoOut, the patchpoint between ampAsymp and osc can be reused as the patchpoint between osc and stereoOut.
For some patches, the
order in which the |
Oscgafi
The Oscgafi
MKUnitGenerator
is the most flexible oscillator
provided by the MusicKit. In addition to
allowing envelope control of amplitude and frequency, it also performs
an interpolation, minimizing the noise that's sometimes introduced
when reading the lookup table. The components of the
MKUnitGenerator
's name summarize these
features:
Table 5-4. Class naming convention for Oscgafi
Component | Meaning |
---|---|
Osc | Oscillator |
g | General |
a | Amplitude control |
f | Frequency control |
i | Interpolation |
Oscgafi
has four memory arguments, in this order:
output
amplitude control input
frequency control input
lookup table input
The permutations of a MKUnitGenerator
with four memory arguments results in 16 leaf classes. Envy uses the
xxyy version (OscgafiUGxxyy
), so the memory arguments correspond to
memory space as follows:
Table 5-5. Memory Space Association to Memory Arguments
Argument | Space |
---|---|
output | x |
amplitude control | x |
frequency control | y |
lookup table | y |
A notable difference between Oscg
and
Oscgafi
is that in the latter, frequency and
amplitude aren't set directly through messages to the oscillator. To
control these attributes, you affect the
MKUnitGenerator
s that are connected to
Oscgafi
's inputs. The MusicKit
provides a C function, called MKUpdateAsymp()
that does this for you. This function is described later as it's used
to apply parameter values to Envy's patch.
In addition, Oscgafi
's frequency input is actually an increment
input―the oscillator's frequency is defined by the increment,
or step size, that it uses when reading its lookup table. This
explains why osc is connected to the
freqPp patchpoint through the
setIncInput: method:
[theTemplate to:osc sel:@selector(setIncInput:) arg:freqPp]; |
Oscgafi
's incAtFreq: method is provided to translate
frequencies into increments. This, too, will be used when applying
parameter values.
The Asymp and Asympenv MKUnitGenerator
s
are envelope handlers; they translates the data in an
MKEnvelope
object and load it onto the
DSP. An MKEnvelope
object
is associated with an Asymp or Asympenv through the
MKUpdateAsymp()
function. The only difference
between Asymp and Asympenv is that the former allows for arbitrarily
long envelopes and the latter is better-suited to interactive
real-time applications.
Envy uses two Asympenvs, one to control the frequency of
Oscgafi
and the other to control its amplitude.
The leaf classes are chosen to match the memory arguments in
Oscgafi
: The Asympenv leaf class that controls amplitude is
AsympenvUGx; for frequency, it's AsympenvUGy.
A number of new conventions for playing and controlling a
MKSynthPatch
are introduced in the following
sections. In particular, the conventions regarding preemption,
rearticulation, and “sticky” parameters are
demonstrated.
A convention of MKSynthPatch
design (one
that wasn't followed in the implementation of Simplicity) is to create
an instance variable for each parameter the
MKSynthPatch
responds to. The variables are
used to maintain the state of the object's patch.
Because of the introduction of envelope control into the patch,
Envy responds to several more parameters than did Simplicity. These
are shown as they are declared as instance variables in the
MKSynthPatch
's interface file (Envy.h):
@interface Envy:SynthPatch { /* Amplitude parameters. */ id ampEnv; /* the Envelope object for amplitude */ double amp1, /* amplitude at y=1 */ amp0, /* amplitude at y=0 */ ampAtt, /* ampEnv attack duration in seconds */ ampRel; /* ampEnv release duration in seconds*/ /* Frequency parameters. */ id freqEnv; /* the Envelope object for frequency */ double freq1, /* frequency at y=1 */ freq0, /* frequency at y=0 */ freqAtt, /* freqEnv attack duration in seconds*/ freqRel; /* freqEnv release duration in seconds */ /* Other parameters. */ double portamento; /* transition time in seconds */ double bearing; /* stereo location */ } |
A set of defaults for the parameter instance variables should
also be included in a MKSynthPatch
design.
Envy implements a method called setDefaults to provide this:
- setDefaults { ampEnv = nil; amp0 = 0.0; amp1 = MK_DEFAULTAMP; /* 0.1 */ ampAtt = MK_NODVAL; /* parameter not present */ ampRel = MK_NODVAL; /* parameter not present */ freqEnv = nil; freq0 = 0.0; freq1 = MK_DEFAULTFREQ; /* 440.0 */ freqAtt = MK_NODVAL; /* parameter not present */ freqRel = MK_NODVAL; /* parameter not present */ portamento = MK_DEFAULTPORTAMENTO; /* 0.1 */ bearing = MK_DEFAULTBEARING; /* 0.0 (center) */ return self; } |
By convention, a MKSynthPatch
's parameter
instance variables should be set to their default values before the
MKSynthPatch
begins a new phrase. This is done
by invoking setDefaults from the
noteEndSelf method. Keep in mind
that noteEnd, which invokes noteEndSelf, is invoked when a new
MKSynthPatch
is created, so setDefaults: will be invoked before the first
MKNote
arrives as well as after the end of each
phrase.
However, one other condition must be considered―that of
the preempted MKSynthPatch
―which, by
definition, isn't sent the noteEnd
message. The MKSynthPatch
class defines a
method preemptFor: that you can
redefine in your subclass to reset the parameter instance variables to
their default values and to provide any other special behavior for a
preempted MKSynthPatch
. The method is invoked
just before the MKSynthPatch
receives, in a
noteOn: message, the
MKNote
for which it was preempted (the argument
to preemptFor: is this same
MKNote
). Envy implements preemptFor: to preempt the amplitude
MKEnvelope
and invoke setDefaults:. It ignores the argument:
- preemptFor:aNote { [[self synthElementAt:ampAsymp] preemptEnvelope]; [self setDefaults]; return self; } |
These two methods follow the same form as those in Simplicity:
- noteOnSelf:aNote { /* Apply the parameters to the patch. */ [self applyParameters:aNote]; /* Make the final connection to the output sample stream.*/ [[self synthElementAt:stereoOut] setInput:outPp]; /* Tell the UnitGenerators to begin running. */ [synthElements makeObjectsPerform:@selector(run)]; return self; } - noteUpdateSelf:aNote { /* Apply the parameters to the patch. */ [self applyParameters:aNote]; return self; } |
Once again, both methods invoke applyParameters: to apply the Note's parameters
to the patch. In addition, noteOnSelf: completes the connection between
Oscgafi
and Out2sum
, and it tells the
MKUnitGenerator
s to run by sending run to each of the synthElements.
Recall that the value returned by noteOffSelf: is taken as the amount of time to
wait (in seconds) before noteEnd is
invoked. Typically, this value is the release time of the
MKSynthPatch
's amplitude
MKEnvelope
:
- (double)noteOffSelf:aNote { /* Apply the parameters. */ [self applyParameters: aNote]; /* Signal the release portion of the frequency Envelope. */ [[self synthElementAt:freqAsymp] finish]; /* Same for amplitude, but also return the release duration. */ return [[self synthElementAt:ampAsymp] finish]; } |
An Asympenv responds to the finish message by signaling the release portion
of its MKEnvelope
; the method returns the
duration of the release.
As in Simplicity, noteEndSelf
sends idle to the Out2sum
object to
remove it from the output sample stream. It also aborts the frequency
MKEnvelope
(we're assured that the amplitude
MKEnvelope
has finished since its demise is
what causes this method to be invoked) and then invokes the setDefaults method, as dictated in the Section called Declaring the Parameters, above.
- noteEndSelf { /* Remove the patch's Out2sum from the output sample stream. */ [[self synthElementAt:stereoOut] idle]; /* Abort the frequency Envelope. */ [[self synthElementAt:freqAsymp] abortEnvelope]; /* Set the instance variables to their default values. */ [self setDefaults]; return self; } |
The manner in which a MKNote
's parameters
are applied to a patch can depend on the performance context in which
the MKSynthPatch
receives the
MKNote
. This context, called phrase
status, is represented as an MKPhraseStatus constant and is
automatically set when the MKSynthPatch
receives a phrase event message (such as noteOn: and noteUpdate:). There are seven phrase
states:
MK_phraseOn
means
that the received MKNote
is a noteOn and the
MKSynthPatch
has been freshly allocated to
synthesize the MKNote
. This status indicates
the beginning of a new phrase.
MK_phraseRearticulate
indicates a noteOn that rearticulates an existing phrase.
MK_phraseOnPreempt
is also used
for noteOns, but it indicates that the
MKSynthPatch
has been preempted to synthesize
the MKNote
. Like
MK_phraseOn
, this status means that a new phrase
is beginning.
MK_phraseUpdate
means
that the MKNote
is a noteUpdate and the
MKSynthPatch
is in the attack or stickpoint
portions of its MKEnvelope
s (the
MKSynthPatch
's synthStatus is
MK_running
).
MK_phraseOff
indicates a noteOff.
MK_phraseOffUpdate
is
for a noteUpdate that arrives during the release portion of the
MKEnvelope
s (synthStatus is
MK_finishing
).
MK_phraseEnd
is used
to indicate the end of a phrase.
A MKSynthPatch
's phrase status, which is
retrieved by sending phraseStatus to
the MKSynthPatch
, is provided solely as a
convenience to MKSynthPatch
designers and is
only valid within the implementations of the noteOnSelf:, noteUpdateSelf:, noteOffSelf:, and noteEndSelf methods. Sent to a
MKSynthPatch
from outside these methods,
phraseStatus returns MK_noPhraseActivity.
You can use phrase status in the design of your
MKSynthPatch
in a test that leads to
specialized behavior. For example, you may want to apply certain
noteUpdate parameters differently, depending on whether the phrase
status is MK_phraseUpdate
or
MK_phraseOffUpdate
. Two conventional uses of
phrase status―as an argument to the
MKUpdateAsymp()
function and to determine if a
MKNote
is the beginning of a new
phrase―are demonstrated in the next section.
The way that Envy applies a MKNote
's
parameters is more sophisticated than the manner employed by
Simplicity. A convention ignored in the design of Simplicity holds
that a noteOn that rearticulates a phrase should inherit, if
necessary, the values of the parameters in the phrase so far. For
example, if a rearticulating noteOn doesn't contain an amplitude
MKEnvelope
, it uses the one set in the previous
noteOn. The implementation of Envy accommodates this by storing its
parameter values as instance variables: It can supply a
“missing” parameter by using the value stored in the
appropriate variable.
Determining the correct value for amplitude and frequency is
more complicated in the implementation of Envy than in that of
Simplicity. Because of its use of MKEnvelope
objects, Envy's amplitude and frequency depend on the values of a
number of related parameters. The MusicKit
provides a C function called MKUpdateAsymp()
that
helps to untangle this web. The function takes, as arguments, all the
objects and parameter values associated with a particular
MKEnvelope
and applies them in a predictable
manner to the attribute that the MKEnvelope
controls.
MKUpdateAsymp()
takes eight
arguments:
An Asymp or Asympenv object
An MKEnvelope
object
The MKEnvelope
's value when y =
0.0
The MKEnvelope
's value when y =
1.0
The MKEnvelope
's attack
time
The MKEnvelope
's release
time
The portamento value (not currently supported for Asympenv)
The current phrase status
The function's behavior is described in
MusicKit Function References.
Briefly, it applies an MKEnvelope
(argument 2)
to an Asymp or Asympenv (argument 1) after properly scaling the
MKEnvelope
's value range (arguments 3 and 4)
and setting its attack and release times (arguments 5 and 6).
Portamento (argument 7) is used only if the phrase status (argument 8)
is MK_phraseRearticulate
.
Envy's implementation of the applyParameters: method demonstrates the
conventional way to apply parameters to a patch that includes
MKEnvelope
s:
- applyParameters:aNote { /* Retrieve and store the parameters. */ id myAmpEnv = [aNote parAsEnvelope:MK_ampEnv]; double myAmp0 = [aNote parAsDouble:MK_amp0]; double myAmp1 = [aNote parAsDouble:MK_amp1]; double myAmpAtt = [aNote parAsDouble:MK_ampAtt]; double myAmpRel = [aNote parAsDouble:MK_ampAtt]; id myFreqEnv = [aNote parAsEnvelope:MK_freqEnv]; double myFreq0 = [aNote parAsDouble:MK_freq0]; double myFreq1 = [aNote freq]; double myFreqAtt = [aNote parAsDouble:MK_freqAtt]; double myFreqRel = [aNote parAsDouble:MK_freqRel]; double myPortamento = [aNote parAsDouble:MK_portamento]; double myBearing = [aNote parAsDouble:MK_bearing]; /* Store the phrase status. */ MKPhraseStatus phraseStatus = [self phraseStatus]; /* Is aNote a noteOn? */ BOOL isNoteOn = [aNote noteType] == MK_noteOn; /* Is aNote the beginning of a new phrase? */ BOOL isNewPhrase = (phraseStatus == MK_phraseOn) || (phraseStatus == MK_phraseOnPreempt); /* Used in the parameter checks. */ BOOL shouldApplyAmp = NO; BOOL shouldApplyFreq = NO; BOOL shouldApplyBearing = NO; /* The same portamento is used in both frequency and amplitude. */ if ( !MKIsNoDVal(myPortamento) ) { portamento = myPortamento; shouldApplyAmp = YES; shouldApplyFreq = YES; } /* Check the amplitude parameters and set the instance variables. */ if (myAmpEnv != nil) { ampEnv = myAmpEnv; shouldApplyAmp = YES; } if (!MKIsNoDVal(myAmp0)) { amp0 = myAmp0; shouldApplyAmp = YES; } if (!MKIsNoDVal(myAmp1)) { amp1 = myAmp1; shouldApplyAmp = YES; } if (!MKIsNoDVal(myAmpAtt)) { ampAtt = myAmpAtt; shouldApplyAmp = YES; } if (!MKIsNoDVal(myAmpRel)) { ampRel = myAmpRel; shouldApplyAmp = YES; } /* Apply the amplitude parameters. */ if (shouldApplyAmp || isNoteOn) MKUpdateAsymp([self synthElementAt:ampAsymp], ampEnv, amp0, amp1, ampAtt, ampRel, portamento, phraseStatus); /* Check the frequency parameters and set the instance variables. */ if (myFreqEnv != nil) { freqEnv = myFreqEnv; shouldApplyFreq = YES; } if (!MKIsNoDVal(myFreq0)) { freq0 = myFreq0; shouldApplyFreq = YES; } if (!MKIsNoDVal(myFreq1)) { freq1 = myFreq1; shouldApplyFreq = YES; } if (!MKIsNoDVal(myFreqAtt)) { freqAtt = myFreqAtt; shouldApplyFreq = YES; } if (!MKIsNoDVal(myFreqRel)) { freqRel = myFreqRel; shouldApplyFreq = YES; } /* Apply the frequency parameters. */ if (shouldApplyFreq || isNoteOn) MKUpdateAsymp([self synthElementAt:freqAsymp], freqEnv, [[self synthElementAt:osc] incAtFreq:freq0], [[self synthElementAt:osc] incAtFreq:freq1], freqAtt, freqRel, portamento, phraseStatus); /* Check and set the bearing. */ if (!MKIsNoDVal(myBearing)) { bearing = myBearing; shouldApplyBearing = YES; } if (shouldApplyBearing || isNewPhrase) [[self synthElementAT:stereoOut] setBearing:bearing]; return self; } |
As in Simplicity's implementation of applyParameters:, the value of each parameter
is stored and then checked to determine whether the parameter is
actually present in the argument MKNote
. In
addition, this implementation updates the values of the instance
variables to those of the parameters that are present.
Each parameter that affects amplitude is checked in its own
conditional statement. If the parameter is present, the shouldApplyAmp variable is set to YES,
indicating that the amplitude MKEnvelope
needs
to be updated. Finally, the value of shouldApplyAmp is logically or'd with the value
of isNoteOn, which is YES if
aNote is a noteOn. Thus, the
MKUpdateAsymp()
call for amplitude is
made if any of the tested parameters are present, and it's always made
if aNote is a noteOn.
The conditionals for applying the frequency parameters are the
same as those for amplitude. Notice, however, that the freq0 and freq1 values aren't passed directly to
MKUpdateAsymp()
. Instead, they're
used to retrieve increment values from osc through its incAtFreq: method. As mentioned earlier,
Oscgafi
's frequency value isn't set as a frequency in hertz, but
rather as an increment into its lookup table.
Finally, the bearing parameter is tested, its instance variable
is set, and the parameter is applied to the patch. Notice that
bearing is automatically applied if aNote is the beginning of a new phrase. Unlike
amplitude and frequency, bearing isn't controlled by an
MKEnvelope
, so it doesn't need to be
automatically applied if the MKNote
is simply a
rearticulation of an existing phrase.
MKWaveTable
Envy, although otherwise entertaining, is of limited timbral
interest―it can only produce a sine wave. Replacing Envy's
sine wave with a MKWaveTable
is quite simple;
its patch isn't affected, nor are the implementations of the noteOnSelf: type methods. The only real change
is in the implementation of applyParameters:.
First, however, you must provide an instance variable and
default value for MK_waveform―the parameter that
identifies MKSynthPatch
's
MKWaveTable
object:
@interface Envy:SynthPatch { id waveform; . . . } - setDefaults { waveform = nil; . . . } |
The applyParameters: method is rewritten to attend to the new parameter:
- applyParameters:aNote { . . . /* Create local variables for the new parameter. */ id myWaveform = [aNote parAsWaveTable:MK_waveform]; BOOL shouldApplyWave = NO; /* Test the parameters. */ if (myWaveform != nil) { waveform = myWaveform; shouldApplyWave = YES; } if (shouldApplyWave || isNewPhrase) [[self synthElementAt:osc] setTable: waveform length: 0 defaultToSineROM: isNewPhrase]; . . . } |
The setTable:length:defaultToSineROM: method sets
the oscillator's lookup table to the specified
MKWaveTable
object. Passing 0 as the length of
the table causes the MusicKit to compute a default value. The
argument to defaultToSineROM: is a
BOOL value that determines whether the sineROM should be used if
memory for the MKWaveTable
can't be allocated.
In this implementation, the argument is YES only if aNote is the beginning of a new phrase. For
all other phrase states, the previously set
MKWaveTable
is used if a new one can't be
allocated.
WaveTables are shared among
MKSynthPatch
es―if two
MKSynthPatch
es declare the same
MKWaveTable
object (with the same length), the
MKWaveTable
is allocated once and the
MKSynthPatch
es read from the same memory. This
feature is provided automatically by all the Oscg
-type
oscillators.