Metering levels

Besides plotting the actual signal or dataset, there are situations where we merely want to monitor changes in some magnitude. The most typical scenario is metering the amplitude of some signal, but we could meter anything really, as long as it is represented by some numerical value.

Monitoring signals

Generic metering in SuperCollider is primarily addressed by the LevelIndicatorclass. To monitor some magnitude specific to a signal, we first need to track it, write the resulting values to some control-rate instance of Bus or to some instance of Buffer, and later use an instance of Routine to manually update the value of LevelIndicator as appropriate. For now, we will limit ourselves to using the Amplitude UGen to only track the amplitude; in Chapter 6, Data Acquisition and Mapping, we will discuss how to track other kinds of magnitudes and how to extract information out of audio signals. Note also that a convenient meter method does exist, yet it is only limited to instances of Server and to monitoring the global I/O streams of all its default channels (for example, Server.default.meter).

(  // Simple Level Metering
Server.default.waitForBoot({

  // create the parent window
  var window = Window.new("Level Metering", Rect(200,400,60,220)).front
  .onClose_({   // stop routine when the window is closed
    updateIndicator.stop;
    sound.free;
  });

  var bus = Bus.control();      // create a Bus to store amplitude data

  // an audio signal
  var sound = { 
    var sound = WhiteNoise.ar(Demand.kr(Dust.kr(20),0,Dbrown(0,1,0.3)));
    var amp = Amplitude.kr(sound);  // track the signal's amplitude
    Out.kr(bus, amp);  // write amplitude data to control bus
    Out.ar(0,sound);   // write sound to output bus
  }.play;

  // create and customize Indicator
  var indicator = LevelIndicator(window,Rect(10,10,40,200))
  .warning_(0.5)           // set warning level
  .critical_(0.7)          // set critical level
  .background_(Color.cyan) // set Color
  .numTicks_(12)           // set number of measurement lines
  .numMajorTicks_(3)       // set number of major measurement lines
  .drawsPeak_(true);       // draw Peak Values

  // update the Indicator's value with a routine
  var updateIndicator = fork{loop{
    bus.get({   // get current value from the bus
      arg value;
      {indicator.value_(value);     // set Indicator's value
        indicator.peakLevel_(value); // set Indicator's peak value
      }.defer(); // schedule in the AppClock
    });
    0.01.wait; // indicator will be updated every 0.01 seconds
  }};
});
)

Again, note that we use a defer block to schedule anything that is GUI-related to the AppClock subclass.

Monitoring numerical data

Apart from a signal's magnitude, LevelIndicator can be used to monitor any kind of data we may be interested in. In the following code, we loop through an eight-channel multidimensional dataset:

(  // Monitoring a complex numerical Dataset
var indicators, updateIndicators; 
var index = 0;  // a global index used to iterate through the dataset
var dataset = Array.fill(8,{Array.fill(1000,{rrand(0,1.0)})}); 
// a multi-dimensional dataset

// create window
var window = Window.new("Monitoring a complex numerical dataset", 360@210).front.onClose_({ updateIndicators.stop });
window.addFlowLayout; // add flowLayout

// create and customize 8 Level indicators
indicators = Array.fill(8, {LevelIndicator(window,40@200)});
indicators.do { arg item;
  item.warning_(0.8).critical_(0.9).background_(Color.cyan).drawsPeak_(true);
};

// update the indicators with a routine
updateIndicators = fork{loop{
  indicators.do{ arg item, i; {
    var value = dataset[i][index];  // read value from the dataset
    item.value_(value);             // set each Indicator's value
    item.peakLevel_(value);         // set each Indicator's peak value
  }.defer();  // schedule in the AppClock
  };
  // increment index or set to 0 if it has exceeded dataset's size
  if ( index < 1000) {index = index + 1;} {index = 0; };
  0.1.wait; // indicators will be updated every 0.1 seconds
}};
)

This time an array of LevelIndicator objects is used instead of a singleton element, and of course, there is no need for some specialized tracking UGen. We merely use an instance of Routine to access the dataset by means of a global index, which is accordingly incremented once some datum is read. This is so that it always reflects the position of the next object. We will also need an if construct to zero out the index once it has exceeded our dataset's size in order to reiterate from the beginning.