diff --git a/src/java/com/threerings/media/sound/MusicManager.java b/src/java/com/threerings/media/sound/MusicManager.java new file mode 100644 index 000000000..4c6b708bc --- /dev/null +++ b/src/java/com/threerings/media/sound/MusicManager.java @@ -0,0 +1,354 @@ +// +// $Id: SoundManager.java 3290 2004-12-29 21:56:58Z ray $ +// +// Narya library - tools for developing networked games +// Copyright (C) 2002-2004 Three Rings Design, Inc., All Rights Reserved +// http://www.threerings.net/code/narya/ +// +// 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.media.sound; + +import java.util.Iterator; +import java.util.LinkedList; + +import com.samskivert.util.Config; +import com.samskivert.util.RunQueue; + +import com.threerings.util.RandomUtil; + +import com.threerings.media.Log; + +/** + * Manages the playing of audio files. + */ +// TODO: +// - fade music out when stopped? +// - be able to pause music +public class MusicManager +{ + /** + * Constructs a music manager. + * + * @param smgr The soundManager we work with. + * @param runQueue the client event run queue. + * + */ + public MusicManager (SoundManager smgr, RunQueue runQueue) + { + _smgr = smgr; + _runQueue = runQueue; + } + + /** + * Shut the damn thing off. + */ + public void shutdown () + { + _musicStack.clear(); + stopMusicPlayer(); + } + + /** + * Returns a string summarizing our volume settings and disabled sound + * types. + */ + public String summarizeState () + { + StringBuffer buf = new StringBuffer(); + buf.append("musicVol=").append(_musicVol); + return buf.append("]").toString(); + } + + /** + * Sets the volume for music. + * + * @param val a volume parameter between 0f and 1f, inclusive. + */ + public void setMusicVolume (float vol) + { + float oldvol = _musicVol; + _musicVol = Math.max(0f, Math.min(1f, vol)); + if (_musicPlayer != null) { + _musicPlayer.setVolume(_musicVol); + } + + if ((oldvol == 0f) && (_musicVol != 0f)) { + playTopMusic(); + } else if ((oldvol != 0f) && (_musicVol == 0f)) { + stopMusicPlayer(); + } + } + + /** + * Get the music volume. + */ + public float getMusicVolume () + { + return _musicVol; + } + + /** + * Start playing the specified music repeatedly. + */ + public void pushMusic (String pkgPath, String key) + { + pushMusic(pkgPath, key, -1); + } + + /** + * Start playing music for the specified number of loops. If no other + * music is pushed, the specified music will play for the number of loops + * specified, and will then be popped off the stack. + */ + public void pushMusic (String pkgPath, String key, int numloops) + { + MusicKey mkey = new MusicKey(pkgPath, key, numloops); + + // stop any existing playing music + if (_musicPlayer != null) { + _musicPlayer.stop(); + handleMusicStopped(); + } + + // add the new song + _musicStack.addFirst(mkey); + + // and play it + playTopMusic(); + } + + /** + * Remove the specified music from the playlist. If it is currently + * playing, it will be stopped and the previous song will be started. + */ + public void removeMusic (String pkgPath, String key) + { + MusicKey mkey = new MusicKey(pkgPath, key, -1); + + if (!_musicStack.isEmpty()) { + MusicKey current = (MusicKey) _musicStack.getFirst(); + + // if we're currently playing this song.. + if (mkey.equals(current)) { + // stop it + if (_musicPlayer != null) { + _musicPlayer.stop(); + } + + // remove it from the stack + _musicStack.removeFirst(); + // start playing the next.. + playTopMusic(); + return; + + } else { + // we aren't currently playing this song. Simply remove. + for (Iterator iter=_musicStack.iterator(); iter.hasNext(); ) { + if (key.equals(iter.next())) { + iter.remove(); + return; + } + } + + } + } + + Log.debug("Sequence stopped that wasn't in the stack anymore " + + "[key=" + mkey + "]."); + } + + /** + * Start the specified sequence. + */ + protected void playTopMusic () + { + if (_musicStack.isEmpty()) { + return; + } + + // if the volume is off, we don't actually want to play anything + // but we want to at least decrement any loopers by one + // and keep them on the top of the queue + if (_musicVol == 0f) { + handleMusicStopped(); + return; + } + + MusicKey info = (MusicKey) _musicStack.getFirst(); + + Config c = _smgr.getConfig(info); + String[] names = c.getValue(info.key, (String[])null); + if ((names == null) || (names.length == 0)) { + Log.warning("No such music [key=" + info + "]."); + _musicStack.removeFirst(); + playTopMusic(); + return; + } + String music = names[RandomUtil.getInt(names.length)]; + + Class playerClass = getMusicPlayerClass(music); + + // if we don't have a player for this song, play the next song + if (playerClass == null) { + _musicStack.removeFirst(); + playTopMusic(); + return; + } + + // shutdown the old player if we're switching music types + if (! playerClass.isInstance(_musicPlayer)) { + if (_musicPlayer != null) { + _musicPlayer.shutdown(); + } + + // set up the new player + try { + _musicPlayer = (MusicPlayer) playerClass.newInstance(); + _musicPlayer.init(_playerListener); + + } catch (Exception e) { + Log.warning("Unable to instantiate music player [class=" + + playerClass + ", e=" + e + "]."); + + // scrap it, try again with the next song + _musicPlayer = null; + _musicStack.removeFirst(); + playTopMusic(); + return; + } + + _musicPlayer.setVolume(_musicVol); + } + + // play! + String bundle = c.getValue("bundle", (String)null); + try { + // TODO: buffer for the music player? + _musicPlayer.start(_smgr._rmgr.getResource(bundle, music)); + } catch (Exception e) { + Log.warning("Error playing music, skipping [e=" + e + + ", bundle=" + bundle + ", music=" + music + "]."); + _musicStack.removeFirst(); + playTopMusic(); + return; + } + } + + /** + * Get the appropriate music player for the specified music file. + */ + protected static Class getMusicPlayerClass (String path) + { + path = path.toLowerCase(); + +// if (path.endsWith(".mid") || path.endsWith(".rmf")) { +// return MidiPlayer.class; + +// } else if (path.endsWith(".mod")) { +// return ModPlayer.class; + +// } else if (path.endsWith(".mp3")) { +// return Mp3Player.class; + +// } else if (path.endsWith(".ogg")) { +// return OggPlayer.class; + +// } else { + return null; +// } + } + + /** + * Stop whatever song is currently playing and deal with the + * MusicKey associated with it. + */ + protected void handleMusicStopped () + { + if (_musicStack.isEmpty()) { + return; + } + + // see what was playing + MusicKey current = (MusicKey) _musicStack.getFirst(); + + // see how many times the song was to loop and act accordingly + switch (current.loops) { + default: + current.loops--; + break; + + case 1: + // sorry charlie + _musicStack.removeFirst(); + break; + } + } + + /** + * Stop the current music player. + */ + protected void stopMusicPlayer () + { + if (_musicPlayer != null) { + _musicPlayer.stop(); + _musicPlayer.shutdown(); + _musicPlayer = null; + } + } + + /** + * A class that tracks the information about our playing music files. + */ + protected static class MusicKey extends SoundManager.SoundKey + { + /** How many times to loop, or -1 for forever. */ + public int loops; + + public MusicKey (String set, String path, int loops) + { + super((byte) -1, set, path); + this.loops = loops; + } + } + + /** The sound manager we work with. */ + protected SoundManager _smgr; + + /** The client event run queue. */ + protected RunQueue _runQueue; + + /** Volume level for music. */ + protected float _musicVol = 1f; + + /** The stack of songs that we're playing. */ + protected LinkedList _musicStack = new LinkedList(); + + /** The current music player, if any. */ + protected MusicPlayer _musicPlayer; + + /** Event listener for receiving information about a song ending. */ + protected MusicPlayer.MusicEventListener _playerListener = + new MusicPlayer.MusicEventListener() { + public void musicStopped () { + _runQueue.postRunnable(new Runnable() { + public void run() { + handleMusicStopped(); + playTopMusic(); + } + }); + } + }; +} diff --git a/src/java/com/threerings/media/sound/SoundManager.java b/src/java/com/threerings/media/sound/SoundManager.java index 4d9f6a69c..0a483e0f4 100644 --- a/src/java/com/threerings/media/sound/SoundManager.java +++ b/src/java/com/threerings/media/sound/SoundManager.java @@ -29,11 +29,9 @@ import java.io.FilenameFilter; import java.io.IOException; import java.io.InputStream; -import java.util.ArrayList; import java.util.HashMap; import java.util.HashSet; import java.util.Iterator; -import java.util.LinkedList; import javax.sound.sampled.AudioFormat; import javax.sound.sampled.AudioInputStream; @@ -63,11 +61,7 @@ import com.threerings.media.MediaPrefs; /** * Manages the playing of audio files. */ -// TODO: -// - fade music out when stopped? -// - be able to pause music public class SoundManager - implements MusicPlayer.MusicEventListener { /** * Create instances of this for your application to differentiate @@ -112,89 +106,6 @@ public class SoundManager _rmgr = rmgr; _defaultClipBundle = defaultClipBundle; _defaultClipPath = defaultClipPath; - - if (!SOUND_ENABLED) return; - - // create a thread to plays sounds and load sound - // data from the resource manager - _player = new Thread("narya SoundManager") { - public void run () { - Object command = null; - SoundKey key = null; - MusicInfo musicInfo = null; - - while (amRunning()) { - // Get the next command and arguments - synchronized (_queue) { - command = _queue.get(); - - // some commands have an additional argument. - if ((PLAY == command) || - (STOPMUSIC == command) || - (LOCK == command) || - (UNLOCK == command)) { - key = (SoundKey) _queue.get(); - - } else if (PLAYMUSIC == command) { - musicInfo = (MusicInfo) _queue.get(); - } - } - - try { - // execute the command outside of the queue synch - if (PLAY == command) { - playSound(key); - - } else if (PLAYMUSIC == command) { - playMusic(musicInfo); - - } else if (STOPMUSIC == command) { - stopMusic(key); - - } else if (LOCK == command) { - if (!isTesting()) { - try { - getClipData(key); // preload - // copy the cached sound into the lock map - _lockedClips.put(key, _clipCache.get(key)); - } catch (Exception e) { - // don't whine about LOCK failures - // unless we have verbose logging on. - if (_verbose.getValue()) { - throw e; - } - } - } - - } else if (UNLOCK == command) { - _lockedClips.remove(key); - - } else if (UPDATE_MUSIC_VOL == command) { - updateMusicVolume(); - - } else if (DIE == command) { - LineSpooler.shutdown(); - shutdownMusic(); - _player = null; - - } else { - Log.warning("Got unknown command [cmd=" + command + - ", key=" + key + - ", info=" + musicInfo + "]."); - } - - } catch (Exception e) { - Log.warning("SoundManager failure [cmd=" + command + - ", key=" + key + - ", info=" + musicInfo + "]."); - Log.logStackTrace(e); - } - } - Log.debug("SoundManager exit."); - } - }; - - _player.start(); } /** @@ -204,17 +115,13 @@ public class SoundManager { synchronized (_queue) { _queue.clear(); - _queue.append(DIE); // signal death + if (_spoolerCount > 0) { + _queue.append(new SoundKey(DIE)); // signal death + } + } + synchronized (_clipCache) { + _lockedClips.clear(); } - } - - /** - * Used by the sound playing thread to determine whether or not to - * shut down. - */ - protected boolean amRunning () - { - return (_player == Thread.currentThread()); } /** @@ -224,7 +131,6 @@ public class SoundManager public String summarizeState () { StringBuffer buf = new StringBuffer(); - buf.append("musicVol=").append(_musicVol); buf.append(", clipVol=").append(_clipVol); buf.append(", disabled=["); int ii = 0; @@ -276,46 +182,13 @@ public class SoundManager return _clipVol; } - /** - * Sets the volume for music. - * - * @param val a volume parameter between 0f and 1f, inclusive. - */ - public void setMusicVolume (float vol) - { - float oldvol = _musicVol; - _musicVol = Math.max(0f, Math.min(1f, vol)); - - if ((oldvol == 0f) && (_musicVol != 0f)) { - _musicAction = START; - } else if ((oldvol != 0f) && (_musicVol == 0f)) { - _musicAction = STOP; - } else { - _musicAction = NONE; - } - _queue.append(UPDATE_MUSIC_VOL); - } - - /** - * Get the music volume. - */ - public float getMusicVolume () - { - return _musicVol; - } - /** * Optionally lock the sound data prior to playing, to guarantee * that it will be quickly available for playing. */ public void lock (String pkgPath, String key) { - if (!SOUND_ENABLED) return; - - synchronized (_queue) { - _queue.append(LOCK); - _queue.append(new SoundKey(pkgPath, key)); - } + enqueue(new SoundKey(LOCK, pkgPath, key), true); } /** @@ -323,12 +196,7 @@ public class SoundManager */ public void unlock (String pkgPath, String key) { - if (!SOUND_ENABLED) return; - - synchronized (_queue) { - _queue.append(UNLOCK); - _queue.append(new SoundKey(pkgPath, key)); - } + enqueue(new SoundKey(UNLOCK, pkgPath, key), true); } /** @@ -337,7 +205,7 @@ public class SoundManager public void lock (String pkgPath, String[] keys) { for (int ii=0; ii < keys.length; ii++) { - lock(pkgPath, keys[ii]); + enqueue(new SoundKey(LOCK, pkgPath, keys[ii]), (ii == 0)); } } @@ -347,7 +215,7 @@ public class SoundManager public void unlock (String pkgPath, String[] keys) { for (int ii=0; ii < keys.length; ii++) { - unlock(pkgPath, keys[ii]); + enqueue(new SoundKey(UNLOCK, pkgPath, keys[ii]), (ii == 0)); } } @@ -366,15 +234,12 @@ public class SoundManager */ public void play (SoundType type, String pkgPath, String key, int delay) { - if (!SOUND_ENABLED) return; - if (type == null) { - // let the lazy kids play too - type = DEFAULT; + type = DEFAULT; // let the lazy kids play too } - if (_player != null && (_clipVol != 0f) && isEnabled(type)) { - final SoundKey skey = new SoundKey(pkgPath, key, delay); + if ((_clipVol != 0f) && isEnabled(type)) { + final SoundKey skey = new SoundKey(PLAY, pkgPath, key, delay); if (delay > 0) { new Interval() { public void expired () { @@ -392,83 +257,191 @@ public class SoundManager */ protected void addToPlayQueue (SoundKey skey) { - synchronized (_queue) { - if (_queue.size() < MAX_QUEUE_SIZE) { - if (_verbose.getValue()) { - Log.info("Sound request [key=" + skey.key + "]."); - } - _queue.append(PLAY); - _queue.append(skey); + boolean queued = enqueue(skey, true); + if (queued) { + if (_verbose.getValue()) { + Log.info("Sound request [key=" + skey.key + "]."); + } - } else /* if (_verbose.getValue()) */ { - Log.warning("SoundManager not playing sound because " + - "too many sounds in queue [key=" + skey + - ", queue=" + _queue + "]."); + } else /* if (_verbose.getValue()) */ { + Log.warning("SoundManager not playing sound because " + + "too many sounds in queue [key=" + skey + "]."); + } + } + + /** + * Enqueue a new SoundKey. + */ + protected boolean enqueue (SoundKey key, boolean okToStartNew) + { + boolean add; + boolean queued; + synchronized (_queue) { + if (key.cmd == PLAY && _queue.size() > MAX_QUEUE_SIZE) { + queued = add = false; + } else { + _queue.append(key); + queued = true; + add = okToStartNew && (_freeSpoolers == 0) && + (_spoolerCount < MAX_SPOOLERS); + if (add) { + _spoolerCount++; + } + } + } + + // and if we need a new thread, add it + if (add) { + new Thread("narya SoundManager line spooler") { + public void run () { + spoolerRun(); + } + }.start(); + } + + return queued; + } + + /** + * This is the primary run method of the sound-playing threads. + */ + protected void spoolerRun () + { + while (true) { + try { + SoundKey key; + synchronized (_queue) { + _freeSpoolers++; + key = (SoundKey) _queue.get(MAX_WAIT_TIME); + _freeSpoolers--; + + if (key == null || key.cmd == DIE) { + _spoolerCount--; + // if dieing and there are others to kill, do so + if (key != null && _spoolerCount > 0) { + _queue.append(key); + } + return; + } + } + + // process the command + processKey(key); + + } catch (Exception e) { + Log.logStackTrace(e); } } } /** - * Start playing the specified music repeatedly. + * Process the requested command in the specified SoundKey. */ - public void pushMusic (String pkgPath, String key) + protected void processKey (SoundKey key) + throws Exception { - pushMusic(pkgPath, key, -1); - } + switch (key.cmd) { + case PLAY: + playSound(key); + break; - /** - * Start playing music for the specified number of loops. - */ - public void pushMusic (String pkgPath, String key, int numloops) - { - if (!SOUND_ENABLED) { - return; - } + case LOCK: + if (!isTesting()) { + synchronized (_clipCache) { + try { + getClipData(key); // preload + // copy cached to lock map + _lockedClips.put(key, _clipCache.get(key)); + } catch (Exception e) { + // don't whine about LOCK failures unless + // we are verbosely logging + if (_verbose.getValue()) { + throw e; + } + } + } + } + break; - synchronized (_queue) { - _queue.append(PLAYMUSIC); - _queue.append(new MusicInfo(pkgPath, key, numloops)); + case UNLOCK: + synchronized (_clipCache) { + _lockedClips.remove(key); + } + break; } } /** - * Remove the specified music from the playlist. If it is currently - * playing, it will be stopped and the previous song will be started. - */ - public void removeMusic (String pkgPath, String key) - { - synchronized (_queue) { - _queue.append(STOPMUSIC); - _queue.append(new SoundKey(pkgPath, key)); - } - } - - /** - * On the SoundManager thread, - * plays the sound file with the given pathname. + * On a spooling thread, */ protected void playSound (SoundKey key) { - if (key.isExpired()) { - if (_verbose.getValue()) { - Log.info("Sound expired [key=" + key.key + "]."); - } - return; - } - + SourceDataLine line = null; try { // get the sound data from our LRU cache byte[] data = getClipData(key); if (data == null) { return; // borked! + + } else if (key.isExpired()) { + if (_verbose.getValue()) { + Log.info("Sound expired [key=" + key.key + "]."); + } + return; } AudioInputStream stream = AudioSystem.getAudioInputStream( new ByteArrayInputStream(data)); + AudioFormat format = stream.getFormat(); - LineSpooler.play(stream, _clipVol, key); + // open the sound line + line = (SourceDataLine) AudioSystem.getLine( + new DataLine.Info(SourceDataLine.class, format)); + line.open(format, LINEBUF_SIZE); + line.start(); + adjustVolume(line, _clipVol); _soundSeemsToWork = true; + // play the sound + byte[] buffer = new byte[LINEBUF_SIZE]; + int count = 0; + while (count != -1) { + try { + count = stream.read(buffer, 0, buffer.length); + } catch (IOException e) { + // this shouldn't ever ever happen because the stream + // we're given is from a reliable source + Log.warning("Error reading clip data! [e=" + e + "]."); + return; + } + + if (count >= 0) { + line.write(buffer, 0, count); + } + } + + // sleep the drain time. We never trust line.drain() because + // it is buggy and locks up on natively multithreaded systems + // (linux, winXP with HT). + float sampleRate = format.getSampleRate(); + if (sampleRate == AudioSystem.NOT_SPECIFIED) { + sampleRate = 11025; // most of our sounds are + } + int sampleSize = format.getSampleSizeInBits(); + if (sampleSize == AudioSystem.NOT_SPECIFIED) { + sampleSize = 16; + } + int drainTime = (int) Math.ceil( + (LINEBUF_SIZE * 8 * 1000) / (sampleRate * sampleSize)); + + // add in a fudge factor of half a second + drainTime += 500; + + try { + Thread.sleep(drainTime); + } catch (InterruptedException ie) { + } + } catch (IOException ioe) { Log.warning("Error loading sound file [key=" + key + ", e=" + ioe + "]."); @@ -488,237 +461,11 @@ public class SoundManager // it to ourselves Log.debug(err); } - } - } - /** - * Play a song from the specified path. - */ - protected void playMusic (MusicInfo info) - { - // stop whatever's currently playing - if (_musicPlayer != null) { - _musicPlayer.stop(); - handleMusicStopped(); - } - - // add the new song - _musicStack.addFirst(info); - - // and play it - playTopMusic(); - } - - /** - * Start the specified sequence. - */ - protected void playTopMusic () - { - if (_musicStack.isEmpty()) { - return; - } - - // if the volume is off, we don't actually want to play anything - // but we want to at least decrement any loopers by one - // and keep them on the top of the queue - if (_musicVol == 0f) { - handleMusicStopped(); - return; - } - - MusicInfo info = (MusicInfo) _musicStack.getFirst(); - - Config c = getConfig(info); - String[] names = c.getValue(info.key, (String[])null); - if ((names == null) || (names.length == 0)) { - Log.warning("No such music [key=" + info + "]."); - _musicStack.removeFirst(); - playTopMusic(); - return; - } - String music = names[RandomUtil.getInt(names.length)]; - - Class playerClass = getMusicPlayerClass(music); - - // if we don't have a player for this song, play the next song - if (playerClass == null) { - _musicStack.removeFirst(); - playTopMusic(); - return; - } - - // shutdown the old player if we're switching music types - if (! playerClass.isInstance(_musicPlayer)) { - if (_musicPlayer != null) { - _musicPlayer.shutdown(); + } finally { + if (line != null) { + line.close(); } - - // set up the new player - try { - _musicPlayer = (MusicPlayer) playerClass.newInstance(); - _musicPlayer.init(this); - - } catch (Exception e) { - Log.warning("Unable to instantiate music player [class=" + - playerClass + ", e=" + e + "]."); - - // scrap it, try again with the next song - _musicPlayer = null; - _musicStack.removeFirst(); - playTopMusic(); - return; - } - - _musicPlayer.setVolume(_musicVol); - } - - // play! - String bundle = c.getValue("bundle", (String)null); - try { - // TODO: buffer for the music player? - _musicPlayer.start(_rmgr.getResource(bundle, music)); - } catch (Exception e) { - Log.warning("Error playing music, skipping [e=" + e + - ", bundle=" + bundle + ", music=" + music + "]."); - _musicStack.removeFirst(); - playTopMusic(); - return; - } - } - - /** - * Get the appropriate music player for the specified music file. - */ - protected static Class getMusicPlayerClass (String path) - { - path = path.toLowerCase(); - -// if (path.endsWith(".mid") || path.endsWith(".rmf")) { -// return MidiPlayer.class; - -// } else if (path.endsWith(".mod")) { -// return ModPlayer.class; - -// } else if (path.endsWith(".mp3")) { -// return Mp3Player.class; - -// } else if (path.endsWith(".ogg")) { -// return OggPlayer.class; - -// } else { - return null; -// } - } - - /** - * Stop whatever song is currently playing and deal with the - * MusicInfo associated with it. - */ - protected void handleMusicStopped () - { - if (_musicStack.isEmpty()) { - return; - } - - // see what was playing - MusicInfo current = (MusicInfo) _musicStack.getFirst(); - - // see how many times the song was to loop and act accordingly - switch (current.loops) { - default: - current.loops--; - break; - - case 1: - // sorry charlie - _musicStack.removeFirst(); - break; - } - } - - /** - * Stop the sequence at the specified path. - */ - protected void stopMusic (SoundKey key) - { - if (! _musicStack.isEmpty()) { - MusicInfo current = (MusicInfo) _musicStack.getFirst(); - - // if we're currently playing this song.. - if (key.equals(current)) { - // stop it - if (_musicPlayer != null) { - _musicPlayer.stop(); - } - - // remove it from the stack - _musicStack.removeFirst(); - // start playing the next.. - playTopMusic(); - return; - - } else { - // we aren't currently playing this song. Simply remove. - for (Iterator iter=_musicStack.iterator(); iter.hasNext(); ) { - if (key.equals(iter.next())) { - iter.remove(); - return; - } - } - - } - } - - Log.debug("Sequence stopped that wasn't in the stack anymore " + - "[key=" + key + "]."); - } - - /** - * Attempt to modify the music volume for any playing tracks. - * - * @param start - */ - protected void updateMusicVolume () - { - if (_musicPlayer != null) { - _musicPlayer.setVolume(_musicVol); - } - switch (_musicAction) { - case START: - playTopMusic(); - break; - - case STOP: - stopMusicPlayer(); - break; - } - } - - // documentation inherited from interface MusicPlayer.MusicEventListener - public void musicStopped () - { - handleMusicStopped(); - playTopMusic(); - } - - /** - * Shutdown the music subsystem. - */ - protected void shutdownMusic () - { - _musicStack.clear(); - stopMusicPlayer(); - } - - /** - * Stop the current music player. - */ - protected void stopMusicPlayer () - { - if (_musicPlayer != null) { - _musicPlayer.stop(); - _musicPlayer.shutdown(); - _musicPlayer = null; } } @@ -731,49 +478,53 @@ public class SoundManager } /** - * Get the audio data for the specified path. + * Called by spooling threads, loads clip data from the resource + * manager or the cache. */ protected byte[] getClipData (SoundKey key) throws IOException, UnsupportedAudioFileException { - // if we're testing, clear all non-locked sounds every time - if (isTesting()) { - _clipCache.clear(); - } - - byte[][] data = (byte[][]) _clipCache.get(key); - - // see if it's in the locked cache (we first look in the regular - // clip cache so that locked clips that are still cached continue - // to be moved to the head of the LRU queue) - if (data == null) { - data = (byte[][]) _lockedClips.get(key); - } - - if (data == null) { - // if there is a test sound, JUST use the test sound. - InputStream stream = getTestClip(key); - if (stream != null) { - data = new byte[1][]; - data[0] = IOUtils.toByteArray(stream); - - } else { - // otherwise, randomize between all available sounds - Config c = getConfig(key); - String[] names = c.getValue(key.key, (String[])null); - if (names == null) { - Log.warning("No such sound [key=" + key + "]."); - return null; - } - - data = new byte[names.length][]; - String bundle = c.getValue("bundle", (String)null); - for (int ii=0; ii < names.length; ii++) { - data[ii] = loadClipData(bundle, names[ii]); - } + byte[][] data; + synchronized (_clipCache) { + // if we're testing, clear all non-locked sounds every time + if (isTesting()) { + _clipCache.clear(); } - _clipCache.put(key, data); + data = (byte[][]) _clipCache.get(key); + + // see if it's in the locked cache (we first look in the regular + // clip cache so that locked clips that are still cached continue + // to be moved to the head of the LRU queue) + if (data == null) { + data = (byte[][]) _lockedClips.get(key); + } + + if (data == null) { + // if there is a test sound, JUST use the test sound. + InputStream stream = getTestClip(key); + if (stream != null) { + data = new byte[1][]; + data[0] = IOUtils.toByteArray(stream); + + } else { + // otherwise, randomize between all available sounds + Config c = getConfig(key); + String[] names = c.getValue(key.key, (String[])null); + if (names == null) { + Log.warning("No such sound [key=" + key + "]."); + return null; + } + + data = new byte[names.length][]; + String bundle = c.getValue("bundle", (String)null); + for (int ii=0; ii < names.length; ii++) { + data[ii] = loadClipData(bundle, names[ii]); + } + } + + _clipCache.put(key, data); + } } return (data.length > 0) ? data[RandomUtil.getInt(data.length)] : null; @@ -878,6 +629,9 @@ public class SoundManager return IOUtils.toByteArray(clipin); } + /** + * Get the cached Config. + */ protected Config getConfig (SoundKey key) { Config c = (Config) _configs.get(key.pkgPath); @@ -935,251 +689,34 @@ public class SoundManager //Log.info("Set gain: " + gain); } - /** - * Handles the playing of sound clip data. - */ - protected static class LineSpooler extends Thread - { - /** - * Attempt to play the specified sound. - */ - public static void play ( - AudioInputStream stream, float volume, SoundKey key) - throws LineUnavailableException - { - AudioFormat format = stream.getFormat(); - LineSpooler spooler; - - for (int ii=0, nn=_openSpoolers.size(); ii < nn; ii++) { - spooler = (LineSpooler) _openSpoolers.get(ii); - - // we have this thread remove the spooler if it's dead - // so that we avoid deadlock conditions - if (spooler.isDead()) { - _openSpoolers.remove(ii--); - nn--; - - } else if (spooler.checkPlay(format, stream, volume, key)) { - return; - } - } - - if (_openSpoolers.size() >= MAX_SPOOLERS) { - throw new LineUnavailableException("Exceeded maximum number " + - "of narya sound spoolers."); - } - - spooler = new LineSpooler(format); - _openSpoolers.add(spooler); - spooler.checkPlay(format, stream, volume, key); - spooler.start(); - } - - /** - * Shutdown the linespooler subsystem. - */ - public static void shutdown () - { - // this is all that is needed, after 30 seconds each spooler - for (int ii=0, nn=_openSpoolers.size(); ii < nn; ii++) { - // this will stop playback now - ((LineSpooler) _openSpoolers.get(ii)).setDead(); - } - // and this will remove all the spoolers. They'll still be - // around while they drain their lines and then wait 30 seconds - // to die... - _openSpoolers.clear(); - } - - /** - * Private constructor. - */ - private LineSpooler (AudioFormat format) - throws LineUnavailableException - { - super("narya SoundManager LineSpooler"); - _format = format; - - _line = (SourceDataLine) AudioSystem.getLine(new DataLine.Info( - SourceDataLine.class, _format)); - _line.open(_format, LINEBUF_SIZE); - _line.start(); - - // calculate how long we should sleep when 'draining' the line - float sampleRate = format.getSampleRate(); - if (sampleRate == AudioSystem.NOT_SPECIFIED) { - sampleRate = 11025; // most of our sounds are - } - int sampleSize = format.getSampleSizeInBits(); - if (sampleSize == AudioSystem.NOT_SPECIFIED) { - sampleSize = 16; - } - _drainTime = (int) Math.ceil( - (LINEBUF_SIZE * 8 * 1000) / (sampleRate * sampleSize)); - if (_verbose.getValue()) { - Log.info("Calculated drainTime as " + _drainTime + - " (sampleSize=" + sampleSize + - ", sampleRate=" + sampleRate + ")"); - } - } - - /** - * Set this line to dead. - */ - protected void setDead () - { - _valid = false; - } - - /** - * Has this line been closed? - */ - protected boolean isDead () - { - return !_valid; - } - - /** - * Check-and-set method to see if we can play and to start - * doing so if we can. - */ - protected synchronized boolean checkPlay ( - AudioFormat format, AudioInputStream stream, float volume, - SoundKey key) - { - if (_valid && (_stream == null) && _format.matches(format)) { - _stream = stream; - _key = key; - SoundManager.adjustVolume(_line, volume); - notify(); - return true; - } - - return false; - } - - /** - * @return true if we have sound data to play. - */ - protected synchronized boolean waitForData () - { - if (_stream == null) { - try { - wait(MAX_WAIT_TIME); - } catch (InterruptedException ie) { - // ignore. - } - if (_stream == null) { - setDead(); // we waited 30 seconds and never got a sound. - return false; - } - } - - return true; - } - - /** - * Main loop for the LineSpooler. - */ - public void run () - { - while (waitForData()) { - playStream(); - } - - _line.close(); - } - - /** - * Play the current stream. - */ - protected void playStream () - { - int count = 0; - byte[] data = new byte[LINEBUF_SIZE]; - - if (_verbose.getValue()) { - Log.info("Sound playing [key=" + _key.key + "]."); - } - while (_valid && count != -1) { - try { - count = _stream.read(data, 0, data.length); - } catch (IOException e) { - // this shouldn't ever ever happen because the stream - // we're given is from a reliable source - Log.warning("Error reading clip data! [e=" + e + "]."); - _stream = null; - return; - } - - if (count >= 0) { - _line.write(data, 0, count); - } - } - - // For now, we never trust _line.drain() because it seems that - // it is simply buggy. On multithreading systems - // (linux, windows XP with HT processors) it consistently booches. - try { - Thread.sleep(_drainTime); - } catch (InterruptedException ie) { - } - - // clear it out so that we can wait for more. - _stream = null; - } - - /** The format that our line was opened with. */ - protected AudioFormat _format; - - /** The stream we're currently spooling out. */ - protected AudioInputStream _stream; - - /** The line that we spool to. */ - protected SourceDataLine _line; - - /** The amount of time we need to sleep after the last write - * of data to the line to be sure that all of it will have been - * played. */ - protected int _drainTime; - - /** Are we still active and usable for spooling sounds, or should - * we be removed. */ - protected boolean _valid = true; - - /** Used for debugging. */ - protected SoundKey _key; - - /** The list of all the currently instantiated spoolers. */ - protected static ArrayList _openSpoolers = new ArrayList(); - - /** The size of the line's buffer. */ - protected static final int LINEBUF_SIZE = 8 * 1024; - - /** The maximum time a spooler will wait for a stream before - * deciding to shut down. */ - protected static final long MAX_WAIT_TIME = 1000L; - - /** The maximum number of spoolers we'll allow. This is a lot. */ - protected static final int MAX_SPOOLERS = 12; - } - /** * A key for tracking sounds. */ protected static class SoundKey { + public byte cmd; + public String pkgPath; public String key; public long stamp; + /** + * Create a SoundKey that just contains the specified command. + * DIE. + */ + public SoundKey (byte cmd) + { + this.cmd = cmd; + } + /** * Quicky constructor for music keys and lock operations. */ - public SoundKey (String pkgPath, String key) + public SoundKey (byte cmd, String pkgPath, String key) { + this(cmd); this.pkgPath = pkgPath; this.key = key; } @@ -1187,9 +724,9 @@ public class SoundManager /** * Constructor for a sound effect soundkey. */ - public SoundKey (String pkgPath, String key, int delay) + public SoundKey (byte cmd, String pkgPath, String key, int delay) { - this(pkgPath, key); + this(cmd, pkgPath, key); stamp = System.currentTimeMillis() + delay; } @@ -1205,7 +742,8 @@ public class SoundManager // documentation inherited public String toString () { - return "SoundKey{pkgPath=" + pkgPath + ", key=" + key + "}"; + return "SoundKey{cmd=" + cmd + ", pkgPath=" + pkgPath + + ", key=" + key + "}"; } // documentation inherited @@ -1226,42 +764,23 @@ public class SoundManager } } - /** - * A class that tracks the information about our playing music files. - */ - protected static class MusicInfo extends SoundKey - { - /** How many times to loop, or -1 for forever. */ - public int loops; - - // TODO rename - public MusicInfo (String set, String path, int loops) - { - super(set, path); - this.loops = loops; - } - } - /** The path of the default sound to use for missing sounds. */ protected String _defaultClipBundle, _defaultClipPath; /** The resource manager from which we obtain audio files. */ protected ResourceManager _rmgr; - /** The thread that plays sounds. */ - protected Thread _player; - /** The queue of sound clips to be played. */ protected Queue _queue = new Queue(); + /** The number of currently active LineSpoolers. */ + protected int _spoolerCount, _freeSpoolers; + /** If we every play a sound successfully, this is set to true. */ protected boolean _soundSeemsToWork = false; - /** Volume levels for both sound clips and music. */ - protected float _clipVol = 1f, _musicVol = 1f; - - /** The action to take when adjusting music volume. */ - protected int _musicAction = NONE; + /** Volume level for sound clips. */ + protected float _clipVol = 1f; /** The cache of recent audio clips . */ protected LRUHashMap _clipCache = new LRUHashMap(10); @@ -1271,26 +790,17 @@ public class SoundManager * agenda. */ protected HashMap _lockedClips = new HashMap(); - /** The stack of songs that we're playing. */ - protected LinkedList _musicStack = new LinkedList(); - - /** The current music player, if any. */ - protected MusicPlayer _musicPlayer; - /** A set of soundTypes for which sound is enabled. */ protected HashSet _disabledTypes = new HashSet(); /** A cache of config objects we've created. */ protected HashMap _configs = new HashMap(); - /** Signals to the queue to do different things. */ - protected String PLAY = new String("PLAY"); - protected String PLAYMUSIC = new String("PLAYMUSIC"); - protected String STOPMUSIC = new String("STOPMUSIC"); - protected String UPDATE_MUSIC_VOL = new String("UPDATE_MUSIC_VOL"); - protected String LOCK = new String("LOCK"); - protected String UNLOCK = new String("UNLOCK"); - protected String DIE = new String("DIE"); + /** Soundkey command constants. */ + protected static final byte PLAY = 0; + protected static final byte LOCK = 1; + protected static final byte UNLOCK = 2; + protected static final byte DIE = 3; /** A pref that specifies a directory for us to get test sounds from. */ protected static RuntimeAdjust.FileAdjust _testDir = @@ -1303,18 +813,20 @@ public class SoundManager "Verbose sound event logging", "narya.media.sound.verbose", MediaPrefs.config, false); - /** Music action constants. */ - protected static final int NONE = 0; - protected static final int START = 1; - protected static final int STOP = 2; - /** The queue size at which we start to ignore requests to play sounds. */ protected static final int MAX_QUEUE_SIZE = 25; - /** Used to disable sound entirely. */ - protected static final boolean SOUND_ENABLED = true; - /** The maximum time after which we throw away a sound rather * than play it. */ protected static final long MAX_SOUND_DELAY = 400L; + + /** The size of the line's buffer. */ + protected static final int LINEBUF_SIZE = 8 * 1024; + + /** The maximum time a spooler will wait for a stream before + * deciding to shut down. */ + protected static final long MAX_WAIT_TIME = 30000L; + + /** The maximum number of spoolers we'll allow. This is a lot. */ + protected static final int MAX_SPOOLERS = 12; }