/* * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * *
* *
* JavaWorld Library, Copyright 2011 Bryan Chadwick *
* *
* FILE: ./world/sound/SoundWorld.java *
* *
* This file is part of JavaWorld. *
* *
* JavaWorld is free software: you can redistribute it and/or *
* modify it under the terms of the GNU General Public License *
* as published by the Free Software Foundation, either version *
* 3 of the License, or (at your option) any later version. *
* *
* JavaWorld 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 General Public License for more details. *
* *
* You should have received a copy of the GNU General Public License *
* along with JavaWorld. If not, see . *
* *
* * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * */
package world.sound;
import java.util.HashMap;
import world.sound.tunes.*;
import world.BigBang;
import image.Scene;
/**
*
*
* A Class representing an imperative World with sound/music and the related methods for drawing the
* world and handling various events. In order to implement a functioning World with sound you
* must extend this class, and implement an {@link world.sound.SoundWorld#onDraw onDraw}
* method. Other handler methods ({@link world.sound.SoundWorld#tickRate tickRate}, {@link world.sound.SoundWorld#onTick onTick},
* {@link world.sound.SoundWorld#onMouse onMouse}, {@link world.sound.SoundWorld#onKey onKey}, {@link world.sound.SoundWorld#onRelease onRelease},
* {@link world.sound.SoundWorld#stopWhen stopWhen}, and {@link world.sound.SoundWorld#lastScene lastScene}) are optional, and
* can be overridden to add new functionality.
*
* Each of the interaction methods can add {@link world.sound.tunes.Note Note}s (sounds) to be played
* (e.g., onTick
) for a length of time after the event. There are two tune-collections
* for adding sounds, the first {@link world.sound.SoundWorld#tickTunes tickTunes}, should be added
* to for notes/sounds that should be played for a specified length of time corresponding to a World
* event. The second, {@link world.sound.SoundWorld#keyTunes keyTunes}, should be added to for
* notes/sounds that should be played for as long as the key is pressed; when the key is released
* the sound will be removed and playing of the note will stop.
*
*
* See the world.sound.tunes package for more details (Notes,
* Chords, etc.).
*
*
*
*
Extending VoidWorld
*
* Below is a simple example of a VoidWorld
that adds a new point at each mouse
* click. The world contains a {@link image.Scene Scene} and a new {@link image.Circle Circle}
* is placed for each "button-down"
event received, and a
* {@link world.sound.tunes.Note Note} is added to the {@link world.sound.SoundWorld#tickTunes tickTunes}
* to be played at the current pitch
, which is incremented.
*
*
* import image.*;
* import world.sound.SoundWorld;
* import world.sound.tunes.Note;
*
* public class MousePointsSoundWorld extends SoundWorld{
* // Simple Main Program
* public static void main(String[] args)
* { new MousePointsSoundWorld().bigBang(); }
*
* // The inner Scene
* Scene scene = new EmptyScene(200, 200);
* // The current pitch to be played
* int pitch = noteDownC;
*
* // Create a new World
* MousePointsSoundWorld(){}
*
* // Draw by returning the inner Scene
* public Scene onDraw(){ return this.scene; }
*
* // On a mouse click add a circle to the inner Scene, increment the
* // current pitch and play a short note
* public void onMouse(int x, int y, String me){
* if(me.equals("button-down")){
* this.pitch++;
* this.tickTunes.addNote(WOOD_BLOCK, new Note(this.pitch, 1));
* this.scene = this.scene.placeImage(
* new Circle(20, "solid", "red")
* .overlay(new Circle(20, "outline", "black")), x, y);
* }
* }
* }
*
*
* After a few mouse clicks, the window will look something like this, though every
* mouse click will have a corresponding sound at an increasing pitch:
*
*
*
*
*/
public abstract class SoundWorld implements SoundConstants{
/** Default Tick rate for the world: ~33 frames per second */
public static double DEFAULT_TICK_RATE = 0.03;
/** Mouse down (button-down) event String */
public static String MOUSE_DOWN = BigBang.MOUSE_DOWN;
/** Mouse up (button-up) event String */
public static String MOUSE_UP = BigBang.MOUSE_UP;
/** Mouse window enter (enter) event String */
public static String MOUSE_ENTER = BigBang.MOUSE_ENTER;
/** Mouse window leave (leave) event String */
public static String MOUSE_LEAVE = BigBang.MOUSE_LEAVE;
/** Mouse motion (move) event String */
public static String MOUSE_MOVE = BigBang.MOUSE_MOVE;
/** Mouse down & move (drag) event String */
public static String MOUSE_DRAG = BigBang.MOUSE_DRAG;
/** Key arrow-up event String */
public static String KEY_ARROW_UP = BigBang.KEY_ARROW_UP;
/** Key arrow-down event String */
public static String KEY_ARROW_DOWN = BigBang.KEY_ARROW_RIGHT;
/** Key arrow-left event String */
public static String KEY_ARROW_LEFT = BigBang.KEY_ARROW_LEFT;
/** Key arrow-right event String */
public static String KEY_ARROW_RIGHT = BigBang.KEY_ARROW_RIGHT;
/** A representation of the current state of the MIDI synthesizer. */
public MusicBox musicBox = new MusicBox();
/** The collection of tunes to play on tick. Any tunes added after an event will begin
* playing as soon as the event is completely processed and will finish playing when
* the sound/Note's duration has elapsed. */
public TuneCollection tickTunes;
/** The collection of tunes to start playing when a key is
* pressed, which will automatically removed when the same
* key is released. */
public TuneCollection keyTunes;
/** The collection of tunes currently playing on tick */
private TuneCollection currentTickTunes;
/** The number of ticks per Tune Tick (so clients can adjust the game speed without
* changing the length of Notes/sounds. */
private int ticksPerTuneTick;
/** The number of ticks left until the next Tune Tick */
private int tuneTickJiffies;
/** the collection of tunes currently playing on key event */
protected HashMap keyReleasedTunes;
/** Default constructor. Simply initializes the tune/music classes. */
public SoundWorld(){
this.initMusic();
// Each tune-tick is about a quarter of a second
this.ticksPerTuneTick = (int)Math.max(1.0, 0.125/this.tickRate());
}
/** Initialize the MIDI synthesizer and the TuneCollections */
private void initMusic(){
/** The MIDI synthesizer that plays the notes */
musicBox = new MusicBox();
if(musicBox.isReady()){
this.tickTunes = new TuneCollection(this.musicBox);
this.currentTickTunes = new TuneCollection(this.musicBox);
this.keyTunes = new TuneCollection(this.musicBox);
this.keyReleasedTunes = new HashMap();
}else{
/** notify the user that music cannot play */
System.out.println("MIDI synthesizer or the soundbank not available.");
System.out.println("Tunes will not be played.");
}
}
/** Return a visualization of this World as a {@link image.Scene Scene}.
* See {@link image.EmptyScene}, {@link image.Scene#placeImage(Image, int, int)}, and
* {@link image.Scene#addLine(int, int, int, int, String)} for documentation on
* constructing Scenes */
public abstract Scene onDraw();
/** Return the tick rate for this World in seconds. For example,
* 0.5 means two ticks per second.
* The rate is only accessed when bigBang() is initially called and the
* window is created. */
public double tickRate(){ return DEFAULT_TICK_RATE; }
/** Change this World based on the Tick of the clock. This
* method is called to get the update the World on each clock tick.
* Sounds ({@link world.sound.tunes.Note Note}s) to play starting on the current tick
* may be added to the {@link world.sound.SoundWorld#tickTunes tickTunes} tune-collection
* to be played for a specified length of time. Notes will stop playing automatically
* when the amount of time corresponding to the note's duration has elapsed.
*/
public void onTick(){ }
/** Wrapper for sound processing on tick */
private void processTick(){
if(musicBox.isReady()){
// advance the tick on current tunes
// and stop playing those that are done
this.tuneTickJiffies--;
if(this.tuneTickJiffies <= 0){
this.tuneTickJiffies = this.ticksPerTuneTick;
this.currentTickTunes.nextBeat();
}
}
// process the changes to the world on this tick
this.onTick();
if(musicBox.isReady()){
// play the tunes collected in the tick TuneCollection
this.tickTunes.playTunes();
this.currentTickTunes.add(this.tickTunes);
this.tickTunes.clear();
}
}
/** Change this World when a mouse event is triggered.
* x and y are the location of the event in the window, and
* event is a String that describes what kind of event
* occurred.
*
*
* Possible Mouse Events
*
* "button-down" : |
* The user presses a mouse button in the World window |
* "button-up" : |
* The user releases a mouse button in the World window |
* "move" : |
* The user moves the mouse in the World window |
* "drag" : |
* The user holds a mouse button and moves the mouse in the World window |
* "enter" : |
* The user moves the mouse in-to the World window |
* "leave" : |
* The user moves the mouse out-of the World window |
*
*
*
* Sounds ({@link world.sound.tunes.Note Note}s) to play starting when a certain mouse event
* occurs may be added to the {@link world.sound.SoundWorld#tickTunes tickTunes} tune-collection
* to be played for a specified length of time. Notes will stop playing automatically
* when the amount of time corresponding to the note's duration has elapsed.
*/
public void onMouse(int x, int y, String event){ }
/** Wrapper for sound processing on mouse */
private void processMouse(int x, int y, String event){
this.onMouse(x, y, event);
if(musicBox.isReady()){
this.tickTunes.playTunes();
this.currentTickTunes.add(this.tickTunes);
this.tickTunes.clearTunes();
}
}
/** Change this World when a key event is
* triggered. The given event is a String that
* describes which key was pressed.
*
*
* Special Key
*
* "up" : |
* The user presses the up-arrow key |
* "down" : |
* The user presses the down-arrow key |
* "left" : |
* The user presses the left-arrow key |
* "right" : |
* The user presses the right-arrow key |
*
*
* Other keys generate a single character String that
* represents the key pressed. For example, Pressing the B key on
* the keyboard generates "b" as an event.
* If the shift key is held while pressing B then "B" is generated.
*
*
* Sounds ({@link world.sound.tunes.Note Note}s) to play when the given key is pressed
* may be added to the {@link world.sound.SoundWorld#keyTunes keyTunes} tune-collection
* to be played until the same key is released. Notes will not stop playing until the
* key is released.
*
*
* Sounds to be played for a specific length of time after a certain key press (i.e., not
* until the key is released) may be added to the {@link world.sound.SoundWorld#tickTunes tickTunes}
* tune-collection (instead of {@link world.sound.SoundWorld#keyTunes keyTunes}) played until
* amount of time corresponding to the note's duration has elapsed.
*
*/
public void onKey(String event){ }
/** Wrapper for sound processing on key press */
protected void processKey(String ke){
// empty the key TuneCollection
if(musicBox.isReady()){
this.keyTunes.clearTunes();
}
// process the changes to the world on this key event
this.onKey(ke);
// play the tunes collected in the key TuneCollection
// save what is currently playing so it plays until released
if(musicBox.isReady()){
if(!this.keyReleasedTunes.containsKey(ke)){
this.keyReleasedTunes.put(ke, this.keyTunes.copy());
this.keyTunes.playTunes();
}
}
}
/** Change this World when a key is released. The given event
* is a String that describes which key was released.
*
*
* Special Keys
*
* "up" : |
* The user presses the up-arrow key |
* "down" : |
* The user presses the down-arrow key |
* "left" : |
* The user presses the left-arrow key |
* "right" : |
* The user presses the right-arrow key |
*
*
* Other keys generate a single character String that
* represents the key released. For example, Pressing then releasing the B key on
* the keyboard generates "b" as an onKey event and again
* as an onRelease event. If the shift key is held while pressing/releasing B then "B" is generated.
*
*
* Sounds ({@link world.sound.tunes.Note Note}s) that were added to the
* {@link world.sound.SoundWorld#keyTunes keyTunes} tune-collection on a previous
* key press will be stopped.
*/
public void onRelease(String event){ }
/** Wrapper for sound processing on key release */
private void processRelease(String ke){
if(musicBox.isReady()){
if(this.keyReleasedTunes.containsKey(ke)){
this.keyReleasedTunes.remove(ke).clear();
}
}
// invoke user-defined onKeyReleased method
this.onRelease(ke);
}
/** Determine if the World/interaction/animation should be
* stopped. Returning a value of true
* discontinues all events (mouse, key, ticks) and causes {@link
* world.sound.SoundWorld#lastScene} to be used to draw the final
* Scene.
*/
public boolean stopWhen(){ return false; }
/** Returns the Scene that should be displayed when the
* interaction/animation completes ({@link world.sound.SoundWorld#stopWhen}
* returns true). */
public Scene lastScene(){ return this.onDraw(); }
/** Wrapper for the call to LastScene. Allows Tunes to be added to the tick collection. */
protected Scene processLastScene(){
Scene ret = this.lastScene();
if(musicBox.isReady()){
this.tickTunes.playTunes();
this.currentTickTunes.add(this.tickTunes);
}
return ret;
}
/** Kick off the interaction/animation. This method returns the final
* state of the world after the user closes the World window. */
public SoundWorld bigBang(){
SoundWorld fin = (SoundWorld)new BigBang(this)
.onDraw(new WorldDraw())
.onTick(new WorldTick(), tickRate())
.onMouse(new WorldMouse())
.onKey(new WorldKey())
.onRelease(new WorldRelease())
.stopWhen(new WorldStop())
.lastScene(new WorldLast())
.bigBang("SoundWorld");
// Let stuff finish a bit...
try{ Thread.sleep(500); }catch(Exception e){}
// Kill all the notes
if(musicBox.isReady()){
this.currentTickTunes.clear();
this.currentTickTunes.clearTunes();
this.tickTunes.clear();
this.tickTunes.clearTunes();
this.keyTunes.clear();
this.keyTunes.clearTunes();
for(TuneCollection t : this.keyReleasedTunes.values()){
t.clear();
t.clearTunes();
}
}
return fin;
}
/** Wrapper for OnDraw callback */
private static class WorldDraw{
@SuppressWarnings("unused")
Scene apply(SoundWorld w)
{ return w.onDraw(); }
}
/** Wrapper for OnTick callback */
private static class WorldTick{
@SuppressWarnings("unused")
SoundWorld apply(SoundWorld w)
{ w.processTick(); return w; }
}
/** Wrapper for OnMouse callback */
private static class WorldMouse{
@SuppressWarnings("unused")
SoundWorld apply(SoundWorld w, int x, int y, String me)
{ w.processMouse(x, y, me); return w; }
}
/** Wrapper for OnKey callback */
private static class WorldKey{
@SuppressWarnings("unused")
SoundWorld apply(SoundWorld w, String ke)
{ w.processKey(ke); return w; }
}
/** Wrapper for OnRelease callback */
private static class WorldRelease{
@SuppressWarnings("unused")
SoundWorld apply(SoundWorld w, String ke)
{ w.processRelease(ke); return w; }
}
/** Wrapper for StopWhen callback */
private static class WorldStop{
@SuppressWarnings("unused")
boolean apply(SoundWorld w)
{ return w.stopWhen(); }
}
/** Wrapper for LastScene callback */
private static class WorldLast{
@SuppressWarnings("unused")
Scene apply(SoundWorld w)
{ return w.processLastScene(); }
}
/** Overridden equality to method. Returns false to make sure that
* changes to the world are redrawn every time. */
public boolean equals(Object o){ return false; }
}