Interactive Swarm Space

ISO to ISO Communication

Table of Contents:

Setting Up a Sender/Receiver for ISO Flock
Setting Up a Receiver for ISO Synth


Setting Up a Sender/Receiver for ISO Flock

OSC Control

For simplicity's sake, we have combined the OSC receiver and a sender for error messages into one object. To set up OSC communication for your ISO Flock you need one single line of code:

simulation.com().createOSCControl( 7400, "127.0.0.1", 7800 );
// receiving port, error msg receiver's IP adress, error msg sending port

In this example code snippet, we are sending to the IP adress 127.0.0.1 i.e. the localhost. Don't use the same port for sending OSC messages and passing back error messsages - also make sure that the ports are unused by other network applications. If necessary refer to this list of UDP and TCP ports commonly used. And last but not least a little no-brainer: Make sure the port numbers match (i.e. your external software sends to a port on which the ISO Flock has a OSC receiver registered)!

Setting up a receiver

Simply insert the following line of code below Simulation::init(1.0);

simulation.com().createReceiver( RECEIVER_NAME_STRING,
                                 LISTENER_PORT_INT,
                                 PROTOCOL_TYPE,
                                 FORMAT_TYPE);

RECEIVER_NAME_STRING = an arbitrary name
LISTENER_PORT_INT = port where to listen for messages
PROTOCOL_TYPE = com::UDP or com::TCP
RECEIVER_NAME_STRING = com::DEFAULT (standard format optimized for ISO-to-ISO communication) or alternatively com::OSC

Example:

simulation.com().createReceiver("FlockReceiver", 7500, com::UDP, com::DEFAULT);

This creates a receiver called "FlockReceiver", that is listening for UDP messages on port 7500 and has the ISO-standard format.

Setting up a sender for ISOFlock

simulation.com().createSender( SENDER_NAME_STRING,
                               LISTENER_PORT_INT,
                               PROTOCOL_TYPE,
                               FORMAT_TYPE );

After you have set up your swarms and behaviors, insert the following line of code in order to enable for sending a specific parameter:

simulation.com().registerParameter( SENDER_NAME_STRING,
                                    SWARM_NAME_STRING,
                                    PARAM_NAME_STRING );

Example:

simulation.com().registerParameter( "FlockSender", "mySwarm", "position" );

That's the most basic form of registering a parameter. You have some options though. You can define parameter value limits for example, by specifying two limiting vectors the same way we have implemented motion boundaries in our Hello World 2: Motion Boundaries example:

simulation.com().registerParameter( "FlockSender", "myswarm", "position", 
    math::Vector<float>(3, -5.0, -5.0, -5.0), math::Vector<float>(3, 5.0, 5.0, 5.0) );
MinMaxbounds

In the example above both the minimum and maximum values are a "math::Vector<real>&" vector or a subclass of it. Since position (usually) has three dimensions, we are using math::Vector3<float> (in ISO Flock "real" is just a typedef on float). These bounds are to be interpreted as follows: The position of an agent in is truncated, meaning if, say, the current xyz-position is (1.3, 2.8, 5.5) it will be truncated down to (1.3, 2.8, 5.0). However, and this is very important, this bounds are also used to normalize agent positions in the range between 0 and 1! So all message values are only within the range of 0 and 1! Look out for that.

If you would like to track one-dimensional parameters however, you would use the math::Vector<float>.



Setting Up a Receiver for ISO Synth

On the synth side it's a little more verbose. Let's start by looking at a rather simple example: We would like our 50-agent flock to control the amplitudes and frequencies of 50 oscilators inside an additive synthesis patch. We assume you have followed the instructions in "Setting up a sender for ISOFlock".

So you have your synthesis patch (in our example AdditiveSynthesisPatch.h and .cpp) and as usual, you use your main() method in the iso_synth_app project to create an instance of your AdditiveSynthesisPatch class. Consequently it is your main() method, where you need to take care of setting up and linking a message receiver to your patch:

int main( int argc, char **argv )
{
   Synth& synth = Synth::get();

   try
   {	
      QApplication a(argc,argv);

      synth.com().createReceiver("FlockReceiver", 7500, com::UDP, com::Default );
		
      AdditiveSynthesisPatch* patch = new AdditiveSynthesisPatch(10, 440.0);
      OutputUnit* outputUnit = new JackOutputUnit( 2, "system" );
      patch->unit("AdditiveSynthesisPatch_out")->connect( outputUnit,
                                                          new ChannelMap(2, 1, 1) );
      Synth::get().com().registerMessageListener( "FlockReceiver", *patch );

      Synth::get().start();
      sleep(700000);
      Synth::get().stop();
      Synth::destroy();

      return a.exec();

   }	
      catch(base::Exception& e)
   {
      Synth::get().exceptionReport(e);
   }
	
    return 0;
}

Let's look closely at the code fragements in bold:

//Get synth singleton and create a receiver called "Flock Receiver" on port 7500, 
//using the UDP protocol and the default ISO message format:
synth.com().createReceiver("FlockReceiver", 7500, com::UDP, com::Default );

//create an instance of your patch:
AdditiveSynthesisPatch* patch = new AdditiveSynthesisPatch(10, 440.0);

//create an audio output object with two channels (stereo):
OutputUnit* outputUnit = new JackOutputUnit( 2, "system" );

//connect your patch's mono output to the two inputs of your output object:
patch->unit("AdditiveSynthesisPatch_out")->connect(outputUnit, new ChannelMap(2, 1, 1) );

//assign (or register) the message listener to your patch 
//(side-note: patch is a pointer, so "*pointer" de-references it)
Synth::get().com().registerMessageListener( "FlockReceiver", *patch );

Now what's left is to define how the data in a message is used to control the synth units in your patch. This is defined in your patch's notify() method. Until now, the actual dissection of the message and applying the collected data to your units is somewhat tedious since you have to know the message structure in order to pick out the right kind of data from the stream. Because of that, here's a quick overview over the standard message structure:

message protocol
ISO message protocol

Value Group Description
ID sequencial message number
Type (not used currently, but could serve es message diversificator)
Swarm Name name of sending swarm
Param Name name of swarm's parameter
Num Agents number of sending agents
Parameter Values block of actual parameter values, can be chars, ints, longs, floats or doubles.

This is the format in which the message is sent to your patch's notify() method. You can see that there are 6 value groups, but only the last one contains the actual message data, the ones before however will help you in decomposing it. And here's how it's done:

//inside your patch's cpp file: 

void 

AdditiveSynthesisPatch::notify(const com::Message& pMessage)
{
   if(mActive == false) return; //if patch is inactive, ignoring message

   try
   {
      // a message has always 6 value groups:
      if(pMessage.valueGroupCount() < 6) return; 
      // skip if already received:

      if( mPreviousMessageId == ( pMessage.values<long>(0) )[0] ) return; 

      // copy reference to local object:

      com::Message message = pMessage; 

      // get num of agents by reading value group 4 ("Num Agents"):
      int parCount = ( message.values<int>(4) )[0]; 
      // get "Parameter Values" as float array:
      const float* parValues = message.values<float>(5); 		
      // get parameter dimension (see comment bellow):
      int parDim = message.valueCount(5) / parCount; 

      // some local helpers for this method:

      sample harmonicFrequency = 0.0;

      sample frequency;

      sample amplitude;
		
      // some iteration indices

      unsigned int oscilatorIndex = 0;

      unsigned int paramValueIndex = 0;
		
		
      // iterate over all values and set frequency/amplitude of oscilators.
      // in our patch: mNumberOfPartitials = 50 oscilators
      while(oscilatorIndex < mNumberOfPartials)  
      {
         // calculate frequency of partitial (for the very first one, this 
         // is the root frequency itself, since harmonicFrequency was initialized as 0.0
         harmonicFrequency += mRootFrequency;

         // read parameter and map to amplitude
         amplitude = parValues[paramValueIndex]; // agent x pos

         // read next parameter value and map to frequency
         // ... is used to modulate the harmonic frequency 
         frequency = parValues[paramValueIndex + 1] * harmonicFrequency; // agent y pos
		      
         // notice: agent z pos is not used (...[paramtValueIndex+2])
		      
         // set oscilator's "frequency"
         portmFrequencyPorts[oscilatorIndex]->set(frequency);
         
         //set oscilator's "amplitude" port
         mAmplitudePorts[oscilatorIndex]->set(amplitude);

         oscilatorIndex++;  //increment index
         paramValueIndex += parDim; //increment param index by 3 (for "x, y, z")
      }

   }

   catch(com::ComException& e)
   {
      Synth::get().exceptionReport( e );
   }

   catch(base::Exception& e)
   {
      Synth::get().exceptionReport( e );
   }	
}

There are quite a lot of points to mention here and we hope to be able to streamline the notify() method in the future:

  • All parameter values (value group 5) are aligned in sequential order:
    agent0x, agent0y, agent0z, agent1x, agent1y, agent1z, agent2x, ...
  • Accessing a value inside a value group:
    int parCount = ( message.values<int>(4) )[0];
    
    Value groups are arrays of a specific type. So eventhough value group 4 "Num Agents" obviously contais just a single value, it's still an array with a single field (access by [0]). The type (message.values<T>) has to correspond with the ISO message protocol from above.
  • parDim: You can deduct the number of dimension the sent parameter (named by value group 3) has by counting the total of all values (value group 5) and dividing it by the number of agents.
  • The method could do without the helper variables (e.g. amplitude, frequency, ...) but it would make your code more difficult to read and debug.

Last updated: May 15, 2024