
com.threerings.openal.SoundManager Maven / Gradle / Ivy
Show all versions of nenya Show documentation
//
// Nenya library - tools for developing networked games
// Copyright (C) 2002-2012 Three Rings Design, Inc., All Rights Reserved
// https://github.com/threerings/nenya
//
// This library is free software; you can redistribute it and/or modify it
// under the terms of the GNU Lesser General Public License as published
// by the Free Software Foundation; either version 2.1 of the License, or
// (at your option) any later version.
//
// This library is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
// Lesser General Public License for more details.
//
// You should have received a copy of the GNU Lesser General Public
// License along with this library; if not, write to the Free Software
// Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
package com.threerings.openal;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.nio.IntBuffer;
import org.lwjgl.BufferUtils;
import org.lwjgl.openal.AL;
import org.lwjgl.openal.AL10;
import com.google.common.collect.Lists;
import com.google.common.collect.Maps;
import com.samskivert.util.IntListUtil;
import com.samskivert.util.LRUHashMap;
import com.samskivert.util.Queue;
import com.samskivert.util.RunQueue;
import com.threerings.openal.ClipBuffer.Observer;
import static com.threerings.openal.Log.log;
/**
* An interface to the OpenAL library that provides a number of additional services:
*
*
* - an object oriented interface to the OpenAL system
*
- a mechanism for loading a group of sounds and freeing their resources all at once
*
- a mechanism for loading sounds in a background thread and preloading sounds that are likely
* to be needed soon
*
*
* Note: the sound manager is not thread safe (other than during its interactions with
* its internal background loading thread). It assumes that all sound loading and play requests
* will be made from a single thread.
*/
public class SoundManager
{
/**
* Creates, initializes and returns the singleton sound manager instance.
*
* @param rqueue a queue that the sound manager can use to post short runnables that must be
* executed on the same thread from which all other sound methods will be called.
*/
public static SoundManager createSoundManager (RunQueue rqueue)
{
if (_soundmgr != null) {
throw new IllegalStateException("A sound manager has already been created.");
}
_soundmgr = new SoundManager(rqueue);
return _soundmgr;
}
/**
* Shuts down the sound manager.
*/
public void shutdown ()
{
if (isInitialized()) {
AL.destroy();
}
}
/**
* Returns true if we were able to initialize the sound system.
*/
public boolean isInitialized ()
{
return (_toLoad != null);
}
/**
* Configures the size of our sound cache. If this value is larger than memory available to the
* underlying sound system, it will be reduced when OpenAL first tells us we're out of memory.
*/
public void setCacheSize (int bytes)
{
_clips.setMaxSize(bytes);
}
/**
* Returns a reference to the listener object.
*/
public Listener getListener ()
{
return _listener;
}
/**
* Configures the base gain (which must be a value between 0 and 1.0) which is multiplied to
* the individual gain assigned to sound effects (but not music).
*/
public void setBaseGain (float gain)
{
if (_baseGain == gain) {
return;
}
_baseGain = gain;
// alert the groups that inherite the gain
for (int ii = 0, nn = _groups.size(); ii < nn; ii++) {
SoundGroup group = _groups.get(ii);
if (group.getBaseGain() < 0f) {
group.baseGainChanged();
}
}
}
/**
* Returns the base gain used for sound effects (not music).
*/
public float getBaseGain ()
{
return _baseGain;
}
/**
* Creates an object that can be used to manage and play a group of sounds. Note: the
* sound group must be disposed when it is no longer needed via a call to {@link
* SoundGroup#dispose}.
*
* @param provider indicates from where the sound group will load its sounds.
* @param sources indicates the maximum number of simultaneous sounds that can play in this
* group.
*/
public SoundGroup createGroup (ClipProvider provider, int sources)
{
return new SoundGroup(this, provider, sources);
}
/**
* Returns a reference to the list of active streams.
*/
public ArrayList getStreams ()
{
return _streams;
}
/**
* Updates all of the streams controlled by the manager. This should be called once per frame
* by the application.
*
* @param time the number of seconds elapsed since the last update
*/
public void updateStreams (float time)
{
// iterate backwards through the list so that streams can dispose of themselves during
// their update
for (int ii = _streams.size() - 1; ii >= 0; ii--) {
_streams.get(ii).update(time);
}
// delete any finalized objects
deleteFinalizedObjects();
}
/**
* Loads a clip buffer for the sound clip loaded via the specified provider with the
* specified path. The loaded clip is placed in the cache.
*/
public void loadClip (ClipProvider provider, String path)
{
loadClip(provider, path, null);
}
/**
* Loads a clip buffer for the sound clip loaded via the specified provider with the
* specified path. The loaded clip is placed in the cache.
*/
public void loadClip (ClipProvider provider, String path, Observer observer)
{
getClip(provider, path, observer);
}
/**
* Creates a sound manager and initializes the OpenAL sound subsystem.
*/
protected SoundManager (RunQueue rqueue)
{
_rqueue = rqueue;
// initialize the OpenAL sound system
try {
AL.create("", 44100, 15, false);
} catch (Exception e) {
log.warning("Failed to initialize sound system.", e);
// don't start the background loading thread
return;
}
int errno = AL10.alGetError();
if (errno != AL10.AL_NO_ERROR) {
log.warning("Failed to initialize sound system [errno=" + errno + "].");
// don't start the background loading thread
return;
}
// configure our LRU map with a removal observer
_clips.setRemovalObserver(new LRUHashMap.RemovalObserver() {
public void removedFromMap (LRUHashMap map,
final ClipBuffer item) {
_rqueue.postRunnable(new Runnable() {
public void run () {
log.debug("Flushing " + item.getKey());
item.dispose();
}
});
}
});
// create our loading queue
_toLoad = new Queue();
// start up the background loader thread
_loader.setDaemon(true);
_loader.start();
}
/**
* Creates a clip buffer for the sound clip loaded via the specified provider with the
* specified path. The clip buffer may come from the cache, and it will immediately be queued
* for loading if it is not already loaded.
*/
protected ClipBuffer getClip (ClipProvider provider, String path)
{
return getClip(provider, path, null);
}
/**
* Creates a clip buffer for the sound clip loaded via the specified provider with the
* specified path. The clip buffer may come from the cache, and it will immediately be queued
* for loading if it is not already loaded.
*/
protected ClipBuffer getClip (ClipProvider provider, String path, Observer observer)
{
String ckey = ClipBuffer.makeKey(provider, path);
ClipBuffer buffer = _clips.get(ckey);
try {
if (buffer == null) {
// check to see if this clip is currently loading
buffer = _loading.get(ckey);
if (buffer == null) {
buffer = new ClipBuffer(this, provider, path);
_loading.put(ckey, buffer);
}
}
buffer.resolve(observer);
return buffer;
} catch (Throwable t) {
log.warning("Failure resolving buffer [key=" + ckey + "].", t);
return null;
}
}
/**
* Queues the supplied clip buffer up for resolution. The {@link Clip} will be loaded into
* memory and then bound into OpenAL on the background thread.
*/
protected void queueClipLoad (ClipBuffer buffer)
{
if (_toLoad != null) {
_toLoad.append(buffer);
}
}
/**
* Queues the supplied clip buffer up using our {@link RunQueue} to notify its observers that
* it failed to load.
*/
protected void queueClipFailure (final ClipBuffer buffer)
{
_rqueue.postRunnable(new Runnable() {
public void run () {
_loading.remove(buffer.getKey());
buffer.failed();
}
});
}
/**
* Adds the supplied clip buffer back to the cache after it has been marked for disposal and
* subsequently re-requested.
*/
protected void restoreClip (ClipBuffer buffer)
{
_clips.put(buffer.getKey(), buffer);
}
/**
* Adds a stream to the list maintained by the manager. Called by streams when they are
* created.
*/
protected void addStream (Stream stream)
{
_streams.add(stream);
}
/**
* Removes a stream from the list maintained by the manager. Called by streams when they are
* disposed.
*/
protected void removeStream (Stream stream)
{
_streams.remove(stream);
}
/**
* Adds a group to the list maintained by the manager. Called by groups when they are created.
*/
protected void addGroup (SoundGroup group)
{
_groups.add(group);
}
/**
* Removes a group from the list maintained by the manager. Called by groups when they are
* disposed.
*/
protected void removeGroup (SoundGroup group)
{
_groups.remove(group);
}
/**
* Called when a source has been finalized.
*/
protected synchronized void sourceFinalized (int id)
{
_finalizedSources = IntListUtil.add(_finalizedSources, id);
}
/**
* Called when a buffer has been finalized.
*/
protected synchronized void bufferFinalized (int id)
{
_finalizedBuffers = IntListUtil.add(_finalizedBuffers, id);
}
/**
* Deletes all finalized objects.
*/
protected synchronized void deleteFinalizedObjects ()
{
if (_finalizedSources != null) {
IntBuffer idbuf = BufferUtils.createIntBuffer(_finalizedSources.length);
idbuf.put(_finalizedSources).rewind();
AL10.alDeleteSources(idbuf);
_finalizedSources = null;
}
if (_finalizedBuffers != null) {
IntBuffer idbuf = BufferUtils.createIntBuffer(_finalizedBuffers.length);
idbuf.put(_finalizedBuffers).rewind();
AL10.alDeleteBuffers(idbuf);
_finalizedBuffers = null;
}
}
/** The thread that loads up sound clips in the background. */
protected Thread _loader = new Thread("SoundManager.Loader") {
@Override
public void run () {
while (true) {
final ClipBuffer buffer = _toLoad.get();
try {
log.debug("Loading " + buffer.getKey() + ".");
final Clip clip = buffer.load();
_rqueue.postRunnable(new Runnable() {
public void run () {
String ckey = buffer.getKey();
log.debug("Loaded " + ckey + ".");
_loading.remove(ckey);
if (buffer.bind(clip)) {
_clips.put(ckey, buffer);
} else {
// TODO: shrink the cache size if the bind failed due to
// OUT_OF_MEMORY
}
}
});
} catch (Throwable t) {
log.warning("Failed to load clip [key=" + buffer.getKey() + "].", t);
// let the clip and its observers know that we are a miserable failure
queueClipFailure(buffer);
}
}
}
};
/** Used to get back from the background thread to our "main" thread. */
protected RunQueue _rqueue;
/** The listener object. */
protected Listener _listener = new Listener();
/** A base gain that is multiplied by the individual gain assigned to sounds. */
protected float _baseGain = 1;
/** Contains a mapping of all currently-loading clips. */
protected HashMap _loading = Maps.newHashMap();
/** Contains a mapping of all loaded clips. */
protected LRUHashMap _clips =
new LRUHashMap(DEFAULT_CACHE_SIZE, _sizer);
/** Contains a queue of clip buffers waiting to be loaded. */
protected Queue _toLoad;
/** The list of active streams. */
protected ArrayList _streams = Lists.newArrayList();
/** The list of active groups. */
protected List _groups = Lists.newArrayList();
/** The list of sources to be deleted. */
protected int[] _finalizedSources;
/** The list of buffers to be deleted. */
protected int[] _finalizedBuffers;
/** The one and only sound manager, here for an exclusive performance by special request.
* Available for all your sound playing needs. */
protected static SoundManager _soundmgr;
/** Used to compute the in-memory size of sound samples. */
protected static LRUHashMap.ItemSizer _sizer =
new LRUHashMap.ItemSizer() {
public int computeSize (ClipBuffer item) {
return item.getSize();
}
};
/** Default to a cache size of one megabyte. */
protected static final int DEFAULT_CACHE_SIZE = 8 * 1024 * 1024;
}