Introduction

In this article, I will describe a method of synchronizing multiple networked computers for interactive musical performance using Csound and Open Sound Control (OSC). I chose to adopt a client/server approach based on the "forward-synchronous" system developed by Roger Dannenberg[1][2]. The basic idea of this system is that the server maintains a master (global) clock, while the clients keep their local clocks synchronized with the global clock by querying the server at regular intervals. Since all the computers agree on the global time, as well as the current beat, tempo, and meter, they can schedule events to occur at precise moments in the future, even if there are network latencies. Dannenberg's system allows for changes in tempo, but not in meter, so I added that capability. I also created a graphical user interface for the system in CsoundQT and constructed a small set of custom OSC messages for communications between machines.

Overview

The system works as follows:

Implementation

The Csound orchestra that implements the system I will describe here is very large, so it would be impractical to include and discuss all of the code in this article. Moreover, a significant amount of that code is related to the graphical user interface, which happens to be built in CsoundQT, but which could be built in any number of user interface programs. In short, most of that code is not relevant to the subject of this article. Consequently, I will focus primarily on the concepts and techniques that are integral to the task of establishing communication and maintaining precise synchronization in a networked laptop ensemble, and discuss selected portions of the Csound code involved. The complete CSD is available for download from the following link Pinkston Example.zip, and I have included numerous comments that should make it relatively easy to understand.

Open Sound Control

Open Sound Control (OSC) is a protocol for communication between computers, synthesizers, and other multimedia devices. It was developed at the UC Berkeley Center for New Music and Audio Technologies (CNMAT) in the late 1990's and has since become an industry standard. (More information about OSC and how to use it can be found at http://www.opensoundcontrol.org[4].) Csound provides two opcodes for use with OSC messages—OSCsend and OSClisten. (Note: At the time of this writing, these opcodes were not included in the Reference section of the Csound manual. However, they can be found in the Opcodes Overview section under OSC and Network.) Before you can use OSClisten, you must first use OSCinit to specify and initialize the network receive port(s). The port numbers are more or less arbitrary, as long as you avoid lower numbered ports that may be in use by the operating system, or by other applications that use the network. For this example, I chose to use port 50000 for messages being received by the server and 50001 for messages being received by the client. Note that only one application may use a given port at a time, so if you plan to run multiple instances of an OSC-based Csound program on the same computer, each instance would need its own receive port(s). Since the ports will be used by multiple instruments and do not change during performance, I define them as global i-time variables. These statements are all placed in the orchestra header:

giInportS   = 50000       ;Server's receive port
giOutportS  = 50001       ;Port server will send to (i.e., the client)
giInport    = 50001       ;Client's receive port
giOutport   = 50000       ;Port client will send to (i.e., the server)

gioscC OSCinit giInport   ;initialize OSClisten for client
gioscS OSCinit giInportS  ;initialize OSClisten for server

OSC messages have the following basic format:

<address-string> <arg1> <arg2>...<argN>

The address string is similar to a directory path, e.g.: /music, /fader/3, /BankA/button/1, etc. There can be multiple arguments following the address string, but the type of each argument must be specified. The most commonly used types are int (i), float (f), and string (s). The numbers and types of arguments listed in a pair of OSCsend and OSClisten opcodes used to send and receive a particular OSC message must agree exactly, as well as the OSC address, or the message will be sent, but not received. Moreover, no error message will be generated by the OSClisten opcode, so it can be difficult to debug such problems.

A section of code (from instr 2 of the example orchestra) that sends an OSC message from the client machine asking to join the user group is shown below. The complete CSD file can be downloaded from the link given above.

;changing the update flag causes OSCsend to transmit 
kupdate =       kupdate+1
        OSCsend kupdate,gSserverIP,giOutport,"/join/user","ss",gSname,gSlocalIP

This statement sends two strings (gSname and gSlocalIP) to port giOutport on IP address gSserverIP. Note that OSCsend transmits messages when the value of its kwhen parameter (kupdate in this example) changes. Using a trigger signal for this purpose is not a good idea, because it will cause messages to be sent twice—once when the trigger value is 1 and then again when it returns to 0. Thus I generally use a trigger in conjunction with a k-variable, which I increment every time the trigger equals 1.

The server listens for such messages, as shown below:

;Listen for user join requests
ktrg4   OSClisten   gioscS, "/join/user", "ss", Suser, SuserIP
        if (ktrg4 == 1) then 
;process this request...

Here, OSClisten is using the port specified in the OSCinit statement in the orchestra header. The variable gioscS contains the "handle" returned by OSCinit. It is this handle that must be used as the first argument to OSClisten, rather than the name of the port, itself. The only OSC messages that this particular OSClisten will recognize are those beginning with the address /join/user and having two strings as arguments. Note that the Suser and SuserIP string variables in the arguments field of OSClisten are output variables, even though they appear to the right of the opcode. They must be initialized prior to being used, which I do at the top of the server instrument (instr 99 in the example orchestra) with the following statements:

Suser   strcpy "" 
SuserIP strcpy ""

When OSClisten receives a valid (matching) OSC message, it sets ktrg4 to 1 for a single k-period. This is both efficient and convenient, because it easy to use the trigger in conjunction with an if statement to only execute the processing code when a new /join/user message has been received.

Connecting the Ensemble

To connect the various client machines, the server must build and maintain lists of user names and IP addresses. This is easy to do in Csound6, because of its support for string arrays. In the orchestra header, I first define a macro, $MAXUSERS, used to determine the size of the string arrays, and then initialize them as follows:

gSUsers[]   init $MAXUSERS  ;Array of user names
gSIPs[]     init $MAXUSERS  ;Array of user IP addresses
gSUsers[0]  strcpy ""       ;initialize the first locations with a null string
gSIPs[0]    strcpy ""

I also define two UDOs, which make it easier to add and find users:

;-------------------------------------------------------------------------------- 
opcode        finduser,k,Sk ;finds a user's number in the user group
              Sname,kNUsers xin
kindex        =           0
loop:
kres          strcmpk     gSUsers[kindex],Sname 
              if          (kres == 0) goto end
              loop_lt     kindex,1,kNUsers,loop
kindex        =           -1    ;not found
end:
              xout        kindex
              endop 
;-------------------------------------------------------------------------------- 
opcode            adduser,k,SSi ;adds a new user to the user group
                  Sname,SIP,imaxusers xin
                  if (gkusers == imaxusers) then 
kindex            =         -1 ;error: too many users
                  kgoto     end
                  endif
                  if (gkusers != 0) then
kuser             finduser Sname,gkusers
                  if (kuser >= 0) then
kindex            = -2 ;error: user has already joined
                  kgoto end
                  endif ;end user test if
                  endif ;end finduser if 
;user has not already joined, so add
gSUsers[gkusers]  strcpyk Sname
gSIPs[gkusers]    strcpyk SIP
gkusers           = gkusers+1 ;updatethecurrentnumberofusers
kindex            = 0 ;return no error 
end:
xout kindex
endop 
;--------------------------------------------------------------------------------

The adduser UDO first checks to see if there is room for another user, then calls the finduser UDO to make sure the name is not already in the array. If it is not, adduser copies the name to the next available location in gSUsers array and the IP address to the corresponding location in the gSIPs array. It also increments gkusers, a global variable (initialized to 0 in the orchestra header) that stores the current total number of users in the group. The server then sends the OSC message /join/confirm to the client machine, to confirm the join request (or deny it, if the user has already joined, or there are too many users in the group).

Once a user has joined the group, s/he can request the server to send a list of user names and ip addresses, using the OSC message /getusers, and can also send and receive chat messages to/from other users in the group. The OSC messages defined for chatting are:

/chatsend <string username > <string chatline> 
/chatreceive <string username> <string chatline>

All chat messages are sent to the server, which checks to see that the author is a valid user, and if so, broadcasts the chat message to the entire group. Here is the relevant server code:

ktrg5     OSClisten   gioscS, "/chatsend", "ss", Suser, Schatline 
          if          (ktrg5 == 1) then
;check if we have any users in the group
          if          (gkusers == 0) goto continue
;check if this user is in the group 
knth      finduser    Suser,gkusers
          if          (knth >= 0) goto broadcast ;found user, broadcast the message
;error handling here...

Broadcasting a chat message involves looping through the entire list of IP addresses and sending the message to each one. Since OSCsend's IP address and port arguments must be set at i-time, as well as the OSC address and argument types, the easiest way to broadcast a message to multiple IP addresses is to use a dedicated instrument for each specific OSC message. Here is the instrument for broadcasting a chat message to a specific IP address:

      instr     101 ;sends a chat line to the specified IP 
SIP   strcpy    p4
      OSCsend   1, SIP, giOutportS, "/chatreceive", "ss", gSuser, gStext 
      turnoff
      endin

The instrument is passed the target IP address string in a p-field, while the name of the author and the message text are stored in global string variables. It simply sends the message to the specified IP address at i-time and then turns itself off. The instrument is called repeatedly from the server instrument using the scoreline opcode within a k-time loop, as follows:

broadcast:
; broadcast the message to all users
gStext    strcpyk   Schatline
gSuser    strcpyk   Suser
kndx      =         1
broadcast_loop: 
Sline     sprintfk  {{i 101 0 1 "%s"}}, gSIPs[kndx-1] 
          scoreline Sline, kndx
          loop_le   kndx, 1, gkusers, broadcast_loop 
          endif     ;end chatsend if

The reason for using scoreline, rather than event, or schedule, is that scoreline permits multiple strings to be passed via p-fields. In this instance, there is just one string being passed, but in other instances, more strings need to be passed. The Csound convention for passing strings as p-field arguments requires putting them in quotations, so it is necessary to use the {{...}} delimiters in the sprintfk statement, to enclose the format string.

The client instrument must be ready and able to receive and display an incoming chat message. This requires the use of an OSClisten and a means to display the message in a GUI. Incoming chat messages are displayed in a scrolling window, with the most recent message at the top. Messages are displayed in the conventional format, author: message text. Below is shown the relevant Csound code, contained in instrument 1 of the example orchestra:

;Listen for incoming chat messages
ktrg1   OSClisten gioscC, "/chatreceive", "ss", Sname, Smsg
        if        (ktrg1 == 1) then
Sname   strcatk   Sname, ": " ;add a colon and space after the author's name 
Sline1  strcatk   Sname, Smsg ;then append the message

;Update the chatwindow in CsoundQT 
        outvalue  "chatline3", Sline3 ;oldest message
        outvalue  "chatline2", Sline2 ;previous message
        outvalue  "chatline1", Sline1 ;current message
Sline3  strcpyk   Sline2 ;move the old lines down now, in
Sline1  strcpyk   Sline1 ;preparation for the next message 
        endif     ;end chat receive if

Synchronization

The key to synchronizing multiple laptops over a conventional TCP/IP-based network is to establish a global time, or master clock, that all the machines can reference. Since internet communication (especially involving wireless networks) is inherently unreliable, especially in terms of timing, it is not practical to rely upon sending any sort of clock signal over the network, as a way to synchronize music. Instead, each client machine needs to have its own internal clock running, and to adjust its local time base dynamically to keep it synchronized with that of the server.

The method I use for this is an adaptation of one devised by Roger Dannenberg, of Carnegie Mellon University. His articles on the method are listed below in the references [1] - [3]. As mentioned in the Overview, the client machines periodically query the server to find out what the current time is, and the server replies to each such query. The challenge lies in dealing with the network latencies, which are inevitable. The client sends a query at time T1, the server receives it and replies at time T2, and the client receives the reply at time T3.

The problem is that the intervals between these times are variable and unpredictable. In any case, no matter what the latency is, by the time the client receives the clock message from the server, it is no longer accurate, because the server's clock has continued to run in the interim. Dannenberg's solution to this problem is two-fold: first, discard any timing messages that take longer than some threshold to arrive; and second, assume that the amount of time that has elapsed since the server replied is 1/2 the round trip time. Hence, if the response to a query takes less than the threshold time to arrive, we estimate the actual server time Ts as follows:

Ts = Tr + (T3-T1)/2

Where Tr is the time reported by the server in response to a query, T1 is the time the query was sent and T3 is the time the reply was received. Hence, T3-T1 is the round trip time, and we add half of that amount to Tr to approximate what the actual server time (Ts) is right now. Knowing that, we can continuously adjust the client's local clock (Tc) to match the server's clock by adding an offset, which is equal to Ts-Tc. (This assumes that the server's clock has been running longer than the client's clock, which is usually the case.)

It is important to note that the threshold must be picked very carefully, through experimentation on the particular network. If it is set too high, then longer round trip times will cause glitches, because the estimated server time will be inaccurate. If it is set too low, on the other hand, then timing messages from the server will only get through occasionally, if ever. Similarly, the query rate must be fast enough to allow for (possibly) numerous slow round trip replies to be discarded, while still keeping accurate track of the server's clock. But it must not be so fast that, with multiple machines all issuing such queries, the server cannot keep up. According to Dannenberg, the best method to use is to periodically send a burst of queries and always use the one with the fastest round trip time. However, this seemed too difficult to implement with Csound, so I went with the threshold method.

The format of the time query OSC message is:

/timeq <string username> <float query_time> <string ipaddress>

...where query_time is the local clock time when the /timeq message was sent. The username is provided so that the server can check to see if the requester is a valid member of the group. However, checking this on every query involves some overhead and is usually not worth the trouble. Consequently, the server can just reply directly to the ipaddress, without bothering to check.

The reply message format is:

/timeq/response <float server_time> <float query_time>

The query_time is provided in the message, along with the current server_time, to facilitate round trip calculation. The client code to handle the /timeq/response message is as follows:

ktrg2   OSClisten gioscC, "/timeq/response", "ff", ktimeS, ktimeQ
        if        (ktrg2 == 1) then
kdiff   =         ktimeL - ktimeQ           ;round trip query time
        if        (kdiff < $THRESH) then ;if less than threshold
ktimeS  =         ktimeL+koffset,.05        ;assume Server time is now 1/2 diff later
koffset =         ktimeS - ktimeL           ;offset is the difference from local time 
        endif     ;end check threshold if
        endif     ;end OSClisten if
gktimeA portk     ktimeS + kdiff/2          ;smooth any changes   

The ktimeL variable is the local clock time, which is adjusted to match the server's time by adding the offset value koffset. This happens continuously (at the krate), whereas koffset is only changed when a /timeq/response message is received with a round-trip time less than the threshold ($THRESH) value.

The client sends /timeq messages to the server at a set rate ($TIMEQHZ), as follows:

ktrigQ  metro   $TIMEQHZ          ;check server TIMEQHZ times/second 
        if      (ktrigQ == 1) then
        OSCsend ktimeL, gSserverIP, giOutport, "/timeq", "sfs", gSname, ktimeL, gSlocalIP 
        endif   ;end metro if

Note that in addition to including ktimeL as the 2nd parameter for the OSC message, I also use it as the kwhen parameter for OSCsend here, since it is always incrementing.

Musical Time

Musical time is expressed in beats (quarter notes at a given tempo), and grouped into measures/bars according to the current meter. A client's local time is derived from the timeinsts opcode, which provides the elapsed time in seconds since an instrument was started, at the k-rate. The local time is converted from seconds to beats by continuously multiplying it by a tempo factor, which is the number of beats/second at the current tempo. The server controls the tempo and meter, and it keeps track of the beat count and the current bar number. It only notifies the clients if the tempo or meter has changed, but clients can issue a beat request to the server, if they ever need this information. The OSC message to request the current beat from the server is:

/beatq <username>

The server responds to the user who sent this message with the following:

/beatq/response <beat><time><tempo><meter><offset_beat><offset_time><offset_bar><bar_beat>

Where:

<beat>
is the integer number of the server's most recent beat
<time>
is the floating point time that the most recent beat occurred is the current floating point tempo in beats/minute
<meter>
is the current integer number of beats/bar
<offset_beat>
is the integer beat number at the last tempo or meter change
<offset_time>
is the floating point time of the last tempo or meter change
<offset_bar>
is the integer bar number at the last tempo or meter change
<bar_beat>
is the integer beat number of the offset bar

All this information is necessary, because each client is responsible for maintaining its own count of beats and measures, based on its local clock. If the meter never changed, the local beat and measure could simply be calculated from the latest beat, time, and tempo provided by the server, based on the amount of time elapsed since then. However, since both the tempo and the meter can change during the performance, the current beat and bar number must be calculated as follows:

current_beat = offset_beat + int((current_time - offset_time) * beats_per_second)
current_bar = offset_bar + int((current_beat - bar_beat) / meter)

To account for the time elapsed since the server reported the current beat, it is necessary to compare the current local time to the time of the last beat reported by the server and wait until the server's next beat boundary before making any adjustments. The wait is implemented with timout. Below is shown the client code (from instrument 3 in the example orchestra) for handling a /beatq/response message from the server:

ktrg      OSClisten   gioscC, "/beatq/response", "iffiifii", kbeatN, ktimeN, \ 
                            kStempo, kSmeter, kSoffset, kSbase, kSoldbar, kSoldbeat
          if          (ktrg == 1) then
kbeatDur  =           60./kStempo
kcurBeat  =           kbeatN + (gktimeA-ktimeN)/kbeatDur    ;current fractional beat 
kwait     =           (1-(kcurBeat-int(kcurBeat)))*kbeatDur ;time until next beat
krflag    =           1                   ;turn on the reset flag
          reinit      start               ;reinit the timout to wait until next beat
          endif                           ;end OSClisten ktrg if
          if (krflag == 0) goto skipover  ;skip timout except when reset flag is set
start:    timout      0,i(kwait),skipover ;wait until next beat boundary
          rireturn

                                          ;after timout completed, update everything 
          outvalue    "Tempo",kStempo     ;display/change to server's current tempo
gkMeter   =           kSmeter             ;change to server's current meter         
kMeter    init        i(gkMeter)-1        ;workaround for outvalue at i-time issue 
kMeter    =           gkMeter-1           ;meter number in menu
          outvalue    "Meter",kMeter      ;display current meter in menu
gkOffset  =           kSoffset            ;reset everything else to match server 
gkBase    =           kSbase              
gkOldbar  =           kSoldbar          
gkOldbeat =           kSoldbeat        
krflag    =           0                   ;clear the reset flag
skipover:                                 ;timout branch target

Performing Synchronized Music

Since all the clients are continuously adjusting their local clocks to keep them synchronized with the server, and since they know the current tempo, meter, beat, and bar, keeping the music synchronized is reasonably straightforward. However, it is not possible to use metro to trigger notes at the current tempo, because metro's internal clock is not synchronized with our adjusted local clock. Instead, triggers must be generated at the desired rhythmic subdivision (or tick duration) directly from the clock, as follows:

tick_number = int(local_time * beats_per_second / ticks_per_beat) 

- if tick_number has changed, generate a trigger

Below is a simple instrument that generates notes at a particular rhythmic subdivision on a slave instrument (instr 10) using schedkwhen:

        instr 5                     ;starts via GUI and schedules notes with pluck 
;Count ticks (subdivisions) since the last downbeat
kTick   =           int(((gktimeA - gkBarTime) * gkTempo/60.) / gkPluckDiv)
kTTrig  changed     kTick
        schedkwhen  kTTrig,0,0,10,gkdelay,.25,.5,kTick endin

Here, gkBarTime is the time of the last downbeat, which is set by the clock instrument (instr 3) every time the bar changes. The variable gkPluckDiv is set by the user via the GUI (monitored by instr 4), and it is equal to the tick duration as a fraction of a beat. (E.g., a sixteenth note would be .25). Hence, kTick will be the integer number of ticks that have occurred since the last downbeat, beginning with 0, and a trigger will be generated every time there is a new tick. (Note: The global variable gkdelay is used by every instrument that schedules notes. It is used to offset the actual start time of the scheduled note to compensate for latencies in the audio interfaces of other client computers. Before a performance, all the computers must manually adjust their delay time to match the playback of the computer with the greatest latency. A simple blip generator instrument is provided to facilitate this.) Below is shown the slave instrument:

            instr     10 ;Pluck Harmonics instrument
iAmp        =         p4
iTick       =         p5 
            kgoto     skip ;Check widget value at i-time only
kCutoff     invalue   "PluckLPFCutoff"
skip:
kamp        linen     iAmp*gkPluckVol,.01,p3,.15
icps0       =         cpsmidinn(24+i(gkPluckKey)) 
            if        (i(gkWrap) == 0) then
;Wrap
iMod        =         i(gkHarmMod)
icps        =         icps0+icps0*wrap(iTick,0,iMod)
else
;Fold
iMod        =         i(gkHarmMod)-1
icps        =         icps0+icps0*mirror(iTick,0,iMod)
            endif 
asig        pluck     kamp,icps,icps0*8,0,6
asig        butlp     asig,cpsoct(i(kCutoff)) 
            outs      asig,asig
            endin

Instrument 10 generates a lowpass-filtered note with pluck. The variables gkPluckVol, gkPluckKey, gkWrap, and gkHarmMod are set by CsoundQT widgets that are read by instrument 4. The pitch of each note is determined by the tick number, which is passed by schedkwhen in p5. As the tick numbers increase from 0, the pitches attempt to climb the harmonic series based on the frequency of gkPluckKey, but are either wrapped or folded between the fundamental (icps0) and icps0 * gkHarmMod. This can result in interesting patterns that either reinforce or contradict the prevailing meter, since the tick number is always reset to 0 at every downbeat, regardless of where we are in the sequence of harmonics. Because ticks are generated directly from the clock, any subdivision is possible, and different instruments can be working with different rhythmic values, which may or may not be related to the meter.

Scheduling Future Events

The system provides a convenient mechanism for scheduling events at some future time. Events are defined as snapshot (or preset) changes, which can include tempo and meter changes, as well as parameter changes for sound generating instruments. They can be scheduled locally, to occur on a single machine, or through the server, to be scheduled on all the machines. They can also be scheduled to occur at a specific bar in the future, or some number of bars relative to the current one. There is a dedicated instrument (instr 98), which handles the scheduling. When instantiated, it simply runs until the scheduled bar has been reached, then recalls the desired snapshot number and turns itself off. The instrument can be called multiple times, so that a sequence of snapshot changes can be scheduled in advance. The OSC message to schedule a snapshot change via the server is:

/schedule/request <string username> <int bar> <int snapshot> <int type>

The username refers to the user requesting the scheduled event, which is a change to the specified snapshot number, to occur at the specified bar. The type of schedule request can be either relative (to the current bar), or absolute (as specific bar number in the future). The server listens for such requests, checks to see if the user is a member of the group, and if so, broadcasts the schedule request to every member of the group, using this OSC message:

/schedule <int bar> <int snapshot> <int type>

When a client machine receives a /schedule message, it simply calls instrument 98, passing it the bar number, snapshot number, and type. An option is provided to allow, or ignore, incoming schedule requests.

Sharing Function Tables

F-tables of any length can be sent using OSC messages, although sending extremely large tables (such as audio files) is impractical. Sending pitch tables or sequence data, however, is entirely feasible. Since F-tables may be of various sizes, and since the OSCsend and OSClisten opcodes require a fixed number of parameters in their argument lists, the contents of a table are sent piecemeal—8 values per OSC message. The F-table that will receive the incoming data must already exist and be sufficiently large to hold all the data. Since it would be awkward if table data started coming in before the target machine was ready to receive it, the convention is that F-tables are only transmitted when a user specifically requests them. Here is the OSC message to request a table:

/gettab <int target_fnum> <int source_fnum> <string target_ip> <int target_port>

The target_ip is just the IP address of the machine requesting the table. However the target_port for table transmission will necessarily be different from the one used to receive most other OSC messages, so it must be specified here. In the example orchestra, port 50002 is used for table transmissions.

A machine receiving the /gettab message should respond to the IP address and port provided by the requestor with a series of /filltab messages, which have the following format:

/filltab <int target_fnum> <int start_loc> <float val1> <float val2>...<float val8>

This message will send 8 floating point values, which are to be loaded into the target function table, beginning at location start_loc. Below is shown the instrument that sends the contents of an F-table to a specific IP address via OSC:

          instr 106               ;dumps a table via OSC
SIP       invalue "TargetIP"
kMyFn     invalue "OutputTable" 
kTgtFn    invalue "TargetTable"
          tb0_init i(kMyFn)       ;table to dump                  
kn        init 0                  ;starting location in the table
imax      = 128                   ;gicount
loop:
  OSCsend kn,SIP,giAltport,"/filltab","iiffffffff",i(kTgtFn),kn,tb0(kn),tb0(kn+1),\ 
          tb0(kn+2),tb0(kn+3),tb0(kn+4),tb0(kn+5),tb0(kn+6),tb0(kn+7)

          loop_lt kn,8,imax,loop
          turnoff
          endin

The receiving computer loads the data into the target function table, as follows:

ktrigT    OSClisten   gioscC, "/filltab","iiffffffff",kfn,kn,\ 
                      kv0,kv1,kv2,kv3,kv4,kv5,kv6,kv7
          if (ktrigT == 1 && kfn == giMyFn) then
          tablew kv0,kn,giMyFn 
          tablew kv1,kn+1,giMyFn 
          tablew kv2,kn+2,giMyFn 
          tablew kv3,kn+3,giMyFn 
          tablew kv4,kn+4,giMyFn 
          tablew kv5,kn+5,giMyFn 
          tablew kv6,kn+6,giMyFn 
          tablew kv7,kn+7,giMyFn
          endif ;end filltab if

As a precaution, a check is made to ensure that the target table number in the incoming /filltab message corresponds to the table the receiving machine is expecting (giMyFn).

Conclusion

An earlier version of the system described here was successfully used by an ensemble of 4 laptops in a performance at The University of Texas. The example orchestra that accompanies this article is a stripped down version of the orchestras used in the performance, which had different instruments, some of which were considerably more complicated. Each laptop performer had a different set of instruments, in fact, but they all used the same client/server code. The ensemble used a dedicated wireless router in performance, but we sometimes used the campus wireless network during rehearsals without encountering any significant timing problems. The server should always be run on the fastest machine, and ideally, should not be doing anything else.

In our concert, however, the computer running the server was also doing a full share of the music generation, without apparent difficulty. Interacting with the CsoundQT GUI, however, often did cause timing problems and glitches. Moving a fader with the mouse, for example, invariably caused timing glitches. Using it in conjunction with an external MIDI controller (a Korg nanoKontrol2), however, seemed to solve that problem.

Future enhancements will include the capability for the server to push table data to the client machines on a scheduled basis, for audio captured on one machine to be shared with other machines, OSC communications with video/graphics software (such as Isadora), and greater use of gestural controllers.

References

[1] Roger Dannenberg, and Eli Brandt, "Time in Distributed Real-Time Systems." [Online] Available: http://www.cs.cmu.edu/~rbd/papers/synchronous99/synchronous99.pdf [Accessed August 15, 2015].

[2] Dawen Liang, Guangyu Xia, and Roger B. Dannenberg, "A Framework for Coordination and Synchronization of Media," in Proceedings of the International Conference on New Interfaces for Musical Expression, Paper Session E, 30 May , 2011, Oslo, Norway. [Online] Available: http://citeseerx.ist.psu.edu/viewdoc/download?doi=10.1.1.450.4513&rep=rep1&type=pdf [Accessed August 15, 2015].

[3] Roger Dannenberg, et al, "The Carnegie Mellon Laptop Orchestra," Carnegie Mellon University, Research Showcase@CMU, August, 2007. [Online] Available: http://repository.cmu.edu/cgi/viewcontent.cgi?article=1511&context=compsci [Accessed August 15, 2015].

[4] Matt Wright, "The Open Sound Control 1.0 Specification," Version 1.0, opensoundcontrol.org. March 26, 2002. [Online] Available: http://opensoundcontrol.org/spec-1_0 [Accessed August 15, 2015].