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) >
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
Updated 11/6/14