Defining custom MixerChannels

This tutorial illustrates how to create a simple four-channel x/y MixerChannel.

All in all, the new mixer definition plus a fully functional gui definition were written in less than half an hour (cutting and pasting from the standard definition templates), demonstrating amply that the new structure allows a lot of flexibility with very little fuss.

The mixer definition

A MixerChannel definition consists of these arguments:

Think of them in terms of four questions:

  1. How many input and output channels should it have (and should its name be based on that, or be arbitrary)?
  2. How does the fader translate the input to the output? (This is defined in terms of a synthdefs.)
  3. What input controls does the MixerChannel need?
  4. What GUI definition should the channel use?

In this example, the GUI definition is omitted for simplicity. We'll get to that in a little bit.

MixerChannelDef(\mix1x4, 1, 4, 
   SynthDef(\mix1x4, { |busin, busout, xpos, ypos, level|
      var   sig = In.ar(busin, 1);
      sig = Pan4.ar(sig, xpos, ypos, level);
      Out.ar(busout, sig);
      ReplaceOut.ar(busin, sig);
   }),
   controls: (xpos: \bipolar,
      ypos: \bipolar,
      level: (value: 0.75, spec: \amp))
);

The first three arguments -- \mix1x4, 1, 4 -- answer the first question: the name, one input channel, four output channels.

The synthdefs must read the signal from the input bus, perform its processing (which here is accomplished with a single UGen, Pan4), and write the new signal to the output bus using Out.ar and also replace the input bus signal using ReplaceOut.ar. The latter is essential for post-fader sends, mixer recording and mixer scoping to work. Note that the processed signal is assigned to a variable so that both the outputs can and share the same UGen. If you write the Pan4 twice, it will be calculated twice (which is inefficient).

Note: The argument names "busin" and "busout" are required. The MixerChannel class will set these arguments depending on the buses that are allocated. Do not change the names!

IMPORTANT: The SynthDef names should be unique to the mixer definition. Be careful not to introduce conflicts here, or some mixers may not work as you expect.

Finally, the controls are written into a dictionary, with the name of the control as the key and, corresponding to the key, the default value and/or ControlSpec to use. For example, (name: spec, name1: spec1) where spec may be any of the following:

"xpos: \bipolar" means that the control will range from -1 to 1 with an initial value of 0 (defined in the ControlSpec). "level: (value: 0.75, spec: \amp)" means to use the \amp ControlSpec, but use 0.75 as the starting value.

That is enough for a working MixerChannel. Let's test it:

m = MixerChannel(\test, s, 1, 4);

// or, alternately, if you want to create the mixerchannel based on
// the definition's name, you could do this.
// Do not do both in this example!
m = MixerChannel.newFromDef(\test, \mix1x4, s);

a = m.play({ SinOsc.ar(Lag.kr(LFNoise0.kr(8).range(200, 800), 0.07), 0) });

m.automate(\xpos, { LFNoise1.kr(0.2) });
m.automate(\ypos, { LFNoise1.kr(0.3) });

The custom controls are available to all the applicable MixerChannel methods: getControl, setControl, automate, stopAuto, etc.

When done:

m.free;

If you want this mixer definition to be available every time you launch sc or compile the class library, add the definition to your startup file. See the Using the Startup FIle helpfile for more details.

GUI customization

Mixer GUIs depend on a set of widget classes to manage the graphics operations. Many standard ones are defined for you:

Consult the class definitions in the MixerSkin.sc file to see how they should be written. A couple of important notes:

For the four channel mixer, let's define a GUI that uses a two-dimensional slider to define the pan behavior. That is, instead of the left/right slider used for panning in the two-channel mixers, we will use a square to move the audio around.

Because only one GUI object (SC2DSlider) is involved, we can subclass MixerWidgetBase and make the definition short and simple:

Mixer2DPanWidget : MixerWidgetBase {
   makeView { |layout, bounds|
      view = SC2DSlider(layout, bounds);
      spec = \bipolar.asSpec;
   }
   doAction {
      mixer.setControl(\xpos, spec.map(view.x), updateGUI:false);
      mixer.setControl(\ypos, spec.map(view.y), updateGUI:false);
   }
   updateView {
      view.x = spec.unmap(mixer.getControl(\xpos));
      view.y = spec.unmap(mixer.getControl(\ypos));
   }
   clearView {
      view.x_(0.5).y_(0.5);
   }
   updateKeys { ^#[\xpos, \ypos] }
}

These are the essential methods your class should define IF you are defining a subclass:

makeView(layout, bounds): An initialization method. The SCView object should be assigned to the view variable. spec may also be used to hold a ControlSpec.

doAction(view): This is called whenever the user manipulates the GUI object. The method should update the appropriate controls in the MixerChannel (held in the mixer variable).

updateGUI:false is important. By default, a change to a mixer control updates the GUI. Here, the change originated from the GUI, so there's no need to call back to the GUI and reupdate.

In this case, failing to specify updateGUI:false resulted in a bug: the callback (which occurs after changing the mixer's x-position) to re-update the GUI calls updateView, which sets the slider's vertical position to the mixer's value (which hasn't been updated yet). As a result, vertical position changes were ignored.

updateView(value): This is called whenever a programmatic action changes the value represented in the GUI. The new value will be passed in as the first argument. Here, we just get the value from the mixer controls because the GUI actually reflects two controls.

updateKeys: This is very important, because it defines which mixer control names will trigger an update in this widget. If there's only one control, simply return it as a symbol:

   updateKeys { ^\level }

More than one (as here), use a literal array:

   updateKeys { ^#[\xpos, \ypos] }

If you don't define a valid key here, the widget will never respond to programmatic updates.

These methods are optional, but help keep the interface consistent:

clearView: This is called when the GUI goes inactive. You will note that some of the widgets change the background to clear, to indicate visually that the mixer GUI is no longer connected to anything. This example doesn't implement that, but it would be easy to add.

restoreView: Not implemented here, it's the reverse of clearView and should undo whatever changes are done in the clear method. Note that it is not necessary to update the view's value, because this method is always called by .refresh, which will also call .updateView.

Depending on the functionality, you may need to write additional methods. That is fine, as long as the above methods are present for the MixerChannelGUI to hook into.

Using the widget in a GUI definition

The new widget needs to be added to the class library, either in MixerSkin.sc or in a separate file. Do this before proceeding, and recompile the class library.

A separate file is better because your custom classes will persist through library updates.

To write a mixer GUI definition, you need to supply only three things:

The two arrays should be the same size, and each element should match up. MixerMuteWidget corresponds to Rect(0, 0, 20, 20), etc.

The boundaries are relative to the top left corner of the mixer GUI. The MixingBoard class automatically calculates the origin (top-left) of each channel's GUI.

d = MixerGUIDef(Point(50, 330), 
   [MixerMuteWidget, MixerRecordWidget, MixerPresendWidget, Mixer2DPanWidget,
      MixerLevelSlider, MixerLevelNumber, MixerPostsendWidget, MixerNameWidget,
      MixerOutbusWidget],
   [Rect(0, 0, 20, 20),
   Rect(30, 0, 20, 20), 
   Rect(0, 25, 50, 30), 
   Rect(0, 65, 50, 50),
   Rect(10, 125, 30, 100), 
   Rect(0, 230, 50, 15), 
   Rect(0, 250, 50, 30), 
   Rect(0, 285, 50, 20),
   Rect(0, 310, 50, 20)
]);

MixerChannelDef has a guidef variable, so that mixers with different templates can have different default GUI styles. You can also define a global default.

MixerChannelDef(\mix1x4).guidef = d;   // specific to this mixer template

MixerChannelGUI.defaultDef = d;   // global default

Note: MixerPresendWidget and MixerPostsendWidget have another special property, unique to these classes. If you list either class more than once, you can display multiple sends in the same GUI. Behind the scenes, the MixerChannelGUI class maintains indices for presends and postsends, so that each widget will be associated with (and control) a different element of the MixerChannel's preSends or postSends array.

The default GUI definitions show only one pre- and post-send each, to conserve screen space. However, it's an easy customization. The default GUI definitions are found in the *initClass method of MixerGUIDef and may be changed there.

Bringing it all together: example

// mixer definitions
(
MixerChannelDef(\mix1x4, 1, 4, 
   SynthDef(\mix1x4, { |busin, busout, xpos, ypos, level|
      var   sig = In.ar(busin, 1);
      Out.ar(busout, Pan4.ar(sig, xpos, ypos, level))
   }),
   SynthDef(\mxb1x4, { |busin, busout, xpos, ypos, level|
      var   sig = In.ar(busin, 1);
      sig = Pan4.ar(sig, xpos, ypos, level);
      Out.ar(busout, sig);
      ReplaceOut.ar(busin, sig);
   }),
   (xpos: { |name| MixerControl(name, nil, 0, \bipolar) },
   ypos: { |name| MixerControl(name, nil, 0, \bipolar) },
   level: { |name| MixerControl(name, nil, 0.75, \amp) }));

d = MixerGUIDef(Point(50, 330), 
   [MixerMuteWidget, MixerRecordWidget, MixerPresendWidget, Mixer2DPanWidget,
      MixerLevelSlider, MixerLevelNumber, MixerPostsendWidget, MixerNameWidget,
      MixerOutbusWidget],
   [Rect(0, 0, 20, 20),
   Rect(30, 0, 20, 20), 
   Rect(0, 25, 50, 30), 
   Rect(0, 65, 50, 50),
   Rect(10, 125, 30, 100), 
   Rect(0, 230, 50, 15), 
   Rect(0, 250, 50, 30), 
   Rect(0, 285, 50, 20),
   Rect(0, 310, 50, 20)
]);

MixerChannelDef(\mix1x4).guidef = d;
)

// now create the mixer and the gui
m = MixerChannel(\test, s, 1, 4);
MixingBoard(\test, nil, m);

// play some sound
a = m.play({ SinOsc.ar(Lag.kr(LFNoise0.kr(8).range(200, 800), 0.07), 0) });

// use the mouse to move the panner around in the box

// automate and watch
m.automate(\xpos, { LFNoise1.kr(0.2) });
m.automate(\ypos, { LFNoise1.kr(0.3) });

m.watch(\xpos); m.watch(\ypos);

Another feature, not discussed so far, is that you can replace a GUI definition on the fly:

// alternate guidef for a horizontal layout
e = MixerGUIDef(Point(460, 50),
   [MixerMuteWidget, MixerRecordWidget, MixerNameWidget, MixerPresendWidget,
      Mixer2DPanWidget, MixerLevelSlider, MixerLevelNumber, MixerPostsendWidget,
      MixerOutbusWidget],
   [Rect(0, 15, 15, 15),
   Rect(20, 15, 15, 15),
   Rect(40, 15, 50, 15),
   Rect(95, 5, 50, 20),
   Rect(150, 0, 50, 50),
   Rect(205, 15, 80, 15),
   Rect(290, 15, 40, 15),
   Rect(335, 5, 50, 15),
   Rect(390, 15, 50, 15)
]);

// use the horizontal layout
m.mcgui.guidef = e;

// go back to the vertical layout
m.mcgui.guidef = d; 

You can do that as many times as satisfies you. (I think it's pretty cool myself.)

// when done playing
m.free;

Using Proto for testing GUI widgets

I like to use Proto objects for testing when possible. The syntax is little less convenient, but it has the strong advantage that the objects can be recompiled without recompiling the whole library.

In this context, it means that when you create a MixerChannel and MixingBoard, if it doesn't look or behave right, you don't need to destroy the objects. Simply recompile only the objects you need, rebuild the MixerGUIDef, apply it to the MixerChannel as shown above, and continue testing.

For comparison's sake, let me put the class definition of Mixer2DPanWidget next to the equivalent Proto.

Mixer2DPanWidget : MixerWidgetBase {
   makeView { |layout, bounds|
      view = GUI.slider2D.new(layout, bounds);
      spec = \bipolar.asSpec;
   }
   doAction {
      mixer.setControl(\xpos, spec.map(view.x), updateGUI:false);
      mixer.setControl(\ypos, spec.map(view.y), updateGUI:false);
   }
   updateView {
      view.x = spec.unmap(mixer.getControl(\xpos));
      view.y = spec.unmap(mixer.getControl(\ypos));
   }
   clearView {
      view.x_(0.5).y_(0.5);
   }
   updateKeys { ^#[\xpos, \ypos] }
}

// the same, as an Proto
Library.put(\mixergui, \pan4,
   Library.at(\mixergui, \base).clone({
      ~makeView = { |layout, bounds|
         ~view = GUI.slider2D.new(layout, bounds);
         ~spec = \bipolar.asSpec;
      };
      ~doAction = { |view|
         ~mixer.setControl(\xpos, ~spec.map(view.x), updateGUI:false);
         ~mixer.setControl(\ypos, ~spec.map(view.y), updateGUI:false);
      };
      ~updateView = {
         ~view.x = ~spec.unmap(~mixer.getControl(\xpos));
         ~view.y = ~spec.unmap(~mixer.getControl(\ypos));
      };
      ~clearView = {
         ~view.x_(0).y_(0);
      };
      ~updateKeys = [\xpos, \ypos];
   }));

The main differences are:

To use an Proto widget in a MixerGUIDef, simply retrieve the object from the library where it is needed in the guidef's viewProtos array.

d = MixerGUIDef(Point(50, 330), 
   [MixerMuteWidget, MixerRecordWidget, MixerPresendWidget, Library.at(\mixergui, \pan4),
      MixerLevelSlider, MixerLevelNumber, MixerPostsendWidget, MixerNameWidget,
      MixerOutbusWidget],
   [Rect(0, 0, 20, 20),
   Rect(30, 0, 20, 20), 
   Rect(0, 25, 50, 30), 
   Rect(0, 65, 50, 50),
   Rect(10, 125, 30, 100), 
   Rect(0, 230, 50, 15), 
   Rect(0, 250, 50, 30), 
   Rect(0, 285, 50, 20),
   Rect(0, 310, 50, 20)
]);

Note that is interchangeable with widgets defined in hard classes.

After it's working to your satisfaction, translate it into a true class definition:

  1. Declare any instance variables that need be defined.
  2. Remove the environment variable indicator "~".
  3. Remove all references to "self" in the methods' argument lists, and if "self" is used in the method definitions, replace it with "this."
  4. If environment variables are defined outside of the method function, the initialization statements will need to be moved into a method.
  5. Clean up the rest of the syntax.

Then it should be ready to include in the class library.

The attached code file, at the top of this page, contains Proto versions of all the standard mixer widgets, for your study and recycling.