KISA 7 - Playing multiple files - Part 2

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

In the previous tutorial, we set up a basic way of handling multiple files. In this tutorial we will add some sophistication. New concepts include:

The changes made in this tutorial appear to solve some of the problems the simplistic method used in Tutorial 6 was plagued with.

The new class, AudioStatus, will help keep track of the available status of each of the Audio player elements. Its use will help simplify picking Audio players. The class is relatively simple.

402  class AudioStatus {
403    boolean available;
404    
405    public AudioStatus (boolean avail) {
406      available = avail;
407    }  // constructor AudioStatus
408    
409    
410    public void setAvailable (boolean avail) {
411      available = avail;
412    } // setAvailable
413  
414    public boolean getAvailable() {return available; }
415    
416  }  // class AudioStatus

In some cases, it may be better to use a class that also included the corresponding audio component. This would be particularly true if additional data items could be included in the class.

An array of these elements are declared. They are in one to one correspondence with the array of Audio components.

 34  AudioStatus[] audioStatusArray = new AudioStatus[numAudio];

Using the audioStatusArray allows us to substantiate all the elements of the audioArray in the setup method. Notice that because audioArray[0] is reserved for the background music audio element as in the previous tutorial, the for loop begins with 1.

107    audioArray[0] = audio;
108    for (i = 1; i < audio108Array.length; i++) {
109      audioArray[i] = new Audio();
110      audioArray[i].addEventListener("playing", playing, false);
111      audioArray[i].addEventListener("ended", endedSound, false);
112      audioArray[i].volume = audio.volume;
113      audioStatusArray[i] = new AudioStatus(true);  // the player is available
114    }  

A couple of notes about this code. audio is the player that was earlier set up to be the primary player and with which the primary controls work. This tutorial is primary concerned with audioArray[1] .. audioArray[9]. Notice that initially, all these 9 players are assigned the status "true"(i.e. they are available for use).

In the above code, an eventListener, endedSound, is specified for the "ended" event. This new listener is different than the one we have used for the primary Audio component, audio. Each time one of the players 1 ... 9 finishes, endedSound is called. Unfortunately, it is unable to determine which of the Audio players caused the event so it checks all 9 of the players and sets the audioStatusArray element to say that the player is available if it has ended.

260  void endedSound() {
261    // Check the audio array for ended.
262    // audio arrray elements are available if they have "ended"
263    int i;
264    for (i = 1; i < audioArray.length; i++) {
265      if (audioArray[i].ended)
266        audioStatusArray[i].available = true;
267    }
268  } // endedSound

The advantage is that when load(String fileName) is called to play the specified file, it can easily find an available player. Actually, it does a little more than find a available player. In lines 271-286, it first checks to if there is an available player that already has the file loaded. If not, the remaining lines pick the first available Audio player. The method returns the selected player's subscript.

270  int load(String fileName) {  
271    // Determines the audio player that will be used.  It attempt
272    // to find an available player that already has the file loaded.
273    // If this is impossible, it just uses the first available 
274    // player.
275    int i;
276    // find the first audio player that is not in use with the file loaded
277    for (i = 1; i < numAudio; i++) {
278      if (audioStatusArray[i].available) {  // audioArray[i] is finished playing
279        // is the source already loaded into the player?  If so, use it.
280        if (audioArray[i].src == fileName + fileExt) {
281          audioStatusArray[i].available = false;  // mark player as being used
282          // return the player number so it will be used
283          return i;
284        }
285      }
286    }
287    
288    // If no player used desired source file, use the first available player
289    for (i = 1; i < numAudio; i++) {
290      if (audioStatusArray[i].available) {
291        // set the source
292        setSource(i, fileName);
293        audioStatusArray[i].available = false;
294        // return the player number
295        return i;
296      }
297    }
298    return -1;  // All the audioArray elements are in use
299  }  // load

One might ask why the audioStatusArray is needed. Couldn't we just check audioArray[i].ended? The answer is "no". Before the first time a player is used, audioArray[i].ended is false because a file has not been played and hence it has not "ended". But it is available for use. That is why in endedSound, we could not just set
audioStatusArray[i].available = audioArray[i].ended.
Instead, we initially set the elements of audioStatusArray to true during setup (see line 113).

Lets return to the setup method. In addition to creating all nine of the Audio players as we saw earlier in lines 107 - 114, we want to dedicate (well, actually temporarily dedicate) players (1 to 3) to the three chord files. This is done in lines 115 - 120. The source is set for these players, an canplaythrough listener is declared, and the file is loaded.

 
115    for (i = 1; i <= chordFiles.length; i++) {
116      audioArray[i].setAttribute("src", chordFiles[i-1] + fileExt);
117      audioArray[i].addEventListener("canplaythrough", chordCanPlayThrough,
118                                     false);
119      audioArray[i].load();
120    }

What does the canplaythrough listener do? This event happens when Audio component believes that if the current rates were to continue, the file could be played through without having to stop for further buffering. As with the soundEnded method, this method cannot tell which of the files caused the event. So whenever one of the chord files "canplaythrough", this method counts the number of chord files that are ready. If all of them are ready, it sets the global chordsReady to true. The coding is designed to make sure that once chordsReady is true, it stay true.

218  void chordCanPlayThrough() {
219    int i;
220    boolean loaded;
221    int readyCount = 0;
222    for (i = 1; i <= chordFiles.length; i++) {
223      loaded = audioArray[i].readyState == 4;
224      if (loaded)
225        readyCount++;
226    }
227    if (readyCount >= chordFiles.length)
228      chordsReady = true;
229  }  // chordCanPlayThrough  

What is the purpose of chordsReady? It is used in the draw method. To avoid allowing the user to click the chords button before the required files are ready, an "if (chord ready) is used:

172      if (chordsReady)
173        chordBtn.draw();

There is one remaining "neat" feature that the audioStatusArray allows us. In tutorial 6, the number of the last audio component selected was displayed by draw. Unfortunately, there was no indication of how many components were in use. We can do better now. This version of KISA draws circles for each of the 9 audio controls used for sounds. They glow green if the player is available, red otherwise. This section of code is also in the draw method.

174      // show available audio controls
175      for (i = 1; i < audioArray.length; i++) {
176        if (audioStatusArray[i].available)
177          c = #00FF00;  // available
178        else 
179          c = #FF0000;  // used
180        fill(c);
181        ellipse(xSoundFiles - 12 + i* 9, ySoundFiles - 47, 6, 6);
182      }

Testing shows that the techniques in this tutorial solve many of the problems found in the previous tutorial without introducing a particularly large amount of new coding. They are recommended when a some precisions is needed when playing multiple audio files.

The next tutorial discusses techniques for synchronizing Audio players when one wants one Audio player to control other controllers.


< Previous (KISA 6)  Next (KISA 8) >

[+/-] The KISA7.html file

[+/-] The KISA7.pde file

  1  // KISA
  2  // Keep It Simple Audio version 7
  3  // James Brink  brinkje@plu.edu
  4  // 4/5/12  4/9/12  5/21/12  6/1/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  Audio[] audioArray = new Audio[numAudio];
 34  AudioStatus[] audioStatusArray = new AudioStatus[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  boolean chordsReady = false;
 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 = "metadata";    // just load info like time
 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 < audio108Array.length; i++) {
109      audioArray[i] = new Audio();
110      audioArray[i].addEventListener("playing", playing, false);
111      audioArray[i].addEventListener("ended", endedSound, false);
112      audioArray[i].volume = audio.volume;
113      audioStatusArray[i] = new AudioStatus(true);  // the player is available
114    }  
115    for (i = 1; i <= chordFiles.length; i++) {
116      audioArray[i].setAttribute("src", chordFiles[i-1] + fileExt);
117      audioArray[i].addEventListener("canplaythrough", chordCanPlayThrough,
118                                     false);
119      audioArray[i].load();
120    }
121  
122    // other  
123    audioStatus = "";
124    setSource(0, files[currentFile]);  // pick the initial audio file
125    audioStatus = "Waiting";           // because preload is "metadata"
126    
127  } // setup
128  
129  void draw() {
130    color c;
131    // Draws the sketch on the canvas
132    background(#FFFFAA);
133    fill(#000000);
134    if (audio == null) {
135      text("Your browser does not handle the HTML 5 audio tag.  You ", 20, 30);
136      text("may want to upgrade your browser to the current version.", 20, 60);
137    return;
138    }
139  
140    textAlign(CENTER);
141    text("KISA 7 with Display", width/2, 30);
142    text("Source file: " + audio.src, width/2, 60);
143    text("Status: " + audioStatus, width/2, 80);
144    text("Current time: " + round(audio.currentTime)
145         + " sec.   Length: " + round(audio.duration)+ " sec.", width/2, 100);
146    text("Volume (0 to 1): " + round(10 * audio.volume)/10.0, width/2, 120);
147  
148    try {
149      playBtn.draw();
150      pauseBtn.draw();
151      stopBtn.draw();
152      volumeDownBtn.draw();
153      volumeUpBtn.draw();
154      timeDownBtn.draw();
155      timeUpBtn.draw();
156      loopBtn.draw();
157      autoplayBtn.draw();
158      for (i = 0; i < numFiles; i++) {
159        fileBtn[i].draw();
160      }
161    } catch (e) {
162     // ignore
163    }
164    // draw sound and chord buttons
165    try {
166      fill(#DDDD99);
167      rect(xSoundFiles - 18, ySoundFiles - 143, 119, 133);
168      for (i = 0; i < numSoundFiles; i++) {
169        if (soundFilesBtn[i] != null)
170          soundFilesBtn[i].draw();
171      }
172      if (chordsReady)
173        chordBtn.draw();
174      // show available audio controls
175      for (i = 1; i < audioArray.length; i++) {
176        if (audioStatusArray[i].available)
177          c = #00FF00;  // available
178        else 
179          c = #FF0000;  // used
180        fill(c);
181        ellipse(xSoundFiles - 12 + i* 9, ySoundFiles - 47, 6, 6);
182      }
183    } catch (e) {
184     // ignore
185    }    // draw the display items
186    try {
187      dis.draw();
188      displayOnBtn.draw();
189    } catch (e) {
190    // ignore
191    }  
192  }  // draw
193  
194  void setSource(int num, String url) {
195    // Called to set the source file.  Do not include the file extension
196    // in the url.  It will be added here.  "num" specifies subscript of
197    // the audio object that will be used.
198    if (num >= 0 && num < numAudio) {
199      audioArray[num].setAttribute("src", url + fileExt);
200      if (num == 0)
201        audioStatus = "File selected";
202    }
203  } // setSource
204  
205  void stop() {
206    // Called stop playing the file by pausing it and setting the
207    // time back to 0;
208    audio.pause();
209    audio.currentTime = 0;
210    audioStatus = "Stopped";
211  }  // stop
212  
213  void loadStart() {
214    // LoadStart event processing
215    audioStatus = "Loading";
216  }  // loadStart
217  
218  void chordCanPlayThrough() {
219    int i;
220    boolean loaded;
221    int readyCount = 0;
222    for (i = 1; i <= chordFiles.length; i++) {
223      loaded = audioArray[i].readyState == 4;
224      if (loaded)
225        readyCount++;
226    }
227    if (readyCount >= chordFiles.length)
228      chordsReady = true;
229  }  // chordCanPlayThrough  
230    
231  void playing() {
232    // Playing event processing
233    audioStatus = "Playing";
234  }  // playing
235  
236  void pause() {
237    // Pause event processing.
238    // There is no stop event but a "stop" causes a pause.  This
239    // method checks to see if the current time = 0. If so it assumes
240    // stopped
241    if (audio.currentTime == 0)
242      audioStatus = "Stopped";
243    else
244      audioStatus = "Paused";
245  }  // pause
246  
247  void ended() {
248    int j;
249    // Ending event processing
250    if (repeatLoop) {
251      currentFile = (currentFile+1) % numFiles;
252      setSource(0, files[currentFile]);
253      setRadioButton(fileBtn, currentFile);
254      audio.play();
255    } else {
256      audioStatus = "Finished playing";
257    }
258  }  // ended
259  
260  void endedSound() {
261    // Check the audio array for ended.
262    // audio arrray elements are available if they have "ended"
263    int i;
264    for (i = 1; i < audioArray.length; i++) {
265      if (audioArray[i].ended)
266        audioStatusArray[i].available = true;
267    }
268  } // endedSound
269  
270  int load(String fileName) {  
271    // Determines the audio player that will be used.  It attempt
272    // to find an available player that already has the file loaded.
273    // If this is impossible, it just uses the first available 
274    // player.
275    int i;
276    // find the first audio player that is not in use with the file loaded
277    for (i = 1; i < numAudio; i++) {
278      if (audioStatusArray[i].available) {  // audioArray[i] is finished playing
279        // is the source already loaded into the player?  If so, use it.
280        if (audioArray[i].src == fileName + fileExt) {
281          audioStatusArray[i].available = false;  // mark player as being used
282          // return the player number so it will be used
283          return i;
284        }
285      }
286    }
287    
288    // If no player used desired source file, use the first available player
289    for (i = 1; i < numAudio; i++) {
290      if (audioStatusArray[i].available) {
291        // set the source
292        setSource(i, fileName);
293        audioStatusArray[i].available = false;
294        // return the player number
295        return i;
296      }
297    }
298    return -1;  // All the audioArray elements are in use
299  }  // load
300  
301  void play(int num) {
302    if (num >= 0 && num < numAudio) {
303      audioArray[num].play();
304    }
305  }  // play
306  
307  String determineFileExt() {
308    // returns the file extension that will be used
309    String desiredExt;
310    Audio aud = new Audio();
311    if (aud.canPlayType && aud.canPlayType("audio/ogg")) {
312      desiredExt = ".ogg";
313    } else {
314      desiredExt = ".mp3";
315    }
316    return desiredExt;
317  }  // determineFileExt
318  
319  void error() {
320    // error event processing
321    audioStatus = "Error";
322  }  // error
323  
324  void mouseClicked() {
325    // Mouse clicked event processing
326    double v, t;
327    int i;
328    int[] chordPlayer = new int[numChordFiles];
329    if (playBtn.isOver()) {
330      audio.play();
331    } else if (pauseBtn.isOver()) {
332      audio.pause();
333    } else if (stopBtn.isOver()) {
334      stop();
335    } else if (volumeUpBtn.isOver()) {
336      v = audio.volume + 0.1;
337      adjustVolume(v);
338    } else if (volumeDownBtn.isOver()) {
339      v = audio.volume - 0.1;
340      adjustVolume(v);
341    } else if (timeUpBtn.isOver()) {
342      t = audio.currentTime + 0.1 * audio.duration;
343      audio.currentTime = constrain(t, 0, audio.duration);
344    } else if (timeDownBtn.isOver()) {
345      t = audio.currentTime - 0.1 * audio.duration;
346      audio.currentTime = constrain(t, 0, audio.duration);
347    } else if (loopBtn.isOver()) {
348      loopBtn.toggleState();
349      repeatLoop = loopBtn.getState();
350    } else if (autoplayBtn.isOver()) {
351      autoplayBtn.toggleState();
352      audio.autoplay = autoplayBtn.getState();
353    } else if (chordBtn.isOver()) {
354      for (i = 0; i < numChordFiles; i++)
355        chordPlayer[i] = load(chordFiles[i]);
356      /*** don't do this - it stall the program! ***
357      while(audioArray[chordPlayer[0]].readyState != 4
358            || audioArray[[chordPlayer[1]].readyState != 4
359            || audioArray[[chordPlayer[2]].readyState != 4) {
360      // waste time
361      }
362      */
363      for (i = 0; i < numChordFiles; i++)
364        play(chordPlayer[i]);
365    } else if (displayOnBtn.isOver()) {
366      displayOnBtn.toggleState();
367      dis.turnDisplayOn(displayOnBtn.getState());
368    } else {
369      for (i = 0; i < numFiles; i++) {
370        if (fileBtn[i].isOver()) {
371          setRadioButton(fileBtn, i);
372          currentFile = i;
373          setSource(0, files[currentFile]);
374          return;
375        }
376      }
377      for (i = 0; i < numSoundFiles; i++) {
378        if (soundFilesBtn[i].isOver()) {
379          play(load(soundFiles[i]))
380        }
381      }
382    }
383  }  // mouseClicked
384  
385  void setRadioButton(ToggleButton[] btnArray, int i)  {
386    int j;
387    for (j = 0; j < numFiles; j++)
388      btnArray[j].setState(false);
389    btnArray[i].setState(true);
390  } // setRadioButton
391  
392  void adjustVolume(double v) {
393    int i;
394    v = constrain(v, 0, 1);
395    for (i = 0; i < numAudio; i++) {
396       if (audioArray[i] != 0)
397         audioArray[i].volume = v;
398    }
399  }  // adjustVolume
400  
401  
402  class AudioStatus {
403    boolean available;
404    
405    public AudioStatus (boolean avail) {
406      available = avail;
407    }  // constructor AudioStatus
408    
409    
410    public void setAvailable (boolean avail) {
411      available = avail;
412    } // setAvailable
413  
414    public boolean getAvailable() {return available; }
415    
416  }  // class AudioStatus

[+/-] The Circle.pde file

[+/-] The Display.pde file

Updated 11/6/14