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            if (log.isInfoEnabled()) {
221                log.info("Initialised JOAL using OpenAL: vendor - {} version - {}", al.alGetString(AL.AL_VENDOR), al.alGetString(AL.AL_VERSION));
222            }
223        } catch (ALException e) {
224            if (log.isDebugEnabled()) {
225                log.debug("Error initialising JOAL: {}", e);
226            }
227            return false;
228        } catch (RuntimeException e) {
229            if (log.isDebugEnabled()) {
230                log.debug("Error initialising OpenAL: {}", e);
231            }
232            return false;
233        }
234
235        // Check for multi-channel support
236        int checkMultiChannel;
237
238        checkMultiChannel = al.alGetEnumValue("AL_FORMAT_QUAD8");
239        checkALError();
240        if (checkMultiChannel != ALConstants.AL_FALSE) {
241            AL_FORMAT_QUAD8 = checkMultiChannel;
242        }
243        checkMultiChannel = al.alGetEnumValue("AL_FORMAT_QUAD16");
244        checkALError();
245        if (checkMultiChannel != ALConstants.AL_FALSE) {
246            AL_FORMAT_QUAD16 = checkMultiChannel;
247        }
248        checkMultiChannel = al.alGetEnumValue("AL_FORMAT_51CHN8");
249        checkALError();
250        if (checkMultiChannel != ALConstants.AL_FALSE) {
251            AL_FORMAT_51CHN8 = checkMultiChannel;
252        }
253        checkMultiChannel = al.alGetEnumValue("AL_FORMAT_51CHN16");
254        checkALError();
255        if (checkMultiChannel != ALConstants.AL_FALSE) {
256            AL_FORMAT_51CHN16 = checkMultiChannel;
257        }
258        checkMultiChannel = al.alGetEnumValue("AL_FORMAT_61CHN8");
259        checkALError();
260        if (checkMultiChannel != ALConstants.AL_FALSE) {
261            AL_FORMAT_61CHN8 = checkMultiChannel;
262        }
263        checkMultiChannel = al.alGetEnumValue("AL_FORMAT_61CHN16");
264        checkALError();
265        if (checkMultiChannel != ALConstants.AL_FALSE) {
266            AL_FORMAT_61CHN16 = checkMultiChannel;
267        }
268        checkMultiChannel = al.alGetEnumValue("AL_FORMAT_71CHN8");
269        checkALError();
270        if (checkMultiChannel != ALConstants.AL_FALSE) {
271            AL_FORMAT_71CHN8 = checkMultiChannel;
272        }
273        checkMultiChannel = al.alGetEnumValue("AL_FORMAT_71CHN16");
274        checkALError();
275        if (checkMultiChannel != ALConstants.AL_FALSE) {
276            AL_FORMAT_71CHN16 = checkMultiChannel;
277        }
278        log.debug("8-bit quadrophonic supported? {}", AL_FORMAT_QUAD8 == AudioBuffer.FORMAT_UNKNOWN ? "No" : "Yes");
279        log.debug("16-bit quadrophonic supported? {}", AL_FORMAT_QUAD16 == AudioBuffer.FORMAT_UNKNOWN ? "No" : "Yes");
280        log.debug("8-bit 5.1 surround supported? {}", AL_FORMAT_51CHN8 == AudioBuffer.FORMAT_UNKNOWN ? "No" : "Yes");
281        log.debug("16-bit 5.1 surround supported? {}", AL_FORMAT_51CHN16 == AudioBuffer.FORMAT_UNKNOWN ? "No" : "Yes");
282        log.debug("8-bit 6.1 surround supported? {}", AL_FORMAT_61CHN8 == AudioBuffer.FORMAT_UNKNOWN ? "No" : "Yes");
283        log.debug("16-bit 6.1 surround supported? {}", AL_FORMAT_61CHN16 == AudioBuffer.FORMAT_UNKNOWN ? "No" : "Yes");
284        log.debug("8 bit 7.1 surround supported? {}", AL_FORMAT_71CHN8 == AudioBuffer.FORMAT_UNKNOWN ? "No" : "Yes");
285        log.debug("16 bit 7.1 surround supported? {}", AL_FORMAT_71CHN16 == AudioBuffer.FORMAT_UNKNOWN ? "No" : "Yes");
286
287        // Check context
288        alc = ALFactory.getALC();
289        alcDevice = alc.alcGetContextsDevice(alc.alcGetCurrentContext());
290        if (!checkALCError(alcDevice)) {
291            int[] size = new int[1];
292            alc.alcGetIntegerv(alcDevice, ALC.ALC_ATTRIBUTES_SIZE, size.length, size, 0);
293            log.debug("Size of ALC_ATTRIBUTES: {}", size[0]);
294            if (!checkALCError(alcDevice) && size[0] > 0) {
295                int[] attributes = new int[size[0]];
296                alc.alcGetIntegerv(alcDevice, ALC.ALC_ALL_ATTRIBUTES, attributes.length, attributes, 0);
297                for (int i = 0; i < attributes.length; i++) {
298                    if (i % 2 != 0) {
299                        continue;
300                    }
301                    switch (attributes[i]) {
302                        case ALC.ALC_INVALID:
303                            log.debug("Invalid");
304                            break;
305                        case ALC.ALC_MONO_SOURCES:
306                            log.debug("Number of mono sources: {}", attributes[i + 1]);
307                            break;
308                        case ALC.ALC_STEREO_SOURCES:
309                            log.debug("Number of stereo sources: {}", attributes[i + 1]);
310                            break;
311                        case ALC.ALC_FREQUENCY:
312                            log.debug("Frequency: {}", attributes[i + 1]);
313                            break;
314                        case ALC.ALC_REFRESH:
315                            log.debug("Refresh: {}", attributes[i + 1]);
316                            break;
317                        default:
318                            log.debug("Attribute {}: {}", i, attributes[i]);
319                    }
320                }
321            }
322        }
323
324        super.init();
325        initialised = true;
326        return true;
327    }
328
329    @Override
330    public String toString() {
331        try {
332            return "JoalAudioFactory, using OpenAL:"
333                    + " vendor - " + al.alGetString(AL.AL_VENDOR)
334                    + " version - " + al.alGetString(AL.AL_VERSION);
335        } catch (NullPointerException e) {
336            log.error("NPE from JoalAudioFactory: {}",e);
337            return "JoalAudioFactory, using Null";
338        }
339    }
340
341    @Override
342    @SuppressFBWarnings(value = "ST_WRITE_TO_STATIC_FROM_INSTANCE_METHOD",
343            justification = "OK to write to static variables to record static library status")
344    public void cleanup() {
345        // Stop the command thread
346        super.cleanup();
347
348        // Get the active AudioManager
349        AudioManager am = InstanceManager.getDefault(jmri.AudioManager.class);
350
351        // Retrieve list of AudioSource objects and remove the sources
352        SortedSet<Audio> sources = new TreeSet<>(am.getNamedBeanSet(Audio.SOURCE));
353        for (Audio source: sources) {
354            if (log.isDebugEnabled()) {
355                log.debug("Removing JoalAudioSource: {}", source.getSystemName());
356            }
357            // Cast to JoalAudioSource and cleanup
358            ((JoalAudioSource) source).cleanup();
359        }
360
361        // Now, retrieve list of AudioBuffer objects and remove the buffers
362        SortedSet<Audio> buffers = new TreeSet<>(am.getNamedBeanSet(Audio.BUFFER));
363        for (Audio buffer : buffers) {
364            if (log.isDebugEnabled()) {
365                log.debug("Removing JoalAudioBuffer: {}", buffer.getSystemName());
366            }
367            // Cast to JoalAudioBuffer and cleanup
368            ((JoalAudioBuffer) buffer).cleanup();
369        }
370
371        // Lastly, retrieve list of AudioListener objects and remove listener.
372        SortedSet<Audio> listeners = new TreeSet<>(am.getNamedBeanSet(Audio.LISTENER));
373        for (Audio listener : listeners) {
374            if (log.isDebugEnabled()) {
375                log.debug("Removing JoalAudioListener: {}", listener.getSystemName());
376            }
377            // Cast to JoalAudioListener and cleanup
378            ((JoalAudioListener) listener).cleanup();
379        }
380
381        // Finally, shutdown OpenAL and close the output device
382        log.debug("Shutting down OpenAL, initialised: {}", initialised);
383        if (initialised) ALut.alutExit();
384        initialised = false;
385    }
386
387    @Override
388    public boolean isInitialised() {
389        return initialised;
390    }
391
392    @Override
393    public AudioBuffer createNewBuffer(String systemName, String userName) {
394        return new JoalAudioBuffer(systemName, userName);
395    }
396
397    @Override
398    public AudioListener createNewListener(String systemName, String userName) {
399        activeAudioListener = new JoalAudioListener(systemName, userName);
400        return activeAudioListener;
401    }
402
403    @Override
404    public AudioListener getActiveAudioListener() {
405        return activeAudioListener;
406    }
407
408    @Override
409    public AudioSource createNewSource(String systemName, String userName) {
410        return new JoalAudioSource(systemName, userName);
411    }
412
413    @Override
414    public void setDistanceAttenuated(boolean attenuated) {
415        super.setDistanceAttenuated(attenuated);
416        if (isDistanceAttenuated()) {
417            al.alDistanceModel(ALConstants.AL_INVERSE_DISTANCE_CLAMPED);
418        } else {
419            al.alDistanceModel(ALConstants.AL_NONE);
420        }
421    }
422
423    /**
424     * Return a reference to the active AL object for use by other Joal objects
425     *
426     * @return active AL object
427     */
428    public static synchronized AL getAL() {
429        return al;
430    }
431
432    /**
433     * Method to check if any error has occurred in the OpenAL sub-system.
434     * <p>
435     * If an error has occurred, log the error as a warning message and return
436     * True.
437     * <p>
438     * If no error has occurred, return False.
439     *
440     * @return True if an error has occurred
441     */
442    public static boolean checkALError() {
443        return checkALError("");
444    }
445
446    /**
447     * Method to check if any error has occurred in the OpenAL sub-system.
448     * <p>
449     * If an error has occurred, log the error as a warning message with the
450     * defined message pre-pended and return True.
451     * <p>
452     * If no error has occurred, return False.
453     *
454     * @param msg additional message prepended to the log
455     * @return True if an error has occurred
456     */
457    public static boolean checkALError(String msg) {
458        // Trim any whitespace then append a space if required
459        msg = msg.trim();
460        if (msg.length() > 0) {
461            msg = msg + " ";
462        }
463
464        // Check for error
465        switch (al.alGetError()) {
466            case AL.AL_NO_ERROR:
467                return false;
468            case AL.AL_INVALID_NAME:
469                log.warn("{}Invalid name parameter", msg);
470                return true;
471            case AL.AL_INVALID_ENUM:
472                log.warn("{}Invalid enumerated parameter value", msg);
473                return true;
474            case AL.AL_INVALID_VALUE:
475                log.warn("{}Invalid parameter value", msg);
476                return true;
477            case AL.AL_INVALID_OPERATION:
478                log.warn("{}Requested operation is not valid", msg);
479                return true;
480            case AL.AL_OUT_OF_MEMORY:
481                log.warn("{}Out of memory", msg);
482                return true;
483            default:
484                log.warn("{}Unrecognised error occurred", msg);
485                return true;
486        }
487    }
488
489    /**
490     * Method to check if any error has occurred in the OpenAL sub-system.
491     * <p>
492     * If an error has occurred, log the error as a warning message and return
493     * True.
494     * <p>
495     * If no error has occurred, return False.
496     *
497     * @param alcDevice OpenAL context device to check
498     * @return True if an error has occurred
499     */
500    public static boolean checkALCError(ALCdevice alcDevice) {
501        return checkALCError(alcDevice, "");
502    }
503
504    /**
505     * Method to check if any error has occurred in the OpenAL context
506     * sub-system.
507     * <p>
508     * If an error has occurred, log the error as a warning message with the
509     * defined message pre-pended and return True.
510     * <p>
511     * If no error has occurred, return False.
512     *
513     * @param alcDevice OpenAL context device to check
514     * @param msg       additional message prepended to the log
515     * @return True if an error has occurred
516     */
517    public static boolean checkALCError(ALCdevice alcDevice, String msg) {
518        // Trim any whitespace then append a space if required
519        msg = msg.trim();
520        if (msg.length() > 0) {
521            msg = msg + " ";
522        }
523
524        // Check for error
525        switch (alc.alcGetError(alcDevice)) {
526            case ALC.ALC_NO_ERROR:
527                return false;
528            case ALC.ALC_INVALID_DEVICE:
529                log.warn("{}Invalid device", msg);
530                return true;
531            case ALC.ALC_INVALID_CONTEXT:
532                log.warn("{}Invalid context", msg);
533                return true;
534            case ALC.ALC_INVALID_ENUM:
535                log.warn("{}Invalid enumerated parameter value", msg);
536                return true;
537            case ALC.ALC_INVALID_VALUE:
538                log.warn("{}Invalid parameter value", msg);
539                return true;
540            case ALC.ALC_OUT_OF_MEMORY:
541                log.warn("{}Out of memory", msg);
542                return true;
543            default:
544                log.warn("{}Unrecognised error occurred", msg);
545                return true;
546        }
547    }
548
549    private static final Logger log = LoggerFactory.getLogger(JoalAudioFactory.class);
550
551}