/* * Copyright (c) 2003-onwards Shaven Puppy Ltd * All rights reserved. * * Redistribution and use in source and binary forms, with or without * modification, are permitted provided that the following conditions are * met: * * * Redistributions of source code must retain the above copyright * notice, this list of conditions and the following disclaimer. * * * Redistributions in binary form must reproduce the above copyright * notice, this list of conditions and the following disclaimer in the * documentation and/or other materials provided with the distribution. * * * Neither the name of 'Shaven Puppy' nor the names of its contributors * may be used to endorse or promote products derived from this software * without specific prior written permission. * * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED * TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR * PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR * CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, * EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, * PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR * PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF * LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING * NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. */ package com.shavenpuppy.jglib.sound; import java.util.*; import org.lwjgl.openal.OpenALException; import com.shavenpuppy.jglib.openal.*; import com.shavenpuppy.jglib.resources.Feature; import com.shavenpuppy.jglib.util.PriorityPool; import static org.lwjgl.openal.AL10.*; /** * The sound player. This queues up commands to play one-shot sound effects using * a limited pool of sources. * * Each sound effect is given a priority. When a sound of higher priority is requested * and there are no spare sources, the oldest, lowest priority sound effect is stopped * and replaced with the new one. * * Every video frame you should call play() which will play your requested sounds. * This syncs it up nicely with the video. */ public class SoundPlayer extends Feature { private static final long serialVersionUID = 1L; private static final boolean DEBUG = false; /** The number of sound sources */ private int sources; /** A Priority Pool which takes care of allocation */ private transient PriorityPool priorityPool; /** The sound effects */ private transient SoundEffect[] fx; /** Streams */ private transient List<ALStreamInstance> streams; /** * Thread for playing streams */ private class StreamThread extends Thread { private boolean finished; StreamThread() { super("Stream Thread"); // Execute at higher priority than the game setPriority(NORM_PRIORITY + 3); setDaemon(true); } /* (non-Javadoc) * @see java.lang.Thread#run() */ @Override public void run() { while (!finished && streams != null) { synchronized (streams) { for (Iterator<ALStreamInstance> i = streams.iterator(); i.hasNext(); ) { ALStreamInstance stream = i.next(); try { if (stream.isCreated()) { stream.tick(); } } catch (Exception e) { i.remove(); } } } try { Thread.sleep(1); // Roughly 1000fps update } catch (InterruptedException e) { } catch (ThreadDeath e) { finished = true; } } } void finish() { finished = true; } } private transient StreamThread streamThread; /** * Construct a sound player that has the specified number of sources. * @param sources The number of sources */ public SoundPlayer(int sources) { this.sources = sources; } /** * @param name */ public SoundPlayer(String name) { super(name); } /** * @see com.shavenpuppy.jglib.openal.ALPlayable#play() */ public void play() { assert isCreated(); priorityPool.tick(); } /** * @see com.shavenpuppy.jglib.Resource#doCreate() */ @Override protected void doCreate() { ArrayList<SoundEffect> tfx = new ArrayList<SoundEffect>(sources); try { for (int i = 0; i < sources; i ++) { SoundEffect se = new SoundEffect(this); se.source.create(); tfx.add(se); } } catch (OpenALException e) { // That's as many as we can have if (tfx.size() < sources) { System.out.println(this+" only managed to create "+tfx.size()+" sources but requested "+sources); } } fx = new SoundEffect[tfx.size()]; tfx.toArray(fx); streams = new LinkedList<ALStreamInstance>(); priorityPool = new PriorityPool(fx); streamThread = new StreamThread(); streamThread.start(); } /** * @see com.shavenpuppy.jglib.Resource#doDestroy() */ @Override protected void doDestroy() { priorityPool.reset(); priorityPool = null; for (int i = 0; i < fx.length; i ++) { fx[i].source.destroy(); fx[i] = null; } synchronized (streams) { streams = null; } streamThread.finish(); streamThread = null; } /** * Allocate a sound effect. The sound effect is plucked from the pool and owned by the * specified owner and returned. If no suitable slot is found then null is returned. * The owner may then subsequently issue commands to the SoundEffect although these * will be ignored if it is no longer the owner of the SoundEffect. * * Don't forget to play() the sound! * * @param buf The sound buffer to play * @param owner The owner of the sound effect * @return a SoundEffect, or null */ public SoundEffect allocate(ALBuffer buf, Object owner) { return allocate(buf, buf.getPriority(), owner); } public SoundEffect allocate(ALBuffer buf, int priority, Object owner) { assert isCreated(); SoundEffect ret = (SoundEffect) priorityPool.allocate(priority, owner); if (ret != null) { // System.out.println(this+"("+hashCode()+") allocated sound "+buf); // We got a sound effect, so initialize it: ret.init(buf); // If there's 2 channels to it, allocate another: if (buf.getFormat() == AL_FORMAT_STEREO16 || buf.getFormat() == AL_FORMAT_STEREO8) { ret.lock(); SoundEffect link = (SoundEffect) priorityPool.allocate(priority, owner); if (link == null || link == ret) { if (DEBUG) { System.out.println("Couldn't get second channel for "+buf); } ret.deactivate(); ret = null; } else { ret.unlock(); ret.setLink(link); } } } return ret; } /** * Allocate a streamed sound effect. The sound effect is plucked from the pool and owned by the * specified owner and returned. If no suitable slot is found then null is returned. * The owner may then subsequently issue commands to the SoundEffect although these * will be ignored if it is no longer the owner of the SoundEffect. * * Don't forget to play() the sound! * * @param buf The stream to play * @param owner The owner of the sound effect * @return a SoundEffect, or null */ public SoundEffect allocate(ALStream buf, Object owner) { return allocate(buf, buf.getPriority(), owner); } public SoundEffect allocate(ALStream buf, int priority, Object owner) { assert isCreated(); SoundEffect ret = (SoundEffect) priorityPool.allocate(priority, owner); if (ret != null) { // We got a sound effect, so initialize it: try { ret.init(buf); } catch (Exception e) { e.printStackTrace(System.err); ret.deactivate(); ret = null; } // If it's a stereo stream we need to attempt to allocate another source and fail // if we can't get one: if (ret != null && (buf.getFormat() == AL_FORMAT_STEREO16 || buf.getFormat() == AL_FORMAT_STEREO8)) { ret.lock(); SoundEffect link = (SoundEffect) priorityPool.allocate(priority, owner); if (link == null || link == ret) { ret.deactivate(); ret = null; } else { ret.unlock(); ret.setLink(link); } } } return ret; } /** * Register a stream so that it will be decoded in the stream thread */ void registerStream(ALStreamInstance stream) { synchronized (streams) { if (!streams.contains(stream)) { streams.add(stream); } } } /** * Deregister a stream */ void deregisterStream(ALStreamInstance stream) { synchronized (streams) { streams.remove(stream); } } }