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.
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 MKSynthPatches. These range from a sophisticated FM MKSynthPatch with vibrato and a wide variety of parameters settable at any time, to a simple MKSynthPatch designed for applications that assume traditional computer music scorefile semantics (such as are assumed by MUSIC-5, CMUSIC, CSOUND, etc.). Much of the complexity of MKSynthPatch design is eliminated if the traditional restrictions are adopted. |
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 MKUnitGenerators 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] |
MKUnitGenerators 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 MKUnitGenerator, it's convenient to refer to the master class rather than a specific leaf class. Furthermore, the “UG” (which stands for “MKUnitGenerator”) is often dropped from the master class name. |
The Oscg family of MKUnitGenerators 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: |
MKUnitGenerators 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 MKNotes 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 MKEnvelopes.
While these four methods aren't subclass responsibilities, the default implementations provided by the MKSynthPatch class do nothing. Thus, if you don't provide an implementation of, for example, noteUpdateSelf:, your MKSynthPatch won't respond to noteUpdates. |
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 MKUnitGenerators 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. MKSynthData implements run to do nothing, so there's no harm in sending this message to a patchpoint. |
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 MKUnitGenerators 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 MKEnvelopes 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 MKEnvelopes, its implementation of noteOffSelf: always returns 0.0 (an example of a MKSynthPatch that uses MKEnvelopes 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 MKUnitGenerators' 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.
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 MKPatchTemplate. */ return theTemplate; } |
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 MKUnitGenerators are executed in a predictable order. Consider how Envy's MKUnitGenerators 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 MKUnitGenerators 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 MKUnitGenerators are executed is the order in which their specifications are added to the MKPatchTemplate. Thus, Envy's MKUnitGenerators 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 MKUnitGenerators are executed doesn't matter. You can add MKUnitGenerators and declare their execution to be unordered by using the addUnitGenerator:ordered: method, passing NO as the second argument (the addUnitGenerator: is actually a shorthand; it invokes addUnitGenerator:ordered: with YES as the second argument). While you can't share patchpoints in a patch that uses unordered MKUnitGenerators, allocating the patch is somewhat more efficient. |
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 MKUnitGenerators 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 MKUnitGenerators 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 MKUnitGenerators 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 MKEnvelopes (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 MKEnvelopes (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 MKEnvelopes:
- 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.
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 MKSynthPatches―if two MKSynthPatches declare the same MKWaveTable object (with the same length), the MKWaveTable is allocated once and the MKSynthPatches read from the same memory. This feature is provided automatically by all the Oscg-type oscillators.