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