From 2b085bd8c2bc18206e3bd7d7e7e10e53c944642b Mon Sep 17 00:00:00 2001 From: Ray Greenwell Date: Mon, 17 Jan 2005 23:30:38 +0000 Subject: [PATCH] Finished a weekend project: rewrote the SoundManager. The SoundManager used to keep the AudioSystem's Line open for up to 30 seconds after a sound was played, maybe because I thought that opening the line was expensive, or because it makes an audible 'tick' in linux if no other sounds are playing. Well, it turns out that the sound looping bug is the result of some internal befuckery of Sun's caused by keeping the line open. Restructed the sound manager so that lines are opened every time a sound is to be played and then closed immediately after. This also allowed me to simplify a thing or two, and sounds should actually be more responsive, in a tiny way, since previously the dobj thread asked to play a sound, the sound manager thread would load the clip data and finally a data spooling thread would play the actual sound. Now there is no sound manager thread- so the dobj thread adds a sound to the queue and one of the playing threads wakes up, reads the data and plays the sound. Factored out all the music stuff into a new MusicManager. There was almost nothing shared between the two, and it was just annoying to have one monolithic manager that had all the logic and variables for both of these distinct functions. The music manager also no longer has a processing queue, everything takes place on the dobj thread. git-svn-id: svn+ssh://src.earth.threerings.net/narya/trunk@3303 542714f4-19e9-0310-aa3c-eee0fc999fb1 --- .../threerings/media/sound/MusicManager.java | 354 +++++++ .../threerings/media/sound/SoundManager.java | 994 +++++------------- 2 files changed, 607 insertions(+), 741 deletions(-) create mode 100644 src/java/com/threerings/media/sound/MusicManager.java 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; }