KISA 6 - Playing multiple files - Part 1

Your browser does not support HTML 5. You may want to update your browser.

Sometimes it is desirable to play multiple audio files at the same time. There are at least a couple of different reasons for doing so.

HTML 5 Audio objects do not have any mixer capabilities and do not allow one object to play multiple files at the same time. Instead, one can set up multiple audio objects each playing a different file simultaneously. Naively, this is a simple solution but in practice there are some complications especially if some preciseness in the resulting audio is required.

To keep things simple, we just added the new capabilities to the previous sketch. Hence instead of balls and paddles that would be used in Pong, we just added buttons. "Source 1", "Source 2" and "Source 3" can be used to simulate a Pong style game except the player must click the buttons to make the sound. The "Chord" button is designed to play three files simultaneously.

We will use the existing coding for the background music. Taking advantage of the arrays setup in the Part 5, a longer file was added so one can see the response when it is loaded. In real applications, many of the controls set up in the previous tutorials could be eliminated.

There are 4 new buttons (3 sound buttons and the chord button) which play sounds independently of the background music and each other. The new "Display" on/off button will be discussed later.

While "pong game" requires only three new files, some times a file might be repeated before a previous invocation has completed. We are not really sure how many sounds might be played simultaneously. So following the lead in Part 5, we will use an array of audio objects. Files are loaded into an currently unused array element when it is to be played. We will be able to assign a particular source file to a different element in that array as many times as needed. When playing a file is completed, the audio element will be made available for use by any of the files, as needed. The array is relatively simple to set up.

The array is larger than the number of files so a given file can be played multiple times simultaneously. Unfortunately there may be a problem. Because files are played in a asynchronous manner, one cannot guarantee that files will be started at exactly when requested. This is particularly a problem when exact timing is essential or when trying to play chords. This may be evident when the "Chord" button is clicked. Part of the time only 2 of the 3 notes in the chord will play. This may be caused by the fact that the code may look for an available Audio element before the previously selected element actually starts playing and it appears to be still available. A solution for this will be attempted in the next tutorial.

If we are going to play short files repeatedly, it doesn't really make sense to check for the file type that will be used every time we play a file. So we will determine the desired file type once and for all in the setup method. A new method, determineFileExt, discovers it for us.

266  String determineFileExt() {
267    // returns the file extension that will be used
268    String desiredExt;
269    Audio aud = new Audio();
270    if (aud.canPlayType && aud.canPlayType("audio/ogg")) {
271      desiredExt = ".ogg";
272    } else {
273      desiredExt = ".mp4";
274    }
275    return desiredExt;
276  }  // determineFileExt

A new variable, fileExt, is declared to hold the result of the method in line 28 and the method is called in line 60:

 28  String fileExt;  // extension being used by the browser: .ogg or .mp3
...
 60    fileExt = determineFileExt();

Lets continue by looking at the declaration of the array of Audio objects:

 32  int numAudio = 10;
 33  int lastAudio;
 34  Audio[] audioArray = new Audio[numAudio];

The number of Audio objects in the array given by value of numAudio is arbitrary but should be at least as large as the maximum number of files that could be played simultaneously. Towards the end of this tutorial we demonstrate technique that will help you determine a satisfactory number. We will not create the individual Audio objects until they are needed.

In earlier versions of KISA, the Audio object audio was used in one song at a time fashion. We want to use it for the background music. To avoid having to start over, it will be equated to audioArray[0]. This simplifies the development of this tutorial because some of the routines can apply to both the background music and the new sounds. This is set up in line 90. In practice one might just want a separate Audio object for this purpose.

107    audioArray[0] = audio;
108    for (i = 1; i < numAudio; i++)
109      audioArray[i] = 0;  // 0 => the element hasn't been used

Otherwise only very few additional changes are needed to use "audio" for the background music and still have all the functionality in the earlier KISA tutorials. In practice, many of the things done with the audio object might be unnecessary. One method had to be modified so that it would apply both to the original audio object and to the rest of the new audio objects. Another argument was added to the setSource method so that it could apply to any of the audioArray objects. Because the original audio object has been equated to audioArray[0], we can replace the original calls to setSource in setup, ended, and mouseClicked with ones that specify object "0".

113    setSource(0, files[currentFile]);  // pick the initial audio file
...
176  void setSource(int num, String url) {
...
222      setRadioButton(fileBtn, currentFile);
...
332          setSource(0, files[currentFile]);

In addition to declaring the Audio array, we need to add some new files and buttons. The new files are declared in lines 19 to 26 and the new buttons in 39-40. To locate these buttons on the canvas, a couple of new variables are also declared:

 19  String[] soundFiles = {"http://brinkje.com/KISA_Sounds/ding",
 20                    "http://brinkje.com/KISA_Sounds/FavSound",
 21                    "http://brinkje.com/KISA_Sounds/truck_horn"};
 22  int numSoundFiles = soundFiles.length;
 23  String[] chordFiles = {"http://brinkje.com/KISA_Sounds/jenHum01",
 24                    "http://brinkje.com/KISA_Sounds/jenHum02",
 25                    "http://brinkje.com/KISA_Sounds/jenHum03"};
 26  int numChordFiles = chordFiles.length;
...
 40  CircleButton[] soundFilesBtn = new CircleButton[numSoundFiles];
 41  CircleButton chordBtn;
 47  int xSoundFiles = 388;  // Location of first sound file
 48  int ySoundFiles = 195;

The buttons are created in setup and drawn in draw. Before drawing the buttons, a box that encloses them is drawn. The code is enclosed in a try/catch structure just in case the there is an attempt to draw them before they are created.

 93    for (i = 0; i < numSoundFiles; i++) {
 94      soundFilesBtn[i] = new CircleButton(xSoundFiles, ySoundFiles,
 95           "Sound " + i);
 96      ySoundFiles += 25;
 97    }
 98  
 99    ySoundFiles += 25;  // leave a blank line
100    chordBtn = new CircleButton(xSoundFiles, ySoundFiles, "Chord");
101    ySoundFiles += 25;
...
153    try {
154      fill(#DDDD99);
155      rect(xSoundFiles - 18, ySoundFiles - 143, 119, 133);
156      for (i = 0; i < numSoundFiles; i++) {
157        if (soundFilesBtn[i] != null)
158          soundFilesBtn[i].draw();
159      }
160      chordBtn.draw();
...
164    } catch (e) {
165     // ignore
166    }

A new function load picks the first available audio element and loads the specified file into it. An audioArray element is available if it has never been used ("=0") or if it has finished (ended) playing the previous source file. The method returns the subscript of the object selected.

229  int load(String fileName) {
230    // Determines the first available audioArray object,
231    // loads the file into it and returns the subscript of that
232    // Audio object.  The audio element must be created if it has
233    // not been used before (=0).  If it has been used and the play
234    // has been completed, then audioArray[i].ended will be true.
235    int i;
236    // find the first audio player that is not in use
237    for (i = 1; i < numAudio; i++)
238    {
239      if (audioArray[i] == 0 // audioArray[i] has not been used
240            || audioArray[i].ended)  // audioArray[i] is finished playing
241      {
242        // if needed, initialize the audioArray element
243        if (audioArray[i] == 0) {
244          audioArray[i] = new Audio();
245          audioArray[i].addEventListener("playing", playing, false);
246          audioArray[i].volume = audio.volume;
247        }
248        // set the source unless audio player already is using the file
249        if (audioArray[i].src != fileName + fileExt) {
250          setSource(i, fileName);
251        }
252        // return the player number
253        return i;
254      }
255    }
256    return -1;  // All the audioArray elements are in use
257  }  // load

Notice that the for loop begins with i = 1 because audioArray[0] is reserved for the background music. There are some alternatives to just picking the first available audioArray object. A Javascript writer suggests avoiding the search for the first available audio object and just assigning the audio objects in a round robin manner. This should work as long as the sound files are about the same length. Another technique would be to first check to see if one of the available audioArray objects already was using the desired source file. Picking such an object could avoid a delay in loading the source into the object.

It important to observe that the load method does not actually load the file. Instead it initiates the loading operation and returns immediately. This loading is skipped if the selected Audio object used the same file in its last use.

After loading the file, it can be played by the following simple method:

259  void play(int num) {
260    if (num >= 0 && num < numAudio) {
261      audioArray[num].play();
262      lastAudio = num;                        // optional
263    }
264  }  // play

We are almost done with the required code. All that is needed is to recognize the new buttons in the mouseClicked method. The first three buttons play the file immediately when it is loaded. However, for the chordBtn we load all the files before playing them in hopes that they start playing in at the "same time". Because of the loop handling in the sound buttons, the chord button is handled first.

312    } else if (chordBtn.isOver()) {
313      for (i = 0; i < numChordFiles; i++)
314        chordPlayer[i] = load(chordFiles[i]);
315      /*** don't do this - it stall the program! ***
316      while(audioArray[chordPlayer[0]].readyState != 4
317            || audioArray[[chordPlayer[1]].readyState != 4
318            || audioArray[[chordPlayer[2]].readyState != 4) {
319      // waste time
320      }
321      */
322      for (i = 0; i < numChordFiles; i++)
323        play(chordPlayer[i]);
...
327    } else {
...
336      for (i = 0; i < numSoundFiles; i++) {
337        if (soundFilesBtn[i].isOver()) {
338          play(load(soundFiles[i]))
339        }
340      }
341    }

As discussed earlier, the code for the chord does not always work correctly. For example, sometimes the first file does not actually play. Why? It appears that the problem occurs because of the asynchronous nature of starting a load. It appears that the search for the first available audioArray object for the second file begins before the actual loading of the first note begins and hence the object selected for the first file reports that it is still "ended" and still available. Thus the code picks the same element for playing the second file.

The "obvious" solution was to waste some time using the loop in lines 293 - 297. But that resulted in completely stalling the program. Why? It appears the computer was so busy going around the loop, it never started the loading the file! I decided to just comment out the bad code to avoid repeating the mistake some other time.

There are just a few additional notes about KISA 6. First one should be able to adjust the volume of the sound effects as well as the background music. The coding for the volume buttons has been changed and a new method was added to adjust the volume on all the audioArray objects.

294    } else if (volumeUpBtn.isOver()) {
295      v = audio.volume + 0.1;
296      adjustVolume(v);
297    } else if (volumeDownBtn.isOver()) {
298      v = audio.volume - 0.1;
299      adjustVolume(v);
...
351  void adjustVolume(double v) {
352    int i;
353    v = constrain(v, 0, 1);
354    for (i = 0; i < numAudio; i++) {
355       if (audioArray[i] != 0)
356         audioArray[i].volume = v;
357    }
358  }  // adjustVolume

To illustrate how files are loaded, the preload attribute was used to delay loading the background music until it is played. The default for "preload" is "auto" which means automatically start loading the file as soon as possible and is used in all the other audioArray objects. In between these two options, there is a third option "metadata" which downloads a minimal amount of information about the file. In practice, these options may not make as much difference as one would expect (after the first use).

 69    audio.preload = "none";    // wait until file is played to load it

One question an implementer might ask is "How can I tell how many audioArray objects does my program need? To help answer this question some "optional" code was added to help you tell how many objects are being used. A new variable was added to keep track of the last audio object used. This code would probably be removed in the production version.

 49  lastAudio = -1;   // subscript of last audio element used     // optional
...
161      if (lastAudio > 0)                         // optional
162        text("Last audio player: " + lastAudio,  // optional
163            xSoundFiles - 10, ySoundFiles - 47); // optional
...
262      lastAudio = num;                        // optional

The Display

Lets finally discuss the "Display on/off" toggle button. Turning this on displays information about the status of each of the audioArray elements that have been declared and used. The following information is provided for each of the audio elements.

The last three of these items are presented graphically. The location is shown with a moving line and the ranges are shown with bar graphs.

To provide some additional information about the status of the element, the color of the bars in the graph change. The color of the buffered bar reflects the network state. The color of the seekable bar reflects the current ready state. The meaning of the different colors is shown in the table.

Comments

The techniques for handling simultaneous audio files used in this tutorial are probably adequate in many situations where there may be some background music, and in addition, random sounds are played such as in some games. Unfortunately they are not adequate when more precision is needed or when multiple files are started simultaneously. The next tutorial will discuss a more sophisticated way of selecting the audioArray objects. Testing indicates it resolves several of the issues with the technique in this tutorial. In particular, it solves the problem of not all the players playing when the chord button is clicked. Part 8 will look at a technique to synchronize multiple Audio players.


< Previous (KISA 5)   Next (KISA 7) >

[+/-] The KISA6.html file

[+/-] The KISA6.pde file

  1  // KISA
  2  // Keep It Simple Audio version 6
  3  // James Brink  brinkje@plu.edu
  4  // 3/28/12  4/12/12   6/18/12  11/5/14
  5  
  6  // The constants can be modified.
  7  int width = 500;  // Width of canvas
  8  int height = 325; // Height of canvas
  9  
 10  // It is assumed that both .ogg and .mp3 versions of the files are available
 11  // in the same folder.  The file names must be complete URLs except for the
 12  // ending .ogg or .mp3 which must be omitted. It will be added as needed for
 13  // the browser.  Use the prefix "file:///" or "http:// as appropriate.
 14  String[] files = {"http://brinkje.com/KISA_Sounds/groove",
 15                    "http://brinkje.com/KISA_Sounds/jingle",
 16                    "http://brinkje.com/KISA_Sounds/marcus_kellis_theme"};
 17  int numFiles = files.length;
 18  int currentFile = 0;   // current file for audio = audioArray[0]
 19  String[] soundFiles = {"http://brinkje.com/KISA_Sounds/ding",
 20                    "http://brinkje.com/KISA_Sounds/FavSound",
 21                    "http://brinkje.com/KISA_Sounds/truck_horn"};
 22  int numSoundFiles = soundFiles.length;
 23  String[] chordFiles = {"http://brinkje.com/KISA_Sounds/jenHum01",
 24                    "http://brinkje.com/KISA_Sounds/jenHum02",
 25                    "http://brinkje.com/KISA_Sounds/jenHum03"};
 26  int numChordFiles = chordFiles.length;
 27  
 28  String fileExt;  // extension being used by the browser: .ogg or .mp3
 29  
 30  // Global variables
 31  Audio audio = new Audio();
 32  int numAudio = 10;
 33  int lastAudio;
 34  Audio[] audioArray = new Audio[numAudio];
 35  
 36  int time;
 37  String audioStatus;
 38  CircleButton playBtn, pauseBtn, stopBtn;
 39  CircleButton volumeDownBtn, volumeUpBtn, timeDownBtn, timeUpBtn;
 40  CircleButton[] soundFilesBtn = new CircleButton[numSoundFiles];
 41  CircleButton chordBtn;
 42  ToggleButton[] fileBtn = new ToggleButton[numFiles];
 43  ToggleButton loopBtn, autoplayBtn;
 44  
 45  boolean repeatLoop = false;  // Should the player loop?
 46  
 47  int xSoundFiles = 388;  // Location of first sound file
 48  int ySoundFiles = 195;
 49  lastAudio = -1;   // subscript of last audio element used     // optional
 50  
 51  // display items
 52  Display dis = new Display(audioArray, width, height, false);
 53  ToggleButton displayOnBtn;
 54  
 55  void setup() {
 56    int i, y;
 57    // Setup the sketch
 58    size(width, height);
 59    smooth();
 60    fileExt = determineFileExt();
 61  
 62    if (audio == null) {
 63      noLoop();
 64      return;
 65    }
 66    frameRate(20);
 67  
 68    // audio info and listeners
 69    audio.preload = "none";    // wait until file is played to load it
 70    audio.addEventListener("loadstart", loadStart, false);
 71    audio.addEventListener("playing", playing, false);
 72    audio.addEventListener("pause", pause, false);
 73    audio.addEventListener("ended", ended, false);
 74    audio.addEventListener("error", error, false);
 75  
 76    // Setup buttons
 77    playBtn = new CircleButton(130, 145, "Play");
 78    pauseBtn = new CircleButton(230, 145, "Pause");
 79    stopBtn = new CircleButton(330, 145, "Stop");
 80    volumeDownBtn = new CircleButton(165, 170, "Volume down");
 81    volumeUpBtn = new CircleButton(270, 170, "Volume up");
 82    timeDownBtn = new CircleButton(165, 195, "Time down");
 83    timeUpBtn = new CircleButton(270, 195, "Time up");
 84    loopBtn = new ToggleButton(165, 220, "Loop", false);
 85    autoplayBtn = new ToggleButton(270, 220, "Autoplay", false);
 86    y = 245;
 87    for (i = 0; i < numFiles; i++) {
 88      fileBtn[i] = new ToggleButton(20, y, files[i], false);
 89      y += 25;
 90    }
 91    fileBtn[currentFile].setState(true);
 92    
 93    for (i = 0; i < numSoundFiles; i++) {
 94      soundFilesBtn[i] = new CircleButton(xSoundFiles, ySoundFiles,
 95           "Sound " + i);
 96      ySoundFiles += 25;
 97    }
 98  
 99    ySoundFiles += 25;  // leave a blank line
100    chordBtn = new CircleButton(xSoundFiles, ySoundFiles, "Chord");
101    ySoundFiles += 25;
102    
103    // display
104    displayOnBtn = new ToggleButton(20, 20, "Display on/off", false);
105  
106    // setup the audio array  
107    audioArray[0] = audio;
108    for (i = 1; i < numAudio; i++)
109      audioArray[i] = 0;  // 0 => the element hasn't been used
110      
111    // other
112    audioStatus = "";
113    setSource(0, files[currentFile]);  // pick the initial audio file
114    audioStatus = "Waiting";           // because preload is "metadata"
115  } // setup
116  
117  void draw() {
118    // Draws the sketch on the canvas
119    
120    background(#FFFFAA);
121    fill(#000000);
122    if (audio == null) {
123      text("Your browser does not handle the HTML 5 audio tag.  You ", 20, 30);
124      text("may want to upgrade your browser to the current version.", 20, 60);
125      return;
126    }
127  
128    textAlign(CENTER);
129    text("KISA 6 with Display", width/2, 30);
130    text("Source file: " + audio.src, width/2, 60);
131    text("Status: " + audioStatus, width/2, 80);
132    text("Current time: " + round(audio.currentTime)
133         + " sec.   Length: " + round(audio.duration)+ " sec.", width/2, 100);
134    text("Volume (0 to 1): " + round(10 * audio.volume)/10.0, width/2, 120);
135  
136    try {
137      playBtn.draw();
138      pauseBtn.draw();
139      stopBtn.draw();
140      volumeDownBtn.draw();
141      volumeUpBtn.draw();
142      timeDownBtn.draw();
143      timeUpBtn.draw();
144      loopBtn.draw();
145      autoplayBtn.draw();
146      for (i = 0; i < numFiles; i++) {
147        fileBtn[i].draw();
148      }
149    } catch (e) {
150     // ignore
151    }
152    // draw sound and chord buttons
153    try {
154      fill(#DDDD99);
155      rect(xSoundFiles - 18, ySoundFiles - 143, 119, 133);
156      for (i = 0; i < numSoundFiles; i++) {
157        if (soundFilesBtn[i] != null)
158          soundFilesBtn[i].draw();
159      }
160      chordBtn.draw();
161      if (lastAudio > 0)                         // optional
162        text("Last audio player: " + lastAudio,  // optional
163            xSoundFiles - 10, ySoundFiles - 47); // optional
164    } catch (e) {
165     // ignore
166    }
167    try {
168      // draw the display items
169      dis.draw();
170      displayOnBtn.draw();
171    } catch (e) {
172    // ignore
173    }  
174  }  // draw
175  
176  void setSource(int num, String url) {
177    // Called to set the source file.  Do not include the file extension
178    // in the url.  It will be added here.  "num" specifies subscript of
179    // the audio object that will be used.
180    if (num >= 0 && num < numAudio) {
181      audioArray[num].setAttribute("src", url + fileExt);
182      if (num == 0)
183        audioStatus = "File selected";
184    }
185  } // setSource
186  
187  void stop() {
188    // Called stop playing the file by pausing it and setting the
189    // time back to 0;
190    audio.pause();
191    audio.currentTime = 0;
192    audioStatus = "Stopped";
193  }  // stop
194  
195  void loadStart() {
196    // LoadStart event processing
197    audioStatus = "Loading";
198  }  // loadStart
199  
200  void playing() {
201    // Playing event processing
202    audioStatus = "Playing";
203  }  // playing
204  
205  void pause() {
206    // Pause event processing.
207    // There is no stop event but a "stop" causes a pause.  This
208    // method checks to see if the current time = 0. If so it assumes
209    // stopped
210    if (audio.currentTime == 0)
211      audioStatus = "Stopped";
212    else
213      audioStatus = "Paused";
214  }  // pause
215  
216  void ended() {
217    int j;
218    // Ending event processing
219    if (repeatLoop) {
220      currentFile = (currentFile+1) % numFiles;
221      setSource(0, files[currentFile]);
222      setRadioButton(fileBtn, currentFile);
223      audio.play();
224    } else {
225      audioStatus = "Finished playing";
226    }
227  }  // ended
228  
229  int load(String fileName) {
230    // Determines the first available audioArray object,
231    // loads the file into it and returns the subscript of that
232    // Audio object.  The audio element must be created if it has
233    // not been used before (=0).  If it has been used and the play
234    // has been completed, then audioArray[i].ended will be true.
235    int i;
236    // find the first audio player that is not in use
237    for (i = 1; i < numAudio; i++)
238    {
239      if (audioArray[i] == 0 // audioArray[i] has not been used
240            || audioArray[i].ended)  // audioArray[i] is finished playing
241      {
242        // if needed, initialize the audioArray element
243        if (audioArray[i] == 0) {
244          audioArray[i] = new Audio();
245          audioArray[i].addEventListener("playing", playing, false);
246          audioArray[i].volume = audio.volume;
247        }
248        // set the source unless audio player already is using the file
249        if (audioArray[i].src != fileName + fileExt) {
250          setSource(i, fileName);
251        }
252        // return the player number
253        return i;
254      }
255    }
256    return -1;  // All the audioArray elements are in use
257  }  // load
258  
259  void play(int num) {
260    if (num >= 0 && num < numAudio) {
261      audioArray[num].play();
262      lastAudio = num;                        // optional
263    }
264  }  // play
265  
266  String determineFileExt() {
267    // returns the file extension that will be used
268    String desiredExt;
269    Audio aud = new Audio();
270    if (aud.canPlayType && aud.canPlayType("audio/ogg")) {
271      desiredExt = ".ogg";
272    } else {
273      desiredExt = ".mp4";
274    }
275    return desiredExt;
276  }  // determineFileExt
277  
278  void error() {
279    // error event processing
280    audioStatus = "Error";
281  }  // error
282  
283  void mouseClicked() {
284    // Mouse clicked event processing
285    double v, t;
286    int i;
287    int[] chordPlayer = new int[numChordFiles];
288    if (playBtn.isOver()) {
289      audio.play();
290    } else if (pauseBtn.isOver()) {
291      audio.pause();
292    } else if (stopBtn.isOver()) {
293      stop();
294    } else if (volumeUpBtn.isOver()) {
295      v = audio.volume + 0.1;
296      adjustVolume(v);
297    } else if (volumeDownBtn.isOver()) {
298      v = audio.volume - 0.1;
299      adjustVolume(v);
300    } else if (timeUpBtn.isOver()) {
301      t = audio.currentTime + 0.1 * audio.duration;
302      audio.currentTime = constrain(t, 0, audio.duration);
303    } else if (timeDownBtn.isOver()) {
304      t = audio.currentTime - 0.1 * audio.duration;
305      audio.currentTime = constrain(t, 0, audio.duration);
306    } else if (loopBtn.isOver()) {
307      loopBtn.toggleState();
308      repeatLoop = loopBtn.getState();
309    } else if (autoplayBtn.isOver()) {
310      autoplayBtn.toggleState();
311      audio.autoplay = autoplayBtn.getState();
312    } else if (chordBtn.isOver()) {
313      for (i = 0; i < numChordFiles; i++)
314        chordPlayer[i] = load(chordFiles[i]);
315      /*** don't do this - it stall the program! ***
316      while(audioArray[chordPlayer[0]].readyState != 4
317            || audioArray[[chordPlayer[1]].readyState != 4
318            || audioArray[[chordPlayer[2]].readyState != 4) {
319      // waste time
320      }
321      */
322      for (i = 0; i < numChordFiles; i++)
323        play(chordPlayer[i]);
324    } else if (displayOnBtn.isOver()) {
325      displayOnBtn.toggleState();
326      dis.turnDisplayOn(displayOnBtn.getState());
327    } else {
328      for (i = 0; i < numFiles; i++) {
329        if (fileBtn[i].isOver()) {
330          setRadioButton(fileBtn, i);
331          currentFile = i;
332          setSource(0, files[currentFile]);
333          return;
334        }
335      }
336      for (i = 0; i < numSoundFiles; i++) {
337        if (soundFilesBtn[i].isOver()) {
338          play(load(soundFiles[i]))
339        }
340      }
341    }
342  }  // mouseClicked
343  
344  void setRadioButton(ToggleButton[] btnArray, int i)  {
345    int j;
346    for (j = 0; j < numFiles; j++)
347      btnArray[j].setState(false);
348    btnArray[i].setState(true);
349  } // setRadioButton
350  
351  void adjustVolume(double v) {
352    int i;
353    v = constrain(v, 0, 1);
354    for (i = 0; i < numAudio; i++) {
355       if (audioArray[i] != 0)
356         audioArray[i].volume = v;
357    }
358  }  // adjustVolume
359  
360  
361  

[+/-] The CircleButton.pde file

[+/-] The Display.pde file

Updated 11/6/14