/******************************************************************************* * Copyright (c) 2001, 2010 Mathew A. Nelson and Robocode contributors * All rights reserved. This program and the accompanying materials * are made available under the terms of the Eclipse Public License v1.0 * which accompanies this distribution, and is available at * http://robocode.sourceforge.net/license/epl-v10.html * * Contributors: * Luis Crespo * - Initial API and implementation * Flemming N. Larsen * - Updated to use methods from the Logger, which replaces logger methods * that have been (re)moved from the robocode.util.Utils class * - The addSound() will now return if the resource name is not specified * - Improved the error message in addSound() if the line is unavailable *******************************************************************************/ package net.sf.robocode.sound; import net.sf.robocode.io.Logger; import javax.sound.sampled.*; import java.io.IOException; import java.net.URL; import java.util.HashMap; import java.util.Map; /** * SoundCache maintains a table of sound clips. More than one instance of the same * sample can be stored, so a given sound effect can be played more than once * symultaneously. * * @author Luis Crespo (original) * @author Flemming N. Larsen (contributor) */ class SoundCache { /** * Table containing all sound clips */ private final Map<Object, ClipClones> soundTable; /** * Mixer used for creating clip instances */ private final Mixer mixer; /** * Holds data, length and format for a given sound effect, which can be used to create * multiple instances of the same clip. */ private static class SoundData { private final AudioFormat format; private final int length; private final byte[] byteData; private SoundData(AudioInputStream ais) throws IOException { int bytesRead, pos; format = ais.getFormat(); length = (int) (ais.getFrameLength() * format.getFrameSize()); byteData = new byte[length]; pos = 0; do { bytesRead = ais.read(byteData, pos, length - pos); if (bytesRead > 0) { pos += bytesRead; } } while (bytesRead > 0 && pos < length); ais.close(); } } /** * Holds an array of clips from the same sample stream, and takes care of * returning the next available clip. If all clips are active, the least * recently used clip will be returned. */ private static class ClipClones { private final Clip[] clips; private int idx; private ClipClones(Mixer mixer, SoundData soundData, int size) throws LineUnavailableException { idx = 0; clips = new Clip[size]; DataLine.Info info = new DataLine.Info(Clip.class, soundData.format); if (!AudioSystem.isLineSupported(info)) { throw new LineUnavailableException("Required data line is not supported by the audio system"); } for (int i = 0; i < size; i++) { clips[i] = (Clip) mixer.getLine(info); clips[i].open(soundData.format, soundData.byteData, 0, soundData.length); } } private void dispose() { for (Clip c : clips) { c.close(); } } private Clip next() { Clip c = clips[idx]; idx = (idx + 1) % clips.length; c.stop(); c.setFramePosition(0); return c; } } /** * Constructs a sound cache to hold sound clips that is created based on the * specified mixer. * * @param mixer the mixer to be used for creating the clip instances */ public SoundCache(Mixer mixer) { this.mixer = mixer; soundTable = new HashMap<Object, ClipClones>(); } /** * Adds a number of clip clones for a given resource holding the audio data. * If there is any error, the method returns silently, and clip instances will * not be found later for the provided key. * * @param key the key to be used for later retrieval of the sound * @param resourceName the resource holding the audio data * @param numClones the number of copies of the clip to be created */ public void addSound(Object key, String resourceName, int numClones) { if (mixer == null || resourceName == null || (resourceName.trim().length() == 0)) { return; } SoundData data = createSoundData(resourceName); if (data == null) { return; } ClipClones clones; try { clones = new ClipClones(mixer, data, numClones); soundTable.put(key, clones); } catch (LineUnavailableException e) { Logger.logError( "The audio mixer " + mixer.getMixerInfo().getName() + " does not support the audio format of the sound clip: " + resourceName); } } /** * Creates an instance of SoundData, to be used later for creating the clip clones. * * @param resourceName the name of the resource holding the audio data * @return the newly created sound data */ private SoundData createSoundData(String resourceName) { SoundData data; URL url = SoundCache.class.getResource(resourceName); if (url == null) { Logger.logError("Could not load sound because of invalid resource name: " + resourceName); return null; } try { AudioInputStream ais = AudioSystem.getAudioInputStream(url); data = new SoundData(ais); } catch (Exception e) { Logger.logError("Error while reading sound from resource: " + resourceName, e); data = null; } return data; } /** * Gets the next available clip instance of a given sound. If all clips for that * sound are active (playing), the least recently used will be returned. * * @param key the key that was used when adding the sound * @return a clip instance ready to be played through Clip.start() */ public Clip getSound(Object key) { ClipClones clones = soundTable.get(key); if (clones == null) { return null; } return clones.next(); } /** * Removes all clip copies of a given sound, closing all its dependent resources * * @param key the key that was used when adding the sound */ public void removeSound(Object key) { ClipClones clones = soundTable.get(key); if (clones == null) { return; } clones.dispose(); soundTable.remove(key); } /** * Empties all clips from the sound cache */ public void clear() { for (ClipClones clones : soundTable.values()) { clones.dispose(); } soundTable.clear(); } }