001package jmri.jmrit.audio; 002 003import com.jogamp.openal.AL; 004import com.jogamp.openal.ALC; 005import com.jogamp.openal.ALCdevice; 006import com.jogamp.openal.ALConstants; 007import com.jogamp.openal.ALException; 008import com.jogamp.openal.ALFactory; 009import com.jogamp.openal.util.ALut; 010import edu.umd.cs.findbugs.annotations.SuppressFBWarnings; 011import java.util.SortedSet; 012import java.util.TreeSet; 013import jmri.Audio; 014import jmri.AudioManager; 015import jmri.InstanceManager; 016import org.slf4j.Logger; 017import org.slf4j.LoggerFactory; 018 019/** 020 * This is the JOAL audio system specific AudioFactory. 021 * <p> 022 * The JOAL sound system supports, where available, full surround-sound with 3D 023 * positioning capabilities. 024 * <p> 025 * When only stereo capable hardware is available, it will automatically create 026 * an approximation of the desired sound-scape. 027 * <p> 028 * This factory initialises JOAL, provides new Joal-specific Audio objects and 029 * deals with clean-up operations. 030 * <br><br><hr><br><b> 031 * This software is based on or using the JOAL Library available from 032 * <a href="http://jogamp.org/joal/www/">http://jogamp.org/joal/www/</a> 033 * </b><br><br> 034 * JOAL is released under the BSD license. The full license terms follow: 035 * <br><i> 036 * Copyright (c) 2003-2006 Sun Microsystems, Inc. All Rights Reserved. 037 * <br> 038 * Redistribution and use in source and binary forms, with or without 039 * modification, are permitted provided that the following conditions are 040 * met: 041 * <br> 042 * - Redistribution of source code must retain the above copyright 043 * notice, this list of conditions and the following disclaimer. 044 * <br> 045 * - Redistribution in binary form must reproduce the above copyright 046 * notice, this list of conditions and the following disclaimer in the 047 * documentation and/or other materials provided with the distribution. 048 * <br> 049 * Neither the name of Sun Microsystems, Inc. or the names of 050 * contributors may be used to endorse or promote products derived from 051 * this software without specific prior written permission. 052 * <br> 053 * This software is provided "AS IS," without a warranty of any kind. ALL 054 * EXPRESS OR IMPLIED CONDITIONS, REPRESENTATIONS AND WARRANTIES, 055 * INCLUDING ANY IMPLIED WARRANTY OF MERCHANTABILITY, FITNESS FOR A 056 * PARTICULAR PURPOSE OR NON-INFRINGEMENT, ARE HEREBY EXCLUDED. SUN 057 * MICROSYSTEMS, INC. ("SUN") AND ITS LICENSORS SHALL NOT BE LIABLE FOR 058 * ANY DAMAGES SUFFERED BY LICENSEE AS A RESULT OF USING, MODIFYING OR 059 * DISTRIBUTING THIS SOFTWARE OR ITS DERIVATIVES. IN NO EVENT WILL SUN OR 060 * ITS LICENSORS BE LIABLE FOR ANY LOST REVENUE, PROFIT OR DATA, OR FOR 061 * DIRECT, INDIRECT, SPECIAL, CONSEQUENTIAL, INCIDENTAL OR PUNITIVE 062 * DAMAGES, HOWEVER CAUSED AND REGARDLESS OF THE THEORY OF LIABILITY, 063 * ARISING OUT OF THE USE OF OR INABILITY TO USE THIS SOFTWARE, EVEN IF 064 * SUN HAS BEEN ADVISED OF THE POSSIBILITY OF SUCH DAMAGES. 065 * <br> 066 * You acknowledge that this software is not designed or intended for use 067 * in the design, construction, operation or maintenance of any nuclear 068 * facility. 069 * <br><br><br></i> 070 * <hr> 071 * This file is part of JMRI. 072 * <p> 073 * JMRI is free software; you can redistribute it and/or modify it under the 074 * terms of version 2 of the GNU General Public License as published by the Free 075 * Software Foundation. See the "COPYING" file for a copy of this license. 076 * <p> 077 * JMRI is distributed in the hope that it will be useful, but WITHOUT ANY 078 * WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR 079 * A PARTICULAR PURPOSE. See the GNU General Public License for more details. 080 * 081 * @author Matthew Harris copyright (c) 2009 082 */ 083public class JoalAudioFactory extends AbstractAudioFactory { 084 085 private static AL al; 086 087 private static ALC alc; 088 089 private static ALCdevice alcDevice; 090 091 //private static ALCcontext alcContext; 092 private static boolean initialised = false; 093 094 private JoalAudioListener activeAudioListener; 095 096 /** 097 * Definition of 8-bit quad multi-channel audio format. 098 * <p> 099 * These formats are only supported by certain OpenAL implementations and 100 * support is determined at runtime. 101 * <p> 102 * Initially set format to unknown. 103 */ 104 static int AL_FORMAT_QUAD8 = AudioBuffer.FORMAT_UNKNOWN; 105 106 /** 107 * Definition of 16-bit quad multi-channel audio format. 108 * <p> 109 * These formats are only supported by certain OpenAL implementations and 110 * support is determined at runtime. 111 * <p> 112 * Initially set format to unknown. 113 */ 114 static int AL_FORMAT_QUAD16 = AudioBuffer.FORMAT_UNKNOWN; 115 116 /** 117 * Definition of 8-bit 5.1 multi-channel audio format. 118 * <p> 119 * These formats are only supported by certain OpenAL implementations and 120 * support is determined at runtime. 121 * <p> 122 * Initially set format to unknown. 123 */ 124 static int AL_FORMAT_51CHN8 = AudioBuffer.FORMAT_UNKNOWN; 125 126 /** 127 * Definition of 16-bit 5.1 multi-channel audio format. 128 * <p> 129 * These formats are only supported by certain OpenAL implementations and 130 * support is determined at runtime. 131 * <p> 132 * Initially set format to unknown. 133 */ 134 static int AL_FORMAT_51CHN16 = AudioBuffer.FORMAT_UNKNOWN; 135 136 /** 137 * Definition of 8-bit 6.1 multi-channel audio format. 138 * <p> 139 * These formats are only supported by certain OpenAL implementations and 140 * support is determined at runtime. 141 * <p> 142 * Initially set format to unknown. 143 */ 144 static int AL_FORMAT_61CHN8 = AudioBuffer.FORMAT_UNKNOWN; 145 146 /** 147 * Definition of 16-bit 6.1 multi-channel audio format. 148 * <p> 149 * These formats are only supported by certain OpenAL implementations and 150 * support is determined at runtime. 151 * <p> 152 * Initially set format to unknown. 153 */ 154 static int AL_FORMAT_61CHN16 = AudioBuffer.FORMAT_UNKNOWN; 155 156 /** 157 * Definition of 8-bit 7.1 multi-channel audio format. 158 * <p> 159 * These formats are only supported by certain OpenAL implementations and 160 * support is determined at runtime. 161 * <p> 162 * Initially set format to unknown. 163 */ 164 static int AL_FORMAT_71CHN8 = AudioBuffer.FORMAT_UNKNOWN; 165 166 /** 167 * Definition of 16-bit 7.1 multi-channel audio format. 168 * <p> 169 * These formats are only supported by certain OpenAL implementations and 170 * support is determined at runtime. 171 * <p> 172 * Initially set format to unknown. 173 */ 174 static int AL_FORMAT_71CHN16 = AudioBuffer.FORMAT_UNKNOWN; 175 176 /** 177 * Initialise this JoalAudioFactory and check for multi-channel support. 178 * <p> 179 * Initial values for multi-channel formats are set to unknown as OpenAL 180 * implementations are only guaranteed to support MONO and STEREO Buffers. 181 * <p> 182 * On initialisation, we need to check if this implementation supports 183 * multi-channel formats. 184 * <p> 185 * This is done by making alGetEnumValue calls to request the value of the 186 * Buffer Format Tag Enum (that will be passed to an alBufferData call). 187 * Enum Values are retrieved by string names. The following names are 188 * defined for multi-channel wave formats ... 189 * <ul> 190 * <li>"AL_FORMAT_QUAD8" : 4 Channel, 8 bit data 191 * <li>"AL_FORMAT_QUAD16" : 4 Channel, 16 bit data 192 * <li>"AL_FORMAT_51CHN8" : 5.1 Channel, 8 bit data 193 * <li>"AL_FORMAT_51CHN16" : 5.1 Channel, 16 bit data 194 * <li>"AL_FORMAT_61CHN8" : 6.1 Channel, 8 bit data 195 * <li>"AL_FORMAT_61CHN16" : 6.1 Channel, 16 bit data 196 * <li>"AL_FORMAT_71CHN8" : 7.1 Channel, 8 bit data 197 * <li>"AL_FORMAT_71CHN16" : 7.1 Channel, 16 bit data 198 * </ul> 199 * 200 * @return true, if initialisation successful 201 */ 202 @Override 203 @SuppressFBWarnings(value = "ST_WRITE_TO_STATIC_FROM_INSTANCE_METHOD", 204 justification = "OK to write to static variables as we only do so if not initialised") 205 public boolean init() { 206 if (initialised) { 207 return true; 208 } 209 210 // Initialise OpenAL and clear the error bit 211 try { 212// // Open default 'preferred' device 213// alcDevice = alc.alcOpenDevice(null); 214// alcContext = alc.alcCreateContext(alcDevice, null); 215// alc.alcMakeContextCurrent(alcContext); 216// 217 ALut.alutInit(); 218 al = ALFactory.getAL(); 219 al.alGetError(); 220 log.info("Initialised JOAL using OpenAL: vendor - {} version - {}", al.alGetString(AL.AL_VENDOR), al.alGetString(AL.AL_VERSION)); 221 } catch (ALException e) { 222 log.warn("Error initialising JOAL", jmri.util.LoggingUtil.shortenStacktrace(e)); 223 return false; 224 } catch (UnsatisfiedLinkError | NoClassDefFoundError e) { 225 log.warn("Error loading OpenAL libraries: {}", e.getMessage()); 226 return false; 227 } catch (RuntimeException e) { 228 log.warn("Error initialising OpenAL", jmri.util.LoggingUtil.shortenStacktrace(e)); 229 return false; 230 } 231 232 // Check for multi-channel support 233 int checkMultiChannel; 234 235 checkMultiChannel = al.alGetEnumValue("AL_FORMAT_QUAD8"); 236 checkALError(); 237 if (checkMultiChannel != ALConstants.AL_FALSE) { 238 AL_FORMAT_QUAD8 = checkMultiChannel; 239 } 240 checkMultiChannel = al.alGetEnumValue("AL_FORMAT_QUAD16"); 241 checkALError(); 242 if (checkMultiChannel != ALConstants.AL_FALSE) { 243 AL_FORMAT_QUAD16 = checkMultiChannel; 244 } 245 checkMultiChannel = al.alGetEnumValue("AL_FORMAT_51CHN8"); 246 checkALError(); 247 if (checkMultiChannel != ALConstants.AL_FALSE) { 248 AL_FORMAT_51CHN8 = checkMultiChannel; 249 } 250 checkMultiChannel = al.alGetEnumValue("AL_FORMAT_51CHN16"); 251 checkALError(); 252 if (checkMultiChannel != ALConstants.AL_FALSE) { 253 AL_FORMAT_51CHN16 = checkMultiChannel; 254 } 255 checkMultiChannel = al.alGetEnumValue("AL_FORMAT_61CHN8"); 256 checkALError(); 257 if (checkMultiChannel != ALConstants.AL_FALSE) { 258 AL_FORMAT_61CHN8 = checkMultiChannel; 259 } 260 checkMultiChannel = al.alGetEnumValue("AL_FORMAT_61CHN16"); 261 checkALError(); 262 if (checkMultiChannel != ALConstants.AL_FALSE) { 263 AL_FORMAT_61CHN16 = checkMultiChannel; 264 } 265 checkMultiChannel = al.alGetEnumValue("AL_FORMAT_71CHN8"); 266 checkALError(); 267 if (checkMultiChannel != ALConstants.AL_FALSE) { 268 AL_FORMAT_71CHN8 = checkMultiChannel; 269 } 270 checkMultiChannel = al.alGetEnumValue("AL_FORMAT_71CHN16"); 271 checkALError(); 272 if (checkMultiChannel != ALConstants.AL_FALSE) { 273 AL_FORMAT_71CHN16 = checkMultiChannel; 274 } 275 log.debug("8-bit quadrophonic supported? {}", AL_FORMAT_QUAD8 == AudioBuffer.FORMAT_UNKNOWN ? "No" : "Yes"); 276 log.debug("16-bit quadrophonic supported? {}", AL_FORMAT_QUAD16 == AudioBuffer.FORMAT_UNKNOWN ? "No" : "Yes"); 277 log.debug("8-bit 5.1 surround supported? {}", AL_FORMAT_51CHN8 == AudioBuffer.FORMAT_UNKNOWN ? "No" : "Yes"); 278 log.debug("16-bit 5.1 surround supported? {}", AL_FORMAT_51CHN16 == AudioBuffer.FORMAT_UNKNOWN ? "No" : "Yes"); 279 log.debug("8-bit 6.1 surround supported? {}", AL_FORMAT_61CHN8 == AudioBuffer.FORMAT_UNKNOWN ? "No" : "Yes"); 280 log.debug("16-bit 6.1 surround supported? {}", AL_FORMAT_61CHN16 == AudioBuffer.FORMAT_UNKNOWN ? "No" : "Yes"); 281 log.debug("8 bit 7.1 surround supported? {}", AL_FORMAT_71CHN8 == AudioBuffer.FORMAT_UNKNOWN ? "No" : "Yes"); 282 log.debug("16 bit 7.1 surround supported? {}", AL_FORMAT_71CHN16 == AudioBuffer.FORMAT_UNKNOWN ? "No" : "Yes"); 283 284 // Check context 285 alc = ALFactory.getALC(); 286 alcDevice = alc.alcGetContextsDevice(alc.alcGetCurrentContext()); 287 if (!checkALCError(alcDevice)) { 288 int[] size = new int[1]; 289 alc.alcGetIntegerv(alcDevice, ALC.ALC_ATTRIBUTES_SIZE, size.length, size, 0); 290 log.debug("Size of ALC_ATTRIBUTES: {}", size[0]); 291 if (!checkALCError(alcDevice) && size[0] > 0) { 292 int[] attributes = new int[size[0]]; 293 alc.alcGetIntegerv(alcDevice, ALC.ALC_ALL_ATTRIBUTES, attributes.length, attributes, 0); 294 for (int i = 0; i < attributes.length; i++) { 295 if (i % 2 != 0) { 296 continue; 297 } 298 switch (attributes[i]) { 299 case ALC.ALC_INVALID: 300 log.debug("Invalid"); 301 break; 302 case ALC.ALC_MONO_SOURCES: 303 log.debug("Number of mono sources: {}", attributes[i + 1]); 304 break; 305 case ALC.ALC_STEREO_SOURCES: 306 log.debug("Number of stereo sources: {}", attributes[i + 1]); 307 break; 308 case ALC.ALC_FREQUENCY: 309 log.debug("Frequency: {}", attributes[i + 1]); 310 break; 311 case ALC.ALC_REFRESH: 312 log.debug("Refresh: {}", attributes[i + 1]); 313 break; 314 default: 315 log.debug("Attribute {}: {}", i, attributes[i]); 316 } 317 } 318 } 319 } 320 321 super.init(); 322 initialised = true; 323 return true; 324 } 325 326 @Override 327 public String toString() { 328 if (al == null) return "JoalAudioFactory, using null"; 329 try { 330 return "JoalAudioFactory, using OpenAL:" 331 + " vendor - " + al.alGetString(AL.AL_VENDOR) 332 + " version - " + al.alGetString(AL.AL_VERSION); 333 } catch (NullPointerException e) { 334 log.error("NPE from JoalAudioFactory",e); 335 return "JoalAudioFactory, using Null"; 336 } 337 } 338 339 @Override 340 @SuppressFBWarnings(value = "ST_WRITE_TO_STATIC_FROM_INSTANCE_METHOD", 341 justification = "OK to write to static variables to record static library status") 342 public void cleanup() { 343 // Stop the command thread 344 super.cleanup(); 345 346 // Get the active AudioManager 347 AudioManager am = InstanceManager.getDefault(jmri.AudioManager.class); 348 349 // Retrieve list of AudioSource objects and remove the sources 350 SortedSet<Audio> sources = new TreeSet<>(am.getNamedBeanSet(Audio.SOURCE)); 351 for (Audio source: sources) { 352 if (log.isDebugEnabled()) { 353 log.debug("Removing JoalAudioSource: {}", source.getSystemName()); 354 } 355 // Includes cleanup 356 source.dispose(); 357 } 358 359 // Now, retrieve list of AudioBuffer objects and remove the buffers 360 SortedSet<Audio> buffers = new TreeSet<>(am.getNamedBeanSet(Audio.BUFFER)); 361 for (Audio buffer : buffers) { 362 if (log.isDebugEnabled()) { 363 log.debug("Removing JoalAudioBuffer: {}", buffer.getSystemName()); 364 } 365 // Includes cleanup 366 buffer.dispose(); 367 } 368 369 // Lastly, retrieve list of AudioListener objects and remove listener. 370 SortedSet<Audio> listeners = new TreeSet<>(am.getNamedBeanSet(Audio.LISTENER)); 371 for (Audio listener : listeners) { 372 if (log.isDebugEnabled()) { 373 log.debug("Removing JoalAudioListener: {}", listener.getSystemName()); 374 } 375 // Includes cleanup 376 listener.dispose(); 377 } 378 379 // Finally, shutdown OpenAL and close the output device 380 log.debug("Shutting down OpenAL, initialised: {}", initialised); 381 if (initialised) ALut.alutExit(); 382 initialised = false; 383 } 384 385 @Override 386 public boolean isInitialised() { 387 return initialised; 388 } 389 390 @Override 391 public AudioBuffer createNewBuffer(String systemName, String userName) { 392 return new JoalAudioBuffer(systemName, userName); 393 } 394 395 @Override 396 public AudioListener createNewListener(String systemName, String userName) { 397 activeAudioListener = new JoalAudioListener(systemName, userName); 398 return activeAudioListener; 399 } 400 401 @Override 402 public AudioListener getActiveAudioListener() { 403 return activeAudioListener; 404 } 405 406 @Override 407 public AudioSource createNewSource(String systemName, String userName) { 408 return new JoalAudioSource(systemName, userName); 409 } 410 411 @Override 412 public void setDistanceAttenuated(boolean attenuated) { 413 super.setDistanceAttenuated(attenuated); 414 if (isDistanceAttenuated()) { 415 al.alDistanceModel(ALConstants.AL_INVERSE_DISTANCE_CLAMPED); 416 } else { 417 al.alDistanceModel(ALConstants.AL_NONE); 418 } 419 } 420 421 /** 422 * Return a reference to the active AL object for use by other Joal objects 423 * 424 * @return active AL object 425 */ 426 public static synchronized AL getAL() { 427 return al; 428 } 429 430 /** 431 * Method to check if any error has occurred in the OpenAL sub-system. 432 * <p> 433 * If an error has occurred, log the error as a warning message and return 434 * True. 435 * <p> 436 * If no error has occurred, return False. 437 * 438 * @return True if an error has occurred 439 */ 440 public static boolean checkALError() { 441 return checkALError(""); 442 } 443 444 /** 445 * Method to check if any error has occurred in the OpenAL sub-system. 446 * <p> 447 * If an error has occurred, log the error as a warning message with the 448 * defined message pre-pended and return True. 449 * <p> 450 * If no error has occurred, return False. 451 * 452 * @param msg additional message prepended to the log 453 * @return True if an error has occurred 454 */ 455 public static boolean checkALError(String msg) { 456 // Trim any whitespace then append a space if required 457 msg = msg.trim(); 458 if (msg.length() > 0) { 459 msg = msg + " "; 460 } 461 462 // Check for error 463 switch (al.alGetError()) { 464 case AL.AL_NO_ERROR: 465 return false; 466 case AL.AL_INVALID_NAME: 467 log.warn("{}Invalid name parameter", msg); 468 return true; 469 case AL.AL_INVALID_ENUM: 470 log.warn("{}Invalid enumerated parameter value", msg); 471 return true; 472 case AL.AL_INVALID_VALUE: 473 log.warn("{}Invalid parameter value", msg); 474 return true; 475 case AL.AL_INVALID_OPERATION: 476 log.warn("{}Requested operation is not valid", msg); 477 return true; 478 case AL.AL_OUT_OF_MEMORY: 479 log.warn("{}Out of memory", msg); 480 return true; 481 default: 482 log.warn("{}Unrecognised error occurred", msg); 483 return true; 484 } 485 } 486 487 /** 488 * Method to check if any error has occurred in the OpenAL sub-system. 489 * <p> 490 * If an error has occurred, log the error as a warning message and return 491 * True. 492 * <p> 493 * If no error has occurred, return False. 494 * 495 * @param alcDevice OpenAL context device to check 496 * @return True if an error has occurred 497 */ 498 public static boolean checkALCError(ALCdevice alcDevice) { 499 return checkALCError(alcDevice, ""); 500 } 501 502 /** 503 * Method to check if any error has occurred in the OpenAL context 504 * sub-system. 505 * <p> 506 * If an error has occurred, log the error as a warning message with the 507 * defined message pre-pended and return True. 508 * <p> 509 * If no error has occurred, return False. 510 * 511 * @param alcDevice OpenAL context device to check 512 * @param msg additional message prepended to the log 513 * @return True if an error has occurred 514 */ 515 public static boolean checkALCError(ALCdevice alcDevice, String msg) { 516 // Trim any whitespace then append a space if required 517 msg = msg.trim(); 518 if (msg.length() > 0) { 519 msg = msg + " "; 520 } 521 522 // Check for error 523 switch (alc.alcGetError(alcDevice)) { 524 case ALC.ALC_NO_ERROR: 525 return false; 526 case ALC.ALC_INVALID_DEVICE: 527 log.warn("{}Invalid device", msg); 528 return true; 529 case ALC.ALC_INVALID_CONTEXT: 530 log.warn("{}Invalid context", msg); 531 return true; 532 case ALC.ALC_INVALID_ENUM: 533 log.warn("{}Invalid enumerated parameter value", msg); 534 return true; 535 case ALC.ALC_INVALID_VALUE: 536 log.warn("{}Invalid parameter value", msg); 537 return true; 538 case ALC.ALC_OUT_OF_MEMORY: 539 log.warn("{}Out of memory", msg); 540 return true; 541 default: 542 log.warn("{}Unrecognised error occurred", msg); 543 return true; 544 } 545 } 546 547 private static final Logger log = LoggerFactory.getLogger(JoalAudioFactory.class); 548 549}