
com.threerings.openal.OpenALSoundPlayer Maven / Gradle / Ivy
//
// 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.Map;
import java.io.ByteArrayInputStream;
import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.nio.ByteBuffer;
import javax.sound.sampled.AudioFileFormat;
import javax.sound.sampled.AudioFormat;
import javax.sound.sampled.AudioInputStream;
import javax.sound.sampled.AudioSystem;
import org.lwjgl.openal.AL10;
import org.lwjgl.util.WaveData;
import com.google.common.base.Preconditions;
import com.google.common.collect.Maps;
import com.samskivert.util.BasicRunQueue;
import com.samskivert.util.RandomUtil;
import com.samskivert.util.ResultListener;
import com.samskivert.util.RunQueue;
import com.threerings.media.FrameManager;
import com.threerings.media.sound.JavaSoundPlayer;
import com.threerings.media.sound.SoundLoader;
import com.threerings.media.sound.SoundPlayer;
import com.threerings.media.timer.MediaTimer;
import static com.threerings.media.Log.log;
/**
* Implements the abstract pieces of {@link SoundPlayer} via OpenAL.
*/
public class OpenALSoundPlayer extends SoundPlayer
implements ClipProvider
{
public OpenALSoundPlayer (SoundLoader loader)
{
_loader = loader;
try {
_alSoundManager = createSoundManager();
_group = _alSoundManager.createGroup(this, SOURCE_COUNT);
} catch (Throwable t) {
log.warning("Unable to initialize OpenAL", "cause", t);
}
_ticker.start();
}
public Clip loadClip (String path)
throws IOException
{
int bundleEnd = path.lastIndexOf(":");
InputStream sound = _loader.getSound(path.substring(0, bundleEnd),
path.substring(bundleEnd + 1));
if (path.endsWith(".ogg")) {
try {
AudioInputStream instream = JavaSoundPlayer.setupAudioStream(sound);
ByteArrayOutputStream outstream = new ByteArrayOutputStream();
byte[] buf = new byte[16 * 1024];
int read;
do {
read = instream.read(buf, 0, buf.length);
if (read >= 0) {
outstream.write(buf, 0, read);
}
} while (read >= 0);
byte[] audio = outstream.toByteArray();
AudioFormat format = instream.getFormat();
long length = audio.length / format.getFrameSize();
instream = new AudioInputStream(new ByteArrayInputStream(audio), format, length);
outstream = new ByteArrayOutputStream();
AudioSystem.write(instream, AudioFileFormat.Type.WAVE, outstream);
sound = new ByteArrayInputStream(outstream.toByteArray());
} catch (Exception e) {
log.warning("Error decompressing audio clip", "path", path, e);
return new Clip();
}
}
return new Clip(WaveData.create(sound));
}
/**
* Returns the loader used by this player.
*/
public SoundLoader getSoundLoader ()
{
return _loader;
}
@Override
public RunQueue getSoundQueue ()
{
return _ticker;
}
@Override
public void setClipVolume (final float vol)
{
super.setClipVolume(vol);
getSoundQueue().postRunnable(new Runnable() {
public void run () {
_alSoundManager.setBaseGain(vol);
}});
}
@Override
public void lock (String pkgPath, String... keys)
{
for (String key : keys) {
for (final String path : getPaths(pkgPath, key)) {
getSoundQueue().postRunnable(new Runnable() {
public void run () {
if (_locked.containsKey(path)) {
return;
}
_alSoundManager.loadClip(OpenALSoundPlayer.this, path,
new ClipBuffer.Observer() {
public void clipFailed (ClipBuffer buffer) {
log.warning("Unable to load sound", "path", path);
}
public void clipLoaded (ClipBuffer buffer) {
_locked.put(path, buffer);
}
});
}
});
}
}
}
@Override
public void unlock (final String pkgPath, String... keys)
{
for (final String key : keys) {
getSoundQueue().postRunnable(new Runnable() {
public void run () {
for (String path : getPaths(pkgPath, key)) {
_locked.remove(path);
}
}
});
}
}
/**
* Streams ogg files from the given bundle and path.
*/
public void stream (final String bundle, final String path, final boolean loop,
final ResultListener listener)
throws IOException
{
if (!path.endsWith(".ogg")) {
log.warning("Unknown file type for streaming", "bundle", bundle, "path", path);
return;
}
InputStream rsrc = _loader.getSound(bundle, path);
final StreamDecoder dec = new OggStreamDecoder();
dec.init(rsrc);
getSoundQueue().postRunnable(new Runnable() {
public void run () {
Stream s = new Stream(_alSoundManager) {
@Override
protected void update (float time) {
super.update(time);
if (_state != AL10.AL_PLAYING) {
return;
}
super.setGain(_clipVol * _streamGain);
}
@Override
public void setGain (float gain) {
_streamGain = gain;
super.setGain(_clipVol * _streamGain);
}
@Override
protected int getFormat () {
return dec.getFormat();
}
@Override
protected int getFrequency () {
return dec.getFrequency();
}
@Override
protected int populateBuffer (ByteBuffer buf) throws IOException {
int read = dec.read(buf);
if (buf.hasRemaining() && loop) {
dec.init(_loader.getSound(bundle, path));
read = Math.max(0, read);
read += dec.read(buf);
}
return read;
}
protected float _streamGain = 1F;
};
s.setGain(_clipVol);
listener.requestCompleted(s);
}});
}
@Override
public Frob loop (String pkgPath, String key, float pan)
{
return loop(pkgPath, key, pan, 1f);
}
public Frob loop (String pkgPath, String key, float pan, float gain)
{
return loop(null, pkgPath, key, gain, null);
}
public Frob loop (SoundType type, String pkgPath, String key, final float gain,
final float[] pos)
{
if (!shouldPlay(type)) {
return null;
}
final SoundGrabber loader = new SoundGrabber(pkgPath, key) {
@Override
protected void soundLoaded () {
sound.setGain(gain);
if (pos != null) {
sound.setPosition(pos[0], pos[1], pos[2]);
}
sound.loop(true);
}};
getSoundQueue().postRunnable(loader);
return new Frob() {
public float getPan () {
return 0;
}
public float getVolume () {
return 0;
}
public void setPan (float pan) {}
public void setVolume (float vol) {}
public void stop () {
getSoundQueue().postRunnable(new Runnable(){
public void run () {
if (loader.sound != null) {
loader.sound.stop();
}
}});
}};
}
@Override
public void play (String pkgPath, String key, float pan)
{
play(pkgPath, key, pan, 1f);
}
public void play (String pkgPath, String key, float pan, final float gain)
{
play(null, pkgPath, key, gain, null);
}
public boolean play (SoundType type, String pkgPath, String key, final float gain,
final float[] pos)
{
if (!shouldPlay(type)) {
return false;
}
getSoundQueue().postRunnable(new SoundGrabber(pkgPath, key) {
@Override
protected void soundLoaded () {
sound.setGain(gain);
if (pos != null) {
sound.setPosition(pos[0], pos[1], pos[2]);
}
sound.play(true);
}
});
return true;
}
@Override
public void shutdown ()
{
getSoundQueue().postRunnable(new Runnable() {
public void run () {
_group.dispose();
_locked.clear();
for (Stream stream : _alSoundManager.getStreams()) {
stream.dispose();
}
}
});
}
/**
* Returns bundle:path for all sounds under key in pkgPath.
*/
protected String[] getPaths (String pkgPath, String key)
{
String bundle = _loader.getBundle(pkgPath);
Preconditions.checkNotNull(bundle,
"Unable to find the bundle name for a package [package=%s, key=%s]", pkgPath, key);
String[] names = _loader.getPaths(pkgPath, key);
Preconditions.checkNotNull(names, "No such sound [package=%s, key=%s]", pkgPath, key);
String[] paths = new String[names.length];
for (int ii = 0; ii < paths.length; ii++) {
paths[ii] = bundle + ":" + names[ii];
}
return paths;
}
/**
* Creates our SoundManager.
*/
protected SoundManager createSoundManager ()
{
return new MediaALSoundManager();
}
/**
* Extends sound manager to allow sounds to be pulled out of the locked map.
*/
protected class MediaALSoundManager extends SoundManager {
protected MediaALSoundManager () {
super(getSoundQueue());
}
@Override
protected ClipBuffer getClip (ClipProvider provider, String path) {
if (_locked.containsKey(path)) {
return _locked.get(path);
}
return super.getClip(provider, path, null);
}
}
/**
* Updates the sound manager's streams every STREAM_UPDATE_INTERVAL and processes sound
* runnables added to its queue.
*/
protected class TickingQueue extends BasicRunQueue
{
public TickingQueue () {
super("SoundPlayerQueue");
}
@Override
protected void iterate () {
long elapsed = _timer.getElapsedMillis() - _lastTick;
Runnable r;
if (elapsed >= STREAM_UPDATE_INTERVAL) {
r = _queue.getNonBlocking();
} else {
r = _queue.get(STREAM_UPDATE_INTERVAL - elapsed);
}
long newTime;
if (_alSoundManager == null) {
// We weren't able to initialize the sound system, and we logged it earlier, so
// just empty the queue and update the tick time without running the code that
// needs the sound manager.
newTime = _timer.getElapsedMillis();
} else {
if (r != null) {
try {
r.run();
} catch (Throwable t) {
log.warning("Runnable posted to SoundPlayerQueue barfed.", t);
}
}
newTime = _timer.getElapsedMillis();
try {
_alSoundManager.updateStreams((newTime - _lastTick) / 1000F);
} catch (Throwable t) {
log.warning("Updating OpenAL streams barfed.", t);
}
}
_lastTick = newTime;
}
protected MediaTimer _timer = FrameManager.createTimer();
protected long _lastTick;
}
/**
* Loads a sound in its run method and calls subclasses with soundLoaded to let them know it's
* ready.
*/
protected abstract class SoundGrabber
implements Runnable
{
public String path;
public Sound sound;
public SoundGrabber (String pkgPath, String key) {
String[] paths = getPaths(pkgPath, key);
path = paths[RandomUtil.getInt(paths.length)];
}
public void run () {
sound = _group.getSound(path);
soundLoaded();
}
protected abstract void soundLoaded ();
}
/** Number of milliseconds to wait between stream updates. */
protected static final int STREAM_UPDATE_INTERVAL = 100;
protected TickingQueue _ticker = new TickingQueue();
protected Map _locked = Maps.newHashMap();
protected SoundLoader _loader;
protected SoundGroup _group;
protected SoundManager _alSoundManager;
/** Number of sounds that can be played simultaneously. */
protected final int SOURCE_COUNT = 10;
}
© 2015 - 2025 Weber Informatics LLC | Privacy Policy