001package jmri.jmrit.audio;
002
003import edu.umd.cs.findbugs.annotations.SuppressFBWarnings;
004
005import java.util.Collections;
006import java.util.SortedSet;
007import java.util.TreeSet;
008import javax.annotation.Nonnull;
009import jmri.Audio;
010import jmri.AudioException;
011import jmri.InstanceManager;
012import jmri.jmrix.internal.InternalSystemConnectionMemo;
013import jmri.managers.AbstractAudioManager;
014import org.slf4j.Logger;
015import org.slf4j.LoggerFactory;
016
017/**
018 * Provide the concrete implementation for the Internal Audio Manager.
019 *
020 * <hr>
021 * This file is part of JMRI.
022 * <p>
023 * JMRI is free software; you can redistribute it and/or modify it under the
024 * terms of version 2 of the GNU General Public License as published by the Free
025 * Software Foundation. See the "COPYING" file for a copy of this license.
026 * <p>
027 * JMRI is distributed in the hope that it will be useful, but WITHOUT ANY
028 * WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR
029 * A PARTICULAR PURPOSE. See the GNU General Public License for more details.
030 *
031 * @author Matthew Harris copyright (c) 2009
032 */
033public class DefaultAudioManager extends AbstractAudioManager {
034
035    private static int countListeners = 0;
036    private static int countSources = 0;
037    private static int countBuffers = 0;
038
039    public DefaultAudioManager(InternalSystemConnectionMemo memo) {
040        super(memo);
041    }
042
043    /**
044     * Reference to the currently active AudioFactory.
045     * Because of underlying (external to Java) implementation details,
046     * JMRI only ever has one AudioFactory, so we make this static.
047     */
048    private static AudioFactory activeAudioFactory = null;
049
050    private synchronized static void setActiveAudioFactory(AudioFactory factory){
051        activeAudioFactory = factory;
052    }
053
054    private static boolean initialised = false;
055
056    private final TreeSet<Audio> listeners = new TreeSet<>(new jmri.util.NamedBeanComparator<>());
057    private final TreeSet<Audio> buffers = new TreeSet<>(new jmri.util.NamedBeanComparator<>());
058    private final TreeSet<Audio> sources = new TreeSet<>(new jmri.util.NamedBeanComparator<>());
059
060    public final Runnable audioShutDownTask = this::cleanup;
061
062    @Override
063    public int getXMLOrder() {
064        return jmri.Manager.AUDIO;
065    }
066
067    @Override
068    protected synchronized Audio createNewAudio(@Nonnull String systemName, String userName) throws AudioException {
069
070        if (activeAudioFactory == null) {
071            log.debug("Initialise in createNewAudio");
072            init();
073        }
074
075        Audio a = null;
076
077        log.debug("sysName: {} userName: {}", systemName, userName);
078        if (userName != null && _tuser.containsKey(userName)) {
079            throw new AudioException("Duplicate name");
080        }
081
082        switch (systemName.charAt(2)) {
083
084            case Audio.BUFFER: {
085                if (countBuffers >= MAX_BUFFERS) {
086                    log.error("Maximum number of buffers reached ({}) " + MAX_BUFFERS, countBuffers);
087                    throw new AudioException("Maximum number of buffers reached (" + countBuffers + ") " + MAX_BUFFERS);
088                }
089                countBuffers++;
090                a = activeAudioFactory.createNewBuffer(systemName, userName);
091                buffers.add(a);
092                break;
093            }
094            case Audio.LISTENER: {
095                if (countListeners >= MAX_LISTENERS) {
096                    log.error("Maximum number of Listeners reached ({}) " + MAX_LISTENERS, countListeners);
097                    throw new AudioException("Maximum number of Listeners reached (" + countListeners + ") " + MAX_LISTENERS);
098                }
099                countListeners++;
100                a = activeAudioFactory.createNewListener(systemName, userName);
101                listeners.add(a);
102                break;
103            }
104            case Audio.SOURCE: {
105                if (countSources >= MAX_SOURCES) {
106                    log.error("Maximum number of Sources reached ({}) " + MAX_SOURCES, countSources);
107                    throw new AudioException("Maximum number of Sources reached (" + countSources + ") " + MAX_SOURCES);
108                }
109                countSources++;
110                a = activeAudioFactory.createNewSource(systemName, userName);
111                sources.add(a);
112                break;
113            }
114            default:
115                throw new IllegalArgumentException();
116        }
117
118        return a;
119    }
120
121    /** {@inheritDoc} */
122    @Override
123    @Nonnull
124    public SortedSet<Audio> getNamedBeanSet(char subType) {
125        switch (subType) {
126            case Audio.BUFFER: {
127                return Collections.unmodifiableSortedSet(buffers);
128            }
129            case Audio.LISTENER: {
130                return Collections.unmodifiableSortedSet(listeners);
131            }
132            case Audio.SOURCE: {
133                return Collections.unmodifiableSortedSet(sources);
134            }
135            default: {
136                throw new IllegalArgumentException();
137            }
138        }
139    }
140
141    /**
142     * Attempt to create and initialise an AudioFactory, working
143     * down a preference hierarchy. Result is in activeAudioFactory.
144     * Uses null implementation to always succeed
145     */
146    private void createFactory() {
147        // was a specific implementation requested?
148        // define as jmri.jmrit.audio.NullAudioFactory to get headless CI form in testing
149        String className = System.getProperty("jmri.jmrit.audio.DefaultAudioManager.implementation");
150        // if present, determines the active factory class
151        if (className != null) {
152            log.debug("Try to initialise {} from property", className);
153            try {
154                Class<?> c = Class.forName(className);
155                if (AudioFactory.class.isAssignableFrom(c)) {
156                    activeAudioFactory = (AudioFactory) c.getConstructor().newInstance();
157                    if (activeAudioFactory.init()) {
158                        // all OK
159                        return;
160                    } else {
161                        log.error("Specified jmri.jmrit.audio.DefaultAudioManager.implementation value {} did not initialize, continuing", className);
162                    }
163                } else {
164                    log.error("Specified jmri.jmrit.audio.DefaultAudioManager.implementation value {} is not a jmri.AudioFactory subclass, continuing", className);
165                }
166            } catch (
167                    ClassNotFoundException |
168                    InstantiationException |
169                    IllegalAccessException |
170                    java.lang.reflect.InvocationTargetException |
171                    NoSuchMethodException |
172                    SecurityException e) {
173                log.error("Unable to instantiate AudioFactory class {} with default constructor", className);
174                // and proceed to fallback choices
175            }
176        }
177
178//      // Try to initialise LWJGL
179//      log.debug("Try to initialise LWJGLAudioFactory");
180//      activeAudioFactory = new LWJGLAudioFactory();
181//      if (activeAudioFactory.init()) return;
182//
183        // Next try JOAL
184        log.debug("Try to initialise JoalAudioFactory");
185        DefaultAudioManager.setActiveAudioFactory( new JoalAudioFactory());
186        if (DefaultAudioManager.activeAudioFactory.init()) return;
187
188        // fall-back to JavaSound
189        log.debug("Try to initialise JavaSoundAudioFactory");
190        DefaultAudioManager.setActiveAudioFactory( new JavaSoundAudioFactory());
191        if (DefaultAudioManager.activeAudioFactory.init()) return;
192
193        // Finally, if JavaSound fails, fall-back to a Null sound system
194        log.debug("Try to initialise NullAudioFactory");
195        DefaultAudioManager.setActiveAudioFactory( new NullAudioFactory());
196        DefaultAudioManager.activeAudioFactory.init();
197        // assumed to succeed.
198    }
199
200    /**
201     * Initialise the manager and make connections.
202     */
203    @SuppressFBWarnings(value = "ST_WRITE_TO_STATIC_FROM_INSTANCE_METHOD")
204    // OK to write to static variables as we only do so if not initialised
205    @Override
206    public synchronized void init() {
207        if (!initialised) {
208
209            // create Factory of appropriate type
210            createFactory();
211
212            // Create default Listener and save in map
213            try {
214                Audio s = createNewAudio("IAL$", "Default Audio Listener");
215                register(s);
216            } catch (AudioException ex) {
217                log.error("Error creating Default Audio Listener", ex);
218            }
219
220            // Register a shutdown task to ensure clean exit
221            InstanceManager.getDefault(jmri.ShutDownManager.class).register(audioShutDownTask);
222
223            initialised = true;
224            if (log.isDebugEnabled()) {
225                log.debug("Initialised AudioFactory type: {}", activeAudioFactory.getClass().getSimpleName());
226            }
227        }
228    }
229
230    /**
231     * {@inheritDoc}
232     */
233    @Override
234    public boolean isInitialised() {
235        return initialised;
236    }
237
238    @Override
239    @SuppressFBWarnings(value = "ST_WRITE_TO_STATIC_FROM_INSTANCE_METHOD",
240            justification = "Synchronized method to ensure correct counter manipulation")
241    public synchronized void deregister(@Nonnull Audio s) {
242        // Decrement the relevant Audio object counter
243        switch (s.getSubType()) {
244            case (Audio.BUFFER): {
245                buffers.remove(s);
246                countBuffers--;
247                log.debug("Remove buffer; count: {}", countBuffers);
248                break;
249            }
250            case (Audio.SOURCE): {
251                sources.remove(s);
252                countSources--;
253                log.debug("Remove source; count: {}", countSources);
254                break;
255            }
256            case (Audio.LISTENER): {
257                listeners.remove(s);
258                countListeners--;
259                log.debug("Remove listener; count: {}", countListeners);
260                break;
261            }
262            default:
263                throw new IllegalArgumentException();
264        }
265        super.deregister(s);
266    }
267
268    @SuppressFBWarnings(value = "ST_WRITE_TO_STATIC_FROM_INSTANCE_METHOD",
269            justification = "OK to write to static variables to record static library status")
270    @Override
271    public void cleanup() {
272        // Shutdown AudioFactory and close the output device
273        log.info("Shutting down active AudioFactory");
274        InstanceManager.getDefault(jmri.ShutDownManager.class).deregister(audioShutDownTask);
275        if (activeAudioFactory != null) activeAudioFactory.cleanup();
276        // Reset counters
277        countBuffers = 0;
278        countSources = 0;
279        countListeners = 0;
280        // Record that we're no longer initialised
281        initialised = false;
282    }
283
284    @Override
285    public void dispose() {
286        buffers.clear();
287        sources.clear();
288        listeners.clear();
289        super.dispose();
290    }
291
292    @Override
293    public AudioFactory getActiveAudioFactory() {
294        return activeAudioFactory;
295    }
296
297    private static final Logger log = LoggerFactory.getLogger(DefaultAudioManager.class);
298
299}