/**
* Copyright (c) 2001-2014 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
*/
package net.sf.robocode.sound;
import net.sf.robocode.battle.IBattleManager;
import net.sf.robocode.settings.ISettingsListener;
import net.sf.robocode.settings.ISettingsManager;
import robocode.control.events.BattleAdaptor;
import robocode.control.events.BattleFinishedEvent;
import robocode.control.events.BattleStartedEvent;
import robocode.control.events.TurnEndedEvent;
import robocode.control.snapshot.IBulletSnapshot;
import robocode.control.snapshot.IRobotSnapshot;
import robocode.control.snapshot.RobotState;
import javax.sound.sampled.AudioSystem;
import javax.sound.sampled.Clip;
import javax.sound.sampled.FloatControl;
import javax.sound.sampled.Mixer;
/**
* The sound manager is responsible of keeping a table of sound effects and
* play the appropriate sound for each bullet or robot event that is supposed
* to make any noise.
*
* @author Luis Crespo (original)
* @author Flemming N. Larsen (contributor)
* @author Titus Chen (contributor)
*/
public class SoundManager implements ISoundManager {
// Cache containing sound clips
private SoundCache sounds;
// Access to properties
private final ISettingsManager properties;
private final IBattleManager battleManager;
private boolean isSoundEnabled = true;
BattleObserver observer;
public SoundManager(final ISettingsManager properties, IBattleManager battleManager) {
this.battleManager = battleManager;
this.properties = properties;
if (isSoundEnabled()) {
observer = new BattleObserver();
battleManager.addListener(observer);
}
properties.addPropertyListener(new ISettingsListener() {
public void settingChanged(String property) {
if (property.equals(ISettingsManager.OPTIONS_SOUND_ENABLESOUND)) {
updateListener();
}
}
});
}
/**
* Returns the current mixer selected from the Robocode properties.
*
* @return the current Mixer instance
*/
public Mixer getMixer() {
return findMixer(properties.getOptionsSoundMixer());
}
private boolean isSoundEnabled() {
return isSoundEnabled && properties.getOptionsSoundEnableSound();
}
public void setEnableSound(boolean enable) {
isSoundEnabled = enable;
updateListener();
}
private void updateListener() {
if (observer == null && isSoundEnabled()) {
observer = new BattleObserver();
battleManager.addListener(observer);
} else if (observer != null && !isSoundEnabled()) {
battleManager.removeListener(observer);
observer = null;
}
}
/**
* Returns the cache containing sound clips.
*
* @return a SoundCache instance
*/
private SoundCache getSounds() {
if (sounds == null) {
sounds = new SoundCache(getMixer());
// Sound effects
sounds.addSound("gunshot", properties.getFileGunshotSfx(), 5);
sounds.addSound("robot death", properties.getRobotDeathSfx(), 3);
sounds.addSound("bullet hits robot", properties.getBulletHitsRobotSfx(), 3);
sounds.addSound("bullet hits bullet", properties.getBulletHitsBulletSfx(), 2);
sounds.addSound("robot collision", properties.getRobotCollisionSfx(), 2);
sounds.addSound("wall collision", properties.getWallCollisionSfx(), 2);
// Music
sounds.addSound("theme", properties.getFileThemeMusic(), 1);
sounds.addSound("background", properties.getFileBackgroundMusic(), 1);
sounds.addSound("endOfBattle", properties.getFileEndOfBattleMusic(), 1);
}
return sounds;
}
/**
* Iterates over the available mixers, looking for the one that matches a given
* class name.
*
* @param mixerClassName the class name of the mixer to be used.
* @return the requested mixer, if found. Otherwise, it returns null.
*/
private Mixer findMixer(String mixerClassName) {
if (mixerClassName == null) {
return null;
}
for (Mixer.Info mi : AudioSystem.getMixerInfo()) {
Mixer m = AudioSystem.getMixer(mi);
if (m.getClass().getSimpleName().equals(mixerClassName)) {
return m;
}
}
return null;
}
/**
* Performs shutdown, by liberating the sound table
*/
public void dispose() {
if (sounds != null) { // Do not call getSounds()!
sounds.clear();
}
}
/**
* Plays a specific sound at a given volume, panning and loop count
*
* @param key the sound name, as stored in the sound table
* @param pan panning to be used (-1=left, 0=middle, +1=right)
* @param volume volume to be used, from 0 to 1
* @param loop the number of times to loop the sound
*/
private void playSound(Object key, float pan, float volume, int loop) {
Clip c = getSounds().getSound(key);
if (c == null) {
return;
}
if (properties.getOptionsSoundEnableMixerPan() && c.isControlSupported(FloatControl.Type.PAN)) {
FloatControl panCtrl = (FloatControl) c.getControl(FloatControl.Type.PAN);
panCtrl.setValue(pan);
}
if (properties.getOptionsSoundEnableMixerVolume() && c.isControlSupported(FloatControl.Type.MASTER_GAIN)) {
FloatControl volCtrl = (FloatControl) c.getControl(FloatControl.Type.MASTER_GAIN);
float min = volCtrl.getMinimum() / 4;
if (volume != 1) {
volCtrl.setValue(min * (1 - volume));
}
}
c.loop(loop);
}
/**
* Plays a specific sound at a given panning with max. volume and without looping.
*
* @param key the sound name, as stored in the sound table
* @param pan panning to be used (-1=left, 0=middle, +1=right)
*/
private void playSound(Object key, float pan) {
playSound(key, pan, 1, 0);
}
/**
* Plays a specific piece of music with a given loop count with no panning and
* max. volume.
*
* @param key the sound name, as stored in the sound table
* @param loop the number of times to loop the music
*/
private void playMusic(Object key, int loop) {
playSound(key, 0, 1, loop);
}
/**
* Plays a bullet sound depending on the bullet's state
*
* @param bp the bullet peer
* @param battleFieldWidth the width of the battle field used for panning.
*/
public void playBulletSound(IBulletSnapshot bp, int battleFieldWidth) {
float pan = 0;
if (properties.getOptionsSoundEnableMixerPan()) {
pan = calcPan((float) bp.getPaintX(), battleFieldWidth);
}
switch (bp.getState()) {
case FIRED:
if (properties.getOptionsSoundEnableGunshot()) {
playSound("gunshot", pan, calcBulletVolume(bp), 0);
}
break;
case HIT_VICTIM:
if (properties.getOptionsSoundEnableBulletHit()) {
playSound("bullet hits robot", pan);
}
break;
case HIT_BULLET:
if (properties.getOptionsSoundEnableBulletHit()) {
playSound("bullet hits bullet", pan);
}
break;
case HIT_WALL:
// Currently, no sound
break;
case EXPLODED:
if (properties.getOptionsSoundEnableRobotDeath()) {
playSound("robot death", pan);
}
break;
default:
}
}
/**
* Plays a robot sound depending on the robot's state
*
* @param robotPeer the robot peer
* @param battleFieldWidth the battle field width used for panning
*/
public void playRobotSound(IRobotSnapshot robotPeer, int battleFieldWidth) {
float pan = 0;
if (properties.getOptionsSoundEnableMixerPan()) {
pan = calcPan((float) robotPeer.getX(), battleFieldWidth);
}
switch (robotPeer.getState()) {
case HIT_ROBOT:
if (properties.getOptionsSoundEnableRobotCollision()) {
playSound("robot collision", pan);
}
break;
case HIT_WALL:
if (properties.getOptionsSoundEnableWallCollision()) {
playSound("wall collision", pan);
}
break;
default:
}
}
/**
* Plays the theme music once.
*/
public void playThemeMusic() {
if (isSoundEnabled()) {
playMusic("theme", 0);
}
}
/**
* Plays the background music, which is looping forever until stopped.
*/
public void playBackgroundMusic() {
playMusic("background", -1);
}
/**
* Stops the background music.
*/
public void stopBackgroundMusic() {
Clip c = getSounds().getSound("background");
if (c != null) {
c.stop();
}
}
/**
* Plays the end of battle music once.
*/
public void playEndOfBattleMusic() {
playMusic("endOfBattle", 0);
}
/**
* Determines pan based on the relative position to the battlefield's width
*
* @param x the bullet or robot position
* @param width the battlefield's width
* @return the panning value, ranging from -1 to +1
*/
private float calcPan(float x, float width) {
float semiWidth = width / 2;
return (x - semiWidth) / semiWidth;
}
/**
* Determines volume based on the bullets's energy
*
* @param bp the bullet peer
* @return the volume value, ranging from 0 to 1
*/
private float calcBulletVolume(IBulletSnapshot bp) {
return (float) (bp.getPower() / robocode.Rules.MAX_BULLET_POWER);
}
private class BattleObserver extends BattleAdaptor {
@Override
public void onBattleStarted(BattleStartedEvent event) {
if (isSoundEnabled()) {
playBackgroundMusic();
}
}
@Override
public void onBattleFinished(BattleFinishedEvent event) {
stopBackgroundMusic();
if (isSoundEnabled()) {
playEndOfBattleMusic();
}
}
@Override
public void onTurnEnded(TurnEndedEvent event) {
if (isSoundEnabled()) {
int battleFieldWidth = battleManager.getBattleProperties().getBattlefieldWidth();
for (IBulletSnapshot bp : event.getTurnSnapshot().getBullets()) {
if (bp.getFrame() == 0) {
playBulletSound(bp, battleFieldWidth);
}
}
boolean playedRobotHitRobot = false;
for (IRobotSnapshot rp : event.getTurnSnapshot().getRobots()) {
// Make sure that robot-hit-robot events do not play twice (one per colliding robot)
if (rp.getState() == RobotState.HIT_ROBOT) {
if (playedRobotHitRobot) {
continue;
}
playedRobotHitRobot = true;
}
playRobotSound(rp, battleFieldWidth);
}
}
}
}
}