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}