Index: /trunk/src_apollo/org/apollo/ApolloSystem.java
===================================================================
--- /trunk/src_apollo/org/apollo/ApolloSystem.java	(revision 315)
+++ /trunk/src_apollo/org/apollo/ApolloSystem.java	(revision 315)
@@ -0,0 +1,276 @@
+package org.apollo;
+
+import java.awt.Image;
+import java.awt.Toolkit;
+import java.awt.event.KeyEvent;
+import java.awt.event.KeyListener;
+import java.awt.event.WindowEvent;
+import java.awt.event.WindowListener;
+import java.net.URL;
+import java.util.Collection;
+import java.util.HashSet;
+import java.util.Set;
+
+import javax.swing.SwingUtilities;
+
+import org.apollo.actions.ApolloActions;
+import org.apollo.agents.MelodySearch;
+import org.apollo.audio.ApolloPlaybackMixer;
+import org.apollo.audio.RecordManager;
+import org.apollo.audio.SampledAudioManager;
+import org.apollo.audio.util.FrameLayoutDaemon;
+import org.apollo.audio.util.MultiTrackPlaybackController;
+import org.apollo.audio.util.PlaybackClock;
+import org.apollo.audio.util.SoundDesk;
+import org.apollo.gui.FramePlaybackBarRenderer;
+import org.apollo.gui.FrameRenderPasses;
+import org.apollo.io.SampledAudioFileImporter;
+import org.apollo.util.AudioSystemLog;
+import org.expeditee.actions.Actions;
+import org.expeditee.gui.Browser;
+import org.expeditee.gui.FrameGraphics;
+import org.expeditee.importer.FrameDNDTransferHandler;
+
+/**
+ * Provides initialization and shutdown services for the Apollo system.
+ * 
+ * @author Brook Novak
+ *
+ */
+public final class ApolloSystem {
+	
+	// TODO: Create actual frames
+	public static final String SYSTEM_FRAMESET_NAME = "apollosystem";
+	public static final String HELP_TOP_FRAMENAME = SYSTEM_FRAMESET_NAME + 2;
+	
+//	 TODO: How to get good results: collection (moteef) and querry
+	// TODO: How to omit indexing on tracks 
+	public static final String HELP_MELODYSEARCH_FRAMENAME = SYSTEM_FRAMESET_NAME + 3; 
+	
+	public static boolean useQualityGraphics = true;
+	
+	/** Would be nice to seperate outside of this class.. */
+	private static boolean isControlDown = false;
+
+	private ApolloSystem() {
+	}
+
+	private static boolean hasInitialized = false;
+	
+	/**
+	 * Initializes Apollo mod for expeditee - prepares all subsystems.
+	 */
+	public static void initialize() {
+		
+		if (hasInitialized) return;
+
+		AudioSystemLog.println("Initializing...");
+		
+		SwingUtilities.invokeLater(new Runnable() {
+			public void run() {
+				
+				 try {
+
+
+					URL url = ClassLoader.getSystemResource("org/apollo/icons/mainicon.png");
+					
+					if (url != null) {
+						 Image img = Toolkit.getDefaultToolkit().getImage(url);
+						 Browser._theBrowser.setIconImage(img);
+					}
+						
+	
+					 
+				 } catch (Exception e) {
+					 e.printStackTrace();
+				 }
+				 
+				// Ensure that resources are released before the application is closed.
+				Browser._theBrowser.addWindowListener(new WindowListener() {
+		
+					public void windowActivated(WindowEvent e) {
+					}
+		
+					public void windowClosed(WindowEvent e) {
+					}
+		
+					public void windowClosing(WindowEvent e) {
+						ApolloSystem.shutdown();
+					}
+		
+					public void windowDeactivated(WindowEvent e) {
+					}
+		
+					public void windowDeiconified(WindowEvent e) {
+					}
+		
+					public void windowIconified(WindowEvent e) {
+					}
+		
+					public void windowOpened(WindowEvent e) {
+					}
+				
+				});
+				
+				// Refresh frame for graphical helpers that display / hide when the
+				// user presses various keys
+				Browser._theBrowser.getContentPane().addKeyListener(new KeyListener() {
+
+					public void keyPressed(KeyEvent e) {
+						isControlDown = e.isControlDown();
+						if (e.getKeyCode() == KeyEvent.VK_CONTROL) {
+							FrameGraphics.refresh(false);
+						}
+						
+					}
+
+					public void keyReleased(KeyEvent e) {
+						isControlDown = e.isControlDown();
+						if (e.getKeyCode() == KeyEvent.VK_CONTROL) {
+							FrameGraphics.refresh(false);
+						} else if (e.getKeyCode() == KeyEvent.VK_G && e.isControlDown()) {
+							useQualityGraphics = !useQualityGraphics;
+						}
+					}
+
+					public void keyTyped(KeyEvent e) {
+					}
+					
+				});
+			}
+		});
+		
+		// TODO: Have sample rates configurable (remmebr to delete all metadata upen new sample rates).
+		// TODO: Fix conversion for me
+		 if (SampledAudioManager.PLAYBACK_SAMPLE_RATE != 22050.0f) {
+			 AudioSystemLog.print("***BETA-WARNING**: Sample-rate will not give melody searches");
+		 }
+		 
+		// Set title and icon
+		 //Browser._theBrowser.etTitle("Apollo");
+		 
+
+		 
+		System.err.println("WARNING: MISSING MAIN ICON"); // TODO
+		
+		AudioSystemLog.println("  Preparing sub-systems...");
+		
+		SampledAudioManager.getInstance();
+		
+		RecordManager.getInstance();
+		
+		ApolloPlaybackMixer.getInstance();
+		
+		FrameLayoutDaemon.getInstance();
+		
+		FrameRenderPasses.getInstance();
+		
+		PlaybackClock.getInstance();
+		
+		FramePlaybackBarRenderer.getInstance();
+		
+		// Setup for importing audio
+		FrameDNDTransferHandler.getInstance().addCustomFileImporter(
+				new SampledAudioFileImporter());
+
+		AudioSystemLog.println("  Loading actions and agents...");
+		
+		// Add apollo actions
+		Actions.LoadMethods(ApolloActions.class);
+		
+		Set<String> agents = new HashSet<String>();
+		agents.add(MelodySearch.class.getName());
+		
+		Collection<String> omitted = Actions.addAgents(agents);
+		
+		for (String agent : omitted) {
+			
+			if (agent == null || agent.length() == 0) continue;
+			
+			String name = agent;
+			
+			int index = agent.lastIndexOf('.');
+			if (index > 0 && agent.length() > (index - 1)) {
+				name = agent.substring(index);
+			}
+			AudioSystemLog.println("  WARNING: Failed to add agent \"" + name + "\"");
+			
+		}
+
+		AudioSystemLog.println("  Loading banks...");
+		SoundDesk.getInstance(); // loads upon creation
+
+		AudioSystemLog.println("Initialized");
+		
+		hasInitialized = true;
+	}
+	
+	
+	
+
+	/**
+	 * Releases all resources currently used by the SampledAudioManager.
+	 */
+	private static void shutdown() {
+		
+		AudioSystemLog.println("Saving banks...");
+		SoundDesk.getInstance().saveMasterMix();
+		SoundDesk.getInstance().saveMixes();
+		
+		AudioSystemLog.println("Releasing resources...");
+		
+		ApolloPlaybackMixer.getInstance().releaseResources();
+		RecordManager.getInstance().releaseResources(); // blocking
+		MultiTrackPlaybackController.getInstance().releaseResources();
+		
+		AudioSystemLog.println("Audio subsystems shutdown");
+	}
+
+	/**
+	 * @return
+	 * 		True if has initialized.
+	 */
+	public static boolean isInitialized() {
+		return hasInitialized;
+	}
+	
+	
+	/**
+	 * @return True if control is down.
+	 */
+	public static boolean isControlDown() {
+		return isControlDown;
+	}
+
+
+
+
+	public static void setControlDown(boolean isControlDown) {
+		ApolloSystem.isControlDown = isControlDown;
+	}
+
+
+
+
+	/**
+	 * The apollo main just ensures that apollo is initialized during startup.
+	 * 
+	 * This may eventually become redundant once Expeditee implements a plugin system
+	 * that allows a plugin / mod to initialize itself at started...
+	 * 
+	 * @param args
+	 */
+	public static void main(String[] args) {
+		
+		// Run expeditee
+		Browser.main(args);
+		
+		// Initialize apollo
+		SwingUtilities.invokeLater(new Runnable() {
+			public void run() {
+				ApolloSystem.initialize();
+			}
+		});
+	}
+	
+}
Index: /trunk/src_apollo/org/apollo/actions/ApolloActions.java
===================================================================
--- /trunk/src_apollo/org/apollo/actions/ApolloActions.java	(revision 315)
+++ /trunk/src_apollo/org/apollo/actions/ApolloActions.java	(revision 315)
@@ -0,0 +1,75 @@
+package org.apollo.actions;
+
+import org.apollo.items.FramePlaybackLauncher;
+import org.apollo.widgets.LinkedTrack;
+import org.apollo.widgets.SampleRecorder;
+import org.expeditee.gui.DisplayIO;
+import org.expeditee.gui.Frame;
+import org.expeditee.gui.FrameMouseActions;
+import org.expeditee.items.Text;
+
+/**
+ * Apollo's action set
+ * 
+ * @author Brook Novak
+ *
+ */
+public class ApolloActions {
+
+	private ApolloActions() {} // Util construction
+	
+	/**
+	 * Attatches a new recorder to the cursor
+	 */
+	public static void spawnRecorder() {
+		
+		Frame target = DisplayIO.getCurrentFrame();
+		if (target  == null) return;
+
+		// Create the widget
+		Text source = new Text(target.getNextItemID());
+		source.setParent(target);
+		source.setPosition(FrameMouseActions.MouseX, FrameMouseActions.MouseY);
+		SampleRecorder recWidget = new SampleRecorder(source, null);
+		
+		// Pick it up
+		FrameMouseActions.pickup(recWidget.getItems());
+	}
+	
+	/**
+	 * Attatches a new linked track to the cursor
+	 */
+	public static void spawnLinkedTrack() {
+		
+		Frame target = DisplayIO.getCurrentFrame();
+		if (target  == null) return;
+
+		// Create the widget
+		Text source = new Text(target.getNextItemID());
+		source.setParent(target);
+		source.setPosition(FrameMouseActions.MouseX, FrameMouseActions.MouseY);
+		
+		LinkedTrack ltWidget = new LinkedTrack(source, null);
+		
+		// Pick it up
+		FrameMouseActions.pickup(ltWidget.getItems());
+	}
+	
+
+	/**
+	 * Attatches a new FramePlaybackLauncher to the cursor
+	 */
+	public static void spawnFramePlayLauncher() {
+		
+		Frame target = DisplayIO.getCurrentFrame();
+		if (target  == null) return;
+
+		// Create the launcher
+		FramePlaybackLauncher launcher = new FramePlaybackLauncher();
+		launcher.setPosition(FrameMouseActions.MouseX, FrameMouseActions.MouseY);
+
+		// Pick it up
+		FrameMouseActions.pickup(launcher);
+	}
+	
+}
Index: /trunk/src_apollo/org/apollo/agents/MelodySearch.java
===================================================================
--- /trunk/src_apollo/org/apollo/agents/MelodySearch.java	(revision 315)
+++ /trunk/src_apollo/org/apollo/agents/MelodySearch.java	(revision 315)
@@ -0,0 +1,494 @@
+package org.apollo.agents;
+
+import java.awt.Color;
+import java.io.File;
+import java.io.FileNotFoundException;
+import java.io.IOException;
+import java.util.Collections;
+import java.util.LinkedList;
+import java.util.List;
+
+import javax.sound.sampled.AudioFormat;
+import javax.swing.SwingUtilities;
+
+import org.apollo.ApolloSystem;
+import org.apollo.io.AudioPathManager;
+import org.apollo.meldex.DynamicProgrammingAlgorithm;
+import org.apollo.meldex.McNabMongeauSankoffAlgorithm;
+import org.apollo.meldex.MeldexConversion;
+import org.apollo.meldex.Melody;
+import org.apollo.meldex.StandardisedMelody;
+import org.apollo.meldex.WavSample;
+import org.apollo.util.ExpediteeFileTextSearch;
+import org.apollo.util.TextItemSearchResult;
+import org.apollo.widgets.LinkedTrack;
+import org.apollo.widgets.SampledTrackEvo4;
+import org.apollo.widgets.TrackWidgetCommons;
+import org.expeditee.agents.SearchAgent;
+import org.expeditee.gui.Frame;
+import org.expeditee.gui.FrameGraphics;
+import org.expeditee.gui.FrameIO;
+import org.expeditee.gui.UserSettings;
+import org.expeditee.items.Item;
+import org.expeditee.items.ItemUtils;
+import org.expeditee.items.widgets.InteractiveWidget;
+import org.expeditee.items.widgets.WidgetCorner;
+import org.expeditee.items.widgets.WidgetEdge;
+
+/**
+ * Performs a melody search against track widgets within a whole frameset.
+ * 
+ * Uses meldex. Thanks David Bainbridge.
+ * 
+ * The agent runs a querry on the given track widget that launched it.
+ * If the track launches it it does a full search for all tracks on the current frameset.
+ * 
+ * @author Brook Novak
+ *
+ */
+public class MelodySearch extends SearchAgent {
+	
+	private long firstFrame = 1;
+
+	private long maxFrame = Integer.MAX_VALUE;
+	
+	/** Either querry from raw audio or track widget: */
+	private SampledTrackEvo4 querryTrack = null; 
+	
+	/** Either querry from raw audio  or track widget: */
+	private byte[] querryRawAudio = null;
+	private AudioFormat querryRawAudioFormat = null;
+	
+	private static final String MELODY_METAFILE_PREFIX = ".";
+	private static final String MELODY_METAFILE_SUFFIX = ".mel";
+	
+	/** The default score cut-off for omitting bad results above this value*/
+	private static final float DEFAULT_SCORE_THRESHOLD = 400.0f;
+	
+	public MelodySearch() {
+		super("MelodySearch");
+	}
+	
+	/**
+	 * 
+	 * @param firstFrame
+	 * 		The first frame number to start searching from (inclusive)
+	 * 
+	 * @param maxFrame
+	 * 		The max frame number to start searching from (inclusive)
+	 */
+	public MelodySearch(long firstFrame, long maxFrame) {
+		super("MelodySearch");
+		this.firstFrame = firstFrame;
+		this.maxFrame = maxFrame;
+	}
+	
+	public void useRawAudio(byte[] querryRawAudio, AudioFormat querryRawAudioFormat) {
+
+		if (querryRawAudio == null || querryRawAudioFormat == null) return;
+		this.querryRawAudio = querryRawAudio;
+		this.querryRawAudioFormat = querryRawAudioFormat;
+	}
+
+	/** 
+	 * {@inheritDoc}
+	 */
+	@Override
+	public boolean initialise(Frame frame, Item item) {
+		if (!super.initialise(frame, item)) return false;
+		
+		// Get the track to querry .. if given one
+		querryTrack = null;
+		
+		if (item != null) {
+			
+			InteractiveWidget iw = null;
+			
+			if (item instanceof WidgetCorner) {
+				
+				iw = ((WidgetCorner)item).getWidgetSource();
+			} if (item instanceof WidgetEdge) {
+				
+				iw = ((WidgetEdge)item).getWidgetSource();
+			}
+
+			if (iw != null && iw instanceof SampledTrackEvo4) {
+				querryTrack = (SampledTrackEvo4)iw;
+			}
+			
+		}
+		
+		return true;
+	}
+
+	/**
+	 * {@inheritDoc}
+	 */
+	@Override
+	protected Frame process(Frame frame) {
+
+		try {
+			if(frame == null) {
+				frame = FrameIO.LoadFrame(_startName + '0');
+			}
+			
+			String path = frame.getPath();
+			
+			int count = FrameIO.getLastNumber(_startName);
+			
+			String trackPrefix = ItemUtils.GetTag(ItemUtils.TAG_IWIDGET) + ": ";
+			String linkedTrackPrefix = trackPrefix;
+			
+			trackPrefix += SampledTrackEvo4.class.getName();
+			linkedTrackPrefix += LinkedTrack.class.getName();
+			
+			Melody querryMelody = null;
+			// Maps FrameName -> MelodySearchResult
+			List<MelodySearchResult> melodyScores = new LinkedList<MelodySearchResult>();
+			
+			// If querrying a track widget then get its melody
+			if (querryTrack != null) {
+				assert(querryRawAudio == null);
+				assert(querryRawAudioFormat == null);
+				
+				try {
+					querryMelody = MeldexConversion.toMelody(querryTrack);
+				} catch (IOException e) {
+					e.printStackTrace();
+				} catch (OutOfMemoryError ex) {
+					ex.printStackTrace();
+				}
+				
+				if (querryMelody == null) { // abort - failed to get audio
+					SwingUtilities.invokeLater(new ExpediteeMessageBayFeedback(
+							"Melody search aborted: Failed to load tracks audio"));
+					_results.addText("Melody search aborted: querry data not good enough to search with", 
+							Color.RED, null, null, false);
+					_results.addText("Click here for help on melody searches", 
+							new Color(0, 180, 0), ApolloSystem.HELP_MELODYSEARCH_FRAMENAME, null, false);
+					
+					_results.save();
+					return null;
+				}
+				
+			// If querrying raw audio then get its melody
+			} else if (querryRawAudio != null && querryRawAudioFormat != null) {
+				assert(querryTrack == null);
+				
+				try {
+					querryMelody = MeldexConversion.toMelody(querryRawAudio, querryRawAudioFormat);
+				} catch (IOException e) {
+					e.printStackTrace();
+				} catch (OutOfMemoryError ex) {
+					ex.printStackTrace();
+				}
+				
+				if (querryMelody == null) { // abort - failed to get audio
+					SwingUtilities.invokeLater(new ExpediteeMessageBayFeedback(
+							"Melody search aborted: Failed to proccess querry data"));
+					_results.addText("Melody search aborted: querry data not good enough to search with", Color.RED, null, null, false);
+					_results.addText("Click here for help on melody searches", 
+							new Color(0, 180, 0), ApolloSystem.HELP_MELODYSEARCH_FRAMENAME, null, false);
+					_results.save();
+					return null;
+				}
+				
+				
+			}
+			
+			// Support range searching... i.e. frame100 - frame500
+			for (long i = firstFrame;i <= maxFrame && i <= count; i++) {
+				
+				// Has requested stop?
+				if (_stop) {
+					break;
+				}
+				
+				String frameName = _startName + i;
+				
+	
+				overwriteMessage("Searching " + frameName); // RISKY
+				// Note: cannot invoke later otherwise can congest the swing queue!
+	
+				// Perform prefix search
+				List<TextItemSearchResult> results = null;
+				try {
+					
+					String fullpath = getFullPath(frameName, path);
+					
+					if (fullpath != null) {
+						results = ExpediteeFileTextSearch.prefixSearch(
+								fullpath, 
+								new String[] {trackPrefix, linkedTrackPrefix});
+					}
+					
+				} catch (FileNotFoundException e) {
+					e.printStackTrace();
+				} catch (IOException e) {
+					e.printStackTrace();
+				}
+				
+				if (results == null) continue; // frame does not exist or an error occured
+				
+				// If the frame exists / succeeded to perform a prefix search then increment the 
+				// searched frame count...
+				_frameCount++;
+				
+				// Is doing a full search for all tracks?
+				if (querryMelody == null) {
+					if(!results.isEmpty()) { 
+						_results.addText(frameName + "(" + results.size() + ")", null, frameName, null, false);
+						FrameGraphics.requestRefresh(true);
+					}
+					
+				} else { // meldex querry
+					
+					MelodySearchResult bestScore = null;
+					
+					for (TextItemSearchResult res : results) {
+						if (res.data == null) continue;
+		
+						// should this widget be indexed?
+						if (res.containsData(SampledTrackEvo4.META_DONT_INDEX_AUDIO_TAG)) continue;
+						
+						// get the local filename
+						String localFilename = null;
+						String trackName = null;
+						
+						// Parse meta
+						for (String data : res.data) {
+							if (data.startsWith(SampledTrackEvo4.META_LOCALNAME_TAG) &&
+									data.length() > SampledTrackEvo4.META_LOCALNAME_TAG.length()) {
+								localFilename = data.substring(SampledTrackEvo4.META_LOCALNAME_TAG.length());
+								if (trackName != null) break;
+							} else if (data.startsWith(TrackWidgetCommons.META_NAME_TAG) &&
+									data.length() > TrackWidgetCommons.META_NAME_TAG.length()) {
+								trackName = data.substring(TrackWidgetCommons.META_NAME_TAG.length());
+								if (localFilename != null) break;
+							}
+							
+						}
+						
+						if (localFilename == null)
+							continue;
+						
+						// Safety: omit this if it is infact the very widget we are searching
+						if (querryTrack != null && querryTrack.getLocalFileName().equals(localFilename))
+							continue;
+						
+						Melody testMelody = null;
+						
+						// Get cached melody from file if it is up to date
+						String metaFilePath = 
+							AudioPathManager.AUDIO_HOME_DIRECTORY 
+							+ MELODY_METAFILE_PREFIX
+							+ localFilename
+							+ MELODY_METAFILE_SUFFIX;
+						
+						File localFile = new File(AudioPathManager.AUDIO_HOME_DIRECTORY + localFilename);
+						if (!localFile.exists()) continue;
+						
+						File metaFile = new File(metaFilePath);
+						
+						// If there is a metafile that contains the serialized melody and is up to date...
+						if (metaFile.exists() && metaFile.lastModified() >= localFile.lastModified()) {
+							
+							try {
+								StandardisedMelody sm = StandardisedMelody.readMelodyFromFile(metaFilePath);
+								if (sm != null && sm instanceof Melody) {
+									testMelody = (Melody)sm;
+								}
+							} catch (FileNotFoundException e) {
+								e.printStackTrace();
+							} catch (IOException e) {
+								e.printStackTrace();
+							} catch (ClassNotFoundException e) {
+								e.printStackTrace();
+							}
+							
+						}
+						
+						if (_stop) break;
+						
+						// If did not manage to load any melody meta ... calculate the meta
+						if (testMelody == null) {
+	
+							// Load wave file
+							WavSample sampleTrack = new WavSample();
+							if (!sampleTrack.loadFromFile(localFile)) {
+								continue;
+							}
+							if (_stop) break;
+							
+							// Transcribe melody
+							try {
+								testMelody = MeldexConversion.toMelody(sampleTrack);
+							} catch (IOException ex) {
+								ex.printStackTrace();
+							} catch (OutOfMemoryError ex) {
+								ex.printStackTrace();
+							}
+							
+							if (testMelody == null) continue;
+							if (_stop) break;
+	
+							// Save meta
+							if (metaFile.exists()) metaFile.delete();
+							
+							try {
+								StandardisedMelody.writeMelodyToFile(metaFilePath, testMelody);
+							} catch (FileNotFoundException e) {
+								e.printStackTrace();
+							} catch (IOException e) {
+								e.printStackTrace();
+							}
+							
+						}
+		
+						assert(testMelody != null);
+						assert(querryMelody != null);
+						
+						// Omit bogus melody transcriptions for which audio must not of have enough rests
+						// to give a strong enough representation. This avoids bogus meta getting better results
+						// than something actually more meaningful.
+						if (testMelody.getLength() <= 1) continue;
+						
+						DynamicProgrammingAlgorithm dpa = new McNabMongeauSankoffAlgorithm();
+			
+						float score = dpa.matchToPattern(querryMelody, testMelody, DynamicProgrammingAlgorithm.MATCH_ANYWHERE);
+						
+						if (bestScore == null || bestScore.getScore() < score) {
+							
+							bestScore = new MelodySearchResult(
+									frameName, 
+									score,
+									trackName,
+									localFilename
+									);
+						}
+						
+						
+					} // next result in frame
+	
+					if (_stop) {
+						break;
+					}
+					
+					if (bestScore != null) {
+						melodyScores.add(bestScore);
+					}
+	
+				}
+				
+				
+			} // Next frame
+			
+			
+			// Did do melody matching? add ordered results
+			if (querryMelody != null) {
+				
+				float threshold = DEFAULT_SCORE_THRESHOLD; // TODO: Have tolerance option
+				
+				// Order results descending
+				Collections.sort(melodyScores);
+
+				if (melodyScores.isEmpty() || melodyScores.get(0).getScore() > threshold) {
+					_results.addText("No matches", Color.RED, null, null, false);
+					_results.addText("Click here to find out how to improve your melody searches", 
+							new Color(0, 180, 0), ApolloSystem.HELP_MELODYSEARCH_FRAMENAME, null, false);
+				} else {
+					
+					int rank = 1;
+					for (MelodySearchResult melRes : melodyScores) {
+
+						if (melRes.getScore() <= threshold) {
+							
+							String name = melRes.getTrackName();
+							if (name == null || name.length() == 0)
+								name = "Unnamed";
+		
+							_results.addText(rank + ": " + melRes.getParentFrame() + " ("
+										+ name + ")",
+										null, 
+										melRes.getParentFrame(), null, false);
+							
+							
+						}
+						
+						rank ++;
+						
+					}
+					
+				}
+	
+			}
+			
+			if (_stop) {
+				_results.addText("Search cancelled", Color.RED, null, null, false);
+			}
+			
+			// Spit out result(s)
+			_results.save();
+	
+			String resultFrameName = _results.getName();
+			if (_clicked != null)
+				_clicked.setLink(resultFrameName);
+	
+			return _results.getFirstFrame();
+			
+			}
+		finally {
+			querryRawAudio = null; // Free memory
+		}
+		
+	}
+	
+
+	
+	
+	/**
+	 * Gets the full path from a given framename and path
+	 * @param frameName
+	 * @param path
+	 * @return
+	 */
+	private String getFullPath(String frameName, String path) {
+		
+		String fullPath = null;
+		if (path == null) {
+			for (String possiblePath : UserSettings.FrameDirs) {
+				fullPath = FrameIO.getFrameFullPathName(possiblePath, frameName);
+				if (fullPath != null)
+					break;
+			}
+		} else {
+			fullPath = FrameIO.getFrameFullPathName(path, frameName);
+		}
+		
+		return fullPath;
+
+	}
+	
+	/**
+	 * Safely outputs a message on the messagebay .. if ran on swing thread.
+	 * @author Brook Novak
+	 *
+	 */
+	private class ExpediteeMessageBayFeedback implements Runnable {
+		
+		String feedbackMessage; 
+		
+		public ExpediteeMessageBayFeedback(String feedbackMessage) {
+			assert(feedbackMessage != null);
+			this.feedbackMessage = feedbackMessage;
+		}
+		
+		
+		public void run() {
+			assert(feedbackMessage != null);
+			overwriteMessage(feedbackMessage);
+		}
+	}
+	
+	
+}
Index: /trunk/src_apollo/org/apollo/agents/MelodySearchResult.java
===================================================================
--- /trunk/src_apollo/org/apollo/agents/MelodySearchResult.java	(revision 315)
+++ /trunk/src_apollo/org/apollo/agents/MelodySearchResult.java	(revision 315)
@@ -0,0 +1,66 @@
+package org.apollo.agents;
+
+
+/**
+ * An immutable search result for melody searches.
+ * 
+ * @author Brook Novak
+ *
+ */
+public class MelodySearchResult implements Comparable<MelodySearchResult> {
+
+	private String parentFrame;
+	private float score;
+	private String trackName ;
+	private String trackLocalFileName;
+	
+	public MelodySearchResult(String parentFrame, float score, String trackName, String trackLocalFileName) {
+		assert(parentFrame != null);
+		assert(trackLocalFileName != null);
+		assert(score >= 0);
+		
+		this.parentFrame = parentFrame;
+		this.score = score;
+		this.trackName = trackName;
+		this.trackLocalFileName = trackLocalFileName;
+	}
+
+	public String getParentFrame() {
+		return parentFrame;
+	}
+
+	public float getScore() {
+		return score;
+	}
+
+	public String getTrackLocalFileName() {
+		return trackLocalFileName;
+	}
+
+	/**
+	 * Can be null.
+	 * @return
+	 */
+	public String getTrackName() {
+		return trackName;
+	}
+
+	public int compareTo(MelodySearchResult o) {
+		return Float.compare(score, o.score);
+	}
+
+	/*public int compareTo(Object o) {
+		
+		if (o != null && o instanceof MelodySearchResult) {
+			return Float.compare(score, ((MelodySearchResult)o).score);
+		}
+		throw new ClassCastException();
+	}*/
+	
+	
+	
+	
+	
+	
+}
+
Index: /trunk/src_apollo/org/apollo/audio/ApolloPlaybackMixer.java
===================================================================
--- /trunk/src_apollo/org/apollo/audio/ApolloPlaybackMixer.java	(revision 315)
+++ /trunk/src_apollo/org/apollo/audio/ApolloPlaybackMixer.java	(revision 315)
@@ -0,0 +1,803 @@
+package org.apollo.audio;
+
+import java.util.ArrayList;
+import java.util.Collection;
+import java.util.LinkedList;
+import java.util.List;
+import java.util.Set;
+import java.util.TreeSet;
+
+import javax.sound.sampled.AudioFormat;
+import javax.sound.sampled.DataLine;
+import javax.sound.sampled.LineUnavailableException;
+import javax.sound.sampled.SourceDataLine;
+
+import org.apollo.mvc.AbstractSubject;
+import org.apollo.mvc.SubjectChangedEvent;
+import org.apollo.util.AudioSystemLog;
+import org.apollo.util.TrackModelHandler;
+import org.apollo.util.TrackModelLoadManager;
+
+/**
+ * A software playback mixer.
+ * 
+ * @author Brook Novak
+ *
+ */
+public class ApolloPlaybackMixer extends AbstractSubject implements TrackModelHandler {
+	
+	/** The timeline frame represents the global frame counter which all tracks synchronize to. */
+	private long timelineFrame = 0; // Wrapping ignored.
+
+	private TreeSet<TrackSequence> sequenceGraph = new TreeSet<TrackSequence>();
+	
+	private PlaybackThread playbackThread = null;
+
+	private float masterVolume = 1.0f;
+	private boolean isMasterMuteOn = false;
+	
+	private boolean isSoloEnable = false;
+	
+	private static ApolloPlaybackMixer instance = new ApolloPlaybackMixer();
+	private ApolloPlaybackMixer() {
+		// When a TrackModel is loading - look in here to see if one is in memory
+		TrackModelLoadManager.getInstance().addTrackModelHandler(this);
+	}
+	
+	public static ApolloPlaybackMixer getInstance() {
+		return instance;
+	}
+	
+	/**
+	 * Stops all playback... kills thread(s).
+	 */
+	public void releaseResources() {
+		
+		// Quickly stop playback thread
+		if (playbackThread != null)
+			playbackThread.stopPlayback(); // will die momenterially
+		
+		// Release references .. dispose of track memory
+		stopAll();
+		
+	}
+	
+
+	/**
+	 * Sets the master volume of the output mixer.
+	 * 
+	 * The master volume is not adjusted directly in the hardware due to java sounds
+	 * sketchy API... the master volume is simulated in software.
+	 * 
+	 * An AudioControlValueChangedEvent event is fired with a FloatControl.Type.VOLUME
+	 * type and the volume (a float, range of 0-1 clamped) is passed as the value
+	 * of the new volume. 
+	 * 
+	 * The volumes are updated for all audios created with the SampledAudioManager
+	 * to the new mix.
+	 * 
+	 * @param volume The new volume to set it to. Ranges from 0-1.
+	 */
+	public void setMasterVolume(float volume) {
+		
+		// Clamp volume argument
+		if (volume < 0.0f)
+			volume = 0.0f;
+		else if (volume > 1.0f)
+			volume = 1.0f;
+
+		if (volume == masterVolume)
+			return;
+
+		masterVolume = volume;
+
+		// Notify obsevers
+		fireSubjectChanged(new SubjectChangedEvent(ApolloSubjectChangedEvent.VOLUME));
+	}
+	
+
+	/**
+	 * Sets the master mute of the output mixer.
+	 * 
+	 * The master mute is not adjusted directly in the hardware due to java sounds
+	 * sketchy API... the master mute is simulated in software.
+	 * 
+	 * An AudioSubjectChangedEvent.MUTE event is fired with a BooleanControl.Type.MUTE
+	 * type and the mute is passed as the value...
+	 * 
+	 * The mutes are updated for all audios created with the SampledAudioManager
+	 * to the new mix.
+	 * 
+	 * @param mute
+	 */
+	public void setMasterMute(boolean muteOn) {
+		if (muteOn == isMasterMuteOn)
+			return;
+
+		isMasterMuteOn = muteOn;
+		
+		// Notify obsevers
+		fireSubjectChanged(new SubjectChangedEvent(ApolloSubjectChangedEvent.MUTE));
+
+	}
+	
+	/** 
+	 * @return
+	 * 		True if master mute is on.
+	 */
+	public boolean isMasterMuteOn() {
+		return isMasterMuteOn;
+	}
+
+	/**
+	 * @return
+	 * 		The master volume. Always between 0 and 1.
+	 */
+	public float getMasterVolume() {
+		return masterVolume;
+	}
+	
+	/**
+	 * @return
+	 * 		True if the mixer is in solo mode.
+	 */
+	public boolean isSoloEnable() {
+		return isSoloEnable;
+	}
+
+	/**
+	 * In solomode, the only track sequences that are played are those with
+	 * the solo flag set.
+	 * 
+	 * @param isSoloEnable
+	 * 		True to set into solo mode.
+	 */
+	public void setSoloEnable(boolean isSoloEnable) {
+		this.isSoloEnable = isSoloEnable;
+	}
+	
+	/**
+	 * Sets all track sequences that are playing / queued to play - solo flag to false.
+	 */
+	public void unsetAllSoloFlags() {
+		synchronized(sequenceGraph) {
+			for (TrackSequence ts : sequenceGraph) ts.isSolo = false;
+		}
+	}
+
+	
+	/**
+	 * Plays a track at the given relative initiation time to the current
+	 * (global) playback position.
+	 * 
+	 * If the track is already playing, or is about to play, then nothing will result in
+	 * invoking this call.
+	 * 
+	 * @param track
+	 * 		The track to play.
+	 * 
+	 * @return
+	 * 		True if queued for playing. False if track already is in the track graph.
+	 * 
+	 * @throws NullPointerException
+	 * 		If track is null.
+	 * 
+	 * @throws IllegalArgumentException
+	 * 		If track has already been played before.
+	 * 
+	 * @throws LineUnavailableException
+	 * 		If failed to get data line to output device
+	 */
+	public boolean play(TrackSequence track) throws LineUnavailableException {
+		if (track == null) throw new NullPointerException("track");
+		if (track.hasFinished()) throw new IllegalArgumentException("track is stale, must create new instance");
+		
+		// Add to graph
+		synchronized(sequenceGraph) {
+			
+			if (sequenceGraph.contains(track)) {
+				return false; // if already playing / queued to play then ignore
+			}
+			
+			// Set initiation to commence relative to the current timeline.
+			track.initiationFrame = timelineFrame + track.getRelativeInitiationFrame();
+			
+			sequenceGraph.add(track);
+
+		}
+		
+		// Ensure that the added track will play
+		commencePlayback();
+		
+		return true;
+		
+	}
+	
+	/**
+	 * Plays a group of tracks exactly at the scheduled frame-time with respect to each other.
+	 * 
+	 * For tracks in the given set that are already playing, or are queued for playing, then
+	 * they will be ignored.
+	 * 
+	 * @param tracks
+	 * 		A set of sequence tracks to play together exactly at their relative initiation points.
+	 * 
+	 * @throws NullPointerException
+	 * 		If tracks is null.
+	 * 
+	 * @throws LineUnavailableException
+	 * 		If failed to get data line to output device
+	 */
+	public void playSynchronized(Set<TrackSequence> tracks) throws LineUnavailableException {
+		if (tracks == null) throw new NullPointerException("tracks");
+		
+		// Add to graph
+		synchronized(sequenceGraph) {
+			
+			long initiationTimeOffset = timelineFrame;
+			
+			// If the playback thread is running... and the new tracks will begin playback automatically, then
+			// schedule the group of tracks to play in the next pass so they all begin together.
+			if (playbackThread != null && !playbackThread.isStopping() && !playbackThread.isAlive()) { 
+				initiationTimeOffset += playbackThread.bufferFrameLength;
+			}
+			
+			for (TrackSequence ts : tracks) {
+				
+				if (ts == null) continue;
+				
+				if (sequenceGraph.contains(ts)) {
+					continue; // if already playing / queued to play then ignore
+				}
+				
+				ts.initiationFrame = ts.getRelativeInitiationFrame() + initiationTimeOffset;
+				sequenceGraph.add(ts);
+			}
+		}
+		
+		// Ensure that the added tracks will play 
+		commencePlayback();
+		
+	}
+	
+	/**
+	 * Ensures that the playback thread is playing / wiull keep playing.
+	 * If a new track is added top the sequence graph, then by calling this it will
+	 * ensure that it will be played.
+	 * 
+	 * @throws LineUnavailableException
+	 * 		If failed to get data line to output device
+	 */
+	private void commencePlayback() throws LineUnavailableException {
+
+		if (playbackThread != null && playbackThread.isAlive() && playbackThread.isStopping()) {
+			
+			try {
+				
+				playbackThread.join(); // wait for old play proccess to finish
+				
+			} catch (InterruptedException e) {
+				
+				e.printStackTrace();
+				return;
+				
+			}
+			playbackThread = null;
+		}
+		
+		if (playbackThread != null && !playbackThread.isAlive()) {
+			playbackThread = null;
+		}
+		
+		// If playbackThread is not null at this point, then it is assumed that it is still playing
+		// therefore does not need to be started/restarted.
+				
+		// If the playback thread is dead, create a new one to initiate playback
+		if (playbackThread == null) {
+			
+			playbackThread = new PlaybackThread();
+			playbackThread.start();
+			
+		} 	
+		
+	}
+	
+	/**
+	 * Stops many track sequences. This is better than calling
+	 * {@link #stop(TrackSequence)} because ensures that all tracks are stopped
+	 * at the same time.
+	 * 
+	 * @param tracks
+	 * 		The tracks to stop.
+	 */
+	public void stop(Collection<TrackSequence> tracks) {
+		if (tracks == null || playbackThread == null) return;
+
+
+		synchronized(sequenceGraph) {
+			for (TrackSequence track : tracks) {
+				stop(track);
+			}
+		}
+		
+	}
+	
+	/**
+	 * Stops a track sequence. Nonblocking, the actual stopp will occur when the mixer has a change to
+	 * respond.
+	 * 
+	 * @param track
+	 * 		The track to stop. If null then will return with no effect.
+	 */
+	public void stop(TrackSequence track) {
+		if (track == null || playbackThread == null) return;
+
+		track.stopPending = true;
+	}
+
+	/**
+	 * Stops all track sequences from playback.
+	 */
+	public void stopAll() {
+		
+		synchronized(sequenceGraph) {
+			for (TrackSequence track : sequenceGraph)
+				track.stopPending = true;
+		}
+		
+	}
+
+	/**
+	 * {@inheritDoc}
+	 */
+	public SampledTrackModel getSharedSampledTrackModel(String localfilename) {
+		if (localfilename == null) return null;
+		
+		// Get a snapshot of the graph
+		ArrayList<TrackSequence> snapshot = null;
+		synchronized(sequenceGraph) {
+			snapshot = new ArrayList<TrackSequence>(sequenceGraph);
+		}
+		
+		// Look for SampledTrackModel-invoked sequences
+		for (TrackSequence ts : snapshot) {
+			Object invoker = ts.getInvoker();
+			
+			if (invoker != null && invoker instanceof SampledTrackModel) {
+				
+				// Match?
+				if (localfilename.equals(((SampledTrackModel)invoker).getLocalFilename())) {
+					return (SampledTrackModel)invoker; // found match
+				}
+			}
+			
+		}
+		
+		// Nothing matched
+		return null;
+	}
+
+	
+	/**
+	 * @return
+	 * 		The actual frame position in the playback stream - that is, the amount of
+	 * 		frames that have been rendered since playback commenced.
+	 * 		Negative if there is no playback.
+	 */
+	public long getLiveFramePosition() {
+		
+		if (playbackThread != null) {
+			if (playbackThread.srcDataLine.isOpen()) {
+				
+				// The timelineFrame should always be larger or equal to the live frame position
+			//	assert(timelineFrame >= playbackThread.srcDataLine.getLongFramePosition());
+				
+				return playbackThread.srcDataLine.getLongFramePosition();
+			}
+		}
+		
+		return -1;
+	}
+	
+	/**
+	 * @return
+	 * 		The audio format of the current playback data line. Null if not avilable - never
+	 * 		null if in a playing state.
+	 */
+	public AudioFormat getLiveAudioFormat() {
+		if (playbackThread != null) {
+			return playbackThread.srcDataLine.getFormat();
+		}
+		return null;		
+	}
+	
+	/**
+	 * The Audio Mixing pipeline.
+	 * 
+	 * All audio mixing math/logic is done within this thread.
+	 * Keeps running until the sequenceGraph is empty.
+	 * Removes tracks from the sequenceGraph automatically when they are finished.
+	 * 
+	 * 
+	 * @author Brook Novak
+	 *
+	 */
+	private class PlaybackThread extends Thread {
+		
+		private SourceDataLine srcDataLine; // never null
+
+		private int bufferFrameLength;
+		private boolean isOutputBigEndian;
+		
+		/**
+		 * Initantly prepares for audio playback: Opens the source data line for output
+		 * 
+		 * @throws LineUnavailableException
+		 */
+		PlaybackThread() throws LineUnavailableException {
+			super("Apollo Playback Mixer Thread");
+			super.setPriority(Thread.MAX_PRIORITY);
+			
+			assert(playbackThread == null); // there should be only one instance of this ever.
+			
+			// Upon creation, open a source data line
+			aquireSourceDataLine();
+			
+			
+			// Reset the global timeline frame to match the live frame position. i.e. wrap back at zero.
+			synchronized(sequenceGraph) { // probably will be empty, but just for safety...
+				
+				for (TrackSequence ts : sequenceGraph) {
+					ts.initiationFrame -= timelineFrame;
+					if (ts.initiationFrame < 0)
+						ts.initiationFrame = 0;
+				}
+				
+				timelineFrame = 0;
+			}
+		}
+		
+		/**
+		 * Opens the source data line for output.
+		 * 
+		 * @throws LineUnavailableException
+		 * 		If failed to acquire the source data line.
+		 */
+		private void aquireSourceDataLine() throws LineUnavailableException {
+
+			// Select an audio output format
+			DataLine.Info info = new DataLine.Info(
+					SourceDataLine.class, 
+					getAudioFormat());
+
+			// Get the source data line to output.
+			srcDataLine = (SourceDataLine) 
+				SampledAudioManager.getInstance().getOutputMixure().getLine(info); // LineUnavailableException
+			
+			srcDataLine.open(); // LineUnavailableException
+			
+			// Cache useful data
+			bufferFrameLength = srcDataLine.getBufferSize() / 2;
+			isOutputBigEndian = srcDataLine.getFormat().isBigEndian();
+			
+			assert(bufferFrameLength > 0);
+			assert(srcDataLine.getFormat().getSampleSizeInBits() == 16);
+		
+		}
+		
+		/**
+		 * Closes the data line so that if currently writing bytes or the next time 
+		 * bytes are written, the thread will end.
+		 * Does not block calling thread. Thus may take a little while for thread to
+		 * end after call returned.
+		 */
+		public void stopPlayback() {
+			srcDataLine.close();
+		}
+		
+		/**
+		 * Note: even if all tracks have been proccessed in the audio pipeline, it will
+		 * commence another pass to check for new tracks added to the graph before finishing.
+		 * 
+		 * @return True if stopping and will not play any tracks added to the queue.
+		 */
+		public boolean isStopping() {
+			return !srcDataLine.isOpen();
+		}
+		
+		/**
+		 * @return the best audio format for playback...
+		 */
+		private AudioFormat getAudioFormat() {
+			return SampledAudioManager.getInstance().getDefaultPlaybackFormat();
+		}
+
+		/**
+		 * The audio mixing pipeline
+		 */
+		public void run() {
+
+			// Notify observers that some audio has started playing
+			AudioSystemLog.println("AUDIO PIPELINE THREAD STARTED");
+			
+			ApolloPlaybackMixer.this.fireSubjectChangedLaterOnSwingThread(
+				new SubjectChangedEvent(ApolloSubjectChangedEvent.PLAYBACK_STARTED));
+			
+			// All tracks to play per pass
+			List<TrackSequence> tracksToPlay = new LinkedList<TrackSequence>();
+			
+			// Keeps track of tracks to remove
+			List<TrackSequence> completedTracks = new LinkedList<TrackSequence>();
+			
+			// The buffer written directly to the source data line
+			byte[] sampleBuffer = new byte[2 * bufferFrameLength];
+			
+			// The mixed frames, where each element refers to a frame
+			int[] mixedFrameBuffer = new int[bufferFrameLength];
+			
+			// Helpers declared outside loop for mz efficiency
+			int msb, lsb;
+			int sample;
+			int totalFramesMixed;
+			int trackCount; // tracks to play at a given pass
+			boolean isMoreQueued; // True if there are more tracks queued. 
+			int frameIndex; 
+			int i;
+			
+			// Begin writing to the source data line
+			if (srcDataLine.isOpen())
+				srcDataLine.start();
+			else return;
+
+			// keep playing as long as line is open (and there is something to play)
+			try 
+			{
+				while (srcDataLine.isOpen()) { // The audio mixing pipline
+
+					// First decide on which tracks to play ... and remove any finished tracks.
+					synchronized(sequenceGraph) {
+						
+						// If there are no more tracks queued for playing, then exit the
+						// playback thread.
+						if (sequenceGraph.isEmpty()) 
+							return;
+		
+						isMoreQueued = false;
+						completedTracks.clear();
+						tracksToPlay.clear();
+						
+						for (TrackSequence ts : sequenceGraph) {
+							
+							// Has this track sequence finished?
+							if (ts.currentFrame > ts.endFrame || ts.stopPending)
+								completedTracks.add(ts);
+							
+							// Is this track playing / is meant to start laying in this pass?
+							else if (ts.initiationFrame <= (timelineFrame + bufferFrameLength))
+								tracksToPlay.add(ts);
+	
+							// If it is not time to play the track yet, then
+							// neither will it for all proceeding tracks either
+							// since they are ordered by their initiation time.
+							else break;
+							
+						}
+						
+						// Get rid of tracks that have finished playing. Notify models that they have stopped
+						for (TrackSequence staleTS : completedTracks) {
+							
+							sequenceGraph.remove(staleTS);
+							
+							staleTS.onStopped((staleTS.currentFrame > staleTS.endFrame)
+									? staleTS.endFrame : staleTS.currentFrame);
+							
+							//removeTrackFromGraph(staleTS, staleTS.endFrame);
+						}
+
+						trackCount = tracksToPlay.size();
+						isMoreQueued = sequenceGraph.size() > trackCount;
+						
+						// If there is nothing queued and there are no tracks to play,
+						// then playback is finished.
+						if (!isMoreQueued && trackCount == 0) 
+							return;
+						
+					} // release lock
+					
+					totalFramesMixed = 0; // this will be set to the maximum amount of frames that were mixed accross all tracks
+					
+					// Clear audio buffer
+					for (i = 0; i < bufferFrameLength; i++) // TODO: Effecient way of clearing buffer?
+						mixedFrameBuffer[i] = 0; 
+					
+					// Perform Mixing : 
+					// Convert the sample size to 16-bit always for best precision while
+					// proccessing audio in the mix pipeline....
+					for (TrackSequence ts : tracksToPlay) {
+						
+						// Notify model that initiated
+						if (!ts.isPlaying()) ts.onInitiated();
+
+						// Skip muted / unsoloed tracks - they add nothing to the sample mix
+						if (ts.isMuted || (isSoloEnable && !ts.isSolo)) { 
+							
+							// Make sure start where initiated, if not already initiated
+							if (ts.initiationFrame >= timelineFrame && ts.initiationFrame < (timelineFrame + bufferFrameLength)) {
+								
+								// Get index in frame buffer where to initiate
+								frameIndex = (int)(ts.initiationFrame - timelineFrame);
+								
+								// Calcuate the length of frames to buffer - adjust silent tracks position
+								ts.currentFrame += (bufferFrameLength - frameIndex);
+
+							} else { // skip full buffer of bytes ... silenced
+								
+								ts.currentFrame += bufferFrameLength; // currentFrame can go outside endframe boundry of the track
+				
+							}
+							
+							totalFramesMixed = bufferFrameLength;
+							
+						} else { // Get samples and add to mix
+	
+							// If the track is yet to initiate - part way through the buffer, then start adding bytes
+							// at initiation point
+							if (ts.initiationFrame >= timelineFrame && ts.initiationFrame < (timelineFrame + bufferFrameLength)) {
+								
+								frameIndex = (int)(ts.initiationFrame - timelineFrame);
+								
+							} else { 
+								
+								frameIndex = 0; 
+								
+							}
+		
+							// For each frame
+							for (;frameIndex < bufferFrameLength && ts.currentFrame <= ts.endFrame; frameIndex++) {
+								
+								// Get sample according to byte order
+								if (ts.isBigEndian) {
+									
+									// First byte is MSB (high order)
+									msb = (int)ts.playbackAudioBytes[ts.currentFrame + ts.currentFrame];
+									 
+									 // Second byte is LSB (low order)
+									lsb = (int)ts.playbackAudioBytes[ts.currentFrame + ts.currentFrame + 1];
+								
+								 } else {
+									 
+									// First byte is LSB (low order)
+									lsb = (int)ts.playbackAudioBytes[ts.currentFrame + ts.currentFrame];
+									 
+									// Second byte is MSB (high order)
+									msb = (int)ts.playbackAudioBytes[ts.currentFrame + ts.currentFrame + 1];
+								}
+								
+								sample = (msb << 0x8) | (0xFF & lsb);
+
+								// Apply track volume
+								sample = (int)(sample * ts.volume);
+								
+								// Add to current mix
+								mixedFrameBuffer[frameIndex] += sample;
+			
+								// Get next sample
+								ts.currentFrame++;
+							}
+							
+							
+							// Keep track of total frames mixed in buffer
+							if (frameIndex > totalFramesMixed)
+								totalFramesMixed = frameIndex;
+						}
+		
+					} // Mix in next track
+	
+					// totalFramesMixed is the amount of frames to play.
+					// If it is zero then it means that there are tracks yet to be intiated, and nothing currently playing
+					assert (totalFramesMixed <= bufferFrameLength);
+					assert (totalFramesMixed > 0 ||
+							(totalFramesMixed == 0 && trackCount == 0 && isMoreQueued));
+					
+					// Post mix with master settings 
+					if (isMasterMuteOn) { // Silence sample buffer if master mute is on
+	
+						for (i = 0; i < sampleBuffer.length; i++) {
+							sampleBuffer[i] = 0;
+						}
+						
+						// Let the muted bytes play
+						totalFramesMixed = bufferFrameLength;
+						
+					} else { // otherwise apply master volume
+						
+						for (i = 0; i < totalFramesMixed; i++) {
+		
+							// Average tracks
+							//mixedFrameBuffer[i] /= trackCount; // depreciated
+							
+							// Apply mastar volume
+							mixedFrameBuffer[i] = (int)(mixedFrameBuffer[i] * masterVolume);
+							
+							// Clip
+							if (mixedFrameBuffer[i] > Short.MAX_VALUE) mixedFrameBuffer[i] = Short.MAX_VALUE;
+							else if (mixedFrameBuffer[i] < Short.MIN_VALUE) mixedFrameBuffer[i] = Short.MIN_VALUE; 
+							
+							// Convert to output format
+							lsb = (mixedFrameBuffer[i] & 0xFF);
+							msb = ((mixedFrameBuffer[i] >> 8) & 0xFF);
+							
+							if (isOutputBigEndian) {
+								sampleBuffer[i+i] = (byte)msb;
+								sampleBuffer[i+i+1] = (byte)lsb;
+							} else {
+								sampleBuffer[i+i] = (byte)lsb;
+								sampleBuffer[i+i+1] = (byte)msb;
+							}
+
+						}
+
+					}
+					
+					// Generate silence only if there are more tracks to be played.
+					// Note that this could be false, but a track might have been queued after
+					// setting the isMoreQueued flag. In such cases... silence is not wanted anyway!
+					if (isMoreQueued) {
+						for (i = totalFramesMixed; i < bufferFrameLength; i++) { // will skip if no need to generate silence
+							sampleBuffer[i+i] = 0;
+							sampleBuffer[i+i+1] = 0;
+						}
+						// Ensure that full buffer is played ... including the silence
+						totalFramesMixed = bufferFrameLength;
+					}
+	
+					// Write proccessed bytes to line out stream and update the timeline frame
+					srcDataLine.write(
+							sampleBuffer, 
+							0, 
+							totalFramesMixed * 2);
+					
+					// Update timeline counter for sequencing management
+					timelineFrame += totalFramesMixed;
+					
+					// The timelineFrame should always be larger or equal to the live frame position
+					assert(timelineFrame >= srcDataLine.getLongFramePosition());
+					
+				} // Next pass
+			
+			} finally { 
+				
+				// Ensure line freed
+				if (srcDataLine.isOpen()) {
+					srcDataLine.close();
+				}
+				
+				// Clear sequence graph. 
+				synchronized(sequenceGraph) {
+					
+					for (TrackSequence track : sequenceGraph) {
+						
+						track.onStopped((track.currentFrame > track.endFrame)
+								? track.endFrame : track.currentFrame);
+					}
+					
+					sequenceGraph.clear();
+					
+				}
+				
+				AudioSystemLog.println("AUDIO PIPELINE THREAD KILLED");
+				
+				// Notify observers that playback has finished.
+				ApolloPlaybackMixer.this.fireSubjectChangedLaterOnSwingThread(
+						new SubjectChangedEvent(ApolloSubjectChangedEvent.PLAYBACK_STOPPED));
+				
+			}
+			
+		}
+		
+
+		
+	}
+
+	
+}
Index: /trunk/src_apollo/org/apollo/audio/ApolloSubjectChangedEvent.java
===================================================================
--- /trunk/src_apollo/org/apollo/audio/ApolloSubjectChangedEvent.java	(revision 315)
+++ /trunk/src_apollo/org/apollo/audio/ApolloSubjectChangedEvent.java	(revision 315)
@@ -0,0 +1,73 @@
+package org.apollo.audio;
+
+/**
+ * Provides Apollos IDs for subject changed events
+ * 
+ * @author Brook Novak
+ *
+ */
+public final class ApolloSubjectChangedEvent {
+	
+	private ApolloSubjectChangedEvent() {}
+
+	public static final int INPUT_MIXER = 1;
+	
+	public static final int VOLUME = 2;
+	
+	public static final int MUTE = 3;
+	
+	public static final int SOLO_PREFIX_CHANGED = 4;
+
+	/** Raised from swing thread. */
+	public static final int PLAYBACK_STARTED = 9;
+	
+	/** Raised from swing thread. */
+	public static final int PLAYBACK_STOPPED = 10;
+	
+	/** Raised from swing thread. */
+	public static final int CAPTURE_STARTED = 11;
+	
+	/** Raised from swing thread. */
+	public static final int CAPTURE_STOPPED = 12;
+	
+	/** The state information is the channel ID */
+	public static final int PAUSE_MARK_CHANGED = 12;
+	
+	public static final int SELECTION_CHANGED = 100;
+	
+	/** In this case, the selection will have changed also. */
+	public static final int AUDIO_REMOVED = 101; 
+	
+	
+	public static final int AUDIO_INSERTED = 110;
+	
+	/** For named models - when the name has changed. */
+	public static final int NAME_CHANGED = 115;
+	
+	/** The added TrackGraphInfo is passed as the state info. Tis can be used
+	 * if the initation time of a track was updated.  */
+	public static final int GRAPH_TRACK_ADDED = 120;
+	
+	/** The removed TrackGraphNode is passed as the state info. */
+	public static final int GRAPH_TRACK_REMOVED = 121;
+	
+	/** The edit TrackGraphInfo is passed as the state info. */
+	public static final int GRAPH_TRACK_EDITED = 122;
+	
+	/** The added linked tracks virtual filename is passed. */
+	public static final int GRAPH_LINKED_TRACK_ADDED = 125;
+	
+	/** The added linked tracks virtual filename is passed. */
+	public static final int GRAPH_LINKED_TRACK_REMOVED = 126;
+	
+	/** The channel ID of the track sequence is passed as state info. */
+	public static final int TRACK_SEQUENCE_CREATED = 150;
+	
+	public static final int MULTIPLAYBACK_LOADING = 150;
+	
+	public static final int LOAD_STATUS_REPORT = 200;
+	
+	public static final int RENDER_TASK_INVALIDATION_RECOMENDATION= 300;
+	
+	
+}
Index: /trunk/src_apollo/org/apollo/audio/AudioCapturePipe.java
===================================================================
--- /trunk/src_apollo/org/apollo/audio/AudioCapturePipe.java	(revision 315)
+++ /trunk/src_apollo/org/apollo/audio/AudioCapturePipe.java	(revision 315)
@@ -0,0 +1,36 @@
+package org.apollo.audio;
+
+import java.io.PipedInputStream;
+
+import javax.sound.sampled.AudioFormat;
+
+/**
+ * Provides audio capture classes for reading captured audio.
+ * 
+ * @author Brook Novak
+ *
+ */
+public final class AudioCapturePipe {
+	
+	private PipedInputStream pin;
+	private AudioFormat audioFormat;
+	private int bufferSize;
+	
+	public AudioCapturePipe(PipedInputStream pin, AudioFormat audioFormat, int bufferSize) {
+		this.pin = pin;
+		this.audioFormat = audioFormat;
+		this.bufferSize = bufferSize;
+	}
+	
+	public AudioFormat getAudioFormat() {
+		return audioFormat;
+	}
+
+	public PipedInputStream getPin() {
+		return pin;
+	}
+	
+	public int getBufferSize() {
+		return bufferSize;
+	}
+}
Index: /trunk/src_apollo/org/apollo/audio/AudioFormatNotSupportedException.java
===================================================================
--- /trunk/src_apollo/org/apollo/audio/AudioFormatNotSupportedException.java	(revision 315)
+++ /trunk/src_apollo/org/apollo/audio/AudioFormatNotSupportedException.java	(revision 315)
@@ -0,0 +1,22 @@
+package org.apollo.audio;
+
+/**
+ * An audio format is not supported by a specific mixer configuration.
+ * 
+ * @author Brook Novak
+ */
+public class AudioFormatNotSupportedException extends Exception {
+	protected static final long serialVersionUID = 0L;
+	
+	public AudioFormatNotSupportedException() {
+		super();
+	}
+	
+	public AudioFormatNotSupportedException(String message) {
+		super(message);
+	}
+	
+	public AudioFormatNotSupportedException(String message, Exception inner) {
+		super(message, inner);
+	}
+}
Index: /trunk/src_apollo/org/apollo/audio/RecordManager.java
===================================================================
--- /trunk/src_apollo/org/apollo/audio/RecordManager.java	(revision 315)
+++ /trunk/src_apollo/org/apollo/audio/RecordManager.java	(revision 315)
@@ -0,0 +1,258 @@
+package org.apollo.audio;
+
+import java.io.IOException;
+import java.io.PipedInputStream;
+import java.io.PipedOutputStream;
+
+import javax.sound.sampled.AudioFormat;
+import javax.sound.sampled.DataLine;
+import javax.sound.sampled.LineUnavailableException;
+import javax.sound.sampled.TargetDataLine;
+
+import org.apollo.mvc.AbstractSubject;
+import org.apollo.mvc.SubjectChangedEvent;
+
+/**
+ * Provides sampled recording services.
+ * 
+ * @author Brook Novak
+ *
+ */
+public class RecordManager extends AbstractSubject  {
+
+	private AudioCaptureThread captureThread = null;
+
+	private static RecordManager instance = new RecordManager(); // single design pattern
+	
+	/**
+	 * @return The singleton instance of the SampledAudioManager
+	 */
+	public static RecordManager getInstance() { // single design pattern
+		return instance;
+	}
+
+	private RecordManager() { // singleton design pattern
+	}
+		
+	/**
+	 * Stops all captures... kills thread. Halts until killed.
+	 */
+	public void releaseResources() {
+		
+		// Quickly stop playback thread
+		if (isCapturing())
+			stopCapturing(); // halts
+	}
+	
+	/**
+	 * @return True if audio is currently being captured.
+	 */
+	public boolean isCapturing() {
+		return (captureThread != null && captureThread.isAlive());
+	}
+
+	/**
+	 * Stops capturing audio if any is being captured. Waits until capture proccess
+	 * stops.
+	 * 
+	 * A CAPTURE_STOPPED event will be raised by invoking this method if there was
+	 * audio being captured prior to invoking this method.
+	 */
+	public void stopCapturing() {
+		// Tell the capture thread to stop
+		if (captureThread != null) {
+			captureThread.stopCapturing();
+			captureThread = null;
+		}
+	}
+
+	/**
+	 * Begins capturing audio from the current input mixture.
+	 * 
+	 * If audio is currently being captured, that capture process is stopped
+	 * (hence a CAPTURE_STOPPED event will be raised). 
+	 * 
+	 * A CAPTURE_STARTED event will be raised by invoking this method. Once the record thread has started
+	 * 
+	 * @param state 
+	 * 				An object for optional state info - this is included in the audio event.
+	 *              The intention being that observers can ID whether the start/stop events are
+	 *              relevelent to them or not...
+	 *              
+	 * @return 
+	 * 			A AudioCapturePipe which the recorded bytes will be written to. 
+	 *              
+	 * @throws LineUnavailableException
+	 * 				If a line from the current input mixer is not available. 
+	 * 				(could be used by another proccess/application)
+	 * 
+	 * @throws IOException
+	 * 				If failed to create a pipe.
+	 */
+	public synchronized AudioCapturePipe captureAudio(Object state)
+			throws LineUnavailableException, IOException {
+
+		// Check to see if input mixer is available
+		if (SampledAudioManager.getInstance().getCurrentInputMixure() == null) {
+			throw new LineUnavailableException("No input mixer avialable");
+		}
+		
+		if (SampledAudioManager.getInstance().getDefaultCaptureFormat() == null) {
+			throw new LineUnavailableException("The current input mixer does not support audio capture in a valid format");
+		}
+		
+		
+		// Stop capturing any audio
+		stopCapturing();
+
+		// Create pipe
+		PipedOutputStream pout = new PipedOutputStream();
+		PipedInputStream pin = new PipedInputStream(pout);
+
+		// Begin capturing
+		assert (captureThread == null || !captureThread.isAlive());
+		captureThread = new AudioCaptureThread(pout, state);
+		captureThread.start();
+		
+		return new AudioCapturePipe(pin, captureThread.format, captureThread.bufferSize);
+	}
+	
+	
+	/**
+	 * This inner class is used for asynchronously read data from a target data line
+	 * to a stream; i.e. audio capture.
+	 * 
+	 * Once started, the audio from a TargetDataLine which is linked ot the input mixer
+	 * until either stopCapturing is invoked or the output stream is closed.
+	 * 
+	 * Fires Capture start/stop events once the thread starts / finishes respectively
+	 * 
+	 * @author Brook Novak
+	 */
+	class AudioCaptureThread extends Thread {
+		
+		private boolean isCancelled = false;
+
+		private TargetDataLine tdl;
+
+		private Object state;
+		
+		private PipedOutputStream pout;
+		
+		private AudioFormat format;
+		
+		private int bufferSize;
+
+		/**
+		 * Constructor.
+		 * The input mixer must not be null.
+		 * 
+		 * @param state 
+		 * 				An object for optional state info - this is included in the audio event.
+		 *              The intention being that observers can ID whether the start/stop events are
+		 *              relevelent to them or not...
+		 *              
+		 * @param pout 
+		 * 				The output stream to write to.
+		 *              
+		 * @throws LineUnavailableException
+		 * 				If a line from the current mixer is not available. 
+		 * 				(could be used by another proccess/application)
+		 * 
+		 */
+		AudioCaptureThread(PipedOutputStream pout, Object state)
+				throws LineUnavailableException {
+			
+			assert(pout != null);
+			assert(SampledAudioManager.getInstance().getCurrentInputMixure() != null);
+			assert(SampledAudioManager.getInstance().getDefaultCaptureFormat() != null);
+
+			this.state = state;
+			this.pout = pout;
+			
+			this.format = SampledAudioManager.getInstance().getDefaultCaptureFormat();
+
+			// Select a target data line
+			DataLine.Info dlInfo = new DataLine.Info(TargetDataLine.class, format);
+
+			// The current input mixer should always support the defaultCaptureFormat
+			assert(SampledAudioManager.getInstance().getCurrentInputMixure().isLineSupported(dlInfo));
+			
+			tdl = (TargetDataLine) SampledAudioManager.getInstance().getCurrentInputMixure().getLine(dlInfo);
+			
+			tdl.open(format); // Throws LineUnavailableException
+			
+			// Ensure a multiple of frame size
+			bufferSize = tdl.getBufferSize();
+			bufferSize -= (bufferSize % format.getFrameSize());
+			if (bufferSize <= 0) bufferSize = format.getFrameSize();
+		}
+
+		/**
+		 * Stops capturing the stream. Waits until thread stopped
+		 * Can return without the thread actually stopping (yet) if the calling thread
+		 * is interupted during the wait.
+		 */
+		public void stopCapturing() {
+			isCancelled = true;
+			if (isAlive()) {
+				tdl.stop(); // blocks (wait time can vary depending on formats..some may take awhile!)
+				tdl.drain();
+				tdl.close();
+				try {
+					join();
+				} catch (InterruptedException e) { /* Consume */
+				}
+			}
+		}
+
+		@Override
+		public void run() {
+			
+			// Notify observers
+			fireSubjectChangedLaterOnSwingThread(new SubjectChangedEvent(
+					ApolloSubjectChangedEvent.CAPTURE_STARTED, state));
+			
+			byte buffer[] = new byte[bufferSize];
+			
+			try {
+				// Start capturing
+				tdl.start();
+				
+				// Dump bytes into pipe
+				while (!isCancelled) {
+					
+				    int count = tdl.read(buffer, 0, buffer.length);
+					
+				    if (count > 0) {
+				    	pout.write(buffer, 0, count);
+				    }
+				 }
+				
+			} catch (IOException e) { // Failed to write to pipe
+				e.printStackTrace();
+				
+			} finally {
+				
+				// If thread execution stopped in a way other than the TDL closing ... ensure that
+				// it is actually closed.
+				if (tdl.isOpen()) {
+					tdl.close();
+				}
+				
+				// Close off pipe - will release reader from blocking and notify them that finished
+				try {
+					pout.close();
+				} catch (IOException e) { /* Consume */ }
+				
+				// Notify observers
+				fireSubjectChangedLaterOnSwingThread(new SubjectChangedEvent(
+						ApolloSubjectChangedEvent.CAPTURE_STOPPED, 
+						state));
+			}
+
+		}
+
+	}
+
+}
Index: /trunk/src_apollo/org/apollo/audio/SampledAudioManager.java
===================================================================
--- /trunk/src_apollo/org/apollo/audio/SampledAudioManager.java	(revision 315)
+++ /trunk/src_apollo/org/apollo/audio/SampledAudioManager.java	(revision 315)
@@ -0,0 +1,395 @@
+package org.apollo.audio;
+
+import java.util.ArrayList;
+import java.util.LinkedList;
+import java.util.List;
+
+import javax.sound.sampled.AudioFormat;
+import javax.sound.sampled.AudioSystem;
+import javax.sound.sampled.DataLine;
+import javax.sound.sampled.Mixer;
+import javax.sound.sampled.Line.Info;
+
+import org.apollo.mvc.AbstractSubject;
+import org.apollo.mvc.SubjectChangedEvent;
+import org.apollo.util.AudioSystemLog;
+
+/**
+ * Manages all audio services. Initializes and shutsdown the audio system when
+ * Expeditee opens/closes.
+ * 
+ * Handles audio formats.
+ * 
+ * @author Brook Novak
+ *
+ */
+public final class SampledAudioManager extends AbstractSubject {
+	
+	/** All internal formats have the same sample rate. */
+	public static final float PLAYBACK_SAMPLE_RATE = 44100.0f; // Audacitys default
+	
+	//public static final float PLAYBACK_SAMPLE_RATE = 22050.0f; // Meldexes internal rate .. todo: fix conversions to use better rate
+
+	// Used for describing the ideal default format for recorded audio and converting un-supported 
+	// imported audio to... The actual formats may differ depending on the data lines used
+	
+	// note: Must be PCM, Mono, 16 bit.
+	private static final AudioFormat DESIRED_FORMAT = new AudioFormat( // Linear PCM Encoding
+			PLAYBACK_SAMPLE_RATE, // Always conform to PLAYBACK_SAMPLE_RATE
+			16, // bits per sample. Must be 16. Audacitys default
+			1, // Always use mono. Audacitys default
+			false, // ALWAYS USED SIGNED FOR BEST PERFORMACE - JAVA DOES NOT HAVE UNSIGNED TYPES
+			true // Byte order
+			);
+
+	private List<Mixer.Info> inputMixers = new LinkedList<Mixer.Info>();
+	
+	private Mixer inputMixer = null; // capture
+	private Mixer outputMixer = null; // playback
+	
+	// Loaded with all supported playback formats on construction
+	private AudioFormat[] supportedPlaybackFormats = null;
+	private AudioFormat defaultPlaybackFormat = null;
+	private AudioFormat defaultCaptureFormat = null;
+	
+	private static SampledAudioManager instance = new SampledAudioManager(); // single design pattern
+
+	/**
+	 * @return The singleton instance of the SampledAudioManager
+	 */
+	public static SampledAudioManager getInstance() { // single design pattern
+		return instance;
+	}
+
+	private SampledAudioManager() { // singleton design pattern
+		LoadMixers();
+	}
+
+	/**
+	 * Detects all input mixers currently installed on system.  Selects the first input mixer it finds.
+	 * The output mixer is set to java software sound engine.
+	 */
+	private void LoadMixers() {
+		
+		// Set output mixer as java software mixer
+		outputMixer = AudioSystem.getMixer(null);
+		
+		if (outputMixer == null) {
+			AudioSystemLog.printError("Could not find output audio device");
+			
+		} else { // Create default output audio format based on output mixer
+			
+			AudioSystemLog.println("Using " + outputMixer + " as output mixer");
+			
+			// Discover all supported playback formats.
+			loadSupportedPlaybackFormats();
+			
+			// Select the default playback format.
+			defaultPlaybackFormat = getBestMatchedSupportedFormat(supportedPlaybackFormats, DESIRED_FORMAT);
+		
+			// Safety
+			if (defaultPlaybackFormat == null) {
+				defaultPlaybackFormat = DESIRED_FORMAT;
+				AudioSystemLog.printError("Unable to find a suitable default format for audio playback");
+			} 
+			
+			AudioSystemLog.println("Default playback format: " + defaultPlaybackFormat);
+			
+		}
+
+		// Get all installed mixers on the system
+		Mixer.Info[] mixers = AudioSystem.getMixerInfo();
+
+		Mixer.Info inputMixerInfo = null;
+		
+		// Determine which mixers can record / playback.
+		for (Mixer.Info mi : mixers) {
+
+			Mixer mixer = AudioSystem.getMixer(mi);
+			if (mixer.getTargetLineInfo().length > 0) {
+				inputMixers.add(mi);
+				if (inputMixerInfo ==  null) { // select a default mixer
+					inputMixerInfo = mi;
+				}
+			}
+		}
+
+		// Set the current input mixer
+		inputMixer = null;
+		if (inputMixerInfo != null) {
+			setCurrentInputMixure(inputMixerInfo); // this will load the new capture audio format
+		} else {
+			AudioSystemLog.printError("Could not find input audio device");
+		}
+		
+	}
+	
+	/**
+	 * Discovers all supported playback audio formats.
+	 */
+	private void loadSupportedPlaybackFormats() {
+		
+		LinkedList<AudioFormat> supported = new LinkedList<AudioFormat>();
+
+		for (Info inf : outputMixer.getSourceLineInfo()) {
+			
+			if (inf instanceof DataLine.Info) {
+				DataLine.Info dinf = (DataLine.Info)inf;
+				
+				for (AudioFormat format : dinf.getFormats()) {
+					supported.add(format);
+				}
+			}
+		}
+		
+		supportedPlaybackFormats = supported.toArray(new AudioFormat[0]);
+
+	}
+	
+	/**
+	 * Loads the best fitting capture format supported by the input mixer. Only format that
+	 * are supported for playback are considered.
+	 * 
+	 * Sets defaultCaptureFormat... Will be null if current input mixer does not support
+	 * the right formats. Or if no input mixer is set.
+	 */
+	private void loadSupportedCaptureFormat() {
+
+		defaultCaptureFormat = null;
+		
+		//AudioSystemLog.println("Detecting supported capture formats:");
+		
+		if (inputMixer == null) return;
+
+		LinkedList<AudioFormat> supported = new LinkedList<AudioFormat>();
+
+		for (Info inf : inputMixer.getTargetLineInfo()) {
+			
+			if (inf instanceof DataLine.Info) {
+				DataLine.Info dinf = (DataLine.Info)inf;
+				
+				for (AudioFormat format : dinf.getFormats()) {
+					if (isFormatSupportedForPlayback(format)) { // only consider if can playback
+						supported.add(format);
+						//AudioSystemLog.println(format);
+					}
+					
+				}
+			}
+		}
+		
+		//AudioSystemLog.println("*End of format list");
+		
+		// Select the best fitting format supported by the input mixer
+		defaultCaptureFormat = getBestMatchedSupportedFormat(
+				supported.toArray(new AudioFormat[0]), 
+				DESIRED_FORMAT);
+		
+		AudioSystemLog.println("Using audio format for capturing: " + defaultCaptureFormat);
+	}
+		
+	/**
+	 * @return The default playback format for this system. 
+	 * Never null, even if output mixer unavailable.
+	 */
+	public AudioFormat getDefaultPlaybackFormat() {
+		return defaultPlaybackFormat;
+	}
+	
+	/**
+	 * @return The default capture format for this system. Null if no input mixer set/available
+	 */
+	AudioFormat getDefaultCaptureFormat() {
+		return defaultCaptureFormat;
+	}
+	
+	/**
+	 * Finds the best matching format. 
+	 * 
+	 * All formats that are not PCM encoded and does not use mono channels are excluded.
+	 * 
+	 * @param targets
+	 * 
+	 * @param toMatch
+	 * 
+	 * @return 
+	 * 		The best matched and supported Audio format. 
+	 * 		Null there were no supported formats.
+	 */
+	private AudioFormat getBestMatchedSupportedFormat(AudioFormat[] targets, AudioFormat toMatch) {
+		assert(targets != null);
+		
+		assert(toMatch != null);
+		
+		int bestIndex = -1;
+		float bestScore = -1.0f;
+		
+		for (int i = 0; i < targets.length; i++) {
+			
+			AudioFormat candiate = targets[i];
+
+			// Not cadidate if not in appollos format.
+			if (!candiate.getEncoding().toString().startsWith("PCM") 
+					|| candiate.getChannels() != 1
+					|| candiate.getSampleSizeInBits() != 16
+					|| (candiate.getSampleRate() != AudioSystem.NOT_SPECIFIED &&
+							candiate.getSampleRate() != PLAYBACK_SAMPLE_RATE))
+				continue;
+			
+			float score = 0.0f;
+			
+			// Compute match score
+			if (candiate.isBigEndian() == toMatch.isBigEndian()) {
+				score += 0.5f;
+			}
+			
+			if (candiate.getSampleSizeInBits() == toMatch.getSampleSizeInBits()) {
+				score += 2.0f;
+			}
+			
+			if (candiate.getSampleRate() == toMatch.getSampleRate() 
+					|| candiate.getSampleRate() == AudioSystem.NOT_SPECIFIED) {
+				score += 2.0f;
+			}
+			
+			if (candiate.getEncoding() == toMatch.getEncoding()) { // there are different PCM encodings
+				score += 6.0f;
+			}
+			
+			if (bestIndex == -1 || score > bestScore) {
+				bestIndex = i;
+				bestScore = score;
+			}
+		}
+		
+		if (bestIndex == -1) return null;
+		
+		// Be sure to specificy the sample rate if not specified
+		AudioFormat bestMatch = targets[bestIndex];
+		if (bestMatch.getSampleRate() == AudioSystem.NOT_SPECIFIED) {
+			bestMatch = new AudioFormat(
+					bestMatch.getEncoding(),
+					toMatch.getSampleRate(),
+					bestMatch.getSampleSizeInBits(),
+					bestMatch.getChannels(),
+					bestMatch.getFrameSize(),
+					toMatch.getFrameRate(),
+					bestMatch.isBigEndian()
+					);
+		}
+		
+		return bestMatch;
+	}
+	
+
+	/**
+	 * Determines if an audio format requires conversion in order to be used 
+	 * in Apollos. 
+	 * 
+	 * Audio formats must be in PCM, mono, 16-bit sample-size,
+	 * SampledAudioManager#PLAYBACK_SAMPLE_RATE sample-rate and be supported
+	 * by the output mixer.
+	 * 
+	 * @param format
+	 * 		The format to test. Must not be null.
+	 * 
+	 * @return
+	 * 		True if the given format requires formatting.
+	 * 
+	 * @throws NullPointerException
+	 * 		If format is null.
+	 */
+	public synchronized boolean isFormatSupportedForPlayback(AudioFormat format) {
+		
+		if (format == null) throw new NullPointerException("format");
+		
+		if(!format.getEncoding().toString().startsWith("PCM") || format.getChannels() != 1
+				|| format.getSampleSizeInBits() != 16
+				|| (format.getSampleRate() != AudioSystem.NOT_SPECIFIED &&
+						format.getSampleRate() != PLAYBACK_SAMPLE_RATE)) {
+			return false;
+		}
+		
+		// Check that the format is supported by the output mixer
+		for (AudioFormat supported : supportedPlaybackFormats) {
+			if (supported.getChannels() != 1) continue;
+			if (
+					format.getEncoding() == supported.getEncoding()
+					
+					&& format.getSampleSizeInBits() == supported.getSampleSizeInBits()
+					
+					&& format.isBigEndian() == supported.isBigEndian()
+
+					&& 		(supported.getSampleRate() == AudioSystem.NOT_SPECIFIED
+							|| format.getSampleRate() == supported.getSampleRate())
+					
+			) {
+				return true;
+			}
+		}
+		
+		return false;
+	}
+	
+	
+	
+	/**
+	 * Sets the current input mixure. Returns immediatly if equal to current input mixer.
+	 * Fires a AudioSubjectChangedEvent.INPUT_MIXER event.
+	 * 
+	 * @param mi The Mixer.Info of the Mixer to set as the new mixer for input.
+	 * 
+	 * @throws IllegalArgumentException 
+	 * 			if the info object does not represent a mixer installed on the system
+	 * 
+	 * @throws NullPointerException 
+	 * 			if mi is null.
+	 */
+	public void setCurrentInputMixure(Mixer.Info mi) {
+		if (mi == null)
+			throw new NullPointerException("mi");
+		else if (!inputMixers.contains(mi))
+			throw new IllegalArgumentException("Mixer not supported");
+		
+		Mixer newMixer = AudioSystem.getMixer(mi); // also throws IllegalArgumentException
+		
+		if (newMixer.equals(inputMixer)) return;
+
+		inputMixer = newMixer;
+		
+		// Determine new capture format according to new input mixer
+		loadSupportedCaptureFormat();
+		
+		fireSubjectChanged(new SubjectChangedEvent(ApolloSubjectChangedEvent.INPUT_MIXER));
+
+	}
+	
+	/**
+	 * @return The current input mixer. Null if none is supported.
+	 */
+	Mixer getCurrentInputMixure() {
+		return inputMixer;
+	}
+
+	/**
+	 * @return The current output mixer. Null if none is supported.
+	 */
+	Mixer getOutputMixure() {
+		return outputMixer;
+	}
+	
+	/**
+	 * @return The current input mixer. Null if none is supported.
+	 */
+	public Mixer.Info getCurrentInputMixureInfo() {
+		return inputMixer.getMixerInfo();
+	}
+
+	/**
+	 * @return A copy of the list of all supported input mixers. Can be empty if none is supported.
+	 */
+	public List<Mixer.Info> getSupportedInputMixures() {
+		return new ArrayList<Mixer.Info>(this.inputMixers);
+	}
+
+}
Index: /trunk/src_apollo/org/apollo/audio/SampledTrackModel.java
===================================================================
--- /trunk/src_apollo/org/apollo/audio/SampledTrackModel.java	(revision 315)
+++ /trunk/src_apollo/org/apollo/audio/SampledTrackModel.java	(revision 315)
@@ -0,0 +1,358 @@
+package org.apollo.audio;
+
+import java.io.IOException;
+
+import javax.sound.sampled.AudioFormat;
+
+import org.apollo.io.AudioIO;
+import org.apollo.mvc.AbstractSubject;
+import org.apollo.mvc.SubjectChangedEvent;
+
+/**
+ * A modifiable sampled audio track.
+ * 
+ * @author Brook Novak
+ *
+ */
+public class SampledTrackModel extends AbstractSubject {
+	
+	private AudioFormat audioFormat = null;
+
+	private byte[] audioBytes = null;
+
+	private int selectionStart = 0; // in frames
+	
+	private int selectionLength = -1; // in frames <= 1 not ranged.
+	
+	private boolean isAudioModified = false;
+	
+	private String currentFilepath = null;
+	
+	private String localFilename = null; // never null, immutable
+	
+	private String name = null;
+	
+	/**
+	 * Constructor.
+	 * 
+	 * @param audioBytes
+	 * 			Pure audio samples. Must not be an empty array.
+	 * 
+	 * @param audioFormat
+	 * 			The format of the given audio samples.
+	 * 
+	 * @param localFilename
+	 * 			The localfilename to associate this track with... imutable. Must never
+	 * 			be null.
+	 * 
+	 * @throws IllegalArgumentException
+	 * 			If audioFormat requires conversion. See SampledAudioManager.isFormatSupportedForPlayback
+	 * 
+	 * @throws NullPointerException
+	 * 			If audioBytes or audioFormat is null
+	 */
+	public SampledTrackModel(byte[] audioBytes, AudioFormat audioFormat, String localFilename) {
+		
+		if (audioBytes == null)
+			throw new NullPointerException("audioBytes");
+		if (audioFormat == null)
+			throw new NullPointerException("audioFormat");
+		if (localFilename == null || localFilename.length() == 0)
+			throw new NullPointerException("localFilename");
+		
+		assert(audioBytes.length > 0);
+		
+		// Check format
+		if (!SampledAudioManager.getInstance().isFormatSupportedForPlayback(audioFormat))
+			throw new IllegalArgumentException("Bad audioFormat");
+		
+		this.audioFormat = audioFormat;
+		this.audioBytes = audioBytes;
+		this.localFilename = localFilename;
+		
+	}
+	
+	/**
+	 * @return True if audio bytes have been changed in some way since creation / last reset.
+	 */
+	public boolean isAudioModified() {
+		return isAudioModified;
+	}
+	
+	/**
+	 * Sets modified flag.
+	 */
+	public void setAudioModifiedFlag(boolean isModified) {
+		isAudioModified = isModified;
+	}
+	
+	/**
+	 * Same as fireSubjectChanged but also sets modified flag
+	 */
+	private void fireAudioBytesChanged(final SubjectChangedEvent event) {
+		isAudioModified = true;
+		fireSubjectChanged(event);
+	}
+	
+	/**
+	 * @return The audio format of the sampled bytes
+	 */
+	public AudioFormat getFormat() {
+		return audioFormat;
+	}
+	
+	/**
+	 * @return The amount of frames contained in this audio track
+	 */
+	public int getFrameCount() {
+		return audioBytes.length / audioFormat.getFrameSize();
+	}
+
+	
+	/**
+	 * @return The start of selection in frames.
+	 */
+	public int getSelectionStart() {
+		return selectionStart;
+	}
+	
+	/**
+	 * @return The length of selection in frames. less or equal to one if selection length is one frame.
+	 * 			Otherwise selection is ranged.
+	 */
+	public int getSelectionLength() {
+		return selectionLength;
+	}
+	
+	/**
+	 * Sets the selection. Clamped
+	 * 
+	 * @param start 
+	 * 			Must be larger or equal to zero. In frames.
+	 * @param length 
+	 * 			In frames. If less or equal to one, then the selection length is one frame.
+	 * 			Otherwise selection is ranged.
+	 */
+	public void setSelection(int start, int length) {
+		
+		if (start < 0) start = 0;
+		
+		if ((start + length) > getFrameCount())
+			length = getFrameCount() - start;
+		
+		selectionStart = start;
+		selectionLength = length;
+		
+		fireSubjectChanged(new SubjectChangedEvent(
+				ApolloSubjectChangedEvent.SELECTION_CHANGED));
+	}
+	
+	/**
+	 * @return The audio bytes of the selected frames
+	 */
+	public byte[] getSelectedFramesCopy() {
+		
+		int len = (selectionLength <= 0) ? 1 : selectionLength;
+		len *= audioFormat.getFrameSize();
+		
+		byte[] selectedBytes = new byte[len];
+		
+		System.arraycopy(
+				audioBytes, 
+				selectionStart * audioFormat.getFrameSize(), 
+				selectedBytes, 
+				0, 
+				len);
+		
+		return selectedBytes;
+	}
+	
+	/**
+	 * Removes the selected frames. If no <i>range</i> is selected then it will return immediatly.
+	 * raises a {@link ApolloSubjectChangedEvent#AUDIO_REMOVED} event if removed frames.
+	 * 
+	 * Resets the selection length to nothing once removed the frames (selection start
+	 * remains the same). Thus raises a selection changed event.
+	 * 
+	 * Does not allow removeing of all bytes ... must have at least frame left.
+	 * 
+	 * Can be playing in a playing state - as nothing "should" break.
+	 */
+	public void removeSelectedBytes() {
+		
+		if(selectionLength <= 1) throw new IllegalStateException("No range selected to remove");
+		
+		int len = selectionLength * audioFormat.getFrameSize();
+		int start = selectionStart * audioFormat.getFrameSize();
+		
+		if ((audioBytes.length - len) == 0) return;
+		
+		byte[] newAudioBytes = new byte[audioBytes.length - len];
+		
+		// Copy first chunk of bytes before selection
+		System.arraycopy(
+				audioBytes, 0, 
+				newAudioBytes, 0, 
+				start);
+		
+		// Copy remaining bytes after selection
+		System.arraycopy(
+				audioBytes, start + len, 
+				newAudioBytes, start, 
+				audioBytes.length - (start + len));
+		
+		audioBytes = newAudioBytes;
+		
+		setSelection(selectionStart, 0); // raises the event.
+
+		fireAudioBytesChanged(new SubjectChangedEvent(
+				ApolloSubjectChangedEvent.AUDIO_REMOVED));
+	}
+	
+	/**
+	 * Inserts bytes <b>AFTER</b> a given frame position. 
+	 * <b>TAKE NOTE:</b> Not zero-indexed. One-indexed.
+	 * 
+	 * @param bytesToAdd
+	 * 			The bytes to add. Must not be null and be length larger than zero.
+	 * 
+	 * @param format
+	 * 			The format of the audio bytes to add. Must not be null.
+	 * 
+	 * @param framePosition
+	 * 			Zero is the lower bound case, where the bytes are added at the very beggining.
+	 * 			The upper bound case is the total frame-count of this track model, where the bytes
+	 * 			are added to the very end of the track.
+	 * 
+	 * @throws IOException
+	 * 		If an error occured while converting
+	 * 
+	 * @throws IllegalArgumentException
+	 * 		If the conversion is not supported.
+	 * 
+	 */
+	public void insertBytes(byte[] bytesToAdd, AudioFormat format, int framePosition) 
+		throws IOException {
+		
+		assert(format != null);
+		assert(bytesToAdd != null);
+		assert(bytesToAdd.length > 0);
+		assert(framePosition >= 0);
+		assert(framePosition <= getFrameCount());
+		assert(SampledAudioManager.getInstance().isFormatSupportedForPlayback(format));
+		
+		// Convert format - if needs to
+		byte[] normalizedBytes = AudioIO.convertAudioBytes(bytesToAdd, format, audioFormat);
+		assert(normalizedBytes != null);
+		
+		// Insert bytes at frame position
+		int insertBytePosition = framePosition * audioFormat.getFrameSize();
+		byte[] newAudioBytes = new byte[audioBytes.length + normalizedBytes.length];
+
+		// Add preceeding audio bytes from original track
+		if (framePosition > 0) 
+			System.arraycopy(
+					audioBytes, 0, 
+					newAudioBytes, 0, 
+					insertBytePosition);
+		
+		// Add the inserted audio bytes
+		System.arraycopy(
+				normalizedBytes, 0, 
+				newAudioBytes, insertBytePosition, 
+				normalizedBytes.length);
+		
+		// Append the proceeding audio bytes of the original track
+		if (framePosition < getFrameCount()) 
+			System.arraycopy(
+					audioBytes, insertBytePosition, 
+					newAudioBytes, insertBytePosition +  normalizedBytes.length, 
+					audioBytes.length - insertBytePosition);
+		
+		// Assign the new bytes
+		audioBytes = newAudioBytes; 
+
+		// Notify observers
+		fireAudioBytesChanged(new SubjectChangedEvent(
+				ApolloSubjectChangedEvent.AUDIO_INSERTED));
+		
+	}
+	
+	/**
+	 * @see SampledTrackModel#getAllAudioBytesCopy()
+	 * 
+	 * @return The <b>actual</b>(same reference) audio bytes for this model. 
+	 */
+	public byte[] getAllAudioBytes() {
+		return audioBytes;
+	}
+	
+	/**
+	 * @see SampledTrackModel#getAllAudioBytes()
+	 * 
+	 * @return ALl the audio bytes - copied.
+	 */
+	public byte[] getAllAudioBytesCopy() {
+		byte[] copy = new byte[audioBytes.length];
+		System.arraycopy(audioBytes, 0, copy, 0, audioBytes.length);
+		return copy;
+	}
+	
+	/**
+	 * @return
+	 * 		The file path associtaed to this model.
+	 * 		Null if none is set.
+	 */
+	public String getFilepath() {
+		return currentFilepath;
+	}
+
+	/**
+	 * @param filepath
+	 * 		The filename to associate with this model.
+	 * 		Can be null.
+	 */
+	public void setFilepath(String filepath) {
+		this.currentFilepath = filepath;
+	}
+
+	/**
+	 * @return
+	 * 		The name associated to this tack. Can be null.
+	 * 
+	 * @see #setName(String)
+	 * 
+	 */
+	public String getName() {
+		return name;
+	}
+
+	/**
+	 * Sets the name for this track. Might be useful for identifying a track in
+	 * a human readable format.
+	 * 
+	 * Note that the name is not used for anything inside this package - it is
+	 * for the convience of the user only.
+	 * 
+	 * creates a {@link ApolloSubjectChangedEvent#NAME_CHANGED} event
+	 * 
+	 * @param name
+	 * 		The name associated to this tack. Can be null.
+	 */
+	public void setName(String name) {
+		if (name != this.name) {
+			this.name = name;
+			fireSubjectChanged(new SubjectChangedEvent(
+					ApolloSubjectChangedEvent.NAME_CHANGED, null));
+		}
+	}
+
+	/**
+	 * @return
+	 * 		The immutable local filename associated with this track model.
+	 */
+	public String getLocalFilename() {
+		return localFilename;
+	}
+
+}
Index: /trunk/src_apollo/org/apollo/audio/TrackSequence.java
===================================================================
--- /trunk/src_apollo/org/apollo/audio/TrackSequence.java	(revision 315)
+++ /trunk/src_apollo/org/apollo/audio/TrackSequence.java	(revision 315)
@@ -0,0 +1,314 @@
+package org.apollo.audio;
+
+import javax.sound.sampled.AudioFormat;
+
+import org.apollo.mvc.AbstractSubject;
+import org.apollo.mvc.SubjectChangedEvent;
+
+/**
+ * 
+ * A TrackSequence is a track which can reside in a track graph... for playing back at 
+ * a scheduled time and mixing with other tracks in the graph.
+ * 
+ * TrackSequence's are immutable (outside package). They can only be played once.
+ * 
+ * @author Brook Novak
+ *
+ */
+public final class TrackSequence extends AbstractSubject implements Comparable {
+
+	/** FRIENDLY: ONLY TO BE ACCESSED BY AUDIO MIXER. */
+	int currentFrame; // buffer position. Not live.
+	
+	private int startFrame;
+	
+	/** That frame at which this track sequence was suspended at */
+	private int suspendedFrame;
+	
+	/** FRIENDLY: ONLY TO BE ACCESSED BY AUDIO MIXER. */
+	int endFrame; // inclusive
+	
+	private int relativeInitiationFrame; 
+	
+	/** FRIENDLY: ONLY TO BE ACCESSED BY AUDIO MIXER. */
+	long initiationFrame; // used in pipeline. Absolute.
+	
+	/** FRIENDLY: ONLY TO BE ACCESSED BY AUDIO MIXER. */
+	boolean isBigEndian;
+	
+	// Not copied, Arrays are immutable (although contents are not!).
+	/** FRIENDLY: ONLY TO BE ACCESSED BY AUDIO MIXER. */
+	byte[] playbackAudioBytes; // snapshot of track bytes, avoid editing of model audio bytes while playing
+	
+	private boolean isPlaying;
+	
+	float volume; // friendly - for fast access in audio mixing pipeline, ... and carefully by the track model
+	
+	boolean isMuted; // friendly - for fast access in audio mixing pipeline ... and carefully by the track model
+	
+	boolean isSolo; // friendly - for fast access in audio mixing pipeline ... 
+	
+	private Object invoker;
+	
+	/** FRIENDLY: ONLY TO BE ACCESSED BY AUDIO MIXER. */
+	boolean stopPending;
+	
+	/**
+	 * Prepares track sequence for playing. Must not be in a playing state.
+	 * 
+	 * @param playbackAudioBytes
+	 * 		The audio bytes to play.
+	 * 
+	 * @param format
+	 * 		The format of the audio bytes.
+	 * 
+	 * @param startFrame
+	 * 		The frame <i>within the track</i> where to start playback. Inclusive.
+	 * 
+	 * @param endFrame
+	 * 		The frame <i>within the track</i> where to end playback. Inclusive.
+	 * 
+	 * @param relativeInitiationFrame
+	 * 		Where to initiate the frame. This is relative to where the track should start playing
+	 * 		in frames, when queued in the track graph.
+	 * 
+	 * @param invoker
+	 * 		A state object that may be of use. 
+	 * 
+	 * @throws IllegalArgumentException
+	 * 		If startFrame >= endFrame. Or startFrame is not in audio range. or end frame is not in audio range
+	 * 
+	 * @throws NullPointerException
+	 * 		If audioBytes or format is null.
+	 * 
+	 */
+	public TrackSequence(byte[] playbackAudioBytes,
+			AudioFormat format,
+			int startFrame,
+			int endFrame,
+			int relativeInitiationFrame,
+			Object invoker) {
+
+		if (startFrame >= endFrame)
+			throw new IllegalArgumentException("startFrame >= endFrame");
+
+		if (format == null)
+			throw new NullPointerException("format");
+		
+		if (playbackAudioBytes == null)
+			throw new NullPointerException("audioBytes");
+		
+		assert(SampledAudioManager.getInstance().isFormatSupportedForPlayback(format));
+
+		this.startFrame = this.currentFrame = startFrame;
+		this.endFrame = endFrame;
+		this.relativeInitiationFrame = relativeInitiationFrame;
+		this.initiationFrame = -1;
+		this.suspendedFrame = -1;
+		this.isBigEndian = format.isBigEndian();
+		this.playbackAudioBytes = playbackAudioBytes;
+		this.isPlaying = false;
+		this.invoker = invoker;
+		this.stopPending = false;
+		
+		assert (currentFrame >= 0 && currentFrame < (playbackAudioBytes.length / format.getFrameSize()));
+		assert (endFrame > currentFrame && endFrame < (playbackAudioBytes.length / format.getFrameSize()));
+		
+	}
+
+	/**
+	 * Prepares track sequence for playing. Must not be in a playing state.
+	 * 
+	 * @param playbackAudioBytes
+	 * 		The audio bytes to play.
+	 * 
+	 * @param format
+	 * 		The format of the audio bytes.
+	 * 
+	 * @param startFrame
+	 * 		The frame <i>within the track</i> where to start playback. Inclusive.
+	 * 
+	 * @param endFrame
+	 * 		The frame <i>within the track</i> where to end playback. Inclusive.
+	 * 
+	 * @param relativeInitiationFrame
+	 * 		Where to initiate the frame. This is relative to where the track should start playing
+	 * 		in frames, when queued in the track graph.
+	 * 
+	 * @throws IllegalArgumentException
+	 * 		If startFrame >= endFrame. Or startFrame is not in audio range. or end frame is not in audio range
+	 * 
+	 * @throws NullPointerException
+	 * 		If audioBytes or format is null.
+	 * 
+	 */
+	TrackSequence(
+			byte[] playbackAudioBytes,
+			AudioFormat format,
+			int startFrame,
+			int endFrame,
+			int relativeInitiationFrame) {
+		this(playbackAudioBytes, format, startFrame, endFrame, relativeInitiationFrame, null);
+	}
+	
+	/**
+	 * Order ascending by initiation frame
+	 */
+	public final int compareTo(Object o) {
+		TrackSequence other = (TrackSequence)o; // throws class cast exception
+	
+		int cmp = (new Long(initiationFrame)).compareTo(other.initiationFrame);
+
+		if (cmp == 0) {
+			
+			return (new Integer(hashCode()).compareTo(other.hashCode()));
+		}
+		
+		return cmp;
+	}
+
+	/**
+	 * @return
+	 * 		Where the track initiates/initiated. This is relative to where the track should start playing
+	 * 		in frames, when queued in the track graph.
+	 */
+	public int getRelativeInitiationFrame() {
+		return relativeInitiationFrame;
+	}
+
+	/**
+	 * @return
+	 * 		The frame position of where in the track the bytes have been mixed and buffered - not nessessarily
+	 * 		been played back. This is not its live playback position.
+	 */
+	public int getCurrentFrame() {
+		return currentFrame;
+	}
+
+	/**
+	 * @return
+	 * 		The last rendered frame when this was stopped.
+	 * 		Negitive if not played and stopped yet.
+	 */
+	public int getSuspendedFrame() {
+		return suspendedFrame;
+	}
+
+	/**
+	 * FRIENDLY: ONLY TO BE ACCESSED BY AUDIO MIXER.
+	 * Fires playback started event - later on swing thread.
+	 */
+	final void onInitiated() {
+		isPlaying = true;
+		fireSubjectChangedLaterOnSwingThread(
+				new SubjectChangedEvent(
+						ApolloSubjectChangedEvent.PLAYBACK_STARTED));
+	}
+	
+	/**
+	 * FRIENDLY: ONLY TO BE ACCESSED BY AUDIO MIXER.
+	 * Fires playback stopped event - later on swing thread.
+	 * 
+	 * @param suspendedFramePosition
+	 * 		Must be in range of start frame and end frame (inclusive)
+	 */
+	final void onStopped(int suspendedFramePosition) {
+		isPlaying = false;
+		
+		assert(suspendedFramePosition >= startFrame);
+		assert(suspendedFramePosition <= endFrame);
+		
+		this.suspendedFrame = suspendedFramePosition;
+		
+		// Dispose of audio bytes
+		playbackAudioBytes = null;
+		
+		fireSubjectChangedLaterOnSwingThread(
+				new SubjectChangedEvent(
+						ApolloSubjectChangedEvent.PLAYBACK_STOPPED));
+		
+	}
+	
+	/**
+	 * @return 
+	 * 			true if the track is playing
+	 */
+	public boolean isPlaying() {
+		return isPlaying;
+	}
+	
+	/**
+	 * 
+	 * @return
+	 * 			True if this sequence has been played before.
+	 * 
+	 */
+	public boolean hasFinished() {
+		return (playbackAudioBytes == null);
+	}
+
+	/**
+	 * @return
+	 * 		The object which invoked this track sequence. Can be null.
+	 * 		Only used externally for 3rd party benifits.
+	 */
+	public Object getInvoker() {
+		return invoker;
+	}
+	
+	/**
+	 * Note that the track won't solo unless the mixer is in solo mode.
+	 * 
+	 * @param isSolo
+	 * 		True to set this track as a solo subject.
+	 */
+	public void setSoloFlag(boolean isSolo) {
+		this.isSolo = isSolo;
+	}
+
+	public boolean isMuted() {
+		return isMuted;
+	}
+
+	public void setMuted(boolean isMuted) {
+		this.isMuted = isMuted;
+	}
+
+	public float getVolume() {
+		return volume;
+	}
+
+	public void setVolume(float volume) {
+		// Clamp volume argument
+		if (volume < 0.0f) volume = 0.0f;
+		else if (volume > 1.0f) volume = 1.0f;
+		this.volume = volume;
+	}
+
+	/**
+	 * @return
+	 * 		The absolute frame when this track is to / was played in the software mixers timeline.
+	 */
+	public long getInitiationFrame() {
+		return initiationFrame;
+	}
+
+	/**
+	 * 
+	 * @return
+	 * 		The start frame <i>within the track</i> at which this track sequence starts / started
+	 */
+	public int getStartFrame() {
+		return startFrame;
+	}
+
+	public int getEndFrame() {
+		return endFrame;
+	}
+	
+	
+	
+	
+	
+
+}
Index: /trunk/src_apollo/org/apollo/audio/structure/AbsoluteTrackNode.java
===================================================================
--- /trunk/src_apollo/org/apollo/audio/structure/AbsoluteTrackNode.java	(revision 315)
+++ /trunk/src_apollo/org/apollo/audio/structure/AbsoluteTrackNode.java	(revision 315)
@@ -0,0 +1,46 @@
+package org.apollo.audio.structure;
+
+/**
+ * An immutable class that points to a TrackGraphInfo and gives
+ * a absolute initiation time.
+ * 
+ * @see OverdubbedFrame#getAbsoluteTrackLayoutDeep()
+ * 
+ * @author Brook Novak
+ *
+ */
+public class AbsoluteTrackNode {
+	
+	private TrackGraphNode tnode;
+	private long absStartTime;
+	private String channelID;
+	private String parentFrameName;
+	
+	AbsoluteTrackNode(TrackGraphNode tnode, long absStartTime, String channelID, String parentFrameName) {
+		assert(tnode != null);
+		assert(channelID != null);
+		assert(absStartTime >= 0);
+		assert(parentFrameName != null);
+		
+		this.tnode = tnode;
+		this.absStartTime = absStartTime;
+		this.channelID = channelID;
+		this.parentFrameName = parentFrameName;
+	}
+
+	public long getABSStartTime() {
+		return absStartTime;
+	}
+
+	public TrackGraphNode getTrackNode() {
+		return tnode;
+	}
+	
+	public String getChannelID() {
+		return channelID;
+	}
+	
+	public String getParentFrameName() {
+		return parentFrameName;
+	}
+}
Index: /trunk/src_apollo/org/apollo/audio/structure/AbstractTrackGraphNode.java
===================================================================
--- /trunk/src_apollo/org/apollo/audio/structure/AbstractTrackGraphNode.java	(revision 315)
+++ /trunk/src_apollo/org/apollo/audio/structure/AbstractTrackGraphNode.java	(revision 315)
@@ -0,0 +1,71 @@
+package org.apollo.audio.structure;
+
+/**
+ * Immutable outside package.
+ * 
+ * @see {@link AudioStructureModel} for thread safe convention.
+ * 
+ * @author Brook Novak
+ *
+ */
+public abstract class AbstractTrackGraphNode {
+
+	private long initiationTime;
+	
+	private String name = null;
+	
+	/**
+	 * Constructor.
+	 * 
+	 * @param initiationTime
+	 * 		In milliseconds. Can be negative - relative.
+	 * 
+	 * @param name
+	 * 		The name to set this to. Can be null
+	 */
+	protected AbstractTrackGraphNode(long initiationTime, String name) {
+		setInitiationTime(initiationTime);
+		this.name = name;
+	}
+
+	/**
+	 * The start time in milliseconds that the track begins to play.
+	 * This start time is relative to the tracks parent frame - and can be
+	 * expressed in a negative time.
+	 * 
+	 * @return
+	 * 		The relative initiation time.
+	 */
+	public long getInitiationTime() {
+		return initiationTime;
+	}
+
+	/**
+	 * @param initiationTime
+	 * 		In milliseconds. Can be negative - relative.
+	 * 
+	 */
+	void setInitiationTime(long initiationTime) {
+		this.initiationTime = initiationTime;
+	}
+	
+	public abstract long getRunningTime();
+
+	/**
+	 * @return
+	 * 		The name given to this track. This can be null. 
+	 * 		It is not gauranteed to be unique.
+	 */
+	public String getName() {
+		return name;
+	}
+
+	/**
+	 * @param name
+	 * 		The name to set this to. Can be null
+	 */
+	void setName(String name) {
+		this.name = name;
+	}
+
+}
Index: /trunk/src_apollo/org/apollo/audio/structure/AudioStructureModel.java
===================================================================
--- /trunk/src_apollo/org/apollo/audio/structure/AudioStructureModel.java	(revision 315)
+++ /trunk/src_apollo/org/apollo/audio/structure/AudioStructureModel.java	(revision 315)
@@ -0,0 +1,1570 @@
+package org.apollo.audio.structure;
+
+import java.io.FileNotFoundException;
+import java.io.IOException;
+import java.lang.reflect.InvocationTargetException;
+import java.util.HashMap;
+import java.util.HashSet;
+import java.util.LinkedList;
+import java.util.List;
+import java.util.Map;
+import java.util.Queue;
+import java.util.Set;
+import java.util.Stack;
+
+import javax.sound.sampled.UnsupportedAudioFileException;
+import javax.swing.SwingUtilities;
+
+import org.apollo.audio.ApolloSubjectChangedEvent;
+import org.apollo.io.AudioIO;
+import org.apollo.io.AudioPathManager;
+import org.apollo.mvc.AbstractSubject;
+import org.apollo.mvc.SubjectChangedEvent;
+import org.apollo.util.ExpediteeFileTextSearch;
+import org.apollo.util.NullableLong;
+import org.apollo.util.TextItemSearchResult;
+import org.apollo.widgets.LinkedTrack;
+import org.apollo.widgets.SampledTrackEvo4;
+import org.apollo.widgets.TrackWidgetCommons;
+import org.expeditee.gui.DisplayIO;
+import org.expeditee.gui.Frame;
+import org.expeditee.gui.FrameGraphics;
+import org.expeditee.gui.FrameIO;
+import org.expeditee.gui.UserSettings;
+import org.expeditee.io.Conversion;
+import org.expeditee.items.ItemUtils;
+import org.expeditee.items.widgets.InteractiveWidget;
+
+/**
+ * A thread safe model of the heirarchical structure of a track graph...
+ * abstracted from Expeditee frames and audio widgets.
+ * 
+ * The track widgets notifty this model for keeping the structure consistent with
+ * expeditees data.
+ * 
+ * 
+ * <b>Thread safe convention:</b>
+ * OverdubbedFrame's and TrackGraphInfo are always handled on the swing thread.
+ * This class provides asynch loading routines but that is purely for loading - not
+ * handling!
+ * 
+ * The track graph is a loop-free directed graph.
+ * 
+ * A graph root is a graph of OverdubbedFrame's
+ * 
+ * Although a {@link TrackGraphNode} or a {@link LinkedTracksGraphNode} may reside
+ * in multiple graphs, there is always only one instance of them - they
+ * are just shared.
+ * 
+ * There are multiple graphs because Graphs can be mutually exlusive.
+ * Or some frames cannot be reached unless they are a starting point (aka root)
+ * 
+ * @author Brook Novak
+ *
+ */
+public class AudioStructureModel extends AbstractSubject {
+	
+	/** 
+	 * A loop-free directed graph. 
+	 * For each graph root, there can be no other graph root that is
+	 * reachable (other than itself).
+	 */
+	private Set<OverdubbedFrame> graphRoots = new HashSet<OverdubbedFrame>(); // SHARED RESOURCE
+	
+	/** All overdubbed frames loaded in memory. MAP = Framename -> Overdub instance */
+	private Map<String, OverdubbedFrame> allOverdubbedFrames = new HashMap<String, OverdubbedFrame>(); // SHARED RESOURCE
+	
+	/** For resources {@link AudioStructureModel#graphRoots} and {@link AudioStructureModel#allOverdubbedFrames} */
+	private Object sharedResourceLocker = new Object();
+	
+	private boolean cancelFetch = false;
+	
+	private DelayedModelUpdator delayedModelUpdator = null;
+
+	private static AudioStructureModel instance = new AudioStructureModel();
+	
+	private AudioStructureModel() { }
+	
+	public static AudioStructureModel getInstance() {
+		return instance;
+	}
+	
+	/**
+	 * <b>MUST NOT NE IN THE EXPEDITEE THREAD! OTHERWISE WILL DEFINITELY DEADLOCK</b>
+	 * 
+	 * Same as {@link #fetchGraph(String)} but waits for any updates to finish before
+	 * the fetch.
+	 * 
+	 * Intention: really to highlight the need to call waitOnUpdates prior to this ... but
+	 * may not bee neccessary...
+	 * 
+	 * @param rootFrameName
+	 * 		refer to #fetchGraph(String)
+	 * @return 
+	 * 		refer to #fetchGraph(String)
+	 * 
+	 * @throws NullPointerException
+	 * 		if rootFrameName is null.
+	 * 
+	 * @throws IllegalArgumentException
+	 * 		If rootFrameName is not a valid framename.
+	 * 
+	 * @throws InterruptedException
+	 * 		refer to #fetchGraph(String)
+	 * 
+	 * @throws TrackGraphLoopException
+	 * 		refer to #fetchGraph(String) - 
+	 * 		<B>However</B> - it could also be that the reason was due to the thread being interupted while
+	 * 		waiting on a update to finish. Thus may want to manually wait for updates prior - see
+	 * 		{@link #waitOnUpdates()}.
+	 * 
+	 * @see #fetchGraph(String)
+	 */
+	public synchronized OverdubbedFrame fetchLatestGraph(String rootFrameName) 
+		throws InterruptedException, TrackGraphLoopException {
+		waitOnUpdates();
+		return fetchGraph(rootFrameName);
+	}
+	
+	/**
+	 * May have to read from file if not yet loaded in memory.
+	 * Thus it could take some time.
+	 * 
+	 * Thread safe.
+	 * 
+	 * NOTE: The intention is that this is called on a dedicated thread... other than
+	 * swings thread. However once a OverdubbedFrame is returned be sure
+	 * to only use it on the swing thread by convention. 
+	 * 
+	 * <b>MUST NOT NE IN THE EXPEDITEE THREAD! OTHERWISE WILL DEFINITELY DEADLOCK</b>
+	 * 
+	 * @param rootFrameName
+	 * 		Must not be null. Mustn't be a link - must be the <i>framename</i>
+	 * 
+	 * @return
+	 * 		The overdubbed frame.
+	 * 		Null if the frame does not exist - or if it does exist but there
+	 * 		are no track / linked-track widgets on it. Not that it can return
+	 * 		a overdubbed frame that contains a heirarchy of linked overdubbed frames
+	 *      but no actual playable tracks.
+	 * 
+	 * @throws NullPointerException
+	 * 		if rootFrameName is null.
+	 * 
+	 * @throws IllegalArgumentException
+	 * 		If rootFrameName is not a valid framename.
+	 * 
+	 * @throws InterruptedException
+	 * 		If the fetch request was cancelled because the model
+	 * 		has changed in some way during the read. Must retry the fetch.
+	 * 
+	 * @throws TrackGraphLoopException
+	 * 		If the requested root introduces loops to the current graph state.
+	 * 		The loop trace will be provided in the exception. 
+	 * 
+	 * @see TrackGraphLoopException#getFullLoopTrace()
+	 * 
+	 */
+	public synchronized OverdubbedFrame fetchGraph(String rootFrameName) 
+		throws InterruptedException, TrackGraphLoopException {
+		
+		if(rootFrameName == null) throw new NullPointerException("rootFrameName");
+		if(!FrameIO.isValidFrameName(rootFrameName))
+			throw new IllegalArgumentException("the rootFrameName \"" 
+					+ rootFrameName +"\" is not a valid framename");
+		
+		// reset flag
+		cancelFetch = false; // note that race conditions here is beside the point...meaningless
+		
+		OverdubbedFrame rootODFrame = null;
+		synchronized(sharedResourceLocker) {
+			rootODFrame = allOverdubbedFrames.get(rootFrameName.toLowerCase()); 
+		}
+		
+		if (rootODFrame != null) {
+			return rootODFrame;
+		}
+		
+		// There is no overdub frame loaded for the requested frame.
+		// Thus create a new root
+		Map<String, OverdubbedFrame> newGraph = new HashMap<String, OverdubbedFrame> ();
+		rootODFrame = buildGraph(rootFrameName, newGraph); // throws InterruptedException's
+
+		// There exists no such frame or is not an actual overdubbed frame
+		if (rootODFrame == null || rootODFrame.isEmpty()) return null;
+		
+		// Must run on swing thread for checking for loops before commiting the new graph to 
+		// the model.
+		NewGraphCommitor commit = new NewGraphCommitor(rootODFrame, newGraph);
+		try {
+			SwingUtilities.invokeAndWait(commit);
+		} catch (InvocationTargetException e) {
+			e.printStackTrace();
+			assert(false);
+		}
+		
+		// Check if commit aborted
+		if (commit.abortedCommit) {
+			
+			if (cancelFetch) { // Due to cancel request?
+				throw new InterruptedException();
+			} 
+			
+			// Must be due to loop
+			assert (!commit.loopTrace.isEmpty());
+			throw new TrackGraphLoopException(commit.loopTrace);
+		}
+		
+		return rootODFrame;
+	}
+	
+	/**
+	 * Assumption: that this is called from another thread other than the swing thread.
+	 * 
+	 * @param rootFrameName
+	 * 
+	 * @param newNodes
+	 * 
+	 * @return
+	 * 		Null if rootFrameName does not exist
+	 * 
+	 * @throws InterruptedException
+	 */
+	private OverdubbedFrame buildGraph(String rootFrameName, Map<String, OverdubbedFrame> newNodes)
+		throws InterruptedException
+	{
+		// Must be a frame name, not a link
+		assert(rootFrameName != null);
+		assert(FrameIO.isValidFrameName(rootFrameName));
+		
+		// If cancelled the immediatly abort fetch
+		if (cancelFetch) { // check for cancel request
+			throw new InterruptedException();
+		}
+
+		// Look for existing node on previously loaded graph...
+		OverdubbedFrame oframe = null;
+		synchronized(sharedResourceLocker) {
+			oframe = allOverdubbedFrames.get(rootFrameName.toLowerCase());
+		}
+	
+		// Check to see if not already created this node during this recursive call
+		if (oframe == null)
+			oframe = newNodes.get(rootFrameName.toLowerCase());
+
+	    if (oframe != null) return oframe; 
+
+		// There are no existing nodes ... thus create a new node from searching for
+		// the frame from file
+		
+		// But first .. look in expeditee's cache since the cache might not be consistent with the file system yet
+		// .. or in the common case - the root could be the current frame.
+		ExpediteeCachedTrackInfoFetcher cacheFetch = new ExpediteeCachedTrackInfoFetcher(rootFrameName);
+		
+		try {
+			SwingUtilities.invokeAndWait(cacheFetch);
+		} catch (InvocationTargetException e) {
+			e.printStackTrace();
+			assert(false);
+		}
+		
+		/* Localfilename -> TrackModelData. */
+		Map<String, TrackModelData> tracks = null;
+
+		/* VirtualFilename -> LinkedTrackModelData. */
+		Map<String, LinkedTrackModelData> linkedTracks = null;
+
+		if (cacheFetch.tracks != null) {
+			
+			tracks = cacheFetch.tracks;
+			assert(cacheFetch.linkedTracks != null);
+			linkedTracks = cacheFetch.linkedTracks;
+			
+		} else { // search on file system
+		
+			String trackPrefix = ItemUtils.GetTag(ItemUtils.TAG_IWIDGET) + ": ";
+			String linkedTrackPrefix = trackPrefix;
+			
+			trackPrefix += SampledTrackEvo4.class.getName();
+			linkedTrackPrefix += LinkedTrack.class.getName();
+
+			String fullPath = null;
+			for (int i = 0; i < UserSettings.FrameDirs.size(); i++) { // RISKY CODE - IN EXPEDITEE SPACE FROM RANDOM TRHEAD
+				String possiblePath = UserSettings.FrameDirs.get(i);
+				fullPath = FrameIO.getFrameFullPathName(possiblePath, rootFrameName);
+				if (fullPath != null)
+					break;
+			}
+			
+			// does even exist?
+			if (fullPath == null) {
+				return null;
+			}
+			
+			try {
+				
+				// Perform prefix search
+				List<TextItemSearchResult> results = ExpediteeFileTextSearch.prefixSearch(
+						fullPath, 
+						new String[] {trackPrefix, linkedTrackPrefix});
+				
+				// Parse search results
+				tracks = new HashMap<String, TrackModelData>();
+				linkedTracks = new HashMap<String, LinkedTrackModelData>();
+				
+				for (TextItemSearchResult result : results) {
+				
+					// Track widget
+					if (result.text.startsWith(trackPrefix)) {
+						
+						String name = null;
+						String localFileName = null;
+						NullableLong initiationTime = null;
+						
+						for (String data : result.data) { // read data lines
+							data = data.trim();
+							
+							if (data.startsWith(SampledTrackEvo4.META_LOCALNAME_TAG) &&
+									data.length() > SampledTrackEvo4.META_LOCALNAME_TAG.length()) {
+								localFileName = data.substring(SampledTrackEvo4.META_LOCALNAME_TAG.length());
+							
+							} else if (data.startsWith(TrackWidgetCommons.META_INITIATIONTIME_TAG) 
+									&& data.length() > TrackWidgetCommons.META_INITIATIONTIME_TAG.length()) {
+								
+								try {
+									initiationTime = new NullableLong(Long.parseLong(
+											data.substring(TrackWidgetCommons.META_INITIATIONTIME_TAG.length())));
+								} catch (NumberFormatException e) { /* Consume */ }
+							
+							} else if (data.startsWith(TrackWidgetCommons.META_NAME_TAG) 
+									&& data.length() > TrackWidgetCommons.META_NAME_TAG.length()) {
+								
+								name = data.substring(TrackWidgetCommons.META_NAME_TAG.length());
+							
+							}
+						}
+						
+						// Add track to map
+						if (localFileName != null) {
+							tracks.put(localFileName, new TrackModelData(
+									initiationTime, -1, name)); // pass -1 for running time to signify that must be read from audio file
+						}
+						
+					// Linked track widget
+					} else {
+						
+						assert(result.text.startsWith(linkedTrackPrefix));
+			
+						// If the linked track infact has a link
+						if (result.explink != null && result.explink.length() > 0) {
+						
+							NullableLong initiationTime = null;
+							String virtualFilename = null;
+							String name = null;
+							
+							for (String data : result.data) { // read data lines
+								data = data.trim();
+								
+								// OK OK, Smell in code here - duplicated from above. - sorta
+								if (data.startsWith(LinkedTrack.META_VIRTUALNAME_TAG) &&
+										data.length() > LinkedTrack.META_VIRTUALNAME_TAG.length()) {
+									virtualFilename = data.substring(LinkedTrack.META_VIRTUALNAME_TAG.length());
+								
+								} else if (data.startsWith(TrackWidgetCommons.META_INITIATIONTIME_TAG) 
+										&& data.length() > TrackWidgetCommons.META_INITIATIONTIME_TAG.length()) {
+									
+									try {
+										initiationTime = new NullableLong(Long.parseLong(
+												data.substring(TrackWidgetCommons.META_INITIATIONTIME_TAG.length())));
+									} catch (NumberFormatException e) { /* Consume */ }
+								
+								} else if (data.startsWith(TrackWidgetCommons.META_NAME_TAG) 
+										&& data.length() > TrackWidgetCommons.META_NAME_TAG.length()) {
+									
+									name = data.substring(TrackWidgetCommons.META_NAME_TAG.length());
+								
+								}
+							}
+							
+							// Add linked track to map
+							if (virtualFilename != null) {
+								linkedTracks.put(virtualFilename, new LinkedTrackModelData(
+										initiationTime, result.explink, name)); // pass -1 for running time to signify that must be read from audio file
+							}
+						
+						}
+						
+		
+					}
+					
+				} // Proccess next result
+				
+			} catch (FileNotFoundException e) {
+				e.printStackTrace();
+				return null;
+			} catch (IOException e) {
+				e.printStackTrace();
+				return null;
+			}
+
+		}
+		
+		if (cancelFetch) { // check for cancel request
+			throw new InterruptedException();
+		}
+		
+		assert(tracks != null);
+		assert(linkedTracks != null);
+		
+		// Add new node (avoid infite recursion)
+		oframe = new OverdubbedFrame(rootFrameName);
+		newNodes.put(rootFrameName.toLowerCase(), oframe);
+		
+		// Load track times from file
+		for (String localFilename : tracks.keySet()) {
+			
+			if (cancelFetch) { // check for cancel request
+				throw new InterruptedException();
+			}
+			
+			TrackModelData tmodel = tracks.get(localFilename);
+			if (tmodel.runningTimeMS <= 0) {
+				try {
+					
+					tmodel.runningTimeMS = AudioIO.getRunningTime(AudioPathManager.AUDIO_HOME_DIRECTORY + localFilename);
+				
+				} catch (IOException e) {
+					e.printStackTrace();
+				} catch (UnsupportedAudioFileException e) {
+					e.printStackTrace();
+				}
+			}
+			
+			// If was able to get the runnning time ... then include in model
+			if (tmodel.runningTimeMS > 0) {
+				
+				if (tmodel.initiationTime == null) {
+					tmodel.initiationTime = new NullableLong(oframe.getFirstInitiationTime());
+					// Remember: initiation times are relative, so setting to zero
+					// could be obscure.
+				}
+				
+				oframe.addTrack(new TrackGraphNode(
+						tmodel.initiationTime.getLongValue(), 
+						localFilename, 
+						tmodel.runningTimeMS,
+						tmodel.name));
+
+			} // otheriwse omit from model.
+			
+		}
+		
+		// Get linked OverdubbedFrame's.. recurse
+		for (String virtualFilename : linkedTracks.keySet()) {
+			
+			LinkedTrackModelData linkedTrackModel = linkedTracks.get(virtualFilename);
+			
+			// At this point, the link may / maynot be absolute,
+			String linkedFrameName = linkedTrackModel.frameLink;
+			assert(linkedFrameName != null);
+			
+			if (linkedFrameName.length() == 0) continue;
+			
+			if (!(Character.isLetter(linkedFrameName.charAt(0)))) { // relative link
+				// Heres a trick: to get the frameset of the relative link - its just the
+				// current root framenames frameset
+				linkedFrameName = Conversion.getFramesetName(rootFrameName) + linkedFrameName; // WARNING: IN EXPEDITEE THREAD
+			}
+
+			OverdubbedFrame od = buildGraph(linkedFrameName, newNodes); // Recurse
+			
+			if (od == null) { // bad link or empty frame
+				od = new OverdubbedFrame(linkedFrameName);
+			}
+			
+			if (linkedTrackModel.initiationTime == null) {
+				linkedTrackModel.initiationTime = new NullableLong(oframe.getFirstInitiationTime());
+				// Remember: initiation times are relative, so setting to zero
+				// could be obscure.
+			}
+			
+			oframe.addLinkedTrack(new LinkedTracksGraphNode(
+					linkedTrackModel.initiationTime.getLongValue(), 
+					od, 
+					virtualFilename,
+					linkedTrackModel.name));
+			
+		}
+	
+		return oframe;
+	}
+	
+	/**
+	 * 
+	 * @author Brook Novak
+	 *
+	 */
+	private class TrackModelData {
+		
+		TrackModelData(NullableLong initiationTime, long runningTimeMS, String name) {
+			this.initiationTime = initiationTime;
+			this.runningTimeMS = runningTimeMS;
+			this.name = name;
+		}
+		
+		/** Null if unavilable */
+		NullableLong initiationTime;
+		
+		/** negative if unavailable */
+		long runningTimeMS; 
+		
+		/** Can be null. Trackname */
+		String name;
+	}
+
+	/**
+	 * 
+	 * @author Brook Novak
+	 *
+	 */
+	private class LinkedTrackModelData {
+		
+		LinkedTrackModelData(NullableLong initiationTime, String frameLink, String name) {
+			assert(frameLink != null);
+			this.initiationTime = initiationTime;
+			this.frameLink = frameLink;
+			this.name = name;
+		}
+		/** Null if unavilable */
+		NullableLong initiationTime;
+		
+		/** The framename */
+		String frameLink;
+		
+		/** Can be null. Trackname */
+		String name;
+	}
+	
+	/**
+	 * Used for fetching track info in expeditees cache
+	 * 
+	 * @author Brook Novak
+	 *
+	 */
+	private class ExpediteeCachedTrackInfoFetcher implements Runnable
+	{
+		private String rootFrameName;
+		
+		/** Localfilename -> TrackModelData. Not null if frame was in memory. */
+		Map<String, TrackModelData> tracks = null;
+
+		/** VirtualFilename -> LinkedTrackModelData. Not null if frame was in memory. */
+		Map<String, LinkedTrackModelData> linkedTracks = null;
+
+		ExpediteeCachedTrackInfoFetcher(String rootFrameName)
+		{
+			assert(rootFrameName != null);
+			this.rootFrameName = rootFrameName;
+		}
+		
+		public void run()
+		{
+			assert(rootFrameName != null);
+			
+			// Check if the current frame
+			Frame rootFrame = null;
+			if (DisplayIO.getCurrentFrame() != null && DisplayIO.getCurrentFrame().getName() != null
+					&& DisplayIO.getCurrentFrame().getName().equals(rootFrameName)) {
+				rootFrame = DisplayIO.getCurrentFrame();
+			} else {
+				// Check if in cache
+				rootFrame = FrameIO.FrameFromCache(rootFrameName);
+			}
+			
+			// Frame exists in memory... rummage around for track meta data
+			if (rootFrame != null) {
+				tracks = new HashMap<String, TrackModelData>();
+				linkedTracks =  new HashMap<String, LinkedTrackModelData>();
+				
+				for (InteractiveWidget iw : rootFrame.getInteractiveWidgets()) {
+					
+					if (iw instanceof SampledTrackEvo4) {
+						SampledTrackEvo4 sampledTrackWidget = (SampledTrackEvo4)iw;
+
+						TrackGraphNode tinf = 
+							AudioStructureModel.getInstance().getTrackGraphInfo(
+									sampledTrackWidget.getLocalFileName(), 
+									rootFrameName);
+						
+						NullableLong initTime = (tinf != null) ? new NullableLong(tinf.getInitiationTime()) :
+							NullableLong.fromLong(sampledTrackWidget.getInitiationTimeFromMeta());
+						if (initTime != null && initTime.getLongValue() < 0) initTime = null; // signify to load default from frame
+						
+						long runningTime = sampledTrackWidget.getRunningMSTimeFromRawAudio();
+						if (runningTime <= 0) runningTime = -1; // signify to load from file
+						
+						tracks.put(sampledTrackWidget.getLocalFileName(), 
+								new TrackModelData(
+										initTime,
+										runningTime,
+										sampledTrackWidget.getName()));
+						
+						// NOTE: If the audio was sotred in a recovery file - then the widget would
+						// be deleted - thus no need to consider recovery files.
+						
+					} else if (iw instanceof LinkedTrack) {
+						LinkedTrack linkedTrackWidget = (LinkedTrack)iw;
+			
+						LinkedTracksGraphNode ltinf = 
+							AudioStructureModel.getInstance().getLinkedTrackGraphInfo(
+									linkedTrackWidget.getVirtualFilename(), 
+									rootFrameName);
+			
+						NullableLong initTime = (ltinf != null) ? new NullableLong(ltinf.getInitiationTime()) :
+							NullableLong.fromLong(linkedTrackWidget.getInitiationTimeFromMeta());
+						if (initTime != null && initTime.getLongValue() < 0) initTime = null; // signify to load default from frame
+						
+						// Don't consider track-links without any links
+						if (linkedTrackWidget.getLink() != null) {
+							linkedTracks.put(linkedTrackWidget.getVirtualFilename(), 
+									new LinkedTrackModelData(
+											initTime,
+											linkedTrackWidget.getLink(),
+											linkedTrackWidget.getName()));
+						}
+
+					}
+					
+				}
+				
+			}
+		}
+	}
+	
+
+	/**
+	 * Used for commiting a new graph in a thread-safe way.
+	 * Checks for loops in the graph before commiting.
+	 * 
+	 * If did not commit, then {@link #abortedCommit} will be true. Otherwise it will be false.
+	 * 
+	 * if {@link #loopTrace} is not empty after the run, then the commit
+	 * was aborted because there was a loop. (And {@link #loopTrace} contains the trace).
+	 * The only other reason for aborting the commit was if a cancel was requested 
+	 * (@see TrackGraphModel#cancelFetch)
+	 * 
+	 * @author Brook Novak
+	 * 
+	 *
+	 */
+	private class NewGraphCommitor implements Runnable
+	{
+		private final OverdubbedFrame rootODFrame;
+		private final Map<String, OverdubbedFrame> newGraph;
+		
+		private boolean abortedCommit = false;
+		
+		/** If not empty after the run, then the commit */
+		Stack<OverdubbedFrame> loopTrace = new Stack<OverdubbedFrame>();
+
+		NewGraphCommitor(OverdubbedFrame rootODFrame, Map<String, OverdubbedFrame> newGraph)
+		{
+			assert(rootODFrame != null);
+			assert(newGraph != null);
+			
+			this.rootODFrame = rootODFrame;
+			this.newGraph = newGraph;
+		}
+		
+		public void run()
+		{
+			
+			// Theoretically this would never need to be called ... but for super safety
+			// keep sync block - since dealing with shared resources.
+			synchronized(sharedResourceLocker) {
+				
+				// At anytime during this point, the new graph could become invalid
+				// during this time while officially adding it to the "consistant" model..
+				// which is obviously BAD.
+				if (cancelFetch) {
+					abortedCommit = true;
+					return; // important: while locked shared resources
+				}
+				
+				// Check that the graph is loop free.
+				boolean ilf = isLoopFree(rootODFrame, loopTrace);
+				if (ilf) { // check from existing roots POV's
+					for (OverdubbedFrame existingRoot : graphRoots) {
+						loopTrace.clear();
+						ilf = isLoopFree(existingRoot, loopTrace);
+						if (!ilf) break;
+					}
+				}
+				
+				if (!ilf) {
+					assert (!loopTrace.isEmpty()); // loopTrace will contain the trace
+					abortedCommit = true;
+					return;
+				} else {
+					assert (loopTrace.isEmpty());
+				}
+				
+				// Since we are creating a new root, existing roots might
+				// be reachable from the new root, thus remove reachable
+				// roots ... since they are no longer roots ...
+				List<OverdubbedFrame> redundantRoots = new LinkedList<OverdubbedFrame>();
+				for (OverdubbedFrame existingRoot : graphRoots) {
+					if (rootODFrame.getChild(existingRoot.getFrameName()) != null) {
+						redundantRoots.add(existingRoot); // this root is reachable from the new root
+					}
+				}
+	
+				// Get rid of the redundant roots
+				graphRoots.removeAll(redundantRoots);
+				
+				// Commit the new loop-free graph to the model
+				graphRoots.add(rootODFrame); // add the mutex or superceeding root
+				allOverdubbedFrames.putAll(newGraph); // Include new graph into all allOverdubbedFrames
+			}
+		}
+	}
+//	
+//	/**
+//	 * Determines if a node is reachable from a given node.
+//	 * 
+//	 * @param source
+//	 * 		Must not be null.
+//	 * 
+//	 * @param target
+//	 * 		Must not be null.
+//	 * 
+//	 * @param visited
+//	 * 		Must be empty. Must not be null.
+//	 * 
+//	 * @return
+//	 * 			True if target is reachable via source.
+//	 */
+//	private boolean isReachable(OverdubbedFrame source, OverdubbedFrame target, Set<OverdubbedFrame> visited) {
+//		assert(source != null);
+//		assert(target != null);
+//		assert(visited != null);
+//		
+//		// Base cases
+//		if (source == target) return true;
+//		else if (visited.contains(source)) return false;
+//		
+//		// Remember visited node
+//		visited.add(source);
+//		
+//		// Recurse
+//		for (Iterator<LinkedTracksGraphInfo> itor = source.getLinkedTrackIterator(); itor.hasNext();) {
+//			LinkedTracksGraphInfo lti = itor.next();
+//			
+//			if (isReachable(lti.getLinkedFrame(), target, visited)) {
+//				return true;
+//			}
+//		}
+//		
+//		// Reached end of this nodes links - found no match
+//		return false;
+//	}
+
+	/**
+	 * Determines if a graph is loop free from a starting point.
+	 * <B>MUST BE ON THE SWING THREAD</B>
+	 * 
+	 * @param current
+	 * 		Where to look for loops from.	
+	 * 
+	 * @param visitedStack
+	 * 		Must be empty. Used internally. If the
+	 * 		function returns false, then the stack will provde a trace of the loop.
+	 * 
+	 * @return
+	 * 		True if the graph at current is loop free.
+	 */
+	private boolean isLoopFree(OverdubbedFrame current, Stack<OverdubbedFrame> visitedStack) {
+		
+		if (visitedStack.contains(current)) {  // found a loop
+			visitedStack.add(current); // add to strack for loop trace.
+			return false;
+		}
+
+		visitedStack.push(current); // save current node to stack
+
+		for (LinkedTracksGraphNode tl : current.getLinkedTracksCopy()) {
+			
+			boolean ilf = isLoopFree(tl.getLinkedFrame(), visitedStack);
+			
+			if (!ilf) return false;
+			
+		}
+		
+		visitedStack.pop(); // pop the current node
+		
+		return true;
+		
+	}
+	
+	/**
+	 * Gets a TrackGraphInfo. <b>MUST BE ON EXPEDITEE THREAD</b>
+	 * 
+	 * @param localFilename
+	 * 			Must not be null.
+	 * 
+	 * @param parentFrameName
+	 * 			If null - the search will be slower.
+	 * 
+	 * @return
+	 * 		The TrackGraphInfo for the given track. Null if not in model
+	 */
+	public TrackGraphNode getTrackGraphInfo(String localFilename, String parentFrameName) {
+		
+		synchronized(sharedResourceLocker) { 
+			
+			assert(localFilename != null);
+			
+			if (parentFrameName != null) {
+				OverdubbedFrame odframe = allOverdubbedFrames.get(parentFrameName.toLowerCase());
+				
+		
+				if (odframe != null) { 
+					return odframe.getTrack(localFilename);
+				}
+				
+			} else { // parentFrameName is null .. do search
+				
+				for (OverdubbedFrame odframe : allOverdubbedFrames.values()) {
+					TrackGraphNode tinf = odframe.getTrack(localFilename);
+					if (tinf != null) return tinf;
+				}
+			}
+		}
+			
+		return null;
+	}
+	
+
+	/**
+	 * Gets a TrackGraphInfo. <b>MUST BE ON EXPEDITEE THREAD</b>
+	 * 
+	 * @param virtualFilename
+	 * 			Must not be null.
+	 * 
+	 * @param parentFrameName
+	 * 			If null - the search will be slower.
+	 * 
+	 * @return
+	 * 		The LinkedTracksGraphInfo for the given virtualFilename. Null if not in model
+	 */
+	public LinkedTracksGraphNode getLinkedTrackGraphInfo(String virtualFilename, String parentFrameName) {
+		
+		synchronized(sharedResourceLocker) { 
+			
+			assert(virtualFilename != null);
+			
+			if (parentFrameName != null) {
+				OverdubbedFrame odframe = allOverdubbedFrames.get(parentFrameName.toLowerCase());
+				
+		
+				if (odframe != null) { 
+					return odframe.getLinkedTrack(virtualFilename);
+				}
+				
+			} else {
+				
+				for (OverdubbedFrame odframe : allOverdubbedFrames.values()) {
+					LinkedTracksGraphNode ltinf = odframe.getLinkedTrack(virtualFilename);
+					if (ltinf != null) return ltinf;
+				}
+				
+			}
+		}
+			
+		return null;
+	}
+	
+	
+	/**
+	 * <B>MUST BE ON SWING THREAD</B>.
+	 * Gets the parent ODFrame of a track - given the tracks local filename.
+	 * 
+	 * @param localFilename
+	 * 		The local filename of the track to get the frame for. Must not be null.
+	 * 
+	 * @return
+	 * 		The parent. Null if no parent exists.
+	 */
+	public OverdubbedFrame getParentOverdubbedFrame(String localFilename) {
+		assert(localFilename != null);
+		
+		synchronized (allOverdubbedFrames) {
+			for (OverdubbedFrame odf : allOverdubbedFrames.values()) {
+				if (odf.containsTrack(localFilename)) {
+					return odf;
+				}
+			}
+		}
+		
+		return null;
+	}
+	
+	/**
+	 * Gets an overdubbed frame representation of a given frame.
+	 * 
+	 * @param framename
+	 * 		The name of the frame.
+	 * 
+	 * @return
+	 * 		The OverdubbedFrame for the given framename. Null if does not exist.
+	 */
+	public OverdubbedFrame getOverdubbedFrame(String framename) {
+		assert(framename != null);
+		
+		synchronized (allOverdubbedFrames) {
+			return allOverdubbedFrames.get(framename.toLowerCase());
+		}
+	}
+	
+	/**
+	 * <B>MUST BE ON SWING THREAD</B>.
+	 * Keeps model consistent with expeditee.
+	 * 
+	 * @param localFilename
+	 * 		Must not be null.
+	 * 
+	 * @param parentFrameName
+	 * 		Can be null.
+	 * 
+	 * @param currentRunningTime
+	 * 		I.e. the new running time after the edit. Must be larger than zero. In milliseconds. 
+	 */
+	public void onTrackWidgetAudioEdited(String localFilename, String parentFrameName, long currentRunningTime) {
+		
+		boolean doNotify = false;
+		TrackGraphNode tinf = null;
+		
+		synchronized(sharedResourceLocker) { // IMPORTANT: Must wait for new graphs to be added to the shared resources
+			
+			assert(localFilename != null);
+			assert (currentRunningTime > 0);
+
+			// Locate parent frame
+			OverdubbedFrame odframe = null;
+			
+			if (parentFrameName != null) odframe = allOverdubbedFrames.get(parentFrameName.toLowerCase());
+			else odframe = getParentOverdubbedFrame(localFilename);
+
+			// adjust running time in model
+			if (odframe != null) { // is loaded?
+				tinf = odframe.getTrack(localFilename);
+				assert(tinf != null); // due to the assumption that the model is consistent
+				if (tinf.getRunningTime() != currentRunningTime) {
+					tinf.setRunningTime(currentRunningTime);
+					// Note: a fetch might be waiting on this - i.e in progress. Thus must be cancelled.
+					// It will die in its own time - and will always be cancelled because it locks
+					// the current locked object.
+					cancelFetch = doNotify = true;
+				}
+			}
+
+	
+		}
+		
+		// Notify observers.
+		if (doNotify)
+			fireSubjectChanged(new SubjectChangedEvent(ApolloSubjectChangedEvent.GRAPH_TRACK_EDITED, tinf));
+	}
+	
+	/**
+	 * <B>MUST BE ON SWING THREAD</B>.
+	 * Keeps model consistent with expeditee.
+	 * 
+	 * @param localFilename
+	 * 		Must not be null.
+	 * 
+	 * @param parentFrameName
+	 * 		Can be null.
+	 * 
+	 * @param newInitiationTime
+	 * 		In milliseconds.Relative - i.e. can be negative
+	 * 
+	 * @param name
+	 * 		The name given to the linked track. Can be null if there is no name...
+	 * 
+	 * @param currentRunningTime
+	 * 		Must be larger than zero. In milliseconds. Used in case the model has not been create for the widget.
+	 */
+	public void onTrackWidgetAnchored(
+			String localFilename, String parentFrameName, 
+			long newInitiationTime, long currentRunningTime,
+			String name) {
+		
+		boolean doNotify = false;
+		TrackGraphNode tinf = null;
+		
+		synchronized(sharedResourceLocker) { // IMPORTANT: Must wait for new graphs to be added to the shared resources
+			
+			assert(localFilename != null);
+
+			// Locate parent frame
+			OverdubbedFrame odframe = null;
+			
+			if (parentFrameName != null) odframe = allOverdubbedFrames.get(parentFrameName.toLowerCase());
+			else odframe = getParentOverdubbedFrame(localFilename);
+			
+			// adjust initiation time in model
+			if (odframe != null) { // is loaded?
+				tinf = odframe.getTrack(localFilename);
+				
+				if (tinf != null && tinf.getInitiationTime() != newInitiationTime) {
+					tinf.setInitiationTime(newInitiationTime);
+					cancelFetch = doNotify = true;
+					
+				} else { // if there is no model but overdub frame is in memory - then create new model for this track.
+					
+					tinf = new TrackGraphNode(
+							newInitiationTime, 
+							localFilename, 
+							currentRunningTime,
+							name);
+					
+					odframe.addTrack(tinf); // safe because on swing thread 
+					
+					// Note: a fetch might be waiting on this - i.e in progress. Thus must be cancelled.
+					cancelFetch = doNotify = true;
+					// It will die in its own time - and will always be cancelled because it locks
+					// the current locked object.
+				}
+				
+
+			}
+
+		}
+		
+		// Notify observers.
+		if (doNotify)
+			fireSubjectChanged(new SubjectChangedEvent(ApolloSubjectChangedEvent.GRAPH_TRACK_ADDED, tinf));
+		
+	}
+	
+	/**
+	 * <B>MUST BE ON SWING THREAD</B>.
+	 * Keeps model consistent with expeditee.
+	 * 
+	 * Note: Invoke when picked up - not when a frame changes... also if it
+	 * is removed due to XRaymode - in that case the whole model will be released.
+	 * 
+	 * @param localFilename
+	 * 		Must not be null.
+	 * 
+	 * @param parentFrameName
+	 * 		Can be null. If given will be faster.
+	 * 
+	 */
+	public void onTrackWidgetRemoved(String localFilename, String parentFrameName) {
+		TrackGraphNode tinf = null;
+		boolean doNotify = false;
+		
+		synchronized(sharedResourceLocker) { // IMPORTANT: Must wait for new graphs to be added to the shared resources
+
+			if (FrameGraphics.isXRayMode()) { // discard whole model
+
+				// Neccessary because if the user goes into xray then moves to a new frame
+				// then it wil screw everything up. Also note they can delete the text source items
+				// in xray mode.
+				// This basically will cause short load times to occur again...
+				allOverdubbedFrames.clear();
+				graphRoots.clear();
+
+				// Note: a fetch might be waiting on this - i.e in progress. Thus must be cancelled.
+				cancelFetch = doNotify = true;
+				// It will die in its own time - and will always be cancelled because it locks
+				// the current locked object.
+				
+			} else {
+				
+				// Locate parent frame
+				OverdubbedFrame odframe = null;
+				
+				if (parentFrameName != null) odframe = allOverdubbedFrames.get(parentFrameName.toLowerCase());
+				else odframe = getParentOverdubbedFrame(localFilename);
+				
+				if (odframe != null) {
+					tinf = odframe.removeTrack(localFilename);
+					assert(tinf != null);
+					cancelFetch = doNotify = true;
+				}
+				
+			}
+
+		}
+			
+		// Notify observers.
+		if (doNotify)
+			fireSubjectChanged(new SubjectChangedEvent(ApolloSubjectChangedEvent.GRAPH_TRACK_REMOVED, tinf));
+	}
+	
+	/**
+	 * <B>MUST BE ON SWING THREAD</B>.
+	 * Keeps model consistent with expeditee.
+	 * 
+	 * @param localFilename
+	 * 		Must not be null.
+	 * 
+	 * @param parentFrameName
+	 * 		Can be null.
+	 * 
+	 * @param currentRunningTime
+	 * 		I.e. the new running time after the edit. Must be larger than zero. In milliseconds. 
+	 */
+	public void onTrackWidgetNameChanged(String localFilename, String parentFrameName, String newName) {
+
+		boolean doNotify = false;
+		
+		synchronized(sharedResourceLocker) { // IMPORTANT: Must wait for new graphs to be added to the shared resources
+			
+			assert(localFilename != null);
+
+			// Locate parent frame
+			OverdubbedFrame odframe = null;
+			
+			if (parentFrameName != null) odframe = allOverdubbedFrames.get(parentFrameName.toLowerCase());
+			else odframe = getParentOverdubbedFrame(localFilename);
+			
+			// adjust name in model
+			if (odframe != null) { // is loaded?
+				AbstractTrackGraphNode tinf = odframe.getTrack(localFilename);
+				assert(tinf != null); // due to the assumption that the model is consistent
+				if (tinf.getName() != newName) {
+					tinf.setName(newName);
+					// Note: a fetch might be waiting on this - i.e in progress. Thus must be cancelled.
+					cancelFetch = doNotify = true;
+					// It will die in its own time - and will always be cancelled because it locks
+					// the current locked object.
+				} 
+			}
+
+		}
+		
+		// Notify observers.
+		if (doNotify)
+			fireSubjectChanged(new SubjectChangedEvent(ApolloSubjectChangedEvent.NAME_CHANGED, localFilename));
+	}
+	
+
+	/**
+	 * <B>MUST BE ON SWING THREAD</B>.
+	 * Keeps model consistent with expeditee.
+	 * 
+	 * @param virtualFilename
+	 * 		Must not be null.
+	 * 
+	 * @param parentFrameName
+	 * 		Can be null.
+	 * 
+	 * @param currentRunningTime
+	 * 		I.e. the new running time after the edit. Must be larger than zero. In milliseconds. 
+	 */
+	public void onLinkedTrackWidgetNameChanged(String virtualFilename, String parentFrameName, String newName) {
+
+		boolean doNotify = false;
+		
+		synchronized(sharedResourceLocker) { // IMPORTANT: Must wait for new graphs to be added to the shared resources
+			
+			assert(virtualFilename != null);
+
+			// Locate parent frame
+			OverdubbedFrame odframe = null;
+			
+			if (parentFrameName != null) odframe = allOverdubbedFrames.get(parentFrameName.toLowerCase());
+			else odframe = getParentOverdubbedFrame(virtualFilename);
+			
+			// adjust name in model
+			if (odframe != null) { // is loaded?
+				AbstractTrackGraphNode tinf = odframe.getLinkedTrack(virtualFilename);
+				assert(tinf != null); // due to the assumption that the model is consistent
+				if (tinf.getName() != newName) {
+					tinf.setName(newName);
+					// Note: a fetch might be waiting on this - i.e in progress. Thus must be cancelled.
+					cancelFetch = doNotify = true;
+					// It will die in its own time - and will always be cancelled because it locks
+					// the current locked object.
+				} 
+			}
+		}
+		
+		// Notify observers.
+		if (doNotify)
+			fireSubjectChanged(new SubjectChangedEvent(ApolloSubjectChangedEvent.NAME_CHANGED, virtualFilename));
+	}
+	
+	
+	/**
+	 * <B>MUST BE ON SWING THREAD</B>.
+	 * Keeps model consistent with expeditee.
+	 * 
+	 * Doe not use for changing a the widgets link. Must first remove, then acnhor with new link - 
+	 * tracklinks links are immutable.
+	 * 
+	 * @param virtualFilename
+	 * 		Must not be null.
+	 * 
+	 * @param parentFrameName
+	 * 		Must not be null.
+	 * 
+	 * @param newInitiationTime
+	 * 		In milliseconds.Relative - i.e. can be negative
+	 * 
+	 * @param absoluteLinkedFrame
+	 * 		Must not be null. Must be a valid framename (absolute).
+	 * 
+	 * @param name
+	 * 		The name given to the linked track. Can be null if there is no name...
+	 * 
+	 */
+	public void onLinkedTrackWidgetAnchored(
+			String virtualFilename, String parentFrameName, 
+			long newInitiationTime, String absoluteLinkedFrame,
+			String name) {
+		
+		boolean doNotify = false;
+		
+		synchronized(sharedResourceLocker) { // IMPORTANT: Must wait for new graphs to be added to the shared resources
+			
+			assert(virtualFilename != null);
+			assert(parentFrameName != null);
+			assert(absoluteLinkedFrame != null);
+			assert(FrameIO.isValidFrameName(absoluteLinkedFrame));
+			
+			// Locate parent frame
+			OverdubbedFrame odframe = allOverdubbedFrames.get(parentFrameName.toLowerCase());
+			
+			if (odframe != null) { // is loaded?
+				
+				LinkedTracksGraphNode linkInf = odframe.getLinkedTrack(virtualFilename);
+
+				if (linkInf != null) {
+					
+					// Update the initation time
+					linkInf.setInitiationTime(newInitiationTime);
+					
+					// The link should be consistant - check for miss-use of procedure call
+					assert(linkInf.getLinkedFrame().getFrameName().equalsIgnoreCase(absoluteLinkedFrame));
+										
+				} else { // if there is no model but overdub frame is in memory - then create new model for this track.
+					
+					updateLater(new LinkedTrackUpdate(
+							newInitiationTime, absoluteLinkedFrame,
+							virtualFilename, parentFrameName, name));
+					
+				}
+				
+				// Note: a fetch might be waiting on this - i.e in progress. Thus must be cancelled.
+				cancelFetch = doNotify = true;
+				// It will die in its own time - and will always be cancelled because it locks
+				// the current locked object.
+			}
+
+		}
+		
+		// Notify observers.
+		if (doNotify)
+			fireSubjectChanged(new SubjectChangedEvent(ApolloSubjectChangedEvent.GRAPH_LINKED_TRACK_ADDED, virtualFilename));
+		
+	}
+	
+	/**
+	 * <B>MUST BE ON SWING THREAD</B>.
+	 * Keeps model consistent with expeditee.
+	 * 
+	 * Note: Invoke when picked up - not when a frame changes... also if it
+	 * is removed due to XRaymode - in that case the whole model will be released.
+	 * 
+	 * <b>Also</b> invoke if the track tracks link has changed.
+	 * 
+	 * @param virtualFilename
+	 * 		Must not be null.
+	 * 
+	 * @param parentFrameName
+	 * 		Can be null. If given will be faster.
+	 * 
+	 */
+	public void onLinkedTrackWidgetRemoved(String virtualFilename, String parentFrameName) {
+		
+		boolean doNotify = false;
+		
+		synchronized(sharedResourceLocker) { // IMPORTANT: Must wait for new graphs to be added to the shared resources
+
+	
+			if (FrameGraphics.isXRayMode()) { // discard whole model
+
+				// Neccessary because if the user goes into xray then moves to a new frame
+				// then it wil screw everything up. Also note they can delete the text source items
+				// in xray mode.
+				// This basically will cause short load times to occur again...
+				allOverdubbedFrames.clear();
+				graphRoots.clear();
+				
+				
+				// Note: a fetch might be waiting on this - i.e in progress. Thus must be cancelled.
+				cancelFetch = doNotify = true;
+				// It will die in its own time - and will always be cancelled because it locks
+				// the current locked object.
+			} else {
+				
+				// Locate parent frame
+				OverdubbedFrame odframe = null;
+				
+				if (parentFrameName != null) {
+					odframe = allOverdubbedFrames.get(parentFrameName.toLowerCase());
+				} else { 
+					for (OverdubbedFrame odf : allOverdubbedFrames.values()) {
+						if (odf.containsLinkedTrack(virtualFilename)) {
+							odframe = odf;
+							break;
+						}
+					}
+				}
+				
+				if (odframe != null) {
+					LinkedTracksGraphNode linkInf = odframe.getLinkedTrack(virtualFilename);
+					
+					if (linkInf != null) {
+						boolean didRemove = odframe.removeLinkedTrack(linkInf);
+						assert(didRemove);
+						
+						// Track links are the actual links in the directed graph. THus if they are
+						// removed then the graph must be checked for creating new root nodes.
+						// That is, the removed link may have isolated a node (or group of nodes)
+						// for which must be reachable via their own start state...
+						boolean isReachable = false;
+						for (OverdubbedFrame existingRoot : graphRoots) {
+							if (existingRoot.getChild(linkInf.getLinkedFrame().getFrameName()) != null) {
+								isReachable = true;
+								break;
+							}
+							
+						}
+						
+						// Ensure that the frame is reachable
+						if (!isReachable) {
+							graphRoots.add(linkInf.getLinkedFrame());
+						}
+						
+						// Note: a fetch might be waiting on this - i.e in progress. Thus must be cancelled.
+						cancelFetch = doNotify = true;
+						// It will die in its own time - and will always be cancelled because it locks
+						// the current locked object.
+					
+					}
+					
+					
+				}
+				
+			}
+		
+		}
+			
+		// Notify observers.
+		if (doNotify)
+			fireSubjectChanged(new SubjectChangedEvent(ApolloSubjectChangedEvent.GRAPH_LINKED_TRACK_REMOVED, virtualFilename));
+	}
+	
+	/**
+	 * @return
+	 * 			True if the graph model is updating.
+	 */
+	public boolean isUpdating() {
+		return (delayedModelUpdator != null && delayedModelUpdator.isAlive());
+	}
+
+	
+	/**
+	 * Waits for updates to finish.
+	 * 
+	 * <b>MUST NOT BE ON SWING THREAD - OR MAY DEADLOCK</b>
+	 * 
+	 * @throws InterruptedException
+	 * 		if any thread has interrupted the current thread
+	 */
+	public void waitOnUpdates() throws InterruptedException {
+		if (delayedModelUpdator != null && delayedModelUpdator.isAlive()) {
+			delayedModelUpdator.join();
+		}
+	}
+	
+	/**
+	 * Queues an update for the consistant model to be updated later... since it may take some time.
+	 * 
+	 * @param update
+	 * 		The update to do. Must not be null.
+	 */
+	private void updateLater(LinkedTrackUpdate update) {
+		
+		synchronized(updateQueue) {
+			
+			assert(update != null);
+
+			// Add the update tot he queu
+			updateQueue.add(update);
+		
+			// Ensure that the update thread is alive
+			if (delayedModelUpdator == null || !delayedModelUpdator.isAlive()) {
+				delayedModelUpdator = new DelayedModelUpdator();
+				delayedModelUpdator.start();
+			}
+			
+		}	
+			
+	}
+
+	private Queue<LinkedTrackUpdate> updateQueue = new LinkedList<LinkedTrackUpdate>(); // SHARED RESOURCE
+	
+	/**
+	 * Used for queuing update data.
+	 * 
+	 * 
+	 * @author Brook Novak
+	 *
+	 */
+	private class LinkedTrackUpdate extends LinkedTrackModelData {
+		
+		LinkedTrackUpdate(
+				long initiationTime, 
+				String absoluteLink, 
+				String virtualFilename, 
+				String parentFrameToAddTo,
+				String name) {
+			super(new NullableLong(initiationTime), absoluteLink, name);
+			
+			assert(virtualFilename != null);
+			assert(parentFrameToAddTo != null);
+			assert(FrameIO.isValidFrameName(absoluteLink));
+			
+			this.virtualFilename = virtualFilename;
+			this.parentFrameToAddTo = parentFrameToAddTo;
+		}
+		
+		String virtualFilename;
+		String parentFrameToAddTo;
+		
+	}
+	
+	/**
+	 * 
+	 * @author Brook Novak
+	 *
+	 */
+	private class DelayedModelUpdator extends Thread {
+		
+		public void run() {
+			while (true) {
+				LinkedTrackUpdate update;
+				synchronized(updateQueue) {
+					if (updateQueue.isEmpty()) return; // important: only quits when updateQueue is synched
+					update = updateQueue.poll();
+				}
+				
+				assert(update != null);
+				
+				// Keep trying the update until success or encountered loops
+				while (true) {
+					try {
+						
+						OverdubbedFrame odframe = fetchGraph(update.frameLink);
+						if (odframe == null) { // does not exist or is empty...
+							odframe = new OverdubbedFrame(update.frameLink);
+						}
+						
+
+						// add to parent only if does not already exist - also being aware of loops
+						final LinkedTrackUpdate updateData = update;
+						final OverdubbedFrame linkedODFrame = odframe;
+						
+						try {
+							SwingUtilities.invokeAndWait(new Runnable() {
+								
+								public void run() { // on swing thread
+
+									synchronized(sharedResourceLocker) {
+										
+										try { // The fetch will be cancelled after this block - within the lock
+																	
+											OverdubbedFrame parentFrame = allOverdubbedFrames.get(updateData.parentFrameToAddTo.toLowerCase());
+											if (parentFrame == null) {
+												// No need to both adding - because model does not even reference this new link...
+												// must have cleared while updating... e.g. user could have switched into xray mode.
+												return;
+											}
+											
+											assert(updateData.initiationTime != null);
+											
+											// Create the linked track instance
+											LinkedTracksGraphNode linktInf = new LinkedTracksGraphNode(
+													updateData.initiationTime.getLongValue(), 
+													linkedODFrame, 
+													updateData.virtualFilename,
+													updateData.name);
+
+											// Check that the graph will be loop free
+											parentFrame.addLinkedTrack(linktInf); // ADDING TEMPORARILY
+
+											// Check from parent frame
+											Stack<OverdubbedFrame> loopTrace = new Stack<OverdubbedFrame>();
+											if (!isLoopFree(parentFrame, loopTrace)) {
+												parentFrame.removeLinkedTrack(linktInf);
+												// Ignore link - has loop
+												return;
+											}
+											
+											// Check for all existing graph roots
+											for (OverdubbedFrame existingRoot : graphRoots) {
+												loopTrace.clear();
+												if (!isLoopFree(existingRoot, loopTrace)) {
+													parentFrame.removeLinkedTrack(linktInf);
+													// Ignore link - has loop
+													 return;
+												}
+											}
+			
+											// Otherwise leave the link in its place 
+											
+											// Ensure that the linked frame is in the allOverdubbedFrames set
+											allOverdubbedFrames.put(linkedODFrame.getFrameName().toLowerCase(), linkedODFrame);
+										
+										} finally { // IMPORTANT: CANCEL WHILE LOCKED
+											
+											// Cancel any fetches
+											cancelFetch = true;
+										}	
+										
+									}  // release lock
+								
+										
+								}
+							});
+						} catch (InvocationTargetException e) {
+							e.printStackTrace();
+							assert(false);
+						}
+		
+						// Done with this update...
+						break;
+						
+						
+					} catch (InterruptedException e) { // Canceled
+						// Consume and retry
+						
+					} catch (TrackGraphLoopException e) { // bad link
+						// Consume - since is fine - the model will just ignore the link
+						break;
+					}
+				} // retry fetch
+				
+			} // proccess next update
+			
+		} // finished updating
+	}
+	
+}
Index: /trunk/src_apollo/org/apollo/audio/structure/LinkedTracksGraphNode.java
===================================================================
--- /trunk/src_apollo/org/apollo/audio/structure/LinkedTracksGraphNode.java	(revision 315)
+++ /trunk/src_apollo/org/apollo/audio/structure/LinkedTracksGraphNode.java	(revision 315)
@@ -0,0 +1,59 @@
+package org.apollo.audio.structure;
+
+/**
+ * Immutable.
+ * 
+ * @see {@link AudioStructureModel} for thread safe convention.
+ * 
+ * @author Brook Novak
+ *
+ */
+public class LinkedTracksGraphNode extends AbstractTrackGraphNode {
+
+	private OverdubbedFrame linkedFrame;
+	
+	private String virtualFilename; // immutable - assigned on construction
+	
+	/**
+	 * 
+	 * @param linkedFrame
+	 * 
+	 * @param virtualFilename
+	 * 
+  	 * @param initiationTime
+  	 *		In milliseconds. Can be negative - relative.
+  	 *
+	 * @param name
+	 * 		The name to set this to. Can be null
+	 * 
+	 * @throws NullPointerException
+	 * 		if linkedFrame is null.
+	 */
+	LinkedTracksGraphNode(long initiationTime, OverdubbedFrame linkedFrame, String virtualFilename, String name) {
+		super(initiationTime, name);
+		if (linkedFrame == null) throw new NullPointerException("linkedFrame");
+		this.linkedFrame = linkedFrame;
+		this.virtualFilename = virtualFilename;
+	}
+	
+	public OverdubbedFrame getLinkedFrame() {
+		return linkedFrame;
+	}
+
+	public String getVirtualFilename() {
+		return virtualFilename;
+	}
+
+	/**
+	 * @see OverdubbedFrame#calculateRunningTime()
+	 * 
+	 * @return
+	 * 		The linked frames running time
+	 */
+	@Override
+	public long getRunningTime() {
+		return linkedFrame.calculateRunningTime();
+	}
+	
+	
+}
Index: /trunk/src_apollo/org/apollo/audio/structure/OverdubbedFrame.java
===================================================================
--- /trunk/src_apollo/org/apollo/audio/structure/OverdubbedFrame.java	(revision 315)
+++ /trunk/src_apollo/org/apollo/audio/structure/OverdubbedFrame.java	(revision 315)
@@ -0,0 +1,608 @@
+package org.apollo.audio.structure;
+
+import java.util.ArrayList;
+import java.util.Collection;
+import java.util.Collections;
+import java.util.HashMap;
+import java.util.HashSet;
+import java.util.LinkedList;
+import java.util.List;
+import java.util.Map;
+import java.util.Set;
+import java.util.Stack;
+
+import org.apollo.audio.util.SoundDesk;
+import org.apollo.mvc.AbstractSubject;
+
+/**
+ * Immutable outside package.
+ * 
+ * @see {@link AudioStructureModel} for thread safe convention.
+ * 
+ * @author Brook Novak
+ *
+ */
+public class OverdubbedFrame extends AbstractSubject {
+
+	private String frameName; // immutable
+	
+	private Set<TrackGraphNode> tracks = new HashSet<TrackGraphNode>();
+	
+	private Set<LinkedTracksGraphNode> linkedTracks = new HashSet<LinkedTracksGraphNode>();
+	
+	/**
+	 * 
+	 * @param frameName
+	 * 
+	 * @throws NullPointerException
+	 * 		if frameName is null.
+	 * 
+	 * @throws IllegalArgumentException
+	 * 		if frameName is empty.
+	 */
+	OverdubbedFrame(String frameName) {
+		if (frameName == null) throw new NullPointerException("frameName");
+		else if (frameName.length() == 0) throw new IllegalArgumentException("frameName.length() == 0");
+		
+		this.frameName = frameName;
+	}
+	
+	/**
+	 * @return
+	 * 		The immutable framename for this overdubbed frame.
+	 * 		Never null / empty.
+	 */
+	public String getFrameName() {
+		return frameName;
+	}
+
+	/**
+	 * @return 
+	 * 		A copy of the linked tracks. Never null.
+	 * 		All unique (from a set)
+	 */
+	public ArrayList<LinkedTracksGraphNode> getLinkedTracksCopy() {
+		return new ArrayList<LinkedTracksGraphNode>(linkedTracks);
+	}
+	
+	/**
+	 * @return
+	 * 		An unmodifiable set of all the linked tracks. Never null.
+	 */
+	public Set<LinkedTracksGraphNode> getUnmodifiableLinkedTracks() {
+		return Collections.unmodifiableSet(linkedTracks);
+	}
+
+	/**
+	 * @return 
+	 * 		A copy of the tracks. Never null.
+	 * 		All unique (from a set)
+	 */
+	public ArrayList<TrackGraphNode> getTracksCopy() {
+		return new ArrayList<TrackGraphNode>(tracks);
+	}
+	
+	/**
+	 * @return
+	 * 		An unmodifiable set of all the tracks. Never null.
+	 */
+	public Set<TrackGraphNode> getUnmodifiableTracks() {
+		return Collections.unmodifiableSet(tracks);
+	}
+	
+	/**
+	 * Must be on swing thread.
+	 * 
+	 * @param localFilename
+	 * 		The track to get. Must not be null.
+	 * 
+	 * @return
+	 * 		The track with the given local filename. Null if does not exist.
+	 * 
+	 * @throws NullPointerException
+	 * 		If localFilename is null.
+	 */
+	public TrackGraphNode getTrack(String localFilename) {
+		if (localFilename == null) throw new NullPointerException("localFilename");
+		
+		for (TrackGraphNode track : tracks) {
+			if (track.getLocalFilename().equals(localFilename)) {
+				return track;
+			}
+		}
+		
+		return null;
+	}
+	
+	/**
+	 * Must be on swing thread.
+	 * 
+	 * @param virtualFilename
+	 * 		The linked track to get. Must not be null.
+	 * 
+	 * @return
+	 * 		The linked track with the given virtual filename. Null if does not exist.
+	 * 
+	 * @throws NullPointerException
+	 * 		If virtualFilename is null.
+	 */
+	public LinkedTracksGraphNode getLinkedTrack(String virtualFilename) {
+		if (virtualFilename == null) throw new NullPointerException("virtualFilename");
+		
+		for (LinkedTracksGraphNode ltrack : linkedTracks) {
+			if (ltrack.getVirtualFilename().equals(virtualFilename)) {
+				return ltrack;
+			}
+		}
+		
+		return null;
+	}
+	
+	
+	/**
+	 * Must be on swing thread.
+	 * Note: Only a shallow check.
+	 * 
+	 * @param localFilename
+	 * 		The track to check for. Must not be null.
+	 * 
+	 * @return
+	 * 		True if this OverdubbedFrame owns the track. False otherwise.
+	 * 
+	 * @throws NullPointerException
+	 * 		If localFilename is null.
+	 */
+	public boolean containsTrack(String localFilename) {
+		if (localFilename == null) throw new NullPointerException("localFilename");
+		
+		for (TrackGraphNode track : tracks) {
+			if (track.getLocalFilename().equals(localFilename)) {
+				return true;
+			}
+		}
+		
+		return false;
+	}
+
+	/**
+	 * Must be on swing thread.
+	 * Note: Only a shallow check.
+	 * 
+	 * @param virtualFilename
+	 * 		The linked track to check for. Must not be null.
+	 * 
+	 * @return
+	 * 		True if this OverdubbedFrame owns the linked track. False otherwise.
+	 * 
+	 * @throws NullPointerException
+	 * 		If virtualFilename is null.
+	 */
+	public boolean containsLinkedTrack(String virtualFilename) {
+		if (virtualFilename == null) throw new NullPointerException("virtualFilename");
+		
+		for (LinkedTracksGraphNode ltrack : linkedTracks) {
+			if (ltrack.getVirtualFilename().equals(virtualFilename)) {
+				return true;
+			}
+		}
+		
+		return false;
+	}
+	
+	void addTrack(TrackGraphNode track) {
+		assert(track != null);
+		tracks.add(track);
+	}
+	
+	void addAllTracks(Collection<TrackGraphNode> track) {
+		assert(track != null);
+		tracks.addAll(track);
+	}
+	
+	void addLinkedTrack(LinkedTracksGraphNode linkedTrack) {
+		assert(linkedTrack != null);
+		linkedTracks.add(linkedTrack);
+	}
+	
+	void addAllLinkedTracks(Collection<LinkedTracksGraphNode> linkedTracks) {
+		assert(linkedTracks != null);
+		linkedTracks.addAll(linkedTracks);
+	}
+	
+	boolean removeTrack(TrackGraphNode track) {
+		assert(track != null);
+		return tracks.remove(track);
+	}
+	
+	TrackGraphNode removeTrack(String localFilename) {
+		assert(localFilename != null);
+		
+		TrackGraphNode track = getTrack(localFilename);
+		
+		if (track != null && tracks.remove(track)) return track;
+		
+		return null;
+	}
+	
+	boolean removeLinkedTrack(LinkedTracksGraphNode ltrack) {
+		assert(ltrack != null);
+		return linkedTracks.remove(ltrack);
+	}
+	
+	/**
+	 * @see #trackExistsDeep
+	 * 
+	 * @return
+	 * 		True is there is no tracks or linked tracks.
+	 */
+	public boolean isEmpty() {
+		
+		return linkedTracks.isEmpty() && tracks.isEmpty();
+	}
+	
+	/**
+	 * Checks if there are any tracks on the frame
+	 * 
+	 * @see #trackExistsDeep()
+	 * 
+	 * @return
+	 * 		True if there exists a track (NOT LINKED-TRACK) in this frame
+	 */
+	public boolean trackExists() {
+		return !tracks.isEmpty();
+	}
+	
+	/**
+	 * Intention: to see if this overdub frame contains anything to play.
+	 * 
+	 * @return
+	 * 		True if there exists a track (NOT LINKED-TRACK) in this frame or its
+	 * 		descendant frames.
+	 */
+	public boolean trackExistsDeep() {
+		return doesTrackExist(new HashSet<OverdubbedFrame>());
+	}
+	
+	private boolean doesTrackExist(Set<OverdubbedFrame> visited) {
+		if (!tracks.isEmpty()) return true;
+		else if (visited.contains(this)) return false;
+		
+		// Remember this node
+		visited.add(this);
+
+		// Recurse step
+		for (LinkedTracksGraphNode linkedTrack : linkedTracks) {
+			if (linkedTrack.getLinkedFrame().doesTrackExist(visited)) 
+				return true;
+		}
+		
+		return false;
+	}
+	
+	/**
+	 * Gets a child frame with the given name... if exists / is reachable from this node.
+	 * 
+	 * @param targetFrameName
+	 * 		THe target frame to get.
+	 * 
+	 * @throws NullPointerException
+	 * 		If targetFrameName is null.
+	 * 
+	 * @return
+	 * 		The target OverdubbedFrame. Null if none existed ...
+	 */
+	public OverdubbedFrame getChild(String targetFrameName) {
+		return getChild(targetFrameName, new HashSet<OverdubbedFrame>());
+	}
+	
+	private OverdubbedFrame getChild(String targetFrameName, Set<OverdubbedFrame> visited) {
+		if(targetFrameName == null) throw new NullPointerException("targetFrameName");
+
+		// Base cases
+		if (targetFrameName.equalsIgnoreCase(frameName)) return this;
+		else if (visited.contains(this)) return null;
+		
+		// Remember this node
+		visited.add(this);
+		
+		// Recurse step
+		for (LinkedTracksGraphNode linkedTrack : linkedTracks) {
+			OverdubbedFrame odFrame = linkedTrack.getLinkedFrame().getChild(targetFrameName, visited);
+			if (odFrame != null) return odFrame;
+		}
+		
+		// Reached end of this nodes links - found no match
+		return null;
+	}
+	
+
+	/**
+	 * Recursivly calculates the total running time for this frame.
+	 * 
+	 * @return
+	 * 		The running time in milliseconds. Always positive. Zero if this frame has no tracks/linked tracks.
+	 */
+	public long calculateRunningTime() {
+		return calculateRunningTime(new HashMap<OverdubbedFrame, Long>());
+	}
+	
+	/**
+	 * Calculates the running time of this overdubbed frame in milliseconds.
+	 * Avoids infinit recursion.
+	 * 
+	 * @param visited
+	 * 		The visitive nodes
+	 * 
+	 * @return
+	 * 		The running time. Always positive. Zero if this frame has no tracks/linked tracks.
+	 */
+	private long calculateRunningTime(Map<OverdubbedFrame, Long> visited) {
+		
+		Long rt = visited.get(this);
+		if (rt != null) { // already considered - re-use value
+			return rt;
+		}
+		
+		long runTime = 0;
+		
+		if (!tracks.isEmpty() || !linkedTracks.isEmpty()) {
+			
+			// Calculate the running time for this frame
+			long initTime = getFirstInitiationTime();
+			long relativeEndTime = Long.MIN_VALUE;
+			
+			for (TrackGraphNode track : tracks) {
+				long ret = track.getInitiationTime() + track.getRunningTime();
+				if (ret > relativeEndTime) relativeEndTime = ret;
+			}
+	
+			for (LinkedTracksGraphNode linkedTrack : linkedTracks) {
+				
+				long ret = linkedTrack.getInitiationTime() + 
+					linkedTrack.getLinkedFrame().calculateRunningTime(visited);
+				
+				if (ret > relativeEndTime) relativeEndTime = ret;
+			}
+	
+			assert (initTime <= relativeEndTime);
+			
+			runTime = relativeEndTime - initTime;
+			
+		}
+		
+		// Save this ODFrames rt
+		visited.put(this, new Long(runTime));
+		
+		return runTime;
+	}
+	
+	/**
+	 * 
+	 * @return
+	 * 		The first initiation time of a track widget or track link on this frame.
+	 * 		Note that these are relative and can be negative.
+	 * 
+	 * 		Defaults to zero if there are no tracks/track links on this frame.
+	 * 
+	 */
+	public long getFirstInitiationTime() { // relative to this frame - i.e. first initiation time on this frame
+		
+		long smallest = 0;
+		boolean hasSet = false;
+		
+		for (LinkedTracksGraphNode linkedTrack : linkedTracks) {
+			if (!hasSet || linkedTrack.getInitiationTime() < smallest) {
+				smallest = linkedTrack.getInitiationTime();
+				hasSet = true;
+			}
+		}
+		
+		for (TrackGraphNode track : tracks) {
+			if (!hasSet || track.getInitiationTime() < smallest) {
+				smallest = track.getInitiationTime();
+				hasSet = true;
+			}
+		}
+		
+		return smallest;
+	}
+
+	@Override
+	public String toString() {
+		return frameName;
+	}
+	
+	/**
+	 * @return
+	 * 		All the TrackGraphInfo's in this frames descendants, including this frame.
+	 * 		Never null. Can of coarse be empty.
+	 */
+	public List<TrackGraphNode> getTracksDeep() {
+		List<TrackGraphNode> res = new LinkedList<TrackGraphNode>();
+		allAddTracksDeep(res, new HashSet<OverdubbedFrame>());
+		return res;
+	}
+	
+	private void allAddTracksDeep(List<TrackGraphNode> tlist, Set<OverdubbedFrame> visited) {
+		if (visited.contains(this)) return; // already considered
+		visited.add(this);
+		
+		tlist.addAll(tracks);
+
+		for (LinkedTracksGraphNode linkedTrack : linkedTracks) {
+			linkedTrack.getLinkedFrame().allAddTracksDeep(tlist, visited);
+		}
+	}
+	
+	/**
+	 * @param masterMixID
+	 * 		The master mix for all the absolute tacks nodes to use. Must not be null or empty.
+	 * 
+	 * @return
+	 * 		The list of tracks and their ABS times according to <i>this frame</i>
+	 * 		starting from ms time 0. Never null - can be empty.
+	 */
+	public List<AbsoluteTrackNode> getAbsoluteTrackLayoutDeep(String masterMixID) {
+		assert(masterMixID != null);
+		assert(masterMixID.length() > 0);
+		
+		List<AbsoluteTrackNode> res = new LinkedList<AbsoluteTrackNode>();
+		Stack<String> virtualPath = new Stack<String>();
+		virtualPath.add(masterMixID);
+		
+		addAbsTrackNode(res, 0, new Stack<OverdubbedFrame>(), virtualPath);
+		
+		return res;
+	}
+	
+	private void addAbsTrackNode(
+			List<AbsoluteTrackNode> abslist, 
+			long currentTime, 
+			Stack<OverdubbedFrame> visited,
+			Stack<String> virtualPath) {
+		
+		assert(currentTime >= 0);
+		
+		// Base case
+		if (visited.contains(this)) return; // already considered - loop safety
+		
+		visited.push(this);
+		
+		// Get offset - align all tracks on this frame to first track/linked track...
+		long firstInitTime = getFirstInitiationTime();
+		
+
+		// Add track info and init times for this frame
+		for (TrackGraphNode tnode : tracks) {
+			abslist.add(new AbsoluteTrackNode(
+					tnode, 
+					currentTime + (tnode.getInitiationTime() - firstInitTime), 
+					SoundDesk.createIndirectLocalChannelID(
+							virtualPath, tnode.getLocalFilename()),
+					frameName));
+		}
+
+		// Recurse: with a new current time according to the linked track init time
+		for (LinkedTracksGraphNode linkedTrack : linkedTracks) {
+			virtualPath.push(linkedTrack.getVirtualFilename()); // build vpath
+			
+			linkedTrack.getLinkedFrame().addAbsTrackNode(
+					abslist, 
+					currentTime + (linkedTrack.getInitiationTime() - firstInitTime),
+					visited,
+					virtualPath);
+			
+			virtualPath.pop(); // maintain vpath
+		}
+		
+		visited.pop();
+	}
+	
+	/**
+	 * Gets all frame positions that occur in a target child frame, from this frame...
+	 * <b>WARNING</b>: Infinite recursion if contains loops in structure.
+	 * 
+	 * @param childFrameName
+	 * 		Must not be null. The frame to get all the translated frame positions 
+	 * 
+	 * @param relativeMSPosition
+	 * 		A relative ms position for this frame
+	 * 
+	 * @return
+	 * 		A list of ms positions that have been translated ... empty
+	 * 		if there are no occurances.
+	 */
+	public List<Integer> getMSPositions(String childFrameName, long relativeMSPosition) {
+		assert(childFrameName != null);
+		assert(childFrameName.length() > 0);
+		
+		List<Integer> positions = new LinkedList<Integer>();
+		
+		addMSPosition(
+				positions, 
+				childFrameName, 
+				relativeMSPosition, 
+				new HashMap<OverdubbedFrame, CachedODFrameInfo>());
+		
+		return positions;
+	
+	}
+
+	private class CachedODFrameInfo {
+		
+		long runningTime;
+		long firstInitTime;
+		
+		public CachedODFrameInfo(long runningTime, long firstInitTime) {
+			this.runningTime = runningTime;
+			this.firstInitTime = firstInitTime;
+		}
+	}
+	
+	/**
+	 * <b>WARNING</b>: Infinite recursion if contains loops in structure.
+	 * A better safety Solution: Keep track of all virtual paths - NOT FRAMES
+	 * 
+	 * @param msPositions
+	 * @param childFrameName
+	 * @param currentRelativePosition
+	 * @param cachedCalcs
+	 * @param visited
+	 */
+	private void addMSPosition(
+			List<Integer> msPositions, 
+			String childFrameName, 
+			long currentRelativePosition,
+			Map<OverdubbedFrame, CachedODFrameInfo> cachedCalcs) {
+		
+		// Avoid recomputation of calcs
+		CachedODFrameInfo calcCache = cachedCalcs.get(this);
+		
+		if (calcCache == null) {
+			
+			calcCache = new CachedODFrameInfo(
+					calculateRunningTime(),
+					getFirstInitiationTime());
+			
+			cachedCalcs.put(this, calcCache);
+			
+		}
+		
+		assert(calcCache != null);
+
+		// Is requested frame position in range of this overdub frame?
+		if (childFrameName.equalsIgnoreCase(frameName) &&
+				currentRelativePosition >= calcCache.firstInitTime 
+				&& currentRelativePosition <= (calcCache.firstInitTime + calcCache.runningTime)) {
+			msPositions.add(new Integer((int)(currentRelativePosition - calcCache.firstInitTime)));
+		}
+
+		// Recurse - even if re-visiting visited frames... risky ... will
+		// get stack overflow if the structure contains loops
+		for (LinkedTracksGraphNode linkedTrack : linkedTracks) {
+			
+			CachedODFrameInfo linkedFramesCachedCalc = cachedCalcs.get(linkedTrack.getLinkedFrame());
+			
+			if (linkedFramesCachedCalc == null) {
+				linkedFramesCachedCalc = new CachedODFrameInfo(
+						linkedTrack.getLinkedFrame().calculateRunningTime(),
+						linkedTrack.getLinkedFrame().getFirstInitiationTime());
+				
+				cachedCalcs.put(linkedTrack.getLinkedFrame(), linkedFramesCachedCalc);
+			}
+			
+			// Only recuse down a link if the cuurent position is over top of the link
+			if (currentRelativePosition >= linkedTrack.getInitiationTime() &&
+					currentRelativePosition <= (linkedTrack.getInitiationTime() + linkedFramesCachedCalc.runningTime)) {
+		
+				linkedTrack.getLinkedFrame().addMSPosition(
+					msPositions, 
+					childFrameName, 
+					linkedFramesCachedCalc.firstInitTime + (currentRelativePosition - linkedTrack.getInitiationTime()),
+					cachedCalcs);
+			}
+		}
+
+	}
+	
+}
Index: /trunk/src_apollo/org/apollo/audio/structure/TrackGraphLoopException.java
===================================================================
--- /trunk/src_apollo/org/apollo/audio/structure/TrackGraphLoopException.java	(revision 315)
+++ /trunk/src_apollo/org/apollo/audio/structure/TrackGraphLoopException.java	(revision 315)
@@ -0,0 +1,59 @@
+package org.apollo.audio.structure;
+
+import java.util.Stack;
+
+/**
+ * Raised when a track graph contains a loop. Provide loop trace information.
+ * 
+ * @author Brook Novak
+ *
+ */
+public class TrackGraphLoopException extends Exception {
+
+
+	private static final long serialVersionUID = 1L;
+	
+	private Stack<OverdubbedFrame> loopTrace;
+	
+	public TrackGraphLoopException(Stack<OverdubbedFrame> loopTrace) {
+		if (loopTrace == null) {
+			loopTrace = new Stack<OverdubbedFrame>();
+		} else this.loopTrace = loopTrace;
+	}
+
+	/**
+	 * The full loop trace ... from where the start of
+	 * the loop checker began
+	 * 
+	 * @return
+	 * 		The loop trace. Never null.
+	 */
+	public Stack<OverdubbedFrame> getFullLoopTrace() {
+		return loopTrace;
+	}
+
+	@Override
+	public String getLocalizedMessage() {
+		StringBuilder sb = new StringBuilder("Contains loop ");
+		
+		String causeFrameName = loopTrace.lastElement().getFrameName();
+		boolean mentionedLoopStart = false;
+		
+		for (OverdubbedFrame odf : loopTrace) {
+			sb.append("\n\t\tat ");
+			sb.append(odf);
+			
+			if (!mentionedLoopStart && odf.getFrameName().equals(causeFrameName)) {
+				sb.append("\t --- Loop starts here ---");
+				mentionedLoopStart = true;
+			}
+		}
+		
+		return sb.toString();
+	}
+
+	
+	
+	
+
+}
Index: /trunk/src_apollo/org/apollo/audio/structure/TrackGraphNode.java
===================================================================
--- /trunk/src_apollo/org/apollo/audio/structure/TrackGraphNode.java	(revision 315)
+++ /trunk/src_apollo/org/apollo/audio/structure/TrackGraphNode.java	(revision 315)
@@ -0,0 +1,82 @@
+package org.apollo.audio.structure;
+
+/**
+ * Immutable outside package.
+ * 
+ * @see {@link AudioStructureModel} for thread safe convention.
+ * 
+ * @author Brook Novak
+ *
+ */
+public class TrackGraphNode extends AbstractTrackGraphNode {
+
+	private String localFilename; // immutable
+
+	private long runningTime; // ms
+	
+	/**
+	 * Friendly constructor.
+	 * 
+	 * @param localFilename
+	 * 		Must not be null or empty.
+	 * 
+	 * @param initiationTime
+	 * 		Must be positive. In milliseconds
+	 * 
+	 * @param runningMS
+	 * 		Must be larger than zero. In milliseconds.
+	 * 
+	 * @param name
+	 * 		The name to set this to. Can be null
+	 * 
+	 * @throws NullPointerException
+	 * 		if localFilename is null.
+	 * 
+	 * @throws IllegalArgumentException
+	 * 		if ms is smaller or equal to zero.
+	 * 		or if localFilename is empty.
+	 * 		or if initiationTime is less than zero
+	 */
+	TrackGraphNode(long initiationTime, String localFilename, long runningMS, String name) {
+		super(initiationTime, name);
+		
+		if (localFilename == null) throw new NullPointerException("localFilename");
+		else if (localFilename.length() == 0) throw new IllegalArgumentException("localFilename.length() == 0");
+		
+		setRunningTime(runningMS);
+		
+		this.localFilename = localFilename;
+	}
+	
+	
+	public String getLocalFilename() {
+		return localFilename;
+	}
+
+	/**
+	 * Precomputed and maintained by the graph model.
+	 * Always larger or equal to zero.
+	 * 
+	 * @return 
+	 * 		The running time in milliseconds.
+	 */
+	@Override
+	public long getRunningTime() {
+		return runningTime;
+	}
+	
+	/**
+	 * 
+	 * @param runningMS
+	 * 		Must be larger than zero. IN milliseconds
+	 * 
+	 * @throws IllegalArgumentException
+	 * 		if ms is smaller or equal to zero.
+	 * 
+	 */
+	void setRunningTime(long runningMS) {
+		if (runningMS <= 0) throw new IllegalArgumentException("runningMS = " + runningMS);
+		this.runningTime = runningMS;
+	}
+
+}
Index: /trunk/src_apollo/org/apollo/audio/util/FrameLayoutDaemon.java
===================================================================
--- /trunk/src_apollo/org/apollo/audio/util/FrameLayoutDaemon.java	(revision 315)
+++ /trunk/src_apollo/org/apollo/audio/util/FrameLayoutDaemon.java	(revision 315)
@@ -0,0 +1,860 @@
+package org.apollo.audio.util;
+
+import java.awt.Point;
+import java.awt.event.ComponentEvent;
+import java.awt.event.ComponentListener;
+import java.awt.event.MouseEvent;
+import java.io.File;
+import java.lang.reflect.InvocationTargetException;
+import java.util.HashMap;
+import java.util.Map;
+
+import javax.swing.SwingUtilities;
+
+import org.apollo.audio.ApolloSubjectChangedEvent;
+import org.apollo.audio.structure.AbstractTrackGraphNode;
+import org.apollo.audio.structure.LinkedTracksGraphNode;
+import org.apollo.audio.structure.OverdubbedFrame;
+import org.apollo.audio.structure.TrackGraphLoopException;
+import org.apollo.audio.structure.AudioStructureModel;
+import org.apollo.audio.structure.TrackGraphNode;
+import org.apollo.io.AudioIO;
+import org.apollo.io.AudioPathManager;
+import org.apollo.mvc.AbstractSubject;
+import org.apollo.mvc.Observer;
+import org.apollo.mvc.Subject;
+import org.apollo.mvc.SubjectChangedEvent;
+import org.apollo.util.NullableLong;
+import org.apollo.widgets.LinkedTrack;
+import org.apollo.widgets.SampledTrackEvo4;
+import org.expeditee.gui.Browser;
+import org.expeditee.gui.DisplayIO;
+import org.expeditee.gui.DisplayIOObserver;
+import org.expeditee.gui.Frame;
+import org.expeditee.gui.FrameMouseActions;
+import org.expeditee.gui.FreeItems;
+import org.expeditee.gui.MouseEventRouter;
+import org.expeditee.items.Item;
+import org.expeditee.items.ItemUtils;
+import org.expeditee.items.widgets.InteractiveWidget;
+import org.expeditee.items.widgets.WidgetCorner;
+import org.expeditee.items.widgets.WidgetEdge;
+
+/**
+ * A daemon that lays out overdubbed frames.
+ * 
+ * It is a singletone. Also it is a subject which raises empty events whenever
+ * a new timeline is created.
+ * 
+ * @author Brook Novak
+ *
+ */
+public class FrameLayoutDaemon extends AbstractSubject implements Observer, DisplayIOObserver {
+	
+	/** The frame that should not be layout out at any point in time. */
+	private Frame suspendedFrame = null;
+	
+	/** The state info passed while suspending layout on {@link #suspendedFrame} */
+	private Object suspendendUserData = null;
+	
+	private LayoutDaemon daemon = null;
+	private Object daemonWaker = new Object();
+	private boolean layoutInvalid = false;
+	
+	private Timeline currentTimeline = null;
+	private Frame timelineOwner = null;
+	
+	public static final int LAYOUT_RIGHT_MARGIN = 30;
+	public static final int LAYOUT_LEFT_MARGIN = 30;
+	public static final int LAYOUT_MAX_INITIATION_PIXEL = 300;
+	public static final int MIN_TRACK_WIDGET_WIDTH = 20;
+	public static final int FREE_SPACE_MARGINS = 30;
+	
+	public static final int TRACK_WIDGET_HEIGHT = 100;
+	
+	public static final int TRACK_WIDGET_DEFAULT_WIDTH = 350;
+
+	private static FrameLayoutDaemon instance = new FrameLayoutDaemon();
+	
+	/**
+	 * Singleton constructor,
+	 */
+	private FrameLayoutDaemon() {
+		daemon = new LayoutDaemon();
+		daemon.start();
+		AudioStructureModel.getInstance().addObserver(this);
+		DisplayIO.addDisplayIOObserver(this);
+		
+		// Listen for resize events
+		SwingUtilities.invokeLater(new Runnable() {
+			public void run() {
+
+				// Keep expanded view centered to the browser window
+				if (Browser._theBrowser != null) {
+					Browser._theBrowser.addComponentListener(new ComponentListener() {
+
+						public void componentHidden(ComponentEvent e) {
+						}
+
+						public void componentMoved(ComponentEvent e) {
+						}
+
+						public void componentResized(ComponentEvent e) {
+							forceRecheck();
+						}
+
+						public void componentShown(ComponentEvent e) {
+						}
+						
+					});
+				}
+				
+			}
+		});
+		
+	}
+
+	/**
+	 * {@inheritDoc}
+	 */
+	public void frameChanged() {
+		forceRecheck();
+	}
+
+	/**
+	 * 
+	 * @return
+	 * 		The singleton instance.
+	 */
+	public static FrameLayoutDaemon getInstance() {
+		return instance;
+	}
+	
+	/**
+	 * @see #getTimelineOwner() for determining which frame the timeline was for.
+	 * 
+	 * @return
+	 * 		The last format hence the current timeline. Can be null
+	 * 		if no format has not occured yet or last format
+	 * 		had no timeline... i.e no tracks were found...
+	 */
+	public Timeline getLastComputedTimeline() {
+		return currentTimeline;
+	}
+	
+	/**
+	 * @see #getLastComputedTimeline()
+	 * 
+	 * @return
+	 * 		Can be null if a timeline has not yet been computed.
+	 */
+	public Frame getTimelineOwner() {
+		return timelineOwner;
+	}
+	
+	public void forceRecheck() {
+		assert(daemon != null && daemon.isAlive());
+		layoutInvalid = true;
+		synchronized(daemonWaker) {
+			daemonWaker.notify();
+		}
+	}
+	
+	/**
+	 * {@inheritDoc}
+	 */
+	public Subject getObservedSubject() {
+		return AudioStructureModel.getInstance();
+	}
+
+	/**
+	 * {@inheritDoc}
+	 */
+	public void setObservedSubject(Subject parent) {
+	}
+	
+	/**
+	 * {@inheritDoc}
+	 */
+	public void modelChanged(Subject source, SubjectChangedEvent event) {
+		if (event.getID() != ApolloSubjectChangedEvent.NAME_CHANGED)
+			forceRecheck();
+	}
+	
+	private void setCurrentTimeline(Timeline newTimeline, Frame timelineOwner) {
+		this.currentTimeline = newTimeline;
+		this.timelineOwner = timelineOwner;
+		fireSubjectChanged(SubjectChangedEvent.EMPTY);
+	}
+
+	/**
+	 * Suspends the layout daemon from laying out a frame.
+	 * Resumes any previous suspended layouts.
+	 * 
+	 * @see #resumeLayout(Object)
+	 * 
+	 * @param frame
+	 * 		The frame to suspend
+	 * 
+	 * @param userData
+	 * 		Any user data to attatch to frame layout.
+	 * 
+	 */
+	public void suspendLayout(Frame frame, Object userData) {
+		assert(frame != null);
+		
+		resumeLayout(null);
+		
+		suspendedFrame = frame;
+		suspendendUserData = userData;
+		
+	}
+	
+	/**
+	 * Resumes the suspended layout if has one.
+	 * 
+	 * @see #suspendLayout(Frame, Object)
+	 * 
+	 * @param userData
+	 * 	 	Null to resume layout regardless, otherwise it will only resume
+	 * 		layout if the user data matches the user data passed when calling
+	 * 		the suspension.
+	 */
+	public void resumeLayout(Object userData) {
+		if (suspendedFrame == null) return;
+		
+		if (userData == null || userData == suspendendUserData) {
+			suspendedFrame = null;
+			suspendendUserData = null;
+		}
+		
+	}
+	
+	/**
+	 * Lays out frames according to the current frame state...
+	 * 
+	 * @author Brook Novak
+	 *
+	 */
+	private class LayoutDaemon extends Thread {
+		
+		private TrackFormatter trackFormatter = new TrackFormatter();
+		
+		LayoutDaemon() {
+			super("TrackLayoutDaemon");
+			super.setDaemon(true);
+		}
+		
+		public void run() {
+			
+			while (true) { // keep daemon running for app lifetime
+				
+				if (!layoutInvalid) {
+					try {
+						synchronized(daemonWaker) {
+							daemonWaker.wait();
+						}
+					} catch (InterruptedException e) {
+						// Time to do some layout
+					}
+				}
+				
+				layoutInvalid = false;
+				
+				boolean hasUpdated = false;
+				do {
+					try {
+						AudioStructureModel.getInstance().waitOnUpdates();
+						hasUpdated = true;
+					} catch (InterruptedException e) {
+						e.printStackTrace();
+						continue;
+					}
+				} while (!hasUpdated);
+				
+				
+				// Note that accessing expeditee model in another thread --
+				// Being real careful not to do anything unsafe
+				Frame currentFrame = DisplayIO.getCurrentFrame();
+				if (currentFrame == null) continue;
+
+				boolean hasFetched = false;
+				do {
+					try {
+						AudioStructureModel.getInstance().fetchGraph(currentFrame.getName());
+						hasFetched = true;
+					} catch (InterruptedException e) { // cancelled
+						/* Consume */
+					} catch (TrackGraphLoopException e) { // contains loop
+						e.printStackTrace();
+						break;
+					}
+				} while (!hasFetched);
+				
+				trackFormatter.toFormat = currentFrame;
+				
+				try {
+					SwingUtilities.invokeAndWait(trackFormatter);
+				} catch (InterruptedException e) {
+					e.printStackTrace();
+				} catch (InvocationTargetException e) {
+					e.printStackTrace();
+				}
+				
+			}
+	
+		}
+		
+		private class TrackFormatter implements Runnable {
+			
+			Frame toFormat = null;
+
+			public void run() {
+				
+				assert(toFormat != null);
+				
+				// To late?
+				if (toFormat != DisplayIO.getCurrentFrame()) return;
+				
+				boolean supressSpatialLayout = suspendedFrame == toFormat;
+	
+				long firstInitiationTime = 0;
+				int initiationXPixel = 0;
+				int rightMostPixel = 0; // FOR SUSPENDED LAYOUTS ONLY
+				long longestEndTime = 0;
+				boolean hasRunningTime = false; // A flag set to true if there is actual audio to [lay
+				
+				Map<InteractiveWidget, AbstractTrackGraphNode> widgetsToFormat = new HashMap<InteractiveWidget, AbstractTrackGraphNode>();
+	
+				for (InteractiveWidget iw : toFormat.getInteractiveWidgets()) {
+					
+					AbstractTrackGraphNode abinf = null;
+
+					if (iw instanceof SampledTrackEvo4) {
+						SampledTrackEvo4 sampledTrackWidget = (SampledTrackEvo4)iw;
+			
+
+						abinf = AudioStructureModel.getInstance().getTrackGraphInfo(
+								sampledTrackWidget.getLocalFileName(), 
+								toFormat.getName());
+
+				
+					} else if (iw instanceof LinkedTrack) { 
+						LinkedTrack linkedTrackWidget = (LinkedTrack)iw;
+						
+						if (linkedTrackWidget.shouldLayout()) {
+							// TrackGraphModel provides best data
+							abinf = AudioStructureModel.getInstance().getLinkedTrackGraphInfo(
+									linkedTrackWidget.getVirtualFilename(), 
+									toFormat.getName());
+						}
+						
+					}
+					
+					if (abinf != null) {
+						
+						if (widgetsToFormat.isEmpty() || 
+								abinf.getInitiationTime() < firstInitiationTime) {
+							firstInitiationTime = abinf.getInitiationTime();
+							initiationXPixel = iw.getX();
+						}
+						
+						// Remember for suspended layouts
+						if (widgetsToFormat.isEmpty() || 
+								(iw.getX() + iw.getWidth()) > rightMostPixel) {
+							rightMostPixel = iw.getX() + iw.getWidth();
+						}
+						
+						
+						long endTime = abinf.getInitiationTime() + abinf.getRunningTime();
+						if (widgetsToFormat.isEmpty() || endTime > longestEndTime) {
+							longestEndTime = endTime;
+						}
+						
+						widgetsToFormat.put(iw, abinf);
+						
+						// Set flag 
+						if (!hasRunningTime) {
+							hasRunningTime = abinf.getRunningTime() > 0;
+						}
+						
+					}
+						
+				}
+				
+				// Anything to format?
+				if (!hasRunningTime || 
+						widgetsToFormat.isEmpty() || 
+						longestEndTime == firstInitiationTime) {
+					
+					// NOTE: longestEndTime == firstInitiationTime indicates linked tracks 
+					// linking to no actual audio data.
+					
+					// Reset the current timeline
+					setCurrentTimeline(null, toFormat);
+					return;
+				}
+				
+				assert (longestEndTime > firstInitiationTime);
+				assert(rightMostPixel >= initiationXPixel);
+				
+				int timelineWidth = TRACK_WIDGET_DEFAULT_WIDTH;
+				
+				if (widgetsToFormat.size() > 1) {
+					
+					// Ensure that left most pixel is at a reasonable position
+					if (!supressSpatialLayout && (initiationXPixel < 0 ||
+							(initiationXPixel > Browser._theBrowser.getWidth() && Browser._theBrowser.getWidth() > LAYOUT_LEFT_MARGIN))) {
+						initiationXPixel = LAYOUT_LEFT_MARGIN;
+					} else if (!supressSpatialLayout && initiationXPixel > LAYOUT_MAX_INITIATION_PIXEL) {
+						initiationXPixel = LAYOUT_MAX_INITIATION_PIXEL;
+					}
+
+					if (supressSpatialLayout) {
+						timelineWidth = rightMostPixel - initiationXPixel;
+					} else {
+						timelineWidth = Browser._theBrowser.getWidth() - initiationXPixel - LAYOUT_RIGHT_MARGIN;
+					}
+					
+					if (timelineWidth < 0) timelineWidth = 1;
+					
+				} else {
+					
+					// If only has one widget - the time line pixel length is set to whatever that is set to
+					timelineWidth = widgetsToFormat.keySet().iterator().next().getWidth();
+					
+				}
+				
+				// Set the new timelinetimelineWidth
+				setCurrentTimeline(
+						new Timeline(
+								firstInitiationTime,
+								longestEndTime - firstInitiationTime,
+								initiationXPixel, 
+								timelineWidth),
+								toFormat);
+
+				// Do the actual formatting
+				if (!supressSpatialLayout) {
+					
+					for (InteractiveWidget iw : widgetsToFormat.keySet()) {
+						
+						AbstractTrackGraphNode atgi = widgetsToFormat.get(iw);
+						assert(atgi != null);
+						
+						// Update width first
+						int width = (int)((double)atgi.getRunningTime() / currentTimeline.getTimePerPixel());
+						
+						// Clamp to something sensible
+						if (width < MIN_TRACK_WIDGET_WIDTH) 
+							width = MIN_TRACK_WIDGET_WIDTH;
+						
+						if (widgetsToFormat.size() == 1) {
+							
+							if (iw.isFixedSize()) {
+								iw.setSize(
+										-1, -1,   // minWidth, maxWidth, 
+										iw.getHeight(), iw.getHeight(),  // minHeight, maxHeight
+										iw.getWidth(), iw.getWidth());  // newWidth, newHeight
+							}
+							
+							
+						} else {
+						
+							if (iw.getWidth() != width || !iw.isFixedSize()) {
+								iw.setSize(
+										width, width,   // minWidth, maxWidth, 
+										iw.getHeight(), iw.getHeight(),  // minHeight, maxHeight
+										width, width);  // newWidth, newHeight
+							}
+							
+						}
+	
+						// Update position last
+						int xpos = currentTimeline.getXAtMSTime(atgi.getInitiationTime());	
+						iw.setPosition(xpos, iw.getY());
+	
+					}
+	
+				}
+				
+				// Also since this daemons responisiblilty it to layout track widgets on frames ...
+				// it should also take care of free space items
+			
+				Item i = FreeItems.getItemAttachedToCursor();
+				InteractiveWidget freespaceTrackToFormat = null;
+				if (i != null && i instanceof WidgetEdge) {
+					freespaceTrackToFormat = ((WidgetEdge)i).getWidgetSource();
+				} else if (i != null && i instanceof WidgetCorner) {
+					freespaceTrackToFormat = ((WidgetCorner)i).getWidgetSource();
+				}
+			
+				if (freespaceTrackToFormat != null) {
+					
+					int width = -1;
+					if (freespaceTrackToFormat instanceof SampledTrackEvo4) {
+						
+						long rt = ((SampledTrackEvo4)freespaceTrackToFormat).getRunningMSTimeFromRawAudio();
+						if (rt <= 0) rt = ((SampledTrackEvo4)freespaceTrackToFormat).getRunningMSTimeFromMeta();
+						if (rt > 0) {
+							width = (int)(currentTimeline.getPixelPerTime() * rt);
+						}
+		
+					} else if (freespaceTrackToFormat instanceof LinkedTrack) {
+						
+						long rt = ((LinkedTrack)freespaceTrackToFormat).getRunningMSTimeFromMeta();
+						if (rt > 0) {
+							width = (int)(currentTimeline.getPixelPerTime() * rt);
+						}
+					}
+					
+					if (width > 0) {
+						
+						if (width > (Browser._theBrowser.getWidth() - (2 * FREE_SPACE_MARGINS)))
+							width = Browser._theBrowser.getWidth() - (2 * FREE_SPACE_MARGINS);
+
+						freespaceTrackToFormat.setSize(
+								width, width, 
+								freespaceTrackToFormat.getHeight(), freespaceTrackToFormat.getHeight(), 
+								width, freespaceTrackToFormat.getHeight());
+						
+						// Keep the floating track on the cursor
+						MouseEvent me = MouseEventRouter.getCurrentMouseEvent();
+						Point containerPoint = SwingUtilities.convertPoint(me.getComponent(), me.getPoint(), Browser._theBrowser.getContentPane());
+						
+						if (!ItemUtils.expandRectangle(freespaceTrackToFormat.getBounds(), 20).contains(containerPoint)) {
+							// If due to resize the track is not longer over the cursor then just center it.
+							int offsetX = width / 2; 
+							int offsetY = freespaceTrackToFormat.getHeight() / 2;
+							freespaceTrackToFormat.setPosition(
+									containerPoint.x - offsetX, 
+									containerPoint.y - offsetY);
+							FrameMouseActions.resetOffset();
+		
+						}
+						
+		
+						
+		
+					}
+					
+				}
+				
+			}
+			
+		}
+		
+	}
+	
+	/**
+	 * Selects best timeline for given frame - may have to infer.
+	 * 
+	 * @param targetFrame
+	 * 
+	 * @return
+	 */
+	public Timeline getTimeline(Frame targetFrame) {
+		assert(targetFrame != null);
+		
+		if (currentTimeline != null && targetFrame == timelineOwner) return currentTimeline;
+		
+		return inferTimeline(targetFrame);
+	}
+
+	/**
+	 * Determines the ms time at a x pixel poistion on a target frame.
+	 * 
+	 * @see #getMSTimeAndWidth(int, long, Frame) for efficient use.
+	 * 
+	 * @param x
+	 * 		The x pixel posotion to determine the ms time from.
+	 * 
+	 * @param targetFrame
+	 * 		The target frame that the ms will be derived from.
+	 * 
+	 * @return
+	 * 		The ms time at the x pixel poistion.
+	 */
+	public long getMSAtX(int x, Frame targetFrame) {
+		return getMSAtX(x, getTimeline(targetFrame));
+	}
+	
+	private long getMSAtX(int x, Timeline tl) {
+		if (tl != null) return tl.getMSTimeAtX(x);
+		return 0; // Default to zero with no timeline available.
+	}
+	
+	/**
+	 * Determines the pixel width from a time range (ms) for a target frame.
+	 * <b>NOTE</B> Width is clamped to it is never smaller than {@link #MIN_TRACK_WIDGET_WIDTH}
+	 * 
+	 * @see #getMSTimeAndWidth(int, long, Frame) for efficient use.
+	 * 
+	 * @param ms
+	 * 		The amount of time to determine a width from (in milliseconds)
+	 * 
+	 * @param targetFrame
+	 * 		The target frame that the <i>clamped</i> width will be derived from.
+	 * 
+	 * @return
+	 * 		The width in pixels of the ms range. -1 if there is no timeline
+	 */
+	public int getWidthFromMS(long ms, Frame targetFrame) {
+		return getWidthFromMS(ms, getTimeline(targetFrame));
+	}
+	
+	private int getWidthFromMS(long ms, Timeline tl) {
+		int width = -1;
+		if (tl != null) {
+			width = (int)((double)ms / tl.getTimePerPixel());
+		}	
+		
+		// Clamp width to something reasonable.
+		if (width > 0 && width < MIN_TRACK_WIDGET_WIDTH) width = MIN_TRACK_WIDGET_WIDTH;
+
+		return width;
+	}
+	
+	/**
+	 * Gets the ms time and width in one call - more efficient than
+	 * calling {@link #getMSAtX(int, Frame)} and {@link #getWidthFromMS(long, Frame)}
+	 * seperatly.
+	 * 
+	 * <b>NOTE</B> Width is clamped to it is never smaller than {@link #MIN_TRACK_WIDGET_WIDTH}
+	 * 
+	 * @param x
+	 * 		The x pixel posotion to determine the ms time from.
+	 * 
+	 * @param ms
+	 * 		The amount of time to determine a width from (in milliseconds)
+	 * 
+	 * @param targetFrame
+	 * 		The target frame that the ms and width will be derived from.
+	 * 
+	 * @return
+	 * 		A Two dimensional array of the ms time and <i>clamped</i> width, respectivly.
+	 */
+	public long[] getMSTimeAndWidth(int x, long ms, Frame targetFrame) {
+		
+		// Re-use timeline
+		Timeline tl = getTimeline(targetFrame);
+		
+		return new long[] { getMSAtX(x, tl), getWidthFromMS(ms, tl)};
+	}
+
+	
+	/**
+	 * <b>MUST BE ON EXPEDITEE THREAD</b>
+	 * 
+	 * If the frame is not loaded in TrackGraphModel then the timeline will
+	 * be a rough estimation using meta data. Otherwise it will be accurate.
+	 * Fast operation - does not recurse. 
+	 * 
+	 * @param frame
+	 * 		The target frame for inferring the timeline.
+	 * 
+	 * @return
+	 * 		A timeline. Null if there are no track widgets fround on the frame.
+	 */
+	public static Timeline inferTimeline(Frame frame) {
+		assert(frame != null);
+		
+		// Unavilable times relative to zero - and used if no availabel times exist.
+		Integer unavilableInitiationXPixel = null;
+		NullableLong unavilableLongestEndTime = null; 
+		Integer unavilableLongestEndTimeRightPixel = null;
+
+		long firstInitiationTime = 0;
+		int initiationXPixel = 0;
+
+		long longestEndTime = -1; 
+		int longestEndTimeRightPixel = 0;
+		
+		// Look for all track widgets
+		for (InteractiveWidget iw : frame.getInteractiveWidgets()) {
+			
+			if (iw instanceof SampledTrackEvo4) {
+				SampledTrackEvo4 sampledTrackWidget = (SampledTrackEvo4)iw;
+
+				// TrackGraphModel provides best data
+				TrackGraphNode tgi = AudioStructureModel.getInstance().getTrackGraphInfo(
+						sampledTrackWidget.getLocalFileName(), 
+						frame.getName());
+		
+				NullableLong it = (tgi != null) ? 
+					new NullableLong(tgi.getInitiationTime()) :
+						NullableLong.fromLong(sampledTrackWidget.getInitiationTimeFromMeta());
+				
+				if (it == null && 
+						(unavilableInitiationXPixel == null || iw.getX() < unavilableInitiationXPixel)) {
+					unavilableInitiationXPixel = iw.getX();
+				} else if(it != null && it.getLongValue() < firstInitiationTime) {
+					firstInitiationTime = it.getLongValue();
+					initiationXPixel = iw.getX();
+				}
+				
+				// Determine running time
+				long runningTime = sampledTrackWidget.getRunningMSTimeFromRawAudio();
+				
+				if (runningTime <= 0 && tgi != null) {
+					runningTime = tgi.getRunningTime();
+					
+				} else if (runningTime <= 0) {
+					runningTime = sampledTrackWidget.getRunningMSTimeFromMeta();
+				}
+				
+				// Note that this will be in rare cases .. the meta should rarely be missing
+				// so cann afford to hang the thread a little for checking the actual time...
+				if (runningTime <= 0) {
+					File f = new File(AudioPathManager.AUDIO_HOME_DIRECTORY +
+							sampledTrackWidget.getLocalFileName());
+					if (f.exists()) {
+						try {
+							runningTime = AudioIO.getRunningTime(f.getPath());
+						} catch(Exception e) {
+							e.printStackTrace();
+						}
+					}
+				}
+				
+				// If all failed, just set to one... this would be rare, and by setting to
+				// 1 it will just give a incorrect timeline - nothing catistrophic.
+				if (runningTime <= 0) {
+					runningTime = 1;
+				}
+				
+				if (it == null) {
+					if (unavilableLongestEndTime == null ||
+							runningTime > unavilableLongestEndTime.getLongValue()) {
+						unavilableLongestEndTime = new NullableLong(runningTime);
+						unavilableLongestEndTimeRightPixel = iw.getX() + iw.getWidth();
+					}
+				} else {
+					long endTime = it.getLongValue() + runningTime;
+					if (endTime > longestEndTime) {
+						longestEndTime = endTime;
+						longestEndTimeRightPixel = iw.getX() + iw.getWidth();
+					}
+				}
+				
+			} else if (iw instanceof LinkedTrack) { 
+				LinkedTrack linkedTrackWidget = (LinkedTrack)iw;
+	
+				// TrackGraphModel provides best data
+				LinkedTracksGraphNode ltgi = AudioStructureModel.getInstance().getLinkedTrackGraphInfo(
+						linkedTrackWidget.getVirtualFilename(), 
+						frame.getName());
+
+				NullableLong it = (ltgi != null) ? 
+						new NullableLong(ltgi.getInitiationTime()) :
+							NullableLong.fromLong(linkedTrackWidget.getInitiationTimeFromMeta());
+				
+				if (it == null && 
+						(unavilableInitiationXPixel == null || iw.getX() < unavilableInitiationXPixel)) {
+					unavilableInitiationXPixel = iw.getX();
+				} else if(it != null && it.getLongValue() < firstInitiationTime) {
+					firstInitiationTime = it.getLongValue();
+					initiationXPixel = iw.getX();
+				}
+				
+				// Determine running time
+				long runningTime = (ltgi != null) ?
+						ltgi.getRunningTime() :
+							linkedTrackWidget.getRunningMSTimeFromMeta();
+
+				// Note that not recusing through the links because the frames might
+				// not be loaded yet. The TrackGraphModel handles such operations - 
+				// and it would be to expensive / overkill to frame search here...
+	
+				
+				// If all failed, just set to one...
+				if (runningTime <= 0) {
+					runningTime = 1;
+				}
+
+				if (it == null) {
+					if (unavilableLongestEndTime == null ||
+							runningTime > unavilableLongestEndTime.getLongValue()) {
+						unavilableLongestEndTime = new NullableLong(runningTime);
+						unavilableLongestEndTimeRightPixel = iw.getX() + iw.getWidth();
+					}
+				} else {
+					long endTime = it.getLongValue() + runningTime;
+					if (endTime > longestEndTime) {
+						longestEndTime = endTime;
+						longestEndTimeRightPixel = iw.getX() + iw.getWidth();
+					}
+				}
+
+			}
+			
+		}
+		
+		if (longestEndTime > 0) {
+			
+			assert(longestEndTime > firstInitiationTime);
+			assert(longestEndTimeRightPixel > initiationXPixel);
+			
+			long rttime = longestEndTime - firstInitiationTime;
+			int pxWidth = longestEndTimeRightPixel - initiationXPixel;
+			if (unavilableLongestEndTime != null &&
+					unavilableLongestEndTime.getLongValue() > rttime) {
+				assert(unavilableInitiationXPixel != null);
+				rttime = unavilableLongestEndTime.getLongValue();
+				pxWidth = unavilableLongestEndTimeRightPixel - unavilableInitiationXPixel;
+			}
+
+			return new Timeline(
+					firstInitiationTime, 
+					rttime, 
+					initiationXPixel,
+					pxWidth);
+			
+		} else if(unavilableInitiationXPixel != null) { // default
+			
+			assert(unavilableInitiationXPixel != null);
+			assert(unavilableLongestEndTimeRightPixel != null);
+			
+			return new Timeline(
+					0, 
+					unavilableLongestEndTime.getLongValue(), 
+					unavilableInitiationXPixel,
+					unavilableLongestEndTimeRightPixel - unavilableInitiationXPixel);
+			
+		}
+		
+		return null;
+
+	}
+	
+	/**
+	 * @return
+	 * 		The total ms time for the current frame. zero or negative if failed or there are no
+	 * 		tracks to play.
+	 * 		
+	 */
+	public static long inferCurrentTotalMSTime() {
+		
+		Frame currentFrame = DisplayIO.getCurrentFrame();
+		String currentFrameName = (currentFrame != null) ? currentFrame.getName() : null;
+		
+		if (currentFrameName == null) return -1;
+
+		OverdubbedFrame odf =  AudioStructureModel.getInstance().getOverdubbedFrame(currentFrameName);
+		
+		if (odf != null) {
+			return odf.calculateRunningTime();
+		}
+		
+		// Infer time from meta
+		Timeline tl = FrameLayoutDaemon.inferTimeline(currentFrame);
+		
+		if (tl != null) {
+			return tl.getRunningTime();
+		}
+		
+		return -1;
+		
+	}
+	
+}
Index: /trunk/src_apollo/org/apollo/audio/util/MultiTrackPlaybackController.java
===================================================================
--- /trunk/src_apollo/org/apollo/audio/util/MultiTrackPlaybackController.java	(revision 315)
+++ /trunk/src_apollo/org/apollo/audio/util/MultiTrackPlaybackController.java	(revision 315)
@@ -0,0 +1,1260 @@
+package org.apollo.audio.util;
+
+import java.util.HashMap;
+import java.util.HashSet;
+import java.util.LinkedList;
+import java.util.List;
+import java.util.Map;
+import java.util.Set;
+
+import javax.sound.sampled.LineUnavailableException;
+import javax.swing.SwingUtilities;
+
+import org.apollo.audio.ApolloPlaybackMixer;
+import org.apollo.audio.ApolloSubjectChangedEvent;
+import org.apollo.audio.SampledAudioManager;
+import org.apollo.audio.SampledTrackModel;
+import org.apollo.audio.TrackSequence;
+import org.apollo.audio.structure.AbsoluteTrackNode;
+import org.apollo.audio.structure.AudioStructureModel;
+import org.apollo.audio.structure.OverdubbedFrame;
+import org.apollo.audio.structure.TrackGraphLoopException;
+import org.apollo.audio.structure.TrackGraphNode;
+import org.apollo.io.AudioPathManager;
+import org.apollo.io.AudioIO.AudioFileLoader;
+import org.apollo.mvc.AbstractSubject;
+import org.apollo.mvc.Observer;
+import org.apollo.mvc.Subject;
+import org.apollo.mvc.SubjectChangedEvent;
+import org.apollo.util.AudioMath;
+import org.apollo.util.StringEx;
+import org.apollo.util.TrackModelHandler;
+import org.apollo.util.TrackModelLoadManager;
+import org.expeditee.gui.DisplayIO;
+import org.expeditee.gui.DisplayIOObserver;
+
+
+public class MultiTrackPlaybackController 
+	extends AbstractSubject 
+	implements TrackModelHandler, Observer, DisplayIOObserver {
+	
+	/** Can be a framename or virtual filename (e.g. from a linked track) */
+	private String rootFrameName = null; // Shared resource
+	
+	private String masterChannelID = null; // Shared resource
+	
+	private boolean markedAsPaused = false; // by user convention .. just centralized model data
+	
+	// Track-sequence like data for multitrack
+	private int suspendedFramePosition = 0;
+	private long initiationFramePosition = 0;
+	private int startFramePosition = 0;
+	private int endFramePosition = 0;
+	
+	private boolean canInstantlyResume = false;
+	
+	/** A flag set when playback is commenced and resets when the first track begins to play.
+	 * .. or if the commence failed.*/
+	private boolean isPlaybackPending = false;
+	
+	private MultiTrackPlaybackLoader loaderThread = null;
+	
+	/** The tracks that are being loaded, played or stopped ... kept for resuming / fast reloading */
+	private List<Overdub> currentOverdubs = new LinkedList<Overdub>(); // SHARED RESOURCE
+	private Set<TrackSequence> currentTrackSequences = null;
+	
+	private OverdubbedFrame currentODFrame = null;
+
+	private int cacheExpiryCounter = 0;
+	private static final int CACHE_DEPTH = 5;
+	
+	/**
+	 * Singleton design pattern
+	 */
+	private static MultiTrackPlaybackController instance = new MultiTrackPlaybackController();
+	public static MultiTrackPlaybackController getInstance() {
+		return instance;
+	}
+	
+	/**
+	 * Singleton constructor:
+	 * Sets up perminant observed subjects.
+	 */
+	private MultiTrackPlaybackController() {
+		
+		// Since track groups are cached .. for a awhile
+		TrackModelLoadManager.getInstance().addTrackModelHandler(this);
+		
+		// Dynamically adjust group of tracks while loading or playing
+		AudioStructureModel.getInstance().addObserver(this);
+		
+		// After a certain amount of frame changes since the last multi playback has occured
+		// the cached track models are freed to stop consuming all the memory.
+		DisplayIO.addDisplayIOObserver(this);
+		
+		// For whenever a track sequence is created - must observe the created track sequences...
+		SoundDesk.getInstance().addObserver(this);
+	}
+	
+	
+	/**
+	 * {@inheritDoc}
+	 */
+	public Subject getObservedSubject() {
+		return null; // many!
+	}
+	
+	/**
+	 * {@inheritDoc}
+	 */
+	public void modelChanged(Subject source, SubjectChangedEvent event) {
+		
+		TrackSequence ts;
+		
+		switch (event.getID()) {
+		case ApolloSubjectChangedEvent.GRAPH_TRACK_REMOVED: // TODO: Remove while loading!
+			
+			
+			if (isPlaying()) {
+				// Does the removed track belong to the current group?
+				TrackGraphNode tnode = (TrackGraphNode)event.getState();
+				
+				synchronized(currentOverdubs) {
+					
+					for (Overdub od : currentOverdubs) {
+						
+						if (od.getTrackModel().getLocalFilename().equals(tnode.getLocalFilename())) {
+							
+							TrackSequence trackSeq = 
+								SoundDesk.getInstance().getTrackSequence(od.getChannelID());
+							
+							if (trackSeq != null) { // && trackSeq.isPlaying()) {
+								ApolloPlaybackMixer.getInstance().stop(trackSeq);
+							}
+							
+						}
+						
+					}
+					
+				}
+				
+			}
+			
+//			 Fall through
+		case ApolloSubjectChangedEvent.GRAPH_LINKED_TRACK_REMOVED: // TODO: Remove while loading!
+			
+			if (event.getID() != ApolloSubjectChangedEvent.GRAPH_TRACK_REMOVED) {
+				if (isPlaying()) {
+					
+					// Does the removed track belong to the current group?
+					String virtualFilename = (String)event.getState();
+					
+					synchronized(currentOverdubs) {
+						
+						for (Overdub od : currentOverdubs) {
+							
+							if (od.getChannelID().indexOf(virtualFilename) >= 0) { // TODO: THIS IS TEMP - NOT RIGHT!! - 99% OK.. - Must revise this all anyway
+								
+								TrackSequence trackSeq = 
+									SoundDesk.getInstance().getTrackSequence(od.getChannelID());
+								
+								if (trackSeq != null) { // && trackSeq.isPlaying()) {
+									ApolloPlaybackMixer.getInstance().stop(trackSeq);
+								}
+								
+							}
+							
+						}
+						
+					}
+					
+				}
+			}
+				
+			// Fall through
+		case ApolloSubjectChangedEvent.GRAPH_LINKED_TRACK_ADDED:
+		case ApolloSubjectChangedEvent.GRAPH_TRACK_ADDED:
+		case ApolloSubjectChangedEvent.GRAPH_TRACK_EDITED: 
+			
+			// TODO: FINER CONTROL: If change is event related to the current group 
+			// of overdubs and if it can be managed.
+			this.canInstantlyResume = false;
+			
+			break;
+			
+		case ApolloSubjectChangedEvent.TRACK_SEQUENCE_CREATED:
+			assert(source == SoundDesk.getInstance());
+			
+			String channelID = (String) event.getState();
+			
+			if (!isPlaybackPending) break;
+			boolean doesBelong = false;
+			
+			// Does the track sequence belong to this set?
+			synchronized(currentOverdubs) {
+				for (Overdub od : currentOverdubs) {
+					if (od.getChannelID().equals(channelID)) {
+						doesBelong = true;
+						break;
+					}
+				}
+			}
+			
+			if (!doesBelong) break;
+			
+			ts = SoundDesk.getInstance().getTrackSequence(channelID);
+			assert(ts != null);
+			
+			if (currentTrackSequences == null)
+				currentTrackSequences = new HashSet<TrackSequence>();
+			
+			assert(!currentTrackSequences.contains(ts));
+
+			ts.addObserver(this);
+			currentTrackSequences.add(ts);
+			
+			// Note: if fails to start payback or is stopped before is playing
+			// then it does not need to unregister that the subject will be eventually freed.
+			
+			// the currentTrackSequences set is nullified if the playback fails anyway...
+			break;
+			
+		case ApolloSubjectChangedEvent.PLAYBACK_STARTED:
+			// Can get many of these over time in one playback call... so make sure that a event is raised
+			// on the first event received...
+			if (isPlaybackPending) {
+				isPlaybackPending = false;
+				initiationFramePosition = ((TrackSequence)source).getInitiationFrame();
+				suspendedFramePosition = 0;
+				fireSubjectChanged(new SubjectChangedEvent(ApolloSubjectChangedEvent.PLAYBACK_STARTED));
+			}
+			break;
+			
+			// Keep track of what is/isn't playing. Note that currentTrackSequences is cleared explicity
+			// event before stop events occur when playback is commences while already playing back
+		case ApolloSubjectChangedEvent.PLAYBACK_STOPPED:
+			
+			if (currentTrackSequences != null && !currentTrackSequences.isEmpty()) {
+				
+				currentTrackSequences.remove(source);
+				
+				ts = (TrackSequence)source;
+				// Calculate suspended frame position .. this could be the last track stopped that actually has started playback
+				if (ts.getCurrentFrame() > ts.getStartFrame()) {
+					int susFrame = ts.getSuspendedFrame() + ts.getRelativeInitiationFrame() + startFramePosition;
+					
+					if (susFrame > suspendedFramePosition)
+						suspendedFramePosition = susFrame;
+
+				}
+		
+				if (currentTrackSequences.isEmpty()) {
+					fireSubjectChanged(new SubjectChangedEvent(ApolloSubjectChangedEvent.PLAYBACK_STOPPED));
+				}
+			}
+			
+			break;
+			
+
+		}
+		
+		
+	}
+	
+
+	/**
+	 * {@inheritDoc}
+	 */
+	public void setObservedSubject(Subject parent) {
+		// Ignore: many subjects observed
+	}
+	
+	/**
+	 * {@inheritDoc}
+	 */
+	public SampledTrackModel getSharedSampledTrackModel(String localfilename) {
+		if (localfilename == null) return null;
+		
+		// The track group is updated at every loaded track
+		synchronized(currentOverdubs) {
+			// Search cached tracks
+			for (Overdub od : currentOverdubs) {
+				if (od.getTrackModel().getFilepath() != null &&
+						localfilename.equals(od.getTrackModel().getLocalFilename())) {
+					return od.getTrackModel();
+				}
+			}
+		}
+
+		return null;
+	}
+	
+	/**
+	 * {@inheritDoc}
+	 */
+	public void frameChanged() {
+		if (hasTrackCacheExpired() || isPlaying() || isLoading() || DisplayIO.getCurrentFrame() == null) return; // already expired - or is playing - so don't expire!
+		
+		String currentFrameName = DisplayIO.getCurrentFrame().getName();
+		
+		// Free track group every so often.. 
+		
+		if (currentODFrame != null && rootFrameName != null &&
+				currentODFrame.getFrameName().equals(this.rootFrameName)) {
+			// Reset if traversed through a frame that belongs to the current group
+			if (currentODFrame.getChild(currentFrameName) != null) { 
+				cacheExpiryCounter = CACHE_DEPTH; // reset
+				return;
+			}
+		}
+	
+		if (currentFrameName.equals(rootFrameName)) {
+			cacheExpiryCounter = CACHE_DEPTH; // resets cache - ok since cache has no expired at this point
+		} else {
+			cacheExpiryCounter--;
+			if (hasTrackCacheExpired()) {
+				freeCurrentTrackGroup();
+			}
+		}
+	}
+	
+	/**
+	 * @return
+	 * 		True if there are no cached tracks
+	 */
+	private boolean hasTrackCacheExpired() {
+		return cacheExpiryCounter <= 0;
+	}
+
+	/**
+	 * 	Releases all resource consumed by this. Stops any threads. Non-blocking.
+	 *  Intention: for shutting down Apollo. But can use if need to get rid of 
+	 *  any resources consumed by this.
+	 *  
+	 *  Note that this does not attempt to stop any tracks being played.
+	 *  
+	 */
+	public void releaseResources() {
+		
+		if (isLoading())
+			loaderThread.cancel();
+		
+		if (isPlaying())
+			stopPlayback();
+	}
+	
+	/**
+	 * 
+	 * @return
+	 * 		True if currently loading tracks.
+	 */
+	public boolean isLoading() {
+		return (loaderThread != null && !loaderThread.loadFinished);
+	}
+	
+	/**
+	 * 
+	 * @param rootFrameName
+	 * 
+	 * @param masterMix
+	 * 
+	 * @return
+	 * 		True if currently loading tracks with the given mix/frame.
+	 */
+	public boolean isLoading(String rootFrameName, String masterMix) {
+		return (isLoading() && isCurrentPlaybackSubject(rootFrameName, masterMix));
+	}
+	
+	/**
+	 * 
+	 * @return
+	 * 		True if currently playing
+	 */
+	public boolean isPlaying() {
+		return currentTrackSequences != null && !currentTrackSequences.isEmpty();
+	}
+	
+	/**
+	 * 
+	 * @param rootFrameName
+	 * 
+	 * @param masterMix
+	 * 
+	 * @return
+	 * 		True if currently playing the given mix/frame.
+	 */
+	public boolean isPlaying(String rootFrameName, String masterMix) {
+		return (isPlaying() && isCurrentPlaybackSubject(rootFrameName, masterMix));
+
+	}
+	
+	/**
+	 * Sets a paused mark - centralized paused info for multiple viewers.
+	 * Fires a {@link ApolloSubjectChangedEvent#PAUSE_MARK_CHANGED} event.
+	 * 
+	 * @param isMarked
+	 * 		True if to become marked. False to reset.
+	 */
+	public void setPauseMark(boolean isMarked) {
+		this.markedAsPaused = isMarked;
+		fireSubjectChanged(new SubjectChangedEvent(ApolloSubjectChangedEvent.PAUSE_MARK_CHANGED));
+	}
+	
+	/**
+	 * @return
+	 * 		True if the multplayback controller is marked as paused.
+	 */
+	public boolean isMarkedAsPaused() {
+		return this.markedAsPaused;
+	}
+	
+	/**
+	 * 
+	 * @param rootFrameName
+	 * 
+	 * @param masterMix
+	 * 
+	 * @return
+	 * 		True if currently playing the given mix/frame.
+	 */
+	public boolean isMarkedAsPaused(String rootFrameName, String masterMix) {
+		return (markedAsPaused && isCurrentPlaybackSubject(rootFrameName, masterMix));
+	}
+	
+	public boolean isCurrentPlaybackSubject(String rootFrameName, String masterMix) {
+		return (StringEx.equals(this.rootFrameName, rootFrameName) &&
+				StringEx.equals(this.masterChannelID, masterMix));
+	}
+
+
+	/**
+	 * In order to explicitly release the references of track models kept in the cache.
+	 * Note that releasing is handled internally.
+	 * 
+	 * If the track group is currently playing then nothing will result from this call
+	 * 
+	 */
+	public void freeCurrentTrackGroup() {
+		if (isPlaying()) return;
+		
+		synchronized(currentOverdubs) {
+			currentOverdubs.clear();
+		}
+		cacheExpiryCounter = -1;
+	}
+	
+	
+	/**
+	 * Adds a listener to the loader for recieving notifications about the load progress.
+	 * 
+	 * @param listener
+	 * 		The listener to attack. Must not be null.
+	 * 		If already on the listener list then it wont be added twice
+	 * 
+	 * @return
+	 * 		Null if did not attack due to not loading.
+	 * 		Otherwise all of the currently loaded tracks.
+	 * 		Note, may still receive notifications for some of the returned
+	 * 		loadeds tracks ... as the events could currently on the swing queue
+	 */
+	public List<String> attachLoadListener(MultitrackLoadListener listener) {
+		assert(listener != null);
+		if (isLoading()) {
+			
+			List<String> loaded = new LinkedList<String>();
+			
+			loaderThread.addLoadListener(listener);
+			synchronized (currentOverdubs) {
+				
+				for (Overdub od : currentOverdubs) {
+					loaded.add(od.getTrackModel().getLocalFilename());
+				}
+			}
+			
+			return loaded;
+		}
+		
+		return null;
+	}
+		
+	/**
+	 * Cancels loading phase of playback for the given frame/mix .. if
+	 * there is anything currently loading.
+	 * 
+	 * @param rootFrameName
+	 * 
+	 * @param masterMix
+	 * 
+	 */
+	public void cancelLoad(String rootFrameName, String masterMix) {
+		if (isCurrentPlaybackSubject(rootFrameName, masterMix) && isLoading()) {
+			loaderThread.cancel();
+		}
+	}
+	
+	/**
+	 * Stops the tracks from playing as soon as possible.
+	 */
+	public void stopPlayback() {
+		ApolloPlaybackMixer.getInstance().stop(currentTrackSequences);
+		// Notes: eventually their stop events will be invoked on this thread.
+		
+	}
+	
+	
+	/**
+	 * Asynchronously plays a frame of a linked track. Must be invoked from the swing thread.
+	 * Non-blocking - but sends feedback messages <i>on a dedicated thread</i> via the 
+	 * given load listener.
+	 * 
+	 * Raises {@link ApolloSubjectChangedEvent#MULTIPLAYBACK_LOADING} event when loading comences.
+	 * Raises playback events on success.
+	 * 
+	 * If this is currentlly in a loading state it will be cancelled and a the playback
+	 * call will halt until its cancelled (in acceptable amount of time for the user to interactive with).
+	 * 
+	 * @param listener
+	 * 		The listener used for callbacks of load progress.
+	 * 		Must never be null.
+	 * 
+	 * @param rootFrameName
+	 * 		The root frame from where all the tracks are to be loaded from.
+	 * 		Must never be null.
+	 * 
+	 * @param masterMixID
+	 * 		The master mix ID.
+	 * 
+	 * @param resume
+	 * 		True if resuming - the last-played files will be resumed instantly iff 
+	 * 		the last played set of tracks don't require reloading... you can resume with
+	 * 		false but by resuming with when you want to resume it could load faster.
+	 * 
+	 * @param relativeStartFrame
+	 * 		The start frame from when all the tracks should commence in the playback mixer.
+	 * 		Must be positive.
+	 * 
+	 * @param startFrame
+	 * 		The start from from <i>within</i> the group of tracks when playback should begin.
+	 * 		Must be positive. Clamped.
+	 * 
+	 * @param endFrame
+	 * 		Must be larger than start from. Clamped.
+	 * 		
+	 */
+	public void playFrame(
+			MultitrackLoadListener listener,
+			String rootFrameName,
+			String masterMixID,
+			boolean resume,
+			int relativeStartFrame,
+			int startFrame,
+			int endFrame) {
+		
+		assert(relativeStartFrame >= 0);
+		assert(startFrame >= 0);
+		assert(endFrame > startFrame);
+		assert(listener != null);
+		assert(rootFrameName != null);
+		assert(masterMixID != null);
+
+		// Check if curently loading:
+		if(loaderThread != null) {
+			loaderThread.cancel(); // non blocking
+			// NOTES: If the load thread is finished and there is an event waiting to be proccessed
+			// from the load thread (sometime after this event has finished proccessing) then explicitly
+			// cancel so that the waiting play event will definitly be aborted.
+			
+			// Also: cannot wait on it to finish because it may be waiting on some things to
+			// proccess on the swing thread... must leave it to die in its won time.
+		}
+		
+		// Check if currently playing
+		if (isPlaying()) {
+			stopPlayback();
+		}
+		
+		// Must clear the current track sequences even though eventually they will be removed
+		// due to the stop call... because in order for them to remove a away event on the queue behind
+		// this call will do so... hence it is impossible to wait on the stop events to raise since
+		// they are waiting for this operation to finish. Thus must explicity clear the track sequences 
+		// in order for playback to commence:
+		if (currentTrackSequences != null && !currentTrackSequences.isEmpty()) {
+			currentTrackSequences.clear();
+			fireSubjectChanged(new SubjectChangedEvent(ApolloSubjectChangedEvent.PLAYBACK_STOPPED));
+		}
+		
+		this.rootFrameName = rootFrameName;
+		this.masterChannelID = masterMixID;
+		
+		List<MultitrackLoadListener> loadListeners = new LinkedList<MultitrackLoadListener>();
+		loadListeners.add(listener);
+
+		// Note that the a group of track resuming may have to reload due to the cache expiring
+		if (resume 
+				&& !hasTrackCacheExpired() 
+				&& canInstantlyResume 
+				&& isCurrentPlaybackSubject(rootFrameName, masterMixID)) {
+
+			// Notify listener that load phase has instnatly completed
+			notifyListeners(loadListeners, MultitrackLoadListener.LOAD_COMPLETE, null, false);
+
+			// Play back ..s
+			commencePlayback(relativeStartFrame, startFrame, endFrame, loadListeners);
+			
+			
+		} else {
+	
+			// Load track and their start positions
+			loaderThread = new MultiTrackPlaybackLoader(relativeStartFrame, startFrame, endFrame, loadListeners);
+			
+			// Notify when in loading state.
+			fireSubjectChanged(new SubjectChangedEvent(ApolloSubjectChangedEvent.MULTIPLAYBACK_LOADING));
+			
+			loaderThread.start(); // will call commencePlayback once loaded.
+			
+		}
+
+	}
+
+
+	/**
+	 * @return
+	 * 		The last frame within all of the tracks that was rendered.
+	 * 		Can be make no sence if the audio structure has dramatically changed.
+	 */
+	public int getLastSuspendedFrame() {
+		return this.suspendedFramePosition;
+	}
+	
+	/**
+	 * @return
+	 * 		The last time a group of overdubs were played this is the first frame 
+	 * 		<i>within the apollo mixers timeline that the group began.</i>
+	 */
+	public long getLastInitiationFrame() {
+		return this.initiationFramePosition;
+	}
+	
+	/**
+	 * @return
+	 * 		The last time a group of overdubs were played this is the starting frame from
+	 * 		<i>within the track-groups combined timeline.</i>
+	 */
+	public int getLastStartFrame() {
+		return this.startFramePosition;
+	}
+	
+	/**
+	 * @return
+	 * 		The last time a group of overdubs were played this is the ending frame from
+	 * 		<i>within the track-groups combined timeline.</i>
+	 */
+	public int getLastEndFrame() {
+		return this.endFramePosition;
+	}
+	
+	/**
+	 * Begins playback for the current set of overdubs. 
+	 * 
+	 * Must not be in a playing state. MUST BE ON THE SWING THREAD.
+	 * 
+	 * @param relativeStartFrame
+	 * 		The start frame from when all the tracks should commence in the playback mixer.
+	 * 		Must be positive.
+	 * 
+	 * @param startFrame
+	 * 		The start from from <i>within</i> the group of tracks when playback should begin.
+	 * 		Must be positive. Clamped.
+	 * 
+	 * @param endFrame
+	 * 		Must be larger than start from. Clamped.
+	 * 
+	 * @param loadListeners
+	 * 		The listeners to receive status reports. Must not be shared. Must not be null or empty.
+	 */
+	private void commencePlayback(
+			int relativeStartFrame,
+			int startFrame,
+			int endFrame,
+			List<MultitrackLoadListener> loadListeners) {
+
+		assert(!isPlaying());
+		assert(!isPlaybackPending);
+		assert(loadListeners != null);
+		assert(!loadListeners.isEmpty());
+		
+		startFramePosition = startFrame;
+		endFramePosition = endFrame;
+		
+		// If got to this stage then instant resume is defiitly supported.
+		canInstantlyResume = true;
+		
+		// Reset the cache now that playing
+		cacheExpiryCounter = CACHE_DEPTH;
+
+		// Construct a list that is not shared by other threads and that
+		// only contains overdubs in the playing range.
+		List<Overdub> dubList = new LinkedList<Overdub>();
+		 
+		 
+		long totalFrames = 0;
+		
+		synchronized(currentOverdubs) {
+				
+			// Prepare each overdub - defining start, end and initation times.
+			for (Overdub od : currentOverdubs) {
+				
+				// Keep track of total frames in track for clamping later
+				long odEndFrame = od.getABSInitiationFrame() + od.getTrackModel().getFrameCount();
+				if (odEndFrame > totalFrames) totalFrames = odEndFrame;
+				
+				if (startFrame >= (od.getABSInitiationFrame() + od.getTrackModel().getFrameCount()) ||
+						endFrame <= od.getABSInitiationFrame()) {
+					// Exclude this track - its not in range
+
+				} else { // play this track - it is in range
+					
+					od.relativeInitiationFrame = (int)(od.getABSInitiationFrame() - startFrame);
+					
+					od.startFrame = (int)((startFrame > od.getABSInitiationFrame()) ? 
+							startFrame - od.getABSInitiationFrame() : 0);
+					
+					od.endFrame = (int)((endFrame < odEndFrame) ?
+							endFrame - od.getABSInitiationFrame() : od.getTrackModel().getFrameCount() - 1);
+				
+					if (od.startFrame < od.endFrame)
+						// Include in playlist
+						dubList.add(od);
+				}
+
+			}
+
+		}
+		
+		// Clamp:
+		if (startFramePosition > totalFrames)
+			startFramePosition = (int)totalFrames;
+		
+		if (endFramePosition > totalFrames)
+			endFramePosition = (int)totalFrames;
+	
+		if (dubList.isEmpty()) {
+			
+			notifyListeners(loadListeners, MultitrackLoadListener.NOTHING_TO_PLAY, null, false);
+			
+		} else {
+
+			boolean succeeded = false;
+			try {
+				
+				// Commence playback...
+				isPlaybackPending = true;
+				SoundDesk.getInstance().playOverdubs(dubList); // fires events which are relayed to observers
+				succeeded = true;
+				
+			} catch (LineUnavailableException e) {
+				
+				notifyListeners(loadListeners, MultitrackLoadListener.LOAD_FAILED_PLAYBACK, e, false);
+				
+			} finally {
+				
+				if (!succeeded) { // reset flag and clear track sequence list
+					currentTrackSequences = null;
+					isPlaybackPending = false;
+				}
+			}
+		}
+	}
+	
+	/**
+	 * Notifies the load listeners .. must notify on swing thread to
+	 * remember to pass flag in need to.
+	 * 
+	 * @param listeners
+	 * 		when accessed if invoteLaterOnSwing
+	 * 
+	 * @param id
+	 * 
+	 * @param state
+	 * 
+	 * @param invoteLaterOnSwing
+	 */
+	private void notifyListeners(
+			List<MultitrackLoadListener> listeners, 
+			int id, 
+			Object state,
+			boolean invoteLaterOnSwing) {
+		
+		if (invoteLaterOnSwing) {
+			
+			final List<MultitrackLoadListener> listeners1 = listeners;
+			final int id1 = id; 
+			final Object state1 = state;
+			SwingUtilities.invokeLater(new Runnable() {
+				public void run() {
+					synchronized(listeners1) {
+						notifyListeners(listeners1, id1, state1, false);
+					}
+				}
+			});
+			
+		} else {
+			
+			for (MultitrackLoadListener listener : listeners)
+				listener.multiplaybackLoadStatusUpdate(id, state);
+		}
+	}
+	
+	/**
+	 * Loads all tracks reachable from a given frame.
+	 * 
+	 * @author Brook Novak
+	 *
+	 */
+	private class MultiTrackPlaybackLoader extends Thread {
+		
+		/** Set to true once finished running. */
+		private boolean cancelRequested = false;
+		private boolean loadFinished = false; // Once playback has commenced - or failed/aborted... not like isAlive
+		
+		private OverdubbedFrame rootODFrame = null;
+		private int relativeStartFrame;
+		private int startFrame;
+		private int endFrame;
+		private List<MultitrackLoadListener> loadListeners = null; // reference immutable, contents not. Shared resource
+		
+		MultiTrackPlaybackLoader(int relativeStartFrame,
+				int startFrame,
+				int endFrame,
+				List<MultitrackLoadListener> loadListeners) {
+			
+			super("Multitrack Loader");
+			
+			this.loadListeners = loadListeners;
+			this.relativeStartFrame = relativeStartFrame;
+			this.startFrame = startFrame;
+			this.endFrame = endFrame;
+		}
+		
+		/**
+		 * Cancels the load stage.
+		 * NOTE: May actually succeed... but when it comes to commencing the playback then
+		 * then the play will abort.
+		 *
+		 */
+		public void cancel() {
+			cancelRequested = true;
+		}
+		
+		public void addLoadListener(MultitrackLoadListener ll) {
+			synchronized(loadListeners) {
+				if (!loadListeners.contains(ll)) loadListeners.add(ll);
+			}
+		}
+		
+		public void run() {
+	
+			assert(rootFrameName != null);
+			assert(masterChannelID != null);
+			
+			synchronized(loadListeners) {
+				assert(loadListeners != null);
+				assert(!loadListeners.isEmpty());
+			}
+			
+			boolean hasSucceeded = false;
+			
+			try {
+
+				if (cancelRequested) {
+					notifyListeners(loadListeners, 
+							MultitrackLoadListener.LOAD_CANCELLED, 
+							null, true);
+					return;
+				}
+	
+				// First fetch the graph for the rootframe to play
+
+				boolean hasUpdated = false;
+				do {
+					try {
+						AudioStructureModel.getInstance().waitOnUpdates();
+						hasUpdated = true;
+					} catch (InterruptedException e) {
+						e.printStackTrace();
+						continue;
+					}
+				} while (!hasUpdated);
+				
+				if (cancelRequested) {
+					notifyListeners(loadListeners, 
+							MultitrackLoadListener.LOAD_CANCELLED, 
+							null, true);
+					return;
+				}
+	
+				boolean hasFetched = false;
+				do {
+					try {
+						rootODFrame = AudioStructureModel.getInstance().fetchGraph(rootFrameName);
+						hasFetched = true;
+					} catch (InterruptedException e) { // cancelled
+						/* Consume */
+					} catch (TrackGraphLoopException e) { // contains loop
+						notifyListeners(loadListeners, MultitrackLoadListener.LOAD_FAILED_BAD_GRAPH, e, true);
+						return;
+						
+					}
+				} while (!hasFetched);	
+				
+				if (cancelRequested) {
+					notifyListeners(loadListeners, 
+							MultitrackLoadListener.LOAD_CANCELLED, 
+							null, true);
+					return;
+				}
+		
+				// Was there anything to play... (or does the frame even exist?)
+				if (rootODFrame == null) {
+					notifyListeners(loadListeners, MultitrackLoadListener.NOTHING_TO_PLAY, null, true);
+					return;
+				}
+				
+				// Get the absolute layout of the track graph.... i.e. a flattened view with
+				// all absolute initiation times stating from ms time 0.
+				ABSTrackGraphRetreiver absTrackGraphRetreiver = new ABSTrackGraphRetreiver(
+						rootODFrame, masterChannelID);
+	
+				try {
+					SwingUtilities.invokeAndWait(absTrackGraphRetreiver);
+				} catch (Exception e) {
+					notifyListeners(loadListeners, MultitrackLoadListener.LOAD_FAILED_GENERIC, e, true);
+					return;
+				}
+				
+				if (cancelRequested) {
+					notifyListeners(loadListeners, 
+							MultitrackLoadListener.LOAD_CANCELLED, 
+							null, true);
+					return;
+				}
+				
+				assert(absTrackGraphRetreiver.absGraph != null);
+				
+				// There are no track to play...
+				if (absTrackGraphRetreiver.absGraph.isEmpty()) {
+					notifyListeners(loadListeners, MultitrackLoadListener.NOTHING_TO_PLAY, null, true);
+					return;
+				}
+				
+				// First pass: get the list of all filenames to load...
+				
+				// Throw away tracks in the current track group that aren't in the new track group...
+				// keeping tracks that are already loaded.
+				// For the tracks that are kept ... re-create there overdubbed frame info
+				List<Overdub> transferred = new LinkedList<Overdub>();
+				
+				synchronized(currentOverdubs) {
+					
+					// Important to chck while locking currentOverdubs since this thread could be
+					// cancelled and another one of these threads loading and  wanting to also change the
+					// current overdubs.
+					if (cancelRequested) {
+						notifyListeners(loadListeners, 
+								MultitrackLoadListener.LOAD_CANCELLED, 
+								null, true);
+						return;
+					}
+	
+					for (Overdub od : currentOverdubs) {
+						
+						for (int i = 0; i < absTrackGraphRetreiver.absGraph.size(); i++) {
+							
+							AbsoluteTrackNode absNode = absTrackGraphRetreiver.absGraph.get(i);
+							
+							if (od.getTrackModel().getLocalFilename().equals(absNode.getTrackNode().getLocalFilename())) {
+								transferred.add(new Overdub(
+										od.getTrackModel(), 
+										absNode.getChannelID(), 
+										AudioMath.millisecondsToFrames(absNode.getABSStartTime(),
+												SampledAudioManager.getInstance().getDefaultPlaybackFormat())));
+								absTrackGraphRetreiver.absGraph.remove(i);
+								i --;
+							}
+	
+						}
+					}
+					
+					currentOverdubs.clear();
+					currentOverdubs.addAll(transferred);
+			
+				}
+				
+				// Notify load handlers of transferred tracks
+				for (Overdub od : transferred) {
+					notifyListeners(loadListeners, MultitrackLoadListener.TRACK_LOADED, 
+							od.getTrackModel().getLocalFilename(), true);
+				}
+				
+				// Go through and load each track one by one.
+				// Note: loadedTrackModels exludes transferred models because they have been dealt with
+				// already. Maps Localfilename - > track model
+				Map<String, SampledTrackModel> loadedTrackModels = new HashMap<String, SampledTrackModel>();
+	
+				// Load the tracks one by one... Update the track group incrementally
+				for (AbsoluteTrackNode absNode : absTrackGraphRetreiver.absGraph) {
+					
+					SampledTrackModel stm = loadedTrackModels.get(absNode.getTrackNode().getLocalFilename());
+					
+					if (stm != null) {
+						assert(stm.getLocalFilename().equals(absNode.getTrackNode().getLocalFilename()));
+					} else { // must load / retreive from somewhere the new track model
+						
+						try {
+							stm = TrackModelLoadManager.getInstance().load(
+									AudioPathManager.AUDIO_HOME_DIRECTORY + absNode.getTrackNode().getLocalFilename(), 
+									absNode.getTrackNode().getLocalFilename(), 
+									new Observer() { // opps, my confusing design pattern gone wrong!
+	
+										public Subject getObservedSubject() {
+											return null;
+										}
+	
+										/**
+										 * Cancel the load operation if a cancel request is opening
+										 */
+										public void modelChanged(Subject source, SubjectChangedEvent event) {
+											assert(event.getID() == ApolloSubjectChangedEvent.LOAD_STATUS_REPORT);
+											if (cancelRequested) {
+												((AudioFileLoader)source).cancelLoad();
+											}
+										}
+	
+										public void setObservedSubject(Subject parent) {
+										}
+										
+									}, // No need to observe
+									true); // Search around in expeditee memory for the track
+		
+						} catch (Exception e) {
+							e.printStackTrace();
+							notifyListeners(loadListeners, 
+									MultitrackLoadListener.TRACK_LOAD_FAILED_IO, 
+									absNode.getTrackNode().getLocalFilename(),
+									true);
+							continue;
+						} 
+	
+						// stm must only be null if a cancel was actually requested.
+						assert(stm != null || (stm == null && cancelRequested));
+						
+						if (stm == null || cancelRequested) {
+							notifyListeners(loadListeners, 
+									MultitrackLoadListener.LOAD_CANCELLED, 
+									null, true);
+							return;
+						}
+						
+					} 
+					
+					synchronized(currentOverdubs) {
+						// Important to chck while locking currentOverdubs since this thread could be
+						// cancelled and another one of these threads loading and  wanting to also change the
+						// current overdubs.
+						if (cancelRequested) {
+							notifyListeners(loadListeners, 
+									MultitrackLoadListener.LOAD_CANCELLED, 
+									null, true);
+							return;
+						}
+						currentOverdubs.add(new Overdub(stm, absNode.getChannelID(), AudioMath.millisecondsToFrames(absNode.getABSStartTime(),
+								SampledAudioManager.getInstance().getDefaultPlaybackFormat())));
+					}
+					
+					// Notify of loaded track
+					notifyListeners(loadListeners, 
+							MultitrackLoadListener.TRACK_LOADED, 
+							absNode.getTrackNode().getLocalFilename(),
+							true);
+	
+				} // load next track
+				
+				// It is possible that all of the loads failed that now left with
+				// nothing to play... so do a check before continuing
+				boolean isEmpty = false;
+				synchronized(currentOverdubs) {
+					isEmpty = currentOverdubs.isEmpty();
+				}
+				
+				if (isEmpty) {
+					notifyListeners(loadListeners, MultitrackLoadListener.NOTHING_TO_PLAY, null, true);
+					return;
+				}
+				
+				hasSucceeded = true;
+				
+				// Reset the cache now that playing (safety precaution)
+				cacheExpiryCounter = CACHE_DEPTH;
+				
+			} finally {
+				// Set flag for load currect state
+				if (!hasSucceeded) loadFinished = true;
+			}
+
+			// Must commence on the swing thread
+			SwingUtilities.invokeLater(new MultiTrackPlaybackCommencer());
+		}
+		
+		/**
+		 * Commences playback from the swing thread.
+		 * @author Brook Novak
+		 */
+		private class MultiTrackPlaybackCommencer implements Runnable {
+
+
+			public void run() {
+
+				try {
+					
+					// Note: loadListeners etc.. won't change since this is inner class created from
+					// parent...
+					assert(loadListeners != null);
+					assert(!loadListeners.isEmpty());
+					
+					// This is called from the loader thread once set to play. 
+					// However during the time when the load thread scheduled this on the AWT Queue
+					// and this actually running another AWT Event might have either directly started playback
+					// OR began loading a new set of tracks... either all must discard this redundant request
+					if (isPlaying() || MultiTrackPlaybackLoader.this != loaderThread || cancelRequested) {
+						// Notice the reference compare with loaderThread: since there could be another
+						// thread now starting... which implies that this should actually cancel!
+						
+						notifyListeners(loadListeners, 
+								MultitrackLoadListener.LOAD_CANCELLED, 
+								null, false);
+				
+						return; // discard redundant request
+					}
+				
+				} finally {
+					// Set load state for c
+					loadFinished = true;
+				}
+				
+				// Remmember root frame of new playback
+				currentODFrame = rootODFrame;
+				
+				commencePlayback(relativeStartFrame, startFrame, endFrame, loadListeners);
+				
+	
+				
+			}
+				
+		}
+		
+		/**
+		 * Retreives the overdub initiation times / channel ids from the swing thread.
+		 * 
+		 * @author Brook Novak
+		 */
+		private class ABSTrackGraphRetreiver implements Runnable {
+			
+			private String masterMixID;
+			private OverdubbedFrame rootODFrame;
+			private List<AbsoluteTrackNode> absGraph = null;
+			
+			ABSTrackGraphRetreiver(OverdubbedFrame rootODFrame, String masterMixID) {
+				this.rootODFrame = rootODFrame;
+				this.masterMixID = masterMixID;
+			}
+			
+			public void run() {
+				assert(rootODFrame != null);
+				assert(masterMixID != null);
+				absGraph = rootODFrame.getAbsoluteTrackLayoutDeep(masterMixID);
+			}
+		}
+		
+	}
+	
+	/**
+	 * A callback interface.
+	 * 
+	 * @author Brook Novak
+	 */
+	public interface MultitrackLoadListener {
+		
+		/**
+		 * Due to explicit cancel. New playback group request. 
+		 * Graph Model change while loading (after fetch).
+		 * Load/play operation aborted.
+		 */
+		public static final int LOAD_CANCELLED = 1;
+		
+		/**
+		 * Failed due to IO issue. State = localfilename of failed track
+		 * Load/play operation <b.not</b> aborted - it will play what it can, if 
+		 * thereends up being nothing to play then evenetually a {@link #NOTHING_TO_PLAY}
+		 * event will be raised.
+		 */
+		public static final int TRACK_LOAD_FAILED_IO = 2;
+		
+		/**
+		 * Failed due to graph containing loop. State = loop exception
+		 * Load/play operation aborted
+		 */
+		public static final int LOAD_FAILED_BAD_GRAPH = 6;
+		
+		/**
+		 * Failed due to playback issue. State = exception
+		 * Load/play operation aborted
+		 */
+		public static final int LOAD_FAILED_PLAYBACK = 3;
+		
+		/**
+		 * All overdubs are loaded into memory and are about to play.
+		 */
+		public static final int LOAD_COMPLETE = 4;
+		
+		/**
+		 * Load/play operation aborted because there is nothing to play.
+		 */
+		public static final int NOTHING_TO_PLAY = 7;
+		
+		/**
+		 * Failed - generic case. State = exception
+		 * Load/play operation aborted
+		 */
+		public static final int LOAD_FAILED_GENERIC = 8;
+		
+		/**
+		 * A track has been loaded. State = localfilename
+		 */
+		public static final int TRACK_LOADED = 9;
+
+		/**
+		 * A callback method that <i>is invoked from the swing thread</i> 
+		 * 
+		 * @param id
+		 * 		A code that describes the event being raised.
+		 * 		For example {@link #TRACK_LOADED}
+		 * 
+		 * @param state
+		 * 		Any state information passed. See id documentations for specific info. 
+		 */
+		public void multiplaybackLoadStatusUpdate(int id, Object state);
+
+	}
+
+	public OverdubbedFrame getCurrentODFrame() {
+		return currentODFrame;
+	}
+	
+	public String getCurrentMasterChannelID() {
+		return masterChannelID;
+	}
+	
+
+	
+
+}
Index: /trunk/src_apollo/org/apollo/audio/util/Overdub.java
===================================================================
--- /trunk/src_apollo/org/apollo/audio/util/Overdub.java	(revision 315)
+++ /trunk/src_apollo/org/apollo/audio/util/Overdub.java	(revision 315)
@@ -0,0 +1,63 @@
+package org.apollo.audio.util;
+
+import org.apollo.audio.SampledTrackModel;
+
+/**
+ * Describes a track that is part of a trackgroup for synchronized multiplayback.
+ * 
+ * @author Brook Novak
+ *
+ */
+final class Overdub {
+
+	private SampledTrackModel tmodel; // immutable never null
+	
+	private String channelID; // immutable never null - points to track sequence data via sounddesk
+	
+	private long absInitiationFrame;
+	
+	int startFrame;
+	
+	int endFrame;
+	
+	int relativeInitiationFrame;
+	
+	Overdub(SampledTrackModel tmodel, String channelID, long absInitiationFrame) {
+		assert(tmodel != null);
+		assert(channelID != null);
+		assert(absInitiationFrame >= 0);
+		
+		this.tmodel = tmodel;
+		this.channelID = channelID;
+		this.startFrame = -1;
+		this.endFrame = -1;
+		this.relativeInitiationFrame = -1;
+		this.absInitiationFrame = absInitiationFrame;
+	}
+
+	public String getChannelID() {
+		return channelID;
+	}
+
+	public int getEndFrame() {
+		return endFrame;
+	}
+
+	public int getRelativeInitiationFrame() {
+		return relativeInitiationFrame;
+	}
+	
+	public long getABSInitiationFrame() {
+		return absInitiationFrame;
+	}
+
+	public int getStartFrame() {
+		return startFrame;
+	}
+
+	public SampledTrackModel getTrackModel() {
+		return tmodel;
+	}
+
+	
+}
Index: /trunk/src_apollo/org/apollo/audio/util/PlaybackClock.java
===================================================================
--- /trunk/src_apollo/org/apollo/audio/util/PlaybackClock.java	(revision 315)
+++ /trunk/src_apollo/org/apollo/audio/util/PlaybackClock.java	(revision 315)
@@ -0,0 +1,213 @@
+package org.apollo.audio.util;
+
+import java.util.LinkedList;
+import java.util.List;
+
+import org.apollo.audio.ApolloPlaybackMixer;
+import org.apollo.audio.ApolloSubjectChangedEvent;
+import org.apollo.mvc.Observer;
+import org.apollo.mvc.Subject;
+import org.apollo.mvc.SubjectChangedEvent;
+import org.apollo.util.AudioMath;
+import org.expeditee.gui.FrameGraphics;
+
+/**
+ * The audio playback clock ticks at {@link #CLOCK_RESOLUTION} milliseconds and
+ * invokes all registered clock listeners <b>on the same thread as the clock<b>.
+ * Thus the clock is not entiry acurrate: its intention of use is for updating
+ * a GUI according to the live frame position in the software mixer.
+ * 
+ * Thus all thread safety precautions and efficiencies are at the users discretion.
+ * The ideal usage would be to simply update some data without the need of locking
+ * and returning asap
+ * 
+ * @see {@link ApolloPlaybackMixer}
+ * 
+ * @author Brook Novak
+ *
+ */
+public class PlaybackClock {
+	
+	public static final long CLOCK_RESOLUTION = 300;
+	
+	private Clock clockThread = null;
+	
+	private List<PlaybackClockListener> playbackClockListeners = new LinkedList<PlaybackClockListener>();
+	
+	private static PlaybackClock instance = new PlaybackClock();
+	public  static PlaybackClock getInstance() {
+		return instance;
+	}
+
+	/**
+	 * Singleton constructor preps the PlaybackClock for listening for
+	 * audio events from the software mixer
+	 *
+	 */
+	private PlaybackClock() {
+		
+		ApolloPlaybackMixer.getInstance().addObserver(new Observer() {
+
+			public Subject getObservedSubject() {
+				return ApolloPlaybackMixer.getInstance();
+			}
+			public void setObservedSubject(Subject parent) {
+			}
+
+			public void modelChanged(Subject source, SubjectChangedEvent event) {
+				
+				if (event.getID() == ApolloSubjectChangedEvent.PLAYBACK_STARTED) {
+					
+					// Start the clock
+					assert(clockThread == null);
+					clockThread = new Clock();
+					clockThread.start();
+					
+				} else if (event.getID() == ApolloSubjectChangedEvent.PLAYBACK_STOPPED) {
+					
+					// Kill the clock
+					clockThread.interrupt();
+					clockThread = null; // it will die in its own time
+				}
+			}
+
+		});
+	}
+	
+	/**
+	 * Adds a PlaybackClockListener for receiveing notifications whenever audio is played.
+	 * 
+	 * @param listener
+	 * 		The listener to add. If already added then it won't be added again.
+	 * 
+	 * @throws NullPointerException
+	 * 		If listener is null.
+	 */
+	public void addPlaybackClockListener(PlaybackClockListener listener) {
+		if (listener == null) throw new NullPointerException("listener");
+		synchronized(playbackClockListeners) {
+			if (!playbackClockListeners.contains(listener))
+				playbackClockListeners.add(listener);
+		}
+	}
+	
+	/**
+	 * Removes a listener such that it wil stop receiving notifications.
+	 * 
+	 * @param listener
+	 *      The listener to remove.
+	 *      
+	 * @throws NullPointerException
+	 * 		If listener is null.
+	 */
+	public void removePlaybackClockListener(PlaybackClockListener listener) {
+		if (listener == null) throw new NullPointerException("listener");
+		synchronized(playbackClockListeners) {
+			playbackClockListeners.remove(listener);
+		}
+	}
+	
+	/**
+	 * The clock periodically wakes and updates playback-related GUI according to the
+	 * live position of the playback. 
+	 * 
+	 * The clock thread dies once its interupted.
+	 * 
+	 * @author Brook Novak
+	 *
+	 */
+	private class Clock extends Thread {
+		
+		Clock() {
+			super("Playback Clock");
+		}
+		
+		public void run() {
+			
+			try {
+				long framePos, msPos;
+				boolean shouldRepaint;
+				
+				while (!interrupted()) {
+
+					// Get the live time in frames - directly from the hardware
+					framePos = ApolloPlaybackMixer.getInstance().getLiveFramePosition();
+	
+					shouldRepaint = false;
+					
+					if (framePos >= 0) { // is playing still?
+						
+						msPos = AudioMath.framesToMilliseconds(
+								framePos,  
+								ApolloPlaybackMixer.getInstance().getLiveAudioFormat());
+		
+						// Update the GUI. Because the clock awakes in small time slices must
+						// avoid updating the GUI on the swing thread: otherwise the AWT Event
+						// queue could easily become saturated.
+						synchronized(playbackClockListeners) { // arg, the most expensive operation!
+
+							for (PlaybackClockListener listener : playbackClockListeners) {
+	
+								listener.onTick(framePos, msPos); // trusting that an exception does not occur
+								// If one does occur the worst that can happen is the the user doesn't
+								// have playback freedback.
+								shouldRepaint = true;
+							}
+							
+						}
+						
+					}
+					
+					// Do a repaint is something was listening
+					if (shouldRepaint) {
+						FrameGraphics.requestRefresh(true); // later - should not congest AWT queue
+					}
+					
+					sleep(CLOCK_RESOLUTION);
+					
+				} // Keep updating GUI until interupted
+			} catch (InterruptedException e) { // consume
+				// Done
+			}
+			
+			// Notify that have completed
+			synchronized(playbackClockListeners) {
+				
+				for (PlaybackClockListener listener : playbackClockListeners) {
+					
+					listener.onTick(-1, -1);
+					
+				}
+				
+			} 
+			
+		}
+	
+	}
+	
+	/**
+	 * A PlaybackClockListener is periodically notified while there is playback occuring.
+	 * 
+	 * @author Brook Novak
+	 *
+	 */
+	public interface PlaybackClockListener {
+		
+		/**
+		 * At every tick, this is invoked <i>directly from the clock thread</i>
+		 * 
+		 * @see {@link PlaybackClock} for implementation considerations.
+		 * 
+		 * @param framePosition
+		 * 		The current position of the playback in frames.
+		 * 		Negative if playback has finished.
+		 * 
+		 * @param msPosition
+		 * 		The current position of the playback in milliseconds.
+		 * 		Negative if playback has finished.
+		 * 
+		 */
+		public void onTick(long framePosition, long msPosition);
+		
+	}
+}
Index: /trunk/src_apollo/org/apollo/audio/util/SoundDesk.java
===================================================================
--- /trunk/src_apollo/org/apollo/audio/util/SoundDesk.java	(revision 315)
+++ /trunk/src_apollo/org/apollo/audio/util/SoundDesk.java	(revision 315)
@@ -0,0 +1,1467 @@
+package org.apollo.audio.util;
+
+import java.util.ArrayList;
+import java.util.HashMap;
+import java.util.HashSet;
+import java.util.LinkedList;
+import java.util.List;
+import java.util.Set;
+import java.util.Stack;
+
+import javax.sound.sampled.LineUnavailableException;
+
+import org.apollo.audio.ApolloPlaybackMixer;
+import org.apollo.audio.ApolloSubjectChangedEvent;
+import org.apollo.audio.SampledTrackModel;
+import org.apollo.audio.TrackSequence;
+import org.apollo.audio.structure.LinkedTracksGraphNode;
+import org.apollo.audio.structure.OverdubbedFrame;
+import org.apollo.audio.structure.TrackGraphNode;
+import org.apollo.io.AudioPathManager;
+import org.apollo.io.MixIO;
+import org.apollo.mvc.AbstractSubject;
+import org.apollo.mvc.Observer;
+import org.apollo.mvc.Subject;
+import org.apollo.mvc.SubjectChangedEvent;
+import org.apollo.widgets.LinkedTrack;
+import org.apollo.widgets.SampledTrackEvo4;
+import org.expeditee.gui.Frame;
+
+/**
+ * A high level tool using the Apollo API providing the following features:
+ * 
+ * <ul>
+ *   <li> Playback / resume of track models
+ *   <li> Playback / resume of linked tracks
+ *   <li> Pause / Resume control
+ *   <li> Solo tracks
+ *   <li> Central mix repository
+ *   <li> Saving and Loading of mixes
+ *   <li> Saving and loading of master mix
+ * </ul>
+ * 
+ * Can think of this as the bridge between the Apollo API and Expeditee widgets.
+ * 
+ * By Apollo API I mean the {@link org.apollo.audio} package.
+ * 
+ * @author Brook Novak
+ *
+ */
+public class SoundDesk extends AbstractSubject implements Observer {
+	
+	/** Channel ID (All types) -> Channel. */
+	private HashMap<String, Channel> channels = new HashMap<String, Channel>();
+	
+	/** TrackSequence indexed Channel Set. */
+	private HashMap<TrackSequence, Channel> tseqIndexedChannels = new HashMap<TrackSequence, Channel>();
+	
+	/** Channel ID (Pure local) -> List of channels indirectly using the channel with channel-ID. */
+	private HashMap<String, LinkedList<Channel>> linkedChannels = new HashMap<String, LinkedList<Channel>>();
+	
+	/** Channel ID (Master) -> List of channels using the master mix. */
+	private HashMap<String, LinkedList<Channel>> childChannels = new HashMap<String, LinkedList<Channel>>();
+	
+	/** A whole branch of mixes can be soloed (even if not created yet) thif the prefix matches
+	 *	the solo prefix
+	 **/
+	private String soloIDPrefix = null;
+	
+	/** Where the mixes are saved to. */
+	private static final String MIX_BANKS_FILEPATH = AudioPathManager.AUDIO_HOME_DIRECTORY + ".banks";
+	
+	private static final String MASTER_MIX_FILEPATH = AudioPathManager.AUDIO_HOME_DIRECTORY + ".mastermix";
+	
+	private static final String NO_SOLO_PREFIX = "~#SOLOOFF#~";
+	
+	private static final char CHANNEL_ID_SEPORATOR = 2357;
+	private static final char LOCAL_CHANNEL_ID_TAG = 3421;
+	
+	/**
+	 * Singleton design pattern.
+	 */
+	private static SoundDesk instance = new SoundDesk();
+	public  static SoundDesk getInstance() {
+		return instance;
+	}
+	
+	/**
+	 * Singleton constructor - loads the mixes from file.
+	 */
+	private SoundDesk() {
+		loadMasterMix();
+		loadMixes();
+	}
+	
+	private void loadMasterMix() {
+		
+		TrackMixSubject mmix = MixIO.loadMasterMix(MASTER_MIX_FILEPATH);
+		
+		if (mmix != null) {
+		
+			ApolloPlaybackMixer.getInstance().setMasterMute(mmix.isMuted());
+		
+			ApolloPlaybackMixer.getInstance().setMasterVolume(mmix.getVolume());
+			
+			soloIDPrefix = mmix.getChannelID().equals(NO_SOLO_PREFIX) ?
+					null : mmix.getChannelID();
+
+		}
+
+	}
+	
+	private void loadMixes() {
+
+		
+		List<TrackMixSubject> banks = new LinkedList<TrackMixSubject>();
+		List<Boolean> usingLocalFlags = new LinkedList<Boolean>();
+		
+		if (MixIO.loadBanks(banks, usingLocalFlags, MIX_BANKS_FILEPATH)) {
+
+			for (int i = 0; i < banks.size(); i++) {
+				
+				TrackMixSubject mix = banks.get(i);
+				assert(mix != null);
+				
+				Boolean flag = usingLocalFlags.get(i);
+				assert(flag != null);
+				
+				createChannel(mix, flag);
+			}
+			
+		}
+	}
+	
+	public void saveMixes() {
+		
+		ArrayList<TrackMixSubject> banks = new ArrayList<TrackMixSubject>(channels.size());
+		ArrayList<Boolean> usingLocalFlags = new ArrayList<Boolean>(channels.size());
+			
+		for (Channel chan : channels.values()) {
+			banks.add(chan.mix);
+			usingLocalFlags.add(new Boolean(chan.isUsingLocalMix));
+		}
+		
+		MixIO.saveBanks(MIX_BANKS_FILEPATH, banks, usingLocalFlags);
+	}
+	
+	public void saveMasterMix() {
+		String id = soloIDPrefix;
+		if (id == null) id = NO_SOLO_PREFIX;
+		
+		MixIO.saveMasterMix(MASTER_MIX_FILEPATH, 
+				new TrackMixSubject(id, 
+						ApolloPlaybackMixer.getInstance().getMasterVolume(),
+						ApolloPlaybackMixer.getInstance().isMasterMuteOn()));
+	}
+	
+	/**
+	 * In order to solo a track sequence, use this to solo a group of
+	 * track sequences depending of their id starts with the given prefix.
+	 * Not that the effect is applied immediatly and the ApolloPlaybackMixer
+	 * mode changes to solo-on/off accordingly. 
+	 * 
+	 * Raises a {@link ApolloSubjectChangedEvent#SOLO_PREFIX_CHANGED} event if changed.
+	 * 
+	 * @param channelIDPrefix
+	 * 		The solo ID. Null for no solo.
+	 */
+	public void setSoloIDPrefix(String channelIDPrefix) {
+		
+		if (this.soloIDPrefix == channelIDPrefix) return;
+		
+		this.soloIDPrefix = channelIDPrefix;
+		
+		// Set flags
+		for (Channel ch : channels.values()) {
+			if (ch.tseq != null) {
+				if (channelIDPrefix == null) ch.tseq.setSoloFlag(false);
+				else ch.tseq.setSoloFlag(ch.mix.getChannelID().startsWith(channelIDPrefix));
+			}
+		}
+		
+		// Note, any track sequences create in the future will have their solo flag set
+		// according to the current soloIDPrefix.
+		
+		// Set solo state
+		ApolloPlaybackMixer.getInstance().setSoloEnable(channelIDPrefix != null);
+		
+		// Notify observers
+		fireSubjectChanged(new SubjectChangedEvent(
+				ApolloSubjectChangedEvent.SOLO_PREFIX_CHANGED));
+	}
+	
+	/**
+	 * Determines whether a track id is part of the current solo group.
+	 * 
+	 * @param channelID
+	 * 		Must not be null.
+	 * 
+	 * @return
+	 * 		True if the given id is in the solo group.
+	 * 
+	 * @throws NullPointerException
+	 * 		If mixID is null.
+	 */
+	public boolean isSolo(String channelID) {
+		if (channelID == null) throw new NullPointerException("channelID");
+		if (soloIDPrefix == null) return false;
+		return channelID.startsWith(soloIDPrefix);
+	}
+	
+	/**
+	 * Determines if a specific mix is playing.
+	 * 
+	 * @param channelID
+	 * 		The id to test - must not be null.
+	 * 
+	 * @return
+	 * 		True if a channel with the given ID is playing.
+	 * 
+	 * @throws NullPointerException
+	 * 		If channelID is null.
+	 */
+	public boolean isPlaying(String channelID) {
+		TrackSequence ts = getTrackSequence(channelID);
+		return ts != null && ts.isPlaying();
+	}
+	
+	/**
+	 * Gets the last frame psoition that a given chanel was played ... that is,
+	 * the last frame position <i>within the track sequences bytes</i>. I.e. not
+	 * refering to the software mixers timeline.
+	 * 
+	 * Note that this is specific to the audio bytes in the tracksequence that
+	 * was last played with the mix.
+	 * 
+	 * @param channelID
+	 * 		Must not be null.
+	 * 
+	 * @return
+	 * 		The last stopped frame position for the given channel ID.
+	 * 		negative if does not exist or has not been played.
+	 * 
+	 * @throws NullPointerException
+	 * 		If channelID is null
+	 */
+	public int getLastPlayedFramePosition(String channelID) {
+		if (channelID == null) throw new NullPointerException("channelID");
+		
+		Channel chan = channels.get(channelID);
+		
+		if (chan != null) return chan.stoppedFramePosition;
+		return -1;
+	}
+	
+	/**
+	 * Checks if a channel has been marked as being paused.
+	 * 
+	 * A channel can be marked as paused via {@link #setPaused(String, boolean)}.
+	 * The mark is reset whenever a channel is next played (or explicitly reset).
+	 * 
+	 * Using this flag in conjunction with {@link #getLastPlayedFramePosition(String)}
+	 * pause/resume functionality can be acheived.
+	 * 
+	 * @param channelID
+	 * 		Must not be null.
+	 * 
+	 * @return
+	 * 		True if the channel exists and is marked as paused.
+	 * 
+	 * @throws NullPointerException
+	 * 		If channelID is null
+	 */
+	public boolean isPaused(String channelID) {
+		if (channelID == null) throw new NullPointerException("channelID");
+		
+		Channel chan = channels.get(channelID);
+		
+		if (chan != null) return chan.isPaused;
+		return false;
+	}
+	
+	/**
+	 * Marks a channel as paused / not-paused.
+	 * 
+	 * The mark of a channel auto-resets whenever the channel is next played.
+	 * 
+	 * Raises a {@link ApolloSubjectChangedEvent#PAUSE_MARK_CHANGED} event
+	 * if true is returned.
+	 * 
+	 * @param channelID
+	 * 		Must not be null.
+	 * 
+	 * @param isPaused
+	 * 		True to mark as paused. False to reset mark.
+	 * 
+	 * @return
+	 * 		True if the mark was set. False if it wasn't set because the
+	 * 		channel did not exist or was already the same.
+	 * 
+	 * @throws NullPointerException
+	 * 		If channelID is null
+	 */
+	public boolean setPaused(String channelID, boolean isPaused) {
+		if (channelID == null) throw new NullPointerException("channelID");
+		
+		Channel chan = channels.get(channelID);
+		
+		if (chan != null && chan.isPaused != isPaused) {
+			
+			chan.isPaused = isPaused;
+			
+			fireSubjectChanged(new SubjectChangedEvent(
+					ApolloSubjectChangedEvent.PAUSE_MARK_CHANGED, channelID));
+
+			return true;
+		}
+		
+		return false;
+	}
+	
+	/**
+	 * Gets a track sequence assoicated with the track id.
+	 * 
+	 * @param channelID
+	 * 		Must not be null.
+	 * 
+	 * @return
+	 * 		The track sequence assoicated with the channelID. Null if none exists.
+	 * 
+	 * @throws NullPointerException
+	 * 		If channelID is null.
+	 */
+	public TrackSequence getTrackSequence(String channelID) {
+		if (channelID == null) throw new NullPointerException("channelID");
+		Channel chan = channels.get(channelID);
+		return (chan == null) ? null : chan.tseq;
+	}
+	
+	/**
+	 * 
+	 * @param channelID
+	 * 		A non-null indirect cahnnel id.
+	 * 
+	 * @param isUsingLocalMix
+	 * 		The flag to set. True to use the local mix settings instead of thwe indrect local
+	 * 		mix... otherwise set to false to override the lcal mix settings and use its own.
+	 * 
+	 * @return
+	 * 		True if set. False if cahnnel does not exist.
+	 * 
+	 * @throws IllegalArgumentException
+	 * 		If the channel exists and the type is not an indirect local channel.
+	 * 
+	 * @throws NullPointerException
+	 * 		If channelID is null.
+	 */
+	public boolean setUseLocalTrackMix(String channelID, boolean isUsingLocalMix) {
+		if (channelID == null) throw new NullPointerException("channelID");
+		Channel chan = channels.get(channelID);
+		
+
+		if (chan != null) {
+
+			if (chan.type != ChannelIDType.IndirectLocal)
+				throw new IllegalArgumentException("The channel ID \"" + channelID + "\" is no an indirect local ID");
+			
+			chan.isUsingLocalMix = isUsingLocalMix;
+			updateMixSettings(chan);
+			return true;
+		}
+		
+		return false;
+	}
+	
+	/**
+	 * Creates a empty mix: adding it to the mix set as well as observing it.
+	 * 
+	 * Assumes that the id has not been created yet.
+	 * 
+	 * @param channelID
+	 * 		The id to create the mix with
+	 * 
+	 * @return
+	 * 		The empty mix - never null.
+	 * 
+	 * @throws IllegalArgumentException
+	 * 		If channelID is invalid.
+	 */
+	private Channel createDefaultChannel(String channelID) {
+		return createChannel(channelID, 1.0f, false, true); // defaults
+	}
+	
+	/**
+	 * Creates a mix. Note that this will be saved in the mix batch file.
+	 * 
+	 * @param channelID
+	 *		A unique <i>unused</i> channel ID for the mix. Must not be null. 
+	 * 
+	 * @param volume
+	 * 		The volume for the new mix. Between 0 and 1. Clamped.
+	 * 
+	 * @param isMuted
+	 * 		Whether the mute of the mix is on or off.
+	 * 
+	 * @param isUsingLocalMix
+	 * 		True if the channel using the mix should instead use the local mix .. this only
+	 * 		applies to {@link ChannelIDType#IndirectLocal} channels.
+	 * 
+	 * @return
+	 * 		The created mix. Null if already exists.
+	 * 
+	 * @throws IllegalArgumentException
+	 * 		If channelID is invalid.
+	 */
+	public TrackMixSubject createMix(String channelID, float volume, boolean isMuted, boolean isUsingLocalMix) {
+		if (channels.containsKey(channelID)) return null;
+		return createChannel(channelID, volume, isMuted, isUsingLocalMix).mix;
+	}
+	
+	/**
+	 * 
+	 * @param channelID
+	 * 
+	 * @param volume
+	 * 
+	 * @param isMuted
+	 * 
+	 * @param isUsingLocalMix
+	 * 
+	 * @return
+	 * 
+	 * @throws IllegalArgumentException
+	 * 		If channelID is invalid.
+	 */
+	private Channel createChannel(String channelID, float volume, boolean isMuted, boolean isUsingLocalMix) {
+		assert(!channels.containsKey(channelID));
+		return createChannel(new TrackMixSubject(channelID, volume, isMuted), isUsingLocalMix);
+	}
+
+	/**
+	 * @see #createChannel(String, float, boolean)
+	 * 
+	 * @param mix
+	 * 		The mix to create.
+	 * 
+	 * @return
+	 * 		NULL if the mix already exists
+	 * 
+	 * @throws IllegalArgumentException
+	 * 		If the mix's channel ID is invalid.
+	 */
+	private Channel createChannel(TrackMixSubject mix, boolean isUsingLocalMix) {
+		assert(mix != null);
+		if (channels.containsKey(mix.getChannelID())) return null;
+
+		Channel chan = new Channel(mix, isUsingLocalMix); // throws IllegalArgumentException
+
+		channels.put(mix.getChannelID(), chan);
+		chan.mix.addObserver(this); // Keep track sequence data synched with mix
+		
+		// If the channel is an indirect local channel then update the linked track map
+		if (chan.type == ChannelIDType.IndirectLocal) {
+			
+			// Get the local channel ID that this channel points to
+			String local = extractLocalChannelID(mix.getChannelID());
+			assert(local != null);
+			
+			LinkedList<Channel> linked = linkedChannels.get(local);
+			
+			// Add the channel to the linked list ....
+			if (linked == null) {
+				linked = new LinkedList<Channel>();
+				linkedChannels.put(local, linked);
+			}
+			
+			if (!linked.contains(chan)) {
+				linked.add(chan);
+			}
+			
+			String parent = extractMasterChannelID(mix.getChannelID());
+			assert(parent != null);
+			
+			LinkedList<Channel> children = childChannels.get(parent);
+			if (children != null && !children.contains(chan))
+				children.add(chan);
+			
+		} else if (chan.type == ChannelIDType.Master) {
+			
+			LinkedList<Channel> children = new LinkedList<Channel>();
+			
+			for (Channel c : channels.values()) {
+				if (c == chan) continue;
+				if (c.masterMixID.equals(chan.mix.getChannelID())) {
+					assert(c.type == ChannelIDType.IndirectLocal);
+					children.add(c);
+				}
+			}
+			
+			childChannels.put(chan.mix.getChannelID(), children);
+			
+		}
+
+		
+		return chan;
+	}
+
+	/**
+	 * Either gets an existing from memory or creates a new mix for the given ID.
+	 * If had to create, default values of the mix are chosen (non-muted and full volume)
+	 * 
+	 * @param channelID
+	 * 		The ID of the mix to get / create. Must not be null.
+	 * 
+	 * @return
+	 * 		The retreived/constructed mix. Never null.
+	 * 
+	 * @throws NullPointerException
+	 * 		If id is null.
+	 * 
+	 * @throws IllegalArgumentException
+	 * 		If channelID is invalid.
+	 */
+	public TrackMixSubject getOrCreateMix(String channelID) {
+		
+		if (channelID == null) throw new NullPointerException("channelID");
+		
+		TrackMixSubject tmix = getMix(channelID);
+		
+		if (tmix == null) {
+			Channel chan = createDefaultChannel(channelID); // throws illegal arg
+			return chan.mix;
+			
+		} else return tmix;
+	}
+
+	/**
+	 * Retreives a mix.
+	 * 
+	 * @param channelID
+	 * 		The ID of the mix to get. Can be null.
+	 * 
+	 * @return
+	 * 		The Mix that is assoicated with the given ID.
+	 * 		Null if no mix exists OR if id was null.
+	 * 
+	 */
+	public TrackMixSubject getMix(String channelID) {
+		if (channelID == null) return null;
+		
+		Channel chan = channels.get(channelID);
+		if (chan != null) return chan.mix;
+		return null;
+	}
+	
+	/**
+	 * Raises {@link ApolloSubjectChangedEvent#TRACK_SEQUENCE_CREATED} events
+	 * for each overdub
+	 * 
+	 * @param overdubs
+	 * 		A non-null list of overdubs. Must not have overdubbs sharing
+	 * 		channels...
+	 * 
+	 * @return
+	 * 		The set of track sequences that were created for playing the overdubs.
+	 * 		Empty if there were none to play if the given overdub list is empty or
+	 * 		if none of them are in playing range.
+	 * 
+	 * @throws LineUnavailableException
+	 * 		If failed to get data line to output device.
+	 * 
+	 * @throws NullPointerException
+	 * 		if overdubs is null.
+	 * 
+	 * @throws IllegalArgumentException
+	 * 		if overdubs contains a null reference.
+	 * 		If overdubs channelID is invalid or a master channel
+	 * 		If contains two or more overdubs using the same channel.
+	 * 		If overdubs startFrame larger or equal to endFrame. 
+	 * 		Or overdubs startFrame is not in audio range. 
+	 * 		Or overdubs end frame is not in audio range
+	 * 
+	 * @throws IllegalStateException
+	 * 		If a channel with the overdubs channelID is already playing.
+	 */
+	public Set<TrackSequence> playOverdubs(
+			List<Overdub> overdubs) 
+			throws LineUnavailableException {
+		
+		if (overdubs == null) throw new NullPointerException("overdubs");
+		
+		Set<Channel> toPlay = new HashSet<Channel>(); // used for checking for dup channels
+		Set<TrackSequence> tracSeqs = new HashSet<TrackSequence>();
+		
+		// Anything to play?
+		if (overdubs.isEmpty()) 
+			return tracSeqs;
+		
+		boolean succeeded = false;
+		
+		try {
+		
+			// Get or create channels from the given IDs.
+			for (Overdub od : overdubs) {
+				
+				if (od == null) 
+					throw new IllegalArgumentException("overdubs contains a null reference"); 
+				
+				Channel chan = channels.get(od.getChannelID()); 
+
+				
+				if (chan == null) { // Create
+					chan = createDefaultChannel(od.getChannelID()); // Throws IllegalArgumentException
+					
+				} else if (toPlay.contains(chan)) { // dissallow re-use of channels within a playback group...
+					throw new IllegalArgumentException("Duplicate use of channel in multiplayback request");
+				}
+				
+
+				 // TODO: Remove
+				System.out.println("TODO: Remove.. when unlock full mixing capabilities");
+				chan.isUsingLocalMix = true;
+				
+				
+				
+				
+				// Check that the channel ID is not a master mix:
+				if (chan.type == ChannelIDType.Master)
+					throw new IllegalArgumentException(
+							"Master channels do not directly playback tracks (channelID = " 
+							+ od.getChannelID() + ")");
+	
+				// Check that the target channel is not already playing
+				if (chan.tseq != null && chan.tseq.isPlaying())
+					throw new IllegalStateException("The channel " + od.getChannelID() + " is already in use");
+				
+				// Create the (new) track sequence to associate the mix with
+				TrackSequence ts = new TrackSequence( // throws exceptions
+						od.getTrackModel().getAllAudioBytes(), 
+						od.getTrackModel().getFormat(),
+						od.getStartFrame(), 
+						od.getEndFrame(), 
+						od.getRelativeInitiationFrame(),
+						od.getTrackModel()); // Important: set as track model so it can be re-used
+				
+				ts.addObserver(this);
+
+				// Reset pause flag
+				chan.isPaused = false;
+				chan.tseq = ts;
+				
+				tseqIndexedChannels.put(ts, chan);
+				tracSeqs.add(ts);
+				
+				// Synch TS with mix
+				ts.setSoloFlag(isSolo(od.getChannelID()));
+				updateMixSettings(chan);
+		
+				// Allow listeners to register to the track sequence so that they can
+				// catch playback events for the mix
+				fireSubjectChanged(new SubjectChangedEvent(
+						ApolloSubjectChangedEvent.TRACK_SEQUENCE_CREATED, od.getChannelID()));
+
+			}
+			
+			
+		//	if (!tracSeqs.isEmpty()) {  // if nothing is in range can become empty
+				// Commence playback... when its scheduled to
+				ApolloPlaybackMixer.getInstance().playSynchronized(tracSeqs);
+			//}
+			
+			succeeded = true;
+		
+		} finally {
+			
+			// Did fail in any way? 
+			if (!succeeded) {
+			
+				// Ensure that track sequences are all removed since some may not be able
+				// to be erased by observation... (modelchanged)
+				for (Channel chan : toPlay) {
+					if (chan.tseq != null && 
+							!chan.tseq.isPlaying()) { // if is created but has not started
+						tseqIndexedChannels.remove(chan.tseq);
+						chan.tseq = null;
+					}
+				}
+				
+			}
+		}
+
+		return tracSeqs;
+	}
+	
+	/**
+	 * Raises an {@link ApolloSubjectChangedEvent#TRACK_SEQUENCE_CREATED} event 
+	 * just before playback commences. Note that the event can occur but the track sequence
+	 * may fail to playback.
+	 * 
+	 * @param tmodel
+	 * 		The track model to play. Must not be null.
+	 * 
+	 * @param channelID
+	 * 		The channel ID to use for the playback. Note that a channel can only be
+	 * 		in use one at a time 
+	 * 
+	 * @param startFrame
+	 * 		The frame <i>within the track</i> where to start playback. Inclusive.
+	 * 
+	 * @param endFrame
+	 * 		The frame <i>within the track</i> where to end playback. Inclusive.
+	 * 
+	 * @param relativeInitiationFrame
+	 * 		Where to initiate the frame. This is relative to where the track should start playing
+	 * 		in frames, when queued in the track graph.
+	 * 
+	 * @throws LineUnavailableException
+	 * 		If failed to get data line to output device
+	 * 
+	 * @throws NullPointerException
+	 * 		if channelID or tmodel is null.
+	 * 
+	 * @throws IllegalArgumentException
+	 * 		If channelID is empty, or invalid, or a master channel
+	 * 		If startFrame larger or equal to endFrame. 
+	 * 		Or startFrame is not in audio range. 
+	 * 		Or end frame is not in audio range
+	 * 
+	 * @throws IllegalStateException
+	 * 		If the channel with the given channelID is already playing.
+	 * 
+	 */
+	public void playSampledTrackModel(	
+			SampledTrackModel tmodel, 
+			String channelID,
+			int startFrame,
+			int endFrame,
+			int relativeInitiationFrame) 
+			throws LineUnavailableException {
+		
+		if (tmodel == null) throw new NullPointerException("tmodel");
+		if (channelID == null) throw new NullPointerException("channelID");
+		if (channelID.length() == 0) throw new IllegalArgumentException("channelID");
+		
+		// Get or create a channel from the given ID.
+		Channel chan = channels.get(channelID);
+		
+		if (chan == null) { // Create
+			chan = createDefaultChannel(channelID); // Throws IllegalArgumentException
+		}
+		
+		
+		System.out.println("TODO: Remove.. when unlock full mixing capabilities");
+		chan.isUsingLocalMix = true; // TODO: Remove
+		
+		
+		// Check that the channel ID is not a master mix:
+		if (chan.type == ChannelIDType.Master)
+			throw new IllegalArgumentException(
+					"Master channels do not directly playback tracks (channelID = " 
+					+ channelID + ")");
+		
+		// Check that the mix is not already playing
+		if (chan.tseq != null && chan.tseq.isPlaying())
+			throw new IllegalStateException("The channel " + channelID + " is already in use");
+		
+		// Create the (new) track sequence to associate the mix with
+		TrackSequence ts = new TrackSequence(
+				tmodel.getAllAudioBytes(), 
+				tmodel.getFormat(), 
+				startFrame, 
+				endFrame, 
+				relativeInitiationFrame,
+				tmodel); // Important: set as track model so it can be re-used
+
+		boolean succeeded = false;
+		
+		try {
+
+			// Whenever the track is stopped, then auto-remove the track sequence and nullify
+			// the ts reference so that the referenced SampledTrackModel can be collected
+			// the the garbage collector
+			ts.addObserver(this);
+		
+			
+			// Reset pause flag
+			chan.isPaused = false;
+			chan.tseq = ts;
+			
+			tseqIndexedChannels.put(ts, chan);
+			
+			// Synch TS with mix
+			ts.setSoloFlag(isSolo(channelID));
+			updateMixSettings(chan);
+	
+			// Allow listeners to register to the track sequence so that they can
+			// catch playback events for the mix
+			fireSubjectChanged(new SubjectChangedEvent(
+					ApolloSubjectChangedEvent.TRACK_SEQUENCE_CREATED, channelID));
+			
+
+			// Commence playback... when its scheduled to
+			ApolloPlaybackMixer.getInstance().play(ts);
+			succeeded = true;
+			
+		} finally {
+			if (!succeeded) {
+				chan.tseq = null;
+				tseqIndexedChannels.remove(ts);
+			}
+		}
+
+	}
+	
+	/**
+	 * Ensures that a channels mix settings are synched as well as all other
+	 * channels that depend on it... (e.g. a pure local channel updating its indirect tracks).
+	 * 
+	 * Omittedly, this is way to over the top than it needs to be :(
+	 * 
+	 * @param chan
+	 * 			Must not be null.
+	 */
+	private void updateMixSettings(Channel chan) {
+		assert(chan != null);
+		
+		// Update track sequence playing on the channel at the moment ..
+		// Only if this track mix is to be used for the track sequence
+		if (chan.tseq != null && 
+				chan.type != ChannelIDType.Master) {
+			
+			boolean isMuted;
+			float volume;
+			if (chan.type == ChannelIDType.IndirectLocal && chan.isUsingLocalMix) {
+
+				String localChannelID = extractLocalChannelID(chan.mix.getChannelID());
+				assert(localChannelID != null); // since type is not master
+				
+				Channel baseChan = channels.get(localChannelID);
+				if (baseChan != null) { // it is possible for local channels to be missing
+					assert(baseChan.type == ChannelIDType.PureLocal);
+					isMuted = baseChan.mix.isMuted();
+					volume = baseChan.mix.getVolume();
+				} else {
+					isMuted = chan.mix.isMuted();
+					volume = chan.mix.getVolume();
+				}
+
+			} else {
+				// pre-mix with master mix - if has one
+				isMuted = chan.mix.isMuted();
+				volume = chan.mix.getVolume();
+			}
+
+			
+			if (chan.type == ChannelIDType.IndirectLocal) {
+				Channel master = channels.get(chan.masterMixID);
+	
+				if (master != null) {
+					assert(master.type == ChannelIDType.Master);
+					isMuted |= master.mix.isMuted();
+					volume *= master.mix.getVolume();
+				}
+			}
+			
+			chan.tseq.setMuted(isMuted);
+			chan.tseq.setVolume(volume);
+		}
+		
+		// Update any channels that are linking to this (local) channel's mix at the momment
+		if (chan.type == ChannelIDType.PureLocal) {
+			
+			List<Channel> lchans = linkedChannels.get(chan.mix.getChannelID());
+			
+			if (lchans != null) {
+				for (Channel lchan : lchans) {
+					if (lchan.isUsingLocalMix && lchan.tseq != null) {
+						
+						// pre-mix with master mix - if has one
+						boolean isMuted = chan.mix.isMuted();
+						float volume = chan.mix.getVolume();
+						
+						Channel master = channels.get(lchan.masterMixID);
+
+						if (master != null) {
+							assert(master.type == ChannelIDType.Master);
+							isMuted |= master.mix.isMuted();
+							volume *= master.mix.getVolume();
+						}
+						
+						lchan.tseq.setMuted(isMuted);
+						lchan.tseq.setVolume(volume);
+	
+					}
+				}
+			}
+			
+		// Update any channels that are using this master mix at the moment
+		} else if (chan.type == ChannelIDType.Master) {
+
+			List<Channel> childChans = childChannels.get(chan.mix.getChannelID());
+			
+			if (childChans != null) {
+				for (Channel cchan : childChans) {
+					assert(cchan.masterMixID.equals(chan.mix.getChannelID()));
+					assert(cchan.type == ChannelIDType.IndirectLocal);
+					
+					// Is playing? i.e. do need to update?
+					if (cchan.tseq == null) continue;
+					
+					TrackMixSubject baseMix = null;
+					
+					// If the child channel using this master mix is also using a local mix then
+					// retreive the local mix ....
+					if (cchan.isUsingLocalMix) {
+						
+						String localChannelID = extractLocalChannelID(cchan.mix.getChannelID());
+						assert(localChannelID != null); // since type is not master
+						
+						Channel baseChan = channels.get(localChannelID);
+						if (baseChan != null) { // it is possible for local channels to be missing
+							assert(baseChan.type == ChannelIDType.PureLocal);
+							baseMix = baseChan.mix;
+						}
+		
+						
+					} else {
+						baseMix = cchan.mix;
+					}
+					
+					boolean isMuted = chan.mix.isMuted() ;
+					float volume = chan.mix.getVolume();
+					
+					if (baseMix != null) {
+						isMuted |= baseMix.isMuted();
+						volume *= baseMix.getVolume();
+					}
+	
+					cchan.tseq.setMuted(isMuted);
+					cchan.tseq.setVolume(volume);
+	
+				}
+			}
+			
+		}
+		
+	}
+
+	public Subject getObservedSubject() {
+		return null;
+	}
+	public void setObservedSubject(Subject parent) {
+	}
+
+	/**
+	 * Synchs the track sequences with the mixes.
+	 * Assumes event is a ApolloSubjectChangedEvent and source
+	 * is a TrackMixSubject
+	 */
+	public void modelChanged(Subject source, SubjectChangedEvent event) {
+		
+		if (source instanceof TrackMixSubject) {
+			
+			TrackMixSubject tmix = (TrackMixSubject)source;
+			
+			Channel chan = channels.get(tmix.getChannelID());
+			assert(chan != null);
+			assert(chan.mix == tmix);
+			
+			updateMixSettings(chan);
+
+		} else if (source instanceof TrackSequence) {
+
+			if (event.getID() == ApolloSubjectChangedEvent.PLAYBACK_STOPPED) {
+				
+				Channel chan = tseqIndexedChannels.get(source);
+				if (chan != null) {
+					freeChannel(chan, (TrackSequence)source);
+				}
+			}
+		}
+		
+	}
+	
+	public void freeChannels(String channelPrefix) {
+		assert (channelPrefix != null);
+		assert (channelPrefix.length() > 0);
+		
+		for (Channel chan : channels.values()) {
+			
+			if (chan.tseq != null && chan.mix.getChannelID().startsWith(channelPrefix)) {
+				
+				freeChannel(chan, chan.tseq);
+			}
+		}
+		
+	}
+	
+	private void freeChannel(Channel chan, TrackSequence ts) {
+		assert (chan != null);
+		assert (ts != null);
+
+		chan.stoppedFramePosition = (chan.tseq != null) ? chan.tseq.getSuspendedFrame() : -1;
+		chan.tseq = null; // Free SampledTrackModel reference for garbage collection
+		tseqIndexedChannels.remove(ts); // "  ditto  "
+		
+	}
+
+	/**
+	 * Creates a <i>pure local</i> channel ID.
+	 * 
+	 * @param sampledTrackWidget
+	 * 		The widget to create the ID from. Must not be null.
+	 * 			
+	 * @return
+	 * 		The channel ID... never null.
+	 */
+	public static String createPureLocalChannelID(SampledTrackEvo4 sampledTrackWidget) {
+		assert(sampledTrackWidget != null);
+		return createPureLocalChannelID(sampledTrackWidget.getLocalFileName());
+	}
+	
+	/**
+	 * Creates a <i>pure local</i> channel ID.
+	 * 
+	 * @param tinf
+	 * 		The TrackGraphInfo to create the ID from. Must not be null.
+	 * 			
+	 * @return
+	 * 		The channel ID... never null.
+	 */
+	public static String createPureLocalChannelID(TrackGraphNode tinf) {
+		assert(tinf != null);
+		return createPureLocalChannelID(tinf.getLocalFilename());
+	}
+	
+	/**
+	 * Creates a <i>pure local</i> channel ID.
+	 * 
+	 * @param localFilename
+	 * 		The local filename to create the channel ID from
+	 * 			
+	 * @return
+	 * 		The channel ID... never null.
+	 */
+	public static String createPureLocalChannelID(String localFilename) {
+		assert(localFilename != null && localFilename.length() > 0);
+		return localFilename + LOCAL_CHANNEL_ID_TAG;
+	}
+	
+	/**
+	 * Creates an <i>indirect</i> local channel id from a given virtual path.
+	 * 
+	 * @param virtualPath
+	 * 		The virtual path to create the id from. Must not be null.
+	 * 
+	 * @param localFilename
+	 * 		The local filename that the virtual path points to. Must not be null.
+	 * 
+	 * @return
+	 * 		An indirect local channel id... Never null.
+	 */
+	public static String createIndirectLocalChannelID(List<String> virtualPath, String localFilename) {
+		assert(virtualPath != null);
+		assert(localFilename != null);
+		
+		// Build prefix
+		StringBuilder sb = new StringBuilder();
+		
+		for (String virtualName : virtualPath) {
+			sb.append(virtualName);
+			sb.append(CHANNEL_ID_SEPORATOR);
+		}
+		
+		sb.append(createPureLocalChannelID(localFilename));
+		
+		return sb.toString();
+	}
+	
+	public static String createMasterChannelID(LinkedTrack lwidget) {
+		return lwidget.getVirtualFilename();
+	}
+	/**
+	 * Creates a master channel ID from a frame.
+	 * 
+	 * @param frame
+	 * 		The frame to create the master mix ID from.
+	 * 		Must not be null.
+	 * 
+	 * @return
+	 * 		The master mix for the given frame.
+	 */
+	public static String createMasterChannelID(Frame frame) {
+		assert(frame != null);
+		return frame.getName();
+	}
+	
+	/**
+	 * Creates a master channel ID from a OverdubbedFrame.
+	 * 
+	 * @param odFrame
+	 * 		The frame to create the master mix ID from.
+	 * 		Must not be null.
+	 * 
+	 * @return
+	 * 		The master mix for the given odFrame.
+	 */
+	public static String createMasterChannelID(OverdubbedFrame odFrame) {
+		assert(odFrame != null);
+		return odFrame.getFrameName();
+	}
+	
+	/**
+	 * Creates a master channel ID from a LinkedTracksGraphInfo.
+	 * 
+	 * @param ltracks
+	 * 		The linked track to create the master mix ID from.
+	 * 		Must not be null.
+	 * 
+	 * @return
+	 * 		The master mix for the given ltracks.
+	 */
+	public static String createMasterChannelID(LinkedTracksGraphNode ltracks) {
+		assert(ltracks != null);
+		return ltracks.getVirtualFilename();
+	}
+	
+	
+	/**
+	 * Recursively creates channel IDs from a OverdubbedFrame. I.e. for all its
+	 * desending tracks. Linked tracks are exluded and only used for recursing.
+	 * 
+	 * <b>Exlcudes the ID for odFrame (the master channel)</b>
+	 * 
+	 * @param odFrame
+	 * 		The frame to start recursivly building the ID's from.
+	 * 
+	 * @return
+	 * 		The list of linked ID's (to actual tracks) from this frame...
+	 * 		Never null. Can be empty if there are no tracks from  the
+	 * 		given frame.
+	 */
+	public static List<String> createChannelIDs(OverdubbedFrame odFrame) {
+		assert(odFrame != null);
+
+		Stack<String> virtualPath = new Stack<String>();
+		virtualPath.push(createMasterChannelID(odFrame)); // Master mix
+		List<String> ids = new LinkedList<String>();
+		createChannelIDs(odFrame, ids, new Stack<OverdubbedFrame>(), virtualPath);
+		assert(virtualPath.size() == 1);
+		
+		return ids;
+	}
+	
+	/**
+	 * Recursively creates channel IDs from a LinkedTracksGraphInfo. I.e. for all its
+	 * desending tracks. Linked tracks are exluded and only used for recursing.
+	 * 
+	 * <b>Exlcudes the ID for ltracks (the master channel)</b>
+	 * 
+	 * <b>Note:</b> calling {@link #createChannelIDs(OverdubbedFrame)} with the links
+	 * OverdubbedFrame is not the equivelent of calling this... because it uses a
+	 * different master channel ID.
+	 * 
+	 * @param ltracks
+	 * 		The link to start recursivly building the ID's from.
+	 * 
+	 * @return
+	 * 		The list of linked ID's (to actual tracks) from this frame...
+	 * 		Never null. Can be empty if there are no tracks from the
+	 * 		given ltracks.
+	 */
+	public static List<String> createChannelIDs(LinkedTracksGraphNode ltracks) {
+		assert(ltracks != null);
+		
+		Stack<String> virtualPath = new Stack<String>();
+		virtualPath.push(createMasterChannelID(ltracks));
+		List<String> ids = new LinkedList<String>();
+		createChannelIDs(ltracks.getLinkedFrame(), ids, new Stack<OverdubbedFrame>(), virtualPath);
+		assert(virtualPath.size() == 1);
+		
+		return ids;
+	}
+	
+	/**
+	 * Recursively creates channel ID.
+	 * @param current
+	 * 		The root at which to recuse at.
+	 * 
+	 * @param ids
+	 * 		A list of strings to populate with channel ids.
+	 * 
+	 * @param visited
+	 * 		Non-null Empty.
+	 * 
+	 * @param virtualPath
+	 * 		Preload with IDs if needed.
+	 */
+	private static void createChannelIDs(
+			OverdubbedFrame current,
+			List<String> ids,
+			Stack<OverdubbedFrame> visited,
+			Stack<String> virtualPath) {
+		
+		// Avoid recursion .. never can be to safe.
+		if (visited.contains(current)) return;
+		visited.push(current);
+
+		if (current.trackExists()) {
+			
+			// Build prefix
+			StringBuilder prefixBuilder = new StringBuilder();
+			
+			for (String virtualName : virtualPath) {
+				prefixBuilder.append(virtualName);
+				prefixBuilder.append(CHANNEL_ID_SEPORATOR);
+			}
+			
+			String prefix = prefixBuilder.toString();
+			assert(prefix.length() > 0);
+			assert(prefix.charAt(prefix.length() - 1) == CHANNEL_ID_SEPORATOR);
+			
+			// Create id's
+			for (TrackGraphNode tinf : current.getUnmodifiableTracks()) {
+				String id = prefix + createPureLocalChannelID(tinf);
+				assert(!ids.contains(id));
+				ids.add(id);
+			}
+		}
+		
+		// Recurse
+		for (LinkedTracksGraphNode ltinf : current.getUnmodifiableLinkedTracks()) {
+			virtualPath.push(ltinf.getVirtualFilename()); // build vpath
+			createChannelIDs(ltinf.getLinkedFrame(), ids, visited, virtualPath);
+			virtualPath.pop(); // maintain vpath
+		}
+
+		visited.pop();
+
+	}
+
+	
+	/**
+	 * Extracts a pure local channelID...
+	 * Thus for a given channel ID made up of virtual links its all the virtual
+	 * ids are stripped and the remaining local channel ID is returned.
+	 * 
+	 * @param channelID
+	 * 		The channel ID to extract the local channel ID from.
+	 * 		Must not be null or empty.
+	 * 
+	 * @return
+	 * 		The local channel ID. Null if channelID does not refer to a local channel ID:
+	 * 		in that case the channel ID would either be invalid or be a pure link channel.
+	 * 
+	 */
+	public static String extractLocalChannelID(String channelID) {
+		assert (channelID != null);
+		assert(channelID.length() > 0);
+		
+		if (channelID.charAt(channelID.length() - 1) == LOCAL_CHANNEL_ID_TAG) {
+			
+			// Starting at the end... search bacward for the start or a seporator
+			int i = channelID.length() - 2;
+			while (i > 0 && channelID.charAt(i) != CHANNEL_ID_SEPORATOR) i--;
+			
+			if (channelID.charAt(i) == CHANNEL_ID_SEPORATOR) i++;
+
+			if (((channelID.length() - 1) - i) == 0) return null;
+			
+			return channelID.substring(i, channelID.length()); // include the local tag
+			
+		}
+		
+		// No local part
+		return null;
+	}
+	
+	/**
+	 * Pure local or indirect local Channel IDs are encoded with local track names, 
+	 * 
+	 * @param channelID
+	 * 		The channel ID to extract the local filename from.
+	 * 
+	 * @return
+	 * 		The local filename from the ID. Null if the channel ID is not a 
+	 * 		pure local / indirect local id.
+	 * 		
+	 */
+	public static String extractLocalFilename(String channelID) {
+		String localID = extractLocalChannelID(channelID);
+		if (localID != null && localID.length() > 1) 
+			return localID.substring(0, localID.length() - 1);
+		
+		return null;
+	}
+	
+	/**
+	 * Extracts the master ID from a channel ID.
+	 * If the channel id is a pure local ID then it will return channelID.
+	 * 
+	 * @param channelID
+	 * 		The channel id to extract the master id from. Must not be null.		
+	 * 
+	 * @return
+	 * 		The master ID. Null if channelID is null or invalid in some way...
+	 */
+	public static String extractMasterChannelID(String channelID) {
+		assert (channelID != null);
+		
+		// master id = top-level id... that is, a pure virtual name, framename or local name
+		
+		int i;
+		for (i = 0; i < channelID.length(); i++) {
+			if (channelID.charAt(i) == CHANNEL_ID_SEPORATOR) break;
+		}
+		
+		if (i > 1) {
+			return channelID.substring(0, i);
+		}
+		
+		return null;
+		
+	}
+	
+	/**
+	 * Gets the classification of a channel ID.
+	 * 
+	 * @see {@link ChannelIDType} for info on the channel types
+	 * 
+	 * @param channelID
+	 * 		The channel id to classify. Must not be null or empty.
+	 * 
+	 * @return
+	 * 		Either the classification for the given ID or Null if the ID is invalid.
+	 */
+	public static ChannelIDType getChannelIDType(String channelID) {
+		assert (channelID != null);
+		assert (channelID.length() > 0);
+		
+		if (channelID.indexOf(CHANNEL_ID_SEPORATOR) >= 0) {
+			
+			if (channelID.charAt(channelID.length() - 1) != LOCAL_CHANNEL_ID_TAG)
+				return null; // Invalid!
+			
+			return ChannelIDType.IndirectLocal;
+			
+		} else if (channelID.charAt(channelID.length() - 1) == LOCAL_CHANNEL_ID_TAG) {
+				return ChannelIDType.PureLocal;
+		}
+		
+		return ChannelIDType.Master;
+		
+	}
+
+	/**
+	 * There is only ever one mix per channel, and one channel per mix
+	 *  (i.e. 1 to 1 relationship).
+	 * 
+	 * @author Brook Novak
+	 *
+	 */
+	private class Channel {
+		
+		ChannelIDType type; // "immutable" never null.
+
+		/** "immutable" - never null */
+		TrackMixSubject mix;
+	
+		/** Meaningless for channels that are a pure local mix or master */
+		boolean isUsingLocalMix;
+		
+		/** Apart from the systems master mix - this is a pre-mix to aggregate 
+		 * with this channels actual/indirect mix. For example a channel that is
+		 * linked might need to mix with its top-level link mix... 
+		 * Man that is a bad explanation! 
+		 * 
+		 * Only meaningful for indirect local channels - otherwise is just the same
+		 * as the TrackMixSubject channel ID.
+		 **/
+		String masterMixID; // never null, itself can be amastermix
+		
+		TrackSequence tseq; // resets to null to avoid holding expensive state ref (track model)
+		
+		// Used for pausing / resuming
+		int stoppedFramePosition = -1;
+		
+		// A mark used outside of this package
+		boolean isPaused = false;
+		
+		/**
+		 * Consturctor.
+		 * 
+		 * @param mixSub
+		 * @param isUsingLocalMix
+		 * 
+		 * @throws IllegalArgumentException
+		 * 		If mixSub's channel ID is invlaid.
+		 */
+		Channel(TrackMixSubject mixSub, boolean isUsingLocalMix) {
+			assert(mixSub != null);
+			mix = mixSub;
+			tseq = null;
+			this.isUsingLocalMix = isUsingLocalMix;
+			this.type = getChannelIDType(mixSub.getChannelID());
+
+			if (type == null)
+				throw new IllegalArgumentException("mixSub contains bad channel ID");
+			
+			masterMixID = extractMasterChannelID(mixSub.getChannelID());
+				
+			assert(integrity());
+		}
+		
+		private boolean integrity() {
+			
+			assert(mix != null);
+			assert(type != null);
+			assert(masterMixID != null);
+			
+			if(type == ChannelIDType.Master) {
+				assert(masterMixID.equals(mix.getChannelID())); 
+				//assert(!isUsingLocalMix);
+				assert(tseq == null);
+			} else if(type == ChannelIDType.PureLocal) {
+				assert(masterMixID.equals(mix.getChannelID())); 
+			} else if(type == ChannelIDType.IndirectLocal) {
+				assert(!masterMixID.equals(mix.getChannelID())); // must have two parts at least
+			}
+
+			return true;
+		}
+	}
+	
+	/**
+	 * Channel IDs have different classifications.
+	 * 
+	 * @author Brook Novak
+	 *
+	 */
+	public enum ChannelIDType {
+		
+		/**
+		 * Pure local channels have mixes that can be used by {@link ChannelIDType#IndirectLocal}
+		 * channels.
+		 */
+		PureLocal, // never links to local mixes .. since is local.
+		
+		/**
+		 * Indirect local channels are channels that have their own mix, but can also use a
+		 * pure local mix instead. Furthermore, they always pre-mix with a
+		 * {@link ChannelIDType#Master} channel.
+		 */
+		IndirectLocal, // Combined of virtual/frame ids and local ids...
+		
+		/**
+		 * Master channels are essentially channels containing mix information. They
+		 * can be linked by many {@link ChannelIDType#IndirectLocal} channels - where the
+		 * linked channels combine their curent mix with it's master mix.
+		 * 
+		 * This is seperate from the systems master mix which is awlays applied in the 
+		 * {@link ApolloPlaybackMixer} for every track.
+		 * 
+		 */
+		Master, // i.e. a channel that is purely used for mixing with child channels
+	}
+
+}
Index: /trunk/src_apollo/org/apollo/audio/util/Timeline.java
===================================================================
--- /trunk/src_apollo/org/apollo/audio/util/Timeline.java	(revision 315)
+++ /trunk/src_apollo/org/apollo/audio/util/Timeline.java	(revision 315)
@@ -0,0 +1,130 @@
+package org.apollo.audio.util;
+
+/**
+ * 
+ * A Timeline for a Frame contains two componants:
+ * 
+ * <ul>
+ * 		<li>The initation point: This is the smallest initiation time 
+ * 			<b>(not neccessarily zero)</b> in the frame - at its X pixel.
+ * 		<li>The end point: This is how long the frame runs for - and the
+ * 			x-pixel position of the last played track in the frame.
+ * </ul>
+ * 
+ * They are simple imutable objects thus are not kept consistant with model.
+ * 
+ * @author Brook Novak
+ *
+ */
+public class Timeline {
+	
+	private long firstInitiationTime;
+	private long runningTime;
+	private int initiationXPixel;
+	private int pixelWidth;
+	
+	/**
+	 * 
+	 * @param firstInitiationTime
+	 * 
+	 * @param runningTime
+	 * 		Must be larger than zero.
+	 * 
+	 * @param initiationXPixel
+	 * 		Relative can be anything
+	 * 
+	 * @param pixelWidth
+	 * 		Must be larger than zero
+	 * 
+	 */
+	public Timeline(long firstInitiationTime, long runningTime, int initiationXPixel, int pixelWidth) {
+		assert(runningTime > 0);
+		assert(pixelWidth > 0);
+		
+		this.firstInitiationTime = firstInitiationTime;
+		this.runningTime = runningTime;
+		this.initiationXPixel = initiationXPixel;
+		this.pixelWidth = pixelWidth;
+	}
+
+
+	public long getFirstInitiationTime() {
+		return firstInitiationTime;
+	}
+
+
+	public int getInitiationXPixel() {
+		return initiationXPixel;
+	}
+
+
+	public long getRunningTime() {
+		return runningTime;
+	}
+	
+	public int getPixelWidth() {
+		return pixelWidth;
+	}
+	
+	/**
+	 * @return
+	 * 		The amount of time in milliseconds per pixel.
+	 */
+	public double getTimePerPixel() {
+		double msPerPixel = runningTime;
+		return msPerPixel /= (double)pixelWidth;
+	}
+
+	/**
+	 * @return
+	 * 		The amount of time in milliseconds per pixel.
+	 */
+	public double getPixelPerTime() {
+		double pxPerMS = (double)pixelWidth;
+		return pxPerMS /= runningTime;
+	}
+	
+	/**
+	 * Calcautes the a time in milliseconds from a given x-pixel position.
+	 * 
+	 * @param x
+	 * 		The X pixel position.
+	 * 
+	 * @return
+	 * 		An extrapolated time value. <B>CAN BE NEGATIVE</B> - if the x position is to the left
+	 * 		of the initiation x pixel and happens to be far enough from the pixel such 
+	 * 		that the ms-diff is larget than the initiation time.
+	 */
+	public long getMSTimeAtX(int x) {
+		double msPerPixel = getTimePerPixel();
+		return firstInitiationTime + (long)(msPerPixel * (x - initiationXPixel));
+	}
+	
+	/**
+	 * Calculates the x-pixel position in a timeline from a given ms time.
+	 * 
+	 * @param time
+	 * 		The time in ms.
+	 * 
+	 * @return
+	 * 		The x pixel.
+	 */
+	public int getXAtMSTime(long time) {
+		
+		long diffTime = time - firstInitiationTime;
+		
+		return initiationXPixel + (int)(diffTime / getTimePerPixel());
+	}
+
+
+	@Override
+	public String toString() {
+		return 	"firstInitiationTime=" + firstInitiationTime
+				+ ", runningTime=" + runningTime
+				+ ", initiationXPixel=" + initiationXPixel
+				+ ", pixelWidth=" + pixelWidth;
+	}
+	
+	
+
+}
Index: /trunk/src_apollo/org/apollo/audio/util/TrackMixSubject.java
===================================================================
--- /trunk/src_apollo/org/apollo/audio/util/TrackMixSubject.java	(revision 315)
+++ /trunk/src_apollo/org/apollo/audio/util/TrackMixSubject.java	(revision 315)
@@ -0,0 +1,177 @@
+package org.apollo.audio.util;
+
+import org.apollo.audio.ApolloSubjectChangedEvent;
+import org.apollo.mvc.AbstractSubject;
+import org.apollo.mvc.SubjectChangedEvent;
+
+/**
+ * A TrackMixSubject is basic in Apollo: where a track mix consists
+ * of a volume and a mute flag.
+ * 
+ * Mixes have immutable IDs (represented as strings) to identify them...
+ * 
+ * @author Brook Novak
+ *
+ */
+public class TrackMixSubject extends AbstractSubject {
+	
+	private String channelID;
+	
+	private float volume;
+	
+	private boolean isMuted;
+
+	/**
+	 * Constructor.
+	 * 
+	 * @param id
+	 * 		Must not be null / empty.
+	 * 
+	 * @param volume
+	 * 		The initial volume.
+	 * 
+	 * @param isMuted
+	 * 		The initial mute flag.
+	 * 
+	 * @throws NullPointerException
+	 * 		If id is null.
+	 * 
+	 * @throws IllegalArgumentException
+	 * 		If id is empty.
+	 */
+	TrackMixSubject(String id, float volume, boolean isMuted) {
+		if (id == null) throw new NullPointerException("id");
+		if (id.length() == 0) throw new IllegalArgumentException("id is empty");
+		this.channelID = id;
+		this.volume = volume;
+		this.isMuted = isMuted;
+	}
+
+	/**
+	 * @return True if this clip is muted. false otherwise.
+	 */
+	public boolean isMuted() {
+		return isMuted;
+	}
+
+	/**
+	 * Mutes / un-mutes this clip. Can be called even if not loaded.
+	 * 
+	 * Raises an ApolloSubjectChangedEvent.MUTE event.
+	 * Null is given as the value if the clip does not support mute. Otherwise
+	 * true/false is given as the value of the mixed mute.
+	 * The event is only raised if the mute is different ot the current mute.
+	 * 
+	 * @param isMuted True to mute. False to unmute.
+	 * 
+	 */
+	public void setMuted(boolean isMuted) {
+		
+		if (this.isMuted == isMuted) return;
+		this.isMuted = isMuted;
+
+		fireSubjectChanged(new SubjectChangedEvent(
+				ApolloSubjectChangedEvent.MUTE));
+	}
+
+
+	/**
+	 * @return The volume for this clip.
+	 */
+	public float getVolume() {
+		return volume;
+	}
+	
+	/**
+	 * Sets the volume for this clip. Can be called even if not loaded.
+	 * 
+	 * raises a {@link ApolloSubjectChangedEvent.VOLUME} event.
+	 * 
+	 * Null is given as the value if the clip does not support master gain.
+	 * The event is only raised if the volume is different ot the current volume.
+	 * 
+	 * Note that this will not actually set the volume of any track sequences, to update
+	 * the volume you must subsequently call {@link #updateVolume(Object)}. This is to
+	 * avoid setting volumes over all track sequences.
+	 * 
+	 * @param vol 
+	 * 		The volume to set. Ranges from 0-1. Clamped.
+	 * 
+	 */
+	public void setVolume(float vol) {
+
+		// Clamp volume argument
+		if (vol < 0.0f)
+			vol = 0.0f;
+		else if (vol > 1.0f)
+			vol = 1.0f;
+
+		if (vol == this.volume)
+			return;
+		
+		volume = vol;
+
+		fireSubjectChanged(new SubjectChangedEvent(
+				ApolloSubjectChangedEvent.VOLUME));
+	}
+	
+	/**
+	 * The unique ID associated with this track.
+	 * 
+	 * @return
+	 * 		The immutable ID. Never null.
+	 */
+	public String getChannelID() {
+		return channelID;
+	}
+
+	/**
+	 * A {@link #TrackMixSubject} is equal to another if they have the same ID.
+	 */
+	@Override
+	public boolean equals(Object obj) {
+		return channelID.equals(obj);
+	}
+
+	@Override
+	public int hashCode() {
+		return channelID.hashCode();
+	}
+	
+	private static final char SEPERATOR = 7423; // something obscure
+
+	public String toParseableString() {
+		return toParseableString(channelID, volume, isMuted);
+	}
+
+	static String toParseableString(String id, float volume, boolean isMuted) {
+		return id + SEPERATOR + volume + SEPERATOR + ((isMuted) ? "1" : "0");
+	}
+
+	public static TrackMixSubject fromString(String st) {
+		
+		if (st == null) return null;
+		
+		String[] parts = st.split(Character.toString(SEPERATOR));
+		
+		if (parts.length != 3) return null;
+		
+		if (parts[0].length() == 0) return null;
+		
+		float vol;
+		boolean isMuted;
+		
+		try {
+			vol = Float.parseFloat(parts[1]);
+		} catch (NumberFormatException e) {
+			return null;
+		}
+		
+		isMuted = parts[2].length() > 0 && parts[2].charAt(0) == '1';
+		
+		return new TrackMixSubject(parts[0], vol, isMuted);
+		
+	}
+
+
+}
Index: /trunk/src_apollo/org/apollo/gui/ApolloColorIndexedModels.java
===================================================================
--- /trunk/src_apollo/org/apollo/gui/ApolloColorIndexedModels.java	(revision 315)
+++ /trunk/src_apollo/org/apollo/gui/ApolloColorIndexedModels.java	(revision 315)
@@ -0,0 +1,76 @@
+package org.apollo.gui;
+
+import java.awt.Color;
+import java.awt.image.IndexColorModel;
+
+public class ApolloColorIndexedModels {
+	
+	public static final Color KEY_COLOR = new Color(123,58,123,0);
+	private static final int KEY_INDEX = 0; 
+	
+	/** Color index model for the track graphs */
+	public static final IndexColorModel graphIndexColorModel;
+
+	
+	public static final Color WAVEFORM_COLOR = new Color(0x1C5BA3);
+	public static final Color WAVEFORM_SELECTION_COLOR = Color.YELLOW;
+	
+	private static final int WAVEFORM_INDEX = 1;
+	private static final int WAVEFORM_SELECTION_INDEX = 2;
+	
+	
+	public static final IndexColorModel linkedTrackIndexColorModel;
+	
+	public static final Color TRACK1_COLOR = new Color(0x1C5BA3);
+	public static final Color TRACK2_COLOR = new Color(0x3280B7);
+	
+	public static final int TRACK1_INDEX = 1;
+	public static final int TRACK2_INDEX = 2;
+
+	static { // Prepare the global color index model for the graphs
+
+		// For waveform graphs:
+		byte[] reds = new byte[256];
+		byte[] greens = new byte[256];
+		byte[] blues = new byte[256];
+		
+		reds[KEY_INDEX] = (byte)KEY_COLOR.getRed();
+		greens[KEY_INDEX] = (byte)KEY_COLOR.getGreen();
+		blues[KEY_INDEX] = (byte)KEY_COLOR.getBlue();
+		
+		reds[WAVEFORM_INDEX] = (byte)WAVEFORM_COLOR.getRed();
+		greens[WAVEFORM_INDEX] = (byte)WAVEFORM_COLOR.getGreen();
+		blues[WAVEFORM_INDEX] = (byte)WAVEFORM_COLOR.getBlue();
+		
+		reds[WAVEFORM_SELECTION_INDEX] = (byte)WAVEFORM_SELECTION_COLOR.getRed();
+		greens[WAVEFORM_SELECTION_INDEX] = (byte)WAVEFORM_SELECTION_COLOR.getGreen();
+		blues[WAVEFORM_SELECTION_INDEX] = (byte)WAVEFORM_SELECTION_COLOR.getBlue();
+		
+		graphIndexColorModel = new IndexColorModel(8, 256, reds, greens, blues, KEY_INDEX);
+		
+		
+		// For track heirarchy graphs:
+		reds = new byte[256];
+		greens = new byte[256];
+		blues = new byte[256];
+		
+		reds[KEY_INDEX] = (byte)KEY_COLOR.getRed();
+		greens[KEY_INDEX] = (byte)KEY_COLOR.getGreen();
+		blues[KEY_INDEX] = (byte)KEY_COLOR.getBlue();
+		
+		reds[TRACK1_INDEX] = (byte)TRACK1_COLOR.getRed();
+		greens[TRACK1_INDEX] = (byte)TRACK1_COLOR.getGreen();
+		blues[TRACK1_INDEX] = (byte)TRACK1_COLOR.getBlue();
+		
+		reds[TRACK2_INDEX] = (byte)TRACK2_COLOR.getRed();
+		greens[TRACK2_INDEX] = (byte)TRACK2_COLOR.getGreen();
+		blues[TRACK2_INDEX] = (byte)TRACK2_COLOR.getBlue();
+		
+		linkedTrackIndexColorModel = new IndexColorModel(8, 256, reds, greens, blues, KEY_INDEX);
+		
+	}
+	
+	
+	
+	
+}
Index: /trunk/src_apollo/org/apollo/gui/EditableSampledTrackGraphView.java
===================================================================
--- /trunk/src_apollo/org/apollo/gui/EditableSampledTrackGraphView.java	(revision 315)
+++ /trunk/src_apollo/org/apollo/gui/EditableSampledTrackGraphView.java	(revision 315)
@@ -0,0 +1,507 @@
+package org.apollo.gui;
+
+import java.awt.Color;
+import java.awt.Graphics;
+import java.awt.Graphics2D;
+import java.awt.Point;
+import java.awt.Rectangle;
+import java.awt.event.KeyEvent;
+import java.awt.event.KeyListener;
+import java.awt.event.MouseEvent;
+import java.awt.event.MouseListener;
+import java.awt.event.MouseMotionListener;
+import java.io.IOException;
+
+import javax.sound.sampled.AudioFormat;
+import javax.swing.SwingUtilities;
+
+import org.apollo.audio.ApolloSubjectChangedEvent;
+import org.apollo.audio.SampledTrackModel;
+import org.apollo.io.IconRepository;
+import org.apollo.mvc.Subject;
+import org.apollo.mvc.SubjectChangedEvent;
+import org.apollo.util.TrackNameCreator;
+import org.apollo.widgets.SampledTrackEvo4;
+import org.expeditee.gui.Browser;
+import org.expeditee.gui.DisplayIO;
+import org.expeditee.gui.Frame;
+import org.expeditee.gui.FrameGraphics;
+import org.expeditee.gui.FrameMouseActions;
+import org.expeditee.gui.FreeItems;
+import org.expeditee.items.Item;
+import org.expeditee.items.widgets.WidgetCorner;
+
+/**
+ * 
+ * EditableSampledTrackGraphView's provide selection, removing, copying and inserting audio.
+ * 
+ * @author Brook Novak
+ *
+ */
+public class EditableSampledTrackGraphView 
+	extends SampledTrackGraphView
+	implements MouseListener, MouseMotionListener, KeyListener {
+	
+	private static final long serialVersionUID = 1L;
+
+	private int selectionMode = SELECTION_MODE_SELECT_ONLY;
+	private int selectionStartX = 0;
+	
+	private SelectionBackSection selectionBackSection;
+
+	private boolean selectAllOnDoubleClick = false;
+
+	public static final Color SELECTION_BACKING_COLOR_SELECT_ONLY = Color.BLACK;
+	private static final Color SELECTION_BACKING_COLOR_SELECT_AND_COPY = Color.GREEN;
+
+	private static final int SELECTION_MODE_SELECT_ONLY = 1;
+	private static final int SELECTION_MODE_SELECT_AND_COPY = 2;
+
+	private static final int COPY_SELECTION_PIXEL_RANGE_THRESHOLD = 8;
+	private static final int COARSE_PAN_PIXEL_LENGTH = 10;
+	private static final int MIN_FRAME_SELECTION_SIZE = 100;
+	
+	public static final int LOCK_ICON_CORNER_OFFSET = 20;
+	
+	public EditableSampledTrackGraphView() {
+
+		// Create selection back section
+		selectionBackSection = new SelectionBackSection();
+		addBackingSection(selectionBackSection);
+		
+		reAddListeners();
+
+		setSelectionMode(SELECTION_MODE_SELECT_ONLY);
+
+	}
+	
+	public void reAddListeners() {
+		removeMouseListener(this);
+		removeMouseMotionListener(this);
+		removeKeyListener(this);
+		
+		addMouseListener(this);
+		addMouseMotionListener(this);
+		addKeyListener(this);
+	}
+	
+	/**
+	 * Use this for setting selectionMode. Note is does not invlaidate.
+	 * 
+	 * @param mode
+	 * 		The new mode to set.
+	 */
+	private void setSelectionMode(int mode) {
+		selectionMode = mode;
+		if (selectionMode == SELECTION_MODE_SELECT_ONLY) {
+			
+		} else if (selectionMode == SELECTION_MODE_SELECT_AND_COPY) {
+			
+		} else {
+			assert(false);
+		}
+	} 
+
+
+	public void keyPressed(KeyEvent e) {
+	}
+	
+	public void selectAllInView() {
+		if (getSampledTrackModel() == null) return;
+		
+		getSampledTrackModel().setSelection(
+				getTimeScaleStart(), 
+				getTimeScaleLength());
+	}
+
+	public void keyReleased(KeyEvent e) {
+		if (getSampledTrackModel() == null) return;
+		if (e.isConsumed()) return;
+		
+		switch (e.getKeyCode()) {
+		case KeyEvent.VK_DELETE:
+			
+			if (getSampledTrackModel().getSelectionLength() > 1 
+					&& !isPlaying()
+					&& (getSampledTrackModel().getFrameCount() - getSampledTrackModel().getSelectionLength())  > MIN_FRAME_SELECTION_SIZE) {
+
+				getSampledTrackModel().removeSelectedBytes();
+				
+			}
+			
+		case KeyEvent.VK_LEFT:
+		case KeyEvent.VK_RIGHT:
+
+			float ftmp = (float)COARSE_PAN_PIXEL_LENGTH / (float)getWidth();
+			int pan = (int)(getTimeScaleLength() *  ftmp);
+			if (pan == 0) pan = 1;
+			
+			if (e.getKeyCode() == KeyEvent.VK_RIGHT &&
+					(getSampledTrackModel().getSelectionStart() +
+					getSampledTrackModel().getSelectionLength()) < getSampledTrackModel().getFrameCount()) { // RIGHT
+				
+				getSampledTrackModel().setSelection(getSampledTrackModel().getSelectionStart() + pan, 
+						getSampledTrackModel().getSelectionLength());
+				
+			} else if(e.getKeyCode() == KeyEvent.VK_LEFT &&
+					getSampledTrackModel().getSelectionStart() > 0) { // LEFT
+				getSampledTrackModel().setSelection(getSampledTrackModel().getSelectionStart() - pan, 
+						getSampledTrackModel().getSelectionLength());
+			}
+			break;
+		}
+	
+		
+	}
+
+
+	public void keyTyped(KeyEvent e) {
+	}
+
+
+	public void mouseClicked(MouseEvent e) {
+		if (e.isConsumed()) return;
+		
+		if (selectAllOnDoubleClick && getSampledTrackModel() != null && e.getClickCount() >= 2) 
+		{
+			selectAllInView();
+			
+			if (e.getButton() == MouseEvent.BUTTON3) { // copy
+				copySelection(e);
+			}
+			
+		} else if (!isPlaying() &&
+				((e.getButton() == MouseEvent.BUTTON2 // Insert
+				|| e.getButton() == MouseEvent.BUTTON3) && !FreeItems.getInstance().isEmpty())) {
+			
+			SampledTrackEvo4 floatingTrack = null;
+			
+			// Look for a floating track for injection of audio bytes
+			for (Item i : FreeItems.getInstance()) {
+				if (i instanceof WidgetCorner) {
+					if (((WidgetCorner)i).getWidgetSource() instanceof SampledTrackEvo4) {
+						floatingTrack = (SampledTrackEvo4)((WidgetCorner)i).getWidgetSource();
+						break;
+					}
+				}
+			}
+			
+			// Perform an injection
+			if (floatingTrack != null) {
+				try {
+					floatingTrack.injectAudio(this, e.getX(), e.getButton() == MouseEvent.BUTTON2);
+				} catch (IOException ex) {
+					ex.printStackTrace();
+				}
+			}
+			
+			FrameGraphics.refresh(true);
+			
+		}
+	}
+
+
+	public void mouseEntered(MouseEvent e) {
+	}
+
+
+	public void mouseExited(MouseEvent e) {
+	}
+
+
+	public void mousePressed(MouseEvent e) {
+		if (getSampledTrackModel() == null) return;
+		if (e.isConsumed()) return;
+		
+		selectionStartX = e.getX();
+		
+		// set selection mode
+		if (e.getButton() == MouseEvent.BUTTON3) 
+			setSelectionMode(SELECTION_MODE_SELECT_AND_COPY);
+		else setSelectionMode(SELECTION_MODE_SELECT_ONLY);
+		
+		// Set selection start, and length as a single frame
+		getSampledTrackModel().setSelection(frameAtX(e.getX()), 1);
+	}
+
+
+	public void mouseReleased(MouseEvent e) {
+		if (getSampledTrackModel() == null) return;
+		if (e.isConsumed()) return;
+
+		// If the mouse was released and the current selection mode is for copying frames...
+		if (selectionMode == SELECTION_MODE_SELECT_AND_COPY) {
+			
+			// Get selection range in pixels to check if selection range is outside threshold...
+			int selectionStartXPixel = XatFrame(getSampledTrackModel().getSelectionStart());
+			int selectionEndXPixel = (getSampledTrackModel().getSelectionLength() > 1) ?
+					XatFrame(getSampledTrackModel().getSelectionStart() + getSampledTrackModel().getSelectionLength()) 
+					: -1;
+			
+			// Only copy if valid range selected
+			int selLength = (selectionEndXPixel > selectionStartXPixel) ? 
+					selectionEndXPixel - selectionStartXPixel : -1;
+			
+			// Only copy if outside threshold - in pixels of course
+			if (selLength > COPY_SELECTION_PIXEL_RANGE_THRESHOLD) {
+				
+				copySelection(e);
+				
+			}
+			
+			// Reset selection mode since copy HAVE FINISHED
+			setSelectionMode(SELECTION_MODE_SELECT_ONLY);
+			
+			// Update the new selection color
+			invalidateSelection();
+		}
+	}
+
+	/**
+	 * Copies the selection into free space as a {@link SampledTrackEvo4}
+	 * 
+	 * @param e
+	 */
+	private void copySelection(MouseEvent e) {
+		
+		if (FreeItems.getInstance().isEmpty()) {
+			// Get copy of selected bytes
+			byte[] copiedAudioRegion = getSampledTrackModel().getSelectedFramesCopy();
+			assert(copiedAudioRegion != null);
+			
+			Frame targetFrame = DisplayIO.getCurrentFrame();
+			if (targetFrame != null) {
+				
+				SampledTrackEvo4 twidget = SampledTrackEvo4.createFromMemory(
+						copiedAudioRegion, 
+						getSampledTrackModel().getFormat(),
+						targetFrame, 
+						0, 
+						0,
+						-1,
+						TrackNameCreator.getDefaultName(),
+						getMix());
+				
+					/*	getSampledTrackModel().getName() != null ?
+								TrackNameCreator.getNameCopy(getSampledTrackModel().getName() + "_section")
+								: null);*/
+				
+				assert(Browser._theBrowser != null);
+				
+				// A workround for getting the widget into fre space centered on cursor
+				Point p = e.getLocationOnScreen();
+				SwingUtilities.convertPointFromScreen(p, Browser._theBrowser.getContentPane());
+				twidget.setPosition(p.x - (twidget.getWidth() / 2), p.y - (twidget.getHeight() / 2));
+
+				for (Item i : twidget.getItems()) {
+					i.setOffset(new Point(
+							(i.getX() - DisplayIO.getMouseX()) + (twidget.getWidth() / 2), 
+							(i.getY() - FrameMouseActions.getY()) + (twidget.getHeight() / 2)
+						));
+				}
+				
+				// Put the new widget into free space
+				FrameMouseActions.pickup(twidget.getItems());
+
+			}
+		
+		}
+	}
+
+
+	public void mouseDragged(MouseEvent e) {
+		if (getSampledTrackModel() == null) return;
+		if (e.isConsumed()) return;
+		if (selectionStartX < 0) return;
+		
+		// Set selection range
+		int frameAtCursor = frameAtX(e.getX());
+		int frameAtStartPoint = frameAtX(selectionStartX);
+		
+		int start = Math.min(frameAtCursor, frameAtStartPoint);
+		int length = Math.max(frameAtCursor, frameAtStartPoint) - start;
+		if (length > 0) {
+			getSampledTrackModel().setSelection(start, length);
+		}
+	}
+
+
+	public void mouseMoved(MouseEvent e) {
+		
+		// Ensure selection is not out of synch - could have missed some mouse events.
+		if (e.getButton() == MouseEvent.NOBUTTON
+				&& selectionMode != SELECTION_MODE_SELECT_ONLY) {
+			setSelectionMode(SELECTION_MODE_SELECT_ONLY);
+			invalidateSelection();
+		}
+			
+	}
+
+	
+	@Override
+	public void modelChanged(Subject source, SubjectChangedEvent event) {
+		super.modelChanged(source, event);
+		
+		if (getSampledTrackModel() == null) return;
+		
+		switch(event.getID()) {
+
+		// Whenever the selection changes, update the selection backing & invalidate
+			case ApolloSubjectChangedEvent.SELECTION_CHANGED:
+				
+				selectionBackSection.left = XatFrame(getSampledTrackModel().getSelectionStart());
+				
+				selectionBackSection.width = (getSampledTrackModel().getSelectionLength() <= 1) ?
+					-1 :
+					XatFrame(getSampledTrackModel().getSelectionStart() + getSampledTrackModel().getSelectionLength())
+					- selectionBackSection.left;
+
+				if (selectionBackSection.visible) {
+
+					invalidateSelection();
+				}
+				
+				break;
+				
+		}
+
+	
+	}
+
+	/**
+	 * If the selection region drawn differs to the current selection region,
+	 * then the dirty regions will be invalidated
+	 *
+	 */
+	public void invalidateSelection() { 
+
+		invalidateAll(); // TODO: FINE GRAINED VERSION - MORE EFFICIENT
+	}
+
+	
+	@Override
+	public void paint(Graphics g) {
+		super.paint(g);
+
+		// Draw selection start bar
+		if (getSampledTrackModel().getSelectionStart() >= getTimeScaleStart()) {
+
+			((Graphics2D)g).setStroke(GRAPH_BAR_STROKE);
+			
+			// Note that if the start line is near the edges of the panel it can be concealed - thus
+			// set a thick line on the edges
+			int x = selectionBackSection.left;
+			if (x == 0) {
+				x = 1;
+			} else  if (x == getWidth()){
+				x = getWidth() - 1;
+			}
+			
+			g.setColor(Color.RED);
+			g.drawLine(
+					x, 
+					0, 
+					x,
+					getHeight());
+			
+		}
+		
+		paintLock(g);
+
+		
+	}
+	
+	private void paintLock(Graphics g) {
+		if (isPlaying()) {
+			IconRepository.getIcon("lock.png").paintIcon(null,
+					g, 
+					getWidth() - LOCK_ICON_CORNER_OFFSET, 
+					LOCK_ICON_CORNER_OFFSET - 16);
+		}
+	}
+	
+	private void invalidateLockIcon() {
+		Point exp = getExpediteePoint();
+		if (exp != null) {
+			FrameGraphics.invalidateArea(new Rectangle(
+					exp.x + getWidth() - LOCK_ICON_CORNER_OFFSET, 
+					exp.y + LOCK_ICON_CORNER_OFFSET  - 16, 
+					16, 16));
+		}
+		
+	}
+	
+	@Override
+	protected void onPlaybackStarted() {
+		super.onPlaybackStarted();
+		invalidateLockIcon();
+	}
+
+	@Override
+	protected void onPlaybackStopped() {
+		super.onPlaybackStopped();
+		invalidateLockIcon();
+	}
+
+	@Override
+	protected void fireTimelineChanged() {
+		super.fireTimelineChanged();
+		// Keep selection consistent when timeline changes
+		selectionBackSection.updateBounds();
+	}
+	
+	/**
+	 * Injects bytes into the observed {@link SampledTrackModel} - if one is set and is not playing.
+	 * Also resets selection - to select all on the new bytes
+	 * 
+	 * @see SampledTrackModel#insertBytes(byte[], javax.sound.sampled.AudioFormat, int)
+	 */
+	public void insertAudio(byte[] bytesToAdd, AudioFormat format, int framePosition) 
+		throws IOException {
+		if (getSampledTrackModel() != null && !isPlaying()) {
+			
+			// Perform insert
+			getSampledTrackModel().insertBytes(bytesToAdd, format, framePosition);
+			
+			// Set new selection
+			getSampledTrackModel().setSelection(framePosition, bytesToAdd.length / format.getFrameSize());
+		}
+		
+		
+	}
+	
+	public void setSelectAllOnDoubleClick(boolean selectAllOnDoubleClick) {
+		this.selectAllOnDoubleClick = selectAllOnDoubleClick;
+	}
+
+	private class SelectionBackSection extends BackSection
+	{
+
+		public SelectionBackSection() {
+			super();
+			visible = true;
+		}
+		
+		@Override
+		void paint(Graphics g) {
+			
+			color =  (selectionMode == SELECTION_MODE_SELECT_ONLY) ?
+					SELECTION_BACKING_COLOR_SELECT_ONLY : SELECTION_BACKING_COLOR_SELECT_AND_COPY;
+			super.paint(g);
+		}
+		
+		@Override
+		void updateBounds() {
+			
+			assert(getSampledTrackModel() != null);
+		
+			left = XatFrame(getSampledTrackModel().getSelectionStart());
+			
+			width = (getSampledTrackModel().getSelectionLength() <= 1) ?
+				-1 :
+				XatFrame(getSampledTrackModel().getSelectionStart() + getSampledTrackModel().getSelectionLength())
+				- left;
+		}
+	}
+
+}
Index: /trunk/src_apollo/org/apollo/gui/ExpandedTrackManager.java
===================================================================
--- /trunk/src_apollo/org/apollo/gui/ExpandedTrackManager.java	(revision 315)
+++ /trunk/src_apollo/org/apollo/gui/ExpandedTrackManager.java	(revision 315)
@@ -0,0 +1,643 @@
+package org.apollo.gui;
+
+import java.awt.Dimension;
+import java.awt.GridBagConstraints;
+import java.awt.GridBagLayout;
+import java.awt.Rectangle;
+import java.awt.event.ActionEvent;
+import java.awt.event.ActionListener;
+import java.awt.event.ComponentEvent;
+import java.awt.event.ComponentListener;
+import java.util.LinkedList;
+
+import javax.swing.JButton;
+import javax.swing.JLabel;
+import javax.swing.SwingUtilities;
+
+import org.apollo.audio.SampledTrackModel;
+import org.apollo.audio.util.TrackMixSubject;
+import org.apollo.io.IconRepository;
+import org.apollo.mvc.AbstractSubject;
+import org.apollo.mvc.SubjectChangedEvent;
+import org.apollo.util.TrackModelHandler;
+import org.apollo.util.TrackModelLoadManager;
+import org.expeditee.gui.Browser;
+import org.expeditee.gui.FrameGraphics;
+import org.expeditee.gui.MessageBay;
+import org.expeditee.gui.Popup;
+import org.expeditee.gui.PopupManager;
+
+/**
+ * All expanded tracks are contained here. Fires SubjectChangedEvent.EMPTY events whenever
+ * expansion selection changes.
+ * 
+ * @author Brook Novak
+ *
+ */
+public class ExpandedTrackManager extends AbstractSubject implements TrackModelHandler {
+	
+	private static final int MAX_EXPANDED_TRACK_SIZE = 4;
+	
+	/** All epxanded tracks ... either in all in view or all selected ... to be viewed */
+	private LinkedList<ExpandedTrackPopup> expandedTracks = new LinkedList<ExpandedTrackPopup> (); // SHARED Model data
+
+	private MultiTrackExpansionPopup trackSelectionPopup = new MultiTrackExpansionPopup();
+	
+	private static ExpandedTrackManager instance = new ExpandedTrackManager();
+	private ExpandedTrackManager() {
+		
+		// Keep view centered to browser size
+		SwingUtilities.invokeLater(new Runnable() {
+			public void run() {
+
+				// Keep expanded view centered to the browser window
+				if (Browser._theBrowser != null) {
+					Browser._theBrowser.addComponentListener(new ComponentListener() {
+
+						public void componentHidden(ComponentEvent e) {
+						}
+
+						public void componentMoved(ComponentEvent e) {
+						}
+
+						public void componentResized(ComponentEvent e) {
+							
+							if (PopupManager.getInstance().isShowing(trackSelectionPopup)) {
+								// User currently selecting..
+								trackSelectionPopup.setLocation(
+										(Browser._theBrowser.getWidth() / 2) - (trackSelectionPopup.getWidth() / 2), 0);
+								
+							} else { // expanded views might be showing
+								
+								// Fails with Maximizing of window on some systems... swing
+								// bug!! really anoying
+								updateLayout();
+	
+							}
+	
+						}
+
+						public void componentShown(ComponentEvent e) {
+						}
+						
+					});
+				}
+				
+			}
+		});
+		
+		// Registewr for (indirectly) handling track models
+		TrackModelLoadManager.getInstance().addTrackModelHandler(this);
+
+	}
+	
+	/**
+	 * 
+	 * @return The singleton instance.
+	 */
+	public static ExpandedTrackManager getInstance() {
+		return instance;
+	}
+	
+	private void fireTrackSelectionChanged() {
+		trackSelectionPopup.onTrackSelectionChanged();
+		fireSubjectChanged(SubjectChangedEvent.EMPTY);
+	}
+	
+
+	/**
+	 * Giver/receiver design pattern for only allowing this singleton
+	 * to creat expanded views.
+	 * 
+	 * @param requested
+	 */
+	void receiveExpandedTrackPopup(ExpandedTrackPopup requested) {
+		synchronized(expandedTracks) {
+			assert (expandedTracks.size() < MAX_EXPANDED_TRACK_SIZE);
+			expandedTracks.add(requested);
+		}
+		// Notify observers
+		fireTrackSelectionChanged();
+	}
+	
+	/**
+	 * Whenever a ExpandedTrackPopup hides, it invokes this.
+	 * @param xtp
+	 */
+	void expandedTrackPopupHidden(ExpandedTrackPopup xtp) {
+		assert(xtp != null);
+		synchronized(expandedTracks) {
+			expandedTracks.remove(xtp);
+		}
+		
+		// User might have closed one but kept more visible
+		updateLayout();
+		
+		// Notify observers
+		fireTrackSelectionChanged();
+	}
+	
+	/**
+	 * Adds a SampledTrackModel ready for expanding.
+	 * 
+	 * @param track
+	 * 		The track to expand. Must not be null.
+	 * 
+	 * @param trackSourceFrameName
+	 * 		Where the expanded tracks track source was expanded from.
+	 * 		Must not be null
+	 * 
+	 * @param animationSource
+	 * 		If null no animation will be used.
+	 * 
+	 * @param mix
+	 * 		The mix that the expanded view should use / manipulate. Must not be null.
+	 * 
+	 * @return
+	 * 		True if SampledTrackModel was added.
+	 * 		False if not - because the selection amount is maxed out or
+	 * 		the track is already selected.
+	 * 
+	 * @throws NullPointerException
+	 * 		If trackModel or trackMix or trackSourceFrameName or localFilename is null.
+	 */
+	public boolean addTrackToSelection(
+			SampledTrackModel track, 
+			String trackSourceFrameName,
+			Rectangle animationSource,
+			TrackMixSubject mix) {
+		
+		assert(track != null);
+		assert(Browser._theBrowser != null);
+		assert(mix != null);
+		
+		synchronized(expandedTracks) {
+	
+			if (expandedTracks.size() < MAX_EXPANDED_TRACK_SIZE 
+					&& !isTrackInExpansionSelection(track)) {
+				
+				ExpandedTrackPopup.giveToExpandedTrackManager(
+						track, 
+						mix,
+						trackSourceFrameName,
+						0,
+						track.getFrameCount()); /** @see #receiveExpandedTrackPopup */
+	
+				Rectangle trackSelectionPopupBounds = null;
+				
+				// Ensure that the trackSelectionPopup is not already showing / animated to show
+				if (!PopupManager.getInstance().isShowing(trackSelectionPopup)) {
+					
+					trackSelectionPopupBounds = new Rectangle(
+							(Browser._theBrowser.getWidth() / 2 ) - (trackSelectionPopup.getWidth() / 2),
+							0, 
+							trackSelectionPopup.getWidth(), 
+							trackSelectionPopup.getHeight());
+					
+					// Creep from above
+					PopupManager.getInstance().showPopup(
+							trackSelectionPopup,
+							trackSelectionPopupBounds.getLocation(),
+							null,
+							PopupManager.getInstance().new ExpandShrinkAnimator(
+									trackSelectionPopupBounds,
+									null));
+					
+				} else {
+					trackSelectionPopup.repaint();
+				}
+				
+				// Animate a fake popup expanding into the trackSelectionPopup from the given source if not null
+				if (animationSource != null) {
+					
+					PopupManager.getInstance().doPureAnimation(PopupManager.getInstance().new ExpandShrinkAnimator(
+							animationSource, null), 
+							(trackSelectionPopupBounds == null) ?
+									trackSelectionPopup.getBounds() : trackSelectionPopupBounds);
+				}
+				
+				return true;
+			}
+		}
+		
+		return false;
+	}
+	
+	/**
+	 * Expands a given SampledTrackModel into view. Hides any other 
+	 * ExpandedTrackPopup currently in view.
+	 * 
+	 * If the given track is arleady in view. It will be hidden and reshown.
+	 * 
+	 * @param track
+	 * 		Must not be null.
+	 * 
+	 * @param mix
+	 * 		The mix that the expanded view should use / manipulate. Must not be null.
+	 * 
+	 * @param animationSource
+	 * 		Must not be null.
+	 * 
+	 * @param trackSourceFrameName
+	 * 		Where the expanded tracks track source was expanded from.
+	 * 		Must not be null
+	 * 
+	 * @param frameStart
+	 * 		the initial zoom - clamped
+	 * 
+	 * @param frameLength
+	 * 		the initial zoom - clamped
+	 * 
+	 * @throws NullPointerException
+	 * 		If trackModel or trackMix or trackSourceFrameName or localFilename is null.
+	 * 
+	 */
+	public void expandSingleTrack(
+			SampledTrackModel track, 
+			Rectangle animationSource, 
+			TrackMixSubject mix,
+			String trackSourceFrameName,
+			int frameStart,
+			int frameLength) {
+		assert(animationSource != null);
+		assert(track != null);
+
+		synchronized(expandedTracks) {
+			// Hide all expandedTracks that are currently showing
+			while (!expandedTracks.isEmpty())
+				PopupManager.getInstance().hidePopup(expandedTracks.getFirst());
+		}
+
+		// Create a expanded tracks view
+		ExpandedTrackPopup.giveToExpandedTrackManager(
+				track, 
+				mix,
+				trackSourceFrameName,
+				frameStart,
+				frameLength); /** @see #receiveExpandedTrackPopup */
+		
+		// Show the expanded track from - expanding from the given animation source
+		expandTracks(animationSource);
+	}
+	
+	/**
+	 * Removes a SampledTrackModel from being expanded...
+	 * @param track
+	 */
+	public void removeTrackFromSelection(SampledTrackModel track) {
+		
+		assert(track != null);
+		
+		synchronized(expandedTracks) {
+		
+			ExpandedTrackPopup expandedView = getExpandedTrackPopup(track);
+			
+			// Does exist?
+			if (expandedView == null) return;
+			
+			// Is already showing?
+			else if (PopupManager.getInstance().isShowing(expandedView)) return;
+			
+			// Remove from selection
+			expandedTracks.remove(expandedView);
+			
+			// Explicitly release memory since not called due to it never being in the PopupManager
+			expandedView.releaseMemory(); 
+			
+			// Notify observers
+			fireTrackSelectionChanged();
+			
+			// Hide the popup selection if nothing selected anymore
+			if (expandedTracks.isEmpty()) {
+				hideTrackSelectionPopup();
+			}
+		}
+	}
+	
+	private void cancelSelection() {
+		
+		hideTrackSelectionPopup();
+		
+		// Remove from selection
+		LinkedList<? extends ExpandedTrackPopup> old = (LinkedList<? extends ExpandedTrackPopup>)expandedTracks.clone();
+		
+		synchronized(expandedTracks) {
+			expandedTracks.clear();
+		}
+		
+		// Explicitly release memory since may have not of called due to it never being in the PopupManager
+		for (ExpandedTrackPopup xtp : old) 
+			xtp.releaseMemory(); 
+		
+		// Notify observers
+		fireTrackSelectionChanged();
+	}
+	
+	/**
+	 * @param track
+	 * 		The track to search for.
+	 * 
+	 * @return
+	 * 		A tracks ExpandedTrackPopup if one exists.
+	 */
+	private ExpandedTrackPopup getExpandedTrackPopup(SampledTrackModel track) {
+		assert(track != null);
+		
+		synchronized(expandedTracks) {
+			for (ExpandedTrackPopup xtp : expandedTracks) {
+				if (xtp.getObservedTrackModel() == track) return xtp;
+			}
+		}
+		
+		return null;
+	}
+	
+	
+	/**
+	 * 
+	 * @param track
+	 * 		Must not be null.
+	 * 
+	 * @return
+	 * 		True if the track is expanded - or is selected to be expanded..
+	 */
+	public boolean isTrackInExpansionSelection(SampledTrackModel track) {
+		assert(track != null);
+		return getExpandedTrackPopup(track) != null;
+	}
+	
+	/**
+	 * @return
+	 * 			True if at least 1 expanded track is showing - or is selected to show.
+	 */
+	public boolean doesExpandedTrackExist() {
+		synchronized(expandedTracks) {
+			return expandedTracks.size() > 0;
+		}
+	}
+	
+	public boolean isAnyExpandedTrackVisible() {
+		for (ExpandedTrackPopup xtp : expandedTracks) {
+			if (PopupManager.getInstance().isShowing(xtp)) return true;
+		}
+		
+		return false;
+	}
+	
+	/**
+	 * Shows all popups (if not already visible) to view and lays them out.
+	 *
+	 */
+	public void showSelection() {
+		expandTracks(null);
+	}
+	
+	/**
+	 * Returns immediatly if there are no tracks selected. Only expands if the popup is not already expanded.
+	 * 
+	 * @param directAnimSource
+	 * 		Set to null to animate expanded popups from the trackSelectionPopup.
+	 * 		Otherwise specify where to animate from. (Intention for latter case: a single expanded view from
+	 * 		a widget).
+	 */
+	private void expandTracks(Rectangle directAnimSource) {
+
+		synchronized(expandedTracks) {
+			
+			if (expandedTracks.isEmpty()) return;
+			
+			// Animate the popups from the trackSelectionPopup if there is not directAnimSource
+			if (directAnimSource == null) {
+				directAnimSource = trackSelectionPopup.getBounds();
+			}
+			
+			// Update the expanded view layouts
+			updateLayout();
+			
+			// Animate the popup(s)
+			for (ExpandedTrackPopup xtp : expandedTracks) {
+	
+				// Note: returns immediatly if already showing
+				PopupManager.getInstance().showPopup(
+						xtp, 
+						xtp.getLocation(), 
+						null,
+						PopupManager.getInstance().new ExpandShrinkAnimator(
+								directAnimSource,
+								null));
+			
+			}
+		}
+		
+		// Hide the trackSelectionPopup
+		hideTrackSelectionPopup();
+
+	}
+	
+	private void hideTrackSelectionPopup() {
+		PopupManager.getInstance().hidePopup(
+				trackSelectionPopup, PopupManager.getInstance().new ExpandShrinkAnimator(
+						new Rectangle((Browser._theBrowser.getWidth() / 2 ) - (trackSelectionPopup.getWidth() / 2),
+								0, trackSelectionPopup.getWidth(), trackSelectionPopup.getHeight()), null));
+	}
+	
+	/**
+	 * Sets the bounds for all ExpandedTrackPopups, whether they are showing or not, to the
+	 * current state of the window.
+	 */
+	private void updateLayout() {
+		
+		synchronized(expandedTracks) {
+				
+	
+			if (expandedTracks.isEmpty()) return;
+			
+			assert (expandedTracks.size() > 0);
+			assert(Browser._theBrowser != null);
+		
+			int cpWidth = Browser._theBrowser.getContentPane().getWidth();
+			int cpHeight = Browser._theBrowser.getContentPane().getHeight();
+			if (!FrameGraphics.isAudienceMode()) { // if message bay is showing - the remove the height
+				
+				cpHeight -= MessageBay.MESSAGE_BUFFER_HEIGHT;
+				
+			}
+			
+			if (cpHeight < 0) cpHeight = 0;
+			
+			// Calc widths
+			final int EXPANDED_HORO_MARGINS = 100;
+			int widths = cpWidth - (2 * EXPANDED_HORO_MARGINS);
+			if (widths < 150) widths = 150; 
+			
+			// Calc heights (per track view)
+			final int DESIRED_EXPANDED_HEIGHT = 340;
+			final int EXPANDED_VERT_MARGINS = 20;
+			int heights;
+			if ((expandedTracks.size() * DESIRED_EXPANDED_HEIGHT) <= (cpHeight - (2 * EXPANDED_VERT_MARGINS))) {
+				heights = DESIRED_EXPANDED_HEIGHT;
+			} else {
+				heights = (cpHeight - (2 * EXPANDED_VERT_MARGINS)) / expandedTracks.size();
+				if (heights < 100) heights = 100;
+			}
+	
+			// Center the popup(s) 
+			int y = ((cpHeight - (1 * EXPANDED_VERT_MARGINS)) - (expandedTracks.size() * heights)) / 2;
+			if (y < 0 || y > cpHeight) y = EXPANDED_VERT_MARGINS;
+			
+			int x = (cpWidth - widths) / 2;
+			if (x < 0) x = 0;
+			
+			// Update bounds
+			for (ExpandedTrackPopup xtp : expandedTracks) {
+	
+				xtp.setBounds(x, y, widths, heights);
+		
+				y += heights;
+				
+			}
+		}
+		
+	}
+
+	/**
+	 * Global Track model re-use .... can be called from any thread
+	 */
+	public SampledTrackModel getSharedSampledTrackModel(String localfilename) {
+		synchronized(expandedTracks) {
+			for (ExpandedTrackPopup xtp : expandedTracks) {
+				if (xtp.getObservedTrackModel().getLocalFilename().equals(localfilename))
+					return xtp.getObservedTrackModel();
+			}
+		}
+		
+		return null;
+	}
+
+	
+	/**
+	 * A little popup that shows whenever there are tracks selected for expanding
+	 * @author bjn8
+	 *
+	 */
+	private class MultiTrackExpansionPopup extends Popup implements ActionListener {
+		
+		private JButton expandButton;
+		private JButton cancelButton;
+		private JLabel selected1;
+		private JLabel selected2;
+		private JLabel selectedMore;
+
+		private static final long serialVersionUID = 1L;
+		//private final Color BACK_COLOR = SampledTrackGraphViewPort.ZOOM_BACKING_COLOR_NORMAL;
+		
+		MultiTrackExpansionPopup() {
+			super(new GridBagLayout());
+			setSize(260, 80);
+			super.setAudoHide(false);
+			
+			expandButton = new JButton(IconRepository.getIcon("expand.png"));
+			expandButton.addActionListener(this);
+			expandButton.setPreferredSize(new Dimension(40, 40));
+			expandButton.setToolTipText("Expand selection");
+			
+			cancelButton = new JButton(IconRepository.getIcon("close.png"));
+			cancelButton.addActionListener(this);
+			cancelButton.setPreferredSize(new Dimension(40, 40));
+			cancelButton.setToolTipText("Cancel selection");
+			
+			selected1 = new JLabel();
+			selected1.setHorizontalTextPosition(JLabel.LEFT);
+			selected1.setVerticalTextPosition(JLabel.CENTER);
+			
+			selected2 = new JLabel();
+			selected2.setHorizontalTextPosition(JLabel.LEFT);
+			selected2.setVerticalTextPosition(JLabel.CENTER);
+			
+			selectedMore = new JLabel();
+			selectedMore.setHorizontalTextPosition(JLabel.CENTER);
+			selectedMore.setVerticalTextPosition(JLabel.CENTER);
+			
+			GridBagConstraints c = new GridBagConstraints();
+			c.gridx = 0;
+			c.gridy = 0;
+			c.fill = GridBagConstraints.CENTER;
+			add(expandButton, c);
+			
+			c = new GridBagConstraints();
+			c.gridx = 1;
+			c.gridy = 0;
+			c.fill = GridBagConstraints.CENTER;
+			add(cancelButton, c);
+			
+			c = new GridBagConstraints();
+			c.gridx = 0;
+			c.gridy = 1;
+			c.gridwidth = 2;
+			c.fill = GridBagConstraints.CENTER;
+			add(selected1, c);
+			
+			c = new GridBagConstraints();
+			c.gridx = 0;
+			c.gridy = 2;
+			c.gridwidth = 2;
+			c.fill = GridBagConstraints.CENTER;
+			add(selected2, c);
+			
+			c = new GridBagConstraints();
+			c.gridx = 0;
+			c.gridy = 1;
+			c.gridwidth = 2;
+			c.gridheight = 2;
+			c.fill = GridBagConstraints.CENTER;
+			add(selectedMore, c);
+
+		}
+		
+		public void onTrackSelectionChanged() {
+			
+			synchronized(expandedTracks) {
+					
+				if (expandedTracks.isEmpty()) return;
+				
+				selected1.setVisible(false);
+				selected2.setVisible(false);
+				selectedMore.setVisible(false);
+	
+				if (expandedTracks.size() <= 2) {
+					for (int i = 0; i < expandedTracks.size(); i++) {
+						
+						ExpandedTrackPopup expp = expandedTracks.get(i);
+						JLabel label = (i == 0) ? selected1 : selected2;
+						
+						String trackName = expp.getObservedTrackModel().getName();
+						
+						if (trackName == null || trackName.length() == 0) trackName = "Unnamed";
+						else if (trackName.length() > 16) trackName = trackName.substring(0, 14) + "...";
+						
+						label.setText(trackName);
+						label.setVisible(true);
+						
+					}
+					
+				} else {
+					selectedMore.setText(expandedTracks.size() + " tracks selected");
+					selectedMore.setVisible(true);
+				}
+			}
+		}
+
+		public void actionPerformed(ActionEvent e) {
+			if (e.getSource() == expandButton) {
+				showSelection();
+			} else if (e.getSource() == cancelButton) {
+				cancelSelection();
+			}
+		}
+		
+		
+
+	}
+	
+	
+}
Index: /trunk/src_apollo/org/apollo/gui/ExpandedTrackPopup.java
===================================================================
--- /trunk/src_apollo/org/apollo/gui/ExpandedTrackPopup.java	(revision 315)
+++ /trunk/src_apollo/org/apollo/gui/ExpandedTrackPopup.java	(revision 315)
@@ -0,0 +1,853 @@
+package org.apollo.gui;
+
+import java.awt.BorderLayout;
+import java.awt.Color;
+import java.awt.Dimension;
+import java.awt.Event;
+import java.awt.Font;
+import java.awt.Graphics;
+import java.awt.GridBagConstraints;
+import java.awt.GridBagLayout;
+import java.awt.Insets;
+import java.awt.Point;
+import java.awt.Rectangle;
+import java.awt.event.ActionEvent;
+import java.awt.event.ActionListener;
+import java.awt.event.KeyEvent;
+import java.awt.event.KeyListener;
+import java.awt.event.MouseEvent;
+import java.awt.event.MouseListener;
+import java.awt.event.MouseMotionListener;
+
+import javax.sound.sampled.LineUnavailableException;
+import javax.swing.Icon;
+import javax.swing.JButton;
+import javax.swing.JPanel;
+import javax.swing.JSlider;
+import javax.swing.JSplitPane;
+import javax.swing.JToggleButton;
+import javax.swing.event.ChangeEvent;
+import javax.swing.event.ChangeListener;
+
+import org.apollo.audio.ApolloPlaybackMixer;
+import org.apollo.audio.ApolloSubjectChangedEvent;
+import org.apollo.audio.SampledTrackModel;
+import org.apollo.audio.TrackSequence;
+import org.apollo.audio.structure.AudioStructureModel;
+import org.apollo.audio.util.SoundDesk;
+import org.apollo.audio.util.TrackMixSubject;
+import org.apollo.io.IconRepository;
+import org.apollo.items.EmulatedTextItem;
+import org.apollo.items.EmulatedTextItem.TextChangeListener;
+import org.apollo.mvc.Observer;
+import org.apollo.mvc.Subject;
+import org.apollo.mvc.SubjectChangedEvent;
+import org.apollo.util.AudioMath;
+import org.apollo.widgets.TrackWidgetCommons;
+import org.expeditee.gui.Browser;
+import org.expeditee.gui.FrameGraphics;
+import org.expeditee.gui.Popup;
+import org.expeditee.gui.PopupManager;
+
+public class ExpandedTrackPopup extends Popup implements ActionListener, Observer {
+	
+	private static final long serialVersionUID = 1L;
+	
+	/** The observed subject. Can never be null */
+	private SampledTrackModel trackModel; // immutable
+	
+	private String trackSourceFrameName; // immutable
+	
+	private TrackMixSubject trackMix; // immutable
+
+	private JButton playPauseButton;
+	private JButton stopButton;
+	private JButton rewindButton;
+	private JButton closeButton;
+	private JToggleButton soloButton;
+	private JToggleButton muteButton;
+	private JSlider volumeSlider;
+	JPanel nameLabelParent;
+	
+	private EmulatedTextItem nameLabel; 
+	
+	private JSplitPane splitPane;
+	private SampledTrackGraphViewPort zoomController;
+	private EditableSampledTrackGraphView zoomedTrackView;
+	private TimeAxisPanel timeAxis;
+	
+	private boolean isUpdatingGUI = false;
+	
+	/**
+	 * Hidden constuctor: only ExpandedTrackManager can create these.
+	 * 
+	 * @see {@link ExpandedTrackManager}
+	 * 
+	 * @param trackModel
+	 * 
+	 * @param trackMix
+	 * 
+	 * @param trackSourceFrameName
+	 * 		Where the expanded tracks track source was expanded from.
+	 * 		Must not be null
+	 * 
+	 * @param frameStart
+	 * 		the initial zoom
+	 * 
+	 * @param frameLength
+	 * 		the initial zoom
+	 * 
+	 * @throws NullPointerException
+	 * 		If trackModel or trackMix is null.
+	 */
+	private ExpandedTrackPopup(
+			SampledTrackModel trackModel, 
+			TrackMixSubject trackMix,
+			String trackSourceFrameName,
+			int frameStart,
+			int frameLength) {
+		super(new BorderLayout());
+		super.setConsumeBackClick(true);
+		
+		if (trackModel == null) throw new NullPointerException("trackModel");
+		if (trackMix == null) throw new NullPointerException("trackMix");
+		if (trackSourceFrameName == null) throw new NullPointerException("trackSourceFrameName");
+		
+		this.trackModel = trackModel;
+		this.trackMix = trackMix;
+
+		final int BUTTON_SIZE = 40;
+		
+		playPauseButton = new JButton();
+		playPauseButton.addActionListener(this);
+		playPauseButton.setIcon(IconRepository.getIcon("play.png"));
+		playPauseButton.setPreferredSize(new Dimension(BUTTON_SIZE, BUTTON_SIZE));
+		playPauseButton.setToolTipText("Play selection / Pause");
+		
+		stopButton = new JButton();
+		stopButton.setEnabled(false);
+		stopButton.addActionListener(this);
+		stopButton.setIcon(IconRepository.getIcon("stop.png"));
+		stopButton.setPreferredSize(new Dimension(BUTTON_SIZE, BUTTON_SIZE));
+		stopButton.setToolTipText("Stop playback");
+		
+		rewindButton = new JButton();
+		rewindButton.addActionListener(this);
+		rewindButton.setIcon(IconRepository.getIcon("rewind.png"));
+		rewindButton.setPreferredSize(new Dimension(BUTTON_SIZE, BUTTON_SIZE));
+		rewindButton.setToolTipText("Rewind to start");
+		
+		// Icon changes
+		muteButton = new JToggleButton();
+		muteButton.setSelectedIcon(IconRepository.getIcon("volmute.png"));
+		muteButton.setPreferredSize(new Dimension(BUTTON_SIZE, BUTTON_SIZE));
+		muteButton.setToolTipText("Toggle mute");
+		muteButton.addChangeListener(new ChangeListener() {
+			public void stateChanged(ChangeEvent e) {
+				if (!ExpandedTrackPopup.this.isUpdatingGUI) {
+					ExpandedTrackPopup.this.trackMix.setMuted(muteButton.isSelected());
+					
+				}
+			}
+		});
+		
+		soloButton = new JToggleButton();
+		soloButton.setIcon(IconRepository.getIcon("solo.png"));
+		soloButton.setSelectedIcon(IconRepository.getIcon("soloon.png"));
+		soloButton.setPreferredSize(new Dimension(BUTTON_SIZE, BUTTON_SIZE));
+		soloButton.setToolTipText("Toggle solo");
+		soloButton.addChangeListener(new ChangeListener() {
+			public void stateChanged(ChangeEvent e) {
+				if (!ExpandedTrackPopup.this.isUpdatingGUI) {
+					SoundDesk.getInstance().setSoloIDPrefix(soloButton.isSelected() ?
+							ExpandedTrackPopup.this.trackMix.getChannelID() : null
+							);
+					
+				}
+			}
+		});
+		
+		closeButton = new JButton();
+		closeButton.addActionListener(this);
+		closeButton.setIcon(IconRepository.getIcon("close.png"));
+		closeButton.setPreferredSize(new Dimension(BUTTON_SIZE, BUTTON_SIZE));
+		closeButton.setToolTipText("Close");
+		
+		final int VOLUME_SPACING = 6;
+		volumeSlider = new JSlider(JSlider.HORIZONTAL);
+		volumeSlider.setMinimum(0);
+		volumeSlider.setMaximum(100);
+		volumeSlider.addChangeListener(new ChangeListener() {
+			public void stateChanged(ChangeEvent e) {
+				if (!ExpandedTrackPopup.this.isUpdatingGUI) {
+					ExpandedTrackPopup.this.trackMix.setVolume(((float)volumeSlider.getValue()) / 100.0f);
+				}
+				// Update the icons
+				updateButtonGUI();
+			}
+		});
+		volumeSlider.setPreferredSize(new Dimension(100 - (2 * VOLUME_SPACING), BUTTON_SIZE));
+		
+		zoomedTrackView = new EditableSampledTrackGraphView();
+		zoomedTrackView.setSelectAllOnDoubleClick(true);
+		
+		zoomController = new SampledTrackGraphViewPort();
+		
+		// Keep timescale in zoomedTrackView consistance with zoom controller
+		zoomController.addZoomChangeListener(new ZoomChangeListener() {
+
+			public void zoomChanged(Event e) {
+				
+				int minFrame = Math.min(
+						zoomController.getZoomStartFrame(), zoomController.getZoomEndFrame());
+				
+				int maxFrame = Math.max(
+						zoomController.getZoomStartFrame(), zoomController.getZoomEndFrame());
+				
+				
+				zoomedTrackView.setTimeScale(minFrame, maxFrame - minFrame);
+			}
+			
+		});
+		
+		timeAxis = new TimeAxisPanel(zoomedTrackView);
+		timeAxis.setPreferredSize(new Dimension(100, 40));
+		
+		nameLabelParent = new JPanel();
+		
+		nameLabelParent.addMouseListener(new MouseListener() {
+
+			public void mouseClicked(MouseEvent e) {
+				if (nameLabel != null) {
+					if (nameLabel.onMouseClicked(e)) {
+						e.consume();
+						return;
+					}
+				}
+			}
+
+			public void mouseEntered(MouseEvent e) {
+			}
+
+			public void mouseExited(MouseEvent e) {
+			}
+
+			public void mousePressed(MouseEvent e) {
+				if (nameLabel != null) {
+					if (nameLabel.onMousePressed(e)) {
+						e.consume();
+					}
+				}
+			}
+
+			public void mouseReleased(MouseEvent e) {
+				if (nameLabel != null) {
+					if (nameLabel.onMouseReleased(e)) {
+						e.consume();
+					}
+				}
+			}
+		
+		});
+		
+		nameLabelParent.addMouseMotionListener(new MouseMotionListener() {
+
+			public void mouseDragged(MouseEvent e) {
+				if (nameLabel != null) {
+					if (nameLabel.onMouseDragged(e)) {
+						e.consume();
+					}
+				}
+			}
+
+			public void mouseMoved(MouseEvent e) {
+				if (nameLabel != null) {
+					nameLabel.onMouseMoved(e, nameLabelParent);
+				}
+			}
+
+		});
+		
+		nameLabelParent.addKeyListener(new KeyListener() {
+
+			public void keyPressed(KeyEvent e) {
+				if (nameLabel != null) {
+					if (nameLabel.onKeyPressed(e, nameLabelParent)) {
+						e.consume();
+					}
+				}
+			}
+
+			public void keyReleased(KeyEvent e) {
+				if (nameLabel != null) {
+					if (nameLabel.onKeyReleased(e, nameLabelParent)) {
+						e.consume();
+					}
+				}
+
+			}
+
+			public void keyTyped(KeyEvent e) {
+			}
+			
+		});
+		
+		nameLabel = new EmulatedTextItem(nameLabelParent, new Point(10, 25));
+		nameLabel.setFontStyle(Font.BOLD);
+		nameLabel.setFontSize(16);
+		nameLabel.setBackgroundColor(Color.WHITE);	
+		nameLabel.setText(trackModel.getName()); 
+
+		nameLabel.addTextChangeListener(new TextChangeListener() { //  a little bit loopy!
+
+			public void onTextChanged(Object source, String newLabel) {
+				if (ExpandedTrackPopup.this.trackModel != null && !nameLabel.getText().equals(ExpandedTrackPopup.this.trackModel.getName())) {
+					ExpandedTrackPopup.this.trackModel.setName(nameLabel.getText());
+				}
+			}
+
+		});
+		
+		JPanel workArea = new JPanel(new BorderLayout());
+		
+		workArea.add(zoomedTrackView, BorderLayout.CENTER);
+		workArea.add(timeAxis, BorderLayout.SOUTH);
+
+		splitPane = new JSplitPane(JSplitPane.VERTICAL_SPLIT);
+		splitPane.setTopComponent(workArea);
+		splitPane.setBottomComponent(zoomController);
+		splitPane.setOneTouchExpandable(true);
+		splitPane.setResizeWeight(1.0);
+
+		// Create the toolbar
+		JPanel toolBarPanel = new JPanel(new GridBagLayout());
+
+		GridBagConstraints c = new GridBagConstraints();
+		c.gridx = 0;
+		c.gridy = 0;
+		c.fill = GridBagConstraints.BOTH;
+		toolBarPanel.add(playPauseButton, c);
+		
+		c = new GridBagConstraints();
+		c.gridx = 1;
+		c.gridy = 0;
+		c.fill = GridBagConstraints.BOTH;
+		toolBarPanel.add(stopButton, c);
+		
+		c = new GridBagConstraints();
+		c.gridx = 2;
+		c.gridy = 0;
+		c.fill = GridBagConstraints.BOTH;
+		toolBarPanel.add(rewindButton, c);
+		
+		c = new GridBagConstraints();
+		c.gridx = 3;
+		c.gridy = 0;
+		c.fill = GridBagConstraints.BOTH;
+		toolBarPanel.add(soloButton, c);
+		
+		c = new GridBagConstraints();
+		c.gridx = 4;
+		c.gridy = 0;
+		c.fill = GridBagConstraints.BOTH;
+		toolBarPanel.add(muteButton, c);
+		
+		c = new GridBagConstraints();
+		c.gridx = 5;
+		c.gridy = 0;
+		c.fill = GridBagConstraints.BOTH;
+		c.insets = new Insets(0,VOLUME_SPACING,0,VOLUME_SPACING);
+		toolBarPanel.add(volumeSlider, c);
+		
+		c = new GridBagConstraints();
+		c.gridx = 6;
+		c.gridy = 0;
+		c.fill = GridBagConstraints.BOTH;
+		c.weightx = 1.0f;
+		toolBarPanel.add(nameLabelParent, c);
+		
+		c = new GridBagConstraints();
+		c.gridx = 7;
+		c.gridy = 0;
+		c.fill = GridBagConstraints.BOTH;
+		toolBarPanel.add(closeButton, c);
+		
+		this.add(toolBarPanel, BorderLayout.NORTH);
+		this.add(splitPane, BorderLayout.CENTER);
+		
+		// Observe track model
+		trackModel.addObserver(zoomedTrackView);
+		trackModel.addObserver(zoomController);
+		trackModel.addObserver(this);
+		
+		// Setup mix to use
+		zoomedTrackView.setMix(trackMix);
+		
+		// Setup the initial zoom
+		zoomController.setZoom(frameStart, frameStart + frameLength);
+		
+		updateBorderColor();
+	}
+	
+	/**
+	 * Giver/receiver design pattern for only allowing the ExpandedTrackManager singleton
+	 * to create expanded views.
+	 * 
+	 * @param trackModel
+	 * 		The track model to observe.
+	 * 
+	 * @param trackMix
+	 * 		The mix to use.
+	 * 
+	 * @param trackSourceFrameName
+	 * 		Where the expanded tracks track source was expanded from.
+	 * 		Must not be null
+	 * 
+	 * @param frameStart
+	 * 		the initial zoom - clamped
+	 * 
+	 * @param frameLength
+	 * 		the initial zoom - clamped
+	 * 
+	 * @throws NullPointerException
+	 * 		If trackModel or trackMix or trackSourceFrameName or localFilename is null.
+	 * 
+	 */
+	static void giveToExpandedTrackManager(
+			SampledTrackModel trackModel, 
+			TrackMixSubject trackMix,
+			String trackSourceFrameName,
+			int frameStart,
+			int frameLength) {
+		
+		ExpandedTrackPopup xtp = new ExpandedTrackPopup(
+				trackModel, 
+				trackMix,
+				trackSourceFrameName,
+				frameStart, 
+				frameLength);
+		
+		ExpandedTrackManager.getInstance().receiveExpandedTrackPopup(xtp);
+	}
+
+	@Override
+	public void onShowing() {
+
+	}
+	
+	@Override
+	public void onShow() {
+		
+		if (splitPane.getHeight() < 140) {
+			splitPane.setDividerLocation(splitPane.getHeight() - 30);
+		} else {
+			splitPane.setDividerLocation(splitPane.getHeight() - 60);
+		}
+		
+		if (zoomedTrackView.getMix() != null) {
+			updateVolume((int)(100 * zoomedTrackView.getMix().getVolume()));
+			updateMute(zoomedTrackView.getMix().isMuted());
+			updateSolo(SoundDesk.getInstance().isSolo(zoomedTrackView.getMix().getChannelID()));
+		}
+		
+		SoundDesk.getInstance().addObserver(this);
+		trackMix.addObserver(this);
+		
+		// Invalidate full frame for shading
+		if (Browser._theBrowser != null) {
+			FrameGraphics.refresh(false);
+		}
+	}
+
+	@Override
+	public void onHide() {
+		ExpandedTrackManager.getInstance().expandedTrackPopupHidden(this);
+		releaseMemory(); // get rid of buffer etc..
+		
+		SoundDesk.getInstance().removeObserver(this);
+		trackMix.removeObserver(this);
+		
+		// Invalidate full frame for shading
+		if (Browser._theBrowser != null) {
+			FrameGraphics.refresh(false);
+		}
+	}
+	
+	
+
+
+	@Override
+	public void paint(Graphics g) {
+		super.paint(g);
+		
+		g.translate(-getX(), -getY());
+		nameLabel.paint(g);
+		g.translate(getX(), getY());
+		
+	}
+
+	public void actionPerformed(ActionEvent e) {
+		if (trackModel == null) return;
+		
+		if (trackMix == null) return;
+
+		if (e.getSource() == playPauseButton) {
+
+			try {
+
+				if (!SoundDesk.getInstance().isPlaying(trackMix.getChannelID())) { // play / resume
+					
+					int startFrame = -1, endFrame = -1;
+					
+					// Resume playback?
+					if (SoundDesk.getInstance().isPaused(trackMix.getChannelID())) {
+						startFrame = SoundDesk.getInstance().getLastPlayedFramePosition(trackMix.getChannelID());
+						if (startFrame >= 0 && startFrame < trackModel.getFrameCount()) {
+			
+							// The user may have edited the audio track and reselected it
+							// since the last pause. Thus select an appropriate end frame
+							endFrame = (trackModel.getSelectionLength() > 1) ?
+									trackModel.getSelectionStart() + trackModel.getSelectionLength():
+										trackModel.getFrameCount() - 1;
+								
+							// Changed selection? it play range invalid?
+							if (endFrame <= startFrame || startFrame < trackModel.getSelectionStart()) {
+								startFrame = -1; // Play new selection (see below)
+							
+							} else if (endFrame >= trackModel.getFrameCount()) {
+								endFrame = trackModel.getFrameCount() - 1;
+							}		
+							
+						}
+					}
+					
+					// Play from beginning of selection to end of selection
+					if (startFrame < 0) {
+						startFrame = trackModel.getSelectionStart();
+						endFrame = (trackModel.getSelectionLength() > 1) ?
+								startFrame + trackModel.getSelectionLength():
+									trackModel.getFrameCount() - 1;
+					}
+
+					// Safety clamp:
+					if (endFrame >= trackModel.getFrameCount()) {
+						endFrame = trackModel.getFrameCount() - 1;
+					}	
+					
+					if (startFrame < endFrame) {
+						SoundDesk.getInstance().playSampledTrackModel(
+								trackModel, 
+								trackMix.getChannelID(), 
+								startFrame, 
+								endFrame, 
+								0);
+					}
+
+				} else { // pause
+					
+					TrackSequence ts = SoundDesk.getInstance().getTrackSequence(trackMix.getChannelID());
+
+					if (ts != null &&
+							ts.isPlaying()) {
+						
+						// Mark channel as paused.
+						SoundDesk.getInstance().setPaused(trackMix.getChannelID(), true);
+						
+						// Stop playback for this channel
+						ApolloPlaybackMixer.getInstance().stop(ts);
+						
+					}
+					
+				}
+				
+			} catch (LineUnavailableException e1) {
+				e1.printStackTrace();
+			}
+			
+		} else if (e.getSource() == stopButton) {
+			
+			TrackSequence ts = SoundDesk.getInstance().getTrackSequence(trackMix.getChannelID());
+
+			// reset any paused mark
+			SoundDesk.getInstance().setPaused(trackMix.getChannelID(), false);
+			
+			if (ts != null &&
+					ts.isPlaying()) {
+				// Stop playback
+				ApolloPlaybackMixer.getInstance().stop(ts);
+			}
+			
+		} else if (e.getSource() == rewindButton) {
+			
+			trackModel.setSelection(0, 0);
+			SoundDesk.getInstance().setPaused(trackMix.getChannelID(), false);
+			
+		} else if (e.getSource() == closeButton) {
+			
+			// If there are more expanded tracks showing then the ExpandedTrackManager
+			// will automatically re-layout the remaing views.
+			PopupManager.getInstance().hidePopup(this, 
+					PopupManager.getInstance().new ExpandShrinkAnimator(
+					new Rectangle(0, getY(), 1, getHeight()),
+							Color.LIGHT_GRAY));
+			
+		}
+		
+	}
+	
+	/**
+	 * Gets rid of image buffers and threads... Frees this instance from
+	 * being referenced from the track model - very very important.
+	 */
+	void releaseMemory() {
+		
+
+		trackModel.removeObserver(zoomedTrackView);
+		trackModel.removeObserver(zoomController);
+		trackModel.removeObserver(this);
+		
+		assert (zoomedTrackView.getObservedSubject() == null);
+		assert (zoomController.getObservedSubject() == null);
+
+		zoomedTrackView.releaseBuffer();
+		zoomController.releaseBuffer();
+		
+		SoundDesk.getInstance().removeObserver(this);
+		trackMix.removeObserver(this);
+		
+	}
+	
+	/**
+	 * @return The observed track model. Never null
+	 */
+	public Subject getObservedSubject() {
+		return trackModel;
+	}
+	
+	/**
+	 * @return The observed track model.
+	 */
+	SampledTrackModel getObservedTrackModel() {
+		return trackModel;
+	}
+
+
+	public void setObservedSubject(Subject parent) {
+	}
+	
+	public void modelChanged(Subject source, SubjectChangedEvent event) {
+		
+
+		// Synch GUI with track state
+		switch (event.getID()) {
+		
+		case ApolloSubjectChangedEvent.TRACK_SEQUENCE_CREATED: // from sound desk
+			
+			if (event.getState().equals(trackMix.getChannelID())) {
+				// The channel being played is the same as this one ...
+				// even if the track model is unloaded must enter into a playing state
+				// if the created track sequence will play
+				TrackSequence ts = SoundDesk.getInstance().getTrackSequence(trackMix.getChannelID());
+				assert(ts != null);
+				assert(!ts.hasFinished());
+				assert(!ts.isPlaying());
+				ts.addObserver(this);
+			}
+
+			break;
+			
+		case ApolloSubjectChangedEvent.PLAYBACK_STARTED: // From observed track sequence
+			stopButton.setEnabled(true);
+			rewindButton.setEnabled(false);
+			playPauseButton.setIcon(IconRepository.getIcon("pause.png"));
+
+			setBorderThickness(TrackWidgetCommons.PLAYING_TRACK_EDGE_THICKNESS);
+
+			break;
+
+		case ApolloSubjectChangedEvent.PLAYBACK_STOPPED: // From observed track sequence
+			
+			rewindButton.setEnabled(true);
+			stopButton.setEnabled(false);
+			playPauseButton.setIcon(IconRepository.getIcon("play.png"));
+			
+			// Note:
+			// No need to remove self from observing the dead track since the track references this
+			// and will get garbage collected
+			
+			setBorderThickness(TrackWidgetCommons.STOPPED_TRACK_EDGE_THICKNESS);
+
+
+			break;
+			
+		case ApolloSubjectChangedEvent.PAUSE_MARK_CHANGED: // When stopped or paused
+			/*
+			if (ae.getState().equals(trackMix.getChannelID())) {
+				
+				if (MixDesk.getInstance().isPaused(trackMix.getChannelID())) {
+					// Do nothing .. the paused mark is set prior to a stop
+				} else {
+					// Esnure that the GUI represents a stopped state
+					stopButton.setEnabled(false);
+					playPauseButton.setIcon(IconRepository.getIcon("play.png"));
+				}
+				
+			}*/
+			
+			break;
+			
+		case ApolloSubjectChangedEvent.VOLUME: // From obseved track mix
+			updateVolume((int)(100 * trackMix.getVolume()));
+			break;
+			
+		case ApolloSubjectChangedEvent.MUTE: // From obseved track mix
+			updateMute(trackMix.isMuted());
+			updateBorderColor();
+			break;
+			
+		case ApolloSubjectChangedEvent.SOLO_PREFIX_CHANGED: // From mix desk
+			updateSolo(SoundDesk.getInstance().isSolo(trackMix.getChannelID()));
+			updateBorderColor();
+			break;
+			
+		case ApolloSubjectChangedEvent.NAME_CHANGED:
+			if (!nameLabel.getText().equals(trackModel.getName())) {
+				nameLabel.setText(trackModel.getName());
+			}
+			
+			// Keep graph model consistant
+			AudioStructureModel.getInstance().onTrackWidgetNameChanged(
+					trackModel.getLocalFilename(), // may not be in model ... but does not matter in such a case
+					null, 
+					trackModel.getName());
+			
+			break;
+			
+		case ApolloSubjectChangedEvent.AUDIO_INSERTED: // from track model
+		case ApolloSubjectChangedEvent.AUDIO_REMOVED:
+			
+			long newRunningTime = AudioMath.framesToMilliseconds(
+					trackModel.getFrameCount(), trackModel.getFormat());
+			assert(newRunningTime > 0);
+			
+			if (trackModel != null) {
+				
+				// Keep TrackGraphModel consistant
+				AudioStructureModel.getInstance().onTrackWidgetAudioEdited(
+						trackModel.getLocalFilename(), 
+						null, 
+						newRunningTime);
+			}
+			
+			break;
+		}
+		
+	}
+	
+
+	
+	/**
+	 * Updates the volume GUI. 
+	 * {@link #volumeChanged()} is not raised as a result of this call.
+	 * 
+	 * @param vol
+	 * 		The volume ranging from 0 - 100. Clamped.
+	 */
+	protected void updateVolume(int vol) {
+		
+		if (volumeSlider.getValue() == vol) return;
+
+		// Clamp
+		if(vol < 0) vol = 0;
+		else if (vol > 100) vol = 100;
+		
+		isUpdatingGUI = true;
+		
+		volumeSlider.setValue(vol);
+		
+		isUpdatingGUI = false;
+	}
+	
+
+	/**
+	 * Updates the mute button GUI. 
+	 * {@link #muteChanged()} is not raised as a result of this call.
+	 * 
+	 * @param isMuted
+	 * 		True if set gui to muted.
+	 */
+	protected void updateMute(boolean isMuted) {
+		
+		if (muteButton.isSelected() == isMuted) return;
+
+		isUpdatingGUI = true;
+		
+		muteButton.setSelected(isMuted);
+
+		isUpdatingGUI = false;
+	}
+
+
+	/**
+	 * Updates the solo button GUI. 
+	 * {@link #muteChanged()} is not raised as a result of this call.
+	 * 
+	 * @param isSolo
+	 * 		True if set gui to solo on.
+	 */
+	protected void updateSolo(boolean isSolo) {
+		
+		if (soloButton.isSelected() == isSolo) return;
+
+		isUpdatingGUI = true;
+		
+		soloButton.setSelected(isSolo);
+
+		isUpdatingGUI = false;
+	}
+	
+	private void updateBorderColor() {
+		
+		// Get border color currently used
+		Color oldC = getBorderColor();
+		
+		Color newC = TrackWidgetCommons.getBorderColor(
+				SoundDesk.getInstance().isSolo(trackMix.getChannelID()), 
+				trackMix.isMuted());
+
+		// Update the color
+		if (!newC.equals(oldC)) {
+			setBorderColor(newC);
+		}
+	}
+	
+	/**
+	 * Sets the mute icon to represent the current volume value in the slider.
+	 * Note: this is not the icon if mute is on.
+	 */
+	private void updateButtonGUI() {
+		
+		Icon newIcon = null;
+		if (volumeSlider.getValue() <= 25) 
+				newIcon = IconRepository.getIcon("vol25.png");
+		else if (volumeSlider.getValue() <= 50) 
+			newIcon = IconRepository.getIcon("vol50.png");
+		else if (volumeSlider.getValue() <= 75) 
+			newIcon = IconRepository.getIcon("vol75.png");
+		else // maxing
+				newIcon = IconRepository.getIcon("vol100.png");
+		
+		muteButton.setIcon(newIcon);
+	}
+
+	/**
+	 * @return
+	 * 		The frame at where this expanded view came from.
+	 * 		Never null.
+	 */
+	public String getTrackSourceFrameName() {
+		return trackSourceFrameName;
+	}
+	
+	
+}
Index: /trunk/src_apollo/org/apollo/gui/FastAlphaEffect.java
===================================================================
--- /trunk/src_apollo/org/apollo/gui/FastAlphaEffect.java	(revision 315)
+++ /trunk/src_apollo/org/apollo/gui/FastAlphaEffect.java	(revision 315)
@@ -0,0 +1,99 @@
+package org.apollo.gui;
+
+import java.awt.Rectangle;
+import java.awt.image.ColorModel;
+import java.awt.image.WritableRaster;
+
+/**
+ * I remember this little trick back in days of DirectDraw:
+ * 
+ * A fast technique for drawing a 50% alpha blend.
+ * 
+ * @author Brook Novak
+ *
+ */
+public final class FastAlphaEffect {
+
+	private FastAlphaEffect() {
+	}
+
+	/**
+	 * @see FastAlphaEffect#fastBlendSingleColor(WritableRaster, ColorModel, int, int, int, int, int)
+	 * @param raster
+	 * @param colorModel
+	 * @param area
+	 * @param rgb
+	 */
+	public static final void fastBlendSingleColor(
+			WritableRaster raster,
+			ColorModel colorModel,
+			Rectangle area,
+			int rgb) {
+		fastBlendSingleColor(raster, colorModel, area.x, area.y, area.width, area.height, rgb);
+	}
+	
+	/**
+	 * 
+	 * @param raster
+	 * 
+	 * @param colorModel
+	 * 
+	 * @param x
+	 * 
+	 * @param y
+	 * 
+	 * @param width
+	 * 
+	 * @param height
+	 * 
+	 * @param rgb
+	 * 		The color to blend.
+	 * 
+	 * 
+	 * @throws NullPointerException
+	 * 			If raster or colorModel is null
+	 * 
+	 */
+	public static final void fastBlendSingleColor(
+			WritableRaster raster,
+			ColorModel colorModel,
+			int x, int y, 
+			int width, int height,
+			int rgb) {
+		
+		if (raster == null)
+			throw new NullPointerException("raster");
+		else if (colorModel == null)
+			throw new NullPointerException("raster");
+		
+		int endcol = x + width;
+		int endrow = y + height;
+		
+		// Clamp bounds
+		if (x < 0) x = 0;
+		if (y < 0) y = 0;
+		if (endcol >= raster.getWidth()) endcol = raster.getWidth() - 1;
+		if (endrow >= raster.getHeight()) endrow = raster.getHeight() - 1;
+		
+		// Safe note: If endrow <= y or endcol <= x then the loops wont be entered
+
+		boolean offsetStart = false;
+		
+		Object colorData = colorModel.getDataElements(rgb, null);
+
+		for (int row = y; row < endrow; row++) {
+			
+				
+			for (int col = (offsetStart) ? x + 1 : x; col < endcol; col+=2) { // paint every second pixel
+		
+					raster.setDataElements(col, row, colorData);
+				
+			}
+			
+			// Get checker effect
+			offsetStart = !offsetStart;
+		
+		}
+		
+	}
+}
Index: /trunk/src_apollo/org/apollo/gui/FramePlaybackBarRenderer.java
===================================================================
--- /trunk/src_apollo/org/apollo/gui/FramePlaybackBarRenderer.java	(revision 315)
+++ /trunk/src_apollo/org/apollo/gui/FramePlaybackBarRenderer.java	(revision 315)
@@ -0,0 +1,244 @@
+package org.apollo.gui;
+
+import java.awt.BasicStroke;
+import java.awt.Color;
+import java.awt.Graphics2D;
+import java.awt.Rectangle;
+import java.awt.Stroke;
+import java.util.LinkedList;
+import java.util.List;
+
+import javax.swing.SwingUtilities;
+
+import org.apollo.audio.ApolloPlaybackMixer;
+import org.apollo.audio.ApolloSubjectChangedEvent;
+import org.apollo.audio.structure.OverdubbedFrame;
+import org.apollo.audio.util.FrameLayoutDaemon;
+import org.apollo.audio.util.MultiTrackPlaybackController;
+import org.apollo.audio.util.PlaybackClock;
+import org.apollo.audio.util.Timeline;
+import org.apollo.audio.util.PlaybackClock.PlaybackClockListener;
+import org.apollo.mvc.Observer;
+import org.apollo.mvc.Subject;
+import org.apollo.mvc.SubjectChangedEvent;
+import org.apollo.util.AudioMath;
+import org.apollo.widgets.FramePlayer;
+import org.expeditee.gui.Browser;
+import org.expeditee.gui.DisplayIO;
+import org.expeditee.gui.Frame;
+import org.expeditee.gui.FrameGraphics;
+
+/**
+ * Renders playback bars while the multi playback controller is playing.
+ * Depending on which frame the user is on etc...
+ * 
+ * @author Brook Novak
+ *
+ */
+public class FramePlaybackBarRenderer implements PlaybackClockListener, Observer {
+	
+	/** Relative to the current multi-track groups root frame. */
+	private long currentMSPosition = 0;
+	
+	private List<Integer> pixelPositions = new LinkedList<Integer>();
+	private String pixelPositionsParent = null; // framename
+	
+	private Timeline currentTimeline = null;
+	
+	private PlaybackFrameBarUpdator updator = new PlaybackFrameBarUpdator();
+	
+	private static final int BAR_STROKE_THICKNESS = 2;
+	private static final Stroke BAR_STROKE = new BasicStroke(BAR_STROKE_THICKNESS);
+	private static final Color BAR_COLOR = Color.DARK_GRAY;
+	
+	private static FramePlaybackBarRenderer instance = new FramePlaybackBarRenderer();
+	
+	private FramePlaybackBarRenderer() {
+		MultiTrackPlaybackController.getInstance().addObserver(this);
+		//DisplayIO.addDisplayIOObserver(this);
+		FrameLayoutDaemon.getInstance().addObserver(this);
+	}
+	
+	public static FramePlaybackBarRenderer getInstance() {
+		return instance;
+	}
+
+	/**
+	 * {@inheritDoc}
+	 */
+	public Subject getObservedSubject() {
+		return null;
+	}
+	
+	/**
+	 * {@inheritDoc}
+	 */
+	public void setObservedSubject(Subject parent) {
+	}
+
+	/**
+	 * {@inheritDoc}
+	 */
+	public void modelChanged(Subject source, SubjectChangedEvent event) {
+
+		if (source == FrameLayoutDaemon.getInstance()) {
+			currentTimeline = FrameLayoutDaemon.getInstance().getLastComputedTimeline();
+			
+			updator.run();
+			// Asumming refresh will occur
+
+			return;
+		}
+		
+		switch (event.getID()) {
+		case ApolloSubjectChangedEvent.PLAYBACK_STARTED:
+			PlaybackClock.getInstance().addPlaybackClockListener(this);
+			break;
+			
+		case ApolloSubjectChangedEvent.PLAYBACK_STOPPED:
+			PlaybackClock.getInstance().removePlaybackClockListener(this);
+			
+			if (MultiTrackPlaybackController.getInstance().isMarkedAsPaused() &&
+					FramePlayer.FRAME_PLAYERMASTER_CHANNEL_ID.equals(
+							MultiTrackPlaybackController.getInstance().getCurrentMasterChannelID())) {
+
+				currentMSPosition = AudioMath.framesToMilliseconds(
+						MultiTrackPlaybackController.getInstance().getLastSuspendedFrame(),  
+						ApolloPlaybackMixer.getInstance().getLiveAudioFormat());
+				
+				updator.run();
+				
+			} else {
+				invalidate();
+				currentMSPosition = -1;
+				pixelPositions.clear();
+			}
+			
+			FrameGraphics.refresh(true);
+			
+			
+			break;
+			
+		case ApolloSubjectChangedEvent.PAUSE_MARK_CHANGED:
+			
+			
+			invalidate();
+			currentMSPosition = -1;
+			pixelPositions.clear();
+			FrameGraphics.refresh(true);
+			
+			break;
+		}
+			
+		
+	}
+
+	
+//	/**
+//	 * {@inheritDoc}
+//	 */
+//	public void frameChanged() {
+//		
+//		// TODO: Update positions... could be paused!
+//		
+//		// ...
+//		
+//	}
+//	
+	
+	private void invalidate() {
+		if (Browser._theBrowser == null) return;
+		
+		int height = Browser._theBrowser.getHeight();
+		
+		for (Integer n : pixelPositions) {
+			FrameGraphics.invalidateArea(new Rectangle(n - 1, 0, BAR_STROKE_THICKNESS + 2, height));
+		}
+	}
+	
+	public void paint(Graphics2D g) {
+		if (Browser._theBrowser == null) return;
+		
+		Frame currentFrame = DisplayIO.getCurrentFrame();
+		
+		if (currentFrame == null || currentFrame.getName() == null || pixelPositionsParent == null ||
+				!currentFrame.getName().equals(pixelPositionsParent)) return;
+		
+		int height = Browser._theBrowser.getHeight();
+		
+		g.setColor(BAR_COLOR);
+		g.setStroke(BAR_STROKE);
+		
+		for (Integer n : pixelPositions) {
+			g.drawLine(n, 0, n, height);
+		}
+	}
+	
+	/**
+	 * {@inheritDoc}
+	 */
+	public void onTick(long framePosition, long msPosition) {
+		
+		if (framePosition < 0) return; // let stop event handle it
+		
+		// Convert the audio mixers frame position to the multiplaybacks frame position
+		
+		int fpos = (int)(framePosition 
+				- MultiTrackPlaybackController.getInstance().getLastInitiationFrame()
+				+ MultiTrackPlaybackController.getInstance().getLastStartFrame());
+		
+		// Clamp
+		if (fpos > MultiTrackPlaybackController.getInstance().getLastEndFrame())
+			fpos = MultiTrackPlaybackController.getInstance().getLastEndFrame();
+		
+		currentMSPosition = AudioMath.framesToMilliseconds(
+				fpos,  
+				ApolloPlaybackMixer.getInstance().getLiveAudioFormat());
+		
+		// Notes: the clock will queue a refresh for the frame after this
+		// event proccesses ... 
+		SwingUtilities.invokeLater(updator);
+	}
+	
+	/**
+	 * Note: refreshing is up to caller
+	 * 
+	 * @author Brook Novak
+	 */
+	private class PlaybackFrameBarUpdator implements Runnable {
+		public void run() {
+			
+			if (currentMSPosition == -1 || currentTimeline == null) return;
+			
+			Frame currentFrame = DisplayIO.getCurrentFrame();
+			
+			if (currentFrame == null || currentFrame.getName() == null) return;
+
+			OverdubbedFrame od = MultiTrackPlaybackController.getInstance().getCurrentODFrame();
+			if (od == null) return;
+
+			// Invalidate old positions
+			invalidate();
+			pixelPositions.clear();
+			
+			long firstInitTime = od.getFirstInitiationTime();
+			
+			List<Integer> msPositions = od.getMSPositions(
+					currentFrame.getName(), 
+					currentMSPosition + firstInitTime);
+			
+			// Convert ms positions to pixel positions according to the current timeline
+			for (Integer n : msPositions) {
+				pixelPositions.add(new Integer(currentTimeline.getXAtMSTime(n + currentTimeline.getFirstInitiationTime())));
+			}
+
+			pixelPositionsParent = currentFrame.getName();
+				
+			// Invalidate new positions
+			invalidate();
+		}
+	}
+	
+	
+
+}
Index: /trunk/src_apollo/org/apollo/gui/FrameRenderPasses.java
===================================================================
--- /trunk/src_apollo/org/apollo/gui/FrameRenderPasses.java	(revision 315)
+++ /trunk/src_apollo/org/apollo/gui/FrameRenderPasses.java	(revision 315)
@@ -0,0 +1,248 @@
+package org.apollo.gui;
+
+import java.awt.Color;
+import java.awt.FontMetrics;
+import java.awt.Graphics;
+import java.awt.Graphics2D;
+import java.awt.Rectangle;
+import java.awt.geom.Area;
+import java.awt.geom.Rectangle2D;
+import java.util.LinkedList;
+
+import org.apollo.ApolloSystem;
+import org.apollo.audio.util.Timeline;
+import org.apollo.items.FramePlaybackLauncher;
+import org.apollo.widgets.LinkedTrack;
+import org.apollo.widgets.SampleRecorder;
+import org.apollo.widgets.SampledTrackEvo4;
+import org.expeditee.gui.Browser;
+import org.expeditee.gui.FrameGraphics;
+import org.expeditee.gui.FrameMouseActions;
+import org.expeditee.gui.FreeItems;
+import org.expeditee.gui.FrameGraphics.FrameRenderPass;
+import org.expeditee.items.Item;
+import org.expeditee.items.widgets.InteractiveWidget;
+import org.expeditee.items.widgets.WidgetCorner;
+import org.expeditee.items.widgets.WidgetEdge;
+
+/**
+ * All final effects rendered here - to give extra feedback to the user and simply to make
+ * it more apealing.
+ * @author Brook Novak
+ *
+ */
+public class FrameRenderPasses implements FrameRenderPass {
+	
+	private static final Color SHADED_EXPANDED_BACKING_COLOR = new Color(80, 80, 80, 140);
+	private static FrameRenderPasses instance = new FrameRenderPasses();
+	
+	private LinkedList<Integer> helperLinesToPaint = null;
+	private boolean shouldDrawFullscreenHelpers = false;
+	
+	private FrameRenderPasses() {
+		FrameGraphics.addFrameRenderPass(this);
+	}
+	
+	public static FrameRenderPasses getInstance() {
+		return instance;
+	}
+
+	public void paintFinalPass(Graphics g) {
+		if (Browser._theBrowser == null) return;
+		int frameHeight = Browser._theBrowser.getContentPane().getHeight();
+		if (ExpandedTrackManager.getInstance().isAnyExpandedTrackVisible()
+				&& !FreeItems.getInstance().isEmpty()) {
+
+			// Conceivable could have multiple
+			LinkedList<SampledTrackEvo4> toPaint = null;
+			
+			for (Item i : FreeItems.getInstance()) {
+				if (i instanceof WidgetCorner) {
+					InteractiveWidget iw = ((WidgetCorner)i).getWidgetSource();
+					
+					if (iw != null && iw instanceof SampledTrackEvo4) {
+						
+						if (toPaint == null) {
+							toPaint = new LinkedList<SampledTrackEvo4>();
+						} else if (toPaint.contains(iw)) {
+							continue;
+						}
+						
+						toPaint.add((SampledTrackEvo4)iw);
+					}
+				}
+			}
+			
+			if (toPaint != null) {
+				for (SampledTrackEvo4 trackWidget : toPaint) {
+					if (g.getClip() == null
+							|| g.getClip().intersects(trackWidget.getComponant().getBounds()))
+					trackWidget.paintInFreeSpace(g, true);
+					
+				}
+			}
+			
+		} 
+		
+		OverdubbedFrameTimeAxis.getInstance().paint(g);
+		
+		TimeAxis timeAxis = OverdubbedFrameTimeAxis.getInstance().getTimeAxis();	
+		Timeline lastTimeline = OverdubbedFrameTimeAxis.getInstance().getLastTimeline();
+		
+		FramePlaybackBarRenderer.getInstance().paint((Graphics2D)g);
+
+		if (helperLinesToPaint != null && timeAxis != null && lastTimeline != null) {
+
+
+			Rectangle r = OverdubbedFrameTimeAxis.getInstance().getCurrentTimelineArea();
+			
+			// Draw helper bar and text
+			for (Integer x : helperLinesToPaint) {
+				
+				if (shouldDrawFullscreenHelpers) {
+					((Graphics2D)g).setStroke(OverdubbedFrameTimeAxis.TRACK_TIMELINE_STROKE);
+					g.setColor(Color.RED);
+					g.drawLine(x, 0, x, frameHeight);
+				} else {
+					((Graphics2D)g).setStroke(OverdubbedFrameTimeAxis.TRACK_TIMELINE_STROKE);
+					g.setColor(Color.RED);
+					g.drawLine(x, r.y, x, r.y + r.height);
+				}
+				
+				
+				long ms = lastTimeline.getMSTimeAtX(x) - lastTimeline.getFirstInitiationTime();
+
+				String label = timeAxis.createSpecificTimeLabel(Math.abs(ms), 500.0);
+				if (ms < 0) label = "-" + label;
+				
+				FontMetrics fm   = g.getFontMetrics(SampledTrackGraphViewPort.TIME_HELPER_FONT);
+				Rectangle2D rect = fm.getStringBounds(label, g);
+				int labelWidth  = (int)(rect.getWidth());
+				int labelHeight  = (int)(rect.getHeight());
+				
+				int y = r.y + (r.height / 2) - ((labelHeight + 4) / 2);
+				
+
+				g.setColor(SampledTrackGraphViewPort.TIME_HELPER_COLOR);
+				g.fillRect(x + 2,  y, labelWidth + 3, labelHeight + 4);
+	
+				g.setColor(SampledTrackGraphViewPort.TIME_HELPER_BORDERCOLOR);
+				((Graphics2D)g).setStroke(SampledTrackGraphViewPort.ZOOM_BACKING_BORDER_STROKE);
+				g.fillRect(x + 2,  y, labelWidth + 3, labelHeight + 4);
+
+				g.setFont(SampledTrackGraphViewPort.TIME_HELPER_FONT);
+				g.setColor(Color.BLACK);
+				g.drawString(label, x + 4, y + labelHeight);
+				
+				
+			}
+			
+		}
+		
+	
+	}
+
+	public void paintPreLayeredPanePass(Graphics g) {
+		if (Browser._theBrowser == null) return;
+		
+		if (ExpandedTrackManager.getInstance().isAnyExpandedTrackVisible()) {
+			
+			g.setColor(SHADED_EXPANDED_BACKING_COLOR);
+			g.fillRect(0, 0, 
+					Browser._theBrowser.getContentPane().getWidth(), 
+					Browser._theBrowser.getContentPane().getHeight());
+		}
+		
+	}
+
+	public Area paintStarted(Area currentClip) {
+		if (Browser._theBrowser == null) return currentClip;
+
+		LinkedList<InteractiveWidget> seen = null;
+		
+		//boolean idDirty = false;
+		//if (helperLinesToPaint != null) {
+		//	idDirty = true;
+			
+		//}
+		
+		invalidateHelperLines(currentClip);
+		
+		helperLinesToPaint = null;
+		shouldDrawFullscreenHelpers = false;
+		
+		/*MouseEvent me = MouseEventRouter.getCurrentMouseEvent();
+		if (me != null) { //&& me.getSource() == Browser._theBrowser.getContentPane()) {
+			shouldDrawFullscreenHelpers = me.isControlDown();
+		}*/
+		shouldDrawFullscreenHelpers = ApolloSystem.isControlDown();
+
+		// Get free tracks and paint line helpers
+		if (!FreeItems.getInstance().isEmpty() || FrameMouseActions.getlastHighlightedItem() != null) {
+
+			LinkedList<Item> possibles = new LinkedList<Item>(FreeItems.getInstance());
+			if (FreeItems.getInstance().isEmpty() &&
+					FrameMouseActions.getlastHighlightedItem() != null)
+				possibles.add(FrameMouseActions.getlastHighlightedItem());
+			
+			for (Item i : possibles) {
+				
+				InteractiveWidget iw = null;
+				
+				if (i instanceof WidgetCorner) {
+					iw = ((WidgetCorner)i).getWidgetSource();
+				} else if (i instanceof WidgetEdge) {
+					iw = ((WidgetEdge)i).getWidgetSource();
+				}
+				
+				if (iw != null) {
+					
+					if (iw != null && (iw instanceof SampledTrackEvo4 ||
+							iw instanceof LinkedTrack || iw instanceof SampleRecorder)) {
+						
+						if (seen == null) {
+							seen = new LinkedList<InteractiveWidget>();
+						} else if (seen.contains(iw)) {
+							continue;
+						}
+						
+						if (helperLinesToPaint == null)
+							helperLinesToPaint = new LinkedList<Integer>();
+		
+						helperLinesToPaint.add(iw.getX());
+					}
+				} else if (i instanceof FramePlaybackLauncher) {
+					if (helperLinesToPaint == null)
+						helperLinesToPaint = new LinkedList<Integer>();
+					helperLinesToPaint.add(i.getX());
+				}
+			}
+			
+
+
+		} 
+
+		// Invalidate on the fly
+		invalidateHelperLines(currentClip);
+		
+		return currentClip;
+		
+	}
+	
+	private void invalidateHelperLines(Area currentClip) {
+		if (helperLinesToPaint != null && currentClip != null && !helperLinesToPaint.isEmpty()) {
+			currentClip.add(new Area(OverdubbedFrameTimeAxis.getInstance().getCurrentTimelineArea()));
+			if (shouldDrawFullscreenHelpers) {
+				int fsHeight = Browser._theBrowser.getContentPane().getHeight();
+				for (Integer n : helperLinesToPaint) {
+					currentClip.add(new Area(new Rectangle(n - 1, 0, 3, fsHeight)));
+				}
+			}
+
+		}
+	}
+	
+	
+	
+
+}
Index: /trunk/src_apollo/org/apollo/gui/OverdubbedFrameTimeAxis.java
===================================================================
--- /trunk/src_apollo/org/apollo/gui/OverdubbedFrameTimeAxis.java	(revision 315)
+++ /trunk/src_apollo/org/apollo/gui/OverdubbedFrameTimeAxis.java	(revision 315)
@@ -0,0 +1,208 @@
+package org.apollo.gui;
+
+import java.awt.BasicStroke;
+import java.awt.Color;
+import java.awt.Graphics;
+import java.awt.Graphics2D;
+import java.awt.Rectangle;
+import java.awt.Stroke;
+import java.awt.event.ComponentEvent;
+import java.awt.event.ComponentListener;
+
+import javax.swing.SwingUtilities;
+
+import org.apollo.audio.util.FrameLayoutDaemon;
+import org.apollo.audio.util.Timeline;
+import org.apollo.mvc.Observer;
+import org.apollo.mvc.Subject;
+import org.apollo.mvc.SubjectChangedEvent;
+import org.expeditee.gui.Browser;
+import org.expeditee.gui.DisplayIO;
+import org.expeditee.gui.FrameGraphics;
+import org.expeditee.gui.MessageBay;
+
+/**
+ * The global timeline on the current overdubbed frame
+ * 
+ * @author Brook Novak
+ * 
+ */
+public class OverdubbedFrameTimeAxis implements Observer { //, DisplayIOObserver {
+
+	private TimeAxis timeAxis = null;
+	private Rectangle lastInvalidated;
+	private Timeline lastTimeline = null;
+
+	private static OverdubbedFrameTimeAxis instance = new OverdubbedFrameTimeAxis();
+
+	private static final int GLOBAL_TIMELINE_HEIGHT = 30;
+	private static final Color GLOBAL_TIMELINE_BACKCOLOR = new Color(220, 220, 220);
+	private static final Color GLOBAL_EMPTY_TIMELINE_BACKCOLOR = new Color(240, 240, 240);
+	
+	public static final Stroke TRACK_TIMELINE_STROKE = new BasicStroke(
+			1.0f, BasicStroke.CAP_SQUARE, BasicStroke.JOIN_BEVEL, 0.0f, new float[] { 5.0f, 5.0f }, 0.0f);
+	
+	private static final Stroke TIMELINE_BORDER_STROKE = new BasicStroke(1.0f);
+			
+	/**
+	 * Singleton constructor,
+	 */
+	private OverdubbedFrameTimeAxis() {
+		
+		FrameLayoutDaemon.getInstance().addObserver(this);
+		
+		//DisplayIO.addDisplayIOObserver(this);
+		
+		// Listen for resize events
+		SwingUtilities.invokeLater(new Runnable() {
+			public void run() {
+
+				// Keep expanded view centered to the browser window
+				if (Browser._theBrowser != null) {
+					Browser._theBrowser.addComponentListener(new ComponentListener() {
+
+						public void componentHidden(ComponentEvent e) {
+						}
+
+						public void componentMoved(ComponentEvent e) {
+						}
+
+						public void componentResized(ComponentEvent e) {
+							updateAxis();
+						}
+
+						public void componentShown(ComponentEvent e) {
+						}
+						
+					});
+				}
+				
+			}
+		});
+	}
+
+
+	/**
+	 * 
+	 * @return
+	 * 		The singleton instance.
+	 */
+	public static OverdubbedFrameTimeAxis getInstance() {
+		return instance;
+	}
+	
+	public void setObservedSubject(Subject parent) {
+	}
+
+	public Subject getObservedSubject() {
+		return FrameLayoutDaemon.getInstance();
+	}
+
+	public void modelChanged(Subject source, SubjectChangedEvent event) {
+		updateAxis();
+	}
+	
+	/*public void frameChanged() {
+		updateAxis();
+	}*/
+	
+	private void updateAxis() {
+		
+		if (Browser._theBrowser == null)
+			return;
+
+		lastTimeline = FrameLayoutDaemon.getInstance().getLastComputedTimeline();
+		if (lastTimeline == null || 
+			FrameLayoutDaemon.getInstance().getTimelineOwner() != DisplayIO.getCurrentFrame()) {
+			
+			// The frame layout daemon only lays out frames once - unless the user rearranges tracks
+			// on a frame then the daemon we re-calc the timeline... thus a timeline must be infferred
+			
+			lastTimeline = FrameLayoutDaemon.inferTimeline(DisplayIO.getCurrentFrame());
+			
+			if (lastTimeline == null) { // no widgets
+				if (timeAxis != null && lastInvalidated != null) {
+					FrameGraphics.invalidateArea(lastInvalidated); // old position
+				}
+				
+				timeAxis = null;
+				return;
+			}
+			
+		}
+		
+		timeAxis = new TimeAxis();
+
+		timeAxis.setAxis(
+				0, 
+				lastTimeline.getRunningTime(), 
+				-1,
+				lastTimeline.getRunningTime(), 
+				lastTimeline.getPixelWidth());
+
+		
+		Rectangle newBounds = getCurrentTimelineArea();
+		if (lastInvalidated != null && !lastInvalidated.equals(newBounds)) {
+			FrameGraphics.invalidateArea(lastInvalidated); // old position
+		}
+		
+		FrameGraphics.invalidateArea(newBounds);
+		
+		lastInvalidated = newBounds;
+		
+		FrameGraphics.refresh(true);
+		
+	}
+
+	public Rectangle getCurrentTimelineArea() {
+		assert(Browser._theBrowser != null);
+		int y = Browser._theBrowser.getContentPane().getHeight() - GLOBAL_TIMELINE_HEIGHT;
+		if (FrameGraphics.getMode() != FrameGraphics.MODE_AUDIENCE)
+			y -= MessageBay.MESSAGE_BUFFER_HEIGHT;
+		
+		if (y < 0) y = 0;
+
+		return new Rectangle(0, y, Browser._theBrowser.getContentPane().getWidth(), GLOBAL_TIMELINE_HEIGHT);
+	}
+	
+
+	public void paint(Graphics g) {
+		if (Browser._theBrowser == null) return;
+
+		if (timeAxis != null && lastTimeline != null) {
+		
+			Rectangle r = getCurrentTimelineArea();
+
+			g.setColor(GLOBAL_EMPTY_TIMELINE_BACKCOLOR);
+			g.fillRect(0, r.y, Browser._theBrowser.getContentPane().getWidth(), r.height);
+			
+			timeAxis.paint(g, 
+					lastTimeline.getInitiationXPixel(),
+					r.y,
+					r.height, 
+					GLOBAL_TIMELINE_BACKCOLOR);
+			
+			
+			g.setColor(Color.DARK_GRAY);
+			
+			((Graphics2D)g).setStroke(TIMELINE_BORDER_STROKE);
+			g.drawLine(0, r.y, Browser._theBrowser.getContentPane().getWidth(), r.y);
+	
+		}
+		
+		
+	}
+
+
+	public Timeline getLastTimeline() {
+		return lastTimeline;
+	}
+
+
+	public TimeAxis getTimeAxis() {
+		return timeAxis;
+	}
+	
+	
+	
+}
Index: /trunk/src_apollo/org/apollo/gui/PeakTroughWaveFormRenderer.java
===================================================================
--- /trunk/src_apollo/org/apollo/gui/PeakTroughWaveFormRenderer.java	(revision 315)
+++ /trunk/src_apollo/org/apollo/gui/PeakTroughWaveFormRenderer.java	(revision 315)
@@ -0,0 +1,174 @@
+package org.apollo.gui;
+
+import javax.sound.sampled.AudioFormat;
+
+import org.apollo.audio.SampledAudioManager;
+
+/**
+ * A WaveFormRenderer where the peaks and troughs are always chosen for every chunk of aggregated frames.
+ * 
+ * @author Brook Novak
+ *
+ */
+public class PeakTroughWaveFormRenderer implements WaveFormRenderer {
+	
+	private static final int DEFAULT_PEAK_TOLERANCE = 20;
+
+	private int sampleSize;
+	private boolean isBigEndian;
+	private boolean isSigned;
+	
+	// Alternate
+	private boolean lastSampleHigh = false;
+	
+	/** When frames becomes more aggregated - the selected peaks are alternated evenly
+	 *  with a given tolerance*/
+	private int peakTolerance;
+
+	/**
+	 * Constructor.
+	 * 
+	 * @param audioFormat
+	 * 			The format of the audio bytes to be rendered.
+	 * 
+	 * @throws NullPointerException
+	 * 			If audio format is null.
+	 * 
+	 * @throws IllegalArgumentException
+	 * 			If audioformat is not supported. See SampledAudioManager.isFormatSupportedForPlayback
+	 */
+	public PeakTroughWaveFormRenderer(AudioFormat audioFormat) {
+		if (audioFormat == null) throw new NullPointerException("audioFormat");
+		
+		if (!SampledAudioManager.getInstance().isFormatSupportedForPlayback(audioFormat)) 
+			throw new IllegalArgumentException();
+
+		sampleSize = audioFormat.getSampleSizeInBits();
+		isSigned = audioFormat.getEncoding().toString().startsWith("PCM_SIGN");
+		isBigEndian = audioFormat.isBigEndian();
+		
+		peakTolerance = DEFAULT_PEAK_TOLERANCE * audioFormat.getFrameSize();
+	}
+
+	/**
+	 * {@inheritDoc}
+	 */
+	public float[] getSampleAmplitudes(byte[] audioBytes, int startFrame, int frameLength, int aggregationSize) {
+		assert(audioBytes != null);
+		assert(startFrame >= 0);
+		assert((startFrame + frameLength) <= (audioBytes.length / (sampleSize / 8)));
+		
+		float[] amplitudes = new float[frameLength / aggregationSize];
+		
+		if (sampleSize == 16) {
+					 
+			for (int i = 0; i < amplitudes.length; i++) {
+				
+				int max = 0, absmax = -1, sample, abssample; // could use short, but int avoid casting everywhere
+
+				int startFrameIndex = (startFrame + (i * aggregationSize)) << 1;
+				int endFrameIndex = startFrameIndex + (aggregationSize << 1);
+				
+				for (int k = startFrameIndex; k < endFrameIndex; k+=2) {
+			
+					int lsb, msb;
+								 
+					if (isBigEndian) {
+						
+						// First byte is MSB (high order)
+						msb = (int)audioBytes[k];
+						 
+						 // Second byte is LSB (low order)
+						lsb = (int)audioBytes[k + 1];
+					
+					 } else {
+						// First byte is LSB (low order)
+						lsb = (int)audioBytes[k];
+						 
+						// Second byte is MSB (high order)
+						msb = (int)audioBytes[k + 1];
+					}
+					
+					sample = (msb << 0x8) | (0xFF & lsb);
+					
+					abssample = Math.abs(sample);
+
+					if (lastSampleHigh && sample < 0) {
+						abssample += peakTolerance;
+					} else if (!lastSampleHigh && sample > 0) {
+						abssample += peakTolerance;
+					}  
+					
+					if (abssample > absmax) {
+						max = sample;
+						absmax = abssample;
+					}
+
+				}
+				
+				lastSampleHigh = max > 0;
+
+				amplitudes[i] = ((float)max) / 32768.0f;
+				
+			}
+
+		} else if (sampleSize == 8) {
+			
+			if (isSigned) {
+		  
+				// Find the peak within the block of aggregated frames
+				for (int i = 0; i < amplitudes.length; i++) {
+							
+					byte max = 0, absmax = -1, sample, abssample;
+
+					int startFrameIndex = startFrame + (i * aggregationSize);
+					int endFrameIndex = startFrameIndex + aggregationSize;
+					
+					for (int k = startFrameIndex; k < endFrameIndex; k++) {
+						
+						sample = audioBytes[k];
+						abssample = (sample < 0) ? (byte)(sample * -1) : sample;
+						
+						if (abssample > absmax) {
+							max = sample;
+							absmax = abssample;
+						}
+					}
+
+					amplitudes[i] = ((float)max) / 128.0f;
+					
+				}
+
+			} else { // unsigned
+
+				// Find the peak within the block of aggregated frames
+				for (int i = 0; i < amplitudes.length; i++) {
+							
+					int max = 0, absmax = -1, sample, abssample; // could use short, but int avoid casting everywhere
+
+					int startFrameIndex = startFrame + (i * aggregationSize);
+					int endFrameIndex = startFrameIndex + aggregationSize;
+					
+					for (int k = startFrameIndex; k < endFrameIndex; k++) {
+						
+						sample = (audioBytes[k] & 0xFF) - 128;
+						abssample = Math.abs(sample);
+						
+						if (abssample > absmax) {
+							max = sample;
+							absmax = abssample;
+						}
+					}
+
+					amplitudes[i] = ((float)max) / 128.0f;
+					
+				}
+				
+			}
+			    		 
+		}
+
+		return amplitudes;
+	}
+
+}
Index: /trunk/src_apollo/org/apollo/gui/PlaybackControlPopup.java
===================================================================
--- /trunk/src_apollo/org/apollo/gui/PlaybackControlPopup.java	(revision 315)
+++ /trunk/src_apollo/org/apollo/gui/PlaybackControlPopup.java	(revision 315)
@@ -0,0 +1,308 @@
+package org.apollo.gui;
+
+import java.awt.Dimension;
+import java.awt.GridBagConstraints;
+import java.awt.GridBagLayout;
+import java.awt.Insets;
+import java.awt.event.ActionEvent;
+import java.awt.event.ActionListener;
+
+import javax.swing.Icon;
+import javax.swing.JButton;
+import javax.swing.JSlider;
+import javax.swing.JToggleButton;
+import javax.swing.event.ChangeEvent;
+import javax.swing.event.ChangeListener;
+
+import org.apollo.io.IconRepository;
+import org.expeditee.gui.Popup;
+
+/**
+ * A pure GUI for playback and mixing popup used for tracks and linked tracks.
+ * Does not implement beahviour - just the gui.
+ * 
+ * @author Brook Novak
+ *
+ */
+public abstract class PlaybackControlPopup extends Popup implements ActionListener {
+
+	public JButton playPauseButton;
+	public JButton stopButton;
+	public JButton rewindButton;
+	public JToggleButton muteButton;
+	public JToggleButton soloButton;
+	public JButton miscButton;
+	public JSlider volumeSlider;
+	
+	private boolean isUpdatingGUI = false;
+
+	private static final int BUTTON_SIZE = 40;
+	
+	protected PlaybackControlPopup() {
+		super(new GridBagLayout());
+		
+		playPauseButton = new JButton();
+		playPauseButton.setActionCommand("playpause");
+		playPauseButton.addActionListener(this);
+		playPauseButton.setIcon(IconRepository.getIcon("play.png"));
+		playPauseButton.setPreferredSize(new Dimension(BUTTON_SIZE, BUTTON_SIZE));
+		playPauseButton.setToolTipText("Play selection / Pause");
+		
+		stopButton = new JButton();
+		stopButton.setEnabled(false);
+		stopButton.addActionListener(this);
+		stopButton.setActionCommand("stop");
+		stopButton.setIcon(IconRepository.getIcon("stop.png"));
+		stopButton.setPreferredSize(new Dimension(BUTTON_SIZE, BUTTON_SIZE));
+		stopButton.setToolTipText("Stop playback");
+		
+		rewindButton = new JButton();
+		rewindButton.addActionListener(this);
+		rewindButton.setActionCommand("rewind");
+		rewindButton.setIcon(IconRepository.getIcon("rewind.png"));
+		rewindButton.setPreferredSize(new Dimension(BUTTON_SIZE, BUTTON_SIZE));
+		rewindButton.setToolTipText("Rewind to start");
+		
+		// Icon changes
+		muteButton = new JToggleButton();
+		muteButton.setSelectedIcon(IconRepository.getIcon("volmute.png"));
+		muteButton.setPreferredSize(new Dimension(BUTTON_SIZE, BUTTON_SIZE));
+		muteButton.setToolTipText("Toggle mute");
+		muteButton.addChangeListener(new ChangeListener() {
+			public void stateChanged(ChangeEvent e) {
+				if (!PlaybackControlPopup.this.isUpdatingGUI) {
+					muteChanged();
+				}
+			}
+		});
+		
+		soloButton = new JToggleButton();
+		soloButton.setIcon(IconRepository.getIcon("solo.png"));
+		soloButton.setSelectedIcon(IconRepository.getIcon("soloon.png"));
+		soloButton.setPreferredSize(new Dimension(BUTTON_SIZE, BUTTON_SIZE));
+		soloButton.setToolTipText("Toggle solo");
+		soloButton.addChangeListener(new ChangeListener() {
+			public void stateChanged(ChangeEvent e) {
+				if (!PlaybackControlPopup.this.isUpdatingGUI) {
+					soloChanged();
+				}
+			}
+		});
+		
+		miscButton = new JButton();
+		miscButton.addActionListener(this);
+		miscButton.setPreferredSize(new Dimension(BUTTON_SIZE, BUTTON_SIZE));
+		
+		final int VOLUME_SPACING = 6;
+		volumeSlider = new JSlider(JSlider.HORIZONTAL);
+		volumeSlider.setPreferredSize(new Dimension(2 * (BUTTON_SIZE - VOLUME_SPACING), BUTTON_SIZE));
+		volumeSlider.setMinimum(0);
+		volumeSlider.setMaximum(100);
+		volumeSlider.addChangeListener(new ChangeListener() {
+			public void stateChanged(ChangeEvent e) {
+				if (!PlaybackControlPopup.this.isUpdatingGUI) {
+					volumeChanged();
+				}
+				// Update the icons
+				updateButtonGUI();
+			}
+		});
+		
+
+		// Create the toolbar
+		GridBagConstraints c = new GridBagConstraints();
+		c.gridx = 0;
+		c.gridy = 0;
+		c.fill = GridBagConstraints.BOTH;
+		add(playPauseButton, c);
+		
+		c = new GridBagConstraints();
+		c.gridx = 1;
+		c.gridy = 0;
+		c.fill = GridBagConstraints.BOTH;
+		add(stopButton, c);
+		
+		c = new GridBagConstraints();
+		c.gridx = 2;
+		c.gridy = 0;
+		c.fill = GridBagConstraints.BOTH;
+		add(rewindButton, c);
+		
+		c = new GridBagConstraints();
+		c.gridx = 3;
+		c.gridy = 0;
+		c.fill = GridBagConstraints.BOTH;
+		add(soloButton, c);
+		
+		c = new GridBagConstraints();
+		c.gridx = 4;
+		c.gridy = 0;
+		c.fill = GridBagConstraints.BOTH;
+		add(muteButton, c);
+		
+		c = new GridBagConstraints();
+		c.gridx = 5;
+		c.gridy = 0;
+		c.fill = GridBagConstraints.BOTH;
+		c.insets = new Insets(0,VOLUME_SPACING,0,VOLUME_SPACING);
+		add(volumeSlider, c);
+		
+		c = new GridBagConstraints();
+		c.gridx = 7;
+		c.gridy = 0;
+		c.fill = GridBagConstraints.BOTH;
+		add(miscButton, c);
+		
+//
+//		GridBagConstraints c = new GridBagConstraints();
+//		c.gridx = 0;
+//		c.gridy = 0;
+//		c.fill = GridBagConstraints.BOTH;
+//		this.add(soloButton, c);
+//		
+//		c = new GridBagConstraints();
+//		c.gridx = 1;
+//		c.gridy = 0;
+//		c.fill = GridBagConstraints.BOTH;
+//		this.add(muteButton, c);
+//
+//		c = new GridBagConstraints();
+//		c.gridx = 2;
+//		c.gridy = 0;
+//		c.gridwidth = 2;
+//		c.insets = new Insets(0,VOLUME_SPACING,0,VOLUME_SPACING);
+//		c.fill = GridBagConstraints.BOTH;
+//		this.add(volumeSlider, c);
+//		
+//		c = new GridBagConstraints();
+//		c.gridx = 0;
+//		c.gridy = 1;
+//		c.fill = GridBagConstraints.BOTH;
+//		this.add(playPauseButton, c);
+//		
+//		c = new GridBagConstraints();
+//		c.gridx = 1;
+//		c.gridy = 1;
+//		c.fill = GridBagConstraints.BOTH;
+//		this.add(stopButton, c);
+//		
+//		c = new GridBagConstraints();
+//		c.gridx = 2;
+//		c.gridy = 1;
+//		c.fill = GridBagConstraints.BOTH;
+//		this.add(rewindButton, c);
+//
+//		c = new GridBagConstraints();
+//		c.gridx = 3;
+//		c.gridy = 1;
+//		c.fill = GridBagConstraints.BOTH;
+//		this.add(miscButton, c);
+//		
+//		this.setSize(BUTTON_SIZE * 4, BUTTON_SIZE * 2);
+		
+		this.setSize(BUTTON_SIZE * 8, BUTTON_SIZE);
+		
+		this.doLayout();
+		
+	}
+
+	/**
+	 * Sets the mute icon to represent the current volume value in the slider.
+	 * Note: this is not the icon if mute is on.
+	 */
+	private void updateButtonGUI() {
+		
+		Icon newIcon = null;
+		if (volumeSlider.getValue() <= 25) 
+				newIcon = IconRepository.getIcon("vol25.png");
+		else if (volumeSlider.getValue() <= 50) 
+			newIcon = IconRepository.getIcon("vol50.png");
+		else if (volumeSlider.getValue() <= 75) 
+			newIcon = IconRepository.getIcon("vol75.png");
+		else // maxing
+				newIcon = IconRepository.getIcon("vol100.png");
+		
+		muteButton.setIcon(newIcon);
+	}
+
+
+	public abstract void actionPerformed(ActionEvent e);
+	
+	/**
+	 * Invoke when the slider changes via the user (i.e. not from a model-changed to event)
+	 */
+	protected abstract void volumeChanged();
+	
+	/**
+	 * Invoke when mute button toggles via the user (i.e. not from a model-changed to event)
+	 */
+	protected abstract void muteChanged();
+	
+	/**
+	 * Invoke when solo button toggles via the user (i.e. not from a model-changed to event)
+	 */
+	protected abstract void soloChanged();
+	
+	
+	/**
+	 * Updates the volume GUI. 
+	 * {@link #volumeChanged()} is not raised as a result of this call.
+	 * 
+	 * @param vol
+	 * 		The volume ranging from 0 - 100. Clamped.
+	 */
+	public void updateVolume(int vol) {
+		
+		if (volumeSlider.getValue() == vol) return;
+
+		// Clamp
+		if(vol < 0) vol = 0;
+		else if (vol > 100) vol = 100;
+		
+		isUpdatingGUI = true;
+		
+		volumeSlider.setValue(vol);
+		
+		isUpdatingGUI = false;
+	}
+	
+
+	/**
+	 * Updates the mute button GUI. 
+	 * {@link #muteChanged()} is not raised as a result of this call.
+	 * 
+	 * @param isMuted
+	 * 		True if set gui to muted.
+	 */
+	public void updateMute(boolean isMuted) {
+		
+		if (muteButton.isSelected() == isMuted) return;
+
+		isUpdatingGUI = true;
+		
+		muteButton.setSelected(isMuted);
+
+		isUpdatingGUI = false;
+	}
+
+
+	/**
+	 * Updates the solo button GUI. 
+	 * {@link #muteChanged()} is not raised as a result of this call.
+	 * 
+	 * @param isSolo
+	 * 		True if set gui to solo on.
+	 */
+	public void updateSolo(boolean isSolo) {
+		
+		if (soloButton.isSelected() == isSolo) return;
+
+		isUpdatingGUI = true;
+		
+		soloButton.setSelected(isSolo);
+
+		isUpdatingGUI = false;
+	}
+	
+			
+}
Index: /trunk/src_apollo/org/apollo/gui/SampledTrackGraphView.java
===================================================================
--- /trunk/src_apollo/org/apollo/gui/SampledTrackGraphView.java	(revision 315)
+++ /trunk/src_apollo/org/apollo/gui/SampledTrackGraphView.java	(revision 315)
@@ -0,0 +1,965 @@
+package org.apollo.gui;
+
+import java.awt.BasicStroke;
+import java.awt.Color;
+import java.awt.Event;
+import java.awt.Font;
+import java.awt.GradientPaint;
+import java.awt.Graphics;
+import java.awt.Graphics2D;
+import java.awt.Paint;
+import java.awt.Point;
+import java.awt.Rectangle;
+import java.awt.Stroke;
+import java.awt.event.ComponentEvent;
+import java.awt.event.ComponentListener;
+import java.awt.image.BufferedImage;
+import java.util.LinkedList;
+import java.util.List;
+
+import javax.swing.JComponent;
+import javax.swing.SwingUtilities;
+
+import org.apollo.ApolloSystem;
+import org.apollo.audio.ApolloSubjectChangedEvent;
+import org.apollo.audio.SampledTrackModel;
+import org.apollo.audio.TrackSequence;
+import org.apollo.audio.util.SoundDesk;
+import org.apollo.audio.util.PlaybackClock;
+import org.apollo.audio.util.TrackMixSubject;
+import org.apollo.audio.util.PlaybackClock.PlaybackClockListener;
+import org.apollo.mvc.Observer;
+import org.apollo.mvc.Subject;
+import org.apollo.mvc.SubjectChangedEvent;
+import org.expeditee.gui.Browser;
+import org.expeditee.gui.FrameGraphics;
+
+/**
+ * A graphical view for SampledTrackModel's.
+ * 
+ * To use the view, just add a SampledTrackGraphView instance to a SampledTrackModel.
+ * Can re-use a SampledTrackGraphView instance.
+ * 
+ * @author Brook Novak
+ */
+public class SampledTrackGraphView extends JComponent implements Observer, PlaybackClockListener {
+	
+	private static final long serialVersionUID = 1L;
+	
+	/** The observed subject. Can be null */
+	private SampledTrackModel trackModel = null;
+	
+	/** The mix to use for the observed track. */
+	private TrackMixSubject trackMix = null;
+	
+	/** The track sequence currently playing. Nullified when playback is stopped. */
+	private TrackSequence currentTrackSequence = null;
+	
+	/** Negative = no playback */
+	private int currentPlaybackFramePosition = -1;
+
+	private WaveFormRenderProccessingUnit.WaveFormRenderTask renderTask = null;
+	private BufferedImage backBuffer = null; // Shared resource
+	private int bufferWidth = -1; // cached from backBuffer - so don't have to lock
+	private int bufferHeight = -1; // cached from backBuffer - so don't have to lock
+	
+	// Model Data
+	private int timescaleFrameStart = 0; // in frames
+	private int timescaleFrameLength = 0; // in frames
+	
+	private final Font RENDER_MESSAGE_FONT = new Font("Arial", Font.ITALIC | Font.BOLD, 14);
+	
+	private boolean alwaysFullViewOn = false;
+	
+	public static final Color DEFAULT_BACKGROUND_COLOR = new Color(200, 200, 200);
+	public static final Color DEFAULT_BACKGROUND_HIGHTLIGHTS_COLOR = new Color(240, 240, 240);
+	
+	private Color backColor = DEFAULT_BACKGROUND_COLOR;
+	private Color backColorHighlights = DEFAULT_BACKGROUND_HIGHTLIGHTS_COLOR;
+	
+	public static final Color PLAYBACK_BAR_COLOR = new Color(225, 187, 15);
+	
+	/** The stroke used for drawing graph bars. E.G: The selection Start bar. */
+	public static final Stroke GRAPH_BAR_STROKE = new BasicStroke(1.0f);
+	public static final int GRAPH_BAR_NWIDTH = 1;
+	
+	/** Used for safely invalidating playback positions at realtime. */
+	private Point expediteePosition;
+	
+	private LinkedList<BackSection> backingSections = new LinkedList<BackSection>();
+	private EffecientInvalidator invalidator = null;
+	private List<TimelineChangeListener> timelineChangeListeners = new LinkedList<TimelineChangeListener>();
+	
+	public SampledTrackGraphView() {
+		
+		// Keep backings consistant with component resize
+		this.addComponentListener(new ComponentListener() {
+			public void componentHidden(ComponentEvent e) {
+			}
+
+			public void componentMoved(ComponentEvent e) {
+				//updateExpediteePoint(); // Doesn;t work here - swing has nasty bug
+			}
+
+			public void componentResized(ComponentEvent e) {
+				
+				if (SampledTrackGraphView.this.trackModel != null) {
+	
+					for (BackSection bs : backingSections) {
+						bs.updateBounds();
+					}
+				}
+			}
+
+			public void componentShown(ComponentEvent e) {
+				//updateExpediteePoint(); // Doesn;t work here - swing has nasty bug
+			}
+		
+		});
+		
+
+	}
+
+	/**
+	 * {@inheritDoc}
+	 */
+	@Override
+	public void setBounds(int x, int y, int width, int height) {
+		super.setBounds(x, y, width, height);
+		updateExpediteePoint(); // work around for Swing bug!
+	}
+
+
+	/**
+	 * Saves this views position in expeditee space.
+	 */
+	private void updateExpediteePoint() {
+		if (Browser._theBrowser == null) return;
+		
+		expediteePosition = SwingUtilities.convertPoint(
+				this, 
+				0, 0, 
+				Browser._theBrowser.getContentPane());
+	}
+	
+	/**
+	 * Take care: can be null if not intialized.
+	 * @return
+	 * 		The point at which this view is in a expeditee frame.
+	 */
+	protected Point getExpediteePoint() {
+		return expediteePosition;
+	}
+
+	public void addTimeScaleChangeListener(TimelineChangeListener tcl) {
+		if (!timelineChangeListeners.contains(tcl))
+			timelineChangeListeners.add(tcl);
+	}
+	
+	protected void fireTimelineChanged() {
+		Event e = new Event(this, 0, null);
+		for (TimelineChangeListener listener : timelineChangeListeners)
+			listener.timelineChanged(e);
+	}
+	
+	/**
+	 * {@inheritDoc}
+	 */
+	public Subject getObservedSubject() {
+		return trackModel;
+	}
+	
+	
+	/**
+	 * @return
+	 * 		The observed track model. Null if none is being viewed.
+	 */
+	protected SampledTrackModel getSampledTrackModel() {
+		return trackModel;
+	}
+	
+	/**
+	 * {@inheritDoc}
+	 */
+	public void setObservedSubject(Subject parent) {
+		
+		// Note: parent can be an observer task. But that type of parent will never
+		// send null.
+		if (parent == null) { // Reset view to view nothing
+			trackModel = null;
+			if (trackMix != null) // avoid infinite recursion (deadlock until out of memory)
+				setMix(null); // stop observing the mix desk - otherwise this reference will never be garbage collected
+			releaseBuffer();
+			
+		} else if (parent instanceof SampledTrackModel) { // Prepares the view
+
+			trackModel = (SampledTrackModel)parent;
+			
+			if (alwaysFullViewOn) {
+				timescaleFrameStart = 0;
+				timescaleFrameLength = trackModel.getFrameCount();
+				fireTimelineChanged();
+			} else  {
+				// Ensure that the timeline is valid - even though will probably change
+				clampCurrentTimescale();
+			}
+		} 
+		
+		
+	}
+	
+	/**
+	 * {@inheritDoc}
+	 */
+	public void modelChanged(Subject source, SubjectChangedEvent event) {
+		if (trackModel == null) return;
+		
+
+		switch(event.getID()) {
+		
+			// Invalidate dirty graph - according to render proccessor
+			case ApolloSubjectChangedEvent.RENDER_TASK_INVALIDATION_RECOMENDATION: // event from render proccessor
+				invalidateAll();
+				break;
+
+			case ApolloSubjectChangedEvent.AUDIO_REMOVED: // event from track model
+			case ApolloSubjectChangedEvent.AUDIO_INSERTED: // event from track model
+				if (alwaysFullViewOn) {
+					timescaleFrameStart = 0;
+					timescaleFrameLength = trackModel.getFrameCount();
+					fireTimelineChanged();
+				} else  {
+					// Ensure that the timeline is valid - even though will probably change
+					clampCurrentTimescale();
+				}
+				redrawBuffer();
+				break;
+				
+			case ApolloSubjectChangedEvent.TRACK_SEQUENCE_CREATED: // event from mix desk
+				assert(trackMix != null);
+				assert(event.getState() != null);
+				
+				// OK a track sequence has just been created, but is it for this
+				// track mix?
+				if (event.getState().equals(trackMix.getChannelID())) {
+					
+					// If so - then observe the created track sequence for playback messages
+					TrackSequence ts = SoundDesk.getInstance().getTrackSequence(trackMix.getChannelID());
+					assert(ts != null);
+					ts.addObserver(this); // NOTE: No need to remove self since these are garbage collected rather quickly
+				}
+				
+				break;
+				
+			case ApolloSubjectChangedEvent.PLAYBACK_STARTED: // event from track sequence
+				
+				// Important to set here - since a track can be created but fail to initiate
+				// playback ... thus must set reference only when gauranteed that it can be unset.
+				currentTrackSequence = (TrackSequence)source;
+				
+				PlaybackClock.getInstance().addPlaybackClockListener(this); // listen to ticks
+				
+				onPlaybackStarted();
+				break;
+				
+			case ApolloSubjectChangedEvent.PLAYBACK_STOPPED: // event from track sequence
+				PlaybackClock.getInstance().removePlaybackClockListener(this); // stop listening to ticks
+				
+				int playbackFramePos = -1;
+				
+				// The stop event might be due to a pause:
+				if (currentTrackSequence != null && trackMix != null && 
+						SoundDesk.getInstance().isPaused(trackMix.getChannelID())) {
+					playbackFramePos = currentTrackSequence.getSuspendedFrame();
+				}
+				
+				// Has the user edited the audio such that the playback position is invalid?
+				if (playbackFramePos > trackModel.getFrameCount()) 
+					playbackFramePos = -1; 
+					
+				// Note: although these nullify the audio bytes when stopped, the state
+				// information carries a reference to the original track model at the moment
+				// which stores a lot of data and thus keeping the reference could
+				// hog the memory.
+				currentTrackSequence = null;
+
+				updatePlaybackPosition(playbackFramePos);
+
+				onPlaybackStopped();
+				break;
+				
+				
+			case ApolloSubjectChangedEvent.PAUSE_MARK_CHANGED:
+
+				if (trackMix != null && event.getState().equals(trackMix.getChannelID())) {
+					
+					if (!SoundDesk.getInstance().isPaused(trackMix.getChannelID())) {
+						updatePlaybackPosition(-1);
+					}
+					
+				}
+				break;
+
+		}
+	}
+	
+	protected void onPlaybackStarted() {}
+	protected void onPlaybackStopped() {}
+	
+	/**
+	 * @return
+	 * 		True if this view is viewing a track sequence that is playing
+	 */
+	public final boolean isPlaying() {
+		return (currentTrackSequence != null && currentTrackSequence.isPlaying());
+	}
+	
+	private int getPlaybackXPos(int playbackFrame) {
+		
+		if (playbackFrame >= timescaleFrameStart && 
+				playbackFrame <= (timescaleFrameStart + timescaleFrameLength)) {
+			
+			return XatFrame(playbackFrame);
+		}
+
+		return -1;
+	}
+	
+	private void updatePlaybackPosition(int absFramePos) {
+
+		if (currentPlaybackFramePosition != absFramePos) {
+
+			int height = getHeight();
+			int viewWidth = getWidth();
+			int x;
+
+			// Invalidate old pos
+			if(expediteePosition != null) {
+			 
+				x = getPlaybackXPos(currentPlaybackFramePosition);
+				
+				if (x >= 0 && x <= viewWidth) { // is the playback position in view?
+					FrameGraphics.invalidateArea(new Rectangle(
+						x + expediteePosition.x, 
+						expediteePosition.y, 
+						GRAPH_BAR_NWIDTH, 
+						height));
+				}
+			}
+
+			// Set new pos
+			currentPlaybackFramePosition = absFramePos;
+			
+			// Invalidate new pos
+			if(expediteePosition != null && currentPlaybackFramePosition >= 0) {
+				
+				x = getPlaybackXPos(currentPlaybackFramePosition);
+				
+				if (x >= 0 && x <= viewWidth) { // is the playback position in view?
+					FrameGraphics.invalidateArea(new Rectangle(
+						x + expediteePosition.x , 
+						expediteePosition.y, 
+						GRAPH_BAR_NWIDTH, 
+						height));
+
+				}
+			}
+
+		}
+		
+		
+	}
+
+	/**
+	 * {@inheritDoc}
+	 */
+	public void onTick(long framePosition, long msPosition) {
+		
+		if (framePosition < 0) return; // Done
+		
+		// The frame position / ms position is according to the software mixers timeline - not
+		// neccessarily starting off when the observed track sequence started. Thus must
+		// determine the actual frame position relative to the current rack sequence being played
+		// for this view:
+		int fpos = (int)(framePosition - currentTrackSequence.getInitiationFrame() + currentTrackSequence.getStartFrame());
+		if (fpos > currentTrackSequence.getEndFrame()) fpos = currentTrackSequence.getEndFrame();
+		updatePlaybackPosition(fpos);
+		
+		// Note: the clock issues render requests
+	}
+
+	/**
+	 * Sets the mix to use for playback / and to identify the channel for observering
+	 * live playback...
+	 * 
+	 * This resets to null whenever this view is set to no longer observe a track model.
+	 * 
+	 * @param trackMix
+	 * 		The new track mix to use. Null to use none.
+	 * 
+	 */
+	public void setMix(TrackMixSubject trackMix) {
+		
+		this.trackMix = trackMix;
+
+		if (trackMix != null) {
+			// Listen for track seq creation events ...
+			// thus if a track seq is created for this new ID then we can listen
+			// for playback events and render playback bars.
+			SoundDesk.getInstance().addObserver(this);
+			
+			// If the mix is already playing - switch to a playing state.
+			if (SoundDesk.getInstance().isPlaying(trackMix.getChannelID())) {
+				
+				currentTrackSequence = SoundDesk.getInstance().getTrackSequence(trackMix.getChannelID());
+				
+				if (currentTrackSequence != null) {
+					
+					// Note: that there are no race conditions because track sequence events
+					// are riased on this thread.
+					//currentPlaybackFramePosition = currentTrackSequence.getCurrentFrame(); // no invalidation
+					currentTrackSequence.addObserver(this);
+					PlaybackClock.getInstance().addPlaybackClockListener(this);
+					onPlaybackStarted();
+					
+				} 
+				
+			} else if (SoundDesk.getInstance().isPaused(trackMix.getChannelID())){
+				
+				int suspos = SoundDesk.getInstance().getLastPlayedFramePosition(trackMix.getChannelID());
+				updatePlaybackPosition(suspos);
+				
+			}
+			
+		} else {
+			SoundDesk.getInstance().removeObserver(this);
+		}
+		
+	}
+	
+	/**
+	 * @return
+	 * 		The mix set to be used for this view. Can be null.
+	 */
+	public TrackMixSubject getMix() {
+		return trackMix;   
+	}
+
+	public void setAlwaysFullView(boolean alwaysFullViewOn) {
+		this.alwaysFullViewOn = alwaysFullViewOn;
+	}
+	                    
+	private void clampCurrentTimescale() {
+		int[] newTimeScale = getClampedTimeScale(timescaleFrameStart, timescaleFrameLength);
+		
+		if (timescaleFrameStart !=  newTimeScale[0]
+		   || timescaleFrameLength !=  newTimeScale[1]) {
+			
+			timescaleFrameStart = newTimeScale[0];
+			timescaleFrameLength = newTimeScale[1];
+			fireTimelineChanged();
+		}
+	}
+	
+	/**
+	 * Calculates an appropriate timescale from the given start and length.
+	 * 
+	 * trackModel Must not be null.
+	 * 
+	 * @param frameStart
+	 * 
+	 * @param frameLength
+	 * 
+	 * @return 
+	 * 		Array of two ints. First element is clamped start frame,
+	 * 		The second element is the clamped length.
+	 */
+	private int[] getClampedTimeScale(int frameStart, int frameLength) {
+		assert(trackModel != null);
+		
+		if (frameLength < 1) frameLength = 1;
+
+		if (frameLength > trackModel.getFrameCount()) {
+			
+			frameLength = trackModel.getFrameCount();
+			frameStart = 0;
+			
+		} else if (frameStart < 0) { // watch out for start 
+				
+			frameStart = 0;
+			
+		} else if ((frameStart + frameLength) > trackModel.getFrameCount()) { // and end
+				
+			frameStart = trackModel.getFrameCount() - frameLength;
+			
+		}
+
+		assert (frameStart >= 0);
+		assert ((frameStart + frameLength) <= trackModel.getFrameCount());
+
+		return new int[] {frameStart, frameLength};
+	}
+
+	/**
+	 * Sets the time scale for the graph viewport. 
+	 * As a result, the graph will be re-rendered asynchronously.
+	 * 
+	 * @param frameStart
+	 * @param frameLength
+	 */
+	public void setTimeScale(int frameStart, int frameLength) {
+		
+		int[] newScale = getClampedTimeScale(frameStart, frameLength);
+
+		if (newScale[0] == timescaleFrameStart && newScale[1] == timescaleFrameLength) 
+			return;
+		
+		// Set new model data
+		timescaleFrameStart = newScale[0];
+		timescaleFrameLength = newScale[1];
+		
+		// Re-render graph with new time scale
+		redrawBuffer();
+		
+		fireTimelineChanged();
+	}
+	
+	/**
+	 * @return
+	 * 		The start <i>frame</i> of the current timescale.
+	 * 		Always in valid position if subject is set.
+	 * 		
+	 */
+	public int getTimeScaleStart() {
+		return timescaleFrameStart;
+	}
+	
+	/**
+	 * @return
+	 * 		The length of the timescale in <i>frames</i>.
+	 * 		Always larger or equal to one if subject is set.
+	 */
+	public int getTimeScaleLength() {
+		return timescaleFrameLength;
+	}
+	
+	/**
+	 * Re-renders the waveform buffers to match the graph size if it needs to.
+	 * This will avoid pixelation artifacts.
+	 */
+	public void updateBuffer() {
+		// Check for resize of view or if not initialized yet
+		if (bufferWidth != getWidth() || bufferHeight != getHeight() || backBuffer == null) {
+			redrawBuffer();
+		}
+	}
+
+	/**
+	 * Releases the image buffer.
+	 * 
+	 * <b>Important:</b> Must call on the swing thread.
+	 */
+	public void releaseBuffer() {
+
+		if (renderTask != null && !renderTask.isStopped())
+			WaveFormRenderProccessingUnit.getInstance().cancelTask(renderTask);
+		
+		renderTask = null; // must be on swing thread because could be painting
+		if (backBuffer != null) backBuffer.flush();
+		backBuffer = null; // must be on swing thread because could be painting
+	}
+
+	/**
+	 * Initiates redrawing. If currently redrawing then it will be cancelled and will start from scratch.
+	 * Re-creates the waveform buffer based on the panels current width and height.
+	 * 
+	 * The graph panel will be fully invalidated
+	 * 
+	 * If the trackModel is not set, then the buffer will not be redrawn.
+	 * 
+	 */
+	protected void redrawBuffer() {
+		
+		if (trackModel == null)
+			return;
+
+		int width = getWidth();
+		int height = getHeight();
+
+		if (width <= 0 || height <= 0) return;
+		
+		// Remember width for re-rendering when graph size changes
+		bufferWidth = width;
+		bufferHeight = height;
+		
+		// Cancel current rendering task
+		if (renderTask != null && !renderTask.isStopped())
+			WaveFormRenderProccessingUnit.getInstance().cancelTask(renderTask);
+		
+		// Re-create the back buffer if need to
+		Object bufferLock = (backBuffer == null) ? new Object() : backBuffer;
+		synchronized(bufferLock) {
+
+			if (backBuffer == null ||
+					backBuffer.getWidth() != width
+					|| backBuffer.getHeight() != height) {
+				
+				// Create new sized buffer
+				backBuffer = new BufferedImage(
+						width, 
+						height,
+						BufferedImage.TYPE_BYTE_INDEXED,
+						ApolloColorIndexedModels.graphIndexColorModel);
+			}
+		}
+
+		
+		// Describe render task
+		renderTask = WaveFormRenderProccessingUnit.getInstance().new WaveFormRenderTask(
+				backBuffer, 
+				trackModel.getAllAudioBytes(),
+				trackModel.getFormat(),
+				timescaleFrameStart,
+				timescaleFrameLength,
+				true
+				);
+		
+		// Wait for invalidation recomendation messages
+		renderTask.addObserver(this);
+
+		// Begin rendering ASAP
+		WaveFormRenderProccessingUnit.getInstance().queueTask(renderTask);
+	}
+
+	public void invalidateAll() { 
+		invalidate(this.getBounds());
+	}
+
+	/**
+	 * @param dirty
+	 * 		The diry area with respect to this views origin... may want to
+	 * 		do an efficient invalidation.
+	 * 		For example if the parent of this is an iwidgets top most container
+	 * 		then may want to explicitly invalidate rather than having to 
+	 * 		create an AWT message...
+	 */
+	protected void invalidate(Rectangle dirty) {
+		if (invalidator != null) invalidator.onGraphDirty(this, dirty);
+		else super.repaint(); // the slower way
+	}
+	
+	public void setInvalidator(EffecientInvalidator invalidator) {
+		this.invalidator = invalidator;
+	}
+
+	/**
+	 * Adds a backing section to the graph - will be painted once visible.
+	 * Does not invalidate the graph.
+	 * 
+	 * Intended use: One BackSection for the lifetime of the instance.
+	 * Just keep updateing the backing section.
+	 * 
+	 * @param bs
+	 */
+	protected void addBackingSection(BackSection bs) {
+		
+		if (!backingSections.contains(bs)) {
+			backingSections.add(bs);
+		}
+	}
+	
+	protected void removeBackingSection(BackSection bs) {
+		backingSections.remove(bs);
+	}
+
+	@Override
+	public void paint(Graphics g) {
+		
+		// only performs rendering if widths are different.
+		// Also note that this is not invoked when widget is resizing because
+		// the widget is painted another way
+		updateBuffer(); // First paint will probably initialize the back buffer
+
+		// Draw backing sections
+		paintBacking((Graphics2D)g);
+		
+		// Draw the wave forms
+		if (backBuffer != null) { // zoomed scale view
+			
+			if (renderTask != null && 
+					(renderTask.isRendering()
+					|| !renderTask.hasStarted())) {
+				
+				// If rendering or pending - lock the buffer
+				synchronized(backBuffer) {
+	
+					g.drawImage(backBuffer, 
+							0, 
+							0, 
+							getWidth(), 
+							getHeight(), 
+							null);
+					
+				}
+				
+				// .. and give feedback to user about rendering progress
+				paintRenderProgress(g);
+				
+			} else { // Not rendering
+			
+				// No need to synchronize waveformBuffer because there is no thread accessing it,
+				// and a new render thread is always created on this thread.
+				g.drawImage(backBuffer, 
+						0, 
+						0, 
+						getWidth(), 
+						getHeight(), 
+						null);
+				
+			}
+			
+		}
+		
+		// Get the playback bar position
+		//int playbackFramePos = currentPlaybackFramePosition;
+		
+		// If not playing but is suspended....
+		/*if (playbackFramePos < 0 && trackMix != null
+				&& MixDesk.getInstance().isPaused(trackMix.getChannelID())) {
+			
+			// Get the last playback pos - can be negitive if not played yet
+			playbackFramePos = MixDesk.getInstance().getLastPlayedFramePosition(trackMix.getChannelID());
+			
+			// Has the user edited the audio such that the playback position is invalid?
+			if (playbackFramePos > trackModel.getFrameCount()) 
+				playbackFramePos = -1; 
+		}*/
+		
+		// Paint the playback bar
+		if (currentPlaybackFramePosition >= 0) {
+			
+			int x = getPlaybackXPos(currentPlaybackFramePosition);
+			
+			if (x >= 0 && x <= getWidth()) {
+				
+				((Graphics2D)g).setStroke(GRAPH_BAR_STROKE);
+				g.setColor(PLAYBACK_BAR_COLOR);
+				g.drawLine(x, 0, x, getHeight());
+			}
+			
+		}
+		
+		if (getComponentCount() > 0) {
+			paintChildren(g);
+		}
+		
+	}
+
+	private void paintBacking(Graphics2D g) {
+
+		// Clear background
+		Paint restore = g.getPaint();
+		
+		if (ApolloSystem.useQualityGraphics) {
+
+			GradientPaint gp = new GradientPaint(
+					(getWidth() / 2), (int)(getHeight() * 0.8), backColor,
+					(getWidth() / 2), 0, backColorHighlights);
+			g.setPaint(gp);
+			
+		} else {
+			g.setColor(backColor);
+		}
+
+		g.fillRect(0, 0, getWidth(), getHeight());
+
+		if (ApolloSystem.useQualityGraphics) {
+			g.setPaint(restore);
+		}
+		
+		// Paint sections
+		for (BackSection bs : backingSections) {
+			bs.paint(g);
+		}
+		
+	}
+
+	private void paintRenderProgress(Graphics g) {
+		
+		g.setColor(Color.BLACK);
+		
+		g.setFont(RENDER_MESSAGE_FONT);
+		
+		String message = (renderTask.isRendering()) ? "Rendering..." : "Pending...";
+/*
+		Area clip = FrameGraphics.getCurrentClip();
+		Shape clipBackUp = g.getClip();
+		Area tmpClip = (clip != null) ? clip : 
+			new Area(new Rectangle(0, 0, 
+					Browser._theBrowser.getContentPane().getWidth(), 
+					Browser._theBrowser.getContentPane().getHeight()));
+		
+		Rectangle r = getBounds();
+		r.translate(expediteePosition.x, expediteePosition.y);
+		tmpClip.intersect(new Area(r));*/
+
+	//	if (!tmpClip.isEmpty()) {
+			
+			//g.setClip(tmpClip);
+			g.drawString(message, (getWidth() / 2) - 40, 
+					(getHeight() / 2) + (RENDER_MESSAGE_FONT.getSize() / 2)); // center roughly
+		//	g.setClip(clipBackUp);
+		//}
+
+	}
+
+	
+
+	/**
+	 * At a given x position in the graph, the matching frame number is returned.
+	 * This considers the current timescale
+	 * 
+	 * @param x
+	 * 
+	 * @return Frame in track model
+	 */
+	public int frameAtX(int x) {
+		return frameAtX(x, timescaleFrameStart, timescaleFrameLength);
+	}
+	
+	private int frameAtX(int x, int frameStart, int frameLength) {
+		
+		double viewPortPercent = x;
+		viewPortPercent /= getWidth();
+		
+		// clamp
+		int frame = (int)(frameLength * viewPortPercent);
+		frame += frameStart;
+		
+		if (frame >= (frameStart + frameLength)) 
+			frame = frameStart + frameLength;
+		else if(frame < frameStart) 
+			frame = frameStart;
+
+		return frame;
+	}
+	
+	
+
+	/**
+	 * At a given frame position in the track model, the matching x coordinate in the graph is returned.
+	 * This considers the current timescale. Clamped to graph bounds.
+	 * 
+	 * @param frame
+	 * 
+	 * @return X pos in graph
+	 */
+	public int XatFrame(long frame) {
+		return XatFrame(frame, timescaleFrameStart, timescaleFrameLength);
+	}
+	
+	private int XatFrame(long frame, long frameStart, long frameLength) {
+		
+		double framePercent = frame - frameStart;
+		framePercent /= frameLength;
+		
+		int x =  (int)(getWidth() * framePercent);
+		
+		// clamp
+		if (x > getWidth()) x = getWidth();
+		else if (x < 0) x = 0;
+
+		return x;
+	}
+	
+	
+	/**
+	 * A back section of a graph... intended for ranged highlighting.
+	 * 
+	 * @author Brook Novak
+	 */
+	protected class BackSection {
+
+		int left;
+		
+		/** If less or equal to 1, then nothing is drawn. */
+		int width;
+		
+		/** A flag for showing/hiding backing */
+		boolean visible;
+		Color color;
+		
+		BackSection() {
+			left = 0;
+			width = 0;
+			visible = false;
+			color = Color.BLACK;
+		}
+	
+		void paint(Graphics g) {
+			if (visible && width > 1) {
+				g.setColor(color);
+				g.fillRect(left, 0, width, getHeight());
+			}
+		}
+		
+		/**
+		 * Invoked when panel resized
+		 */
+		void updateBounds() {
+		}
+		
+	}
+	
+	/**
+	 * A SampledTrackGraphView can have a EffecientInvalidator where instead of
+	 * invalidating via swing AWT event it invalidates via the implementor of
+	 * a EffecientInvalidator if one is set.
+	 * 
+	 * @author Brook Novak
+	 *
+	 */
+	public interface EffecientInvalidator {
+		public void onGraphDirty(SampledTrackGraphView graph, Rectangle dirty);
+	}
+
+	public Color getBackColor() {
+		return backColor;
+	}
+
+	/**
+	 * Changes the background color and invalidates the graph.
+	 * Doesnt invalidate if the color has not changed.
+	 * 
+	 * @param backColor
+	 * 		The new Background color. Must not be null.
+	 * 	
+	 */
+	public void setBackColor(Color backColor, Color highlights) {
+		setBackColor(backColor, highlights, true);
+	}
+	
+	/**
+	 * Changes the background color.
+	 * 
+	 * @param backColor
+	 * 		The new Background color. Must not be null.
+	 * 
+	 * @param invalidate
+	 * 		True to invalidate the graph. False to miss invalidation.
+	 * 		Doesnt invalidate if the color has not changed.
+	 */
+	public void setBackColor(Color backColor, Color highlights, boolean invalidate) {
+		if (backColor == null) throw new NullPointerException("backColor");
+		if (this.backColor != backColor || backColorHighlights != highlights) {
+			this.backColor = backColor;
+			backColorHighlights = highlights;
+			if (invalidate) invalidateAll();
+		}
+	}
+
+	
+	
+	
+
+}
Index: /trunk/src_apollo/org/apollo/gui/SampledTrackGraphViewPort.java
===================================================================
--- /trunk/src_apollo/org/apollo/gui/SampledTrackGraphViewPort.java	(revision 315)
+++ /trunk/src_apollo/org/apollo/gui/SampledTrackGraphViewPort.java	(revision 315)
@@ -0,0 +1,651 @@
+package org.apollo.gui;
+
+import java.awt.BasicStroke;
+import java.awt.Color;
+import java.awt.Event;
+import java.awt.Font;
+import java.awt.FontMetrics;
+import java.awt.Graphics;
+import java.awt.Graphics2D;
+import java.awt.Stroke;
+import java.awt.event.ComponentEvent;
+import java.awt.event.ComponentListener;
+import java.awt.event.KeyEvent;
+import java.awt.event.KeyListener;
+import java.awt.event.MouseEvent;
+import java.awt.event.MouseListener;
+import java.awt.event.MouseMotionListener;
+import java.awt.event.MouseWheelEvent;
+import java.awt.event.MouseWheelListener;
+import java.awt.geom.Rectangle2D;
+import java.awt.image.BufferedImage;
+import java.util.LinkedList;
+import java.util.List;
+
+import org.apollo.audio.ApolloSubjectChangedEvent;
+import org.apollo.audio.SampledTrackModel;
+import org.apollo.mvc.Subject;
+import org.apollo.mvc.SubjectChangedEvent;
+import org.apollo.util.AudioMath;
+import org.expeditee.gui.Browser;
+
+public class SampledTrackGraphViewPort extends SampledTrackGraphView
+	implements MouseListener, MouseMotionListener, MouseWheelListener, KeyListener {
+
+	private static final long serialVersionUID = 1L;
+	
+	private static final int ZOOM_SELECTION_PIXEL_RANGE_THRESHOLD = 1;
+	private static final int COARSE_PAN_PIXEL_LENGTH = 14;
+	private static final int SCROLL_ZOOM_PIXEL_LENGTH = 10; // for scroll-wheel
+	
+	private static final int ZOOM_ADJUSTMENT_MODE_NONE = 0;
+	private static final int ZOOM_ADJUSTMENT_MODE_REZOOM = 1;
+	private static final int ZOOM_ADJUSTMENT_MODE_PAN = 2;
+	private static final int ZOOM_ADJUSTMENT_MODE_MODIFYZOOM = 3;
+	
+	public static final Color ZOOM_BACKING_COLOR_NORMAL = new Color(251, 255, 168);
+	private static final Color ZOOM_BACKING_COLOR_HIGHLIGHT = new Color(255, 255, 224);
+	private static final Color ZOOM_BACKING_BORDER_COLOR = new Color(215, 219, 144);
+	public static final Stroke ZOOM_BACKING_BORDER_STROKE = new BasicStroke(1.0f);
+	
+	public static final Font TIME_HELPER_FONT = new Font("Arial", Font.PLAIN, 12);
+	public static final Color TIME_HELPER_COLOR = new Color(132, 175, 201);
+	public static final Color TIME_HELPER_BORDERCOLOR = new Color(111, 146, 168);
+	
+	// displays the current zoom or the new zoom-selection
+	// (actual zoom is not set until mouse released
+	private ZoomBackSection zoomBackSection;
+
+	private int zoomAdjustmentMode = ZOOM_ADJUSTMENT_MODE_NONE;
+	private int adjustmentStartX = 0;
+	private int adjustmentEndX = 0;
+	private int panOffset = 0;
+
+	// The model data
+	private int zoomStartFrame = -1;
+	private int zoomEndFrame = -1;
+	
+	private List<ZoomChangeListener> zoomChangeListeners = new LinkedList<ZoomChangeListener>();
+
+	public SampledTrackGraphViewPort() {
+		super();
+		
+		setBackColor(new Color(250, 250, 250), new Color(230, 230, 230));
+		setAlwaysFullView(true);
+
+		// Create back sections
+		zoomBackSection = new ZoomBackSection();
+		addBackingSection(zoomBackSection);
+		
+		addMouseListener(this);
+		addMouseMotionListener(this);
+		addMouseWheelListener(this);
+		addKeyListener(this);
+		this.addComponentListener(new ComponentListener() {
+
+			public void componentHidden(ComponentEvent e) {
+			}
+
+			public void componentMoved(ComponentEvent e) {
+			}
+
+			public void componentResized(ComponentEvent e) {
+				if (getSampledTrackModel() == null) return;
+				
+				// Initialization
+				if (zoomStartFrame < 0 || zoomEndFrame < 0) {
+					
+					fullyZoomOut();
+					
+				} else {
+					
+					// Update backing
+					zoomBackSection.updateBounds(false);
+				}
+			}
+
+			public void componentShown(ComponentEvent e) {
+			}
+		
+		});
+	}
+
+	public void addZoomChangeListener(ZoomChangeListener zcl) {
+		if (!zoomChangeListeners.contains(zcl))
+			zoomChangeListeners.add(zcl);
+	}
+
+	@Override
+	public void setObservedSubject(Subject parent) {
+		super.setObservedSubject(parent);
+		
+		if (parent != null && parent instanceof SampledTrackModel) {
+			fullyZoomOut();
+		}
+	}
+	
+	public void fullyZoomOut() 
+	{
+		if (getSampledTrackModel() == null) return;
+		setZoom(0, getSampledTrackModel().getFrameCount());
+	}
+
+	/**
+	 * Sets the current zoom. In frames.
+	 * Invalidates
+	 * 
+	 * @param startFrame
+	 * 
+	 * @param endFrame
+	 * 
+	 */
+	public void setZoom(int startFrame, int endFrame) {
+		if (getSampledTrackModel() == null) return;
+	
+		if (startFrame < 0) 
+			startFrame = 0;
+		if (startFrame > getSampledTrackModel().getFrameCount()) 
+			startFrame = getSampledTrackModel().getFrameCount();
+		
+		if (endFrame < 0) 
+			endFrame = 0;
+		if (endFrame > getSampledTrackModel().getFrameCount()) 
+			endFrame = getSampledTrackModel().getFrameCount();
+		
+		// Anything different?
+		if (startFrame == zoomStartFrame && endFrame == zoomEndFrame)
+			return;
+		
+		zoomStartFrame = startFrame;
+		zoomEndFrame = endFrame;
+		
+		// Update backing
+		zoomBackSection.updateBounds(false);
+		
+		invalidateAll();
+		
+		// Fire model changed event
+		Event e = new Event(this, 0, null);
+		for (ZoomChangeListener zcl : zoomChangeListeners)
+			zcl.zoomChanged(e);
+		
+	}
+	
+	public int getZoomStartFrame() {
+		return this.zoomStartFrame;
+	}
+	
+	public int getZoomEndFrame() {
+		return this.zoomEndFrame;
+	}
+	
+	public void keyPressed(KeyEvent e) {
+		if (getSampledTrackModel() == null) return;
+		
+		// Fine pan the zoom
+		if (e.getKeyCode() == KeyEvent.VK_LEFT && Math.min(zoomStartFrame, zoomEndFrame) > 0) {
+
+			float ftmp = (float)COARSE_PAN_PIXEL_LENGTH / (float)getWidth();
+			int pan = (int)(Math.abs(zoomStartFrame - zoomEndFrame) *  ftmp);
+			if (pan == 0) pan = 1;
+			
+			setZoom(zoomStartFrame - pan, zoomEndFrame - pan);
+			
+		} else if (e.getKeyCode() == KeyEvent.VK_RIGHT && 
+				Math.max(zoomStartFrame, zoomEndFrame) < getSampledTrackModel().getFrameCount()) {
+			
+			float ftmp = (float)COARSE_PAN_PIXEL_LENGTH / (float)getWidth();
+			int pan = (int)(Math.abs(zoomStartFrame - zoomEndFrame) *  ftmp);
+			if (pan == 0) pan = 1;
+			
+			setZoom(zoomStartFrame + pan, zoomEndFrame + pan);
+		}
+		
+		// Fine zoom the view
+		else if (e.getKeyCode() == KeyEvent.VK_UP) {
+			zoom(-1);
+		} else if (e.getKeyCode() == KeyEvent.VK_DOWN) {
+			zoom(1);
+		}
+	
+	}
+
+	public void keyReleased(KeyEvent e) {
+	}
+
+	public void keyTyped(KeyEvent e) {
+	}
+
+	public void mouseClicked(MouseEvent e) {
+		if (e.getClickCount() >= 2) { // zoom out fully on double click
+			fullyZoomOut();
+		}
+	}
+	
+	/**
+	 * Zooms in/out of viewport...
+	 * 
+	 * @param times
+	 * 		How many zooms to do. Negative for zoom in. Positive for zoom out
+	 */
+	private void zoom(int times) {
+		
+		if (getSampledTrackModel() == null || times == 0) return;
+		
+		float ftmp = (float)SCROLL_ZOOM_PIXEL_LENGTH / (float)getWidth();
+		int zoom = (int)(getTimeScaleLength() *  ftmp * times);
+		
+		if (Math.abs(zoom) > Math.abs(zoomStartFrame - zoomEndFrame)) {
+			
+			if (times < 0) {
+				zoom = Math.abs(zoomStartFrame - zoomEndFrame) / 2;
+				zoom *= -1;
+			} else {
+				zoom = Math.abs(zoomStartFrame - zoomEndFrame) * 2;
+			}
+		}
+		
+		if (zoom == 0) zoom = 1;
+
+		int start = 0, end = 0;
+		
+		if (zoomStartFrame < zoomEndFrame) {
+			
+			start = zoomStartFrame - (zoom / 2);
+			end = zoomEndFrame + (zoom / 2);
+			
+		} else {
+			
+			start = zoomStartFrame + (zoom / 2);
+			end = zoomEndFrame - (zoom / 2);
+			
+		}
+
+		setZoom(start, end);
+		
+	}
+
+	public void mouseWheelMoved(MouseWheelEvent e) {
+		zoom(e.getWheelRotation());
+	}
+
+	public void mouseEntered(MouseEvent e) {
+	}
+
+
+	public void mouseExited(MouseEvent e) {
+	}
+
+	public void mousePressed(MouseEvent e) {
+		if (getSampledTrackModel() == null) return;
+		
+		// set selection mode
+		if (e.getButton() == MouseEvent.BUTTON1)  { // re-zoom from scratch
+			setZoomAdjustmentMode(ZOOM_ADJUSTMENT_MODE_REZOOM);
+			adjustmentStartX = adjustmentEndX = e.getX();
+			
+		} else if(e.getButton() == MouseEvent.BUTTON2) { 
+			
+			int adjustStart = XatFrame(zoomStartFrame);
+			int adjustEnd = XatFrame(zoomEndFrame);
+			
+			// Only start panning if mouse over zoom area
+			if (e.getX() >= Math.min(adjustStart, adjustEnd)
+					&& e.getX() <= Math.max(adjustStart, adjustEnd)) {
+				
+				adjustmentStartX = adjustStart;
+				adjustmentEndX = adjustEnd;
+				
+				setZoomAdjustmentMode(ZOOM_ADJUSTMENT_MODE_PAN);
+				panOffset = e.getX() -  Math.min(adjustmentStartX, adjustmentEndX);
+				assert(panOffset >= 0);
+			}
+			
+		} else if(e.getButton() == MouseEvent.BUTTON3) { // add/subtract to current zoom
+			
+			setZoomAdjustmentMode(ZOOM_ADJUSTMENT_MODE_MODIFYZOOM);
+			
+			adjustmentStartX = XatFrame(zoomStartFrame);
+			adjustmentEndX = XatFrame(zoomEndFrame);
+			
+			
+			if (e.isControlDown()) {
+				subtractCurrentZoomSelection(e.getX());
+			} else {
+				addCurrentZoomSelection(e.getX());
+			}
+			
+		} else {
+			setZoomAdjustmentMode(ZOOM_ADJUSTMENT_MODE_NONE);
+		}
+		
+		if (zoomAdjustmentMode != ZOOM_ADJUSTMENT_MODE_NONE) {
+			zoomBackSection.updateBounds(true);
+			invalidateAll();
+		}
+
+	}
+
+	public void mouseReleased(MouseEvent e) {
+		if (getSampledTrackModel() == null) return;
+
+		// Set the actual zoom only on mouse release events
+		if (zoomAdjustmentMode != ZOOM_ADJUSTMENT_MODE_NONE) {
+			
+			setZoomAdjustmentMode(ZOOM_ADJUSTMENT_MODE_NONE);
+			
+			if (Math.abs(adjustmentStartX - adjustmentEndX) >= ZOOM_SELECTION_PIXEL_RANGE_THRESHOLD)
+				setZoom(frameAtX(adjustmentStartX), frameAtX(adjustmentEndX));
+			else invalidateAll();
+
+		}
+		
+	}
+
+
+	public void mouseDragged(MouseEvent e) {
+		if (getSampledTrackModel() == null) return;
+		
+		switch (zoomAdjustmentMode) {
+		case ZOOM_ADJUSTMENT_MODE_REZOOM:
+			adjustmentEndX = e.getX();
+			break;
+			
+		case ZOOM_ADJUSTMENT_MODE_PAN:
+
+			assert(panOffset >= 0);
+			
+			int len = Math.abs(adjustmentEndX - adjustmentStartX);
+			
+			if (adjustmentStartX < adjustmentEndX) {
+				adjustmentStartX = e.getX() - panOffset;
+				adjustmentEndX = adjustmentStartX + len;
+					
+				// Clamp
+				if (adjustmentStartX < 0) {
+					adjustmentStartX = 0;
+					adjustmentEndX = len;
+				}
+				
+				else if (adjustmentEndX > getWidth()) {
+					adjustmentEndX = getWidth();
+					adjustmentStartX = adjustmentEndX - len;
+				}
+				
+			} else {
+				adjustmentEndX = e.getX() - panOffset;
+				adjustmentStartX = adjustmentEndX + len;
+					
+				// Clamp
+				if (adjustmentEndX < 0) {
+					adjustmentEndX = 0;
+					adjustmentStartX = len;
+				}
+				
+				else if (adjustmentStartX > getWidth()) {
+					adjustmentStartX = getWidth();
+					adjustmentEndX = adjustmentStartX - len;
+				}
+			}
+			
+			
+			break;
+			
+		case ZOOM_ADJUSTMENT_MODE_MODIFYZOOM:
+			if (e.isControlDown()) {
+				subtractCurrentZoomSelection(e.getX());
+			} else {
+				addCurrentZoomSelection(e.getX());
+			}
+			break;
+		}
+		
+		if (zoomAdjustmentMode != ZOOM_ADJUSTMENT_MODE_NONE) {
+			zoomBackSection.updateBounds(true);
+			invalidateAll();
+		}
+
+	}
+	
+
+	public void mouseMoved(MouseEvent e) {
+		
+		// If missed a mouse release event must reset the adjustment state...
+		if (e.getButton() == MouseEvent.NOBUTTON 
+				&& zoomAdjustmentMode != ZOOM_ADJUSTMENT_MODE_NONE) {
+			setZoomAdjustmentMode(ZOOM_ADJUSTMENT_MODE_NONE);
+			invalidateAll();
+		}
+		
+	}
+	
+	private void addCurrentZoomSelection(int x) {
+		
+		if (adjustmentStartX < adjustmentEndX) {
+			
+			if (x < adjustmentStartX) // expand to left
+				adjustmentStartX = x;
+			
+			else if (x > adjustmentEndX) // expand to right
+				adjustmentEndX = x;
+			
+		} else {
+			
+			if (x < adjustmentEndX) // expand to left
+				adjustmentEndX = x;
+			
+			else if (x > adjustmentStartX) // expand to right
+				adjustmentStartX = x;
+			
+		}
+	}
+	
+	private void subtractCurrentZoomSelection(int x) {
+		
+		if (adjustmentStartX < adjustmentEndX) {
+			
+			if (x > adjustmentStartX) // shrink to left
+				adjustmentStartX = x;
+			
+			else if (x < adjustmentEndX) // shrink to right
+				adjustmentEndX = x;
+			
+		} else {
+			
+			if (x > adjustmentEndX) // shrink to right
+				adjustmentEndX = x;
+			
+			else if (x < adjustmentStartX) // shrink to left
+				adjustmentStartX = x;
+		}
+		
+	}
+
+
+	@Override
+	public void modelChanged(Subject source, SubjectChangedEvent event) {
+		super.modelChanged(source, event);
+		
+		if (getSampledTrackModel() == null) return;
+
+
+		switch(event.getID()) {
+
+			case ApolloSubjectChangedEvent.AUDIO_INSERTED:
+			case ApolloSubjectChangedEvent.AUDIO_REMOVED:
+				zoomBackSection.updateBounds(false);
+				break;
+				
+		}
+	}
+	
+	
+	/**
+	 * Use this for setting the ajudstment mode.
+	 * Updates GUI. But does not Invalidate.
+	 * 
+	 * @param mode
+	 */
+	private void setZoomAdjustmentMode(int mode) {
+		
+		zoomAdjustmentMode = mode;
+		
+		if (mode == ZOOM_ADJUSTMENT_MODE_NONE) {
+			zoomBackSection.updateBounds(false);
+		} else if (mode == ZOOM_ADJUSTMENT_MODE_REZOOM) {
+			
+		} else if (mode == ZOOM_ADJUSTMENT_MODE_PAN) {
+			
+		} else if (mode == ZOOM_ADJUSTMENT_MODE_MODIFYZOOM) {
+			
+		} else {
+			assert(false);
+		}
+
+	}
+
+	@Override
+	public void paint(Graphics g) {
+		super.paint(g);
+		zoomBackSection.postPaint(g);
+	}
+	
+	
+	private class ZoomBackSection extends BackSection
+	{
+		
+		private String startTimeHelpLabel = null;
+		private String endTimeHelpLabel = null;
+		private int labelStartX = 0;
+		private int labelEndX = 0;
+		private int labelStartY = 0;
+		private int labelEndY = 0;
+		private int labelWidth = 0;
+		private int labelHeight = 0;
+		
+		public ZoomBackSection() {
+			super();
+			visible = true;
+		}
+		
+		@Override
+		void paint(Graphics g) {
+			
+			color =  (zoomAdjustmentMode == ZOOM_ADJUSTMENT_MODE_NONE) ?
+					ZOOM_BACKING_COLOR_NORMAL : ZOOM_BACKING_COLOR_HIGHLIGHT;
+			super.paint(g);
+			
+			g.setColor(ZOOM_BACKING_BORDER_COLOR);
+			((Graphics2D)g).setStroke(ZOOM_BACKING_BORDER_STROKE);
+			g.drawLine(left, 0, left, getHeight());
+			g.drawLine(left + width, 0, left + width, getHeight());
+			
+		}
+		
+		void postPaint(Graphics g) {
+			// Paint little helper labels when ajusting view port
+			if (startTimeHelpLabel != null && endTimeHelpLabel != null &&
+					zoomAdjustmentMode != ZOOM_ADJUSTMENT_MODE_NONE) {
+				
+				g.setColor(TIME_HELPER_COLOR);
+				g.fillRect(labelStartX - 2, labelStartY - labelHeight + 2, labelWidth + 3, labelHeight);
+				g.fillRect(labelEndX - 2, labelEndY  - labelHeight + 2, labelWidth + 3, labelHeight);
+				
+				g.setColor(TIME_HELPER_BORDERCOLOR);
+				((Graphics2D)g).setStroke(ZOOM_BACKING_BORDER_STROKE);
+				g.drawRect(labelStartX - 2, labelStartY - labelHeight + 2, labelWidth + 3, labelHeight);
+				g.drawRect(labelEndX - 2, labelEndY  - labelHeight + 2, labelWidth + 3, labelHeight);
+
+				g.setFont(TIME_HELPER_FONT);
+				g.setColor(Color.BLACK);
+				g.drawString(startTimeHelpLabel, labelStartX, labelStartY);
+				g.drawString(endTimeHelpLabel, labelEndX, labelEndY);
+				
+			}
+		}
+		
+		void updateBounds(boolean toAdjust) {
+			if (toAdjust) {
+				left = Math.min(adjustmentStartX, adjustmentEndX);
+				
+				width = Math.abs(adjustmentStartX - adjustmentEndX);
+				
+				updateHelperLabels();
+				
+			} else {
+				left = XatFrame(Math.min(zoomStartFrame, zoomEndFrame));
+				
+				width = Math.abs(XatFrame(zoomStartFrame) - XatFrame(zoomEndFrame));
+			}
+			
+			if (width < 3) width = 3;
+		}
+		
+		private void updateHelperLabels() {
+			if (getSampledTrackModel() == null) return;
+			
+			Graphics g = null;
+			try {
+			
+				// create time stings
+				long startms = AudioMath.framesToMilliseconds(
+						frameAtX(Math.min(adjustmentStartX, adjustmentEndX)), 
+						getSampledTrackModel().getFormat());
+				
+				long endms = AudioMath.framesToMilliseconds(
+						frameAtX(Math.max(adjustmentStartX, adjustmentEndX)), 
+						getSampledTrackModel().getFormat());
+
+				startTimeHelpLabel = createHelperLabel(startms);
+				endTimeHelpLabel = createHelperLabel(endms);
+				
+				assert(startTimeHelpLabel != null);
+				assert(endTimeHelpLabel != null);
+				
+				// Create a temp graphics for centered label positioning
+				if (Browser._theBrowser != null && Browser._theBrowser.g != null) {
+					g = Browser._theBrowser.g.create();
+				} else {
+					g = new BufferedImage(1,1,BufferedImage.TYPE_BYTE_INDEXED).getGraphics();
+				}
+		
+				// Position labels
+				FontMetrics fm   = g.getFontMetrics(TIME_HELPER_FONT);
+				Rectangle2D rect = fm.getStringBounds(startTimeHelpLabel, g);
+				
+				labelWidth = (int)rect.getWidth();
+				labelHeight = (int)rect.getHeight();
+				
+				labelStartX = Math.min(adjustmentStartX, adjustmentEndX) - labelWidth - 5;
+				labelEndX = Math.max(adjustmentStartX, adjustmentEndX) + 5;
+				
+				labelStartY = labelEndY = (getHeight() >> 1) + (labelHeight >> 1);
+
+			} finally {
+				if (g != null)
+					g.dispose();
+			}
+			
+		}
+		
+		/**
+		 * @param time
+		 * @return Never null
+		 */
+		private String createHelperLabel(long time) {
+			assert(getSampledTrackModel() != null);
+			
+			long totalMS = AudioMath.framesToMilliseconds(getSampledTrackModel().getFrameCount(),
+					getSampledTrackModel().getFormat());
+			
+			
+			Long mins = (totalMS < 60000) ? null : time / 60000;
+			
+			Long secs = (totalMS < 1000) ? null : (time / 1000) % 60;
+
+			Long ms = time % 1000;
+			
+			return TimeAxis.createTimeLabel(mins, secs, ms);
+		}
+	}
+}
Index: /trunk/src_apollo/org/apollo/gui/SoundDeskPopup.java
===================================================================
--- /trunk/src_apollo/org/apollo/gui/SoundDeskPopup.java	(revision 315)
+++ /trunk/src_apollo/org/apollo/gui/SoundDeskPopup.java	(revision 315)
@@ -0,0 +1,428 @@
+package org.apollo.gui;
+
+/**
+ * 
+ * @author Brook Novak
+ * @deprecated
+ */
+public class SoundDeskPopup {
+
+	
+	//
+//		
+//		/**
+//		 * Why doesn't java support partial classes!
+//		 * 
+//		 * @author Brook Novak
+//		 *
+//		 */
+//		private class SoundDeskPopup extends Popup implements ActionListener {
+//			
+//			private JButton playPauseButton;
+//			private JButton stopButton;
+//			private JButton rewindButton;
+//			private JButton closeButton;
+//			private JToggleButton soloButton;
+//			private JToggleButton muteButton;
+//			private JSlider volumeSlider;
+//			private JPanel nameLabelParent;
+//			private JTree trackTree;
+//			private JButton scrollLeftButton;
+//			private JButton scrollRightButton;
+//			private JPanel channelPane;
+//			
+//			private EmulatedTextItem nameLabel; 
+//			
+//			private boolean isUpdatingGUI = false;
+//			
+//			/**
+//			 * Initializes the GUI
+//			 */
+//			SoundDeskPopup() {
+//				
+//				super(new BorderLayout());
+	//
+//				JPanel toolbar = createToolbar();
+//				
+//				JPanel optionsPane = createOptionsPane();
+//				
+//				JSplitPane centerStage = new JSplitPane(JSplitPane.HORIZONTAL_SPLIT);
+//				
+//				
+//				
+//			}
+//			
+//			/**
+//			 * Creates the "toolbar" like appearance at the top
+//			 * 
+//			 * @return
+//			 * 		The JPanel containing the toolbar buttons.
+//			 */
+//			private JPanel createToolbar() {
+//				
+//				final int BUTTON_SIZE = 40;
+//				
+//				playPauseButton = new JButton();
+//				playPauseButton.addActionListener(this);
+//				playPauseButton.setIcon(IconRepository.getIcon("play.png"));
+//				playPauseButton.setPreferredSize(new Dimension(BUTTON_SIZE, BUTTON_SIZE));
+//				playPauseButton.setToolTipText("Play selection / Pause");
+//				
+//				stopButton = new JButton();
+//				stopButton.setEnabled(false);
+//				stopButton.addActionListener(this);
+//				stopButton.setIcon(IconRepository.getIcon("stop.png"));
+//				stopButton.setPreferredSize(new Dimension(BUTTON_SIZE, BUTTON_SIZE));
+//				stopButton.setToolTipText("Stop playback");
+//				
+//				rewindButton = new JButton();
+//				rewindButton.addActionListener(this);
+//				rewindButton.setIcon(IconRepository.getIcon("rewind.png"));
+//				rewindButton.setPreferredSize(new Dimension(BUTTON_SIZE, BUTTON_SIZE));
+//				rewindButton.setToolTipText("Rewind to start");
+//				
+//				// Icon changes
+//				muteButton = new JToggleButton();
+//				muteButton.setSelectedIcon(IconRepository.getIcon("volmute.png"));
+//				muteButton.setPreferredSize(new Dimension(BUTTON_SIZE, BUTTON_SIZE));
+//				muteButton.setToolTipText("Toggle mute");
+//				muteButton.addChangeListener(new ChangeListener() {
+//					public void stateChanged(ChangeEvent e) {
+//						if (!isUpdatingGUI) {
+//							masterMix.setMuted(muteButton.isSelected());
+//							
+//						}
+//					}
+//				});
+//				
+//				soloButton = new JToggleButton();
+//				soloButton.setIcon(IconRepository.getIcon("solo.png"));
+//				soloButton.setSelectedIcon(IconRepository.getIcon("soloon.png"));
+//				soloButton.setPreferredSize(new Dimension(BUTTON_SIZE, BUTTON_SIZE));
+//				soloButton.setToolTipText("Toggle solo");
+//				soloButton.addChangeListener(new ChangeListener() {
+//					public void stateChanged(ChangeEvent e) {
+//						if (!isUpdatingGUI) {
+//							SoundDesk.getInstance().setSoloIDPrefix(soloButton.isSelected() ?
+//									masterMix.getChannelID() : null
+//									);
+//							
+//						}
+//					}
+//				});
+//				
+//				closeButton = new JButton();
+//				closeButton.addActionListener(this);
+//				closeButton.setIcon(IconRepository.getIcon("close.png"));
+//				closeButton.setPreferredSize(new Dimension(BUTTON_SIZE, BUTTON_SIZE));
+//				closeButton.setToolTipText("Close");
+//				
+//				final int VOLUME_SPACING = 6;
+//				volumeSlider = new JSlider(JSlider.HORIZONTAL);
+//				volumeSlider.setMinimum(0);
+//				volumeSlider.setMaximum(100);
+//				volumeSlider.addChangeListener(new ChangeListener() {
+//					public void stateChanged(ChangeEvent e) {
+//						if (!isUpdatingGUI) {
+//							masterMix.setVolume(((float)volumeSlider.getValue()) / 100.0f);
+//						}
+//						// Update the icons
+//						updateButtonGUI();
+//					}
+//				});
+//				
+//				volumeSlider.setPreferredSize(new Dimension(100 - (2 * VOLUME_SPACING), BUTTON_SIZE));
+//				
+	//
+//				nameLabelParent = new JPanel();
+//				
+//				nameLabelParent.addMouseListener(new MouseListener() {
+	//
+//					public void mouseClicked(MouseEvent e) {
+//						if (nameLabel != null) {
+//							if (nameLabel.onMouseClicked(e)) {
+//								e.consume();
+//								return;
+//							}
+//						}
+//					}
+	//
+//					public void mouseEntered(MouseEvent e) {
+//					}
+	//
+//					public void mouseExited(MouseEvent e) {
+//					}
+	//
+//					public void mousePressed(MouseEvent e) {
+//						if (nameLabel != null) {
+//							if (nameLabel.onMousePressed(e)) {
+//								e.consume();
+//							}
+//						}
+//					}
+	//
+//					public void mouseReleased(MouseEvent e) {
+//						if (nameLabel != null) {
+//							if (nameLabel.onMouseReleased(e)) {
+//								e.consume();
+//							}
+//						}
+//					}
+//				
+//				});
+//				
+//				nameLabelParent.addMouseMotionListener(new MouseMotionListener() {
+	//
+//					public void mouseDragged(MouseEvent e) {
+//						if (nameLabel != null) {
+//							if (nameLabel.onMouseDragged(e)) {
+//								e.consume();
+//							}
+//						}
+//					}
+	//
+//					public void mouseMoved(MouseEvent e) {
+//						if (nameLabel != null) {
+//							nameLabel.onMouseMoved(e, nameLabelParent);
+//						}
+//					}
+	//
+//				});
+//				
+//				nameLabelParent.addKeyListener(new KeyListener() {
+	//
+//					public void keyPressed(KeyEvent e) {
+//						if (nameLabel != null) {
+//							if (nameLabel.onKeyPressed(e, nameLabelParent)) {
+//								e.consume();
+//							}
+//						}
+//					}
+	//
+//					public void keyReleased(KeyEvent e) {
+//						if (nameLabel != null) {
+//							if (nameLabel.onKeyReleased(e, nameLabelParent)) {
+//								e.consume();
+//							}
+//						}
+	//
+//					}
+	//
+//					public void keyTyped(KeyEvent e) {
+//					}
+//					
+//				});
+//				
+//				nameLabel = new EmulatedTextItem(nameLabelParent, new Point(10, 25));
+//				nameLabel.setFontStyle(Font.BOLD);
+//				nameLabel.setFontSize(16);
+//				nameLabel.setBackgroundColor(Color.WHITE);	
+//				nameLabel.setText(LinkedTrack.this.getName()); 
+	//
+//				nameLabel.addTextChangeListener(new TextChangeListener() { //  a little bit loopy!
+	//
+//					public void onTextChanged(Object source, String newLabel) {
+//						if (!nameLabel.getText().equals(LinkedTrack.this.getName())) {
+//							SoundDeskPopup.this.setName(nameLabel.getText());
+//						}
+//					}
+	//
+//				});
+//				
+//				// Create the toolbar
+//				JPanel toolBarPanel = new JPanel(new GridBagLayout());
+	//
+//				GridBagConstraints c = new GridBagConstraints();
+//				c.gridx = 0;
+//				c.gridy = 0;
+//				c.fill = GridBagConstraints.BOTH;
+//				toolBarPanel.add(playPauseButton, c);
+//				
+//				c = new GridBagConstraints();
+//				c.gridx = 1;
+//				c.gridy = 0;
+//				c.fill = GridBagConstraints.BOTH;
+//				toolBarPanel.add(stopButton, c);
+//				
+//				c = new GridBagConstraints();
+//				c.gridx = 2;
+//				c.gridy = 0;
+//				c.fill = GridBagConstraints.BOTH;
+//				toolBarPanel.add(rewindButton, c);
+//				
+//				c = new GridBagConstraints();
+//				c.gridx = 3;
+//				c.gridy = 0;
+//				c.fill = GridBagConstraints.BOTH;
+//				toolBarPanel.add(soloButton, c);
+//				
+//				c = new GridBagConstraints();
+//				c.gridx = 4;
+//				c.gridy = 0;
+//				c.fill = GridBagConstraints.BOTH;
+//				toolBarPanel.add(muteButton, c);
+//				
+//				c = new GridBagConstraints();
+//				c.gridx = 5;
+//				c.gridy = 0;
+//				c.fill = GridBagConstraints.BOTH;
+//				c.insets = new Insets(0,VOLUME_SPACING,0,VOLUME_SPACING);
+//				toolBarPanel.add(volumeSlider, c);
+//				
+//				c = new GridBagConstraints();
+//				c.gridx = 6;
+//				c.gridy = 0;
+//				c.fill = GridBagConstraints.BOTH;
+//				c.weightx = 1.0f;
+//				toolBarPanel.add(nameLabelParent, c);
+//				
+//				c = new GridBagConstraints();
+//				c.gridx = 7;
+//				c.gridy = 0;
+//				c.fill = GridBagConstraints.BOTH;
+//				toolBarPanel.add(closeButton, c);
+//				
+//				return toolBarPanel;
+	//
+//			}
+//			
+//			private JPanel createOptionsPane() {
+//				
+//				JPanel panel = new JPanel(new BorderLayout());
+//				
+//				JLabel tmpLabl = new JLabel("todo: Options - check boxes");
+//				tmpLabl.setPreferredSize(new Dimension(100, 30));
+//				panel.add(tmpLabl, BorderLayout.CENTER);
+//				
+//				return panel;
+//			}
+//			
+//			private JSplitPane createCenterStage() {
+//				
+//				DefaultMutableTreeNode root = new DefaultMutableTreeNode();
+//				createTreeNodes(root);
+//				trackTree = new JTree(root);
+//				
+//				
+//				scrollRightButton = new JButton(">");
+//		
+//				scrollLeftButton = new JButton("<"); 
+//				
+//				channelPane = new JPanel();
+//				
+//				
+//				JPanel leftComposite = new JPanel(new BorderLayout());
+//		
+//				JSplitPane sp = new JSplitPane(JSplitPane.HORIZONTAL_SPLIT);
+//				
+//				// todo: Wrap with scroll pane
+//				
+//				sp.setLeftComponent(trackTree);
+//				sp.setLeftComponent(trackTree);
+//				
+//				return sp;
+//			}
+//			
+//			private void createTreeNodes(DefaultMutableTreeNode root) {
+//				
+//			}
+//			
+//			public void actionPerformed(ActionEvent e) {
+//				
+//				if (!(state == PLAYING || state == STOPPED)) return;
+//				
+//				if (e.getSource() == closeButton) {
+//					
+//					// todo ... close
+//					
+//				} else {
+//					
+//					// Relay shared action
+//					masterActionListener.actionPerformed(e);
+//					
+//				}
+//				
+//			}
+//			
+//			/**
+//			 * Sets the mute icon to represent the current volume value in the slider.
+//			 * Note: this is not the icon if mute is on.
+//			 */
+//			private void updateButtonGUI() {
+//				
+//				Icon newIcon = null;
+//				if (volumeSlider.getValue() <= 25) 
+//						newIcon = IconRepository.getIcon("vol25.png");
+//				else if (volumeSlider.getValue() <= 50) 
+//					newIcon = IconRepository.getIcon("vol50.png");
+//				else if (volumeSlider.getValue() <= 75) 
+//					newIcon = IconRepository.getIcon("vol75.png");
+//				else // maxing
+//						newIcon = IconRepository.getIcon("vol100.png");
+//				
+//				muteButton.setIcon(newIcon);
+//			}
+//			
+//			/**
+//			 * Updates the volume GUI. 
+//			 * {@link #volumeChanged()} is not raised as a result of this call.
+//			 * 
+//			 * @param vol
+//			 * 		The volume ranging from 0 - 100. Clamped.
+//			 */
+//			protected void updateVolume(int vol) {
+//				
+//				if (volumeSlider.getValue() == vol) return;
+	//
+//				// Clamp
+//				if(vol < 0) vol = 0;
+//				else if (vol > 100) vol = 100;
+//				
+//				isUpdatingGUI = true;
+//				
+//				volumeSlider.setValue(vol);
+//				
+//				isUpdatingGUI = false;
+//			}
+//			
+	//
+//			/**
+//			 * Updates the mute button GUI. 
+//			 * {@link #muteChanged()} is not raised as a result of this call.
+//			 * 
+//			 * @param isMuted
+//			 * 		True if set gui to muted.
+//			 */
+//			protected void updateMute(boolean isMuted) {
+//				
+//				if (muteButton.isSelected() == isMuted) return;
+	//
+//				isUpdatingGUI = true;
+//				
+//				muteButton.setSelected(isMuted);
+	//
+//				isUpdatingGUI = false;
+//			}
+	//
+	//
+//			/**
+//			 * Updates the solo button GUI. 
+//			 * {@link #muteChanged()} is not raised as a result of this call.
+//			 * 
+//			 * @param isSolo
+//			 * 		True if set gui to solo on.
+//			 */
+//			protected void updateSolo(boolean isSolo) {
+//				
+//				if (soloButton.isSelected() == isSolo) return;
+	//
+//				isUpdatingGUI = true;
+//				
+//				soloButton.setSelected(isSolo);
+	//
+//				isUpdatingGUI = false;
+//			}
+//			
+//			
+//		}
+}
Index: /trunk/src_apollo/org/apollo/gui/TimeAxis.java
===================================================================
--- /trunk/src_apollo/org/apollo/gui/TimeAxis.java	(revision 315)
+++ /trunk/src_apollo/org/apollo/gui/TimeAxis.java	(revision 315)
@@ -0,0 +1,378 @@
+package org.apollo.gui;
+
+import java.awt.BasicStroke;
+import java.awt.Color;
+import java.awt.Font;
+import java.awt.FontMetrics;
+import java.awt.Graphics;
+import java.awt.Graphics2D;
+import java.awt.Stroke;
+import java.awt.geom.Rectangle2D;
+import java.awt.image.BufferedImage;
+import java.util.LinkedList;
+
+import org.expeditee.gui.Browser;
+
+/**
+ * A model time scale along the X axis that can be rendered.
+ * 
+ * @author Brook Novak
+ *
+ */
+public class TimeAxis {
+	
+	private static final long serialVersionUID = 1L;
+	
+	public static final Font TIME_BAR_FONT = new Font("Arial", Font.PLAIN, 14);
+	private static final Stroke TICK_STROKE = new BasicStroke(1.0f);
+	
+	private static final int DESIRED_MAJOR_TICK_PIXEL_SPACING = 100;
+	private static final int MIN_TICK_COUNT = 3; // per between majors
+
+	private String[] majorTickLabels = null;
+	private int[] majorTickXPositions = null;
+	private int[] majorTickLabelXPositions = null;
+	private int[] minorTickXPositions = null;
+	private String alternateLabel = null;
+	
+	private int width = 0;
+	private long totalTrackMSLength = 0;
+
+	public TimeAxis() {
+	}
+
+	public void setAxis(
+			long startTimeMS, 
+			long timeLengthMS, 
+			long timeLengthSamples,
+			long totalTrackMSLength, 
+			int width) {
+
+		assert(width > 0);
+		assert(startTimeMS >= 0);
+		assert((startTimeMS + timeLengthMS) <= totalTrackMSLength);
+		
+		this.width = width;
+		this.totalTrackMSLength = totalTrackMSLength;
+
+		double majorTickPixelSpacing = DESIRED_MAJOR_TICK_PIXEL_SPACING;
+		double majorTickCount = width / majorTickPixelSpacing;
+		double timeAggregation = timeLengthMS / majorTickCount;
+		
+		if (timeAggregation <= 0) { // in between ms 
+			majorTickLabels = null;
+			majorTickXPositions = null;
+			majorTickLabelXPositions = null;
+			minorTickXPositions = null;
+			alternateLabel = (timeLengthSamples >= 1) ? 
+					timeLengthSamples + " samples"
+					: " ";
+			return;
+		}
+
+		// Re-adjust majorTickCount and aggregationSize to nice values
+		// update majorTickPixelSpacing.
+		
+		// so users can follow the axis more easily
+		boolean agregationAdjusted = true;
+		
+		if (timeAggregation > ((5 * 60000) + 30000)) {
+			agregationAdjusted = false;
+		} else if (timeAggregation > ((4 * 60000) + 30000)) { // 4:30 - 5:30
+			timeAggregation = 5 * 60000;
+		} else if (timeAggregation > ((3 * 60000) + 30000)) { // 3:30 - 4:30
+			timeAggregation = 4 * 60000;
+		} else if (timeAggregation > ((2 * 60000) + 30000)) { // 2:30 - 3:30
+			timeAggregation = 3 * 60000;
+		} else if (timeAggregation > (60000 + 30000)) { // 1:30 - 2:30
+			timeAggregation = 2 * 60000;
+		} else if (timeAggregation > 40000) { // 0:40 - 1:30
+			timeAggregation = 60000;
+		} else if (timeAggregation > 25000) { // 0:25 - 0:40
+			timeAggregation = 30000;
+		} else if (timeAggregation > 15000) { // 0:15 - 0:25
+			timeAggregation = 20000;
+		} else if (timeAggregation > 9000) { // 0:9 - 0:15
+			timeAggregation = 10000;
+		} else if (timeAggregation > 6500) { // 0:6.5 - 0:9
+			timeAggregation = 8000;
+		} else if (timeAggregation > 4500) { // 4.5s - 6.5s
+			timeAggregation = 5000;
+		} else if (timeAggregation > 3500) { // 3.5s - 4.5s
+			timeAggregation = 4000;
+		} else if (timeAggregation > 2500) { // 2.5s - 3.5s
+			timeAggregation = 3000;
+		} else if (timeAggregation > 1500) { // 1.5s - 2.5s
+			timeAggregation = 2000;
+		} else if (timeAggregation > 850) { // 850ms - 1.5s
+			timeAggregation = 1000;
+		} else if (timeAggregation > 600) { // 600ms - 850ms
+			timeAggregation = 750;
+		} else if (timeAggregation > 380) { // 380ms - 600ms
+			timeAggregation = 500;
+		} else if (timeAggregation > 120) { // 120ms - 380ms
+			timeAggregation = 250;
+		} else if (timeAggregation > 50) { // 50ms - 120ms
+			timeAggregation = 100;
+		} else {
+			agregationAdjusted = false;
+		}
+
+		if (agregationAdjusted) {
+			majorTickCount = timeLengthMS / timeAggregation;
+			majorTickPixelSpacing = width / majorTickCount;
+		}
+
+		// The axis starts at zero always... calc the offset for the first tick
+		
+		double currentTickTime = startTimeMS - (startTimeMS % timeAggregation);
+		if (currentTickTime < startTimeMS) {
+			currentTickTime += timeAggregation;
+		}
+		
+		
+		double offsetPortion = ((double)(startTimeMS % timeAggregation) / (double)timeAggregation);
+		if (offsetPortion > 0) offsetPortion = 1.0 - offsetPortion; 
+		double majorTickXStart = offsetPortion * majorTickPixelSpacing;
+		double currentTickX = majorTickXStart;
+		
+		// Create major x ticks
+		LinkedList<Integer> majorTickPositions = new LinkedList<Integer>();
+		LinkedList<String> majorTickLabels = new LinkedList<String>();
+		LinkedList<Integer> majorTickLabelPositions = new LinkedList<Integer>();
+	
+		Graphics g = null;
+		try {
+		
+			
+			// Create a temp graphics for centered label positioning
+			if (Browser._theBrowser != null && Browser._theBrowser.g != null) {
+				g = Browser._theBrowser.g.create();
+			} else {
+				g = new BufferedImage(1,1,BufferedImage.TYPE_BYTE_INDEXED).getGraphics();
+			}
+			
+			do {
+	
+				majorTickPositions.add(new Integer((int)currentTickX));
+
+				String label = createSpecificTimeLabel((long)currentTickTime, timeAggregation);
+				
+				majorTickLabels.add(label);
+
+				// Center label
+				FontMetrics fm   = g.getFontMetrics(TIME_BAR_FONT);
+				Rectangle2D rect = fm.getStringBounds(label, g);
+				int textWidth  = (int)(rect.getWidth());
+				
+				majorTickLabelPositions.add(new Integer((int)currentTickX - (textWidth >> 1)));
+
+				currentTickX += majorTickPixelSpacing;
+				currentTickTime += timeAggregation;
+	
+			} while (currentTickX < width);
+
+			
+		} finally {
+			if (g != null)
+				g.dispose();
+		}
+		
+		this.majorTickLabels = majorTickLabels.toArray(new String[0]);
+		
+		majorTickXPositions = new int[majorTickPositions.size()];
+		for (int i = 0; i < majorTickPositions.size(); i++)
+			majorTickXPositions[i] = majorTickPositions.get(i);
+		
+		majorTickLabelXPositions = new int[majorTickLabelPositions.size()];
+		for (int i = 0; i < majorTickLabelPositions.size(); i++)
+			majorTickLabelXPositions[i] = majorTickLabelPositions.get(i);
+		
+		
+		// Build minor ticks
+		double minorTickPixelSpacing = majorTickPixelSpacing / (MIN_TICK_COUNT + 1);
+		
+		if (minorTickPixelSpacing > 5) { // have some safety condition - avoid clutter
+		
+			// Find minor tick start X
+			double tx = majorTickXStart;
+			while (tx > 0) tx -= minorTickPixelSpacing;
+			tx += minorTickPixelSpacing;
+			if (tx == majorTickXStart) tx += minorTickPixelSpacing;
+			
+			LinkedList<Integer> minorPositions = new LinkedList<Integer>();
+			
+			// Draw the minor ticks
+			while (tx < width) {
+	
+				minorPositions.add(new Integer((int)tx));
+				
+				tx += minorTickPixelSpacing;
+	
+				// Skip major positions
+				if (((tx - majorTickXStart) % majorTickPixelSpacing) == 0) 
+					tx += minorTickPixelSpacing;
+				
+			}
+			
+			minorTickXPositions = new int[minorPositions.size()];
+			for (int i = 0; i < minorTickXPositions.length; i++) 
+				minorTickXPositions[i] = minorPositions.get(i).intValue();
+		}
+	}
+	
+	
+	public String createSpecificTimeLabel(long atTime, double timeAggregation) {
+
+		Long min = null;
+		Long sec = null;
+		Long ms = null;
+		
+		if (timeAggregation < 1000) { // omit MS if aggregation size >= 1sec
+			
+			ms = new Long((atTime) % 1000);
+			
+		}
+		
+		if (timeAggregation < 60000 &&
+				totalTrackMSLength >= 1000) { // omit sec if aggregation size >= 1min or track length < 1 sec
+			
+			sec = new Long(((atTime) / 1000) % 60);
+			
+		}
+		
+		if (totalTrackMSLength >= 60000) { // omit min if track length < 1 min
+			
+			min = new Long((atTime) / 60000);
+			
+		}
+
+		String label = createTimeLabel(min, sec, ms);
+		if (label == null) {
+			label = "00:00:00";
+		}
+		
+		return label;
+		
+	}
+	
+	public static String createTimeLabel(Long min, Long sec, Long ms) {
+		
+		if (min == null && sec == null && ms != null) {
+			return ms + "ms";
+		}
+		
+		if (min == null && sec != null && ms == null) {
+			return sec + "secs";
+		}
+		
+		if (min == null && sec != null && ms != null) {
+			String strms = ms.toString();
+			String strsec = sec.toString();
+			
+			while (strms.length() < 3) strms = "0" + strms;
+			
+			return strsec + "." + strms + "secs";
+		}
+		
+		if (min != null && sec == null && ms == null) {
+			return min + "mins";
+		}
+		
+		
+		if (min != null && ms != null) {
+			
+			if (sec == null) sec = 0L;
+			
+			String strms = ms.toString();
+			String strsec = sec.toString();
+			String strmin = min.toString();
+			
+			while (strms.length() < 3) strms = "0" + strms;
+			while (strsec.length() < 2) strsec = "0" + strsec;
+			
+			return strmin + ":" + strsec + "." + strms;
+		}
+		
+		if (min != null && sec != null && ms == null) {
+
+			String strsec = sec.toString();
+			String strmin = min.toString();
+			
+			while (strsec.length() < 2) strsec = "0" + strsec;
+			
+			return strmin + ":" + strsec;
+			
+		}
+		
+		return null; // all is null
+
+	}
+	
+	public void paint(Graphics g, int x, int y, int height, Color backgroundColor) {
+		
+		if (majorTickXPositions == null) return;
+		
+		int majorTickHeight = height / 2;
+		int minorTickHeight = height / 4;
+
+		// Draw backing
+		g.setColor(backgroundColor);
+		g.fillRect(x, y, width, height);
+		
+		g.setFont(TIME_BAR_FONT);
+		g.setColor(Color.BLACK);
+		((Graphics2D)g).setStroke(TICK_STROKE);
+		
+		if (majorTickLabels != null) {
+	
+	
+			// Draw Major ticks and labels
+			for (int i = 0; i < majorTickLabels.length; i++) {
+				
+				// Label
+				g.drawString(
+						majorTickLabels[i], 
+						x + majorTickLabelXPositions[i], 
+						y + height - 2);
+				
+				// Tick
+				g.drawLine(
+						x + majorTickXPositions[i], 
+						y, 
+						x + majorTickXPositions[i], 
+						y + majorTickHeight);
+				
+			}
+			
+			// Draw minor ticks
+			if (minorTickXPositions != null) {
+				for (int tx : minorTickXPositions) {
+					g.drawLine(
+							x + tx, 
+							y, 
+							x + tx, 
+							y + minorTickHeight);
+				}
+			}
+		
+		} else if (alternateLabel != null) {
+			
+			FontMetrics fm   = g.getFontMetrics(TIME_BAR_FONT);
+			Rectangle2D rect = fm.getStringBounds(alternateLabel, g);
+			int textWidth  = (int)(rect.getWidth());
+			
+			g.drawString(
+					alternateLabel, 
+					x + ((width >> 1) - (textWidth >> 1)), 
+					y + (height - 2));
+
+		}
+		
+	}
+
+	public int getWidth() {
+		return width;
+	}
+
+}
Index: /trunk/src_apollo/org/apollo/gui/TimeAxisPanel.java
===================================================================
--- /trunk/src_apollo/org/apollo/gui/TimeAxisPanel.java	(revision 315)
+++ /trunk/src_apollo/org/apollo/gui/TimeAxisPanel.java	(revision 315)
@@ -0,0 +1,109 @@
+package org.apollo.gui;
+
+import java.awt.Event;
+import java.awt.Graphics;
+import java.awt.event.ComponentEvent;
+import java.awt.event.ComponentListener;
+import java.awt.event.MouseEvent;
+import java.awt.event.MouseListener;
+
+import javax.sound.sampled.AudioFormat;
+import javax.swing.JPanel;
+
+import org.apollo.util.AudioMath;
+
+/**
+ * A time scale along the X axis that can be rendered.
+ * 
+ * Observes a SampledTrackGraphView - keeps time axis consistent with
+ * current time scale on the observed SampledTrackGraphView.
+ * 
+ * @author Brook Novak
+ *
+ */
+public class TimeAxisPanel extends JPanel implements TimelineChangeListener, MouseListener {
+	
+	private static final long serialVersionUID = 1L;
+	
+	private SampledTrackGraphView observedSubject;
+	
+	private TimeAxis timeAxis = new TimeAxis();
+	
+	public TimeAxisPanel(SampledTrackGraphView graph) {
+		assert (graph != null);
+		
+		observedSubject = graph;
+		graph.addTimeScaleChangeListener(this); // observe
+		
+		// Auto-adjust scale when component resizes.
+		this.addComponentListener(new ComponentListener() {
+
+			public void componentHidden(ComponentEvent e) {
+			}
+
+			public void componentMoved(ComponentEvent e) {
+			}
+
+			public void componentResized(ComponentEvent e) {
+				updateAxis();
+			}
+
+			public void componentShown(ComponentEvent e) {
+			}
+		
+		});
+		
+		updateAxis();
+	}
+	
+
+	public void timelineChanged(Event e) {
+		 updateAxis();
+	}
+	
+
+	private void updateAxis() {
+		
+		// Is track model even set yet?
+		if (observedSubject.getSampledTrackModel() == null ||
+				getWidth() == 0) return;
+		
+		AudioFormat format = observedSubject.getSampledTrackModel().getFormat();
+		
+		long startTimeMS = AudioMath.framesToMilliseconds(
+				observedSubject.getTimeScaleStart(), format);
+		
+		long timeLengthMS = AudioMath.framesToMilliseconds(
+				observedSubject.getTimeScaleLength(), format);
+		
+		long totalTrackMSLength = AudioMath.framesToMilliseconds(
+				observedSubject.getSampledTrackModel().getFrameCount(), format);
+		
+		timeAxis.setAxis(
+				startTimeMS, 
+				timeLengthMS, 
+				observedSubject.getTimeScaleLength(), 
+				totalTrackMSLength, 
+				getWidth());
+		
+		repaint();
+		
+	}
+	
+	@Override
+	public void paint(Graphics g) {
+		timeAxis.paint(g, 0, 0, getHeight(), getBackground());
+	}
+
+	/** BELOW: TRAPS MOUSE EVENTS TO GIVE FOCUS */
+	public void mouseClicked(MouseEvent e) {
+	}
+	public void mouseEntered(MouseEvent e) {
+	}
+	public void mouseExited(MouseEvent e) {
+	}
+	public void mousePressed(MouseEvent e) {
+	}
+	public void mouseReleased(MouseEvent e) {
+	}
+}
Index: /trunk/src_apollo/org/apollo/gui/TimelineChangeListener.java
===================================================================
--- /trunk/src_apollo/org/apollo/gui/TimelineChangeListener.java	(revision 315)
+++ /trunk/src_apollo/org/apollo/gui/TimelineChangeListener.java	(revision 315)
@@ -0,0 +1,7 @@
+package org.apollo.gui;
+
+import java.awt.Event;
+
+public interface TimelineChangeListener {
+	void timelineChanged(Event e);
+}
Index: /trunk/src_apollo/org/apollo/gui/VolumeControlButton.java
===================================================================
--- /trunk/src_apollo/org/apollo/gui/VolumeControlButton.java	(revision 315)
+++ /trunk/src_apollo/org/apollo/gui/VolumeControlButton.java	(revision 315)
@@ -0,0 +1,349 @@
+package org.apollo.gui;
+
+import java.awt.Color;
+import java.awt.Dimension;
+import java.awt.GridBagConstraints;
+import java.awt.GridBagLayout;
+import java.awt.Insets;
+import java.awt.Point;
+import java.awt.event.ActionEvent;
+import java.awt.event.ActionListener;
+import java.util.LinkedList;
+import java.util.List;
+
+import javax.swing.Icon;
+import javax.swing.JButton;
+import javax.swing.JSlider;
+import javax.swing.event.ChangeEvent;
+import javax.swing.event.ChangeListener;
+
+import org.apollo.io.IconRepository;
+import org.expeditee.gui.Popup;
+import org.expeditee.gui.PopupManager;
+
+/**
+ * A volume control button which can be reused in multiple situations... Follows swings MVC pattern.
+ * 
+ * @author Brook Novak
+ */
+public class VolumeControlButton extends JButton {
+
+	private static final long serialVersionUID = 1L;
+
+	private VolumeControlPopup vcPopup;
+	
+	private int volumeValue = 0;
+	private boolean muted = false;
+	
+	private List<ChangeListener> changeListeners = new LinkedList<ChangeListener>();
+	
+	private String volumeTag;
+	
+	private Color SELECTED_BACKCOLOR = Color.GRAY;
+	private Color UNSELECTED_BACKCOLOR = null;
+	
+	private static final int VOL_ADJUSTMENT = 10;
+	private static final int VOL_HEIGHT = 150;
+
+	/**
+	 * @param volumeTag
+	 * 		The tag line to appear before the volume value in the tip tool text.
+	 * 		Null = no tag line.
+	 * 		E.G. "Master Volume".
+	 */
+	public VolumeControlButton(String volumeTag) {
+		super();
+		UNSELECTED_BACKCOLOR = getBackground();
+		vcPopup = new VolumeControlPopup();
+		vcPopup.setVisible(false);
+		this.volumeTag = volumeTag;
+		toggleSelectionState(false);
+		updateButtonGUI();
+	}
+	
+	/**
+	 * Adds a change listener which will be notified whenever the volume value
+	 * changes via the GUI.
+	 * 
+	 * @param listener
+	 */
+	public void addVolumeChangeListener(ChangeListener listener) {
+		assert(listener != null);
+		changeListeners.add(listener);
+	}
+	
+	private void fireVolumeChanged() {
+		for (ChangeListener a : changeListeners) {
+			a.stateChanged(new ChangeEvent(this));
+		}
+	}
+
+	private void toggleSelectionState(boolean selected) {
+		if (selected) setBackground(SELECTED_BACKCOLOR);
+		else setBackground(UNSELECTED_BACKCOLOR);
+	}
+	
+	/**
+	 * {@inheritDoc}
+	 * Displays the volume control popup when the button is pressed
+	 */
+	@Override
+	protected void fireActionPerformed(ActionEvent event) {
+		super.fireActionPerformed(event);
+		
+		if (!vcPopup.isVisible()) { // show popup
+			
+			vcPopup.showPopup();
+
+		} else { // hide popup
+
+			PopupManager.getInstance().hidePopup(vcPopup, 
+					PopupManager.getInstance().new ExpandShrinkAnimator(
+							VolumeControlButton.this.getBounds(),
+							Color.LIGHT_GRAY));
+		}
+	}
+
+	/**
+	 * Sets the volume GUI. This will not fire any event - it will just update the gui.
+	 * @param n
+	 * return true if changed
+	 */
+	public boolean setVolumeValue(int n) {
+		
+		// Clamp
+		if (n > 100) n = 100;
+		else if (n < 0) n = 0;
+		
+		if (n != volumeValue) {
+			volumeValue = n;
+			updateButtonGUI();
+			vcPopup.updatePopupGUI();
+			return true;
+		}
+		
+		return false;
+	}
+	
+	/**
+	 * Sets the volume GUI. This will not fire any event - it will just update the gui.
+	 * 
+	 * @param mute
+	 * 
+	 * @return
+	 */
+	public void setMuted(boolean mute) {
+		muted = mute;
+		updateButtonGUI();
+	}
+		
+
+	private void updateButtonGUI() {
+		
+		Icon newIcon;
+		if(muted)  
+				newIcon = IconRepository.getIcon("volmute.png");
+		else if (volumeValue <= 25) 
+				newIcon = IconRepository.getIcon("vol25.png");
+		else if (volumeValue <= 50) 
+			newIcon = IconRepository.getIcon("vol50.png");
+		else if (volumeValue <= 75) 
+			newIcon = IconRepository.getIcon("vol75.png");
+		else // maxing
+				newIcon = IconRepository.getIcon("vol100.png");
+
+		if (newIcon != getIcon()) setIcon(newIcon);
+		
+		if (muted) {
+			if (volumeTag != null) setToolTipText(volumeTag + ": Muted");
+			else setToolTipText("Muted");
+		}
+		else {
+			if (volumeTag != null) setToolTipText(volumeTag + ": " + volumeValue + "%");
+			else setToolTipText(volumeValue + "%");
+		}
+	}
+
+	/**
+	 * 
+	 * @return The volume value of the gui
+	 */
+	public int getVolumeValue() {
+		return volumeValue;
+	}
+	
+
+	/**
+	 * 
+	 * @return The mute value of the gui
+	 */
+	public boolean getMuteValue() {
+		return muted;
+	}
+
+	private class VolumeControlPopup extends Popup {
+		
+		private static final long serialVersionUID = 1L;
+	
+		private JSlider volumeSlider;
+		private JButton increaseButton;
+		private JButton decreaseButton;
+		
+		/**
+		 * Constructor.
+		 */
+		public VolumeControlPopup() {
+			super();
+
+			volumeSlider = new JSlider(JSlider.VERTICAL);
+			volumeSlider.setMinimum(0);
+			volumeSlider.setMaximum(100);
+			volumeSlider.addChangeListener(new ChangeListener() {
+				public void stateChanged(ChangeEvent e) {
+					onVolumeSliderChanged();
+				}
+			});
+
+			increaseButton = new JButton();
+			//increaseButton.setIcon(IconRepository.getIcon("incvol.ico"));
+			increaseButton.setText("+");
+			increaseButton.addActionListener( new ActionListener() {
+				public void actionPerformed(ActionEvent e) { 
+					onIncrementVolume();
+				}
+			});
+			
+			decreaseButton = new JButton();
+			//decreaseButton.setIcon(IconRepository.getIcon("decvol.ico"));
+			decreaseButton.setText("-");
+			decreaseButton.addActionListener( new ActionListener() {
+				public void actionPerformed(ActionEvent e) { 
+					onDeincrementVolume();
+				}
+			});
+	
+			updatePopupGUI();
+		}
+		
+		private void onIncrementVolume() {
+			if (setVolumeValue(volumeValue + VOL_ADJUSTMENT)) {
+				fireVolumeChanged();
+			}
+		}
+		
+		private void onDeincrementVolume() {
+			if (setVolumeValue(volumeValue - VOL_ADJUSTMENT)) {
+				fireVolumeChanged();
+			}
+		}
+		
+		private void onVolumeSliderChanged() {
+			if (setVolumeValue(volumeSlider.getValue())) {
+				fireVolumeChanged();
+			}
+		}
+		
+		private void updatePopupGUI() {
+			volumeSlider.setValue(volumeValue);
+			increaseButton.setEnabled(volumeValue < 100);
+			decreaseButton.setEnabled(volumeValue > 0);
+		}
+		
+		private void prepLayout() {
+			
+			if (this.getComponents().length == 0) {
+				
+				final int INNER_SPACING = 0;
+				final int OUTER_SPACING = 0;
+				final int YPAD = 0;
+				final int BUTTON_MARGIN = 0;
+				
+				int targetWidth = VolumeControlButton.this.getWidth();
+				Dimension buttonSizes = new Dimension(targetWidth,  targetWidth);
+				
+				decreaseButton.setPreferredSize(buttonSizes);
+				increaseButton.setPreferredSize(buttonSizes); 
+				volumeSlider.setPreferredSize(new Dimension(volumeSlider.getWidth(), VOL_HEIGHT));
+				decreaseButton.setSize(buttonSizes);
+				increaseButton.setSize(buttonSizes); 
+				volumeSlider.setSize(new Dimension(volumeSlider.getWidth(), VOL_HEIGHT));
+
+				increaseButton.setMargin(new Insets(BUTTON_MARGIN,BUTTON_MARGIN,BUTTON_MARGIN,BUTTON_MARGIN));
+				decreaseButton.setMargin(increaseButton.getMargin());
+	
+				GridBagLayout layout = new GridBagLayout();
+		
+				layout.rowHeights = new int[] {
+						increaseButton.getHeight(), volumeSlider.getHeight(), decreaseButton.getHeight()
+				};
+				
+				setLayout(layout);
+
+				GridBagConstraints c = new GridBagConstraints();
+				c.gridx = 0;
+				c.gridy = 0;
+				c.fill = GridBagConstraints.BOTH;
+				c.insets = new Insets(OUTER_SPACING,OUTER_SPACING,INNER_SPACING,OUTER_SPACING);
+				c.ipady = YPAD;
+				this.add(increaseButton, c);
+				
+				c = new GridBagConstraints();
+				c.gridx = 0;
+				c.gridy = 1;
+				c.fill = GridBagConstraints.BOTH;
+				c.insets = new Insets(INNER_SPACING,OUTER_SPACING,INNER_SPACING,OUTER_SPACING);
+				c.ipady = YPAD;
+				this.add(volumeSlider, c);
+				
+				c = new GridBagConstraints();
+				c.gridx = 0;
+				c.gridy = 2;
+				c.fill = GridBagConstraints.BOTH;
+				c.insets = new Insets(INNER_SPACING,OUTER_SPACING,OUTER_SPACING,OUTER_SPACING);
+				c.ipady = YPAD;
+				this.add(decreaseButton, c);
+				
+				setSize(targetWidth, volumeSlider.getHeight() + (2 * buttonSizes.height) 
+						+ (2 * OUTER_SPACING));
+				this.doLayout();
+			}
+
+		}
+		
+		private void showPopup() {
+			prepLayout();
+
+			// Determine where popup should show
+			int x = VolumeControlButton.this.getX();
+			int y = VolumeControlButton.this.getY() - getHeight() - 2; // by default show above
+			
+			if (y < 0) {
+				y = VolumeControlButton.this.getY() + VolumeControlButton.this.getHeight() + 2;
+			}
+
+			PopupManager.getInstance().showPopup(
+					this, 
+					new Point(x, y), 
+					VolumeControlButton.this,
+					PopupManager.getInstance().new ExpandShrinkAnimator(
+							VolumeControlButton.this.getBounds(),
+							Color.LIGHT_GRAY));
+		}
+
+		@Override
+		public void onHide() {
+			// Toggle state = volume popup showing
+			toggleSelectionState(false);
+		}
+
+		@Override
+		public void onShow() {
+			// Toggle state = volume popup showing
+			toggleSelectionState(true);
+		}
+		
+		
+		
+	}
+
+}
Index: /trunk/src_apollo/org/apollo/gui/WaveFormRenderProccessingUnit.java
===================================================================
--- /trunk/src_apollo/org/apollo/gui/WaveFormRenderProccessingUnit.java	(revision 315)
+++ /trunk/src_apollo/org/apollo/gui/WaveFormRenderProccessingUnit.java	(revision 315)
@@ -0,0 +1,394 @@
+package org.apollo.gui;
+
+import java.awt.BasicStroke;
+import java.awt.Graphics2D;
+import java.awt.image.BufferedImage;
+import java.util.LinkedList;
+
+import javax.sound.sampled.AudioFormat;
+
+import org.apollo.audio.ApolloSubjectChangedEvent;
+import org.apollo.mvc.AbstractSubject;
+import org.apollo.mvc.SubjectChangedEvent;
+
+public class WaveFormRenderProccessingUnit {
+	
+	/** Limit the amount of render threads. */
+	private RenderThread[] renderThreads = new RenderThread[4];
+	
+	private LinkedList<WaveFormRenderTask> taskQueue = new LinkedList<WaveFormRenderTask>();
+
+	private static WaveFormRenderProccessingUnit instance = new WaveFormRenderProccessingUnit(); // single design pattern
+	
+	/**
+	 * @return The singleton instance of the WaveFormRenderProccessingUnit
+	 */
+	public static WaveFormRenderProccessingUnit getInstance() { // single design pattern
+		return instance;
+	}
+
+	private WaveFormRenderProccessingUnit() { // singleton design pattern
+	}
+
+	/**
+	 * Queues a task for rendering as soon as possible (asynchronously).
+	 * 
+	 * @param task
+	 * 		The task to queue
+	 * 
+	 * @throws NullPointerException
+	 * 		If task is null.
+	 * 
+	 * @throws IllegalArgumentException
+	 * 		If task has been proccessed before (must not be re-used).
+	 * 		Or if it is already queued for rendering.
+	 */
+	public void queueTask(WaveFormRenderTask task) {
+		if (task == null) throw new NullPointerException("task");
+		
+		// Ensure not re-using a WaveFormRenderTask
+		if (task.hasStarted()) throw new IllegalArgumentException("task");
+
+		// Add to the queue
+		synchronized (taskQueue) {
+			
+			// Check that not already on the queue
+			if (taskQueue.contains(task)) {
+				throw new IllegalArgumentException("task");
+			}
+			
+			taskQueue.add(task);
+		
+		
+			// If there is are dead thread, re-animate it to ensure
+			// the task begins as soon as possible.
+			for (int i = 0; i < renderThreads.length; i++) {
+				
+				if (renderThreads[i] == null
+						||  renderThreads[i].isFinishing()
+						|| !renderThreads[i].isAlive()) {
+					
+					renderThreads[i] = new RenderThread(i);
+					renderThreads[i].start();
+					break;
+				}
+				
+			}
+		
+		}
+
+		
+	}
+	
+	/**
+	 * Cancels a task from rendering.
+	 * 
+	 * @param task
+	 * 		The task to cancel.
+	 * 
+	 * @throws NullPointerException
+	 * 		If task is null.
+	 */
+	public void cancelTask(WaveFormRenderTask task) {
+		if (task == null) throw new NullPointerException("task");
+		
+		task.keepRendering = false;
+		synchronized (taskQueue) {
+				taskQueue.remove(task);
+		}
+	}
+
+	/**
+	 * Render of wave forms is done in a dedicated thread because some graphs may have
+	 * to proccess / render a lot of samples depending on the zoom / size of the audio track.
+	 * 
+	 * From a usability perspective, this means that the waveform can be computed accuratly
+	 * because responce time is no longer an issue.
+	 * 
+	 * @author Brook Novak
+	 *
+	 */
+	private class RenderThread extends Thread {
+		
+		private WaveFormRenderTask currentTask;
+		
+		private final static float AMPLITUDES_PER_PIXEL = 0.5f; // resolution: how many amplitudes per pixel to render 
+		private final static int FRAME_RENDER_RATE = 2000; // The approximate amount of frames to render per pass
+		
+		private boolean isFinishing = false;
+		
+		public boolean isFinishing() {
+			return isFinishing;
+		}
+		
+		/**
+		 * 
+		 * @param audioBytes The reference to the audio bytes to render.
+		 * 
+		 * @param frameLength in frames.
+		 */
+		public RenderThread(int id) {
+			super("RenderThread" + id);
+			// Give renderering lowest priority
+			setPriority(Thread.MIN_PRIORITY);
+			
+		}
+		
+		@Override
+		public void run() {
+			
+			while (true) {
+				// Grab a task from the queue
+				synchronized(taskQueue) {
+					
+					if (taskQueue.isEmpty()) {
+						isFinishing = true;
+						return;
+					}
+					currentTask = taskQueue.remove();
+				}
+				
+				// Perform rendering until the task has been cancelled
+				doRender();
+			}
+		}
+		
+		private void doRender() {
+			assert(currentTask != null);
+			assert(!currentTask.hasStarted());
+			
+			// Quick check
+			if (!currentTask.keepRendering) return;
+			
+			currentTask.setState(WaveFormRenderTask.STATE_RENDERING);
+			
+			Graphics2D g = null;
+			
+			try {
+				
+				int halfMaxHeight;
+				int bufferWidth;
+				
+				// Create the graphics and prepare the buffer
+				
+				synchronized(currentTask.imageBuffer) {
+					
+					halfMaxHeight = currentTask.imageBuffer.getHeight() / 2;
+					bufferWidth = currentTask.imageBuffer.getWidth();
+					
+					g = currentTask.imageBuffer.createGraphics();
+	
+					g.setStroke(new BasicStroke(1.0f));
+
+					// Clear the buffer with transparent pixels
+					g.setBackground(ApolloColorIndexedModels.KEY_COLOR);
+					g.clearRect(0, 0, bufferWidth, currentTask.imageBuffer.getHeight());
+					
+				}
+				
+				// Render waveforms in chunks - so that can cancel rendering and the
+				// widget can show progress incrementally
+				int lastXPosition = -1;
+				int lastYPosition = -1;
+			
+				// Choose how many amplitudes to render in the graph. i.e. waveform resolution
+				int totalAmplitudes = Math.min((int)(bufferWidth * AMPLITUDES_PER_PIXEL), currentTask.frameLength); // get amount of amps to render
+				if (totalAmplitudes == 0) return;
+				
+				int currentAmplitude = 0;
+	
+				// Calculate the amount of frames to aggregate
+				int aggregationSize = currentTask.frameLength / totalAmplitudes;
+				assert(aggregationSize >= 1);
+	
+				// Limit the amount of amplitudes to render (the small chunks) based on the
+				// aggregation size - since it correlation to proccess time.
+				int amplitudeCountPerPass = (FRAME_RENDER_RATE / aggregationSize);
+				if (amplitudeCountPerPass == 0) amplitudeCountPerPass = 1;
+	
+				int renderStart;
+				int renderLength;
+				
+				// render until finished or cancelled
+				while (currentTask.keepRendering && currentAmplitude < totalAmplitudes) { 
+					
+					renderStart = currentAmplitude * aggregationSize;
+					
+					// At the last pass, render the last remaining bytes
+					renderLength = Math.min(
+							amplitudeCountPerPass * aggregationSize, 
+							currentTask.frameLength - renderStart);
+					
+					// At the last pass, be sure that the aggregate size does not exeed the render count
+					// so that last samples are not ignored
+					aggregationSize =  Math.min(aggregationSize, renderLength);
+					
+					// Perform the waveform rendering
+					float[] amps = currentTask.renderer.getSampleAmplitudes(
+							currentTask.audioBytes, 
+							currentTask.startFrame + renderStart, 
+							renderLength, 
+							aggregationSize);
+					
+					// Draw the rendered waveform to the buffer
+					synchronized(currentTask.imageBuffer) { 
+						
+						g.setColor(ApolloColorIndexedModels.WAVEFORM_COLOR);
+						
+						for (float h : amps) {
+							
+							int x = (int)(currentAmplitude * ((float)bufferWidth / (float)(totalAmplitudes - 1))); 
+							int y = halfMaxHeight - (int)(h * halfMaxHeight);
+							
+							if (currentAmplitude == 0) { // never draw line on first point
+								
+								lastXPosition = 0;
+								lastYPosition = y;
+								
+							} else {
+				
+								g.drawLine(
+										lastXPosition, 
+										lastYPosition, 
+										x, 
+										y);
+								
+								lastXPosition = x;
+								lastYPosition = y;
+								
+							}
+							
+							currentAmplitude ++;
+						}
+						
+					}
+					
+					
+				} // next pass
+				
+				// Lession learnt: do not request lots of requests to repaint
+				// later on the AWT Event queu otherwise it will get congested
+				// and will freeze up the interact for annoying periods of time.
+				currentTask.setState(WaveFormRenderTask.STATE_STOPPED);
+				
+			} finally {
+				
+				if (g != null) g.dispose();
+				
+				// safety
+				if (!currentTask.isStopped())
+					currentTask.setState(WaveFormRenderTask.STATE_STOPPED);
+				
+				currentTask = null;
+			}
+		}
+		
+	}
+
+	/**
+	 * A task descriptor for rendering wave forms via the WaveFormRenderProccessingUnit.
+	 * 
+	 * Raises {@value ApolloSubjectChangedEvent#RENDER_TASK_INVALIDATION_RECOMENDATION} 
+	 * when the WaveFormRenderProccessingUnit recommends a refresh
+	 * of the BufferedImage. This includes when the WaveFormRenderTask state changes
+	 * 
+	 * The BufferedImage is always locked before handled by the render thread.
+	 * 
+	 * Only can use these once... per render task. I.E. Do not re-use
+	 * 
+	 * @author Brook Novak
+	 *
+	 */
+	public class WaveFormRenderTask extends AbstractSubject {
+		
+		private boolean keepRendering = true; // a cancel flag
+		
+		private int state = STATE_PENDING;
+		
+		private static final int STATE_PENDING = 1;
+		private static final int STATE_RENDERING = 2;
+		private static final int STATE_STOPPED = 3;
+		
+		private BufferedImage imageBuffer; // nullified when stopped.
+		private byte[] audioBytes; // nullified when stopped. - Arrays (not contents) are immutable so no need to worry about threading issues with indexes
+		private final int startFrame;
+		private final int frameLength; // in frames
+		private final WaveFormRenderer renderer;
+		private final boolean recommendInvalidations;
+		
+		public WaveFormRenderTask(
+				BufferedImage imageBuffer, 
+				byte[] audioBytes, 
+				AudioFormat format,
+				int startFrame, 
+				int frameLength, 
+				boolean recommendInvalidations) {
+			
+			assert(audioBytes != null);
+			assert(format != null);
+			assert(imageBuffer != null);
+			assert(((startFrame + frameLength) * format.getFrameSize()) <= audioBytes.length);
+
+			this.imageBuffer = imageBuffer;
+			this.audioBytes = audioBytes;
+			this.startFrame = startFrame;
+			this.frameLength = frameLength;
+			this.recommendInvalidations = recommendInvalidations;
+			renderer = new PeakTroughWaveFormRenderer(format);
+		}
+		
+		private void setState(int newState) {
+			this.state = newState;
+			
+			if (keepRendering) { // invalidate if cancel not requested
+				recommendInvalidation();
+			}
+			
+			// Nullify expensive references when stopped so they can be freed by the garbage collector
+			if (state == STATE_STOPPED) {
+				audioBytes = null;
+				imageBuffer = null;
+			}
+			
+		}
+		
+		/**
+		 * 
+		 * @return
+		 * 		True if this is rendering .. thus being proccessed.
+		 */
+		public boolean isRendering() {
+			return state == STATE_RENDERING;
+		}
+
+		
+		/**
+		 * <b>WARNING:</b> Think about the race conditions ... now cannot no when this may start therefore
+		 * it is always safe to lock buffer if in pending state.
+		 * 
+		 * @return
+		 * 		True if this has not started to render
+		 */
+		public boolean hasStarted() {
+			return state != STATE_PENDING;
+		}
+
+		/**
+		 * 
+		 * @return
+		 * 		True if has stopped
+		 */
+		public boolean isStopped() {
+			return state == STATE_STOPPED;
+			
+		}
+		
+		private void recommendInvalidation() {
+			if (recommendInvalidations)
+				fireSubjectChangedLaterOnSwingThread(
+					new SubjectChangedEvent(ApolloSubjectChangedEvent.RENDER_TASK_INVALIDATION_RECOMENDATION));
+		}
+		
+	}
+	
+}
Index: /trunk/src_apollo/org/apollo/gui/WaveFormRenderer.java
===================================================================
--- /trunk/src_apollo/org/apollo/gui/WaveFormRenderer.java	(revision 315)
+++ /trunk/src_apollo/org/apollo/gui/WaveFormRenderer.java	(revision 315)
@@ -0,0 +1,34 @@
+package org.apollo.gui;
+
+/**
+ * Provides waveform rendering services.
+ * 
+ * @author Brook Novak
+ */
+public interface WaveFormRenderer {
+
+	/**
+	 * Renders waveforms in a given array of audio samples - producing an array of wave-form 
+	 * amplitutes ranging for -1.0 to 1.0.
+	 * 
+	 * A single height is calculated for one or more frames, which is specified by the
+	 * aggregationSize. The way in which waveforms are rendered is implementation dependant.
+	 * 
+	 * @param audioBytes
+	 * 		The array of pure samples.
+	 * 
+	 * @param startFrame
+	 * 		The starting frame to begin rendering
+	 * 
+	 * @param frameLength
+	 * 		The amount of frames to consider for rendering.
+	 * 
+	 * @param aggregationSize
+	 * 		The amout of frames to aggregate.
+	 * 
+	 * @return 
+	 * 		An array of wave-form amplitutes ranging for -1.0 to 1.0.
+	 * 		Note that this will be empty if aggregationSize > frameLength.
+	 */
+	public float[] getSampleAmplitudes(byte[] audioBytes, int startFrame, int frameLength, int aggregationSize);
+}
Index: /trunk/src_apollo/org/apollo/gui/ZoomChangeListener.java
===================================================================
--- /trunk/src_apollo/org/apollo/gui/ZoomChangeListener.java	(revision 315)
+++ /trunk/src_apollo/org/apollo/gui/ZoomChangeListener.java	(revision 315)
@@ -0,0 +1,7 @@
+package org.apollo.gui;
+
+import java.awt.Event;
+
+public interface ZoomChangeListener {
+	void zoomChanged(Event e);
+}
Index: /trunk/src_apollo/org/apollo/io/AudioIO.java
===================================================================
--- /trunk/src_apollo/org/apollo/io/AudioIO.java	(revision 315)
+++ /trunk/src_apollo/org/apollo/io/AudioIO.java	(revision 315)
@@ -0,0 +1,635 @@
+package org.apollo.io;
+
+import java.io.ByteArrayInputStream;
+import java.io.ByteArrayOutputStream;
+import java.io.File;
+import java.io.FileOutputStream;
+import java.io.IOException;
+import java.util.LinkedList;
+
+import javax.sound.sampled.AudioFileFormat;
+import javax.sound.sampled.AudioFormat;
+import javax.sound.sampled.AudioInputStream;
+import javax.sound.sampled.AudioSystem;
+import javax.sound.sampled.UnsupportedAudioFileException;
+import javax.sound.sampled.AudioFileFormat.Type;
+
+import org.apollo.audio.ApolloSubjectChangedEvent;
+import org.apollo.audio.SampledAudioManager;
+import org.apollo.mvc.AbstractSubject;
+import org.apollo.mvc.Observer;
+import org.apollo.mvc.SubjectChangedEvent;
+import org.apollo.util.AudioMath;
+
+/**
+ * Provides audio IO and conversion operations.
+ * 
+ * @author Brook Novak
+ *
+ */
+public class AudioIO {
+	
+	private AudioIO () {}
+	private static AudioIO innerclassCreator = new AudioIO();
+	
+	/**
+	 * Determines the running length of a audio file.
+	 * 
+	 * @param path
+	 * 		The path to the audio file.
+	 * 
+	 * @return
+	 * 		The running time of the sound file in milliseconds.
+	 * 		-1 if the audio frame count is not specified due to the encoding method.
+	 * 
+	 * @throws IOException
+	 * 		If failed to create file for saving, or an error occured while writing audio bytes.
+	 * 
+	 * @throws UnsupportedAudioFileException
+	 * 		If audio format is unsupported or the encoding is not PCM.
+	 * 
+	 * @throws NullPointerException
+	 * 			If path is null.
+	 * 
+	 */
+	public static long getRunningTime(String path) throws IOException, UnsupportedAudioFileException {
+		if (path == null) throw new NullPointerException("path");
+		
+		AudioFileFormat fileFormat = AudioSystem.getAudioFileFormat(new File(path));
+		
+		if (fileFormat.getFrameLength() == AudioSystem.NOT_SPECIFIED)
+			return -1;
+		
+		return AudioMath.framesToMilliseconds(fileFormat.getFrameLength(), fileFormat.getFormat());
+	}
+	
+
+	/**
+	 * Determines whether or not a file can be imported. Depending on the
+	 * file exists and is an audio file with a supported format (or conversion).
+	 * 
+	 * @param f
+	 * 		The file to check.
+	 * 
+	 * @return
+	 * 		True if the file can be imported. Otherwise false.
+	 */
+	public static boolean canImportFile(File f) {
+		
+		if (f == null || !f.exists() || !f.canRead()) return false;
+		
+		AudioFileFormat fileFormat;
+		try {
+			fileFormat = AudioSystem.getAudioFileFormat(f);
+		} catch (UnsupportedAudioFileException e) {
+			return false;
+		} catch (IOException e) {
+			return false;
+		}
+		
+		return (SampledAudioManager.getInstance().isFormatSupportedForPlayback(fileFormat.getFormat()) ||
+				AudioSystem.isConversionSupported(
+						SampledAudioManager.getInstance().getDefaultPlaybackFormat(),
+						fileFormat.getFormat()));
+	}
+	
+	/**
+	 * Converts audio bytes into a target format. ...If needs to.
+	 * 
+	 * @param sourceBytes
+	 * 		The bytes to convert. Must not be null.
+	 * 
+	 * @param sourceFormat
+	 * 		The format of the bytes that will be converted. Must not be null.
+	 * 
+	 * @param targetFormat
+	 * 		The format to convert the bytes to. Must not be null.
+	 * 
+	 * @return
+	 * 		The converted bytes. If the source and target formats match then sourceBytes is returned.
+	 * 
+	 * @throws IOException
+	 * 		If an error occured while converting
+	 * 
+	 * @throws NullPointerException
+	 * 		If any of the arguments are null.
+	 * 
+	 * @throws IllegalArgumentException
+	 * 		If the conversion is not supported.
+	 * 
+	 */
+	public static byte[] convertAudioBytes(byte[] sourceBytes, AudioFormat sourceFormat, AudioFormat targetFormat) 
+		throws IOException {
+		
+		if (sourceBytes == null) throw new NullPointerException("sourceBytes");
+		if (sourceFormat == null) throw new NullPointerException("sourceFormat");
+		if (targetFormat == null) throw new NullPointerException("targetFormat");
+		if (sourceBytes.length == 0) throw new IllegalArgumentException("sourceBytes length is zero");
+		
+		if (!AudioSystem.isConversionSupported(targetFormat, sourceFormat)) 
+			throw new IllegalArgumentException("Conversion not supported");
+		
+		// Need converting?
+		if (targetFormat.equals(sourceFormat)) return sourceBytes;
+		
+		int frameCount = sourceBytes.length / sourceFormat.getFrameSize();
+		
+		// Create AudioInputStream to stream source bytes
+		AudioInputStream aisOriginal = new AudioInputStream(
+				new ByteArrayInputStream(sourceBytes), 
+				sourceFormat,
+				frameCount);
+		
+		// Chain stream with a series of conversion streams. Note that you cannot just
+		// use one conversion stream to process the audio all at once.
+		AudioInputStream aisConverter = aisOriginal;
+		
+		// First convert to the correct PCM format so can carry out appropriate conversions
+		//if (!sourceFormat.getEncoding().equals(targetFormat.getEncoding())) {
+//		AudioFormat.Encoding conversionEncoding = null;
+//		
+//		if (targetFormat.getEncoding().toString().startsWith("PCM")) // convert signed/unsigned if need to
+//			conversionEncoding = targetFormat.getEncoding();
+//		
+//		else if (!targetFormat.getEncoding().toString().startsWith("PCM"))
+//			conversionEncoding =
+//				(sourceFormat.getSampleSizeInBits() == 8) ?
+//						AudioFormat.Encoding.PCM_UNSIGNED :
+//						AudioFormat.Encoding.PCM_SIGNED;
+//		
+//		if (conversionEncoding != null) {
+//			aisConverter = AudioSystem.getAudioInputStream(conversionEncoding, aisConverter);
+//		}
+//		
+//	}
+		
+		if (!sourceFormat.getEncoding().toString().startsWith("PCM")) {
+			
+			AudioFormat.Encoding conversionEncoding = 
+				(sourceFormat.getSampleSizeInBits() == 8) ?
+				AudioFormat.Encoding.PCM_UNSIGNED :
+				AudioFormat.Encoding.PCM_SIGNED;
+			
+			aisConverter = AudioSystem.getAudioInputStream(conversionEncoding, aisConverter);
+
+		}
+		
+		// Next convert number of channels
+		if (sourceFormat.getChannels() != targetFormat.getChannels()) {
+			
+			AudioFormat chainedFormat = aisConverter.getFormat();
+			
+			aisConverter = AudioSystem.getAudioInputStream(
+					new AudioFormat(
+							chainedFormat.getEncoding(),
+							chainedFormat.getSampleRate(),
+							chainedFormat.getSampleSizeInBits(),
+							targetFormat.getChannels(),
+							targetFormat.getChannels() * ((chainedFormat.getSampleSizeInBits() + 7) / 8),
+							chainedFormat.getFrameRate(),
+							chainedFormat.isBigEndian()),
+					aisConverter);
+		}
+		
+		
+		// Next convert sample size AND endianess.
+		if ((aisConverter.getFormat().getSampleSizeInBits() != targetFormat.getSampleSizeInBits())
+				|| (aisConverter.getFormat().isBigEndian() != targetFormat.isBigEndian())) {
+			
+			AudioFormat chainedFormat = aisConverter.getFormat();
+
+			aisConverter = AudioSystem.getAudioInputStream(
+					new AudioFormat(
+						chainedFormat.getEncoding(),
+						chainedFormat.getSampleRate(),
+						targetFormat.getSampleSizeInBits(),
+						chainedFormat.getChannels(),
+						chainedFormat.getChannels() * ((targetFormat.getSampleSizeInBits() + 7) / 8),
+						chainedFormat.getFrameRate(),
+						targetFormat.isBigEndian()),
+					aisConverter);
+
+		}
+
+		// convert sample rate - this relies on a plugin. I.E. tritonous' PCM2PCM SPI
+		final float DELTA = 1E-9F;
+		if (Math.abs(aisConverter.getFormat().getSampleRate() - targetFormat.getSampleRate()) >= DELTA) {
+
+			AudioFormat chainedFormat = aisConverter.getFormat();
+
+			aisConverter = AudioSystem.getAudioInputStream(
+					new AudioFormat(
+						chainedFormat.getEncoding(),
+						targetFormat.getSampleRate(),
+						chainedFormat.getSampleSizeInBits(),
+						chainedFormat.getChannels(),
+						chainedFormat.getFrameSize(),
+						targetFormat.getFrameRate(),
+						chainedFormat.isBigEndian()),
+					aisConverter);
+
+		}
+
+		// convert to back to non-PCM encoding, or to different PCM encoding, if need to.
+		if (!targetFormat.getEncoding().equals(aisConverter.getFormat().getEncoding())) {
+			aisConverter = AudioSystem.getAudioInputStream(targetFormat.getEncoding(), aisConverter);
+		}
+		
+		LinkedList<byte[]> convertedBytes = new LinkedList<byte[]>();
+		int BUFFERSIZE = 50000 * sourceFormat.getFrameSize();
+		
+		// Read the bytes and convert
+		int bytesRead = 0;
+		
+		while(bytesRead != -1) {
+			
+			int bytePosition = 0;
+			bytesRead = 0;
+			byte[] buffer = new byte[BUFFERSIZE];
+			
+			while (bytePosition < BUFFERSIZE) {
+
+				bytesRead = aisConverter.read(buffer, bytePosition, buffer.length - bytePosition);
+				
+				if (bytesRead == -1) break;
+				
+				bytePosition += bytesRead;
+				
+			}
+			
+			if (bytePosition > 0 && bytesRead > 0) {
+				
+				convertedBytes.add(buffer);
+				
+			} else if (bytePosition > 0) { // last chunk
+				byte[] finalChunk = new byte[bytePosition];
+				System.arraycopy(buffer, 0, finalChunk, 0, bytePosition);
+				convertedBytes.add(finalChunk);
+			}
+			
+		}
+		
+		int size = 0;
+		
+		for (byte[] ba : convertedBytes) size += ba.length;
+		
+		byte[] converted = new byte[size];
+		int pos = 0;
+		
+		// Assemble
+		for (byte[] ba : convertedBytes) {
+			System.arraycopy(
+					ba, 
+					0, 
+					converted, 
+					pos, 
+					ba.length);
+			
+			pos += ba.length;
+		}
+
+		
+		return converted;
+	}
+	
+	/**
+	 * Writes PCM encoded audio bytes as a wave file.
+	 * 
+	 * @param path
+	 * 		The path of the wave file to write to. This will be overriden.
+	 * 
+	 * @param bytes
+	 * 		The PCM encoded audio bytes to write
+	 * 
+	 * @param format
+	 * 		The format of the audio bytes. Must be in PCM.
+	 * 
+	 * @throws IOException
+	 * 		If failed to create file for saving, or an error occured while writing audio bytes.
+	 * 
+	 * @throws UnsupportedAudioFileException
+	 * 		If audio format is unsupported or the encoding is not PCM.
+	 * 
+	 * @throws NullPointerException
+	 * 			If path, bytes or format is null.
+	 */
+	public static synchronized void savePCMAudioToWaveFile(String path, byte[] bytes, AudioFormat format) 
+		throws IOException, UnsupportedAudioFileException {
+		
+		if (bytes == null) throw new NullPointerException("bytes");
+		if (format == null) throw new NullPointerException("format");
+		
+		if (!format.getEncoding().toString().startsWith("PCM"))
+			throw new UnsupportedAudioFileException();
+		
+		saveAudioToFile(path, bytes, format, Type.WAVE, bytes.length / format.getFrameSize());
+	}
+	
+	/**
+	 * Writes audio bytes to file.
+	 * 
+	 * @param path
+	 * 		The path of the audio file to write to. This will be overriden.
+	 * 
+	 * @param bytes
+	 * 		The audio bytes to write
+	 * 
+	 * @param format
+	 * 		The format of the audio bytes.
+	 * 
+	 * @param fileType
+	 * 		The type of file to output to.
+	 * 
+	 * @param length
+	 * 		The amount of audio bytes to write
+	 * 
+	 * @throws IOException
+	 * 		If failed to create file for saving, or an error occured while writing audio bytes.
+	 * 
+	 * @throws UnsupportedAudioFileException
+	 * 		If audio format is unsupported
+	 * 
+	 * @throws NullPointerException
+	 * 			If path, bytes, format or fileType is null.
+	 */
+	public static synchronized void saveAudioToFile(String path, byte[] bytes, 
+			AudioFormat format, Type fileType, long length) 
+		throws IOException, UnsupportedAudioFileException {
+		
+		if (path == null) throw new NullPointerException("path");
+		if (bytes == null) throw new NullPointerException("bytes");
+		if (format == null) throw new NullPointerException("format");
+		if (fileType == null) throw new NullPointerException("fileType");
+
+		// Create file out stream
+		File f = new File(path);
+		FileOutputStream out = new FileOutputStream(f);
+	
+		// Create audio input stream
+		AudioInputStream aistream = new AudioInputStream(
+				new ByteArrayInputStream(bytes),
+				format,
+				length);
+		
+		// Write audio file
+		AudioSystem.write(aistream, fileType, out);
+
+	}
+
+	/**
+	 * Loads an audio file into memory. The loaded bytes are converted if they need to be
+	 * in order to be used in apollos.
+	 * 
+	 * This operation can be cancelled via cancelLoad. 
+	 * 
+	 * @param file 
+	 * 		The file to load. must not be null and the file must exist.
+	 * 
+	 * @param statusObserver
+	 * 		If given (can be null), then the statusObserver will be notified with 
+	 * 		LOAD_STATUS_REPORT AudioSubjectChangedEvent events (on the calling thread).
+	 * 		The state will be a float with the current percent (between 0.0 and 1.0 inclusive).
+	 * 		The subject source will be this instance.
+	 * 
+	 * @return
+	 * 		The loaded audio - in a format that can be used in apollos.
+	 * 		Null if cancelled.
+	 * 
+	 * @throws IOException
+	 * 		If an exception occured while loade audio bytes.
+	 * 		
+	 * @throws UnsupportedAudioFileException
+	 * `	if the File does not point to valid audio file data recognized by the system.
+	 * 
+	 */
+	public static synchronized LoadedAudioData loadAudioFile(File file, Observer statusObserver) 
+		throws IOException, UnsupportedAudioFileException {
+		
+		return (innerclassCreator.new AudioFileLoader()).loadAudioFile(file, statusObserver);
+		
+	}
+	
+
+
+	/**
+	 * Loads audio files into memory - converts to format ready to be used in apollos.
+	 * 
+	 * This is also a subject - it optionally reports the load status while loading.
+	 * The loading operatoin is also cancellable.
+	 *  
+	 * @author Brook Novak
+	 *
+	 */
+	public class AudioFileLoader extends AbstractSubject {
+		
+		/**
+		 * Util constructor
+		 */
+		private AudioFileLoader() {}
+	
+		private boolean cancelLoad = false;
+		
+		/**
+		 * Cancels current load operation if any.
+		 */
+		public void cancelLoad() {
+			cancelLoad = true;
+		}
+
+		/**
+		 * Loads an audio file into memory. The loaded bytes are converted if they need to be
+		 * in order to be used in apollos.
+		 * 
+		 * This operation can be cancelled via cancelLoad. 
+		 * 
+		 * @param file 
+		 * 		The file to load. must not be null and the file must exist.
+		 * 
+		 * @param statusObserver
+		 * 		If given (an be null), then the statusObserver will be notified with 
+		 * 		LOAD_STATUS_REPORT AudioSubjectChangedEvent events (on the calling thread).
+		 * 		The state will be a float with the current percent (between 0.0 and 1.0 inclusive).
+		 * 		The subject source will be this instance.
+		 * 
+		 * @return
+		 * 		The loaded audio - in a format that can be used in apollos.
+		 * 		Null if cancelled.
+		 * 
+		 * @throws IOException
+		 * 		If an exception occured while loade audio bytes.
+		 * 		
+		 * @throws UnsupportedAudioFileException
+		 * `	if the File does not point to valid audio file data recognized by the system.
+		 * 
+		 */
+		public LoadedAudioData loadAudioFile(File file, Observer statusObserver) throws IOException, UnsupportedAudioFileException {
+			assert(file != null && file.exists());
+			
+			cancelLoad = false;
+			
+			AudioInputStream fileStream = null; // stream directly from file
+			AudioInputStream decodeStream = null; // stream decoding fileStream
+			
+			try {
+				
+				// Get the audio form information
+				AudioFileFormat fformat = AudioSystem.getAudioFileFormat(file);
+				
+				// Get the file stream
+				fileStream = AudioSystem.getAudioInputStream(file);
+				
+				AudioInputStream sampleStream;
+				AudioFormat sampleFormat;
+				boolean isConverting;
+				
+				// Check if file needs to convert
+				if (!SampledAudioManager.getInstance().isFormatSupportedForPlayback(fformat.getFormat())) {
+
+					sampleFormat = SampledAudioManager.getInstance().getDefaultPlaybackFormat();
+					
+					if (!AudioSystem.isConversionSupported(sampleFormat, fformat.getFormat())) {
+						throw new UnsupportedAudioFileException("Audio not supported");
+					}
+					
+					decodeStream = AudioSystem.getAudioInputStream(sampleFormat, fileStream);
+					sampleStream = decodeStream;
+					isConverting = true;
+
+					
+				} else { // otherwise read bytes directly from file stream
+					sampleStream = fileStream;
+					sampleFormat = fformat.getFormat();
+					isConverting = false;
+				}
+				
+				assert (SampledAudioManager.getInstance().isFormatSupportedForPlayback(sampleFormat));
+	
+				// Initialize the ByteBuffer - and size if possible (not possible for vairable frame size encoding)
+				ByteArrayOutputStream loadedBytes = (fformat.getFrameLength() != AudioSystem.NOT_SPECIFIED) ?
+						new ByteArrayOutputStream(fformat.getFrameLength() * sampleFormat.getFrameSize()) : 
+						new ByteArrayOutputStream();
+
+				byte[] buffer = new byte[sampleFormat.getFrameSize() * (int)sampleFormat.getFrameRate()];
+				int bytesRead = 0;
+				int totalBytesRead = 0;
+				float percent = 0.0f;
+				
+				while (!cancelLoad) { // keep loading until cancelled
+					
+					// Report current percent to given observer (if any)
+					if (statusObserver != null) {
+						statusObserver.modelChanged(this, new SubjectChangedEvent(
+								ApolloSubjectChangedEvent.LOAD_STATUS_REPORT, new Float(percent)));
+					}
+	
+					// Read bytes from the stream into memory
+					bytesRead = sampleStream.read(buffer, 0, buffer.length);
+					
+					if (bytesRead == -1) break; // complete
+					
+					loadedBytes.write(buffer, 0, bytesRead);
+					
+					// Update percent complete
+					totalBytesRead += bytesRead;
+					percent = (float)totalBytesRead / (float)fformat.getByteLength();
+				}
+	
+				loadedBytes.close();
+				
+				// if incomplete - then cancelled.
+				if (bytesRead != -1) {
+					assert(cancelLoad);
+					return null;
+				}
+	
+				// otherwise return the loaded audio
+				return new LoadedAudioData(loadedBytes.toByteArray(), sampleFormat, isConverting);
+			
+			} finally { // Always ensure that decoder and file streams are closed
+
+				if (decodeStream != null) {
+					decodeStream.close();
+				}
+				
+				if (fileStream != null) {
+					fileStream.close();
+				}
+	
+			}
+		}
+	
+	}
+	
+	/**
+	 * For manual testing purposes
+	 * 
+	 * @param args
+	 * 	1 Arg - the file to convert
+	 */
+	public static void main(String[] args) {
+		if (args.length == 1) {
+			
+			System.out.println("Testing with " + args[0]);
+			
+			try {
+				LoadedAudioData loaded = loadAudioFile(new File(args[0]), null);
+				if (loaded != null && loaded.getAudioBytes() != null) {
+					
+					System.out.println("Loaded file into internal format: " + loaded.getAudioFormat());
+					
+					if (loaded.wasConverted()) {
+						savePCMAudioToWaveFile(
+							args[0] + "_pre-converted.wav", 
+							loaded.getAudioBytes(),
+							loaded.getAudioFormat());
+					}
+					
+
+					AudioFormat lowQualFormat = new AudioFormat(
+							22050,
+							8, // 8-bit
+							1, // mono
+							loaded.getAudioFormat().getEncoding().toString().endsWith("SIGNED"), 
+							true); // Big endian 
+
+					byte[] stdData = AudioIO.convertAudioBytes(
+							loaded.getAudioBytes(), 
+							loaded.getAudioFormat(),
+							lowQualFormat);
+					
+					if (stdData != null) {
+						
+						System.out.println("Saving file into low-quality format: " + lowQualFormat);
+						
+						savePCMAudioToWaveFile(
+								args[0] + "_converted.wav", 
+								stdData,
+								lowQualFormat);
+						
+						System.out.println("Conversion succeeded");
+						return;
+						
+					
+					}
+
+					
+				}
+				
+			} catch (IOException e) {
+				e.printStackTrace();
+			} catch (UnsupportedAudioFileException e) {
+				e.printStackTrace();
+			}
+			
+			System.err.println("Conversion failed");
+			return;
+			
+		}
+		
+		System.err.println("Must supply 1 argument: the file path to convert");
+		
+		
+		
+	}
+
+}
Index: /trunk/src_apollo/org/apollo/io/AudioPathManager.java
===================================================================
--- /trunk/src_apollo/org/apollo/io/AudioPathManager.java	(revision 315)
+++ /trunk/src_apollo/org/apollo/io/AudioPathManager.java	(revision 315)
@@ -0,0 +1,245 @@
+package org.apollo.io;
+
+import java.io.BufferedReader;
+import java.io.File;
+import java.io.FileReader;
+import java.io.FileWriter;
+import java.io.IOException;
+import java.io.UnsupportedEncodingException;
+import java.util.Random;
+
+import org.apollo.audio.structure.AudioStructureModel;
+import org.apollo.util.AudioSystemLog;
+import org.apollo.util.RegExpFileFilter;
+
+/**
+ * Generates audio paths.
+ * 
+ * @author Brook Novak
+ *
+ */
+public class AudioPathManager {
+
+	private AudioPathManager() {
+		// util constructor
+	}
+
+	/** Suffixed with the native file separator */
+	public static final String AUDIO_HOME_DIRECTORY = "audio" + File.separatorChar;
+	//	TODO: TMP FOR WORKING IN LINUX LABS: Avoids using my quota
+	//public static final String AUDIO_HOME_DIRECTORY = "/research/bjn8/apollosaudio/";
+	
+	/** Stores all virtual names created by this repository. */
+	private static final String VIRTUAL_NAME_BASEFILE = ".vnames";
+
+	public static final int PREAMBLE_LENGTH = 6;
+
+	private static long counter = 0;
+
+	private static Random rand = new Random(System.currentTimeMillis());
+
+	/**
+	 * Class initializer.
+	 * Ensures that the home directory(ies) exist and preps the counter
+	 */
+	static {
+
+		// Ensure audio directory exists
+		File dir = new File(AUDIO_HOME_DIRECTORY);
+		if (!dir.exists()) {
+			if (!dir.mkdir()) {
+				AudioSystemLog.println("Unable to create missing audio home directory");
+			}
+		}
+
+		// Load counter
+		loadCounter(dir);
+	}
+
+	/**
+	 * Loads the counter
+	 * @param dir The dir where the audio files live.
+	 */
+	public static void loadCounter(File dir) {
+		String[] files = dir.list(new RegExpFileFilter("^[A-Z]+\\d+.*$"));
+		long count = 0;
+		if (files != null) {
+			for (String f : files) {
+	
+				int start = -1, end = -1;
+				for (int i = 0; i < f.length(); i++) {
+					if (Character.isDigit(f.charAt(i)) && start == -1) {
+						start = i;
+					} else if (!Character.isDigit(f.charAt(i)) && start != -1) {
+						end = i;
+						break;
+					}
+				}
+				
+				if (start >= 0 && end == -1) end = f.length();
+	
+				if (start > 0 && end >= start) { // not start >= 0 since missing preable
+	
+					String tmp = f.substring(start, end);
+					long l = Long.parseLong(tmp);
+					if (l >= count)
+						count = l + 1;
+				}
+			}
+		}
+
+		counter = count;
+	}
+	
+	/**
+	 * @return The current counter.
+	 */
+	public static long getCounter() {
+		return counter;
+	}
+
+	/**
+	 * Generates a filename that is unique within the AUDIO_HOME_DIRECTORY.
+	 * 
+	 * @param extension
+	 * 			The extension of the filename to generate. Exclude period.
+	 * 
+	 * @return 
+	 * 		A free unused filename with the given extension. Excludes directory.
+	 * 		Even if the path return is not used, it will never be re-generated.
+	 */
+	public static String generateLocateFileName(String extension) {
+
+		String filename = generateRandomFilenameBase() + "." + extension;
+		
+		File f = new File(AUDIO_HOME_DIRECTORY + filename);
+
+		if (AudioStructureModel.getInstance().getTrackGraphInfo(filename, null) != null ||
+				f.exists()) { // recurse: until found a free path:
+			return generateLocateFileName(extension);
+		}
+
+		return filename;
+	}
+	
+	private static String generateRandomFilenameBase() {
+		
+		counter++; 
+		if (counter < 0) counter = 0; // wraps positivey
+
+		// Generate random alpha preamble
+		byte[] bytes = new byte[PREAMBLE_LENGTH];
+		rand.nextBytes(bytes);
+		
+
+		for (int i = 0; i < PREAMBLE_LENGTH; i++) {
+			if (bytes[i] < 0) bytes[i] *= -1;
+			int alphacap = (65 + (Math.abs(bytes[i]) % 26)); // A-Z in US ASCII encoding
+			bytes[i] = (byte) alphacap;
+			
+		}
+
+		// Decode ASCII Byte array to javas UTF-16 encoding
+		String preamble = null;
+
+		try {
+			preamble = new String(bytes, "US-ASCII");
+		} catch (UnsupportedEncodingException e) {
+			e.printStackTrace();
+			preamble = "AUDIO";
+		}
+
+		// Build the filename string
+		return preamble + counter;
+		
+	}
+
+	
+	
+	/**
+	 * Lots of room for improvement here, ensures that a virtual filename is unique .. stores 
+	 * allocated virtual filenames on file.
+	 * 
+	 * @return
+	 * 			The new virtual filename.
+	 */
+	public static String generateVirtualFilename() {
+
+		// Ensure that the name base file exists. Assumes the audio home directory exists.
+		File vnameBase = new File(AUDIO_HOME_DIRECTORY + VIRTUAL_NAME_BASEFILE);
+		if (!vnameBase.exists()) {
+			try {
+				vnameBase.createNewFile();
+			} catch (IOException e) {
+				e.printStackTrace();
+			}
+		}
+		
+		String vname = null;
+		
+		while (vname == null) {
+			vname = generateRandomFilenameBase() + ".vrt";
+			assert(vname != null);
+			
+			BufferedReader in = null;
+			String line = null;
+			
+			try {
+				
+				// Open the vbase for reading
+				in = new BufferedReader(new FileReader(vnameBase)); 
+				
+				// Read the sbase file and check all names
+				while ((line = in.readLine()) != null) {
+					line = line.trim();
+					if (line.equalsIgnoreCase(vname)) break;
+				}
+				
+			} catch (Exception e) {
+				e.printStackTrace();
+				break; // just use the last generated filename...
+			
+			// Clean up
+			} finally {
+				if (in != null) {
+					try {
+						in.close();
+					} catch (IOException e) {
+						e.printStackTrace();
+					}
+				}
+			}
+			
+			// Check to see if name is infact unique
+			if (AudioStructureModel.getInstance().getLinkedTrackGraphInfo(vname, null) == null &&
+					(line == null ||  !line.equalsIgnoreCase(vname))) {
+				// If so, then enter new entry into the db.
+				FileWriter out = null;
+				
+				try {
+					// Open the vbase for appending
+					out = new FileWriter(vnameBase, true); 
+					out.write("\n" + vname);
+					
+				} catch (IOException e) {
+					e.printStackTrace();
+					
+				// Clean up
+				} finally {
+					if (out != null) {
+						try {
+							out.close();
+						} catch (IOException e) {
+							e.printStackTrace();
+						}
+					}
+				}
+
+			} else vname = null;
+		}
+		
+		// Return the generated vname
+		return vname;
+
+	}
+}
Index: /trunk/src_apollo/org/apollo/io/IconRepository.java
===================================================================
--- /trunk/src_apollo/org/apollo/io/IconRepository.java	(revision 315)
+++ /trunk/src_apollo/org/apollo/io/IconRepository.java	(revision 315)
@@ -0,0 +1,80 @@
+package org.apollo.io;
+
+import java.awt.BasicStroke;
+import java.awt.Color;
+import java.awt.Graphics2D;
+import java.awt.Image;
+import java.awt.image.BufferedImage;
+import java.net.URL;
+import java.util.HashMap;
+
+import javax.swing.Icon;
+import javax.swing.ImageIcon;
+
+/**
+ * An icon provider. Only loads icons once and when they are requested (somple proxy approach).
+ * 
+ * @author Brook Novak
+ *
+ */
+public final class IconRepository {
+	
+	private static HashMap<String, Icon> _loadedIcons = new HashMap<String, Icon>();
+	private static Icon missingIcon = null;
+
+	private IconRepository() {}
+	
+	private static Icon getMissingIcon() {
+		
+		if (missingIcon == null) {
+			
+			Image image = new BufferedImage(16, 16, BufferedImage.TYPE_INT_RGB);
+			Graphics2D g = (Graphics2D)image.getGraphics();
+			
+			g.setStroke(new BasicStroke(2.0f));
+			
+			// White square with X
+			g.setColor(Color.WHITE);
+			g.fillRect(0, 0, 16, 16);
+			
+			g.setColor(Color.RED);
+			g.drawLine(0, 0, 16, 16);
+			g.drawLine(0, 16, 16, 0);
+			
+			missingIcon = new ImageIcon(image);
+		}
+			
+		return missingIcon;
+	}
+	
+	/**
+	 * 
+	 * @param name Exclude path / package
+	 * @return
+	 */
+	public static Icon getIcon(String name) {
+		Icon icon = _loadedIcons.get(name);
+		
+		if (icon == null) {
+			try {
+				
+				URL url = ClassLoader.getSystemResource("org/apollo/icons/" + name);
+				
+				if (url != null) {
+					icon = new ImageIcon(url);
+				}
+				
+			} catch (Exception e) {
+				e.printStackTrace();
+			}
+			
+			if (icon == null) { 
+				System.err.println("WARNING: Cannot find icon named \"" + name +"\"");
+				return getMissingIcon();
+			}
+		} 
+		
+		return icon;
+	}
+	
+}
Index: /trunk/src_apollo/org/apollo/io/LoadedAudioData.java
===================================================================
--- /trunk/src_apollo/org/apollo/io/LoadedAudioData.java	(revision 315)
+++ /trunk/src_apollo/org/apollo/io/LoadedAudioData.java	(revision 315)
@@ -0,0 +1,36 @@
+package org.apollo.io;
+
+import javax.sound.sampled.AudioFormat;
+
+/**
+ * Immutable class storing loaded audio information.
+ * 
+ * @author Brook Novak
+ *
+ */
+public final class LoadedAudioData {
+	
+	private byte[] audioBytes;
+	private AudioFormat audioFormat;
+	private boolean wasConverted;
+	
+	public LoadedAudioData(byte[] audioBytes, AudioFormat audioFormat, boolean wasConverted) {
+		this.audioBytes = audioBytes;
+		this.audioFormat = audioFormat;
+		this.wasConverted = wasConverted;
+	}
+
+	public byte[] getAudioBytes() {
+		return audioBytes;
+	}
+
+	public AudioFormat getAudioFormat() {
+		return audioFormat;
+	}
+
+	public boolean wasConverted() {
+		return wasConverted;
+	}
+	
+	
+}
Index: /trunk/src_apollo/org/apollo/io/MixIO.java
===================================================================
--- /trunk/src_apollo/org/apollo/io/MixIO.java	(revision 315)
+++ /trunk/src_apollo/org/apollo/io/MixIO.java	(revision 315)
@@ -0,0 +1,285 @@
+package org.apollo.io;
+
+import java.io.BufferedReader;
+import java.io.File;
+import java.io.FileReader;
+import java.io.FileWriter;
+import java.io.IOException;
+import java.util.List;
+
+import org.apollo.audio.util.TrackMixSubject;
+
+/**
+ * A naive approach for saving and loading of mixes....
+ * Very hacky at this point and if released should re-think how banks are saved and loaded.
+ * 
+ * @author Brook Novak
+ *
+ */
+public class MixIO {
+
+	/**
+	 * Loads the master mix
+	 * 
+	 * @param masterMixFilepath
+	 * @return
+	 * 		The solo prefix.
+	 */
+	public static TrackMixSubject loadMasterMix(String masterMixFilepath) {
+		assert(masterMixFilepath != null);
+		
+		File mixFile = new File(masterMixFilepath);
+		if (!mixFile.exists()) {
+			try {
+				mixFile.createNewFile();
+			} catch (IOException e) {
+				e.printStackTrace();
+				System.err.println("ERROR: The master mix has been lost");
+				return null;
+			}
+		}
+
+		BufferedReader in = null;
+		
+		TrackMixSubject masterMix = null;
+		
+		try {
+			
+			// Open the vbase for reading
+			in = new BufferedReader(new FileReader(mixFile)); 
+			
+			String line = null;
+			// Read the sbase file and check all names
+			while ((line = in.readLine()) != null) {
+				line = line.trim();
+				masterMix = TrackMixSubject.fromString(line);
+				if (masterMix != null) break;
+			}
+			
+		} catch (Exception e) {
+			e.printStackTrace();
+		// Clean up
+		} finally {
+			if (in != null) {
+				try {
+					in.close();
+				} catch (IOException e) {
+					e.printStackTrace();
+				}
+			}
+		}
+
+		return masterMix;
+	}
+	
+	/**
+	 * Saves a master mix
+	 * 
+	 * @param masterMixFilepath
+	 * 		Where to save
+	 * 
+	 * @param mmix
+	 * 		The mix to save as a master mix
+	 */
+	public static void saveMasterMix(String masterMixFilepath, TrackMixSubject mmix) {
+		
+		assert(masterMixFilepath != null);
+		assert(mmix != null);
+		
+		File mixFile = new File(masterMixFilepath);
+		if (!mixFile.exists()) {
+			try {
+				mixFile.createNewFile();
+			} catch (IOException e) {
+				e.printStackTrace();
+				System.err.println("ERROR: Unable to save master mix");
+				return;
+			}
+		}
+		
+		FileWriter out = null;
+		
+		try {
+			out = new FileWriter(mixFile, false); 
+			out.write(mmix.toParseableString());
+			
+		} catch (IOException e) {
+			e.printStackTrace();
+			
+		// Clean up
+		} finally {
+			if (out != null) {
+				try {
+					out.close();
+				} catch (IOException e) {
+					e.printStackTrace();
+				}
+			}
+		}
+		
+	}
+	
+
+	/**
+	 * Saves banks to file
+	 * 
+	 * @param mixBatchPath
+	 * 		Where to save
+	 * 
+	 * @param banks
+	 * 		The mixes to write to file
+	 * 
+	 * @param usingLocalFlags
+	 * 		Parallel list with banks. The isUsingLocal flag...
+	 */
+	public static void saveBanks(String mixBatchPath, List<TrackMixSubject> banks, List<Boolean> usingLocalFlags) {
+		
+		assert(mixBatchPath != null);
+		assert(banks != null);
+		assert(usingLocalFlags != null);
+		assert(usingLocalFlags.size() == banks.size());
+		
+		File mixFile = new File(mixBatchPath);
+		if (!mixFile.exists()) {
+			try {
+				mixFile.createNewFile();
+			} catch (IOException e) {
+				e.printStackTrace();
+				System.err.println("ERROR: Unable to save mixes");
+				return;
+			}
+		}
+		
+		FileWriter out = null;
+		
+		try {
+
+			out = new FileWriter(mixFile, false); 
+			
+			for (int i = 0; i < banks.size(); i++) {
+				
+				TrackMixSubject mix = banks.get(i);
+				assert(mix != null);
+				
+				Boolean flag = usingLocalFlags.get(i);
+				assert(flag != null);
+				
+				out.write(mix.toParseableString() + "\n");
+				out.write(flag.booleanValue() ? "1\n" : "0\n");
+			}
+
+		} catch (IOException e) {
+			e.printStackTrace();
+			
+		// Clean up
+		} finally {
+			if (out != null) {
+				try {
+					out.close();
+				} catch (IOException e) {
+					e.printStackTrace();
+				}
+			}
+		}
+		
+	}
+	
+	/**
+	 * 
+	 * @param banksFilepath
+	 * 		The filepath of the banks file to load.
+	 * 
+	 * @param banks
+	 * 		A list to load the banks into. Must not be null - must be empty.
+	 * 
+	 * @param usingLocalFlags
+	 * 		A list to load the channel flags into... Must not be null - must be empty.
+	 * 
+	 * @return
+	 * 		True of success. False if IO operatoin failed / file does not exist. 
+	 */
+	public static boolean loadBanks(
+			List<TrackMixSubject> banks,
+			List<Boolean> usingLocalFlags,
+			String banksFilepath) {
+		
+		assert(banks != null);
+		assert(usingLocalFlags != null);
+		assert(banksFilepath != null);
+		assert(banks.isEmpty());
+		assert(usingLocalFlags.isEmpty());
+		
+		File mixFile = new File(banksFilepath);
+		if (!mixFile.exists()) {
+			try {
+				mixFile.createNewFile();
+			} catch (IOException e) {
+				e.printStackTrace();
+				System.err.println("ERROR: The master mix has been lost");
+			}
+			return false;
+		}
+
+		BufferedReader in = null;
+		
+		try {
+			
+			// Open the vbase for reading
+			in = new BufferedReader(new FileReader(mixFile)); 
+			
+			String line = null;
+			// Read the sbase file...
+			boolean waitingOnFlag = false;
+			while ((line = in.readLine()) != null) {
+				
+				line = line.trim();
+				if (line.length() == 0) continue;
+				
+				TrackMixSubject mix = TrackMixSubject.fromString(line);
+				
+				if (mix != null) {
+					
+					if (waitingOnFlag) { // missing flag for last mix line
+						usingLocalFlags.add(new Boolean(false)); // default to false
+					}
+					
+					banks.add(mix);
+					waitingOnFlag = true;
+					
+				} else if (waitingOnFlag) { // waiting on flag?
+					
+					if (line.equals("1")) {
+						usingLocalFlags.add(new Boolean(true));
+						waitingOnFlag = false;
+					} else if (line.equals("0")) {
+						usingLocalFlags.add(new Boolean(false));
+						waitingOnFlag = false;
+					}
+				}
+				
+				
+			}
+			
+			if (usingLocalFlags.size() == (banks.size() - 1)) { // missing last flag
+				usingLocalFlags.add(new Boolean(false));
+			}
+			
+			assert(usingLocalFlags.size() == banks.size());
+			
+		} catch (Exception e) {
+			e.printStackTrace();
+		// Clean up
+		} finally {
+			if (in != null) {
+				try {
+					in.close();
+				} catch (IOException e) {
+					e.printStackTrace();
+				}
+			}
+		}
+
+		return true;
+	}
+	
+}
Index: /trunk/src_apollo/org/apollo/io/SampledAudioFileImporter.java
===================================================================
--- /trunk/src_apollo/org/apollo/io/SampledAudioFileImporter.java	(revision 315)
+++ /trunk/src_apollo/org/apollo/io/SampledAudioFileImporter.java	(revision 315)
@@ -0,0 +1,38 @@
+package org.apollo.io;
+
+import java.awt.Point;
+import java.io.File;
+import java.io.IOException;
+
+import org.apollo.widgets.SampledTrackEvo4;
+import org.expeditee.gui.DisplayIO;
+import org.expeditee.importer.FileImporter;
+import org.expeditee.items.Item;
+
+/**
+ * Imports sampled audio files as track widgets into the current frame.
+ * 
+ * @author Brook Novak
+ *
+ */
+public class SampledAudioFileImporter implements FileImporter {
+
+	public Item importFile(File f, Point location) throws IOException {
+
+		if (location == null || !AudioIO.canImportFile(f) ||
+				DisplayIO.getCurrentFrame() == null) return null;
+		
+		SampledTrackEvo4 trackWidget = SampledTrackEvo4.createFromFile(
+				f,
+				DisplayIO.getCurrentFrame(), 
+				location.x, 
+				location.y);
+		
+		// Add the sampled track widget to the current frame
+		DisplayIO.getCurrentFrame().addAllItems(trackWidget.getItems());
+		
+		return trackWidget.getSource(); // Don't allow for other importers to deal with this file
+		
+	}
+
+}
Index: /trunk/src_apollo/org/apollo/items/EmulatedTextItem.java
===================================================================
--- /trunk/src_apollo/org/apollo/items/EmulatedTextItem.java	(revision 315)
+++ /trunk/src_apollo/org/apollo/items/EmulatedTextItem.java	(revision 315)
@@ -0,0 +1,645 @@
+package org.apollo.items;
+
+import java.awt.AWTException;
+import java.awt.Color;
+import java.awt.Component;
+import java.awt.Cursor;
+import java.awt.Font;
+import java.awt.Graphics;
+import java.awt.Graphics2D;
+import java.awt.Point;
+import java.awt.Rectangle;
+import java.awt.Robot;
+import java.awt.Shape;
+import java.awt.event.KeyEvent;
+import java.awt.event.KeyListener;
+import java.awt.event.MouseEvent;
+import java.awt.event.MouseListener;
+import java.awt.event.MouseMotionListener;
+import java.awt.geom.Area;
+import java.awt.geom.Point2D;
+import java.util.LinkedList;
+import java.util.List;
+
+import javax.swing.JComponent;
+import javax.swing.SwingUtilities;
+
+import org.expeditee.gui.Browser;
+import org.expeditee.gui.DisplayIO;
+import org.expeditee.gui.Frame;
+import org.expeditee.gui.FrameGraphics;
+import org.expeditee.gui.FrameMouseActions;
+import org.expeditee.gui.MouseEventRouter;
+import org.expeditee.gui.Popup;
+import org.expeditee.gui.PopupManager;
+import org.expeditee.items.Text;
+import org.expeditee.items.Item.HighlightMode;
+
+/**
+ * 
+ * Its a terrible hack - but it will do.
+ * 
+ * @author Brook Novak
+ *
+ */
+public class EmulatedTextItem {
+
+	private static final long serialVersionUID = 1L;
+	
+	private static Robot robot;
+
+	private List<TextChangeListener> textChangeListeners = new LinkedList<TextChangeListener>();
+	
+	private RestrictedTextItem emulatedSource;
+	private LabelEditPopup popup = null;
+	private JComponent parentComponant = null;
+	private Point masterOffset;
+	
+	private static final Font DEFAULT_FONT = new Font("Serif-Plain", Font.PLAIN, 12);
+	
+	static {
+		try {
+			robot = new Robot();
+		} catch (AWTException e) {
+			e.printStackTrace();
+		}
+	}
+	
+	/**
+	 * Constructor.
+	 * 
+	 * @param parent
+	 * 		Must not be null: the parent that lives directly on the content pane.
+	 * 
+	 * @param offset
+	 * 		The offset to add to the label position - from the parents top left corner. 
+	 * 		Must not be null.
+	 */
+	public EmulatedTextItem(JComponent parent, Point offset) {
+		assert(parent != null);
+		assert(offset != null);
+		
+		parentComponant = parent;
+		masterOffset = offset;
+		emulatedSource = new RestrictedTextItem();
+		emulatedSource.setPosition(parent.getX() + offset.x, parent.getY() + offset.y);
+		emulatedSource.setFont(DEFAULT_FONT);
+
+	}
+	
+	
+	public void setFontStyle(int style) {
+		emulatedSource.setFont(emulatedSource.getFont().deriveFont(style));
+	}
+	
+	public void setFontSize(float size) {
+		emulatedSource.setSize(size);
+	}
+	
+	
+	/**
+	 * Determines whether or no a mouse event is on the emulated text item.
+	 * @param swingME
+	 * 		Can be null.
+	 * 
+	 * @return
+	 * 		True if the event is over the emulated item.
+	 */
+	private boolean isOnEmulatedText(MouseEvent swingME) {
+		if (swingME == null) return false;
+		Point p = convertToExpediteeSpace(swingME.getLocationOnScreen());
+		return isOnEmulatedText(p.x, p.y);
+	}
+	
+	private boolean isOnEmulatedText(int expX, int expY) {
+		return emulatedSource.contains(expX, expY);
+	}
+	
+	private Point convertToExpediteeSpace(Point screenPos) {
+		assert(screenPos != null);
+		
+		Point expPoint = new Point(0, 0);
+		SwingUtilities.convertPointToScreen(expPoint, Browser._theBrowser.getContentPane());
+		
+		return new Point(screenPos.x - expPoint.x, screenPos.y - expPoint.y);
+	}
+	
+	/**
+	 * Applies highlights - shows popup
+	 * 
+	 * @param e
+	 * @param parent
+	 */
+	public boolean onMouseMoved(MouseEvent e, Component parent) {
+
+		if (isOnEmulatedText(e)) {
+			
+			if (emulatedSource.getHighlightMode() != HighlightMode.Normal) {
+				emulatedSource.setHighlightMode(HighlightMode.Normal);
+				repaint();
+			}
+			
+			if (parent != popup && 
+					(emulatedSource.getBoundsWidth() + masterOffset.x) > (parent.getWidth() - 10)) { // have some give to transition over widget edge
+				if (popup == null) {
+					popup = new LabelEditPopup();
+				}
+				
+				int x = (emulatedSource.getPolygon() == null) ? parent.getX() + masterOffset.x : emulatedSource.getPolygon().getBounds().x;
+				int y = (emulatedSource.getPolygon() == null) ? parent.getY() + masterOffset.y : emulatedSource.getPolygon().getBounds().y;
+				x -= 2;
+				y -= 2;
+
+				if (!PopupManager.getInstance().isShowing(popup)) {
+					PopupManager.getInstance().showPopup(
+							popup, 
+							new Point(x, y), 
+							popup);  
+				}
+				
+			}
+			
+			return true;
+			
+		} else {
+			
+			lostFocus();
+
+		}
+		
+		return false;
+		
+	}
+	
+	/**
+	 * 
+	 * @param e
+	 * 
+	 * @param parent
+	 */
+	public boolean onKeyPressed(KeyEvent e, Component parent) {
+
+		MouseEvent currentME = MouseEventRouter.getCurrentMouseEvent();
+		
+		if (currentME == null) return false;
+		
+		Point p = convertToExpediteeSpace(currentME.getLocationOnScreen());
+
+		// If so and the event really comes from its parent ...
+		if (currentME.getComponent() == parent && 
+				(isOnEmulatedText(p.x, p.y))) {
+			
+			int yOffset = (parent == popup) ? -6 : -6;
+			
+			Browser._theBrowser.setCursor(Cursor.getPredefinedCursor(Cursor.TEXT_CURSOR));
+
+			// If a alphanumeric text entry...
+			if (!e.isActionKey() && 
+					(Character.isLetterOrDigit(e.getKeyChar()) || 
+							(e.getKeyChar() == KeyEvent.VK_BACK_SPACE && !emulatedSource.isEmpty()) ||
+							(e.getKeyChar() == KeyEvent.VK_DELETE && !emulatedSource.isEmpty()) ||
+							e.getKeyChar() == KeyEvent.VK_SPACE ||
+							e.getKeyChar() == KeyEvent.VK_UNDERSCORE)) {
+
+
+				Rectangle[] oldArea = emulatedSource.getDrawingArea();
+				int oldWidth = emulatedSource.getBoundsWidth();
+				
+				// Insert the text according to the current mouse position
+				Point2D.Float newMouse = emulatedSource.insertChar(
+						e.getKeyChar(), 
+						p.x, 
+						p.y);
+				
+				List<String> lines = emulatedSource.getTextList();
+				if (lines != null && lines.size() > 1) {
+					emulatedSource.setText(lines.get(0));
+				}
+				
+				// Notify observers
+				fireTextChanged();
+
+				// Move "cursured mouse"
+				if (newMouse != null) {
+					Point bloc = Browser._theBrowser.getContentPane().getLocationOnScreen();
+					robot.mouseMove(bloc.x + (int)newMouse.x, bloc.y + (int)newMouse.y + yOffset);
+				}
+				
+				// Invalidate emulated item if not in popup (popup will naturally handle invalidation)
+				if (parent != popup) {
+
+					if (oldWidth > emulatedSource.getBoundsWidth()) {
+						for (Rectangle r : oldArea) 
+							FrameGraphics.invalidateArea(r);
+					}
+					
+					repaint();
+
+				}
+				
+			} else if (e.getKeyCode() == KeyEvent.VK_LEFT || e.getKeyCode() == KeyEvent.VK_RIGHT) {
+				
+				Point2D.Float newMouse = emulatedSource.moveCursor(
+						(e.getKeyCode() == KeyEvent.VK_LEFT) ? Text.LEFT : Text.RIGHT, 
+						(float)p.getX(), (float)p.getY(), false);
+				
+				if (newMouse != null) {
+					Point bloc = Browser._theBrowser.getContentPane().getLocationOnScreen();
+					robot.mouseMove(bloc.x + (int)newMouse.x, bloc.y + (int)newMouse.y + yOffset);
+				}
+
+			}
+			
+			if (emulatedSource.hasSelection()) {
+				emulatedSource.clearSelection();
+				repaint();
+			}
+			
+			return true;
+			
+		}
+		
+		return false;
+	}
+	
+	/**
+	 * 
+	 * @param e
+	 * 
+	 * @param parent
+	 */
+	public boolean onKeyReleased(KeyEvent e, Component parent) {
+		return isOnEmulatedText(MouseEventRouter.getCurrentMouseEvent());
+		
+	}
+	
+	
+	public boolean onMouseReleased(MouseEvent e) {
+		
+		Point p = convertToExpediteeSpace(e.getLocationOnScreen());
+
+		// If so and the event really comes from its parent ...
+		if (isOnEmulatedText(p.x, p.y)) {
+			
+			
+			String toMoveIntoFreespace = null;
+			
+			if (e.getButton() == MouseEvent.BUTTON1) {
+				// LINK
+			} else if (e.getButton() == MouseEvent.BUTTON2 && 
+					emulatedSource.hasSelection() && 
+					!emulatedSource.isEmpty() &&
+					emulatedSource.selectionMouseButton == MouseEvent.BUTTON2) {
+	
+				invalidate();
+				
+				toMoveIntoFreespace = emulatedSource.cutSelectedText();
+				
+			} else if (e.getButton() == MouseEvent.BUTTON3 && 
+					emulatedSource.hasSelection() && 
+					!emulatedSource.isEmpty() &&
+					emulatedSource.selectionMouseButton == MouseEvent.BUTTON3) {
+				toMoveIntoFreespace = emulatedSource.copySelectedText();
+				
+			} else if (e.getButton() == MouseEvent.BUTTON3 && 
+					!emulatedSource.hasSelection() && 
+					!emulatedSource.isEmpty()) {
+				toMoveIntoFreespace = emulatedSource.getText();
+			}
+			
+			if (toMoveIntoFreespace != null) {
+				
+				Frame target = DisplayIO.getCurrentFrame();
+				if (target != null) {
+					Text selectionCopy = new Text(target.getNextItemID(), toMoveIntoFreespace);
+					selectionCopy.setPosition(p.x, p.y);
+					selectionCopy.setSize(emulatedSource.getSize());
+					FrameMouseActions.pickup(selectionCopy);
+					lostFocus();
+				}
+				
+				
+			}
+			
+			emulatedSource.clearSelection();
+			repaint();
+			
+			return true;
+		}
+		
+		return false;
+	}
+	
+	public boolean onMousePressed(MouseEvent e) {
+
+		Point p = convertToExpediteeSpace(e.getLocationOnScreen());
+
+		// If so and the event really comes from its parent ...
+		if (isOnEmulatedText(p.x, p.y)) {
+			
+			emulatedSource.clearSelection();
+			emulatedSource.setSelectionStart(p.x, p.y, e.getButton());
+
+			repaint();
+			
+			return true;
+		}
+		
+		return false;
+	}
+	
+	public boolean onMouseClicked(MouseEvent e) {
+		return isOnEmulatedText(e);
+	}
+	public boolean onMouseDragged(MouseEvent e) {
+
+		Point p = convertToExpediteeSpace(e.getLocationOnScreen());
+
+		// If so and the event really comes from its parent ...
+		if (isOnEmulatedText(p.x, p.y)) {
+
+			Browser._theBrowser.setCursor(Cursor.getPredefinedCursor(Cursor.TEXT_CURSOR));
+			
+			emulatedSource.setSelectionEnd(p.x, p.y);
+			repaint();
+			
+			return true;
+		}
+		
+		return false;
+	}
+	
+	public void addTextChangeListener(TextChangeListener listener) {
+		assert(listener != null);
+		if (!textChangeListeners.contains(listener))
+			textChangeListeners.add(listener);
+	}
+	
+	public void lostFocus() {
+		Browser._theBrowser.setCursor(Cursor.getPredefinedCursor(Cursor.DEFAULT_CURSOR));
+		
+		boolean dirty = false;
+		if (emulatedSource.hasSelection()) {
+			emulatedSource.clearSelection();
+			dirty = true;
+		}
+		
+		if (emulatedSource.getHighlightMode() != HighlightMode.None) {
+			emulatedSource.setHighlightMode(HighlightMode.None);
+			dirty = true;
+		}
+
+		if (popup != null &&
+				PopupManager.getInstance().isShowing(popup)) {
+			PopupManager.getInstance().hidePopup(popup);
+		}
+		
+		if (dirty) repaint();
+		
+	}
+	
+	/**
+	 * Sets the text and fires a text changed event if the text has changed.
+	 * 
+	 * @param text
+	 * 		Null is allowed
+	 */
+	public void setText(String text) {
+		if (text == null) text = "";
+		
+		if (emulatedSource.getText().equals(text)) return;
+		
+		invalidate();
+		
+		this.emulatedSource.setText(text);
+		
+		invalidate();
+		
+		fireTextChanged();
+	}
+	
+	public String getText() {
+		return emulatedSource.getText();
+	}
+	
+	private void fireTextChanged() {
+		
+		for (TextChangeListener listener : textChangeListeners)
+			listener.onTextChanged(this, this.emulatedSource.getText());
+		
+	}
+
+	public void setBackgroundColor(Color c) {
+		emulatedSource.setBackgroundColor(c);
+	}
+	
+	private void repaint() {
+
+		if (popup != null && PopupManager.getInstance().isShowing(popup)) {
+			popup.invalidateAppearance();
+			FrameGraphics.refresh(true);
+		} else {
+			invalidate();
+			FrameGraphics.refresh(true);
+		}
+	}
+	
+	private void invalidate() {
+		for (Rectangle r : emulatedSource.getDrawingArea()) 
+			FrameGraphics.invalidateArea(r);
+	}
+
+	public void paint(Graphics g) {
+		
+		if ((popup != null && PopupManager.getInstance().isShowing(popup)) ||
+				!parentComponant.isShowing())
+			return;
+		
+		Point pos;
+		try {
+		pos = this.convertToExpediteeSpace(parentComponant.getLocationOnScreen());
+		} catch (Exception e) {
+			e.printStackTrace();
+			return;
+		}
+
+		paint(	g, 
+				pos.x + masterOffset.x, 
+				pos.y + masterOffset.y, 
+				parentComponant.getWidth() - masterOffset.x);
+	}
+	
+	private void paint(Graphics g, int parentX, int parentY, int maxWidth) {
+
+		if (Browser._theBrowser == null) return;
+		
+		/*int xoffset = (emulatedSource.getLink() == null) ? 6 : 18;
+		int yoffset = emulatedSource.getBoundsHeight() - 3;
+		parentX += xoffset;
+		parentY += yoffset;*/
+		
+		// Set the emulated position according to the parent position - the parent position
+		// can be from the popup or the actual parent componant
+		if (emulatedSource.getX() != parentX || emulatedSource.getY() != parentY)
+			emulatedSource.setPosition(parentX, parentY);
+
+		Shape saveClip = g.getClip();
+		
+		if (maxWidth > 0) {
+			
+			Area clip = FrameGraphics.getCurrentClip();
+
+			Area tmpClip = (clip != null) ? clip : 
+				new Area(new Rectangle(0, 0, 
+						Browser._theBrowser.getContentPane().getWidth(), 
+						Browser._theBrowser.getContentPane().getHeight()));
+			
+		/*	tmpClip.intersect(new Area(new Rectangle(0, 0,
+					parentX + maxWidth - xoffset, Browser._theBrowser.getContentPane().getHeight()
+			)));*/
+			
+			tmpClip.intersect(new Area(new Rectangle(0, 0,
+					parentX + maxWidth, Browser._theBrowser.getContentPane().getHeight()
+			)));
+	
+			// No intersection
+			if (!tmpClip.isEmpty()) 
+				g.setClip(tmpClip);
+		
+		}
+		
+		emulatedSource.paint((Graphics2D)g);
+		
+		// Restore
+		if (maxWidth > 0) {
+			g.setClip(saveClip);
+		}
+		
+		// Restore emulated position for consuming mouse movements
+		Point pos = this.convertToExpediteeSpace(parentComponant.getLocationOnScreen());
+		emulatedSource.setPosition(
+				pos.x + masterOffset.x, 
+				pos.y + masterOffset.y);
+	}
+	
+	public interface TextChangeListener
+	{
+		public void onTextChanged(Object source, String newLabel);
+	}
+	
+	private class LabelEditPopup extends Popup implements KeyListener, MouseListener, MouseMotionListener {
+		
+		private static final long serialVersionUID = 1L;
+		
+		LabelEditPopup() {
+			super.setAudoHide(true);
+			super.setConsumeBackClick(false);
+			super.setBorderThickness(0);
+			
+			updateSize();
+			
+			addKeyListener(this);
+			addMouseListener(this);
+			addMouseMotionListener(this);
+		}
+		
+		public void updateSize() {
+			setSize(emulatedSource.getBoundsWidth() + 4, emulatedSource.getBoundsHeight() + 4);
+		}
+	
+
+		public void keyPressed(KeyEvent e) {
+			onKeyPressed(e, LabelEditPopup.this);
+			updateSize();
+		}
+
+		public void keyReleased(KeyEvent e) {
+		}
+		
+
+		public void keyTyped(KeyEvent e) {
+		}
+		
+		
+		public void mouseClicked(MouseEvent e) {
+		}
+
+		public void mouseEntered(MouseEvent e) {
+		}
+
+		public void mouseExited(MouseEvent e) {
+		}
+
+		public void mousePressed(MouseEvent e) {
+			onMousePressed(e);
+		}
+
+		public void mouseReleased(MouseEvent e) {
+			onMouseReleased(e);
+			updateSize();
+		}
+		
+
+		public void mouseDragged(MouseEvent e) {
+			onMouseDragged(e);
+		}
+
+		public void mouseMoved(MouseEvent e) {
+
+		}
+
+		@Override
+		public void onHide() {
+			super.onHide();
+		}
+	
+		@Override
+		public void onShow() {
+			super.onShow();
+		}
+		
+		
+
+		@Override
+		public void paint(Graphics g) {
+			
+			EmulatedTextItem.this.paint(g, 7, 16, -1);
+
+		}
+	
+	}
+	
+	private class RestrictedTextItem extends Text {
+		
+		private Color selectionColor = null;
+		private int selectionMouseButton = -1;
+		
+		RestrictedTextItem() {
+			super(1);
+		}
+		
+		private void updateSelection(int mouseButton) {
+			selectionColor = super.getSelectionColor(mouseButton);
+			selectionMouseButton = mouseButton;
+		}
+
+		public void setSelectionStart(float mouseX, float mouseY, int mouseButton) {
+			super.setSelectionStart(mouseX, mouseY);
+			updateSelection(mouseButton);
+		}
+		
+		
+
+		@Override
+		protected Color getSelectionColor(int mouseButton) {
+			
+			if (selectionColor != null)
+				return selectionColor;
+			
+			else return super.getSelectionColor(mouseButton);
+
+		}
+		
+		
+	}
+}
Index: /trunk/src_apollo/org/apollo/items/FramePlaybackLauncher.java
===================================================================
--- /trunk/src_apollo/org/apollo/items/FramePlaybackLauncher.java	(revision 315)
+++ /trunk/src_apollo/org/apollo/items/FramePlaybackLauncher.java	(revision 315)
@@ -0,0 +1,175 @@
+package org.apollo.items;
+
+import java.awt.BasicStroke;
+import java.awt.Color;
+import java.awt.Graphics2D;
+import java.awt.Polygon;
+import java.awt.Stroke;
+
+import org.apollo.audio.SampledAudioManager;
+import org.apollo.audio.util.FrameLayoutDaemon;
+import org.apollo.audio.util.MultiTrackPlaybackController;
+import org.apollo.audio.util.SoundDesk;
+import org.apollo.audio.util.Timeline;
+import org.apollo.audio.util.MultiTrackPlaybackController.MultitrackLoadListener;
+import org.apollo.io.IconRepository;
+import org.apollo.util.AudioMath;
+import org.apollo.widgets.FramePlayer;
+import org.expeditee.gui.Browser;
+import org.expeditee.gui.DisplayIO;
+import org.expeditee.gui.Frame;
+import org.expeditee.items.Item;
+import org.expeditee.items.ItemParentStateChangedEvent;
+
+/**
+ * A transient widget that destroys self once anchored...
+ * once anchored it commences playback of a frame if there is something to play.
+ * 
+ * @author Brook Novak
+ *
+ */
+public class FramePlaybackLauncher extends Item {
+	
+	private static int WIDTH = 80;
+	private static int HEIGHT = 80;
+	private static Stroke BORDER_STOKE = new BasicStroke(1.0f);
+	
+	public FramePlaybackLauncher() {
+		this.setID(getParentOrCurrentFrame().getNextItemID());
+	}
+
+	@Override
+	public Item copy() {
+		FramePlaybackLauncher copy = new FramePlaybackLauncher();
+		
+		DuplicateItem(this, copy);
+		
+		return copy;
+	}
+
+	@Override
+	public Item merge(Item merger, int mouseX, int mouseY) {
+		return null;
+	}
+
+	@Override
+	public void paint(Graphics2D g) {
+		if (Browser._theBrowser == null) return;
+		
+		g.setColor(Color.LIGHT_GRAY);
+		g.fillRect((int)_x, (int)_y, WIDTH, HEIGHT);
+		
+		g.setColor(Color.BLACK);
+		g.setStroke(BORDER_STOKE);
+		g.drawRect((int)_x, (int)_y, WIDTH, HEIGHT);
+		
+		IconRepository.getIcon("frameplay.png").paintIcon(
+				Browser._theBrowser.getContentPane(), g, getX() + 25, getY() + 25);
+		
+	}
+
+	@Override
+	public void setAnnotation(boolean val) {
+	}
+
+	
+	@Override
+	public int getHeight() {
+		return HEIGHT;
+	}
+
+	@Override
+	public int getWidth() {
+		return WIDTH;
+	}
+
+	@Override
+	public void updatePolygon() {
+		
+		_poly = new Polygon();
+
+		int x = (int)_x;
+		int y = (int)_y;
+		
+		_poly.addPoint(x, y);
+		_poly.addPoint(x + WIDTH, y);
+		_poly.addPoint(x + WIDTH, y + HEIGHT);
+		_poly.addPoint(x, y + HEIGHT);
+
+	}
+
+	@Override
+	public void onParentStateChanged(ItemParentStateChangedEvent e) {
+		super.onParentStateChanged(e);
+
+		switch (e.getEventType()) {
+
+		case ItemParentStateChangedEvent.EVENT_TYPE_ADDED:
+		case ItemParentStateChangedEvent.EVENT_TYPE_ADDED_VIA_OVERLAY:
+		case ItemParentStateChangedEvent.EVENT_TYPE_SHOWN:
+		case ItemParentStateChangedEvent.EVENT_TYPE_SHOWN_VIA_OVERLAY:
+			// todo: Invoke later for concurrent modification possibility?
+			selfDestruct();
+			break;
+
+		}
+	}
+	
+	private void selfDestruct() {
+		
+		Frame parent = getParent();
+		parent.removeItem(this);
+		
+		Frame currentFrame = DisplayIO.getCurrentFrame();
+		
+		if (currentFrame != null) {
+			
+			Timeline tl = FrameLayoutDaemon.getInstance().getLastComputedTimeline();
+			
+			if (tl == null || FrameLayoutDaemon.getInstance().getTimelineOwner() == null ||
+					FrameLayoutDaemon.getInstance().getTimelineOwner() != currentFrame) {
+				tl = FrameLayoutDaemon.inferTimeline(currentFrame);
+			}
+			
+			if (tl != null) {
+		
+				long initiateMS = tl.getMSTimeAtX((int)_x);
+				
+				// Clamp
+				if (initiateMS < tl.getFirstInitiationTime())
+					initiateMS = tl.getFirstInitiationTime();
+				
+				else if (initiateMS > (tl.getFirstInitiationTime() + tl.getRunningTime())) 
+					initiateMS  = tl.getFirstInitiationTime() + tl.getRunningTime();
+
+				int startFrame = AudioMath.millisecondsToFrames(
+						initiateMS - tl.getFirstInitiationTime(),  
+						SampledAudioManager.getInstance().getDefaultPlaybackFormat());
+				
+				// To ensure that the frame channels are not in use
+				MultiTrackPlaybackController.getInstance().stopPlayback();
+				SoundDesk.getInstance().freeChannels(FramePlayer.FRAME_PLAYERMASTER_CHANNEL_ID);
+
+				FramePlayer.playFrame(
+						new MultitrackLoadListener() {
+		
+							public void multiplaybackLoadStatusUpdate(int id, Object state) {
+								// Ignore
+							}
+							
+						}, 
+						currentFrame.getName(), 
+						false, 
+						0, 
+						startFrame, 
+						Integer.MAX_VALUE
+						);
+			
+			}
+					
+		}
+	}
+	
+	
+	
+}
Index: /trunk/src_apollo/org/apollo/meldex/DynamicProgrammingAlgorithm.java
===================================================================
--- /trunk/src_apollo/org/apollo/meldex/DynamicProgrammingAlgorithm.java	(revision 315)
+++ /trunk/src_apollo/org/apollo/meldex/DynamicProgrammingAlgorithm.java	(revision 315)
@@ -0,0 +1,229 @@
+package org.apollo.meldex;
+
+/**
+ * This abstract class implements all of the mechanics involved in performing a match using an
+ * algorithm involving dynamic programming. Subclasses need to only implement a weighting
+ * function for comparing two melody events. Subclasses can also easily implement special
+ * functionality above the replacement/deletion/insertion that is standard.
+ *
+ * !! Can do matching at start better?? !!
+ */
+
+public abstract class DynamicProgrammingAlgorithm 
+{
+    public static final int MATCH_ANYWHERE = 1;
+    public static final int MATCH_AT_START = 2;
+
+    protected static final boolean debugOutput = true;
+
+
+    public DynamicProgrammingAlgorithm()
+    {
+    }
+
+
+    /**
+     * Matches a melody against the query pattern and returns a distance measure.
+     *
+     * @param P The query pattern 
+     * @param T The melody to match against the query pattern.
+     * @param matchingMode How to match against the query pattern (start/anywhere etc.)
+     *
+     * @return A distance measure signifying the distance between the query and match patterns.
+     */
+    public int matchToPattern(Melody P, Melody T, int matchingMode)
+    {
+	// int maxk = options.getIntegerOptionValue("approximation");
+	int maxk = Integer.MAX_VALUE;
+
+	int m = P.getLength();
+	int n = T.getLength();
+
+	// For the comparison to be meaningful the pitch types must be the same
+	if (P.getPitchType() != T.getPitchType())
+	    throw new Error("Internal Error: Pitch type of query and match differs.");
+
+	// Check whether the algorithm has some special functionality
+	boolean hasSpecialFunctionality = hasSpecialFunctionality();
+
+	// Allocate distance matrix
+	int[][] D = new int[n + 1][m + 1];
+	D[0][0] = 0;
+
+	// Initialise top row of matrix
+	if (matchingMode == MATCH_AT_START) {
+	    // Match at start: have to pay to start further into the pattern
+	    for (int i = 1; i <= n; i++)
+		D[i][0] = D[i-1][0] + weightFunction(null, T.getEvent(i-1));
+	}
+	if (matchingMode == MATCH_ANYWHERE) {
+	    // Match anywhere: free to start at any point in the pattern
+	    for (int i = 1; i <= n; i++)
+		D[i][0] = 0;
+	}
+
+	// Initialise left column of matrix
+	for (int j = 1; j <= m; j++)
+	    D[0][j] = D[0][j-1] + weightFunction(P.getEvent(j-1), null);
+
+	// Fill the distance matrix
+	for (int j = 1; j <= m; j++) {
+	    MelodyEvent queryEvent = P.getEvent(j-1);
+	    for (int i = 1; i <= n; i++) {
+		MelodyEvent matchEvent = T.getEvent(i-1);
+
+		// Case 1: The two events are the same or different (replacement)
+		D[i][j] = D[i-1][j-1] + weightFunction(queryEvent, matchEvent);
+
+		// Case 2: The two events are different (deletion)
+		D[i][j] = Math.min(D[i][j], (D[i-1][j] + weightFunction(null, matchEvent)));
+
+		// Case 3: The two events are different (insertion)
+		D[i][j] = Math.min(D[i][j], (D[i][j-1] + weightFunction(queryEvent, null)));
+
+		// If the algorithm has special functionality, give it a chance now
+		if (hasSpecialFunctionality)
+		    doAlgorithmSpecifics(D, i, j, P, T, queryEvent, matchEvent);
+	    }
+	}
+
+	// Display the distance matrix
+	if (debugOutput)
+	     displayMatrix(D, m, P, n, T);
+
+	// Find and return the minimum edit distance
+	int minDist = Integer.MAX_VALUE;
+	for (int i = 0; i <= n; i++) {  // Changed from i = 1 to i = 0 for McNab compatibility
+	    if (D[i][m] < minDist)
+		minDist = D[i][m];
+	}
+
+	// Return -1 if the minimum distance is greater than the approximation value
+	return ((minDist > maxk) ? -1 : minDist);
+    }
+
+
+    /**
+     * Returns a distance measure signifying the distance between the query and match events.
+     * This must be implemented by concrete subclasses.
+     *
+     * @param queryEvent The event to compare from the query pattern.
+     * @param matchEvent The event to compare from the match pattern.
+     *
+     * @return a distance measure signifying the distance between the query and match events.
+     */
+    protected abstract int weightFunction(MelodyEvent queryEvent, MelodyEvent matchEvent);
+
+
+    /**
+     * Returns whether the algorithm has special functionality or not.
+     * This should be overridden by concrete subclasses that have special functionality.
+     *
+     * @return true if the algorithm has special functionality, false otherwise.
+     */
+    protected boolean hasSpecialFunctionality()
+    {
+	return false;
+    }
+
+
+    /**
+     * Gives an algorithm a chance to perform any special functionality that it has.
+     * This should be overridden by concrete subclasses that have special functionality.
+     *
+     * @param D The distance matrix at the current point in time.
+     * @param i The current x position in the distance matrix.
+     * @param j The current y position in the distance matrix.
+     * @param T The melody that is being matched to the query pattern.
+     * @param queryEvent The melody event at position j-1 in the query pattern.
+     * @param matchEvent The melody event at position i-1 in the match pattern.
+     */
+    protected void doAlgorithmSpecifics(int[][] D, int i, int j, Melody P, Melody T,
+					MelodyEvent queryEvent, MelodyEvent matchEvent)
+    {
+	// Nothing to do
+    }
+
+
+    /**
+     * Displays a completed distance matrix.
+     *
+     * @param matrix The distance matrix to display.
+     * @param P The query pattern (displayed down the left).
+     * @param T The match pattern (displayed along the top).
+     */
+    private void displayMatrix(int[][] matrix, int m, Melody P, int n, Melody T)
+    {
+	int width = 5;
+
+	// Display the top line (match pattern)
+	System.out.print(rightJustifyString("", width));
+	System.out.print("  ");
+	System.out.print(rightJustifyString("", width));
+	for (int i = 1; i <= n; i++) {
+	    MelodyEvent matchEvent = T.getEvent(i-1);
+	    int matchPitch = matchEvent.getPitch();
+	    String pitchString;
+	    if (matchPitch == MelodyEvent.REST_EVENT)
+		pitchString = "REST";
+	    else if (matchPitch == MelodyEvent.WILD_EVENT)
+		pitchString = "WILD";
+	    else
+		pitchString = String.valueOf(matchPitch);
+
+	    System.out.print(rightJustifyString(pitchString, width));
+	}
+	System.out.print("\n");
+
+	// Display the second line
+	System.out.print(rightJustifyString("", width));
+	System.out.print(" -");
+	for (int i = 0; i < ((n+1) * width); i++)
+	    System.out.print("-");
+	System.out.print("\n");
+
+	// Display the remaining lines
+	for (int j = 0; j <= m; j++) {
+	    if (j == 0)
+		System.out.print(rightJustifyString("", width));
+	    else {
+		MelodyEvent queryEvent = P.getEvent(j-1);
+		int queryPitch = queryEvent.getPitch();
+		String pitchString;
+		if (queryPitch == MelodyEvent.REST_EVENT)
+		    pitchString = "REST";
+		else if (queryPitch == MelodyEvent.WILD_EVENT)
+		    pitchString = "WILD";
+		else
+		    pitchString = String.valueOf(queryPitch);
+
+		System.out.print(rightJustifyString(pitchString, width));
+	    }
+	    System.out.print(" |");
+
+	    for (int i = 0; i <= n; i++)
+		System.out.print(rightJustifyString(String.valueOf(matrix[i][j]), width));
+	    System.out.print("\n");
+	}
+    }
+
+
+    /**
+     * Right-justifies a string by adding spaces before it.
+     *
+     * @param source The string to right-justify.
+     * @param width The number of characters that the right-justified string will take up.
+     *
+     * @return The right-justified string, or the original string if the width is too small.
+     */
+    private String rightJustifyString(String source, int width)
+    {
+	String result = source;
+
+	// Add width - source.length spaces to the start of the string
+	for (int i = 0; i < (width - source.length()); i++)
+	    result = " " + result;
+
+	return result;
+    }
+}
Index: /trunk/src_apollo/org/apollo/meldex/McNabMongeauSankoffAlgorithm.java
===================================================================
--- /trunk/src_apollo/org/apollo/meldex/McNabMongeauSankoffAlgorithm.java	(revision 315)
+++ /trunk/src_apollo/org/apollo/meldex/McNabMongeauSankoffAlgorithm.java	(revision 315)
@@ -0,0 +1,214 @@
+package org.apollo.meldex;
+
+/**
+ * An implementation of an approximate string matching algorithm described by Marcel Mongeau
+ * and David Sankoff ("Comparison of Musical Sequences", Computers and the Humanities, Vol 24
+ * (1990), p161 - 175) and modified by Rodger McNab for music matching (REF).
+ * <p>To do: Test fragmentation and consolidation.
+ */
+public class McNabMongeauSankoffAlgorithm extends DynamicProgrammingAlgorithm
+{
+    private static final boolean doFragmentationConsolidation = false;
+
+    /** Constant values used by this algorithm */
+    private final double K1_FACTOR = 112.0;
+
+
+    /**
+     * Creates a new McNabMongeauSankoffAlgorithm and initialises it for use.
+     */
+    public McNabMongeauSankoffAlgorithm()
+    {
+    }
+
+
+    /**
+     * Returns whether this algorithm has special functionality that is enabled.
+     *
+     * @return whether this algorithm has special functionality that is enabled.
+     */
+    protected boolean hasSpecialFunctionality()
+    {
+	return 	doFragmentationConsolidation;
+    }
+
+
+    /**
+     * Lets the algorithm perform its special functionality (fragmentation and consolidation).
+     *
+     * @param D The distance matrix at the current point in time.
+     * @param i The current x position in the distance matrix.
+     * @param j The current y position in the distance matrix.
+     * @param T The melody that is being matched to the query pattern.
+     * @param queryEvent The melody event at position j-1 in the query pattern.
+     * @param matchEvent The melody event at position i-1 in the match pattern.
+     */
+    protected void doAlgorithmSpecifics(int[][] D, int i, int j, Melody P, Melody T,
+					MelodyEvent queryEvent, MelodyEvent matchEvent)
+    {
+	if (doFragmentationConsolidation) {
+	    int pitchType = T.getPitchType();
+
+	    // Do fragmentations (one query event matches many match events)
+	    int fragPitch = matchEvent.getPitch();
+	    double queryDuration = queryEvent.getDuration();
+	    double totalDuration = matchEvent.getDuration();
+
+	    // Subsequent pitches must be the same
+	    int requiredPitch = 0;
+	    if (pitchType == Melody.ABSOLUTE_PITCH)
+		requiredPitch = fragPitch;
+
+	    // Sum the fragmented weights of the previous k match events
+	    int k = 2;
+	    while ((k <= i) && (fragPitch == requiredPitch)) {
+		// Total fragmentation length should not exceed query event length
+		if (totalDuration > queryDuration)
+		    break;
+
+		// All fragments must have the same pitch
+		MelodyEvent fragEvent = T.getEvent(i-k);
+		fragPitch = fragEvent.getPitch();
+		if (pitchType == Melody.ABSOLUTE_PITCH && fragPitch != requiredPitch)
+		    break;
+
+		// Calculate the new pitch and duration cost for this fragmentation
+		double wPitch = weightPitch(queryEvent.getPitch(), fragPitch);
+		totalDuration += fragEvent.getDuration();
+		double wDuration = weightDuration(queryDuration, totalDuration);
+		double shortestDuration = (queryDuration < totalDuration ? queryDuration : totalDuration);
+
+		// Calculate the total cost for this fragmentation
+		int fragWeight = (int) ((wPitch * shortestDuration) + (K1_FACTOR * wDuration));
+
+		if (debugOutput) {
+		    System.out.println("Frag: k = " + k + "\tDi-k,j-1 = " + D[i-k][j-1] + "\tPitch weight: " + wPitch + "\tDuration weight: " + wDuration + "\tTotal weight: " + fragWeight);
+		}
+
+		// Case 4: Many events are equivalent to one (fragmentation)
+		D[i][j] = Math.min(D[i][j], (D[i-k][j-1] + fragWeight));
+		k++;
+	    }
+
+	    // Do consolidations (one match event matches many query events)
+	    int consPitch = queryEvent.getPitch();
+	    double matchDuration = matchEvent.getDuration();
+	    totalDuration = queryEvent.getDuration();
+
+	    // Subsequent pitches must be the same
+	    requiredPitch = 0;
+	    if (pitchType == Melody.ABSOLUTE_PITCH)
+		requiredPitch = consPitch;
+
+	    // Sum the fragmented weights of the previous k query events
+	    k = 2;
+	    while ((k <= j) && (consPitch == requiredPitch)) {
+		// Total consolidation length should not exceed match event length
+		if (totalDuration > matchDuration)
+		    break;
+
+		// All fragments must have the same pitch
+		MelodyEvent consEvent = P.getEvent(j-k);
+		consPitch = consEvent.getPitch();
+		if (pitchType == Melody.ABSOLUTE_PITCH && consPitch != requiredPitch)
+		    break;
+
+		// Calculate the new pitch and duration cost for this consolidation
+		double wPitch = weightPitch(matchEvent.getPitch(), consPitch);
+		totalDuration += consEvent.getDuration();
+		double wDuration = weightDuration(matchDuration, totalDuration);
+		double shortestDuration = (matchDuration < totalDuration ? matchDuration : totalDuration);
+
+		// Calculate the total cost for this consolidation
+		int consWeight = (int) ((wPitch * shortestDuration) + (K1_FACTOR * wDuration));
+
+		if (debugOutput) {
+		    System.out.println("Cons: k = " + k + "\tDi-1,j-k = " + D[i-1][j-k] + "\tPitch weight: " + wPitch + "\tDuration weight: " + wDuration + "\tTotal weight: " + consWeight);
+		}
+
+		// Case 5: Many events are equivalent to one (consolidation)
+		D[i][j] = Math.min(D[i][j], (D[i-1][j-k] + consWeight));
+		k++;
+	    }
+	}
+    }
+
+
+    /**
+     * Returns a distance measure signifying the distance between the query and match events.
+     *
+     * @param queryEvent The event to compare from the query pattern.
+     * @param matchEvent The event to compare from the match pattern.
+     *
+     * @return a distance measure signifying the distance between the query and match events.
+     */
+    protected int weightFunction(MelodyEvent queryEvent, MelodyEvent matchEvent)
+    {
+	// Calculate the pitch weighting
+	int queryPitch = ((queryEvent == null) ? 0 : queryEvent.getPitch());
+	int matchPitch = ((matchEvent == null) ? 0 : matchEvent.getPitch());
+	double wPitch = weightPitch(queryPitch, matchPitch);
+
+	return (int)wPitch;
+
+	/*
+
+	// Calculate the duration weighting
+	double queryLength = ((queryEvent == null) ? 0.0 : queryEvent.getDuration());
+	double matchLength = ((matchEvent == null) ? 0.0 : matchEvent.getDuration());
+     	double wDuration = weightDuration(queryLength, matchLength);
+	double shortestLength = (queryLength < matchLength ? queryLength : matchLength);
+
+	// Calculate the final weighting (linear combination of pitch and duration)
+	return (int) ((wPitch * shortestLength) + (K1_FACTOR * wDuration));
+	*/
+
+    }
+
+
+    /**
+     * Returns a measure signifying the distance between the query and match pitches.
+     *
+     * @param queryPitch The pitch of the query event.
+     * @param matchPitch The pitch of the match event.
+     *
+     * @return a measure signifying the distance between the query and match pitches.
+     */
+    private double weightPitch(int queryPitch, int matchPitch)
+    {
+	// If either of the events is a wild then the pitch weight is 0
+	if ((queryPitch == MelodyEvent.WILD_EVENT) || (matchPitch == MelodyEvent.WILD_EVENT))
+	    return 0.0;
+
+	// If one or other of the events is a rest, use a special pitch factor of 80.0
+	if ((queryPitch == MelodyEvent.REST_EVENT) ^ (matchPitch == MelodyEvent.REST_EVENT))
+	    return 80.0;
+
+	// Otherwise calculate a pitch factor based on the pitch difference
+	int pitchDifference = Math.abs((queryPitch - matchPitch));
+
+	// Special case absolute pitch
+	/* if (P.getPitchType() == Melody.ABSOLUTE_PITCH) {
+	    pitchDifference = pitchDifference % 12;
+	    if (pitchDifference > 6)
+		pitchDifference = 12 - pitchDifference;
+		} */
+
+	return (pitchDifference * 32.0);
+    }
+
+
+    /**
+     * Returns a measure signifying the distance between the query and match durations.
+     *
+     * @param queryLength The duration of the query event.
+     * @param matchLength The duration of the match event.
+     *
+     * @return a measure signifying the distance between the query and match durations.
+     */
+    private double weightDuration(double queryLength, double matchLength)
+    {
+	// The duration weight is directly proportional to the distance between the two lengths
+	return Math.abs((queryLength - matchLength));
+    }
+}
Index: /trunk/src_apollo/org/apollo/meldex/MeldexConversion.java
===================================================================
--- /trunk/src_apollo/org/apollo/meldex/MeldexConversion.java	(revision 315)
+++ /trunk/src_apollo/org/apollo/meldex/MeldexConversion.java	(revision 315)
@@ -0,0 +1,161 @@
+package org.apollo.meldex;
+
+import java.io.File;
+import java.io.IOException;
+
+import javax.sound.sampled.AudioFormat;
+
+import org.apollo.io.AudioIO;
+import org.apollo.widgets.SampledTrackEvo4;
+
+/**
+ * A helpful util for conversion in the apollo project
+ * 
+ * @author Brook Novak
+ */
+public class MeldexConversion {
+	
+	private MeldexConversion() {}
+
+	/**
+	 * 
+	 * @param trackWidget
+	 * @return
+	 * @throws IOException
+	 * 		If conversion fails.
+	 */
+	public static Melody toMelody(SampledTrackEvo4 trackWidget) throws IOException {
+		
+		assert(trackWidget != null);
+		
+		byte[] bytes = trackWidget.getAudioBytes();
+		AudioFormat format = trackWidget.getAudioFormat();
+		
+		if (bytes != null && format != null) {
+			return toMelody(bytes, format);
+		}
+		
+		String path = trackWidget.getLatestSavedAudioPath();
+		assert(path != null);
+	
+		File waveFile = new File(path);
+		if (!waveFile.exists()) return null;
+		
+		WavSample sampleTrack = new WavSample();
+
+		if (!sampleTrack.loadFromFile(waveFile)) {
+			return null;
+		}
+		
+		return toMelody(sampleTrack);
+		
+	}
+	
+	/**
+	 * 
+	 * @param bytes
+	 * @param format
+	 * @return
+	 * @throws IOException
+	 * 		If conversion fails.
+	 */
+	public static Melody toMelody(byte[] bytes, AudioFormat format)  throws IOException {
+		assert(bytes != null && bytes.length > 0);
+		assert(format != null);
+		WavSample sampleTrack = new WavSample(bytes, format);
+		return toMelody(sampleTrack);
+	}
+	
+	/**
+	 * 
+	 * @param sampleTrack
+	 * @return
+	 * 
+	 * @throws IOException
+	 * 		If conversion fails.
+	 */
+	public static Melody toMelody(WavSample sampleTrack) throws IOException {
+		assert(sampleTrack != null);
+
+		Transcriber transcriber = new Transcriber();
+		
+		RogTrack transcribedQuery = transcriber.transcribeSample(sampleTrack);
+		System.out.println("toMelody: " + transcribedQuery);
+		
+		if (transcribedQuery != null) {
+			return transcribedQuery.toMelody();
+		}
+		
+		return null;
+		
+	}
+	
+	/**
+	 * Converts PCM audio bytes to the corerct format for transcribing raw audio.
+	 * 
+	 * @param source
+	 * @param sourceFormat
+	 * @return
+	 * @throws IOException
+	 */
+	public static byte[] toStandardizedFormat(byte[] source, AudioFormat sourceFormat, float sampleRate) 
+		throws IOException {
+		
+		assert(source != null);
+		assert(sourceFormat != null);
+		
+
+		// Get the standardised unsigned 8-bit mono data
+		AudioFormat partialStandardizedFormat = new AudioFormat(
+				sampleRate,
+				sourceFormat.getSampleSizeInBits(),
+				1, // mono
+				sourceFormat.getSampleSizeInBits() != 8, 
+				true); 
+		
+		byte[] stdData = AudioIO.convertAudioBytes(
+				source, 
+				sourceFormat, 
+				partialStandardizedFormat);
+		
+		if (stdData == null) {
+		    return null;
+		}
+		
+		// Perform conversion to 8-bit unsigned
+		if (partialStandardizedFormat.getSampleSizeInBits() != 8) {
+			byte[] preData = stdData;
+			stdData = null;
+			
+			if (partialStandardizedFormat.getSampleSizeInBits() == 16) {
+				
+				int newSize = preData.length >> 1;
+				stdData = new byte[newSize];
+				int sample;
+				float fSample;
+				
+				for (int i = 0; i < newSize; i++) {
+					sample = preData[(i << 1)] << 8; // MSB - big endian
+					sample |= (preData[(i << 1) + 1] & 0xFF); // LSB - big endian
+					
+					fSample = sample;
+					fSample /= 256.0f;
+					fSample += Byte.MAX_VALUE;
+					sample = (int)fSample;
+					stdData[i] = (byte)(sample & 0xFF);
+
+					//System.out.println(sample + " -> " + fSample + " -> " + stdData[i]);
+				}
+				
+				preData = null;
+			}
+			
+		}
+		
+		return stdData;
+	}
+	
+	
+	
+
+}
Index: /trunk/src_apollo/org/apollo/meldex/MeldexMatch.java
===================================================================
--- /trunk/src_apollo/org/apollo/meldex/MeldexMatch.java	(revision 315)
+++ /trunk/src_apollo/org/apollo/meldex/MeldexMatch.java	(revision 315)
@@ -0,0 +1,271 @@
+package org.apollo.meldex;
+
+import java.io.ByteArrayInputStream;
+import java.io.ByteArrayOutputStream;
+import java.io.File;
+import java.io.IOException;
+
+import javax.sound.sampled.AudioFormat;
+import javax.sound.sampled.AudioInputStream;
+import javax.sound.sampled.AudioSystem;
+import javax.sound.sampled.DataLine;
+import javax.sound.sampled.TargetDataLine;
+
+/**
+ * Calculates a "Score" on how well two given
+ *
+ */
+class MeldexMatch {
+
+	final protected int MIC_FREQUENCY = 8;
+
+	protected DataLine.Info micInfo_;
+
+	protected AudioFormat micFormat_;
+
+	protected TargetDataLine micLine_;
+
+	protected Transcriber transcriber_ = null;
+
+	public MeldexMatch() {
+		transcriber_ = new Transcriber();
+	}
+
+	protected void establishMicInfo() {
+		AudioFormat micFormat22050, micFormat11025, micFormat8000, micFormat8096;
+		DataLine.Info micInfo22050, micInfo11025, micInfo8000, micInfo8096;
+
+		// Define the format for our microphone data line
+		AudioFormat.Encoding encoding = AudioFormat.Encoding.PCM_UNSIGNED;
+		int sampleSize = 8;
+		int channels = 1;
+		boolean bigEndian = false;
+		float rate;
+
+		// Define a variety of formats, each with a different sampling rate
+		rate = 22050F;
+		micFormat22050 = new AudioFormat(encoding, rate, sampleSize, channels,
+				(sampleSize / 8) * channels, rate, bigEndian);
+		rate = 11025F;
+		micFormat11025 = new AudioFormat(encoding, rate, sampleSize, channels,
+				(sampleSize / 8) * channels, rate, bigEndian);
+		rate = 8000F;
+		micFormat8000 = new AudioFormat(encoding, rate, sampleSize, channels,
+				(sampleSize / 8) * channels, rate, bigEndian);
+		rate = 8096F;
+		micFormat8096 = new AudioFormat(encoding, rate, sampleSize, channels,
+				(sampleSize / 8) * channels, rate, bigEndian);
+
+		// Describe a variety of data lines, each with a different sampling rate
+		micInfo22050 = new DataLine.Info(TargetDataLine.class, micFormat22050);
+		micInfo11025 = new DataLine.Info(TargetDataLine.class, micFormat11025);
+		micInfo8000 = new DataLine.Info(TargetDataLine.class, micFormat8000);
+		micInfo8096 = new DataLine.Info(TargetDataLine.class, micFormat8096);
+
+		// Try to find a microphone line that we can use
+		if (AudioSystem.isLineSupported(micInfo22050)) {
+			micInfo_ = micInfo22050;
+			micFormat_ = micFormat22050;
+		} else if (AudioSystem.isLineSupported(micInfo11025)) {
+			micInfo_ = micInfo11025;
+			micFormat_ = micFormat11025;
+		} else if (AudioSystem.isLineSupported(micInfo8000)) {
+			micInfo_ = micInfo8000;
+			micFormat_ = micFormat8000;
+		} else if (AudioSystem.isLineSupported(micInfo8096)) {
+			micInfo_ = micInfo8096;
+			micFormat_ = micFormat8096;
+		} else {
+			micInfo_ = null;
+			micFormat_ = null;
+		}
+	}
+
+	protected boolean initMic() {
+		establishMicInfo();
+
+		// Get the microphone line
+		try {
+			micLine_ = (TargetDataLine) AudioSystem.getLine(micInfo_);
+		} catch (Exception ex) {
+			System.err
+					.println("Exception occurred getting the microphone line.\n\n"
+							+ ex);
+			return false;
+		}
+
+		// Calculate the size of the microphone buffer required
+		int frameSize = (micFormat_.getChannels() * (micFormat_
+				.getSampleSizeInBits() / 8));
+		int bytesPerSecond = (int) (micFormat_.getSampleRate() * frameSize);
+		int micBufferSize = (int) (bytesPerSecond / MIC_FREQUENCY);
+
+		// Open the microphone data line for recording
+		try {
+			micLine_.open(micFormat_, micBufferSize);
+		} catch (Exception ex) {
+			System.err
+					.println("Exception occurred opening the microphone line.\n\n"
+							+ ex);
+			return false;
+		}
+
+		return true;
+	}
+
+	protected void record_prompt(int p, int fixed) {
+		System.out.print("Recording: [");
+		for (int i = 0; i < p; i++) {
+			System.out.print(".");
+		}
+		for (int i = p; i < fixed; i++) {
+			System.out.print(" ");
+		}
+
+		System.out.print("]\r");
+		System.out.flush();
+	}
+
+	public AudioInputStream record() {
+		// Make sure that we found a supported microphone line
+		if (micInfo_ == null) {
+			System.err
+					.println("Sorry, a suitable microphone line could not be found.");
+			return null;
+		}
+
+		// Create a buffer for the recorded data
+		byte[] micBuffer = new byte[micLine_.getBufferSize()];
+
+		// Create an output stream to save the recorded data
+		ByteArrayOutputStream micOut = new ByteArrayOutputStream();
+
+		// Start the microphone actually recording
+		micLine_.start();
+
+		// Continue recording until the Stop button is pressed
+		int fixed = 30;
+		int i = 0;
+		while (i <= fixed) {
+			record_prompt(i, fixed);
+
+			// Read some data from the microphone line...
+			int numBytesRead = micLine_.read(micBuffer, 0, micLine_
+					.getBufferSize());
+
+			// ...save it to the output stream
+			micOut.write(micBuffer, 0, numBytesRead);
+
+			i++;
+		}
+		System.out.println("");
+
+		// We have finished recording, so stop and close the microphone line
+		micLine_.stop();
+		micLine_.close();
+
+		// Flush and close the output stream
+		try {
+			micOut.flush();
+			micOut.close();
+		} catch (Exception ex) {
+			System.err
+					.println("Exception occurred flushing/closing output stream.\n\n"
+							+ ex);
+			return null;
+		}
+
+		// Load the data recorded into the audio input stream ready for playback/processing
+		byte audioBytes[] = micOut.toByteArray();
+		ByteArrayInputStream bais = new ByteArrayInputStream(audioBytes);
+		AudioInputStream audioData = new AudioInputStream(bais, micFormat_,
+				audioBytes.length / micFormat_.getFrameSize());
+
+		return audioData;
+	}
+
+	public void doMatch(WavSample sample_query, WavSample sample_track) throws IOException {
+		
+		RogTrack transcribed_query = transcriber_
+				.transcribeSample(sample_query);
+		
+		RogTrack transcribed_track = transcriber_
+				.transcribeSample(sample_track);
+
+		System.out.println("Query");
+		System.out.println(transcribed_query);
+		System.out.println("Track");
+		System.out.println(transcribed_track);
+
+		Melody melody_query = transcribed_query.toMelody();
+		Melody melody_track = transcribed_track.toMelody();
+
+		DynamicProgrammingAlgorithm dpa = new McNabMongeauSankoffAlgorithm();
+		// int match_mode = DynamicProgrammingAlgorithm.MATCH_AT_START;
+		int match_mode = DynamicProgrammingAlgorithm.MATCH_ANYWHERE;
+
+		float score = dpa
+				.matchToPattern(melody_query, melody_track, match_mode);
+
+		System.out.println("Edit distance = " + score);
+
+	}
+
+	public void liveQuery(String audiotrack) {
+		File input_file = new File(audiotrack);
+
+		WavSample sample_track = new WavSample();
+
+		if (sample_track.loadFromFile(input_file) != true) {
+			System.err.println("Error: Unable to load wav file: " + audiotrack);
+		} else {
+			initMic();
+			AudioInputStream audioData = record();
+
+			WavSample sample_query = new WavSample(audioData);
+
+			try {
+				doMatch(sample_query, sample_track);
+			} catch (IOException e) {
+				// TODO Auto-generated catch block
+				e.printStackTrace();
+			}
+		}
+
+	}
+
+	public void cannedQuery(String audioquery, String audiotrack) {
+		File file_query = new File(audioquery);
+		File file_track = new File(audiotrack);
+
+		WavSample sample_query = new WavSample();
+		WavSample sample_track = new WavSample();
+
+		if (sample_query.loadFromFile(file_query) != true) {
+			System.err.println("Error: Unable to load wav file: " + audioquery);
+		} else if (sample_track.loadFromFile(file_track) != true) {
+			System.err.println("Error: Unable to load wav file: " + audiotrack);
+		} else {
+			try {
+				doMatch(sample_query, sample_track);
+			} catch (IOException e) {
+				// TODO Auto-generated catch block
+				e.printStackTrace();
+			}
+		}
+	}
+
+	public static void main(String args[]) {
+		MeldexMatch meldex = new MeldexMatch();
+
+		int argc = args.length;
+		if (argc == 1) {
+			meldex.liveQuery(args[0]);
+		} else if (argc == 2) {
+			meldex.cannedQuery(args[0], args[1]);
+		} else {
+			System.err.println("Usage: MeldexMatch wav-file1 [wav-file2]");
+			System.exit(-1);
+		}
+	}
+}
Index: /trunk/src_apollo/org/apollo/meldex/Melody.java
===================================================================
--- /trunk/src_apollo/org/apollo/meldex/Melody.java	(revision 315)
+++ /trunk/src_apollo/org/apollo/meldex/Melody.java	(revision 315)
@@ -0,0 +1,47 @@
+package org.apollo.meldex;
+
+
+/**
+ * This class represents a monophonic melody: an ordered list of melody events. This class is
+ * basically just a wrapper around an array of MelodyEvents (an array is used instead of a
+ * Vector for efficiency reasons). This class is serializable since it must be able to be
+ * written to the matching query collection files.
+ */
+
+public class Melody extends StandardisedMelody
+{
+
+	private static final long serialVersionUID = -8903591998036012467L;
+	
+	/** Pitch type: absolute pitch. */
+    public static final int ABSOLUTE_PITCH = 1;
+    /** Pitch type: relative pitch (interval/contour). */
+    public static final int RELATIVE_PITCH = 2;
+
+    /** The pitch type (absolute/relative) of this melody. */
+    private int pitchType;
+
+
+    /**
+     * Creates a new Melody with the specified pitch type and the given melody events.
+     *
+     * @param pitchType The pitch type (absolute/relative) of the melody.
+     * @param events The melody events that form the melody.
+     */
+    public Melody(int pitchType, MelodyEvent[] events)
+    {
+	super(events);
+	this.pitchType = pitchType;
+    }
+
+
+    /**
+     * Returns the pitch type (absolute/relative) of this melody.
+     *
+     * @return the pitch type (absolute/relative) of this melody.
+     */
+    public int getPitchType()
+    {
+	return pitchType;
+    }
+}
Index: /trunk/src_apollo/org/apollo/meldex/MelodyEvent.java
===================================================================
--- /trunk/src_apollo/org/apollo/meldex/MelodyEvent.java	(revision 315)
+++ /trunk/src_apollo/org/apollo/meldex/MelodyEvent.java	(revision 315)
@@ -0,0 +1,60 @@
+package org.apollo.meldex;
+
+import java.io.Serializable;
+
+
+/**
+ * This class represents a single event in a monophonic melody: a pitch-duration pair. This
+ * class is serializable since it must be able to be written to the matching query collection
+ * files.
+ */
+public class MelodyEvent implements Serializable
+{
+
+	private static final long serialVersionUID = -2160853951522226811L;
+	
+	/** Melody event type: this event is a rest (has no pitch). */
+    public static final int REST_EVENT = -128;
+    /** Melody event type: this event is a wild (matches any other melody event). */
+    public static final int WILD_EVENT =  128;
+
+    /** The pitch of this melody event, or the event type if it is not a note. */
+    private int pitch;
+    /** The duration of this melody event. */
+    private float duration;
+
+
+    /**
+     * Creates a new melody event.
+     *
+     * @param eventPitch The pitch of the melody event, or the event type if it is not a note.
+     * @param eventDuration The duration of the melody event.
+     */
+    public MelodyEvent(int eventPitch, float eventDuration)
+    {
+	pitch = eventPitch;
+	duration = eventDuration;
+    }
+
+
+    /**
+     * Returns the pitch of the melody event, or the event type if it is not a note.
+     *
+     * @return the pitch of the melody event, or the event type if it is not a note.
+     */
+    public int getPitch()
+    {
+	return pitch;
+    }
+
+
+    /**
+     * Returns the duration of the melody event.
+     *
+     * @return the duration of the melody event.
+     */
+    public float getDuration()
+    {
+	return duration;
+    }
+}
Index: /trunk/src_apollo/org/apollo/meldex/NoteRounder.java
===================================================================
--- /trunk/src_apollo/org/apollo/meldex/NoteRounder.java	(revision 315)
+++ /trunk/src_apollo/org/apollo/meldex/NoteRounder.java	(revision 315)
@@ -0,0 +1,40 @@
+package org.apollo.meldex;
+
+public class NoteRounder
+{
+    private int prevValue;
+
+
+    public void setInitialValue(int value)
+    {
+	prevValue = value;
+    }
+
+
+    public int round(int value, int mult)
+    {
+	return (((value + (mult/2)) / mult) * mult);
+    }
+
+
+    public int roundCents(int value)
+    {
+	return (round(value, 100));
+    }
+
+
+    public int roundNote(int value, boolean useAdaptiveTuning)
+    {
+	// If we aren't using adaptive tuning, ignore the previous value
+	if (useAdaptiveTuning == false) {
+	    return (roundCents(value));
+	}
+
+	// Otherwise calculate the new adaptive value
+	else {
+	    int finalValue = roundCents(value + prevValue);
+	    prevValue = (prevValue + (finalValue - value)) / 2;
+	    return finalValue;
+	}
+    }
+}
Index: /trunk/src_apollo/org/apollo/meldex/PitchTracker.java
===================================================================
--- /trunk/src_apollo/org/apollo/meldex/PitchTracker.java	(revision 315)
+++ /trunk/src_apollo/org/apollo/meldex/PitchTracker.java	(revision 315)
@@ -0,0 +1,440 @@
+package org.apollo.meldex;
+
+import java.util.ArrayList;
+
+@SuppressWarnings("unchecked") // code in java 1.4
+public class PitchTracker
+{
+    // The sample rate (frequency) of the loaded sample
+    int sampleRate;
+
+    // The filtered data
+    byte[] filterData;
+    int filterLength;
+
+    // The pitch data
+    ArrayList pitchData;
+
+    // Our lovely Pitch Period Estimators
+    PitchEstimator[] ppe = new PitchEstimator[6];
+
+    // Our lovely winner calculator
+    WinnerCalculator winnerCalc = new WinnerCalculator();
+
+    int lastMinHeight, lastMaxHeight;
+    int lastPeriodStart;
+
+
+    public ArrayList process(byte[] rawData, int rawLength, int rawSampleRate)
+    {
+	// Save the sample rate of the loaded sample
+	sampleRate = rawSampleRate;
+
+	// Allocate a new pitch data array to store our pitch values
+	pitchData = new ArrayList();
+
+	// Reset our 6 pitch period estimators
+	for (int i = 0; i < 6; i++) {
+	    ppe[i] = new PitchEstimator();
+	}
+
+	// Reset our other variables
+	lastMinHeight = 0;
+	lastMaxHeight = 0;
+	lastPeriodStart = 0;
+
+	// Perform a low pass filter on the raw data
+	lowPassFilter(rawData, rawLength);
+
+	// Find the peaks that exist in the filtered data
+	findPeaks(filterData, filterLength);
+
+	return pitchData;
+    }
+
+
+    public void lowPassFilter(byte[] rawData, int rawLength)
+    {
+	// I have absolutely no idea where these numbers come from - ask Rodger!!
+	int filters[] = { 26968, 16384, 19494, 21549, 22266, 21549, 19494, 16384, 26968 };
+
+	// Allocate memory for the filtered data values
+	filterLength = rawLength;
+	filterData = new byte[filterLength];
+
+	// Perform a low pass filter on the note data
+	for (int rawPos = 0; rawPos < rawLength; rawPos++) {
+
+	    int value = 0;
+	    for (int filterNum = 0; filterNum < 9; filterNum++) {
+
+		// Remember to convert note data value to an unsigned value
+		if ((rawPos - filterNum) >= 0) {
+		    value += ((rawData[rawPos - filterNum] & 255) * filters[filterNum]);
+		}
+		// We are out of the array bounds, so use a zero value (unsigned 127)
+		else {
+		    value += (127 * filters[filterNum]);
+		}
+	    }
+
+	    value = (value + 500888) / 195000;
+	    filterData[rawPos] = (byte) value;
+	}
+    }
+
+
+    public void findPeaks(byte[] data, int length)
+    {
+	int maxPos = 0, maxHeight = 0;
+	int minPos = 0, minHeight = 0;
+	int peakPos = 0, peakHeight = 0;
+	boolean trackingMaximum = true;
+	boolean lookingAtPeak = false;
+
+	// Find the peaks that exist in the data
+	for (int pos = 0; pos < length; pos++) {
+
+	    int value = 0;
+
+	    // Are we currently tracking a maximum??
+	    if (trackingMaximum == true) {
+		value = (data[pos] & 255) - 127;
+
+		// Have we found our maximum??
+		if (value < 0) {
+		    maxPos = peakPos;
+		    maxHeight = peakHeight;
+		    peakPos = pos;
+		    peakHeight = 0;
+		    trackingMaximum = false;
+		    lookingAtPeak = false;
+		    value = -value;
+		}
+	    }
+	    // No, so we must be currently tracking a minimum...
+	    else {
+		value = 127 - (data[pos] & 255);
+
+		// Have we found our minimum??
+		if (value < 0) {
+		    minPos = peakPos;
+		    minHeight = peakHeight;
+		    peakPos = pos;
+		    peakHeight = 0;
+		    trackingMaximum = true;
+		    lookingAtPeak = false;
+		    value = -value;
+
+		    // Process the Max/Min value that we have found
+		    processMaxMin(maxPos, maxHeight, minPos, minHeight);
+		}
+	    }
+
+	    // Deal with this sample
+	    if (value > peakHeight) {
+		peakHeight = value;
+		peakPos = pos;
+		lookingAtPeak = true;
+	    }
+	    else if (value < peakHeight && lookingAtPeak == true) {
+		// We have found a maximum
+		peakPos = (peakPos + pos - 1) / 2;
+		lookingAtPeak = false;
+	    }
+	}
+    }
+
+    public void processMaxMin(int maxPos, int maxHeight, int minPos, int minHeight)
+    {
+	// int mil13 = (int) (0.0125 * sampleRate);  // !! PURE-ROG !!
+	int eightieth = (int) (sampleRate / 80);
+	int period;
+	boolean newEstimate = false;
+
+	// Calculate the next pitch estimate
+	period = ppe[0].nextEstimate(maxPos, maxHeight);
+	if (period >= 0) {
+	    newEstimate = true;
+	    ppe[0].setEstimate(period);
+	}
+
+	// Calculate the next pitch estimate
+	period = ppe[1].nextEstimate(maxPos, maxHeight + lastMinHeight);
+	if (period >= 0) {
+	    newEstimate = true;
+	    ppe[1].setEstimate(period);
+	}
+
+	// Calculate the next pitch estimate
+	if (maxHeight > lastMaxHeight) {
+	    period = ppe[2].nextEstimate(maxPos, maxHeight - lastMaxHeight);
+	}
+	else {
+	    period = ppe[2].nextEstimate(maxPos, 0);
+	}
+	if (period >= 0) {
+	    newEstimate = true;
+	    ppe[2].setEstimate(period);
+	}
+
+	// Calculate the next pitch estimate
+	period = ppe[3].nextEstimate(minPos, minHeight);
+	if (period >= 0) {
+	    newEstimate = true;
+	    ppe[3].setEstimate(period);
+	}
+
+	// Calculate the next pitch estimate
+	period = ppe[4].nextEstimate(minPos, minHeight + maxHeight);
+	if (period >= 0) {
+	    newEstimate = true;
+	    ppe[4].setEstimate(period);
+	}
+
+	// Calculate the next pitch estimate
+	if (minHeight > lastMinHeight) {
+	    period = ppe[5].nextEstimate(minPos, minHeight - lastMinHeight);
+	}
+	else {
+	    period = ppe[5].nextEstimate(minPos, 0);
+	}
+	if (period >= 0) {
+	    newEstimate = true;
+	    ppe[5].setEstimate(period);
+	}
+
+	// If we have a new estimate, calculate the winner...
+	if (newEstimate == true) {
+	    PitchValue winner = winnerCalc.calculateWinner();
+
+	    // Period starts should be strictly increasing...
+	    int periodStart = winner.position;
+	    if (periodStart <= lastPeriodStart) {
+		periodStart = lastPeriodStart + 1;
+	    }
+
+	    // If there has been a gap of more than 13 milliseconds, fill it with zero
+	    // if ((lastPeriodStart + mil13) < periodStart) {  // !! PURE-ROG !!
+	    if ((lastPeriodStart + eightieth) < periodStart) {
+		// pitchData.add(new PitchValue(0, lastPeriodStart + mil13));  // !! PURE-ROG !!
+		pitchData.add(new PitchValue(0, lastPeriodStart + eightieth));
+	    }
+
+	    // Save the pitch data
+	    pitchData.add(new PitchValue(winner.period, periodStart));
+
+	    lastPeriodStart = periodStart;
+	}
+
+	// Save the height values for next time
+	lastMaxHeight = maxHeight;
+	lastMinHeight = minHeight;
+    }
+
+
+    // ----------------------------------------
+    //  Class : PitchEstimator
+    // ----------------------------------------
+    class PitchEstimator
+    {
+	int minSmoothPeriod = (int) (0.0012 * sampleRate);
+	int maxSmoothPeriod = (int) (0.0012 * sampleRate * 10.0);
+	int timeEndBlanking = 0;
+	int timeLastEstimate = 0;
+	int lnLast = 0;
+	int smoothPeriod = minSmoothPeriod;
+	int currentPeriod = 0;
+	int currentPeriodStart = 0;
+	int pitchEstimate[] = { 0, 0, 0, 0, 0, 0 };
+
+
+	public int nextEstimate(int time, int height)
+	{
+	    // No change in the estimate
+	    if (time < timeEndBlanking) {
+		return -1;
+	    }
+
+	    int lnValue = 0;
+	    if (height != 0) {
+		lnValue = (int) (256 * Math.log((double) height));
+	    }
+
+	    // Absolutely no idea at all what this stuff is doing... :-)
+	    if (lnValue >= (lnLast - ((178 * (time - timeEndBlanking)) / smoothPeriod))) {
+		currentPeriod = time - timeLastEstimate;
+		currentPeriodStart = timeLastEstimate;
+
+		smoothPeriod = (smoothPeriod + currentPeriod) / 2;
+		if (smoothPeriod < minSmoothPeriod) {
+		    smoothPeriod = minSmoothPeriod;
+		}
+		if (smoothPeriod > maxSmoothPeriod) {
+		    smoothPeriod = maxSmoothPeriod;
+		}
+
+		timeLastEstimate = time;
+		timeEndBlanking = time + (smoothPeriod / 3);
+		lnLast = lnValue;
+
+		// Return the calculated period estimate
+		return currentPeriod;
+	    }
+
+	    // No change in the estimate
+	    return -1;
+	}
+
+
+	public void setEstimate(int estimate)
+	{
+	    pitchEstimate[2] = pitchEstimate[1];
+	    pitchEstimate[1] = pitchEstimate[0];
+	    pitchEstimate[0] = estimate;
+	    pitchEstimate[3] = pitchEstimate[0] + pitchEstimate[1];
+	    pitchEstimate[4] = pitchEstimate[1] + pitchEstimate[2];
+	    pitchEstimate[5] = pitchEstimate[0] + pitchEstimate[1] + pitchEstimate[2]; 
+	}
+
+
+	public int getEstimate()
+	{
+	    return pitchEstimate[0];
+	}
+
+
+	public int getEstimatePosition()
+	{
+	    return currentPeriodStart;
+	}
+
+
+	public void compare(int period, int[] serr, int[] coin, int startNum)
+	{
+	    int error;
+	    for (int i = startNum; i < 6; i++) {
+		if (period > pitchEstimate[i]) {
+		    error = period - pitchEstimate[i];
+		}
+		else {
+		    error = pitchEstimate[i] - period;
+		}
+
+		if (error <= serr[0]) {
+		    coin[0]++;
+		}
+		else if (error <= serr[1]) {
+		    coin[1]++;
+		}
+		else if (error <= serr[2]) {
+		    coin[2]++;
+		}
+		else if (error <= serr[3]) {
+		    coin[3]++;
+		}
+	    }
+	}
+    }
+
+
+    // ----------------------------------------
+    //  Class : WinnerCalculator
+    // ----------------------------------------
+    class WinnerCalculator
+    {    
+	public PitchValue calculateWinner()
+	{
+	    // int mil1 = (int) (0.001 * sampleRate);  // !! PURE-ROG !!
+	    int thousandth = (int) (sampleRate / 1000);
+	    // int mil13 = (int) (0.0125 * sampleRate);  // !! PURE-ROG !!
+	    int eightieth = (int) (sampleRate / 80);
+	    int winningEstimateNum = 0;
+	    int winningCoincidence = 0;
+	    int winningPeriod = 0;
+
+	    // Create a new PitchData object to store the winning pitch
+	    PitchValue winner = new PitchValue(0, 0);
+
+	    // Calculate the winner of the 6 estimates...
+	    for (int estNum = 0; estNum < 6; estNum++) {
+
+		int period = ppe[estNum].getEstimate();
+
+		// Check that we are within the boundaries of the pitch tracker
+		// if (period > mil1 && period < mil13) {  // !! PURE-ROG !!
+		if (period > thousandth && period < eightieth) {
+
+		    // Make sure we haven't calculated coincidences for this period before
+		    int checkNum;
+		    for (checkNum = 0; checkNum < estNum; checkNum++) {
+			// Check if it has been calculated before
+			if (period == ppe[checkNum].getEstimate()) {
+			    break;
+			}
+		    }
+
+		    // We need to calculate the coincidences for this period
+		    if (checkNum == estNum) {
+			int value = calculateCoincidences(estNum, period);
+			if (value > winningCoincidence) {
+			    winningCoincidence = value;
+			    winningEstimateNum = estNum;
+			    winningPeriod = period;
+			}
+		    }
+		}
+	    }
+
+	    // Return the winning pitch
+	    winner.period = winningPeriod;
+	    winner.position = ppe[winningEstimateNum].getEstimatePosition();
+	    return winner;
+	}
+
+
+	public int calculateCoincidences(int estNum, int period)
+	{
+	    // Allocate memory for the coincidence array
+	    int[] coin = { 0, 0, 0, 0 };
+
+	    // Allocate memory for some strange array
+	    int[] serr = new int[4];
+	    for (int i = 0; i < 4; i++) {
+		serr[i] = ((i+1) * period) / 32;
+		if (serr[i] <= 0) {
+		    serr[i] = 1;
+		}
+	    }
+
+	    // Do something weird...
+	    for (int i = 0; i < 6; i++) {
+		if (i == estNum) {
+		    ppe[i].compare(period, serr, coin, 1);
+		}
+		else {
+		    ppe[i].compare(period, serr, coin, 0);
+		}
+	    }
+
+	    // More weird stuff to follow...
+	    coin[1] += coin[0];
+	    coin[2] += coin[1];
+	    coin[3] += coin[2];
+
+	    int maxCoin = (coin[0] - 1);
+	    if ((coin[1] - 2) > maxCoin) {
+		maxCoin = (coin[1] - 2);
+	    }
+	    if ((coin[2] - 5) > maxCoin) {
+		maxCoin = (coin[2] - 5);
+	    }
+	    if ((coin[3] - 7) > maxCoin) {
+		maxCoin = (coin[3] - 7);
+	    }
+
+	    // Return the maximum coincidence value
+	    return maxCoin;
+	}
+    }
+}
Index: /trunk/src_apollo/org/apollo/meldex/PitchValue.java
===================================================================
--- /trunk/src_apollo/org/apollo/meldex/PitchValue.java	(revision 315)
+++ /trunk/src_apollo/org/apollo/meldex/PitchValue.java	(revision 315)
@@ -0,0 +1,16 @@
+package org.apollo.meldex;
+
+
+public class PitchValue
+{
+    // A pitch data value consists of a period and a position
+    double period;
+    int position;
+
+
+    public PitchValue(double per, int pos)
+    {
+	period = per;
+	position = pos;
+    }
+}
Index: /trunk/src_apollo/org/apollo/meldex/RogTrack.java
===================================================================
--- /trunk/src_apollo/org/apollo/meldex/RogTrack.java	(revision 315)
+++ /trunk/src_apollo/org/apollo/meldex/RogTrack.java	(revision 315)
@@ -0,0 +1,506 @@
+package org.apollo.meldex;
+
+
+import java.io.File;
+import java.io.FileInputStream;
+import java.io.FileOutputStream;
+import java.util.ArrayList;
+import javax.swing.JOptionPane;
+
+@SuppressWarnings("unchecked") // code in java 1.4
+public class RogTrack
+{
+    // The number of semitones from the key centre
+    final int nameOffset[] = { 0, 0, 1, 2, 2, 3, 3, 4, 4, 5, 6, 6 };
+
+    // Data about the track (loaded from the file)
+    private String databaseName = "";
+    private String songName = "";
+    private int wholeNoteDivisions = 16;
+
+    // Data representing the track events
+    ArrayList trackData = new ArrayList();
+
+
+    // ---------------------------------------
+    //   WavToRog specific functions
+    // ---------------------------------------
+    public RogTrack()
+    {
+    }
+
+
+    public void addTimeSignature(int numer, int denom)
+    {
+	trackData.add(new RogTrackEvent(RogTrackEvent.TIME_SIG_EVENT, numer, denom));
+    }
+
+
+    public void addNote(int noteLength, int notePitch, int key)
+    {
+	int noteName = getNoteName(key, notePitch);
+	trackData.add(new RogTrackEvent(noteLength, noteName, notePitch));
+    }
+
+
+    public void addRest(int restLength)
+    {
+	if (restLength > 0) {
+	    trackData.add(new RogTrackEvent(restLength, 0, 0));
+	}
+    }
+
+
+    public void addKeySignature(int numSharpsFlats, int isMinor)
+    {
+	trackData.add(0, new RogTrackEvent(RogTrackEvent.KEY_SIG_EVENT, isMinor, numSharpsFlats));
+    }
+
+
+    public int calculateBestKey()
+    {
+	int accidental;
+	int minAccidental = trackData.size();
+	int minKey = 0;
+
+	for (int numSharpFlat = 0; numSharpFlat < 8; numSharpFlat++) {
+	    accidental = checkAccidental(numSharpFlat);
+	    if (accidental < minAccidental) {
+		minAccidental = accidental;
+		minKey = numSharpFlat;
+	    }
+
+	    accidental = checkAccidental(-numSharpFlat);
+	    if (accidental < minAccidental) {
+		minAccidental = accidental;
+		minKey = -numSharpFlat;
+	    }
+	}
+
+	return minKey;
+    }
+
+
+    public int checkAccidental(int key)
+    {
+	int sharpsFlats[] = { 0, 0, 0, 0, 0, 0, 0 };
+	int nameValue[] = { 0, 2, 4, 5, 7, 9, 11 };
+
+	// Calculate the sharps and flats for this key
+	if (key < 0) {
+	    for (int flatNum = 1; flatNum <= (-key); flatNum++) {
+		sharpsFlats[((8 + flatNum) * 3) % 7]--;
+	    }
+	}
+	else {
+	    for (int sharpNum = 1; sharpNum <= key; sharpNum++) {
+		sharpsFlats[((sharpNum * 4) + 6) % 7]++;
+	    }
+	}
+
+	// Iterate through the track
+	int sumAccidental = 0;
+	for (int i = 0; i < trackData.size(); i++) {
+	    RogTrackEvent te = (RogTrackEvent) trackData.get(i);
+
+	    // Have we found a note??
+	    if (te.type <= RogTrackEvent.LAST_NOTE_EVENT) {
+		int semitone = te.param2;
+
+		// Calculate the accidental for this key
+		int noteName = getNoteName(key, semitone);
+		int numSharpsFlats = (semitone % 12) - nameValue[noteName];
+
+		if (numSharpsFlats < -6) {
+		    numSharpsFlats += 12;
+		}
+		if (numSharpsFlats > 6) {
+		    numSharpsFlats -= 12;
+		}
+
+		if (numSharpsFlats != sharpsFlats[noteName]) {
+		    sumAccidental++;
+		}
+	    }
+	}
+
+	return sumAccidental;
+    }
+
+
+    public void setNoteNames(int numSharpsFlats)
+    {
+	for (int i = 0; i < trackData.size(); i++) {
+	    RogTrackEvent te = (RogTrackEvent) trackData.get(i);
+	    if (te.type <= RogTrackEvent.LAST_NOTE_EVENT && te.param2 > 0) {
+		te.param1 = getNoteName(numSharpsFlats, te.param2);
+	    }
+	}
+    }
+
+
+    public int getNoteName(int key, int notePitch)
+    {
+	// Calculate the name of the key
+	int keyName = (((7 - key) * 3) % 7);
+
+	// Calculate the semitone start position
+	int keySemitoneStart = (((12 - key) * 5) % 12);
+
+	// Calculate the note name
+	int noteName = (keyName + nameOffset[(notePitch + 12 - keySemitoneStart) % 12]) % 7;
+	return noteName;
+    }
+
+
+    public String toString()
+    {
+	String str = "song \"" + databaseName + "\" \"" + songName + "\" " + wholeNoteDivisions + "\n";
+
+	// Display each of the TrackEvents in turn
+	for (int i = 0; i < trackData.size(); i++) {
+	    RogTrackEvent te = (RogTrackEvent) trackData.get(i);
+
+	    // Is this a note??
+	    if (te.type <= RogTrackEvent.LAST_NOTE_EVENT) {
+		char noteName;
+		if (te.param1 < 5) {
+		    noteName = (char) (te.param1 + 'C');
+		}
+		else {
+		    noteName = (char) (te.param1 - 5 + 'A');
+		}
+		str = str + te.type + " " + noteName + " " + te.param2 + "\n";
+	    }
+
+	    // Is this a time signature??
+	    else if (te.type == RogTrackEvent.TIME_SIG_EVENT) {
+		str = str + "timesig " + te.param1 + " " + te.param2 + "\n";
+	    }
+
+	    // Is this a key signature??
+	    else if (te.type == RogTrackEvent.KEY_SIG_EVENT) {
+		str = str + "keysig " + te.param1 + " " + te.param2 + "\n";
+	    }
+
+	    // Is this a fermata event??
+	    else if (te.type == RogTrackEvent.FERMATA_EVENT) {
+		str = str + "fermata\n";
+	    }
+
+	    // Is this a tempo event??
+	    else if (te.type == RogTrackEvent.TEMPO_EVENT) {
+		str = str + "tempo " + te.param1 + "\n";
+	    }
+	}
+
+	// Return the finished string
+	return str;
+    }
+
+    public void fromString(String rog_text)
+    {
+	int rog_text_len = rog_text.length();
+	byte[] rog_bytes = rog_text.getBytes();
+
+	parseFile(rog_bytes, rog_text_len);
+    }
+
+
+    /**
+     * 
+     * @return
+     * 		The converted melody. Null if was unable to convert
+     */
+    public Melody toMelody()
+    {
+	int track_len = trackData.size();
+
+	ArrayList filtered_td = new ArrayList();
+
+	for (int i = 0; i < track_len; i++) {
+	    RogTrackEvent te = (RogTrackEvent) trackData.get(i);
+
+	    // Is this a note??
+	    if (te.type <= RogTrackEvent.LAST_NOTE_EVENT) {
+		filtered_td.add(te);
+	    }
+	}
+
+	int filtered_track_len = filtered_td.size();
+
+	if (filtered_track_len<1) {
+	    //System.err.println("Insufficient track data for form relative pitch melody");
+	    return null;
+	}
+
+	ArrayList me_list = new ArrayList();
+
+	for (int i = 0; i < filtered_track_len-1; i++) {
+	    RogTrackEvent te = (RogTrackEvent) filtered_td.get(i);
+	    RogTrackEvent next_te = (RogTrackEvent) filtered_td.get(i+1);
+
+	    int duration = te.type;
+
+	    if (next_te.param2 == 0) { 
+		// is a rest
+		MelodyEvent me = new MelodyEvent(MelodyEvent.REST_EVENT,duration);
+		me_list.add(me);
+
+		// skip ahead one further position
+		i++;
+		continue;
+	    }
+
+	    int rel_pitch = next_te.param2 - te.param2;
+
+	    MelodyEvent me = new MelodyEvent(rel_pitch,duration);
+	    me_list.add(me);
+	}
+
+	int me_list_len = me_list.size();
+
+	MelodyEvent[] me_array = new MelodyEvent[me_list_len];
+	for (int i=0; i<me_list_len; i++) {
+	    me_array[i] = (MelodyEvent)me_list.get(i);
+	}
+
+	Melody me = new Melody(Melody.RELATIVE_PITCH,me_array);
+
+	return me;
+    }
+
+
+    // ---------------------------------------
+    //   RogToGif specific functions
+    // ---------------------------------------
+	
+    // ----------------------------------------------------------------------------------
+    //  Method  : loadFromFile
+    //  Returns : false - if the track could not be successfully loaded
+    //            true  - if the track was successfully loaded
+    // ----------------------------------------------------------------------------------
+    public boolean loadFromFile(File rogFile)
+    {
+	try {
+	    // Open the file with a FileInputStream, ready for parsing
+	    FileInputStream fileIn = new FileInputStream(rogFile);
+
+	    // Get the number of bytes in the file
+	    int rogLength = fileIn.available();
+
+	    // Create a buffer to store the file, and read the file into it
+	    byte[] rogData = new byte[rogLength];
+	    fileIn.read(rogData, 0, rogLength);
+
+	    // Parse the .rog file
+	    parseFile(rogData, rogLength);
+	}
+	catch (Exception ex) {
+	    JOptionPane.showMessageDialog(null, "Exception occurred reading file.\n\n" + ex);
+	    return false;
+	}
+
+	// File loaded successfully
+	return true;
+    }
+
+
+    private void parseFile(byte[] rogData, int rogLength)
+    {
+	// Break the file up into lines and parse each one seperately
+	int lineStart = 0;
+	for (int pos = 0; pos < rogLength; pos++) {
+	    if (rogData[pos] == '\n') {
+		parseLine(new String(rogData, lineStart, (pos - lineStart)));
+		lineStart = pos + 1;
+	    }
+	}
+	// If the last line is not newline-terminated then we have to parse it now
+	if (lineStart != rogLength) {
+	    parseLine(new String(rogData, lineStart, (rogLength - lineStart)));
+	}
+    }
+
+
+    private void parseLine(String line)
+    {
+	String sent = null;
+	String word = nextWord(line, 0);
+	int parsed = (word.length() + 1);
+
+	if (word.equals("") || word.equals("metadata")) {
+	    // do nothing => ignore it!
+	}
+	else if (word.equals("song")) {
+	    // Parse the first parameter - databaseName
+	    sent = nextSentence(line, parsed + 1);
+	    parsed += (sent.length() + 3);
+	    databaseName = sent;
+
+	    // Parse the second parameter - songName
+	    sent = nextSentence(line, parsed + 1);
+	    parsed += (sent.length() + 3);
+	    songName = sent;
+
+	    // Parse the third parameter - wholeDivision
+	    word = nextWord(line, parsed);
+	    parsed += (word.length() + 1);
+	    wholeNoteDivisions = (int) Double.parseDouble(word);
+	}
+
+	else if (word.equals("fermata")) {
+	    // Add a fermata event to the track (no parameters)
+	    trackData.add(new RogTrackEvent(RogTrackEvent.FERMATA_EVENT, 0, 0));
+	}
+
+	else if (word.equals("tempo")) {
+	    // Parse the first parameter - tempoBPM
+	    word = nextWord(line, parsed);
+	    parsed += (word.length() + 1);
+	    int tempoBPM = (int) Double.parseDouble(word);
+
+	    // Add a tempo event to the track
+	    trackData.add(new RogTrackEvent(RogTrackEvent.TEMPO_EVENT, tempoBPM, 0));
+	}
+
+	else if (word.equals("keysig")) {
+	    // Parse the first parameter - isMinor
+	    word = nextWord(line, parsed);
+	    parsed += (word.length() + 1);
+	    int isMinor = (int) Double.parseDouble(word);
+
+	    // Parse the second parameter - numSharpsFlats
+	    word = nextWord(line, parsed);
+	    parsed += (word.length() + 1);
+	    int numSharpsFlats = (int) Double.parseDouble(word);
+
+	    // Add this key signature event to the track
+	    trackData.add(0, new RogTrackEvent(RogTrackEvent.KEY_SIG_EVENT, isMinor, numSharpsFlats));
+	}
+
+	else if (word.equals("timesig")) {
+	    // Parse the first parameter - numerator
+	    word = nextWord(line, parsed);
+	    parsed += (word.length() + 1);
+	    int numerator = (int) Double.parseDouble(word);
+
+	    // Parse the second parameter - denominator
+	    word = nextWord(line, parsed);
+	    parsed += (word.length() + 1);
+	    int denominator = (int) Double.parseDouble(word);
+
+	    // Add this time signature event to the track
+	    trackData.add(new RogTrackEvent(RogTrackEvent.TIME_SIG_EVENT, numerator, denominator));
+	}
+
+	else {
+	    // This must be a note line, so parse the duration
+	    int duration = (int) Double.parseDouble(word);
+
+	    // Parse the first parameter - noteName
+	    word = nextWord(line, parsed);
+	    parsed += (word.length() + 1);
+	    char noteName = word.charAt(0);
+
+	    // Check that the noteName is valid
+	    if (noteName < 'A' || noteName > 'G') {
+		JOptionPane.showMessageDialog(null, "Parsing error - check that the Rog file is valid.");
+		return;
+	    }
+
+	    // Convert the noteName character into it's MIDI integer equivalent
+	    int noteNum;
+	    if (noteName >= 'C') {
+		noteNum = noteName - 'C';
+	    }
+	    else {
+		noteNum = noteName - 'A' + 5;
+	    }
+
+	    // Parse the second parameter - pitch
+	    word = nextWord(line, parsed);
+	    parsed += (word.length() + 1);
+	    int pitch = (int) Double.parseDouble(word);
+
+	    // Add this note event to the track
+	    trackData.add(new RogTrackEvent(duration, noteNum, pitch));
+	}
+    }
+
+
+    private String nextSentence(String line, int sentStart)
+    {
+	for (int pos = sentStart; pos < line.length(); pos++) {
+	    // We have found a quote mark, so return the sentence found
+	    if (line.charAt(pos) == '"') {
+		return (new String(line.substring(sentStart, pos)));
+	    }
+	}
+	// We have reached the end of the string, so return the remainder
+	return (new String(line.substring(sentStart, line.length())));
+    }
+
+
+    private String nextWord(String line, int wordStart)
+    {
+	for (int pos = wordStart; pos < line.length(); pos++) {
+	    // We have found a space, so return the word found
+	    if (line.charAt(pos) == ' ') {
+		return (new String(line.substring(wordStart, pos)));
+	    }
+	}
+	// We have reached the end of the string, so return the remainder
+	return (new String(line.substring(wordStart, line.length())));
+    }
+
+
+    public String getDatabaseName()
+    {
+	return databaseName;
+    }
+
+
+    public String getSongName()
+    {
+	return songName;
+    }
+
+
+    public int getWholeNoteDivisions()
+    {
+	return wholeNoteDivisions;
+    }
+
+
+    // ---------------------------------------
+    //   SymInput specific functions
+    // ---------------------------------------
+    public void setWholeNoteDivisions(int wnd)
+    {
+	wholeNoteDivisions = wnd;
+    }
+
+
+    // ----------------------------------------------------------------------------------
+    //  Method  : saveToFile
+    //  Returns : false - if the track could not be successfully saved
+    //            true  - if the track was successfully saved
+    // ----------------------------------------------------------------------------------
+    public boolean saveToFile(File outFile)
+    {
+	// Convert the track to a string and save it to the specified file
+	try {
+	    FileOutputStream fileOut = new FileOutputStream(outFile);
+	    fileOut.write(toString().getBytes());
+	    fileOut.close();
+	}
+	catch (Exception ex) {
+	    JOptionPane.showMessageDialog(null, "Exception occurred writing to file.\n\n" + ex);
+	    return false;
+	}
+
+	// File saved successfully
+	return true;
+    }
+}
Index: /trunk/src_apollo/org/apollo/meldex/RogTrackEvent.java
===================================================================
--- /trunk/src_apollo/org/apollo/meldex/RogTrackEvent.java	(revision 315)
+++ /trunk/src_apollo/org/apollo/meldex/RogTrackEvent.java	(revision 315)
@@ -0,0 +1,24 @@
+package org.apollo.meldex;
+
+public class RogTrackEvent
+{
+    // Special track event constants
+    final static int LAST_NOTE_EVENT = 127;
+    final static int FERMATA_EVENT = 128;
+    final static int KEY_SIG_EVENT = 129;
+    final static int TIME_SIG_EVENT = 130;
+    final static int TEMPO_EVENT = 131;
+
+    // The type of the track event and it's parameters
+    int type;
+    int param1;
+    int param2;
+
+
+    public RogTrackEvent(int t, int p1, int p2)
+    {
+	type = t;
+	param1 = p1;
+	param2 = p2;
+    }
+}
Index: /trunk/src_apollo/org/apollo/meldex/StandardisedMelody.java
===================================================================
--- /trunk/src_apollo/org/apollo/meldex/StandardisedMelody.java	(revision 315)
+++ /trunk/src_apollo/org/apollo/meldex/StandardisedMelody.java	(revision 315)
@@ -0,0 +1,98 @@
+package org.apollo.meldex;
+
+import java.io.FileInputStream;
+import java.io.FileNotFoundException;
+import java.io.FileOutputStream;
+import java.io.IOException;
+import java.io.ObjectInputStream;
+import java.io.ObjectOutputStream;
+import java.io.Serializable;
+
+
+/**
+ */
+public class StandardisedMelody implements Serializable
+{
+
+	private static final long serialVersionUID = -7246438481007658500L;
+	
+	/** The array of MelodyEvents that form this melody. */
+    private MelodyEvent[] events;
+
+
+    /**
+     * Creates a new StandardisedMelody with the given melody events.
+     *
+     * @param events The melody events that form the melody.
+     */
+    public StandardisedMelody(MelodyEvent[] events)
+    {
+	this.events = events;
+    }
+
+
+    /**
+     * Returns the length (number of melody events) of this melody.
+     *
+     * @return the length (number of melody events) of this melody.
+     */
+    public int getLength()
+    {
+	return events.length;
+    }
+
+
+    /**
+     * Returns a specified melody event from the melody.
+     *
+     * @param index The index of the required melody event.
+     *
+     * @return The melody event at the specified index.
+     */
+    public MelodyEvent getEvent(int index)
+    {
+	return events[index];
+    }
+
+
+    /**
+     */
+    public void setEvent(int index, MelodyEvent event)
+    {
+	events[index] = event;
+    }
+
+
+    /**
+     */
+    public static void writeMelodyToFile(String melodyFileName, StandardisedMelody melody)
+    	throws FileNotFoundException, IOException
+    {
+	    // Open the melody file for writing serialized objects
+	    FileOutputStream fos = new FileOutputStream(melodyFileName);
+	    ObjectOutputStream melodyFileStream = new ObjectOutputStream(fos);
+
+	    // Write the melody to the melody file
+	    melodyFileStream.writeObject(melody);
+
+	    // Flush and close the collection file
+	    melodyFileStream.flush();
+	    melodyFileStream.close();
+
+    }
+
+
+    /**
+     */
+    public static StandardisedMelody readMelodyFromFile(String melodyFileName) 
+    	throws FileNotFoundException, IOException, ClassNotFoundException
+    {
+	    // Open the melody file for reading serialized objects
+	    FileInputStream fis = new FileInputStream(melodyFileName);
+	    ObjectInputStream melodyFileStream = new ObjectInputStream(fis);
+
+	    // Read the melody from the melody file
+	    return (StandardisedMelody) melodyFileStream.readObject();
+
+    }
+}
Index: /trunk/src_apollo/org/apollo/meldex/Transcriber.java
===================================================================
--- /trunk/src_apollo/org/apollo/meldex/Transcriber.java	(revision 315)
+++ /trunk/src_apollo/org/apollo/meldex/Transcriber.java	(revision 315)
@@ -0,0 +1,383 @@
+package org.apollo.meldex;
+
+import java.io.IOException;
+import java.util.ArrayList;
+
+@SuppressWarnings("unchecked") // code in java 1.4
+public class Transcriber
+{
+    // The values of these constants are taken directly from Rodger's code
+    final double RMS_WINDOW = 10.0;
+    final double RMS_STEP = (RMS_WINDOW / 2);
+
+    // The sample rate (frequency) of the loaded sample
+    int sampleRate;
+
+    // Create instances of our classes
+    PitchTracker pitchTracker = new PitchTracker();
+    NoteRounder noteRounder = new NoteRounder();
+
+
+    /**
+     * 
+     * @param sample
+     * 
+     * @return
+     * 		The wave sample converted into a RogTrack structure if successful.
+     * 		Null if unsuccessful.
+     * 
+     * @throws IOException
+     * 		If conversion to standardized form fails
+     */
+    public RogTrack transcribeSample(WavSample sample) throws IOException
+    {
+	// This shouldn't have to be fixed...
+	sampleRate = 22050;
+    //	sampleRate = (int)SampledAudioManager.PLAYBACK_SAMPLE_RATE;
+
+	// These are values that should end up being parameters
+	int tempoBPM = 120;
+	int minRestLength = 4, minNoteLength = 2;
+	boolean addRestLeftovers = true;
+
+	// Check that the sample is valid
+	if (sample == null || sample.getFormat() == null || sample.getRawAudio() == null) {
+	    return null;
+	}
+
+	// Get the standardised unsigned 8-bit mono data
+	//byte[] stdData = sample.getStandardisedData(true); // THis is bad of memory - was getting lots heap errors
+//	AudioFormat standardizedFormat = new AudioFormat(
+//			//sample.getFormat().getSampleRate(),
+//			sampleRate,
+//			//sample.getFormat().getSampleRate(),
+//			8, // 8-bit
+//			1, // mono
+//			false, // unsigned
+//			true); // Big endian 
+//	
+//	byte[] stdData = AudioIO.convertAudioBytes(
+//			sample.getRawAudio(), 
+//			sample.getFormat(), 
+//			standardizedFormat);
+	
+	byte[] stdData = MeldexConversion.toStandardizedFormat(
+			sample.getRawAudio(), sample.getFormat(), sampleRate);
+	
+	if (stdData == null) {
+	    return null;
+	}
+
+	// Create a new RogTrack to store the result of the transcription
+	RogTrack track = new RogTrack();
+
+	// We don't know the time signature
+	track.addTimeSignature(0, 0);
+
+	// Calculate the Root Mean Squared data for the loaded sample
+	float[] rmsData = calculateRMS(stdData, stdData.length);
+
+	// Check that we have some data to process
+	if (rmsData.length <= 0) {
+	    return null;
+	}
+
+	
+	// int rmsStep = 111;  // !! PURE-ROG!!
+	int rmsStep = (sampleRate / 200);
+	//int rmsStep = (sampleRate / 1000);
+	
+
+	// Calculate the number of RMS steps per sixteenth note
+	int rmsStepsPer16th = (int) ((60F / (float) (tempoBPM * 4F)) / (RMS_STEP / 1000F));
+
+	// Calculate the sum of the squared RMS data
+	double sumRMSDataSquared = 0;
+	for (int rmsPos = 0; rmsPos < rmsData.length; rmsPos++) {
+	    sumRMSDataSquared += (rmsData[rmsPos] * rmsData[rmsPos]);
+	}
+
+	// Calculate the upper and lower thresholds (asymmetrical??)
+	double threshold = Math.sqrt(sumRMSDataSquared / rmsData.length);
+	float upperThreshold = (float) (threshold * 0.55);
+	float lowerThreshold = (float) (threshold * 0.35);
+
+	// Now we find all of the notes in the sample...
+	int lastCents = -1, lastStart = 0, lastEnd = 0;
+	int restLen, noteLen;
+	for (int rmsPos = 0; rmsPos < rmsData.length; rmsPos++) {
+
+	    // Have we found the start of a note??
+	    if (rmsData[rmsPos] >= upperThreshold) {
+
+		// Yes, so find the end of the note
+		int noteStart = rmsPos;
+		while (rmsPos < rmsData.length) {
+		    if (rmsData[rmsPos] <= lowerThreshold) {
+			break;
+		    }
+		    rmsPos++;
+		}
+
+		int noteEnd = rmsPos;
+		// If it is longer than 1/3 of the minimum note length, process it
+		if ((noteEnd - noteStart) >= ((minNoteLength * rmsStepsPer16th) / 3)) {
+
+		    // Create a new array for the interesting data
+		    int noteLength = (noteEnd - noteStart) * rmsStep;
+		    byte[] noteData = new byte[noteLength];
+		    for (int notePos = 0; notePos < noteLength; notePos++) {
+			noteData[notePos] = stdData[(noteStart * rmsStep) + notePos];
+		    }
+
+		    // Process the note
+		    int cents = processNote(noteData, noteLength);
+		    if (cents > 1000) {
+
+			// If it is the first note then initialise the NoteRounder
+			if (lastCents < 0) {
+			    int noteRound = (noteRounder.roundCents(cents) - cents); 
+			    noteRounder.setInitialValue(noteRound);
+			}
+			// Otherwise we calculate the note and rest values
+			else {
+			    int finalNote = noteRounder.roundNote(lastCents, true);
+
+			    // Calculate the rest length
+			    int rmsRestLeft = 0;
+			    if (rmsStepsPer16th <= 0) {
+				restLen = 0;
+			    }
+			    else {
+				int rmsRestLen = (noteStart - lastEnd);
+				int mult = (minRestLength * rmsStepsPer16th);
+				int roundRest = noteRounder.round(rmsRestLen, mult);
+				restLen = (roundRest / rmsStepsPer16th);
+				if (addRestLeftovers == true) {
+				    rmsRestLeft = rmsRestLen - (restLen * rmsStepsPer16th);
+				}
+			    }
+
+			    // Calculate the note length
+			    if (rmsStepsPer16th <= 0) {
+				noteLen = 0;
+			    }
+			    else {
+				int rmsNoteLen = (lastEnd - lastStart);
+				int mult = (minNoteLength * rmsStepsPer16th);
+				if (addRestLeftovers == true && rmsRestLeft > 0) {
+				    rmsNoteLen += rmsRestLeft;
+				}
+				int roundNote = noteRounder.round(rmsNoteLen, mult);
+				noteLen = (roundNote / rmsStepsPer16th);
+			    }
+
+			    // Add the notes and rests to the track
+			    track.addNote(noteLen, (finalNote / 100), 0);
+			    track.addRest(restLen);
+			}
+
+			lastCents = cents;
+			lastStart = noteStart;
+			lastEnd = noteEnd;
+		    }
+		}
+	    }
+	}
+
+	// Deal with the last note (if there is one)
+	if (lastCents > 0) {
+	    int finalNote = noteRounder.roundNote(lastCents, true);
+
+	    // Calculate the note length
+	    if (rmsStepsPer16th <= 0) {
+		noteLen = 0;
+	    }
+	    else {
+		int rmsNoteLen = (lastEnd - lastStart);
+		int mult = (minNoteLength * rmsStepsPer16th);
+		int roundNote = noteRounder.round(rmsNoteLen, mult);
+		noteLen = (roundNote / rmsStepsPer16th);
+
+		// Add the last note to the track
+		track.addNote(noteLen, (finalNote / 100), 0);
+	    }
+	}
+
+	// Calculate the key signature
+	int key = track.calculateBestKey();
+
+	// Add a key signature event to the track
+	track.addKeySignature(key, 0);
+
+	// Set the note names for the new key signature
+	track.setNoteNames(key);
+
+	// Return the Track with the transcribed data
+	return track;
+    }
+
+
+    private float[] calculateRMS(byte[] stdData, int stdLength)
+    {
+	int accMS = 0;
+	int accMSPrev = 0;
+	int frameStart = 0;
+
+	// int rmsStep = 111;  // !! PURE ROG!!
+	int rmsStep = (22050 / 200);  // !! SEMI-PURE ROG !!
+
+	// Allocate memory for the RMS data values
+	int rmsLength = (int) (stdLength / (float) rmsStep);
+	float[] rmsData = new float[rmsLength];
+
+	for (int stdPos = 0, rmsPos = 0; stdPos < stdLength; stdPos++) {
+	    // Calculate the accumulated Mean Squared value
+	    int value = (stdData[stdPos] & 255) - 127;   // !! SHOULD 127 BE 128?? !!
+	    accMS += (value * value);
+
+	    // If we have finished a frame calculate a value for the output
+	    if ((stdPos - frameStart) >= rmsStep) {
+		double result = Math.sqrt((double)(accMS+accMSPrev) / (double)(rmsStep*2));
+		frameStart = stdPos;
+		accMSPrev = accMS;
+		accMS = 0;
+		rmsData[rmsPos] = (float) result;
+		rmsPos++;
+	    }
+	}
+
+	return rmsData;
+    }
+
+
+    private int processNote(byte[] data, int length)
+    {
+	// Pitch track the note
+	ArrayList pitchData = pitchTracker.process(data, length, sampleRate);
+
+	// Average the pitch data
+	int pitchLength = averagePitchData(pitchData);
+
+	// Calculate the histogram of the pitch data
+	return calculateHistogram(pitchData, pitchLength);
+    }
+
+
+    private int averagePitchData(ArrayList pitchData)
+    {
+	// Loop through the pitch values...
+	int i = 0, k = 0;
+	while (i < (pitchData.size() - 1)) {
+	    int startPos = ((PitchValue) pitchData.get(i)).position;
+	    double period = ((PitchValue) pitchData.get(i)).period;
+	    double averagePeriod = period;
+	    double runningPeriod = ((PitchValue) pitchData.get(i+1)).position - startPos;
+	    double numPeriods = runningPeriod / period;
+
+	    // Start from the next pitch
+	    int j = 1;
+	    while ((i+j) < (pitchData.size() - 1)) {
+		// Get the next pitch estimate
+		period = ((PitchValue) pitchData.get(i+j)).period;
+		int position = ((PitchValue) pitchData.get(i+j)).position;
+
+		// Make sure that this period is covered by the average so far
+		if (period > (averagePeriod * 1.1) || period < (averagePeriod * 0.909)) {
+		    break;
+		}
+
+		// Stop if we have covered more than 20 msec
+		int nextPos = ((PitchValue) pitchData.get(i+j+1)).position;
+		// if ((nextPos - startPos) >= 445) {  // !! PURE-ROG !!
+		if ((nextPos - startPos) >= (sampleRate * 0.02)) {
+		    break;
+		}
+
+		// Add to this period
+		runningPeriod += (nextPos - position);
+		numPeriods += ((nextPos - position) / period);
+		averagePeriod = runningPeriod / numPeriods;
+		j++;
+	    }
+
+	    // Write over the original pitch value with the averaged pitch value
+	    pitchData.set(k, new PitchValue(averagePeriod, startPos));
+	    k++;
+
+	    // Increment i
+	    i += j;
+	}
+
+	// Return the number of averaged pitch values
+	return k;
+    }
+
+
+    private int calculateHistogram(ArrayList data, int length)
+    {
+	// This probably shouldn't be a constant
+	final int histLength = 960;
+
+	// Allocate memory for the histogram data and initialise it to zero
+	int[] histData = new int[histLength];
+	for (int i = 0; i < histLength; i++) {
+	    histData[i] = 0;
+	}
+
+	// Calculate the histogram data
+	for (int i = 0; i < (length - 1); i++) {
+	    double period = ((PitchValue) data.get(i)).period;
+	    double position = ((PitchValue) data.get(i)).position;
+	    double periodLength = ((PitchValue) data.get(i+1)).position - position;
+
+	    // We only need to do this if the pitch does not equal zero
+	    if (period > 0) {
+		// Do some weird stuff...
+		double logValue = (Math.log(period / sampleRate)) / (Math.log(10));
+		double cents = -(logValue / 0.000250858) - 3637.622659;
+		int histPos = (int) (Math.floor((cents - 3600.0) / 5.0));
+		if (histPos >= 0 && histPos < histLength) {
+		    histData[histPos] += periodLength;
+		}
+	    }
+	}
+
+	// Select the cents value using the histogram data
+	return selectCents(histData, histLength);
+    }
+
+
+    private int selectCents(int[] histData, int histLength)
+    {
+	// Find the maximum average histogram value
+	int maxHistValue = 0, maxHistPos = 0;
+	for (int i = 0; i < (histLength - 20); i++) {
+	    // Sum the next 20 histogram values (1 semitone)
+	    int histValue = 0;
+	    for (int j = 0; j < 20; j++) {
+		histValue += histData[i+j];
+	    }
+
+	    // Maintain the maximum histogram value
+	    if (histValue > maxHistValue) {
+		maxHistValue = histValue;
+		maxHistPos = i;
+	    }
+	}
+
+	// Find the average cents value of the maximum histogram
+	int sumCents = 0;
+	for (int j = 0; j < 20; j++) {
+	    sumCents += (histData[maxHistPos+j] * (((maxHistPos+j) * 5) + 3602));
+	}
+
+	// Calculate sum of (cents * time) divided by total time
+	if (maxHistValue > 0) {
+	    return (sumCents / maxHistValue);
+	}
+	else {
+	    return 0;
+	}
+    }
+}
Index: /trunk/src_apollo/org/apollo/meldex/WavSample.java
===================================================================
--- /trunk/src_apollo/org/apollo/meldex/WavSample.java	(revision 315)
+++ /trunk/src_apollo/org/apollo/meldex/WavSample.java	(revision 315)
@@ -0,0 +1,380 @@
+package org.apollo.meldex;
+
+
+import java.io.File;
+import java.util.ArrayList;
+import javax.sound.sampled.AudioFileFormat;
+import javax.sound.sampled.AudioFormat;
+import javax.sound.sampled.AudioInputStream;
+import javax.sound.sampled.AudioSystem;
+import javax.swing.JOptionPane;
+
+@SuppressWarnings("unchecked") // code in java 1.4
+public class WavSample
+{
+    // Data representing the currently loaded file at a high level of abstraction
+    private File file = null;
+    private AudioFormat format = null;
+    AudioInputStream audioData = null;
+
+    // The actual raw audio data (without header). This shouldn't need to be used often.
+    byte[] rawData = null;
+
+    // The standardised unsigned 8-bit mono data. This should be used instead of the raw data.
+    private byte[] stdData = null;
+
+
+    public WavSample(byte[] rawData, AudioFormat format)
+    {
+    	this.rawData = rawData;
+    	this.format = format;
+    }
+    
+    public WavSample()
+    {
+    }
+
+
+    public WavSample(AudioInputStream data)
+    {
+	audioData = data;
+
+	buildSampleData();
+    }
+
+
+    // ----------------------------------------------------------------------------------
+    //  Method  : buildSampleData
+    //  Returns : true  - if the sample data was successfully built
+    //            false - if the sample data was not successfully built
+    // ----------------------------------------------------------------------------------
+    private boolean buildSampleData()
+    {
+	// Reset the sample to the beginning
+	if (reset() == false) {
+	    return false;
+	}
+
+	// Get the format of the loaded file
+	format = audioData.getFormat();
+
+	try {
+	    // Get the length of the loaded sample
+	    int rawLength = audioData.available();
+
+	    // Load the sample data into an array ready for processing
+	    rawData = new byte[rawLength];
+	    audioData.read(rawData, 0, rawLength);
+	}
+	catch (Exception ex) {
+	    JOptionPane.showMessageDialog(null, "Exception occurred creating sample.\n\n" + ex);
+	    return false;
+	}
+
+	// Sample data built successfully
+	return true;
+    }
+
+
+    // ----------------------------------------------------------------------------------
+    //  Method  : reset
+    //  Returns : true  - if the sample was successfully reset
+    //            false - if the sample was not successfully reset
+    // ----------------------------------------------------------------------------------
+    public boolean reset()
+    {
+	// For some bizarre reason we cannot reset a loaded file, so it is necessary to reload it
+	if (file != null) {
+	    try {
+		audioData = AudioSystem.getAudioInputStream(file);
+	    }
+	    catch (Exception ex) {
+		JOptionPane.showMessageDialog(null, "Exception occurred reloading sample.\n\n" + ex);
+		return false;
+	    }
+	}
+
+	// Otherwise we just reset the stream to the start, ready for playback
+	else if (audioData != null) {
+	    try {
+		audioData.reset();
+	    }
+	    catch (Exception ex) {
+		JOptionPane.showMessageDialog(null, "Exception occurred resetting audio stream.\n\n" + ex);
+		return false;
+	    }
+	}
+
+	// Reset performed successfully
+	return true;
+    }
+
+
+    // ----------------------------------------------------------------------------------
+    //  Method  : getFormat
+    //  Returns : null - if the sample does not have a specified format
+    //            else - the AudioFormat of the audio sample
+    // ----------------------------------------------------------------------------------
+    public AudioFormat getFormat()
+    {
+	if (format == null) {
+	    if (audioData != null) {
+		format = audioData.getFormat();
+	    }
+	}
+
+	return format;
+    }
+
+
+    // ----------------------------------------------------------------------------------
+    //  Method  : getBytesPerSecond
+    //  Returns : -1   - if the value could not be successfully calculated
+    //            else - the number of bytes of audio data per second
+    // ----------------------------------------------------------------------------------
+    public int getBytesPerSecond()
+    {
+	if (format == null) {
+	    return -1;
+	}
+
+	int frameSize = (format.getChannels() * (format.getSampleSizeInBits() / 8));
+	return (int) (format.getSampleRate() * frameSize);
+    }
+
+
+    // ----------------------------------------------------------------------------------
+    //  Method  : getStandardisedData
+    //  Returns : null - if the sample could not be successfully standardised
+    //            else - byte[] containing the standardised unsigned 8-bit mono data
+    // ----------------------------------------------------------------------------------
+    public byte[] getStandardisedData(boolean createIfNotComputed)
+    {
+	if (stdData == null && createIfNotComputed) {
+	    // Create the standardised unsigned 8-bit mono data
+	    // stdData = toStandardForm(format, rawData, rawData.length, (int) format.getSampleRate());
+	    stdData = toStandardForm(format, rawData, (int) format.getSampleRate());
+	}
+
+	return stdData;
+    }
+
+
+    // ----------------------------------------------------------------------------------
+    //  Method  : toStandardForm
+    //  Returns : null - if the sample could not be successfully standardised
+    //            else - byte[] containing the standardised unsigned 8-bit mono data
+    // ----------------------------------------------------------------------------------
+    // public static byte[] toStandardForm(AudioFormat format, byte[] inData, int amount, int frequency)
+    public static byte[] toStandardForm(AudioFormat format, byte[] inData, int frequency)
+    {
+	// Check for (supposedly) invalid sample formats
+	if (format.getSampleSizeInBits() == 8) {
+	    if (format.getEncoding().equals(AudioFormat.Encoding.PCM_SIGNED)) {
+		JOptionPane.showMessageDialog(null, "Oops! Internal Error: Unexpected audio format (signed 8-bit).");
+		return null;
+	    }
+	}
+	if (format.getSampleSizeInBits() == 16) {
+	    if (format.getEncoding().equals(AudioFormat.Encoding.PCM_UNSIGNED)) {
+		JOptionPane.showMessageDialog(null, "Oops! Internal Error: Unexpected audio format (unsigned 16-bit).");
+		return null;
+	    }
+	}
+
+	int baseSampleSize = (int) (format.getSampleRate() / frequency);
+	int frameSize = (format.getChannels() * (format.getSampleSizeInBits() / 8));
+	int sampleSize = (baseSampleSize * frameSize);
+
+	// Create a new list to store the standardised sample values
+	ArrayList stdTemp = new ArrayList();
+
+	// Simply pick values from the sample at the frequency specified
+	// for (int i = 0; i < amount; i += sampleSize) {
+	for (int i = 0; i < inData.length; i += sampleSize) {
+	    for (int j = 0; j < frameSize; j++) {
+		int val = inData[i+j];
+		stdTemp.add(new Integer(val));
+	    }
+	}
+
+	// Convert the sample to standard unsigned, 8-bit, mono values
+	stdTemp = toStandardEndian(format, stdTemp);
+	stdTemp = toUnsignedValues(format, stdTemp);
+	stdTemp = to8bit(format, stdTemp);
+	stdTemp = toMono(format, stdTemp);
+
+	// Allocate memory for the standardised data
+	byte[] tmpData = new byte[stdTemp.size()];
+
+	// Convert the ArrayList back to an array of bytes
+	for (int i = 0; i < stdTemp.size(); i++) {
+	    int val = ((Integer) stdTemp.get(i)).intValue();
+	    tmpData[i] = (byte) val;
+	}
+
+	return tmpData;
+    }
+
+
+    private static ArrayList toStandardEndian(AudioFormat format, ArrayList inData)
+    {
+	// Endian details do not apply to 8-bit samples, so no conversion needed
+	if (format.getSampleSizeInBits() == 8) {
+	    return inData;
+	}
+
+	// Create a new list to store the results
+	ArrayList outData = new ArrayList();
+
+	// Simply convert each number in the sample to the endian form for the current system
+	for (int i = 0; i < inData.size(); i += 2) {
+
+	    int MSB, LSB;
+	    if (format.isBigEndian()) {
+		MSB = ((Integer) inData.get(i)).intValue();    // First byte is MSB (high order)
+		LSB = ((Integer) inData.get(i+1)).intValue();  // Second byte is LSB (low order)
+	    }
+	    else {
+		LSB = ((Integer) inData.get(i)).intValue();    // First byte is LSB (low order)
+		MSB = ((Integer) inData.get(i+1)).intValue();  // Second byte is MSB (high order)
+	    }
+
+	    int val = (MSB << 8) + (LSB & 255);
+	    outData.add(new Integer(val));
+	}
+
+	return outData;
+    }
+
+
+    private static ArrayList toUnsignedValues(AudioFormat format, ArrayList inData)
+    {
+	// PCM_UNSIGNED samples are what we want, so no conversion needed
+	if (format.getEncoding().equals(AudioFormat.Encoding.PCM_UNSIGNED)) {
+	    return inData;
+	}
+
+	// Create a new list to store the results
+	ArrayList outData = new ArrayList();
+
+	// Simply convert each signed number in the sample to an unsigned value
+	for (int i = 0; i < inData.size(); i++) {
+
+	    int val = ((Integer) inData.get(i)).intValue();
+	    val = val - 32768;
+	    if (val < -32768) {
+		val += 65536;
+	    }
+
+	    outData.add(new Integer(val));
+	}
+
+	return outData;
+    }
+
+
+    private static ArrayList to8bit(AudioFormat format, ArrayList inData)
+    {
+	// 8-bit data is what we want, so no conversion necessary
+	if (format.getSampleSizeInBits() == 8) {
+	    return inData;
+	}
+
+	// Create a new list to store the results
+	ArrayList outData = new ArrayList();
+
+	// Simply reduce each value in the sample to an 8-bit value
+	for (int i = 0; i < inData.size(); i++) {
+
+	    int val = ((Integer) inData.get(i)).intValue();
+	    val = val / 256;
+
+	    outData.add(new Integer(val));
+	}
+
+	return outData;
+    }
+
+
+    private static ArrayList toMono(AudioFormat format, ArrayList inData)
+    {
+	// Mono-channel data is what we want, so no conversion necessary
+	if (format.getChannels() == 1) {
+	    return inData;
+	}
+
+	// Create a new list to store the results
+	ArrayList outData = new ArrayList();
+
+	// Simply average all of the channels for each frame
+	for (int i = 0; i < inData.size(); i += format.getChannels()) {
+
+	    int sum = 0;
+	    for (int j = 0; j < format.getChannels(); j++) {
+		int val = ((Integer) inData.get(i+j)).intValue();
+		// We need to use the true unsigned value
+		if (val < 0) {
+		    val = val + 256;
+		}
+		sum += val;
+	    }
+
+	    int val = sum / format.getChannels();
+	    // Revert back to signed representation
+	    if (val > 127) {
+		val = val - 256;
+	    }
+
+	    outData.add(new Integer(val));
+	}
+
+	return outData;
+    }
+
+
+    // ----------------------------------------------------------------------------------
+    //  Method  : loadFromFile
+    //  Returns : false - if the track could not be successfully loaded
+    //            true  - if the track was successfully loaded
+    // ----------------------------------------------------------------------------------
+    public boolean loadFromFile(File wavFile)
+    {
+	file = wavFile;
+
+	return buildSampleData();
+    }
+
+
+    // ----------------------------------------------------------------------------------
+    //  Method  : saveToFile
+    //  Returns : false - if the sample could not be successfully saved
+    //            true  - if the sample was successfully saved
+    // ----------------------------------------------------------------------------------
+    public boolean saveToFile(File outFile)
+    {
+	// Reset the sample to the beginning
+	if (reset() == false) {
+	    return false;
+	}
+
+	// Write the audio data to the selected file in the format specified
+	try {
+	    if (AudioSystem.write(audioData, AudioFileFormat.Type.WAVE, outFile) == -1) {
+		JOptionPane.showMessageDialog(null, "Problem occurred writing to file.");
+		return false;
+	    }
+	}
+	catch (Exception ex) {
+	    JOptionPane.showMessageDialog(null, "Exception occurred saving file.\n\n" + ex);
+	    return false;
+	}
+
+	// File saved successfully
+	return true;
+    }
+    
+    public byte[] getRawAudio() {
+    	return rawData;
+    }
+}
Index: /trunk/src_apollo/org/apollo/mvc/AbstractSubject.java
===================================================================
--- /trunk/src_apollo/org/apollo/mvc/AbstractSubject.java	(revision 315)
+++ /trunk/src_apollo/org/apollo/mvc/AbstractSubject.java	(revision 315)
@@ -0,0 +1,76 @@
+package org.apollo.mvc;
+
+import java.util.HashSet;
+import java.util.Set;
+
+import javax.swing.SwingUtilities;
+
+/**
+ * A Skeletal implementation of a Subject in the model-view-controller package.
+ * 
+ * @author Brook Novak
+ */
+public abstract class AbstractSubject implements Subject {
+
+	/** Collection of registered observers. */
+	protected Set<Observer> observers = new HashSet <Observer>();
+
+	/**
+	 * Adds an observer to the set of registered observers.
+	 * @param ob The observer to add.
+	 */
+	public final void addObserver(final Observer ob) {
+        synchronized (observers) {
+        	observers.add(ob);
+        }
+		ob.setObservedSubject(this);
+	}
+
+	/**
+	 * Removes an observer from the set of registered observers.
+	 * 
+	 * @param ob The observer to remove
+	 */
+	public final void removeObserver(final Observer ob) {
+        synchronized (observers) {
+        	observers.remove(ob);
+        }
+		ob.setObservedSubject(null);
+	}
+
+	/**
+	 * Notifies all observers that this object has changed with the given
+	 * ModelChangeEvent.
+	 * 
+	 * @param event
+	 *            A ModelChangeEvent which specifies how this object has
+	 *            changed.
+	 */
+	protected final void fireSubjectChanged(final SubjectChangedEvent event) {
+        
+        synchronized (observers) {
+    		for (Observer ob : observers) {
+    			ob.modelChanged(this, event);
+    		}
+        }
+	}
+	
+	/**
+	 * Notifies all observers that this object has changed with the given
+	 * ModelChangeEvent. On the swing thread.
+	 * 
+	 * @param event
+	 *            A ModelChangeEvent which specifies how this object has
+	 *            changed.
+	 */
+	protected final void fireSubjectChangedLaterOnSwingThread(final SubjectChangedEvent event) {
+		
+		SwingUtilities.invokeLater(new Runnable() {
+			public void run() {
+				fireSubjectChanged(event);
+			}
+		});
+		
+	}
+	
+}
Index: /trunk/src_apollo/org/apollo/mvc/Observer.java
===================================================================
--- /trunk/src_apollo/org/apollo/mvc/Observer.java	(revision 315)
+++ /trunk/src_apollo/org/apollo/mvc/Observer.java	(revision 315)
@@ -0,0 +1,40 @@
+package org.apollo.mvc;
+
+/**
+ * An Observer as any object that would like to be notified
+ * when another object changes. To do this the object that will change
+ * must implement the Subject interface and then the Observer object
+ * should be registered with the Subject by calling addObserver()
+ * 
+ * @author Brook NOvak
+ */
+public interface Observer {
+	
+	/**
+	 * The method called when a Subject that this observer has
+	 * registered with changes and the Subject has specified a SubjectChangedEvent.
+	 * 
+	 * @param event A type of SubjectChangedEvent
+	 * @param source The subject which the event was fired from
+	 */
+	void modelChanged(Subject source, SubjectChangedEvent event);
+	
+	/**
+	 * Informs this observer what it is observing.
+	 * The subject will call this automatically.
+	 * The observer must know what it's observing so that
+	 * it can update based on the subject's data.
+	 * 
+	 * @param parent the subject we are observing
+	 */
+	void setObservedSubject(Subject parent);
+
+	/**
+	 * Provides functionality to retrieve the subject that this class
+	 * will be observing.
+	 * 
+	 * @return the subject we are observing
+	 */
+	Subject getObservedSubject();
+	
+}
Index: /trunk/src_apollo/org/apollo/mvc/Subject.java
===================================================================
--- /trunk/src_apollo/org/apollo/mvc/Subject.java	(revision 315)
+++ /trunk/src_apollo/org/apollo/mvc/Subject.java	(revision 315)
@@ -0,0 +1,22 @@
+package org.apollo.mvc;
+
+/**
+ * A Subject is any object that wants to notify other objects of
+ * changes in its state. 
+ *@author Brook Novak
+ */
+public interface Subject {
+
+	/**
+	 * Adds an observer to the set of Observers for this subject.
+	 * @param ob the Observer to be added.
+	 */
+	void addObserver(Observer ob);
+
+	/**
+	 * Removes an Observer from the set of Observers for this subject.
+	 * @param ob the Observer to be removed.
+	 */
+	void removeObserver(Observer ob);
+
+}
Index: /trunk/src_apollo/org/apollo/mvc/SubjectChangedEvent.java
===================================================================
--- /trunk/src_apollo/org/apollo/mvc/SubjectChangedEvent.java	(revision 315)
+++ /trunk/src_apollo/org/apollo/mvc/SubjectChangedEvent.java	(revision 315)
@@ -0,0 +1,33 @@
+package org.apollo.mvc;
+
+public class SubjectChangedEvent {
+	
+	private int id;
+	private Object state;
+	
+	public SubjectChangedEvent(int id) {
+		this(id, null);
+	}
+	
+	public SubjectChangedEvent(int id, Object state) {
+		this.id = id;
+		this.state = state; 
+	}
+
+	public int getID() {
+		return id;
+	}
+	
+	public Object getState() {
+		return state;
+	}
+	
+	public boolean isEmpty() {
+		return (this == EMPTY);
+	}
+	
+	/** An event with no data. */
+	public static final SubjectChangedEvent EMPTY = new SubjectChangedEvent(-1);
+
+
+}
Index: /trunk/src_apollo/org/apollo/mvc/SwingEventQueue.java
===================================================================
--- /trunk/src_apollo/org/apollo/mvc/SwingEventQueue.java	(revision 315)
+++ /trunk/src_apollo/org/apollo/mvc/SwingEventQueue.java	(revision 315)
@@ -0,0 +1,72 @@
+package org.apollo.mvc;
+
+import java.util.LinkedList;
+import java.util.Queue;
+
+import javax.swing.SwingUtilities;
+
+/**
+ * If your observer needs to reacts to events on the swing thread (i.e. interacting with swing
+ * objects somewhere in the event) and the observed subject raised events on other threads,
+ * this class thunnels all events into the swing thread and ensures that they are raised in
+ * the swing thread (thread safety!).
+ * 
+ * @author Brook Novak
+ *
+ */
+public class SwingEventQueue {
+	
+	private SwingThreadedObserver target;
+
+	// Using a FIFO queue implementation
+	private Queue<QueudEvent> eventQueue = new LinkedList<QueudEvent>();
+	
+	private EventProccessor eventProccessor = new EventProccessor();
+	
+	/**
+	 * COnstructor.
+	 * @param target The SwingThreadedObserver instance to thunnel events to on the swing thread.
+	 */
+	public SwingEventQueue(SwingThreadedObserver target) {
+		if (target == null) throw new NullPointerException("target");
+		this.target = target;
+	}
+
+	/**
+	 * Adds an event to the wing queue. The event will be re-fired on the swing thread
+	 * and raised on the target.
+	 * 
+	 * @param event A type of SubjectChangedEvent
+	 * @param source The subject which the event was fired from
+	 */
+	public synchronized void queueEvent(Subject source, SubjectChangedEvent event) {
+		synchronized (eventQueue) {
+			eventQueue.add(new QueudEvent(source, event));
+		}
+		SwingUtilities.invokeLater(eventProccessor);
+	}
+
+	class EventProccessor implements Runnable {
+
+		public void run() { // invoked on swing thread
+
+			synchronized (eventQueue) {
+				while (!eventQueue.isEmpty()) {
+					QueudEvent qe = eventQueue.remove();
+					target.modelChangedOnSwingThread(qe.source, qe.event);
+				}
+			}
+		}
+
+	}
+	
+	class QueudEvent {
+		public QueudEvent(Subject source, SubjectChangedEvent event) {
+			this.event = event;
+			this.source = source;
+		}
+		SubjectChangedEvent event;
+		Subject source;
+	}
+
+}
Index: /trunk/src_apollo/org/apollo/mvc/SwingThreadedObserver.java
===================================================================
--- /trunk/src_apollo/org/apollo/mvc/SwingThreadedObserver.java	(revision 315)
+++ /trunk/src_apollo/org/apollo/mvc/SwingThreadedObserver.java	(revision 315)
@@ -0,0 +1,12 @@
+package org.apollo.mvc;
+
+public interface SwingThreadedObserver {
+	
+	/**
+	 * Whenever the observed SwingEventQueue dequeu's an event on the swing thread.
+	 * 
+	 * @param event A type of SubjectChangedEvent
+	 * @param source The subject which the event was fired from
+	 */
+	void modelChangedOnSwingThread(Subject source, SubjectChangedEvent event);
+}
Index: /trunk/src_apollo/org/apollo/util/AudioMath.java
===================================================================
--- /trunk/src_apollo/org/apollo/util/AudioMath.java	(revision 315)
+++ /trunk/src_apollo/org/apollo/util/AudioMath.java	(revision 315)
@@ -0,0 +1,43 @@
+package org.apollo.util;
+
+import javax.sound.sampled.AudioFormat;
+
+/**
+ * Misc audio routines.
+ * 
+ * @author Brook Novak
+ *
+ */
+public class AudioMath {
+	private AudioMath() {}
+    
+	public static float volToDb(float volume) {
+		return (float) (Math.log(volume==0.0 ? 0.0001 : volume) / Math.log(10.0) * 20.0);
+	}
+
+	public static float dBToVol(float dB) {
+		return (float) Math.pow(10.0, dB / 20.0);
+	}
+	
+	/**
+	 * 
+	 * @param ms
+	 * @param format
+	 * @return How many frames are in the given ms time
+	 */
+	public static int millisecondsToFrames(long ms, AudioFormat format) {
+		double portion = ms;
+		portion /= 1000.0;
+		return (int)(format.getFrameRate() * portion);
+	}
+	
+	public static long framesToMilliseconds(long frames, AudioFormat format) {
+		double portion = frames;
+		portion /= (double)format.getFrameRate();
+		return (long)(portion * 1000.0);
+	}
+	
+	public static int framesToSeconds(long frames, AudioFormat format) {
+		return (int)(frames / format.getFrameRate());
+	}
+}
Index: /trunk/src_apollo/org/apollo/util/AudioSystemLog.java
===================================================================
--- /trunk/src_apollo/org/apollo/util/AudioSystemLog.java	(revision 315)
+++ /trunk/src_apollo/org/apollo/util/AudioSystemLog.java	(revision 315)
@@ -0,0 +1,33 @@
+package org.apollo.util;
+
+public class AudioSystemLog {
+	
+	public static void println(Object o) {
+		println(o.toString());
+	}
+	
+	public static void print(Object o) {
+		print(o.toString());
+	}
+	
+	public static void println(String message) {
+		print("Apollos: " + message + "\n");
+	}
+	
+	public static void print(String message) {
+		System.out.print(message);
+	}
+
+	public static void printError(String errorMessage) {
+		System.err.print("*Apollos-Error*: " + errorMessage);
+	}
+	
+	public static void printException(Exception e) {
+		e.printStackTrace();
+	}
+	
+	public static void printException(String errorMessage, Exception e) {
+		System.err.println("*Apollos-Error*: " + errorMessage + ". Exception stacktrace to follow:");
+		e.printStackTrace();
+	}
+}
Index: /trunk/src_apollo/org/apollo/util/ExpediteeFileTextSearch.java
===================================================================
--- /trunk/src_apollo/org/apollo/util/ExpediteeFileTextSearch.java	(revision 315)
+++ /trunk/src_apollo/org/apollo/util/ExpediteeFileTextSearch.java	(revision 315)
@@ -0,0 +1,148 @@
+package org.apollo.util;
+
+import java.io.BufferedReader;
+import java.io.FileNotFoundException;
+import java.io.FileReader;
+import java.io.IOException;
+import java.util.LinkedList;
+import java.util.List;
+
+/**
+ * Performs text item searches in expeditee files.
+ * 
+ * @author Brook Novak
+ *
+ */
+public class ExpediteeFileTextSearch {
+	
+	private ExpediteeFileTextSearch() {}
+
+	/**
+	 * Peforms a perfix search.
+	 * 
+	 * @param framePath
+	 * 		The path of the frame file to search. Must not be null.
+	 * 
+	 * @param textPrefixes
+	 * 		The prefixes of the text to match against. Must not be null or empty.
+	 * 
+	 * @return
+	 * 		A list of results. Never null.
+	 * 
+	 * @throws FileNotFoundException
+	 * 		If the frame path does not exist.
+	 * 
+	 * @throws IOException
+	 * 		If a read operation failed.
+	 * 
+	 * @throws NullPointerException
+	 * 		If framePath or textPrefixes is null.
+	 * 
+	 * @throws IllegalArgumentException
+	 * 		If textPrefixes is empty.
+	 */
+	public static List<TextItemSearchResult> prefixSearch(String framePath, String[] textPrefixes)
+		throws FileNotFoundException, IOException {
+		
+		if (framePath == null)
+			throw new NullPointerException("framePath");
+		else if (textPrefixes == null)
+			throw new NullPointerException("textPrefix");
+		else if (textPrefixes.length == 0)
+			throw new IllegalArgumentException("textPrefix");
+		
+		for (String prefix : textPrefixes)
+			if (prefix == null) throw new NullPointerException("textPrefix");
+		
+		List<TextItemSearchResult> results = new LinkedList<TextItemSearchResult>();
+
+		BufferedReader reader = null;
+		
+		try 
+		{
+			reader = new BufferedReader(new FileReader(framePath));
+			
+			String line = null;
+			
+			String strippedTextualLine = null; // not null if match
+			String framelinkLine = null;
+			LinkedList<String> currentDataLines = new LinkedList<String>();
+
+			while (((line = reader.readLine()) != null)) {
+				
+				if (line.length() <= 2) continue;
+				
+				if (line.startsWith("S")) {
+					
+					if (strippedTextualLine != null) {
+						results.add(createResult(strippedTextualLine, framelinkLine, currentDataLines));
+					}
+					
+					// Reset - next item to parse
+					currentDataLines.clear();
+					strippedTextualLine = null;
+					framelinkLine = null;
+				
+				} else if (line.startsWith("T ")) { // leave space - important
+					
+					strippedTextualLine = line.substring(2);
+					boolean matches = false; 
+					for (String prefix : textPrefixes) { // check for match
+						if (strippedTextualLine.startsWith(prefix)) {
+							matches = true;
+							break;
+						}
+					}
+					
+					if (!matches) strippedTextualLine = null;
+							
+				} else if (line.startsWith("D ")) { // leave space - important
+					
+					currentDataLines.add(line);
+				
+				} else if (line.startsWith("F ")) { // leave space - important
+					
+					framelinkLine = line;
+					
+				}
+				
+			}
+			
+			// Add last item
+			if (strippedTextualLine != null) {
+				results.add(createResult(strippedTextualLine, framelinkLine, currentDataLines));
+			}
+			
+		} finally {
+			if (reader != null) {
+				reader.close();
+			}
+		}
+		
+		return results;
+	}
+	
+	
+	private static TextItemSearchResult createResult(
+			String strippedTextualLine,
+			String framelinkLine,
+			LinkedList<String> dataLines) {
+		
+		assert(strippedTextualLine != null);
+		assert(dataLines != null);
+		
+		
+		TextItemSearchResult result = new TextItemSearchResult();
+		
+		result.text = strippedTextualLine;
+		result.explink = (framelinkLine != null) ? framelinkLine.substring(2) : null; // not stripped
+		result.data = new LinkedList<String>();
+		for (String dataLine : dataLines) {
+			result.data.add(dataLine.substring(2)); // data lines not stripped
+		}
+		
+		return result;
+		
+	}
+	
+}
Index: /trunk/src_apollo/org/apollo/util/NullableLong.java
===================================================================
--- /trunk/src_apollo/org/apollo/util/NullableLong.java	(revision 315)
+++ /trunk/src_apollo/org/apollo/util/NullableLong.java	(revision 315)
@@ -0,0 +1,59 @@
+package org.apollo.util;
+
+/**
+ * java.lang.Long does not allow:
+ * 	Long test = null;
+ * 
+ * This provides a simple nullable long value.
+ * 
+ * @author Brook Novak
+ *
+ */
+public class NullableLong implements Comparable<NullableLong> {
+	
+	private long longValue;
+	
+	public NullableLong(long value) {
+		this.longValue = value;
+	}
+	
+	private NullableLong(Long value) {
+		if (value == null) throw new NullPointerException();
+		this.longValue = value.longValue();
+	}
+	
+	public static NullableLong fromLong(Long value) {
+		if (value == null) return null;
+		return new NullableLong(value);
+	}
+
+	public int compareTo(NullableLong o) {
+		return new Long(longValue).compareTo(new Long(o.longValue));
+	}
+
+	@Override
+	public boolean equals(Object obj) {
+		if (obj == null) return false;
+		
+		if (obj instanceof NullableLong) {
+			return longValue == ((NullableLong)obj).longValue;
+		}
+		
+		return false;
+	}
+
+	@Override
+	public int hashCode() {
+		return new Long(longValue).intValue();
+	}
+
+	@Override
+	public String toString() {
+		return Long.toString(longValue);
+	}
+
+	public long getLongValue() {
+		return longValue;
+	}
+
+}
Index: /trunk/src_apollo/org/apollo/util/ODFrameHeirarchyFetcher.java
===================================================================
--- /trunk/src_apollo/org/apollo/util/ODFrameHeirarchyFetcher.java	(revision 315)
+++ /trunk/src_apollo/org/apollo/util/ODFrameHeirarchyFetcher.java	(revision 315)
@@ -0,0 +1,231 @@
+package org.apollo.util;
+
+import java.util.LinkedList;
+import java.util.Queue;
+
+import javax.swing.SwingUtilities;
+
+import org.apollo.audio.structure.OverdubbedFrame;
+import org.apollo.audio.structure.TrackGraphLoopException;
+import org.apollo.audio.structure.AudioStructureModel;
+import org.expeditee.gui.FrameIO;
+
+/**
+ * Fetches overdubbed frame heirarchies on a dedicated thread.
+ * 
+ * @author Brook Novak
+ *
+ */
+public class ODFrameHeirarchyFetcher {
+
+	private FetchWorker fetcher = null;
+	
+	/** Using a FIFO queue implementation */
+	private Queue<FetchRequest> fetchRequestQueue = new LinkedList<FetchRequest>(); // SHARED RESOURCE
+	
+	/** Singleton design pattern */
+	private static ODFrameHeirarchyFetcher instance = new ODFrameHeirarchyFetcher();
+	private ODFrameHeirarchyFetcher() { }
+	public static ODFrameHeirarchyFetcher getInstance() {
+		return instance;
+	}
+	
+	/**
+	 * Asynchronously (non-blocking) performs a fetch for a given frame name. Once the fetch is
+	 * complete the odRec's {@link ODFrameReceiver#receiveResult(OverdubbedFrame, TrackGraphLoopException)}
+	 * is invoked.
+	 * 
+	 * If there is a request for the given ODFrameReceiver that is already pending, the old request is replaced
+	 * if the rootFrameName differs, otherwise the old fetch remains and the new fetch is ignored.
+	 * 
+	 * @see {@link ODFrameReceiver)}
+	 * 
+	 * @param rootFrameName
+	 * 		The frame name to get the heirarchy for. Must be a valid framename
+	 * 
+	 * @param odRec
+	 * 		The ODFrameReceiver to receive the result.
+	 * 
+	 * @throws NullPointerException
+	 * 		If rootFrameName or odRec is null.
+	 * 
+	 * @throws IllegalArgumentException
+	 * 		If rootFrameName is not a valid framename.
+	 */
+	public void doFetch(String rootFrameName, ODFrameReceiver odRec) {
+		if (rootFrameName == null) throw new NullPointerException("rootFrameName");
+		if (!FrameIO.isValidFrameName(rootFrameName)) 
+			throw new IllegalArgumentException("rootFrameName is an invalid framename");
+		
+		if (odRec == null) throw new NullPointerException("odRec");
+
+		
+		FetchRequest newfr = new FetchRequest(rootFrameName, odRec);
+		
+		synchronized(fetchRequestQueue) {
+			
+			// See if a fetch for the receiver already exists
+			FetchRequest match = null;
+			for (FetchRequest existingfr : fetchRequestQueue) {
+				if (newfr.equals(existingfr)) {
+					match = existingfr;
+					break;
+				}
+			}
+			
+			// If a request already exists
+			if (match != null) {
+				// Check if the new fetch request is for a different odframe
+				if (!match.getRootFrameName().equals(rootFrameName)) {
+					fetchRequestQueue.remove(match); // replace redundant request
+					fetchRequestQueue.add(newfr);
+				}
+			} else {
+				fetchRequestQueue.add(newfr);
+			}
+			
+			// Ensure that the fetch thread is running
+			if (fetcher == null || !fetcher.isAlive()) {
+				fetcher = new FetchWorker();
+				fetcher.start();
+			}
+		}
+
+	}
+	
+	
+	private class FetchWorker extends Thread {
+
+		FetchWorker() {
+			super("FetchWorker");
+		}
+		
+		public void run() {
+			
+			while (true) {
+
+				FetchRequest request = null;
+				
+				synchronized(fetchRequestQueue) {
+	
+					if (!fetchRequestQueue.isEmpty())
+						request = fetchRequestQueue.remove();
+					
+					if (request == null) return;
+				}
+	
+				boolean hasUpdated = false;
+				do {
+					try {
+						AudioStructureModel.getInstance().waitOnUpdates();
+						hasUpdated = true;
+					} catch (InterruptedException e) {
+						e.printStackTrace();
+						continue;
+					}
+				} while (!hasUpdated);
+
+				while (true) {
+					try {
+						request.odFrameResult = AudioStructureModel.getInstance().fetchGraph(request.getRootFrameName());
+						break;
+					} catch (InterruptedException e) { // cancelled, retry
+						/* Consume */
+						
+					} catch (TrackGraphLoopException e) { // contains loop
+						request.loopExResult = e;
+						break;
+					}
+				} 
+				
+				// Run result notiofaction for this request on swing thread
+				SwingUtilities.invokeLater(request);
+				
+			}
+				
+				
+		
+		}
+	}
+	
+	/**
+	 * Immutable two-tuple for storing fetch requests.
+	 * 
+	 * @author Brook Novak
+	 */
+	private class FetchRequest implements Runnable {
+		
+		private String rootFrameName;
+		private ODFrameReceiver receiver;
+		
+		OverdubbedFrame odFrameResult = null;
+		TrackGraphLoopException loopExResult = null;
+		
+		/**
+		 * 
+		 * @param rootFrameName
+		 * 			Must be a valid framename.
+		 * 
+		 * @param receiver
+		 */
+		public FetchRequest(String rootFrameName, ODFrameReceiver receiver) {
+			this.rootFrameName = rootFrameName;
+			this.receiver = receiver;
+			assert(rootFrameName != null);
+			assert(FrameIO.isValidFrameName(rootFrameName)) ;
+			assert(receiver != null);
+		}
+
+		@Override
+		public boolean equals(Object obj) {
+			return receiver.equals(obj);
+		}
+
+		@Override
+		public int hashCode() {
+			return receiver.hashCode();
+		}
+
+		public ODFrameReceiver getReceiver() {
+			return receiver;
+		}
+
+		public String getRootFrameName() {
+			return rootFrameName;
+		}
+		
+		/**
+		 * To be invoked on swing thread
+		 */
+		public void run() {
+			receiver.receiveResult(odFrameResult, loopExResult);
+		}
+
+	}
+	
+	/**
+	 * A frame receive can request for fetches and have their 
+	 * {@link #receiveResult(OverdubbedFrame, TrackGraphLoopException)}
+	 * method invoked once a result has occured.
+	 * 
+	 * @author Brook Novak
+	 */
+	public interface ODFrameReceiver {
+		
+		/**
+		 * Invoked once a request has been proccessed. <b>This is invoked on the swing thread.</b>
+		 * 
+		 * @see ODFrameHeirarchyFetcher#doFetch(String, org.apollo.util.ODFrameHeirarchyFetcher.ODFrameReceiver)
+		 * 
+		 * @param odFrame
+		 * 		The overdubbed frame. Null if the frame does not exist - or if it does exist but there are no track widgets on it.
+		 * 		Or Null if there was a loop.
+		 * 		
+		 * @param loopEx
+		 * 		If there was a loop, then this will be set containing the loop info.
+		 */
+		public void receiveResult(OverdubbedFrame odFrame, TrackGraphLoopException loopEx);
+	
+	}
+
+}
Index: /trunk/src_apollo/org/apollo/util/PopupReaper.java
===================================================================
--- /trunk/src_apollo/org/apollo/util/PopupReaper.java	(revision 315)
+++ /trunk/src_apollo/org/apollo/util/PopupReaper.java	(revision 315)
@@ -0,0 +1,354 @@
+package org.apollo.util;
+
+import java.awt.Component;
+import java.awt.Point;
+import java.awt.Rectangle;
+import java.awt.event.MouseEvent;
+import java.lang.reflect.InvocationTargetException;
+import java.util.ArrayList;
+import java.util.Collection;
+import java.util.HashMap;
+import java.util.Map;
+
+import javax.swing.SwingUtilities;
+
+import org.expeditee.gui.MouseEventRouter;
+import org.expeditee.gui.Popup;
+import org.expeditee.gui.PopupManager;
+import org.expeditee.gui.PopupManager.PopupAnimator;
+
+/**
+ * Yet another daemon .. whos job is to hide popups after their lifetimes have passed
+ * 
+ * @author Brook Novak
+ */
+
+public class PopupReaper {
+	
+	private Map<Popup, TemporaryPopup> tempPopups = new HashMap<Popup, TemporaryPopup>();// SHARED RESOURCE - the locker
+
+	private ReapDaemon reaper = null;
+	
+	private static PopupReaper instance = new PopupReaper();
+	private PopupReaper() {
+	}
+	public  static PopupReaper getInstance() {
+		return instance;
+	}
+
+	/**
+	 * Initializes a popups lifetime so that the popup will eventually hide at a given time period.
+	 * 
+	 * @param p
+	 * 		The popup to hide in <code>lifetime</code> ms. Must not be null.
+	 * 
+	 * @param hideAnimation
+	 * 		The animation to render when the popup hides when its lifetime has passed. Null for
+	 *      no animation.
+	 * 
+	 * @param lifetime
+	 * 		The new time for the given popup to live for.
+	 * 		Must be larger or equal to zero. In milliseconds.
+	 * 
+	 * @return
+	 * 		True if the given popups lifetime has been re-initialized. False if the popups
+	 *		lifetime was either never initialized or has already been hidden or is in the proccesses of hiding.
+	 * 
+	 * @throws NullPointerException
+	 * 		If p is null.
+	 * 
+	 * @throws IllegalArgumentException
+	 * 		If lifetime is negative.
+	 * 
+	 */
+	public void initPopupLifetime(Popup p, PopupAnimator hideAnimation, int lifetime) {
+		if (p == null) throw new NullPointerException("p");
+		if (lifetime < 0) throw new IllegalArgumentException("lifetime < 0");
+		
+		// Init daemon
+		if (reaper == null) {
+			reaper = new ReapDaemon();
+			reaper.start();
+		}
+
+		synchronized(tempPopups) { // locks _tempPopups and _tempPopupQueue by convention
+			
+			TemporaryPopup tp = tempPopups.get(p);
+			
+			if (tp != null) {
+				
+				tp.setNewLifetime(lifetime);
+				
+			} else {
+				tp = new TemporaryPopup(p, hideAnimation, lifetime);
+				tempPopups.put(p, tp);
+				
+				// Notify reaper of new popup to reap .. eventually. It will set its new wait time
+				reaper.interrupt();
+			}
+			
+
+			
+		}
+		
+	}
+	
+	/**
+	 * Resets a popups lifetime to a new value.
+	 * 
+	 * @param p
+	 * 		THe popup to hide in <code>lifetime</code> ms. Must not be null.
+	 * 
+	 * @param lifetime
+	 * 		The new time for the given popup to live for.
+	 * 		Must be larger or equal to zero. In milliseconds.
+	 * 
+	 * @return
+	 * 		True if the given popups lifetime has been re-initialized. False if the popups
+	 *		lifetime was either never initialized or has already been hidden or is in the proccesses of hiding.
+	 * 
+	 * @throws NullPointerException
+	 * 		If p is null.
+	 * 
+	 * @throws IllegalArgumentException
+	 * 		If lifetime is negative.
+	 * 
+	 */
+	public boolean revivePopup(Popup p, int lifetime) {
+		if (p == null) throw new NullPointerException("p");
+		if (lifetime < 0) throw new IllegalArgumentException("lifetime < 0");
+		
+		synchronized(tempPopups) { // locks _tempPopups and _tempPopupQueue by convention
+			
+			TemporaryPopup tp = tempPopups.get(p);
+			
+			if (tp != null) {
+				tp.setNewLifetime(lifetime);
+				return true;
+			} 
+			
+		}
+		
+		return false;
+	}
+
+	
+	private class ReapDaemon extends Thread {
+		
+		ReapDaemon() {
+			super("Popup Reaper");
+			super.setPriority(Thread.MIN_PRIORITY);
+			super.setDaemon(true);
+		}
+	
+		public void run() {
+			
+			while (true) {
+			
+				long waitTime = reap();
+				
+				try {
+					if (waitTime < 0) {
+						sleep(10000); // some arbitary wait
+					} else if (waitTime > 0) {
+						sleep(waitTime);
+					}
+				} catch (InterruptedException e) {
+				}
+
+			} // keep reaping forever
+			
+		}
+		
+		/**
+		 * 
+		 * @return
+		 * 		A Positive wait time, or a negative for nothing to to wait on.
+		 */
+		private long reap() {
+
+			Collection<TemporaryPopup> snapshot;
+			
+			// Grab a temp popup
+			synchronized(tempPopups) { 
+				snapshot = new ArrayList<TemporaryPopup>(tempPopups.values());
+			}
+			
+			long nextDelay = -1;
+			
+			for (TemporaryPopup tp : snapshot) {
+				
+				long del = tp.getDelay();
+				if (del <= 0) { // this popups lifetime is up .. maybe - if the mouse is not over it
+	
+					DoHide hider = new DoHide(tp);
+					
+					try {
+						SwingUtilities.invokeAndWait(hider);
+					} catch (InterruptedException e) { 
+						return 0; // retry straight away
+					} catch (InvocationTargetException e) {
+						e.printStackTrace();
+					}
+					
+					if (hider.didHide) {
+						
+						synchronized(tempPopups) { 
+							tempPopups.remove(tp.popup);
+						}
+						
+					} else { // did not hide because mouse cursor is over pop / invoker
+						tp.resetLifetime();
+						del = tp.getDelay();
+					}
+	
+				}
+				
+				if (nextDelay < 0 || (del >= 0 && del < nextDelay)) {
+					// Smallest delay time
+					nextDelay = del;
+				}
+	
+			}
+			
+			return nextDelay;
+			
+		}
+		
+	}
+
+	/**
+	 * Hides a temp popup on the swing thread.
+	 * {@link #didHide} is set to true on hide, false if did not hide because mouse cursor
+	 * was over the popup / popup invoker.
+	 * 
+	 * @author Brook Novak
+	 *
+	 */
+	private class DoHide implements Runnable {
+		
+		private TemporaryPopup tempPopup;
+		public boolean didHide = false;
+		
+		public DoHide(TemporaryPopup tempPopup) {
+			this.tempPopup = tempPopup;
+		}
+		
+		/**
+		 * 
+		 * @param c
+		 * 
+		 * @param screenPoint
+		 * 
+		 * @return
+		 * 		True if screenPoint is over c.
+		 */
+		private boolean isPointInComponant(Component c, Point screenPoint) {
+			assert(c != null);
+			assert(screenPoint != null);
+			
+			Point compPositoinOnScreen = new Point(0,0);
+			SwingUtilities.convertPointToScreen(compPositoinOnScreen, c);
+			Rectangle bounds = c.getBounds();
+			bounds.x = compPositoinOnScreen.x;
+			bounds.y = compPositoinOnScreen.y;
+			
+			return bounds.contains(screenPoint);
+		}
+	
+		public void run() {
+			assert(tempPopup != null);
+			
+			MouseEvent me = MouseEventRouter.getCurrentMouseEvent();
+			if (me != null) {
+				
+				if (isPointInComponant(tempPopup.popup, me.getLocationOnScreen()))
+					return;
+				
+				Component invoker = PopupManager.getInstance().getInvoker(tempPopup.popup);
+				if (invoker != null && isPointInComponant(invoker, me.getLocationOnScreen()))
+					return;
+				
+			}
+
+			if (tempPopup.hideAnim != null) {
+				PopupManager.getInstance().hidePopup(tempPopup.popup, tempPopup.hideAnim);
+			} else {
+				PopupManager.getInstance().hidePopup(tempPopup.popup);
+			}
+			
+			didHide = true;
+			
+		}
+	}
+	
+	/**
+	 * Represents a popup that will eventually hide by the reaper.
+	 * 
+	 * @author Brook Novak
+	 *
+	 */
+	private class TemporaryPopup implements Comparable {
+
+		private Popup popup;
+		private PopupAnimator hideAnim;
+		private long destructionTime;
+		private int lifetime;
+		
+		/**
+		 * Constructor.
+		 * 
+		 * @param popup
+		 * 			The encapsulated popup - must not be null.
+		 * 
+		 * @param anim
+		 * 			Null for no anim...
+		 * 
+		 * @param lifetime
+		 * 			The time from now to which the popup should hide. In MS
+		 * 
+		 *
+		 */
+		public TemporaryPopup(Popup popup, PopupAnimator anim, int lifetime) {
+			assert(popup != null);
+			assert(lifetime >= 0);
+			
+			this.popup = popup;
+			this.hideAnim = anim;
+			
+			setNewLifetime(lifetime);
+		}
+
+		public void setNewLifetime(int lifetime) {
+			assert(lifetime >= 0);
+			this.lifetime = lifetime;
+			this.destructionTime = System.currentTimeMillis() + lifetime;
+		}
+		
+		public void resetLifetime() {
+			setNewLifetime(lifetime);
+		}
+		
+		public long getDelay() {
+			return destructionTime - System.currentTimeMillis();
+		}
+		
+		/**
+		 * {@inheritDoc}
+		 */
+		public int compareTo(Object o) {
+			
+			long dt2 = ((TemporaryPopup)o).destructionTime;
+			
+			if (destructionTime < dt2)
+		         return -1;
+			
+		      if (destructionTime > dt2)
+		         return 1;
+		      
+			return 0;
+		}
+		
+
+	}
+}
Index: /trunk/src_apollo/org/apollo/util/ProbeDaemon.java
===================================================================
--- /trunk/src_apollo/org/apollo/util/ProbeDaemon.java	(revision 315)
+++ /trunk/src_apollo/org/apollo/util/ProbeDaemon.java	(revision 315)
@@ -0,0 +1,63 @@
+package org.apollo.util;
+
+import java.io.BufferedReader;
+import java.io.IOException;
+import java.io.InputStreamReader;
+
+import org.apollo.audio.util.FrameLayoutDaemon;
+import org.apollo.audio.util.Timeline;
+import org.expeditee.gui.DisplayIO;
+
+public class ProbeDaemon extends Thread {
+	public ProbeDaemon() {
+		super("ProbeDaemon");
+		super.setDaemon(true);
+	}
+	
+	public void run() {
+		
+		System.err.println("APOLLO PROBE DAEMON RUNNING - THIS IS NOT AN ERROR BUT DON'T FORGET TO REMOVE ME :)");
+		
+		BufferedReader rd = new BufferedReader(new InputStreamReader(System.in));
+		
+		
+		while (true) {
+			
+			try {
+				String line = rd.readLine();
+				if (line != null) {
+					procccessMessage(line);
+				}
+			} catch (IOException e) {
+				e.printStackTrace();
+			}
+			
+		}
+
+	}
+
+	private void procccessMessage(String message) {
+		
+		assert(message != null);
+		
+		String output = "";
+		
+		if (message.equalsIgnoreCase("tl") || message.equalsIgnoreCase("timeline")) {
+			String timeline = "none";
+			Timeline tl = FrameLayoutDaemon.getInstance().getLastComputedTimeline();
+			if (tl != null && DisplayIO.getCurrentFrame() == FrameLayoutDaemon.getInstance().getTimelineOwner()) {
+				timeline = tl.toString();
+			}
+
+
+			output = "Current timeline: " + timeline;
+
+			
+		} else {
+			output = "I don't understand \"" + message + "\"";
+		}
+		
+		System.out.println("ProbeDaemon: " + output);
+
+	}
+}
Index: /trunk/src_apollo/org/apollo/util/RegExpFileFilter.java
===================================================================
--- /trunk/src_apollo/org/apollo/util/RegExpFileFilter.java	(revision 315)
+++ /trunk/src_apollo/org/apollo/util/RegExpFileFilter.java	(revision 315)
@@ -0,0 +1,22 @@
+package org.apollo.util;
+
+import java.io.File;
+import java.io.FilenameFilter;
+import java.util.regex.Pattern;
+
+/**
+ * A FileFilter that filters via regular expressions.
+ * @author Bruce Eckel
+ */
+public class RegExpFileFilter implements FilenameFilter {
+	private Pattern pattern;
+
+	public RegExpFileFilter(String regex) {
+		pattern = Pattern.compile(regex);
+	}
+
+	public boolean accept(File dir, String name) {
+		// Strip path information, search for regex:
+		return pattern.matcher(new File(name).getName()).matches();
+	}
+}
Index: /trunk/src_apollo/org/apollo/util/StringEx.java
===================================================================
--- /trunk/src_apollo/org/apollo/util/StringEx.java	(revision 315)
+++ /trunk/src_apollo/org/apollo/util/StringEx.java	(revision 315)
@@ -0,0 +1,33 @@
+package org.apollo.util;
+
+public final class StringEx {
+	
+	private StringEx() {}
+	
+	/**
+	 * This is a string equals method that handles null pointers
+	 * 
+	 * Notes: Javas thread-pool system doesn't mean that a string cannot only
+	 * be created once in java ... one can explicity use the new operater for 
+	 * a string to be duplicated... thus using the "==" operater is to risky.
+	 * Especially since many methods such as substring uses the new operator!
+	 * 
+	 * @param str1
+	 * 
+	 * @param str2
+	 * 
+	 * @return
+	 */
+	public static boolean equals(String str1, String str2) {
+		
+		if (str1 == str2) return true; // they both null or same ref?
+		
+		else if (str1 == null) return false; // one is null
+		
+		else if (str2 == null) return false; // one is null
+		
+		return str1.equals(str2); // Evaluate..
+		
+	}
+
+}
Index: /trunk/src_apollo/org/apollo/util/TextItemSearchResult.java
===================================================================
--- /trunk/src_apollo/org/apollo/util/TextItemSearchResult.java	(revision 315)
+++ /trunk/src_apollo/org/apollo/util/TextItemSearchResult.java	(revision 315)
@@ -0,0 +1,39 @@
+package org.apollo.util;
+
+import java.util.List;
+/**
+ * Represents a single mutable search result.
+ * 
+ * @author Brook Novak
+ */
+public class TextItemSearchResult {
+	/** The textual content of the text item. */
+	public String text;
+	
+	/** The link of the item. Null if no link: note: this could be a number ... in expeditees format without F tag. */
+	public String explink;
+	
+	/** Any data that is with the text item. */
+	public List<String> data;
+	
+
+	public boolean containsData(String str) {
+		assert(str != null);
+		if (data != null) 
+			return data.contains(str);
+		return false;
+	}
+	
+	public boolean containsDataTrimmedIgnoreCase(String str) {
+		assert(str != null);
+		if (data != null) {
+			for (String d : data) {
+				if (d != null && d.trim().equalsIgnoreCase(str)) {
+					return true;
+				}
+			}
+		}
+			
+		return false;
+	}
+}
Index: /trunk/src_apollo/org/apollo/util/TrackModelHandler.java
===================================================================
--- /trunk/src_apollo/org/apollo/util/TrackModelHandler.java	(revision 315)
+++ /trunk/src_apollo/org/apollo/util/TrackModelHandler.java	(revision 315)
@@ -0,0 +1,24 @@
+package org.apollo.util;
+
+import org.apollo.audio.SampledTrackModel;
+
+/**
+ * A work around for sharing SampledTrackModel references - avoids loading multiple copies of the same audio
+ * file and thus can enforce single playback of one audio file at a time.
+ * 
+ * @author Brook Novak
+ *
+ */
+public interface TrackModelHandler {
+	/**
+	 * The contract is that any reference that is / will be available through this instance must be
+	 * returned to avoid double loading of the same tracks.
+	 * 
+	 * This can be called from any thread...
+	 * 
+	 * @param localFileName
+	 * 
+	 * @return
+	 */
+	public SampledTrackModel getSharedSampledTrackModel(String localFileName);
+}
Index: /trunk/src_apollo/org/apollo/util/TrackModelLoadManager.java
===================================================================
--- /trunk/src_apollo/org/apollo/util/TrackModelLoadManager.java	(revision 315)
+++ /trunk/src_apollo/org/apollo/util/TrackModelLoadManager.java	(revision 315)
@@ -0,0 +1,201 @@
+package org.apollo.util;
+
+import java.io.File;
+import java.io.IOException;
+import java.lang.reflect.InvocationTargetException;
+import java.util.HashSet;
+import java.util.Set;
+
+import javax.sound.sampled.UnsupportedAudioFileException;
+import javax.swing.SwingUtilities;
+
+import org.apollo.audio.SampledTrackModel;
+import org.apollo.io.AudioIO;
+import org.apollo.io.LoadedAudioData;
+import org.apollo.mvc.Observer;
+import org.expeditee.items.widgets.HeavyDutyInteractiveWidget;
+import org.expeditee.items.widgets.WidgetCacheManager;
+
+/**
+ * Purpose: to avoid race conditions when loading track models from many threads.
+ * 
+ * All sharing of track models is done here.
+ * 
+ * Looked at disposed design patterns... but they become way to deep!
+ * 
+ * @author Brook Novak
+ *
+ */
+public class TrackModelLoadManager {
+
+	private Set<TrackModelHandler> trackModelHandlers = new HashSet<TrackModelHandler>();
+	
+	private static TrackModelLoadManager instance = new TrackModelLoadManager();
+	
+	public static TrackModelLoadManager getInstance() {
+		return instance;
+	}
+	
+	private TrackModelLoadManager() {
+	}
+	
+	
+	/**
+	 * Don't go adding everything possible... only add a handler if the handlers lifetime is
+	 * for the whole application, or is "disposable".
+	 * 
+	 * @param handler
+	 */
+	public void addTrackModelHandler(TrackModelHandler handler) { 
+		assert(handler != null);
+		synchronized(trackModelHandlers) {
+			trackModelHandlers.add(handler);
+		}
+	}
+	
+	/**
+	 * Always remove to get rid of reference and keep memory finding routines fast
+	 * 
+	 * @param handler
+	 * 
+	 */
+	public void removeTrackModelHandler(TrackModelHandler handler) { 
+		assert(handler != null);
+		synchronized(trackModelHandlers) {
+			trackModelHandlers.remove(handler);
+		}
+	}
+	
+	/**
+	 * Loads a SampledTrackModel. First it attempts to find is in memory, if fails then it
+	 * will resort to loading from file.
+	 * 
+	 * Note that if loaded from file and required conversions, the SampledTrackModel will
+	 * be marked as being modified (requiring saving).
+	 * 
+	 * @param filepath
+	 * 		The filepath of the audio file to load. The SampledTrackModel will set its filename as
+	 * 		this path value.
+	 * 
+	 * @param checkExpediteeMemory
+	 * 		If set to true, then it will search for SampledTrackModels in expeditee cache
+	 * 		on the swing thread. Therefore MUST <b>NOT</b> BE ON THE SWINGTHREAD IF THIS IS SET TO TRUE.
+	 * 
+	 * @param observer
+	 * 		If given (can be null), then the observer will be notified with 
+	 * 		LOAD_STATUS_REPORT AudioSubjectChangedEvent events (on the calling thread).
+	 * 		The state will be a float with the current percent (between 0.0 and 1.0 inclusive).
+	 * 		The subject source will be this instance.
+	 * 
+	 * @param localFilename
+	 * 		The local filename to associate the loaded track with.
+	 * 		Must not be null.
+	 * 
+	 * @return
+	 * 			Null iff cancelled.
+	 * 
+	 * @throws IOException
+	 * @throws UnsupportedAudioFileException
+	 */
+	public synchronized SampledTrackModel load(
+			String filepath,
+			String localfilename,
+			Observer observer,
+			boolean checkExpediteeMemory) throws IOException, UnsupportedAudioFileException {
+		
+		assert(localfilename != null);
+
+		// Check to see if already in memory
+		synchronized(trackModelHandlers) {
+			for (TrackModelHandler handler : trackModelHandlers) {
+				SampledTrackModel tm = handler.getSharedSampledTrackModel(localfilename);
+				if (tm != null) {
+					return tm;
+				}
+			}
+		}
+		
+		if (checkExpediteeMemory) {
+			// MUST NOT BE ON SWING THREAD!
+			// THIS IS IMPORTANT FOR WHEN FRAMES HAVE BEEN CHECK AND THEY WHERE NOT LOADED AT
+			// THE TIME AND THUS CACHED WIDGETS NOT IN MEMORY, BUT LATER IS CACHED ...
+			
+			CachedTrackModelLocator locator = new CachedTrackModelLocator(localfilename);
+			
+			try {
+				SwingUtilities.invokeAndWait(locator);
+			} catch (InterruptedException e) {
+				e.printStackTrace();
+			} catch (InvocationTargetException e) {
+				e.printStackTrace();
+			}
+			
+			if (locator.located != null) {
+				return locator.located;
+			}
+			
+		}
+
+
+		// If cannot find in memory, then attempt to load from file.
+		LoadedAudioData loadedAudio = AudioIO.loadAudioFile(new File(filepath), observer);
+		
+		// Could have been cancelled
+		if (loadedAudio == null) return null;
+		
+		// Create the track
+		SampledTrackModel track = new SampledTrackModel(
+				loadedAudio.getAudioBytes(), 
+				loadedAudio.getAudioFormat(),
+				localfilename);
+
+		track.setFilepath(filepath);
+		
+		// Set as modified if the audio bytes had to be converted
+		if (loadedAudio.wasConverted()) {
+			track.setAudioModifiedFlag(true);
+		}
+		
+		return track;
+		
+	}
+	
+	
+
+	/**
+	 * Look in expeditee memory - this should be quick - will hold up the user
+	 */
+	private class CachedTrackModelLocator implements Runnable
+	{
+		private String localfileName;
+		private SampledTrackModel located = null;
+		
+		CachedTrackModelLocator(String localfileName) {
+			assert(localfileName != null);
+			this.localfileName = localfileName;
+		}
+		
+		public void run() {
+		
+			// Exploit knowledge that all SampledTrack are Cached HDW
+			for (HeavyDutyInteractiveWidget hdw : WidgetCacheManager.getTransientWidgets()) {
+				if (hdw instanceof TrackModelHandler) {
+
+					SampledTrackModel strack = ((TrackModelHandler)hdw).getSharedSampledTrackModel(localfileName);
+					
+					if (strack != null) {
+						assert(strack.getLocalFilename().equals(localfileName));
+						
+						// found a match that is cahced
+						located = strack;
+						return;
+					}
+				}
+			}
+			
+		}
+		
+	}
+	
+
+}
Index: /trunk/src_apollo/org/apollo/util/TrackNameCreator.java
===================================================================
--- /trunk/src_apollo/org/apollo/util/TrackNameCreator.java	(revision 315)
+++ /trunk/src_apollo/org/apollo/util/TrackNameCreator.java	(revision 315)
@@ -0,0 +1,74 @@
+package org.apollo.util;
+
+import java.util.HashMap;
+
+/**
+ * Creates default names for tracks.
+ * 
+ * @author Brook Novak
+ *
+ */
+public class TrackNameCreator {
+	
+	private static HashMap<String, Integer> nameCounters = new HashMap<String, Integer>();
+
+	private TrackNameCreator() {}
+	
+	
+	private static final String DEFAULT_BASENAME = "untitled";
+	
+	// TODO: LOADING AND SAVING OF COUNTER MAP
+	
+	/**
+	 * 
+	 * @param nameBase
+	 * 		If null, the defulat name
+	 * @return
+	 */
+	public static String getDefaultName() {
+		return getNameCopy(null);
+	}
+	
+	/**
+	 * Copies a name - increments counter value at the end of the name.
+	 * 
+	 * @param name
+	 * 		If null then a default name copy is supplied. The name can already
+	 *      include the counter - this will be extracted if so.
+	 * 
+	 * @return
+	 * 		A copy of the given name. Never null.
+	 */
+	public static String getNameCopy(String name) {
+		
+		if (name == null) name = DEFAULT_BASENAME;
+		
+		int i;
+		for (i = name.length() - 1; i > 0; i--) {
+			if (!Character.isDigit(name.charAt(i))) {
+				break;
+			}		
+		}
+		
+		// Extract case sensitive basename
+		if (i <= 0) {
+			name = DEFAULT_BASENAME;
+		} else {
+			name = name.substring(0, i + 1);
+		}
+
+		Integer count = nameCounters.get(name.toLowerCase());
+		
+		if (count == null) {
+			count = new Integer(1);
+		} else {
+			count = new Integer(count.intValue() + 1);
+		}
+		
+		// Add or replace current count for this namebase
+		nameCounters.put(name.toLowerCase(), count);
+		
+		return name + count.toString();
+
+	}
+}
Index: /trunk/src_apollo/org/apollo/widgets/AudioConfiguration.java
===================================================================
--- /trunk/src_apollo/org/apollo/widgets/AudioConfiguration.java	(revision 315)
+++ /trunk/src_apollo/org/apollo/widgets/AudioConfiguration.java	(revision 315)
@@ -0,0 +1,303 @@
+package org.apollo.widgets;
+
+import java.awt.Component;
+import java.awt.Dimension;
+import java.awt.Font;
+import java.awt.GridBagConstraints;
+import java.awt.GridBagLayout;
+import java.awt.Insets;
+import java.awt.event.ActionEvent;
+import java.awt.event.ActionListener;
+import java.util.List;
+
+import javax.sound.sampled.AudioSystem;
+import javax.sound.sampled.Mixer;
+import javax.swing.Box;
+import javax.swing.BoxLayout;
+import javax.swing.JCheckBox;
+import javax.swing.JComboBox;
+import javax.swing.JLabel;
+import javax.swing.JPanel;
+import javax.swing.JSlider;
+import javax.swing.event.ChangeEvent;
+import javax.swing.event.ChangeListener;
+
+import org.apollo.audio.ApolloPlaybackMixer;
+import org.apollo.audio.ApolloSubjectChangedEvent;
+import org.apollo.audio.SampledAudioManager;
+import org.apollo.mvc.Observer;
+import org.apollo.mvc.Subject;
+import org.apollo.mvc.SubjectChangedEvent;
+import org.expeditee.items.ItemParentStateChangedEvent;
+import org.expeditee.items.Text;
+import org.expeditee.items.widgets.InteractiveWidget;
+
+public class AudioConfiguration extends InteractiveWidget implements Observer {
+	
+	private static MixerItem[] inputMixerItems = null;
+
+	private JComboBox inputMixureCombo;
+	private JSlider masterVolumeSlider;
+	private JCheckBox masterMuteCheckBox;
+	private JLabel inputMixerDescription;
+	
+	public AudioConfiguration(Text source, String[] args) {
+		super(source, new JPanel(), 260, -1, 150, 150);
+		
+		Font titleFont = new Font("Arial", Font.BOLD, 12);
+		Font descFont = new Font("Arial", Font.PLAIN, 12);
+
+		inputMixureCombo = new JComboBox(getInputMixerItems());
+		
+		inputMixureCombo.addActionListener( new ActionListener() {
+			public void actionPerformed(ActionEvent e) { 
+				if (inputMixerItems.length > 0) {
+					SampledAudioManager.getInstance().setCurrentInputMixure(
+							((MixerItem)inputMixureCombo.getSelectedItem())._inf);
+				}
+			}
+		});
+		
+		masterVolumeSlider = new JSlider(); 
+		masterVolumeSlider.setMinimum(0);
+		masterVolumeSlider.setMaximum(100);
+		masterVolumeSlider.setMaximumSize(new Dimension(300, 20));
+
+		masterVolumeSlider.addChangeListener(new ChangeListener() {
+			public void stateChanged(ChangeEvent e) {
+				float f = (float)masterVolumeSlider.getValue();
+				f /= 100.0f;
+				ApolloPlaybackMixer.getInstance().setMasterVolume(f);
+			}
+		});
+		
+		masterMuteCheckBox = new JCheckBox(); 
+		masterMuteCheckBox.setText("Master Mute");
+		masterMuteCheckBox.addChangeListener(new ChangeListener() {
+			public void stateChanged(ChangeEvent e) {
+				ApolloPlaybackMixer.getInstance().setMasterMute(masterMuteCheckBox.isSelected());
+			}
+		});
+		
+		
+		inputMixerDescription = new JLabel();
+		inputMixerDescription.setFont(descFont);
+		
+		JLabel masterVolumeLevelLabel = new JLabel("Master volume:");
+		masterVolumeLevelLabel.setFont(titleFont);
+		
+		JLabel deviceLabel = new JLabel("Input device:");
+		deviceLabel.setFont(titleFont);
+		// Assemble
+		JPanel mixerPane = new JPanel(new GridBagLayout());
+		
+		
+
+		GridBagConstraints c = new GridBagConstraints();
+		c.gridx = 0;
+		c.gridy = 0;
+		c.fill = GridBagConstraints.BOTH;
+		c.insets = new Insets(0,10,0,0);
+		mixerPane.add(deviceLabel, c);
+		
+		c = new GridBagConstraints();
+		c.gridx = 1;
+		c.gridy = 0;
+		c.weightx = 1.0f;
+		c.fill = GridBagConstraints.BOTH;
+		c.insets = new Insets(0,10,0,10);
+		mixerPane.add(inputMixureCombo, c);
+		
+		mixerPane.setAlignmentX(Component.CENTER_ALIGNMENT);
+		masterVolumeLevelLabel.setAlignmentX(Component.CENTER_ALIGNMENT);
+		inputMixerDescription.setAlignmentX(Component.CENTER_ALIGNMENT);
+		masterVolumeSlider.setAlignmentX(Component.CENTER_ALIGNMENT);
+		masterMuteCheckBox.setAlignmentX(Component.CENTER_ALIGNMENT);
+		
+		_swingComponent.add(Box.createRigidArea(new Dimension(1, 12)));
+		_swingComponent.setLayout(new BoxLayout(_swingComponent, BoxLayout.Y_AXIS));
+		_swingComponent.add(mixerPane);
+		_swingComponent.add(Box.createRigidArea(new Dimension(1, 4)));
+		_swingComponent.add(inputMixerDescription);
+		_swingComponent.add(Box.createRigidArea(new Dimension(1, 12)));
+		_swingComponent.add(masterVolumeLevelLabel);
+		_swingComponent.add(masterVolumeSlider);
+		_swingComponent.add(Box.createRigidArea(new Dimension(1, 12)));
+		_swingComponent.add(masterMuteCheckBox);
+		_swingComponent.add(Box.createRigidArea(new Dimension(1, 12)));
+		
+	
+		
+		// Set state to match SampledAudioManager
+		updateSelectedMixers();
+		updateMasterVolumeControl();
+		updateMasterMuteControl();
+		
+		_swingComponent.doLayout();
+		
+	}
+	
+	@Override
+	protected void onParentStateChanged(int eventType) {
+		super.onParentStateChanged(eventType);
+		
+		switch (eventType) {
+
+		case ItemParentStateChangedEvent.EVENT_TYPE_REMOVED:
+		case ItemParentStateChangedEvent.EVENT_TYPE_REMOVED_VIA_OVERLAY:
+		case ItemParentStateChangedEvent.EVENT_TYPE_HIDDEN:
+			ApolloPlaybackMixer.getInstance().removeObserver(this);
+			SampledAudioManager.getInstance().removeObserver(this);
+			break;
+
+		case ItemParentStateChangedEvent.EVENT_TYPE_ADDED:
+		case ItemParentStateChangedEvent.EVENT_TYPE_ADDED_VIA_OVERLAY:
+		case ItemParentStateChangedEvent.EVENT_TYPE_SHOWN:
+		case ItemParentStateChangedEvent.EVENT_TYPE_SHOWN_VIA_OVERLAY:
+			ApolloPlaybackMixer.getInstance().addObserver(this);
+			SampledAudioManager.getInstance().addObserver(this);
+			break;
+
+		}
+	}
+	
+	
+	/**
+	 * Syncs the I/O mixer combos to the current I/O mixers set in the SampledAudioManager
+	 *
+	 */
+	private void updateSelectedMixers() {
+		
+		// Update selected input mixer
+		Mixer.Info inf = SampledAudioManager.getInstance().getCurrentInputMixureInfo();
+		
+		for (MixerItem mi : inputMixerItems) {
+			if (mi._inf.equals(inf)) {
+				if (inputMixureCombo.getSelectedItem() != mi)
+					inputMixureCombo.setSelectedItem(mi);
+			}
+		}
+		
+		/*StringBuilder desc = new StringBuilder(inf.getDescription().length());
+		int i = 0;
+		final int CUTTOFF_LENGTH = 10;
+		while (i < inf.getDescription().length()) {
+			String part = null;
+			
+			if ((i + CUTTOFF_LENGTH) > inf.getDescription().length()) {
+				part = inf.getDescription().substring(i);
+			} else {
+				part = inf.getDescription().substring(i, i + CUTTOFF_LENGTH);
+			}
+			desc.append(part);
+			desc.append('\n');
+			i += CUTTOFF_LENGTH;
+		}*/
+		
+		inputMixerDescription.setText(inf.getDescription());
+		
+	}
+	
+	/**
+	 * Synchs volumeSlider with master volume state.
+	 */
+	private void updateMasterVolumeControl() {
+		
+		int mv = (int)(ApolloPlaybackMixer.getInstance().getMasterVolume() * 100.0f);
+		
+		if (masterVolumeSlider.getValue() != mv) {
+			masterVolumeSlider.setValue(mv);
+		}
+		
+	}
+
+	/**
+	 * Synchs mute checkbox with master mute state.
+	 */
+	private void updateMasterMuteControl() {
+		
+		if (this.masterMuteCheckBox.isSelected() != ApolloPlaybackMixer.getInstance().isMasterMuteOn()) {
+			masterMuteCheckBox.setSelected(!masterMuteCheckBox.isSelected());
+		}
+		
+		if (masterMuteCheckBox.isSelected()) {
+			masterMuteCheckBox.setText("Master Mute (on)");
+		} else {
+			masterMuteCheckBox.setText("Master Mute (off)");
+		}
+		
+	}
+
+	
+	@Override
+	protected String[] getArgs() {
+		return null;
+	}
+	
+	/**
+	 * Obtain currently available mixers to choose from
+	 */
+	private MixerItem[] getInputMixerItems() {
+		
+		List<Mixer.Info> mixers;
+		
+		if (inputMixerItems == null) {
+			
+			mixers = SampledAudioManager.getInstance().getSupportedInputMixures();
+			inputMixerItems = new MixerItem[mixers.size()];
+			for (int i = 0; i < inputMixerItems.length; i++) {
+				inputMixerItems[i] = new MixerItem(mixers.get(i));
+			}
+			
+		}
+		return inputMixerItems;
+	}
+	
+	public Subject getObservedSubject() {
+		return SampledAudioManager.getInstance();
+	}
+
+	public void modelChanged(Subject source, SubjectChangedEvent event) {
+
+		switch (event.getID()) {
+		case ApolloSubjectChangedEvent.VOLUME:
+			updateMasterVolumeControl();
+			break;
+		case ApolloSubjectChangedEvent.MUTE:
+			updateMasterMuteControl();
+			break;
+		case ApolloSubjectChangedEvent.INPUT_MIXER:
+			updateSelectedMixers();
+			break;
+		}
+
+	}
+
+	public void setObservedSubject(Subject parent) {
+	}
+
+	
+	/**
+	 * Used by combobox for selecting a mixer
+	 * @author Brook Novak
+	 */
+	class MixerItem {
+		
+		Mixer.Info _inf;
+		int targetlineCount;
+		
+		MixerItem(Mixer.Info inf) {
+			if (inf == null) throw new NullPointerException("inf");
+			_inf = inf;
+			targetlineCount = AudioSystem.getMixer(_inf).getTargetLineInfo().length;
+		}
+		
+		@Override
+		public String toString() {
+			return _inf.getName();
+			// return _inf.getVendor() + " " +_inf.getName() + " " +_inf.getVersion();
+			//return _inf.getDescription() + " [" + targetlineCount + " target lines]";
+		}
+
+	}
+}
Index: /trunk/src_apollo/org/apollo/widgets/FramePlayer.java
===================================================================
--- /trunk/src_apollo/org/apollo/widgets/FramePlayer.java	(revision 315)
+++ /trunk/src_apollo/org/apollo/widgets/FramePlayer.java	(revision 315)
@@ -0,0 +1,669 @@
+package org.apollo.widgets;
+
+import java.awt.Color;
+import java.awt.Dimension;
+import java.awt.GridBagConstraints;
+import java.awt.GridBagLayout;
+import java.awt.Insets;
+import java.awt.event.ActionEvent;
+import java.awt.event.ActionListener;
+import java.util.List;
+
+import javax.swing.Icon;
+import javax.swing.JButton;
+import javax.swing.JPanel;
+import javax.swing.JSlider;
+import javax.swing.JToggleButton;
+import javax.swing.event.ChangeEvent;
+import javax.swing.event.ChangeListener;
+
+import org.apollo.audio.ApolloSubjectChangedEvent;
+import org.apollo.audio.SampledAudioManager;
+import org.apollo.audio.util.FrameLayoutDaemon;
+import org.apollo.audio.util.MultiTrackPlaybackController;
+import org.apollo.audio.util.SoundDesk;
+import org.apollo.audio.util.TrackMixSubject;
+import org.apollo.audio.util.MultiTrackPlaybackController.MultitrackLoadListener;
+import org.apollo.io.IconRepository;
+import org.apollo.mvc.Observer;
+import org.apollo.mvc.Subject;
+import org.apollo.mvc.SubjectChangedEvent;
+import org.apollo.util.AudioMath;
+import org.apollo.util.AudioSystemLog;
+import org.expeditee.gui.DisplayIO;
+import org.expeditee.gui.Frame;
+import org.expeditee.gui.FrameGraphics;
+import org.expeditee.items.ItemParentStateChangedEvent;
+import org.expeditee.items.Text;
+import org.expeditee.items.widgets.InteractiveWidget;
+
+/**
+ * This a last minute hack ... should be revised
+ * 
+ * @author Brook Novak
+ *
+ */
+public class FramePlayer extends InteractiveWidget 
+	implements Observer, MultitrackLoadListener, ActionListener {
+
+	private static final int BUTTON_SIZE = 40;
+	
+	private static TrackMixSubject masterMix = null;
+	private static String currentPlayingFrame = null;
+	
+	private JButton playPauseButton;
+	private JButton stopButton;
+	private JButton rewindButton;
+	private JToggleButton muteButton;
+	private JToggleButton soloButton;
+	private JSlider volumeSlider;
+
+	private int state = READY;
+	private String abortMessage = null;
+	
+	private boolean isUpdatingGUI = false;
+	
+	public static String FRAME_PLAYERMASTER_CHANNEL_ID = "#$frameplayer#master$";
+	
+	private static final Color LOADING_BORDER_COLOR = new Color(22, 205, 5);
+	//private static final Color FAILED_MESSAGE_COLOR = Color.RED;
+	//private static final Font MESSAGE_FONT = TrackWidgetCommons.FREESPACE_TRACKNAME_FONT;
+
+	
+	/** States - mutex in this widget but not really .. for example since can be loading tracks and the graph at the same time... */
+	private  static final int READY = 1; // Waiting for user interaction
+	private  static final int PLAYBACK_LOADING = 2; // Loading tracks from file/cache to play/resume
+	private  static final int PLAYING = 3; // Playing audio.
+	
+	
+	static {
+		masterMix = SoundDesk.getInstance().getOrCreateMix(FRAME_PLAYERMASTER_CHANNEL_ID);
+	}
+	
+	/**
+	 * Constructor called by Expeditee. 
+	 * 
+	 * @param source
+	 * @param args
+	 */
+	public FramePlayer(Text source, String[] args) {
+		super(source, new JPanel(new GridBagLayout()), 
+				BUTTON_SIZE * 8, BUTTON_SIZE * 8, 
+				BUTTON_SIZE, BUTTON_SIZE);
+
+		playPauseButton = new JButton();
+		playPauseButton.addActionListener(this);
+		playPauseButton.setIcon(IconRepository.getIcon("play.png"));
+		playPauseButton.setPreferredSize(new Dimension(BUTTON_SIZE, BUTTON_SIZE));
+		playPauseButton.setToolTipText("Play selection / Pause");
+		
+		stopButton = new JButton();
+		stopButton.setEnabled(false);
+		stopButton.addActionListener(this);
+		stopButton.setIcon(IconRepository.getIcon("stop.png"));
+		stopButton.setPreferredSize(new Dimension(BUTTON_SIZE, BUTTON_SIZE));
+		stopButton.setToolTipText("Stop playback");
+		
+		rewindButton = new JButton();
+		rewindButton.addActionListener(this);
+		rewindButton.setIcon(IconRepository.getIcon("rewind.png"));
+		rewindButton.setPreferredSize(new Dimension(BUTTON_SIZE, BUTTON_SIZE));
+		rewindButton.setToolTipText("Rewind to start");
+		
+		// Icon changes
+		muteButton = new JToggleButton();
+		muteButton.setSelectedIcon(IconRepository.getIcon("volmute.png"));
+		muteButton.setPreferredSize(new Dimension(BUTTON_SIZE, BUTTON_SIZE));
+		muteButton.setToolTipText("Toggle mute");
+		muteButton.addChangeListener(new ChangeListener() {
+			public void stateChanged(ChangeEvent e) {
+				if (!FramePlayer.this.isUpdatingGUI) {
+					muteChanged();
+				}
+			}
+		});
+		
+		soloButton = new JToggleButton();
+		soloButton.setIcon(IconRepository.getIcon("solo.png"));
+		soloButton.setSelectedIcon(IconRepository.getIcon("soloon.png"));
+		soloButton.setPreferredSize(new Dimension(BUTTON_SIZE, BUTTON_SIZE));
+		soloButton.setToolTipText("Toggle solo");
+		soloButton.addChangeListener(new ChangeListener() {
+			public void stateChanged(ChangeEvent e) {
+				if (!FramePlayer.this.isUpdatingGUI) {
+					soloChanged();
+				}
+			}
+		});
+		
+		
+		final int VOLUME_SPACING = 6;
+		volumeSlider = new JSlider(JSlider.HORIZONTAL);
+		volumeSlider.setMinimum(0);
+		volumeSlider.setMaximum(100);
+		volumeSlider.addChangeListener(new ChangeListener() {
+			public void stateChanged(ChangeEvent e) {
+				if (!FramePlayer.this.isUpdatingGUI) {
+					volumeChanged();
+				}
+				// Update the icons
+				updateButtonGUI();
+			}
+		});
+		
+		volumeSlider.setPreferredSize(new Dimension((3 * BUTTON_SIZE) - (2 * VOLUME_SPACING), BUTTON_SIZE));
+
+		GridBagConstraints c = new GridBagConstraints();
+		c.gridx = 0;
+		c.gridy = 0;
+		c.fill = GridBagConstraints.BOTH;
+		_swingComponent.add(playPauseButton, c);
+		
+		c = new GridBagConstraints();
+		c.gridx = 1;
+		c.gridy = 0;
+		c.fill = GridBagConstraints.BOTH;
+		_swingComponent.add(stopButton, c);
+		
+		c = new GridBagConstraints();
+		c.gridx = 2;
+		c.gridy = 0;
+		c.fill = GridBagConstraints.BOTH;
+		_swingComponent.add(rewindButton, c);
+		
+		c = new GridBagConstraints();
+		c.gridx = 3;
+		c.gridy = 0;
+		c.fill = GridBagConstraints.BOTH;
+		_swingComponent.add(soloButton, c);
+		
+		c = new GridBagConstraints();
+		c.gridx = 4;
+		c.gridy = 0;
+		c.fill = GridBagConstraints.BOTH;
+		_swingComponent.add(muteButton, c);
+		
+		c = new GridBagConstraints();
+		c.gridx = 5;
+		c.gridy = 0;
+		c.fill = GridBagConstraints.BOTH;
+		c.insets = new Insets(0,VOLUME_SPACING,0,VOLUME_SPACING);
+		_swingComponent.add(volumeSlider, c);
+		
+		updateButtonGUI();
+		updateBorderColor();
+
+	}
+	
+
+	/**
+	 * Sets the GUI and all the popups to reflect the current state.
+	 * Must be on the swing thread.
+	 * 
+	 * @param newState
+	 * 			The new state. The same state then it is ignored, unless
+	 * 			its LOADING_TRACK_GRAPH - in that case the graph is reloaded.
+	 */
+	private void setState(int newState) {
+		if (this.state == newState) return; // no need to process request.
+
+		switch(newState) {
+		case READY: 
+
+			rewindButton.setEnabled(true);
+			stopButton.setEnabled(false);
+			playPauseButton.setEnabled(true);
+			playPauseButton.setIcon(IconRepository.getIcon("play.png"));
+
+			setWidgetEdgeThickness(TrackWidgetCommons.STOPPED_TRACK_EDGE_THICKNESS);
+			
+			break;
+			
+		case PLAYBACK_LOADING: 
+			stopButton.setEnabled(false);
+			rewindButton.setEnabled(false);
+			playPauseButton.setEnabled(false);
+			break;
+			
+		case PLAYING: 
+			stopButton.setEnabled(true);
+			rewindButton.setEnabled(false);
+			playPauseButton.setEnabled(true);
+			playPauseButton.setIcon(IconRepository.getIcon("pause.png"));
+
+			setWidgetEdgeThickness(TrackWidgetCommons.PLAYING_TRACK_EDGE_THICKNESS);
+
+			break;
+
+		}
+
+		state = newState;
+		
+		updateBorderColor();
+		
+		invalidateSelf();
+		FrameGraphics.refresh(true);
+	}
+	
+	/**
+	 * {@inheritDoc}
+	 */
+	@Override
+	protected void onParentStateChanged(int eventType) {
+		super.onParentStateChanged(eventType);
+		
+
+		//Frame currentFrame = DisplayIO.getCurrentFrame();
+	//	String currentFrameName = (currentFrame != null) ? currentFrame.getName() : null;
+
+		switch (eventType) {
+
+		case ItemParentStateChangedEvent.EVENT_TYPE_ADDED:
+		case ItemParentStateChangedEvent.EVENT_TYPE_ADDED_VIA_OVERLAY: 
+		case ItemParentStateChangedEvent.EVENT_TYPE_SHOWN:
+		case ItemParentStateChangedEvent.EVENT_TYPE_SHOWN_VIA_OVERLAY:
+
+			// Setup observers
+			SoundDesk.getInstance().addObserver(this); // for solo 
+			MultiTrackPlaybackController.getInstance().addObserver(this); // the core!
+			masterMix.addObserver(this);
+		
+			// Evaluate the state of this add set the state accordingly
+			if (currentPlayingFrame != null
+					&& MultiTrackPlaybackController.getInstance().isLoading(
+							currentPlayingFrame, 
+							masterMix.getChannelID())) {
+				
+				// Ensure that am receiving notifiactions:
+				List<String> loaded = MultiTrackPlaybackController.getInstance().attachLoadListener(this);
+				assert(loaded != null);
+				
+				setState(PLAYBACK_LOADING);
+				
+			} else if (currentPlayingFrame != null 
+					&& MultiTrackPlaybackController.getInstance().isPlaying(
+							currentPlayingFrame, 
+							masterMix.getChannelID())) {
+				
+				setState(PLAYING);
+				
+			} else {
+				setState(READY);
+			}
+
+			break;
+			
+		case ItemParentStateChangedEvent.EVENT_TYPE_REMOVED:
+
+			// Cancel loading of audio
+			if (state == PLAYBACK_LOADING) {
+				assert(currentPlayingFrame != null);
+				
+				MultiTrackPlaybackController.getInstance().cancelLoad(
+						currentPlayingFrame, 
+						masterMix.getChannelID());
+			}
+
+		case ItemParentStateChangedEvent.EVENT_TYPE_REMOVED_VIA_OVERLAY: // TODO revise - and in sampled track widget
+		case ItemParentStateChangedEvent.EVENT_TYPE_HIDDEN:
+			
+			// Remove observers
+			SoundDesk.getInstance().removeObserver(this);
+			MultiTrackPlaybackController.getInstance().removeObserver(this);
+			masterMix.removeObserver(this);
+			
+			setState(READY);
+			break;
+
+		}
+	}
+
+
+
+	/**
+	 * {@inheritDoc}
+	 */
+	public void multiplaybackLoadStatusUpdate(int id, Object state) {
+
+		// NOTE: Could switch to an unloaded state
+		if (this.state != PLAYBACK_LOADING) return;
+		
+		switch(id) {
+		case MultitrackLoadListener.LOAD_CANCELLED:
+			setState(READY);
+			break;
+		case MultitrackLoadListener.LOAD_COMPLETE:
+			break;
+		case MultitrackLoadListener.LOAD_FAILED_BAD_GRAPH:
+			abortMessage = "Graph contains loops";
+			((Exception)state).printStackTrace();
+			break;
+		case MultitrackLoadListener.LOAD_FAILED_GENERIC:
+			abortMessage = "Unexpected error";
+			((Exception)state).printStackTrace();
+			break;
+		case MultitrackLoadListener.LOAD_FAILED_PLAYBACK:
+			abortMessage = "Graph contains loops";
+			break;
+		case MultitrackLoadListener.NOTHING_TO_PLAY:
+			abortMessage = "Nothing to play"; // could be due to user slecting empty space
+			break;
+		case MultitrackLoadListener.TRACK_LOAD_FAILED_IO:
+			// This is special... the loader does not abort... and it tries to load more.
+			((Exception)state).printStackTrace();
+			break;
+		case MultitrackLoadListener.TRACK_LOADED:
+			break;
+
+		}
+		
+		if (abortMessage != null) {
+			AudioSystemLog.println("Aborted playback - " + abortMessage);
+			setState(READY);
+		}
+		
+	}
+	
+	/**
+	 * {@inheritDoc}
+	 */
+	public Subject getObservedSubject() {
+		return null;
+	}
+	
+	/**
+	 * {@inheritDoc}
+	 */
+	public void modelChanged(Subject source, SubjectChangedEvent event) {
+
+		// Synch GUI with track state
+		switch (event.getID()) {
+			
+		case ApolloSubjectChangedEvent.PLAYBACK_STARTED:
+			
+			if (currentPlayingFrame != null &&
+				MultiTrackPlaybackController.getInstance().isCurrentPlaybackSubject(
+						currentPlayingFrame, masterMix.getChannelID())) {
+				setState(PLAYING);
+			} 
+			
+			break;
+			
+		case ApolloSubjectChangedEvent.PLAYBACK_STOPPED:
+			
+			if (state == PLAYING) {
+				//assert(currentPlayingFrame != null);
+
+				// Transition into new state
+				setState(READY);
+
+			}
+			break;
+			
+		case ApolloSubjectChangedEvent.MULTIPLAYBACK_LOADING: 
+			
+			// Adjust state accordingly
+			if (currentPlayingFrame != null &&
+					MultiTrackPlaybackController.getInstance().isCurrentPlaybackSubject(
+							currentPlayingFrame, masterMix.getChannelID()) && state != PLAYBACK_LOADING) {
+				MultiTrackPlaybackController.getInstance().attachLoadListener(this);
+				setState(PLAYBACK_LOADING);
+			}
+			break;
+			
+		case ApolloSubjectChangedEvent.VOLUME: // From obseved track mix
+			updateVolume();
+			break;
+			
+		case ApolloSubjectChangedEvent.MUTE: // From obseved track mix
+			updateMute();
+			updateBorderColor();
+			break;
+			
+		case ApolloSubjectChangedEvent.SOLO_PREFIX_CHANGED: // From mix desk
+			updateSolo();
+			updateBorderColor();
+			break;
+			
+			
+		}
+		
+	}
+	
+	/**
+	 * {@inheritDoc}
+	 */
+	public void setObservedSubject(Subject parent) {
+	}
+	
+	/**
+	 * {@inheritDoc}
+	 */
+	@Override
+	protected String[] getArgs() {
+		return null;
+	}
+
+	/**
+	 * {@inheritDoc}
+	 */
+	public void actionPerformed(ActionEvent e) {
+		
+		if (!(state == PLAYING || state == READY)) return; // safety
+
+		Frame currentFrame = DisplayIO.getCurrentFrame();
+		String currentFrameName = (currentFrame != null) ? currentFrame.getName() : null;
+		
+		if (e.getSource() == playPauseButton) {
+			
+			if (state == READY && currentFrameName != null) { // comence playback
+				
+				int startFrame = -1, endFrame = -1;
+	
+				// Resume playback?
+				if (currentPlayingFrame != null && 
+						MultiTrackPlaybackController.getInstance().isMarkedAsPaused(
+								currentPlayingFrame, masterMix.getChannelID())) {
+					
+					long runningTime = FrameLayoutDaemon.inferCurrentTotalMSTime();
+					long inferredTotalFrameLength = -1;
+					if (runningTime > 0) {
+						inferredTotalFrameLength = AudioMath.millisecondsToFrames(
+								runningTime, 
+								SampledAudioManager.getInstance().getDefaultPlaybackFormat());
+					}
+					
+
+					startFrame = MultiTrackPlaybackController.getInstance().getLastSuspendedFrame();
+		
+					if (inferredTotalFrameLength > 0 &&
+							startFrame >= 0 && startFrame < inferredTotalFrameLength) {
+						// Work around for playing to end of frame: give biggest value
+						// since it is eventually clamped:
+						endFrame =  Integer.MAX_VALUE;
+					}
+				}
+				
+				// Play from beginning of selection to end of selection
+				if (startFrame < 0) {
+					startFrame = 0;
+					endFrame =  Integer.MAX_VALUE; // see notes about for workaround hack
+				}
+			
+				if (startFrame < endFrame) {
+
+					playFrame(this, 
+							currentFrameName, 
+							false, // TODO: Set appropriatly
+							0, 
+							startFrame, 
+							endFrame);
+					
+					setState(PLAYBACK_LOADING);
+					
+				}
+
+			} else { // pause
+
+				MultiTrackPlaybackController.getInstance().setPauseMark(true);
+				MultiTrackPlaybackController.getInstance().stopPlayback();
+
+			}
+
+			
+		} else if (e.getSource() == stopButton) {
+			assert(currentPlayingFrame != null);
+			
+			if (MultiTrackPlaybackController.getInstance().isCurrentPlaybackSubject(
+					currentPlayingFrame, masterMix.getChannelID())) {
+				MultiTrackPlaybackController.getInstance().setPauseMark(false);
+				MultiTrackPlaybackController.getInstance().stopPlayback();
+			}
+			
+			
+		} else if (e.getSource() == rewindButton) {
+			assert(state != PLAYING);
+			MultiTrackPlaybackController.getInstance().setPauseMark(false);
+			
+		}
+		
+	}
+	
+
+	private void updateBorderColor() {
+		
+		// Get border color currently used
+		Color oldC = getSource().getBorderColor();
+		Color newC = null;
+		
+		if (this.state == PLAYBACK_LOADING) {
+			
+			newC = LOADING_BORDER_COLOR;
+			
+		} else {
+
+			newC = TrackWidgetCommons.getBorderColor(
+					SoundDesk.getInstance().isSolo(masterMix.getChannelID()), 
+					masterMix.isMuted());
+
+		}
+		
+		// Update the color
+		if (!newC.equals(oldC)) {
+			setWidgetEdgeColor(newC);
+		}
+	}
+	
+	/**
+	 * Sets the mute icon to represent the current volume value in the slider.
+	 * Note: this is not the icon if mute is on.
+	 */
+	private void updateButtonGUI() {
+		
+		Icon newIcon = null;
+		if (volumeSlider.getValue() <= 25) 
+				newIcon = IconRepository.getIcon("vol25.png");
+		else if (volumeSlider.getValue() <= 50) 
+			newIcon = IconRepository.getIcon("vol50.png");
+		else if (volumeSlider.getValue() <= 75) 
+			newIcon = IconRepository.getIcon("vol75.png");
+		else // maxing
+				newIcon = IconRepository.getIcon("vol100.png");
+		
+		muteButton.setIcon(newIcon);
+	}
+	
+
+	public void volumeChanged() {
+		masterMix.setVolume(((float)volumeSlider.getValue()) / 100.0f);
+	}
+
+	public void muteChanged() {
+		masterMix.setMuted(muteButton.isSelected());
+	}
+
+	public void soloChanged() {
+		SoundDesk.getInstance().setSoloIDPrefix(soloButton.isSelected() ?
+				masterMix.getChannelID() : null
+				);
+	}
+	
+
+	/**
+	 * Updates the volume GUI for all views
+	 */
+	public void updateVolume() {
+		int volume = (int)(100 * masterMix.getVolume());
+
+		if (volumeSlider.getValue() == volume) return;
+		
+		isUpdatingGUI = true;
+		
+		volumeSlider.setValue(volume);
+		
+		isUpdatingGUI = false;
+	}
+	
+
+	/**
+	 * Updates the mute button GUI for all views. 
+	 */
+	public void updateMute() {
+		if (muteButton.isSelected() == masterMix.isMuted()) return;
+
+		isUpdatingGUI = true;
+		
+		muteButton.setSelected(masterMix.isMuted());
+
+		isUpdatingGUI = false;
+	}
+
+
+	/**
+	 * Updates the solo button GUI for all views. 
+	 */
+	public void updateSolo() {
+		boolean isSolo = SoundDesk.getInstance().isSolo(masterMix.getChannelID());
+
+		if (soloButton.isSelected() == isSolo) return;
+
+		isUpdatingGUI = true;
+		
+		soloButton.setSelected(isSolo);
+
+		isUpdatingGUI = false;
+		
+	}
+	
+
+	/**
+	 * Plays a frame with the FramePlayer mix.
+	 * 
+	 * @see MultiTrackPlaybackController#playFrame(MultitrackLoadListener, String, String, boolean, int, int, int)
+	 * 
+	 */
+	public static void playFrame(
+			MultitrackLoadListener loadListener,
+			String rootFrameName,
+			boolean resume,
+			int relativeStartFrame,
+			int startFrame,
+			int endFrame) {
+		
+		if (loadListener == null) throw new NullPointerException("loadListener");
+		if (rootFrameName == null) throw new NullPointerException("rootFrameName");
+		
+		
+		currentPlayingFrame = rootFrameName;
+		
+		MultiTrackPlaybackController.getInstance().playFrame(
+				loadListener, 
+				rootFrameName, 
+				masterMix.getChannelID(),
+				resume,
+				relativeStartFrame, 
+				startFrame, 
+				endFrame);
+	}
+	
+}
Index: /trunk/src_apollo/org/apollo/widgets/LinkedTrack.java
===================================================================
--- /trunk/src_apollo/org/apollo/widgets/LinkedTrack.java	(revision 315)
+++ /trunk/src_apollo/org/apollo/widgets/LinkedTrack.java	(revision 315)
@@ -0,0 +1,1882 @@
+package org.apollo.widgets;
+
+import java.awt.BasicStroke;
+import java.awt.Color;
+import java.awt.Font;
+import java.awt.FontMetrics;
+import java.awt.GradientPaint;
+import java.awt.Graphics;
+import java.awt.Graphics2D;
+import java.awt.Paint;
+import java.awt.Point;
+import java.awt.Rectangle;
+import java.awt.Shape;
+import java.awt.Stroke;
+import java.awt.event.ActionEvent;
+import java.awt.event.ActionListener;
+import java.awt.event.KeyEvent;
+import java.awt.event.KeyListener;
+import java.awt.event.MouseEvent;
+import java.awt.event.MouseListener;
+import java.awt.event.MouseMotionListener;
+import java.awt.geom.Area;
+import java.awt.geom.Rectangle2D;
+import java.util.LinkedList;
+import java.util.List;
+
+import javax.swing.JPanel;
+import javax.swing.JSlider;
+import javax.swing.JToggleButton;
+import javax.swing.SwingUtilities;
+
+import org.apollo.ApolloSystem;
+import org.apollo.audio.ApolloSubjectChangedEvent;
+import org.apollo.audio.SampledAudioManager;
+import org.apollo.audio.structure.AbsoluteTrackNode;
+import org.apollo.audio.structure.AudioStructureModel;
+import org.apollo.audio.structure.LinkedTracksGraphNode;
+import org.apollo.audio.structure.OverdubbedFrame;
+import org.apollo.audio.structure.TrackGraphLoopException;
+import org.apollo.audio.util.FrameLayoutDaemon;
+import org.apollo.audio.util.MultiTrackPlaybackController;
+import org.apollo.audio.util.PlaybackClock;
+import org.apollo.audio.util.SoundDesk;
+import org.apollo.audio.util.TrackMixSubject;
+import org.apollo.audio.util.MultiTrackPlaybackController.MultitrackLoadListener;
+import org.apollo.audio.util.PlaybackClock.PlaybackClockListener;
+import org.apollo.gui.ExpandedTrackManager;
+import org.apollo.gui.PlaybackControlPopup;
+import org.apollo.gui.SampledTrackGraphView;
+import org.apollo.io.AudioPathManager;
+import org.apollo.io.IconRepository;
+import org.apollo.items.EmulatedTextItem;
+import org.apollo.items.EmulatedTextItem.TextChangeListener;
+import org.apollo.mvc.Observer;
+import org.apollo.mvc.Subject;
+import org.apollo.mvc.SubjectChangedEvent;
+import org.apollo.util.AudioMath;
+import org.apollo.util.AudioSystemLog;
+import org.apollo.util.ODFrameHeirarchyFetcher;
+import org.apollo.util.PopupReaper;
+import org.apollo.util.StringEx;
+import org.apollo.util.ODFrameHeirarchyFetcher.ODFrameReceiver;
+import org.expeditee.gui.Browser;
+import org.expeditee.gui.DisplayIO;
+import org.expeditee.gui.Frame;
+import org.expeditee.gui.FrameGraphics;
+import org.expeditee.gui.FrameUtils;
+import org.expeditee.gui.MouseEventRouter;
+import org.expeditee.gui.PopupManager;
+import org.expeditee.items.Item;
+import org.expeditee.items.ItemParentStateChangedEvent;
+import org.expeditee.items.Text;
+import org.expeditee.items.widgets.InteractiveWidget;
+import org.expeditee.items.widgets.InteractiveWidgetInitialisationFailedException;
+import org.expeditee.items.widgets.InteractiveWidgetNotAvailableException;
+
+public class LinkedTrack extends InteractiveWidget implements ODFrameReceiver, Observer, MultitrackLoadListener, PlaybackClockListener {
+	
+	private String virtualFilename; // immutable 
+	
+	private TrackMixSubject masterMix; // immutable
+	
+	private OverdubbedFrame linkedOverdubbedFrame = null;
+	
+	// NOTE: Using meta data as name model data - since there are two views for the name cannot store in a single label
+	private EmulatedTextItem nameLabel = null; 
+	
+	private PlaybackPopup playbackControlPopup = null;
+
+
+	// private SoundDeskPopup soundDeskPopup = null;
+	private MouseActions mouseActions = new MouseActions();
+	private MasterControlActionListener masterActionListener = new MasterControlActionListener();
+	
+	/** All the track information for playing / editing / rendering.
+	 * They ordered such that they are grouped by localfilename */
+	private List<Track> tracks = new LinkedList<Track>();
+	private int uniqueLocalTrackCount = 0; // the amount of different local filenames referenced by this track
+	private long totalFrameLength = 0;
+	private int currentPlaybackFramePosition = -1;
+	
+	
+	private int selectionStartX = 0;
+	private int selectionStart = 0;
+	private int selectionLength = -1;
+	
+	
+	private int state = NOT_INITIALIZED;
+	private int pendingGraphFetchCount = 0; // used for discarding redundant graph fetches
+	private String failMessage = null; // shown if in a failed state
+	private String abortMessage = null;
+	
+	private static final Color FAILED_MESSAGE_COLOR = Color.RED;
+	private static final Font MESSAGE_FONT = TrackWidgetCommons.FREESPACE_TRACKNAME_FONT;
+	private static final Font HELPER_FONT = new Font("Arial", Font.BOLD, 12);
+	
+	private static final Color[] TRACK_COLOR_WHEEL = { 
+							new Color(66, 145, 255), 
+							new Color(255, 163, 33), 
+							new Color(155, 227, 48), 
+						};
+	private static final Color TRACK_LOAD_COLOR = Color.BLACK;
+	private static final Color TRACK_FAIL_COLOR = Color.RED;
+	
+	private static final Color SELECTION_COLOR = new Color(0,0,0,100);
+
+	private static final Stroke TRACK_BORDER = new BasicStroke(1.0f);
+	private static final Stroke TRACK_SELECTED_BORDER = new BasicStroke(2.0f);
+	private static final Color TRACK_BORDER_COLOR = Color.BLACK;
+	private static final Color TRACK_SELECTED_BORDER_COLOR = new Color(253, 255, 201);
+	
+	
+	/** States - mutex in this widget but not really .. for example since can be loading tracks and the graph at the same time... */
+	private  static final int NOT_INITIALIZED = 0; // Not showing...
+	private  static final int LOADING_TRACK_GRAPH = 1; // Loading the track layout information
+	private  static final int FAILED_STATE = 2; // e.g. Bad graph... cannot use
+	private  static final int STOPPED = 3; // Graph loaded and ready to play.
+	private  static final int PLAYBACK_LOADING = 4; // Loading tracks from file/cache to play/resume
+	private  static final int PLAYING = 5; // Playing audio.
+	private  static final int EMPTY_STATE = 6; // e.g. No link set OR points to noththing to play
+	
+	/** For parsing metadata */
+	public static final String META_VIRTUALNAME_TAG = "vn=";
+	
+	/**
+	 * Constructor called by Expeditee. 
+	 * 
+	 * @param source
+	 * @param args
+	 */
+	public LinkedTrack(Text source, String[] args) {
+		super(source, new JPanel(), 
+				100, -1, 
+				FrameLayoutDaemon.TRACK_WIDGET_HEIGHT, FrameLayoutDaemon.TRACK_WIDGET_HEIGHT);
+
+		// Read the metadata
+		virtualFilename = getStrippedDataString(META_VIRTUALNAME_TAG);
+
+		if (virtualFilename == null) { // virtualFilename is immutable. Must set on construction.
+			virtualFilename = AudioPathManager.generateVirtualFilename();
+				updateData(META_VIRTUALNAME_TAG, META_VIRTUALNAME_TAG + virtualFilename);
+		}
+		
+		masterMix = SoundDesk.getInstance().getOrCreateMix(SoundDesk.createMasterChannelID(this));
+
+		// Set widget as a fixed size widget- using width from last recorded width in the meta data.
+		int width = getStrippedDataInt(TrackWidgetCommons.META_LAST_WIDTH_TAG, -1);
+		if (width >= 0 && width < FrameLayoutDaemon.MIN_TRACK_WIDGET_WIDTH) {
+			width = FrameLayoutDaemon.MIN_TRACK_WIDGET_WIDTH;
+		}
+		
+		if (width < 0) {
+			setSize(-1, -1, 
+					FrameLayoutDaemon.TRACK_WIDGET_HEIGHT, FrameLayoutDaemon.TRACK_WIDGET_HEIGHT, 
+					FrameLayoutDaemon.TRACK_WIDGET_DEFAULT_WIDTH, FrameLayoutDaemon.TRACK_WIDGET_HEIGHT);
+		} else {
+			setSize(-1, -1, 
+					FrameLayoutDaemon.TRACK_WIDGET_HEIGHT, FrameLayoutDaemon.TRACK_WIDGET_HEIGHT, 
+					width, FrameLayoutDaemon.TRACK_WIDGET_HEIGHT);
+		}
+		
+		playbackControlPopup = new PlaybackPopup();
+		
+		nameLabel = new EmulatedTextItem(_swingComponent, new Point(10, 20));
+		nameLabel.setBackgroundColor(Color.WHITE);	
+		
+		String metaName = getStrippedDataString(TrackWidgetCommons.META_NAME_TAG);
+		if (metaName == null) metaName = "Untitled";
+		nameLabel.setText(metaName); 
+
+		nameLabel.addTextChangeListener(new TextChangeListener() { 
+
+			public void onTextChanged(Object source, String newLabel) {
+	
+				if (state != PLAYING && state != STOPPED) return;
+				
+				setName(nameLabel.getText());
+
+			}
+
+		});
+		
+		_swingComponent.addKeyListener(new KeyListener() {
+
+			public void keyPressed(KeyEvent e) {
+				if (state != PLAYING && state != STOPPED) return;
+				if (nameLabel.onKeyPressed(e, _swingComponent)) {
+					e.consume();
+					return;
+				}
+			}
+
+			public void keyReleased(KeyEvent e) {
+				if (state != PLAYING && state != STOPPED) return;
+				if (nameLabel.onKeyReleased(e, _swingComponent)) {
+					e.consume();
+					return;
+				}
+			}
+
+			public void keyTyped(KeyEvent e) {
+			}
+			
+		});
+		
+		
+		updateBorderColor();
+		
+		
+	}
+	
+	/**
+	 * Must set naem via this centralized method.
+	 * @param newName
+	 */
+	private void setName(String newName) {
+
+		String modelLabel = LinkedTrack.this.getStrippedDataString(TrackWidgetCommons.META_NAME_TAG);
+		
+		if (!StringEx.equals(modelLabel, newName)) {
+			updateData(TrackWidgetCommons.META_NAME_TAG, newName);
+		} 
+		
+		if (!StringEx.equals(nameLabel.getText(), newName)) {
+			nameLabel.setText(newName);
+		}
+			
+//		if (soundDeskPopup != null && 
+//				!StringEx.equals(soundDeskPopup.nameLabel.getText(), newName)) {
+//			soundDeskPopup.nameLabel.setText(newName);
+//		}
+				
+		// Keep graph model consistant
+		
+		Frame parent = getParentFrame();
+		
+		AudioStructureModel.getInstance().onLinkedTrackWidgetNameChanged(
+				virtualFilename, 
+				(parent != null) ? parent.getName() : null, 
+				newName);
+		
+	}
+	
+	public boolean shouldLayout() {
+		return (state != EMPTY_STATE && state != FAILED_STATE);
+	}
+	
+	/**
+	 * Sets the GUI and all the popups to reflect the current state.
+	 * Must be on the swing thread.
+	 * 
+	 * @param newState
+	 * 			The new state. The same state then it is ignored, unless
+	 * 			its LOADING_TRACK_GRAPH - in that case the graph is reloaded.
+	 */
+	private void setState(int newState) {
+		if (this.state == newState && newState != LOADING_TRACK_GRAPH) return; // no need to process request.
+		
+		//assert ((state == NOT_INITIALIZED && !isVisible()) || 
+		//		(state != NOT_INITIALIZED && isVisible()));
+		
+		evaluate_state:
+			
+		switch(newState) {
+		case NOT_INITIALIZED: // became invisible
+			
+			playbackControlPopup.stopButton.setEnabled(false);
+			playbackControlPopup.rewindButton.setEnabled(false);
+			playbackControlPopup.playPauseButton.setEnabled(false);
+			tracks.clear();
+			break;
+			
+		case LOADING_TRACK_GRAPH:
+			
+			// Note: Can be invoked even if already in this state
+			
+			// Get the frame that this widget is linking to
+			String linkedFrame = getAbsoluteLink(); // must be in famename form
+			
+			if (linkedFrame != null) {
+				
+				// Request a fetch from the model - eventually will return a result
+				// and the graph image will be build
+				pendingGraphFetchCount++;
+				ODFrameHeirarchyFetcher.getInstance().doFetch(linkedFrame, this);
+				// Once finished the callback will construct the model data / graph.
+			}  else {
+				// No link - switch state to empty
+				newState = EMPTY_STATE;
+				break evaluate_state; // re-eval
+			}
+			
+			playbackControlPopup.stopButton.setEnabled(false);
+			playbackControlPopup.rewindButton.setEnabled(false);
+			playbackControlPopup.playPauseButton.setEnabled(false);
+
+			break;
+			
+		case FAILED_STATE: 
+			assert(failMessage != null);
+			playbackControlPopup.stopButton.setEnabled(false);
+			playbackControlPopup.rewindButton.setEnabled(false);
+			playbackControlPopup.playPauseButton.setEnabled(false);
+			
+			setSize(100, -1, 
+					FrameLayoutDaemon.TRACK_WIDGET_HEIGHT, FrameLayoutDaemon.TRACK_WIDGET_HEIGHT, 
+					200, FrameLayoutDaemon.TRACK_WIDGET_HEIGHT);
+			
+			break;
+			
+		case STOPPED:
+			playbackControlPopup.rewindButton.setEnabled(true);
+			playbackControlPopup.stopButton.setEnabled(false);
+			playbackControlPopup.playPauseButton.setEnabled(true);
+			playbackControlPopup.playPauseButton.setIcon(IconRepository.getIcon("play.png"));
+		
+
+			setWidgetEdgeThickness(TrackWidgetCommons.STOPPED_TRACK_EDGE_THICKNESS);
+			
+			break;
+			
+		case PLAYBACK_LOADING: 
+			playbackControlPopup.stopButton.setEnabled(false);
+			playbackControlPopup.rewindButton.setEnabled(false);
+			playbackControlPopup.playPauseButton.setEnabled(false);
+			break;
+			
+		case PLAYING: 
+			playbackControlPopup.stopButton.setEnabled(true);
+			playbackControlPopup.rewindButton.setEnabled(false);
+			playbackControlPopup.playPauseButton.setEnabled(true);
+			playbackControlPopup.playPauseButton.setIcon(IconRepository.getIcon("pause.png"));
+
+			setWidgetEdgeThickness(TrackWidgetCommons.PLAYING_TRACK_EDGE_THICKNESS);
+			
+			PlaybackClock.getInstance().addPlaybackClockListener(this); // listen to ticks
+
+			break;
+			
+		case EMPTY_STATE:
+			setSize(100, -1, 
+					FrameLayoutDaemon.TRACK_WIDGET_HEIGHT, FrameLayoutDaemon.TRACK_WIDGET_HEIGHT, 
+					200, FrameLayoutDaemon.TRACK_WIDGET_HEIGHT);
+			playbackControlPopup.stopButton.setEnabled(false);
+			playbackControlPopup.rewindButton.setEnabled(false);
+			playbackControlPopup.playPauseButton.setEnabled(false);
+			break;
+			
+			default:
+				assert(false);
+
+		}
+		
+		if (newState != PLAYING) { // safety
+			PlaybackClock.getInstance().removePlaybackClockListener(this);
+		}
+		
+		// If in empty state then clear mouse listeners to give expeditee events
+		if (newState == EMPTY_STATE) {
+			_swingComponent.removeMouseListener(mouseActions);
+			_swingComponent.removeMouseMotionListener(mouseActions);
+			
+		} else {
+			
+			if (_swingComponent.getMouseListeners().length == 0) {
+				_swingComponent.addMouseListener(mouseActions);
+			}
+		
+			if (_swingComponent.getMouseMotionListeners().length == 0) {
+				_swingComponent.addMouseMotionListener(mouseActions);
+			}
+		}
+		
+		state = newState;
+		
+		invalidateSelf();
+		FrameGraphics.refresh(true);
+	}
+	
+	/**
+	 * {@inheritDoc}
+	 */
+	public Subject getObservedSubject() {
+		return null; // many
+	}
+	
+	/**
+	 * {@inheritDoc}
+	 */
+	public void setObservedSubject(Subject parent) {
+		// many
+	}
+	
+	/**
+	 * {@inheritDoc}
+	 */
+	public void modelChanged(Subject source, SubjectChangedEvent event) {
+		
+		// Note: no need to check for audio structure model changing since can only change
+		// if in another frame.
+
+		// Synch GUI with track state
+		switch (event.getID()) {
+			
+		case ApolloSubjectChangedEvent.PLAYBACK_STARTED:
+			
+			if (state == PLAYBACK_LOADING && linkedOverdubbedFrame != null && 
+				MultiTrackPlaybackController.getInstance().isCurrentPlaybackSubject(
+					this.linkedOverdubbedFrame.getFrameName(), masterMix.getChannelID())) {
+				setState(PLAYING);
+			} 
+			break;
+			
+		case ApolloSubjectChangedEvent.PLAYBACK_STOPPED:
+			
+			if (state == PLAYING) {
+				assert(linkedOverdubbedFrame != null);
+
+				int playbackFramePos = -1;
+				
+				// The stop event might be due to a pause:
+				if (MultiTrackPlaybackController.getInstance().isMarkedAsPaused(
+						linkedOverdubbedFrame.getFrameName(), masterMix.getChannelID())) {
+					playbackFramePos = MultiTrackPlaybackController.getInstance().getLastSuspendedFrame();
+				}
+				
+				// Has the user edited the audio such that the playback position is invalid?
+				if (playbackFramePos > totalFrameLength) 
+					playbackFramePos = -1; 
+	
+				// Set the playback position to the exact suspended frame pos.
+				updatePlaybackPosition(playbackFramePos);
+				
+				// Transition into new state
+				setState(STOPPED);
+
+			}
+			break;
+			
+		case ApolloSubjectChangedEvent.VOLUME: // From obseved track mix
+			updateVolume();
+			break;
+			
+		case ApolloSubjectChangedEvent.MUTE: // From obseved track mix
+			updateMute();
+			updateBorderColor();
+			break;
+			
+		case ApolloSubjectChangedEvent.SOLO_PREFIX_CHANGED: // From mix desk
+			updateSolo();
+			updateBorderColor();
+			break;
+			
+		}
+	}
+	
+	/**
+	 * {@inheritDoc}
+	 */
+	public void onTick(long framePosition, long msPosition) {
+		
+		if (framePosition < 0) return; // Done
+		
+		// The frame position / ms position is according to the software mixers timeline - not
+		// neccessarily starting off when the observed multi track sequences started. Thus must
+		// determine the actual frame position relative to the current rack sequence being played
+		// for this view:
+		int fpos = (int)(framePosition 
+				- MultiTrackPlaybackController.getInstance().getLastInitiationFrame()
+				+ MultiTrackPlaybackController.getInstance().getLastStartFrame());
+		
+		// Clamp
+		if (fpos > MultiTrackPlaybackController.getInstance().getLastEndFrame())
+			fpos = MultiTrackPlaybackController.getInstance().getLastEndFrame();
+
+		updatePlaybackPosition(fpos);
+
+	}
+
+	/**
+	 * {@inheritDoc}
+	 */
+	public void multiplaybackLoadStatusUpdate(int id, Object state) {
+		
+		// NOTE: Could switch to an unloaded state
+		if (this.state != PLAYBACK_LOADING) return;
+		
+		switch(id) {
+		case MultitrackLoadListener.LOAD_CANCELLED:
+			setState(STOPPED);
+			break;
+		case MultitrackLoadListener.LOAD_COMPLETE:
+			// Ensure that all tracks area dsiplayed and being loaded.
+			for (Track t : tracks) setTrackState(t, TRACKSTATE_READY);
+			invalidateSelf();
+			FrameGraphics.refresh(true);
+			break;
+		case MultitrackLoadListener.LOAD_FAILED_BAD_GRAPH:
+			abortMessage = "Graph contains loops";
+			((Exception)state).printStackTrace();
+			break;
+		case MultitrackLoadListener.LOAD_FAILED_GENERIC:
+			abortMessage = "Unexpected error";
+			((Exception)state).printStackTrace();
+			break;
+		case MultitrackLoadListener.LOAD_FAILED_PLAYBACK:
+			abortMessage = "Graph contains loops";
+			break;
+		case MultitrackLoadListener.NOTHING_TO_PLAY:
+			abortMessage = "Nothing to play"; // could be due to user slecting empty space
+			break;
+		case MultitrackLoadListener.TRACK_LOAD_FAILED_IO:
+			// This is special... the loader does not abort... and it tries to load more.
+			setTrackState((String) state, TRACKSTATE_FAILED);
+			((Exception)state).printStackTrace();
+			break;
+		case MultitrackLoadListener.TRACK_LOADED:
+			setTrackState((String) state, TRACKSTATE_READY);
+			invalidateSelf();
+			FrameGraphics.refresh(true);
+			break;
+
+		}
+		
+		if (abortMessage != null) {
+			AudioSystemLog.println("Aborted playback - " + abortMessage);
+			setState(STOPPED);
+		}
+		
+	}
+
+	/**
+	 * {@inheritDoc}
+	 */
+	@Override
+	protected String[] getArgs() {
+		return null;
+	}
+
+	/**
+	 * {@inheritDoc}
+	 */
+	@Override
+	protected List<String> getData() {
+
+		List<String> data = new LinkedList<String>();
+		
+		data.add(META_VIRTUALNAME_TAG + virtualFilename);
+		
+		String name = getName();
+	
+		if (name != null)
+			data.add(TrackWidgetCommons.META_NAME_TAG + name);
+
+		data.add(TrackWidgetCommons.META_LAST_WIDTH_TAG + getWidth());
+		
+		Long initTime = null;
+		
+		Frame f = getParentFrame();
+		if (f != null) {
+			LinkedTracksGraphNode ltinf = AudioStructureModel.getInstance().getLinkedTrackGraphInfo(virtualFilename, f.getName());
+			if (ltinf != null) {
+				initTime = ltinf.getInitiationTime();
+			}
+		}
+		
+		if (initTime == null) initTime = getInitiationTimeFromMeta(); // old meta
+		if (initTime == null) initTime = 0L;
+
+		data.add(TrackWidgetCommons.META_INITIATIONTIME_TAG + initTime);
+
+		return data;
+	}
+	
+	/**
+	 * {@inheritDoc}
+	 */
+	@Override
+	public InteractiveWidget copy() 
+		throws InteractiveWidgetNotAvailableException, InteractiveWidgetInitialisationFailedException {
+		
+		Item source = getCurrentRepresentation().copy();
+		String clonedAnnotation = getAnnotationString();
+		source.setText(clonedAnnotation);
+		
+		// But since this is a copy: must allocate a new virtual filename of its own...
+		// thus remove the vname meta
+		List<String> data = getData();
+
+		for (int i = 0; i < data.size(); i++) {
+			String str = data.get(i);
+			if (str.trim().startsWith(META_VIRTUALNAME_TAG)) {
+				data.remove(i);
+				i--; // continue - super safety to get rid of any hint of vname meta
+			}
+		}
+
+		source.setData(data);
+		
+		return InteractiveWidget.createWidget((Text)source);
+
+	}
+
+	/**
+	 * {@inheritDoc}
+	 */
+	@Override
+	protected void onParentStateChanged(int eventType) {
+		super.onParentStateChanged(eventType);
+		
+		Frame parent = this.getParentFrame();
+		String parentName = (parent != null) ? parent.getName() : null;
+
+		switch (eventType) {
+
+		case ItemParentStateChangedEvent.EVENT_TYPE_ADDED:
+			
+			// Resume any layouts suspended by this track widget
+			FrameLayoutDaemon.getInstance().resumeLayout(this);
+			
+			// Update model.
+			String link = getAbsoluteLink();
+
+			// Determine new initation time according to anchored position
+			long initTime = (parent != null) ? 
+						FrameLayoutDaemon.getInstance().getMSAtX(getX(), parent)
+						: 0;
+
+			if (link != null) {
+				
+				AudioStructureModel.getInstance().onLinkedTrackWidgetAnchored(
+						virtualFilename, 
+						parentName, 
+						initTime,
+						link,
+						getName());
+			}
+			
+			// Fall through ... and load the graph
+		case ItemParentStateChangedEvent.EVENT_TYPE_ADDED_VIA_OVERLAY: // TODO revise - and in sampled track widget
+		case ItemParentStateChangedEvent.EVENT_TYPE_SHOWN:
+		case ItemParentStateChangedEvent.EVENT_TYPE_SHOWN_VIA_OVERLAY:
+
+			// Setup observers
+			SoundDesk.getInstance().addObserver(this);
+			MultiTrackPlaybackController.getInstance().addObserver(this);
+			masterMix.addObserver(this);
+			
+			// Load the graph
+			setState(LOADING_TRACK_GRAPH);
+
+			break;
+			
+		case ItemParentStateChangedEvent.EVENT_TYPE_REMOVED:
+			
+			// If yanking this from the frame into free space suspend the layout daemon
+			// for this frame
+			if (MouseEventRouter.getCurrentMouseEvent() != null &&
+					MouseEventRouter.getCurrentMouseEvent().getButton() == MouseEvent.BUTTON2) {
+				Frame suspended = DisplayIO.getCurrentFrame();
+				if (suspended != null) {
+					FrameLayoutDaemon.getInstance().suspendLayout(suspended, this);
+				}
+			}
+			
+			// Update model.
+			AudioStructureModel.getInstance().onLinkedTrackWidgetRemoved(
+					virtualFilename, 
+					parentName);
+			
+			// Cancel loading of audio
+			if (state == PLAYBACK_LOADING) {
+				assert(linkedOverdubbedFrame != null);
+				MultiTrackPlaybackController.getInstance().cancelLoad(
+						linkedOverdubbedFrame.getFrameName(), 
+						masterMix.getChannelID());
+			}
+			
+			
+			// TODO: Temp workaround - why are these widgets not refreshing?
+			SwingUtilities.invokeLater(new Runnable() {
+				public void run() {
+					invalidateSelf();
+					FrameGraphics.refresh(true);
+				}
+			});
+
+		case ItemParentStateChangedEvent.EVENT_TYPE_REMOVED_VIA_OVERLAY: // TODO revise - and in sampled track widget
+		case ItemParentStateChangedEvent.EVENT_TYPE_HIDDEN:
+			
+			// Remove observers
+			SoundDesk.getInstance().removeObserver(this);
+			MultiTrackPlaybackController.getInstance().removeObserver(this);
+			masterMix.removeObserver(this);
+			
+			
+			setState(NOT_INITIALIZED);
+			break;
+
+		}
+		
+	}
+	
+	
+
+	@Override
+	public void onDelete() {
+		super.onDelete();
+		// Resume any layouts suspended by this track widget
+		FrameLayoutDaemon.getInstance().resumeLayout(this);
+	}
+
+	/**
+	 * {@inheritDoc}
+	 */
+	@Override
+	public void setLink(String link) {
+		
+		String oldABSLink = getAbsoluteLink();
+		
+		super.setLink(link);
+		
+		String newABSLink = getAbsoluteLink();
+		
+		if (StringEx.equals(oldABSLink, newABSLink)) return;
+		
+		Frame parent = this.getParentFrame();
+		String parentName = (parent != null) ? parent.getName() : null;
+		
+		// Changing the link is the same as removing a track-link in the graph model (if had one)
+		AudioStructureModel.getInstance().onLinkedTrackWidgetRemoved(
+				virtualFilename, 
+				parentName);
+
+		
+		// And adding a new one (if applicable)
+		if (parent != null && newABSLink != null) {
+			
+			Long initTime = (parent != null) ? 
+					FrameLayoutDaemon.getInstance().getMSAtX(getX(), parent) :
+						getInitiationTimeFromMeta();
+
+			if (initTime == null) initTime = 0L;
+
+			AudioStructureModel.getInstance().onLinkedTrackWidgetAnchored(
+					virtualFilename, 
+					parentName, 
+					initTime, // TODO: Revise: actually would be better to get from model .. if possible
+					newABSLink,
+					getName());
+
+		}
+		
+		// Reload track graph
+		setState(LOADING_TRACK_GRAPH);
+
+		//FrameLayoutDaemon.getInstance().forceRecheck(); // no need - daemon is notified via structure model
+
+		
+	}
+
+	/**
+	 * {@inheritDoc}
+	 */
+	@Override
+	protected void onSizeChanged() {
+		super.onSizeChanged();
+		
+		// Re-adjust track sizes
+		updateGraphLayout();
+		
+	}
+
+	/**
+	 * @return
+	 * 		The virtual filename. Never null.
+	 */
+	public String getVirtualFilename() {
+		return virtualFilename;
+	}
+
+	/**
+	 * Determines the running time from the meta data.
+	 * 
+	 * @return
+	 * 		The running time or this track in MS.
+	 * 		-1 if unavilable.
+	 */
+	public long getRunningMSTimeFromMeta() {
+		return getStrippedDataLong(TrackWidgetCommons.META_RUNNINGMSTIME_TAG, new Long(-1));
+	}
+	
+	/**
+	 * Determines the initiation time from the meta data.
+	 * 
+	 * @return
+	 * 		The initiation time or this track in MS.
+	 * 		null if unavilable.
+	 */
+	public Long getInitiationTimeFromMeta() {
+		return getStrippedDataLong(TrackWidgetCommons.META_INITIATIONTIME_TAG, null);
+	}
+	
+	/**
+	 * @return
+	 * 		The name given to this widget... can be null.
+	 */
+	public String getName() {
+		
+		String name = nameLabel.getText();
+
+		if (name == null) {
+			name = getStrippedDataString(TrackWidgetCommons.META_NAME_TAG);
+		}
+
+		return name;
+	}
+	
+	/**
+	 * {@inheritDoc}
+	 */
+	@Override
+	public void paint(Graphics g) {
+		
+		if (Browser._theBrowser == null) return;
+		
+		// Paint backing
+		g.setColor(SampledTrackGraphView.DEFAULT_BACKGROUND_COLOR);
+		g.fillRect(getX(), getY(), getWidth(), getHeight());
+		
+		String centralizedMessage = null;
+
+		// Render according to the currently widget state
+		switch(state) {
+		case NOT_INITIALIZED: // became invisible
+			break;
+
+		case FAILED_STATE: 
+		case EMPTY_STATE:
+		case LOADING_TRACK_GRAPH:
+			
+			if (state == FAILED_STATE) {
+				centralizedMessage = failMessage;
+				assert(failMessage != null);
+				g.setColor(FAILED_MESSAGE_COLOR);
+			} else if (state == LOADING_TRACK_GRAPH){
+				centralizedMessage = "Loading...";
+				g.setColor(Color.DARK_GRAY);
+			} else { // empty
+				if (getLink() == null) {
+					centralizedMessage = "Click to create";
+				} else {
+					centralizedMessage = "Nothing to play";
+				}
+				
+				g.setColor(Color.DARK_GRAY);
+			}
+
+			break;
+			
+		case STOPPED:
+		case PLAYBACK_LOADING: 
+		case PLAYING: 
+			int x = getX();
+			int y = getY();
+			Track selected = null;
+
+			for (Track track : tracks) {
+				track.paintTrackArea((Graphics2D)g, x, y);
+				if (track.isSelected) {
+					selected = track;
+				} else {
+					track.paintTrackBorder(g, x, y);
+					
+					if (track.area.height > 15) { // don't clutter up busy linked tracks
+						drawTrackName(track, g, Color.DARK_GRAY);
+					}
+	
+				}
+				
+	
+			}
+			
+			// Selection can spill over other track areas so to avoid z fighting always ensure that
+			// the selected border is drawn last
+			if (selected != null) {
+				selected.paintTrackBorder(g, x, y);
+				drawTrackName(selected, g, Color.BLACK);
+			}
+
+			
+			if (abortMessage != null) {
+				
+				
+				
+				
+				
+				// TODO!!
+				// Draw string...
+				
+				
+				
+				
+				
+			}
+			
+			if (state == PLAYBACK_LOADING) {
+				
+				centralizedMessage = "Loading dubs...";
+				g.setColor(Color.BLACK);
+				
+			} else {
+
+				// Paint the selection range
+				int selLeftX = XatFrame(selectionStart);
+				if (selectionLength > 1) {
+					g.setColor(SELECTION_COLOR);
+					g.fillRect(	getX() + selLeftX, 
+								getY(), 
+								XatFrame(selectionStart + selectionLength) - selLeftX, 
+								getHeight());
+				}
+				
+				// Draw selection start bar
+	
+				((Graphics2D)g).setStroke(SampledTrackGraphView.GRAPH_BAR_STROKE);
+				
+				// Note that if the start line is near the edges of the panel it can be concealed - thus
+				// set a thick line on the edges
+				x = selLeftX + getX();
+				if (x == 0) {
+					x = 1;
+				} else  if (x == getWidth()){
+					x = getWidth() - 1;
+				}
+				
+				g.setColor(Color.RED);
+				g.drawLine(
+						x, 
+						getY(), 
+						x,
+						getY() + getHeight());
+	
+				
+				// Paint the playback bar
+				if (currentPlaybackFramePosition >= 0 && linkedOverdubbedFrame != null) {
+					
+					x = XatFrame(currentPlaybackFramePosition) + getX();
+					
+					((Graphics2D)g).setStroke(SampledTrackGraphView.GRAPH_BAR_STROKE);
+					g.setColor(SampledTrackGraphView.PLAYBACK_BAR_COLOR);
+					g.drawLine(x, getY(), x, getY() + getHeight());
+				
+				}
+			
+			}
+			
+
+			
+			break;
+			
+		}
+		
+		// Draw centralized message string
+		if (centralizedMessage != null) {
+	
+			// Ensure that load bar and text doesn't spill over widgets invalidation area
+			Area clip = FrameGraphics.getCurrentClip();
+			
+			Shape clipBackUp = g.getClip();
+			
+			Area tmpClip = (clip != null) ? clip : 
+				new Area(new Rectangle(0, 0, 
+						Browser._theBrowser.getContentPane().getWidth(), 
+						Browser._theBrowser.getContentPane().getHeight()));
+			
+			tmpClip.intersect(new Area(getBounds()));
+	
+			if (!tmpClip.isEmpty()) {
+	
+				g.setClip(tmpClip);
+				
+				FontMetrics fm   = g.getFontMetrics(MESSAGE_FONT);
+				Rectangle2D rect = fm.getStringBounds(centralizedMessage, g);
+	
+				// Calc centered position
+				int x = getX() + ((getWidth() / 2) - (int)(rect.getWidth() / 2));
+				int y = getY() + ((getHeight() / 2) + (int)(rect.getHeight() / 2) - 4);
+				
+				// Draw text
+				g.setFont(MESSAGE_FONT);
+				g.drawString(centralizedMessage, x, y);
+				
+				// Draw add icon if waiting for a link
+				if (getLink() == null) {
+					IconRepository.getIcon("goto.png").paintIcon(null,
+							g, 
+							x - 34, 
+							getY() + (getHeight() / 2) - 12);
+				}
+				
+				g.setClip(clipBackUp);
+			}
+		}
+
+		// Paint name label over everything
+		if (state != NOT_INITIALIZED && state != FAILED_STATE && state != EMPTY_STATE)
+			nameLabel.paint(g);
+		
+		super.paintLink((Graphics2D)g);
+
+	}
+	
+	private void drawTrackName(Track track, Graphics g, Color textColor) {
+
+		// Draw helper message for user to give meaningful information of what they
+		// are actually highlighting
+		String helperMessage = track.getTrackName();
+		if (helperMessage == null) helperMessage = "<Unamed>";
+		helperMessage = helperMessage + " (" + track.getParentFrameName() +  ")";
+		
+		FontMetrics fm   = g.getFontMetrics(HELPER_FONT);
+		Rectangle2D rect = fm.getStringBounds(helperMessage, g);
+
+		// Calc centered position on track bounds -- which can be smaller than the font bounds`
+		int xoff = (int)((track.area.width - rect.getWidth()) / 2);
+		int yoff = (int)((track.area.height / 2) + (rect.getHeight() / 2));
+		
+		xoff += (getX() + track.area.x);
+		yoff += (getY() + track.area.y);
+		
+		// Draw text
+		g.setColor(textColor);
+		g.setFont(HELPER_FONT);
+		g.drawString(helperMessage, xoff, yoff);
+	}
+	
+	@Override
+	protected void paintInFreeSpace(Graphics g) {
+
+		super.paintInFreeSpace(g);
+
+		Shape clipBackUp = g.getClip();
+		Rectangle tmpClip = (clipBackUp != null) ? clipBackUp.getBounds() : 
+			new Rectangle(0, 0, 
+					Browser._theBrowser.getContentPane().getWidth(), 
+					Browser._theBrowser.getContentPane().getHeight());
+			
+		g.setClip(tmpClip.intersection(getBounds()));
+	
+		// Draw the name
+		String name = getName();
+		if (name == null) {
+			name = "Unnamed";
+		}
+
+		g.setFont(TrackWidgetCommons.FREESPACE_TRACKNAME_FONT);
+		g.setColor(TrackWidgetCommons.FREESPACE_TRACKNAME_TEXT_COLOR);
+		
+		// Center track name
+		FontMetrics fm   = g.getFontMetrics(TrackWidgetCommons.FREESPACE_TRACKNAME_FONT);
+		Rectangle2D rect = fm.getStringBounds(name, g);
+		
+		g.drawString(
+				name, 
+				this.getX() + (int)((getWidth() - rect.getWidth()) / 2), 
+				this.getY() + (int)((getHeight() - rect.getHeight()) / 2) + (int)rect.getHeight()
+				);
+		
+		g.setClip(clipBackUp);
+	
+
+	}
+
+	private void updatePlaybackPosition(int absFramePos) {
+
+		if (currentPlaybackFramePosition != absFramePos) {
+
+			int height = getHeight();
+			int offsetX = getX();
+			int offsetY = getY();
+			int x;
+
+			// Invalidate old pos
+			if (currentPlaybackFramePosition >= 0) {
+				x = XatFrame(currentPlaybackFramePosition);
+				
+				FrameGraphics.invalidateArea(new Rectangle(
+					x + offsetX, 
+					offsetY, 
+					SampledTrackGraphView.GRAPH_BAR_NWIDTH, // be consistent
+					height));
+			}
+
+			// Set new pos
+			currentPlaybackFramePosition = absFramePos;
+			
+			if (currentPlaybackFramePosition >= 0) {
+				
+				// Invalidate new pos
+				x = XatFrame(currentPlaybackFramePosition);
+		
+				FrameGraphics.invalidateArea(new Rectangle(
+						x + offsetX , 
+						offsetY, 
+						SampledTrackGraphView.GRAPH_BAR_NWIDTH, // be consistent
+						height));
+			}
+
+		}
+
+		
+	}
+	
+	private int XatFrame(int frame) {
+		float width = getWidth();
+		float div = totalFrameLength;
+		div = ((float)frame) / div;
+		return (int)(width * div);
+	}
+	
+	private int frameAtX(int x) {
+		float totalFrames = totalFrameLength;
+		float div = getWidth();
+		div = ((float)x) / div;
+		return (int)(totalFrames * div);
+	}
+	
+	/**
+	 * {@inheritDoc}
+	 * Once a result is received, the graph is build
+	 */
+	public void receiveResult(OverdubbedFrame odFrame, TrackGraphLoopException loopEx) {
+		
+		linkedOverdubbedFrame = odFrame;
+		
+		pendingGraphFetchCount --;
+		
+		// If there are new requests pending for rebuilding the graph then leave the state
+		// as loading graph.
+		if (pendingGraphFetchCount > 0)  {
+			linkedOverdubbedFrame = null;
+			return;
+		}
+		
+		// Reset tracks
+		tracks.clear();
+		uniqueLocalTrackCount = 0;
+		totalFrameLength = 0;
+		
+		// Only continue to build the graph iff this widget is still actaully visible
+		// If allowed the graph to build, then could consume up precious memory :(
+		if (!isVisible()) return;
+		
+		// Before any construction of a graph - the requested graph is only for the overdubbed
+		// frame that this widget is linking to. However, this linked widget may not be
+		// at all valid, that is, it might be the cause of a loop! If it is then this widget
+		// would be un-usable.
+		Frame parent = getParentFrame();
+		String parentFrameName = (parent != null) ? parent.getName() : null;
+		if (parentFrameName != null) {
+			OverdubbedFrame odframe = AudioStructureModel.getInstance().getOverdubbedFrame(parentFrameName);
+			if (odframe != null && !odframe.containsLinkedTrack(virtualFilename)) {
+				failMessage = "Bad link: contains a loop";
+				setState(FAILED_STATE);
+				return;
+			}
+		}
+
+		if (loopEx != null) {
+			failMessage = "Bad link: contains a loop";
+			setState(FAILED_STATE);
+		} else if (odFrame == null) { // In this case either the frame does not exist or is an empty OD frame
+			setState(EMPTY_STATE);
+		} else {
+			
+			// Infer total frame length
+			totalFrameLength = AudioMath.millisecondsToFrames(
+					odFrame.calculateRunningTime(), SampledAudioManager.getInstance().getDefaultPlaybackFormat());
+			
+			List<AbsoluteTrackNode> absNodes = odFrame.getAbsoluteTrackLayoutDeep(
+					masterMix.getChannelID());
+			
+			if (absNodes.isEmpty()) {
+				setState(EMPTY_STATE);
+			} else {
+				
+				while (!absNodes.isEmpty()) {
+					
+					// Group tracks in list by type so can be rendered such that they look 
+					// like they represent the same audio...
+					String localFilename = absNodes.get(0).getTrackNode().getLocalFilename();
+					
+					for (int i = 0; i < absNodes.size(); i++) {
+						AbsoluteTrackNode node = absNodes.get(i);
+						if (node.getTrackNode().getLocalFilename().equals(localFilename)) {
+							tracks.add(new Track(absNodes.remove(i)));
+							i--;
+						}
+					}
+					
+					uniqueLocalTrackCount++;
+	
+				}
+				
+				// Update the layout
+				updateGraphLayout();
+				
+				// Evaluate the state of this add set the state accordingly
+				if (MultiTrackPlaybackController.getInstance().isLoading(
+						linkedOverdubbedFrame.getFrameName(), 
+						masterMix.getChannelID())) {
+					
+					// Ensure that am receiving notifiactions:
+					List<String> loaded = MultiTrackPlaybackController.getInstance().attachLoadListener(this);
+					assert(loaded != null);
+					
+					setState(PLAYBACK_LOADING);
+
+					// Update GUI according to current load state
+					for (String ln : loaded)
+						setTrackState(ln, TRACK_LOADED);
+					
+				} else if (MultiTrackPlaybackController.getInstance().isPlaying(
+						linkedOverdubbedFrame.getFrameName(), 
+						masterMix.getChannelID())) {
+					
+					setState(PLAYING);
+					
+				} else {
+					setState(STOPPED);
+				}
+
+				
+			}
+			
+			
+			
+		}
+		
+	}
+	
+	/**
+	 * Lays out any tracks to fix the current widget size.
+	 */
+	private void updateGraphLayout() {
+		
+		if (tracks.isEmpty() || !this.isVisible() || linkedOverdubbedFrame == null)
+			return;
+		
+		long totalRunningTime = linkedOverdubbedFrame.calculateRunningTime();
+		
+		// Tracks are assumed to be ordered such that they are grouped. (If not then
+		// graph may look confusing).
+		String groupLocalFilename = tracks.get(0).getLocalFilename();
+		int currentColorIndex = 0;
+		float currentY = 0;
+		
+		// Helpers:
+		int width = getWidth();
+		int height = getHeight();
+		
+		float trackHeight = (float)height / uniqueLocalTrackCount;
+		int nTrackHeight = (int)trackHeight;
+		if (nTrackHeight == 0) nTrackHeight = 1;
+		
+		for (Track track : tracks) {
+			
+			float x = track.getABSStartTime();
+			x /= totalRunningTime;
+			
+			float trackWidth = track.getRunningTime();
+			trackWidth /= totalRunningTime;
+
+			if (!groupLocalFilename.equals(track.getLocalFilename())) {
+				currentColorIndex++;
+				if (currentColorIndex >= TRACK_COLOR_WHEEL.length) currentColorIndex = 0;
+				currentY += trackHeight;
+				groupLocalFilename = track.getLocalFilename();
+			}
+			
+			track.area = new Rectangle(
+					(int) (x * width), 
+					(int) currentY,
+					(int) (trackWidth * width),
+					nTrackHeight
+					);
+			
+			// Have some give to become selectable / viewable
+			if (track.area.width <= 1) {
+				
+				track.area.width = 2;
+				if ((track.area.x + 2) > width)
+					track.area.x = width - 2;
+			}
+
+			track.baseColor = TRACK_COLOR_WHEEL[currentColorIndex];
+
+		}
+	}
+	
+	/**
+	 * Sets the selection. Invalidates
+	 * 
+	 * @param start 
+	 * 			In frames. Clamped to be positive
+	 * @param length 
+	 * 			In frames. If less or equal to one, then the selection length is one frame.
+	 * 			Otherwise selection is ranged.
+	 */
+	public void setSelection(int start, int length) {
+		
+		if (start < 0) start = 0;
+		selectionStart = start;
+		selectionLength = length;
+		
+		invalidateSelf();
+	}
+	
+	private void setTrackState(String localFilename, int newState) {
+		assert(localFilename != null);
+		
+		for (Track track : tracks) {
+			if (localFilename.equals(track.getLocalFilename())) {
+				track.state = newState;
+			}
+		}
+		
+	}
+	
+	private void setTrackState(Track track, int newState) {
+		track.state = newState;
+	}
+
+	private static final int TRACKSTATE_READY = 1;
+	private static final int TRACKSTATE_LOADING = 2;
+	private static final int TRACKSTATE_FAILED = 3;
+	
+	
+	
+	private void updateBorderColor() {
+		
+		// Get border color currently used
+		Color oldC = getSource().getBorderColor();
+		
+		Color newC = TrackWidgetCommons.getBorderColor(
+				SoundDesk.getInstance().isSolo(masterMix.getChannelID()), 
+						masterMix.isMuted());
+		
+		// Update the color
+		if (!newC.equals(oldC)) {
+			setWidgetEdgeColor(newC);
+		}
+	}
+	
+	/**
+	 * Updates the volume GUI for all views
+	 */
+	public void updateVolume() {
+		int volume = (int)(100 * masterMix.getVolume());
+		if (playbackControlPopup != null)
+			playbackControlPopup.updateVolume(volume);
+		
+//		if (soundDeskPopup != null) 
+//			soundDeskPopup.updateVolume(volume);
+	}
+	
+
+	/**
+	 * Updates the mute button GUI for all views. 
+	 */
+	public void updateMute() {
+		if (playbackControlPopup != null)
+			playbackControlPopup.updateMute(masterMix.isMuted());
+		
+//		if (soundDeskPopup != null) 
+//			soundDeskPopup.updateMute(masterMix.isMuted());
+	}
+
+
+	/**
+	 * Updates the solo button GUI for all views. 
+	 */
+	public void updateSolo() {
+		boolean isSolo = SoundDesk.getInstance().isSolo(masterMix.getChannelID());
+		if (playbackControlPopup != null)
+			playbackControlPopup.updateSolo(isSolo);
+//		if (soundDeskPopup != null)
+//			soundDeskPopup.updateSolo(isSolo);
+		
+	}
+	
+
+	/**
+	 * The small popup for common actions.
+	 * 
+	 * @author Brook Novak
+	 *
+	 */
+	private class PlaybackPopup extends PlaybackControlPopup {
+
+		private static final long serialVersionUID = 1L;
+		
+		public PlaybackPopup() {
+			miscButton.setActionCommand("goto");
+			miscButton.setIcon(IconRepository.getIcon("goto.png"));
+			miscButton.setToolTipText("Goto linked frame");
+		}
+
+		@Override
+		public void onHide() {
+			super.onHide();
+		}
+
+		@Override
+		public void onShow() {
+			super.onShow();
+			updateVolume((int)(100 * masterMix.getVolume()));
+			updateMute(masterMix.isMuted());
+			updateSolo(SoundDesk.getInstance().isSolo(masterMix.getChannelID()));
+		}
+
+		public void actionPerformed(ActionEvent e) {
+			
+			if (e.getSource() == miscButton) {
+				
+				String absLink = LinkedTrack.this.getAbsoluteLink();
+				if (absLink != null) {
+					FrameUtils.DisplayFrame(absLink);
+				}
+				
+			} else {
+				
+				// Relay shared action
+				masterActionListener.actionPerformed(e);
+				
+			}
+		}
+
+		@Override
+		protected void volumeChanged() {
+			masterActionListener.volumeChanged(volumeSlider);
+		}
+
+		@Override
+		protected void muteChanged() {
+			masterActionListener.muteChanged(muteButton);
+		}
+
+		@Override
+		protected void soloChanged() {
+			masterActionListener.soloChanged(soloButton);
+		}
+
+	}
+
+	/**
+	 * Common actions shared by the small playback popup and the sound desk popup.
+	 * Unlike the track widget these are managed within the linked widget since they
+	 * only are visible if the track widget is in view.
+	 *  
+	 * @author Brook Novak
+	 */
+	private class MasterControlActionListener implements ActionListener {
+
+		public void actionPerformed(ActionEvent e) {
+			
+			if (!(state == PLAYING || state == STOPPED)) return;
+			
+			assert(linkedOverdubbedFrame != null);
+			
+			String actionCommand = (e.getActionCommand() == null) ? "" : e.getActionCommand();
+			
+			if (actionCommand.equals("playpause")) {
+
+				if (!MultiTrackPlaybackController.getInstance().isPlaying(
+						linkedOverdubbedFrame.getFrameName(), masterMix.getChannelID())) {
+					
+					int startFrame = -1, endFrame = -1;
+		
+					// Resume playback?
+					if (MultiTrackPlaybackController.getInstance().isMarkedAsPaused(
+							linkedOverdubbedFrame.getFrameName(), masterMix.getChannelID())) {
+						
+						startFrame = MultiTrackPlaybackController.getInstance().getLastSuspendedFrame();
+			
+						if (startFrame >= 0 && startFrame < totalFrameLength) {
+							
+							assert(selectionStart >= 0);
+			
+							// The user may have edited the audio track and reselected it
+							// since the last pause. Thus select an appropriate end frame
+							endFrame = (int)((selectionLength > 1) ?
+									selectionStart + selectionLength : totalFrameLength - 1);
+								
+							// Changed selection? it play range invalid?
+							if (endFrame <= startFrame || startFrame < selectionStart) {
+								startFrame = -1; // Play new selection (see below)
+							
+							} else if (endFrame >= totalFrameLength) {
+								endFrame = (int)totalFrameLength - 1;
+							}		
+							
+						}
+					}
+					
+					// Play from beginning of selection to end of selection
+					if (startFrame < 0) {
+						startFrame = selectionStart;
+						endFrame = (int)((selectionLength > 1) ?
+								startFrame + selectionLength:
+									totalFrameLength - 1);
+					}
+				
+					if (startFrame < endFrame) {
+						
+						MultiTrackPlaybackController.getInstance().playFrame(
+								LinkedTrack.this, 
+								linkedOverdubbedFrame.getFrameName(), 
+								masterMix.getChannelID(), 
+								false, // TODO: Set appropriatly
+								0, 
+								startFrame, 
+								endFrame);
+						
+						setState(PLAYBACK_LOADING);
+						
+					}
+
+				} else { // pause
+
+					MultiTrackPlaybackController.getInstance().setPauseMark(true);
+					MultiTrackPlaybackController.getInstance().stopPlayback();
+	
+				}
+
+				
+			} else if (actionCommand.equals("stop")) {
+				
+				if (MultiTrackPlaybackController.getInstance().isCurrentPlaybackSubject(
+						linkedOverdubbedFrame.getFrameName(), masterMix.getChannelID())) {
+					MultiTrackPlaybackController.getInstance().setPauseMark(false);
+					MultiTrackPlaybackController.getInstance().stopPlayback();
+				}
+				
+				
+			} else if (actionCommand.equals("rewind")) {
+				assert(state != PLAYING);
+
+				MultiTrackPlaybackController.getInstance().setPauseMark(false);
+				currentPlaybackFramePosition = -1;
+				setSelection(0, 0); // invalidates
+				FrameGraphics.refresh(true);
+				
+			}
+		}
+		
+		public void volumeChanged(JSlider volumeSlider) {
+			masterMix.setVolume(((float)volumeSlider.getValue()) / 100.0f);
+		}
+
+		public void muteChanged(JToggleButton muteButton) {
+			masterMix.setMuted(muteButton.isSelected());
+		}
+
+		public void soloChanged(JToggleButton soloButton) {
+			SoundDesk.getInstance().setSoloIDPrefix(soloButton.isSelected() ?
+					masterMix.getChannelID() : null
+					);
+		}
+		
+	}
+	
+	private class MouseActions implements MouseListener, MouseMotionListener {
+
+		public void mouseClicked(MouseEvent e) {
+			assert(state != EMPTY_STATE);
+			
+			if (state != PLAYING && state != STOPPED) return;
+			
+			if (nameLabel.onMouseClicked(e)) {
+				e.consume();
+				return;
+			}
+			
+		}
+
+
+		public void mouseEntered(MouseEvent e) {
+			assert(state != EMPTY_STATE);
+		}
+
+
+		public void mouseExited(MouseEvent e) {
+			assert(state != EMPTY_STATE);
+			
+		}
+
+
+		public void mousePressed(MouseEvent e) {
+			
+			assert(state != EMPTY_STATE);
+			
+			if (nameLabel.onMousePressed(e)) {
+				e.consume();
+				return;
+			}
+			
+			selectionStartX = e.getX();
+			
+		
+			// Set selection start, and length as a single frame
+			setSelection(frameAtX(selectionStartX), 1);
+			
+			FrameGraphics.refresh(true);
+		}
+
+
+		public void mouseReleased(MouseEvent e) {
+			
+
+			assert(state != EMPTY_STATE);
+			
+			
+			// Was the release a simple click
+			if (selectionStartX == e.getX() && e.getButton() == MouseEvent.BUTTON1) {
+				
+				// Traverse to the parent from of the selected track
+				String link = null;
+				for (Track track : tracks) {
+					if (track.area.contains(e.getPoint())) {
+						link = track.getParentFrameName();
+						break;
+					}
+				}
+	
+				if (link == null) link = LinkedTrack.this.getAbsoluteLink();
+				
+				assert(link != null); // should be in an empty state otherwise
+
+				FrameUtils.DisplayFrame(link);
+			}
+
+			if (nameLabel.onMouseReleased(e)) {
+				e.consume();
+				return;
+			}
+
+			
+		}
+		
+
+		public void mouseDragged(MouseEvent e) {
+			
+			assert(state != EMPTY_STATE);
+			
+			if (state != PLAYING && state != STOPPED) return;
+
+			if (nameLabel.onMouseDragged(e)) {
+				e.consume();
+				return;
+			}
+		
+			if (selectionStartX < 0) return;
+			
+			// Clamp mouse selection range
+			int width =  getWidth();
+			int x = e.getX();
+			
+			if (x > width) x = width;
+			else if (x < 0) x = 0;
+			
+			// Set selection range
+			int frameAtCursor = frameAtX(x);
+			int frameAtStartPoint = frameAtX(selectionStartX);
+			
+			int start = Math.min(frameAtCursor, frameAtStartPoint);
+			int length = Math.max(frameAtCursor, frameAtStartPoint) - start;
+			if (length > 0) {
+				setSelection(start, length);
+			}
+			
+			FrameGraphics.refresh(true);
+		}
+
+		public void mouseMoved(MouseEvent e) {
+			assert(state != EMPTY_STATE);
+			
+			if (state != PLAYING && state != STOPPED) return;
+
+			if (nameLabel.onMouseMoved(e, _swingComponent)) {
+				e.consume();
+				return;
+			}
+
+			if (playbackControlPopup == null) {
+				playbackControlPopup = new PlaybackPopup();
+			} 
+
+			// Only show popup iff there is nothing expanded / expanding.
+			if (!PopupManager.getInstance().isShowing(playbackControlPopup) &&
+					!ExpandedTrackManager.getInstance().doesExpandedTrackExist()) {	
+				
+				// Get rid of all popups
+				PopupManager.getInstance().hideAutohidePopups();
+				
+				Rectangle animationSource = _swingComponent.getBounds();
+				
+				// Determine where popup should show
+				int x = LinkedTrack.this.getX();
+				int y = LinkedTrack.this.getY() - playbackControlPopup.getHeight() - 2; // by default show above
+				
+				// I get sick.dizzy from the popup expanding from the whole thing...
+				animationSource.height = 1;
+				animationSource.width = Math.min(animationSource.width, playbackControlPopup.getWidth());
+				
+				if (y < 0) {
+					y = LinkedTrack.this.getY() + LinkedTrack.this.getHeight() + 2;
+					animationSource.y = y - 2;
+				} 
+
+				// Animate the popup
+				PopupManager.getInstance().showPopup(
+						playbackControlPopup, 
+						new Point(x, y), 
+						_swingComponent,
+						PopupManager.getInstance().new ExpandShrinkAnimator(
+								animationSource,
+								Color.LIGHT_GRAY));
+				
+				PopupReaper.getInstance().initPopupLifetime(
+						playbackControlPopup, 
+						PopupManager.getInstance().new ExpandShrinkAnimator(
+								animationSource,
+								Color.LIGHT_GRAY), 
+								TrackWidgetCommons.POPUP_LIFETIME);
+				
+			} else {
+				PopupReaper.getInstance().revivePopup(playbackControlPopup, TrackWidgetCommons.POPUP_LIFETIME);
+			}
+			
+
+			// Handle selection / invalidation of mouse hovering over track segments
+			boolean isOneSelected = false;
+			boolean shouldInvalidate = false;
+			for (Track track : tracks) {
+				
+				if (isOneSelected && track.isSelected) {
+					track.isSelected = false;
+					shouldInvalidate = true;
+				} else if (!isOneSelected) {
+					isOneSelected = track.area.contains(e.getPoint());
+					shouldInvalidate |= (isOneSelected != track.isSelected);
+					track.isSelected = isOneSelected;
+				}
+	
+			}
+			
+			if (shouldInvalidate) {
+				invalidateSelf();
+				FrameGraphics.refresh(true);
+			}
+		}
+	}
+	
+
+	/**
+	 * A wrapper for {@link AbsoluteTrackNode} - including graphical representation data.
+	 * @author Brook Novak
+	 */
+	private class Track {
+		
+		private AbsoluteTrackNode absNode; // immutable. Never null
+		
+		private Rectangle area;
+		
+		private Color baseColor;
+		
+		boolean isSelected = false;
+		
+		private int state = TRACKSTATE_READY;
+		
+		Track(AbsoluteTrackNode absNode) {
+			assert(absNode != null);
+			this.absNode = absNode;
+			
+			// This is a shame... but due to time restictrictions must limit the system to
+			// only point to the local mix. I think its a bit overkill as this stage :(
+			SoundDesk.getInstance().setUseLocalTrackMix(absNode.getChannelID(), true);
+
+			
+		}
+		
+		public String getChannelID() {
+			return absNode.getChannelID();
+		}
+		
+		public long getABSStartTime() {
+			return absNode.getABSStartTime();
+		}
+		
+		public String getLocalFilename() {
+			return absNode.getTrackNode().getLocalFilename();
+		}
+
+		public long getRunningTime() {
+			return absNode.getTrackNode().getRunningTime();
+		}
+		
+		public String getParentFrameName() {
+			return absNode.getParentFrameName();
+		}
+		
+		public String getTrackName() {
+			return absNode.getTrackNode().getName();
+		}
+		
+		public void paintTrackArea(Graphics2D g, int x, int y) {
+			
+			if (state == TRACKSTATE_LOADING && LinkedTrack.this.state == PLAYBACK_LOADING) {
+				g.setColor(TRACK_LOAD_COLOR);
+			} else if (state == TRACKSTATE_FAILED) {
+				g.setColor(TRACK_FAIL_COLOR);
+			} else {
+				g.setColor(baseColor);
+			}
+			
+			Paint restore = g.getPaint();
+			if (ApolloSystem.useQualityGraphics) {
+
+				GradientPaint gp = new GradientPaint(
+						(int) (area.x + x + (area.width / 2)), area.y + y + (int)(area.height * 0.8), baseColor,
+						(int) (area.x + x + (area.width / 2)), area.y + y, TRACK_SELECTED_BORDER_COLOR);
+				g.setPaint(gp);
+			}
+			
+			// Draw a filled rect - represented a track in the heirarchy
+			g.fillRect(area.x + x, area.y + y, area.width, area.height);
+			
+			if (ApolloSystem.useQualityGraphics) {
+				g.setPaint(restore);
+			}
+
+		}
+		
+		public void paintTrackBorder(Graphics g, int x, int y) {
+			
+			if (isSelected) {
+				g.setColor(TRACK_SELECTED_BORDER_COLOR);
+				((Graphics2D)g).setStroke(TRACK_SELECTED_BORDER);
+			} else {
+				g.setColor(TRACK_BORDER_COLOR);
+				((Graphics2D)g).setStroke(TRACK_BORDER); // todo muted colors etc...
+			}
+			
+			g.drawRect(area.x + x, area.y + y, area.width, area.height);
+		}
+		
+	}
+	
+
+}
Index: /trunk/src_apollo/org/apollo/widgets/MasterVolume.java
===================================================================
--- /trunk/src_apollo/org/apollo/widgets/MasterVolume.java	(revision 315)
+++ /trunk/src_apollo/org/apollo/widgets/MasterVolume.java	(revision 315)
@@ -0,0 +1,103 @@
+package org.apollo.widgets;
+
+import javax.swing.event.ChangeEvent;
+import javax.swing.event.ChangeListener;
+
+import org.apollo.audio.ApolloPlaybackMixer;
+import org.apollo.audio.ApolloSubjectChangedEvent;
+import org.apollo.gui.VolumeControlButton;
+import org.apollo.mvc.Observer;
+import org.apollo.mvc.Subject;
+import org.apollo.mvc.SubjectChangedEvent;
+import org.expeditee.items.ItemParentStateChangedEvent;
+import org.expeditee.items.Text;
+import org.expeditee.items.widgets.InteractiveWidget;
+
+/**
+ * Provides master volume control
+ * 
+ * @author Brook Novak
+ *
+ */
+public class MasterVolume extends InteractiveWidget implements Observer {
+	
+	private VolumeControlButton vcButton;
+
+	public MasterVolume(Text source, String[] args) {
+		super(source, new VolumeControlButton("Master Volume"), 40, 40, 40, 40);
+		vcButton = (VolumeControlButton)super._swingComponent;
+		
+		vcButton.addVolumeChangeListener(new ChangeListener() {
+			public void stateChanged(ChangeEvent e) {
+				float f = (float)vcButton.getVolumeValue();
+				f /= 100.0f;
+				ApolloPlaybackMixer.getInstance().setMasterVolume(f);
+				if (f == 0.0f) { // auto-mute
+					ApolloPlaybackMixer.getInstance().setMasterMute(true);
+					// this will raise a mute event and thus will change the button to muted state
+				} else { // auto-un-mute
+					ApolloPlaybackMixer.getInstance().setMasterMute(false);
+				}
+			}
+		});
+		
+		
+		vcButton.setVolumeValue(
+				(int)(ApolloPlaybackMixer.getInstance().getMasterVolume() * 100.0f)
+				);
+		vcButton.setMuted(ApolloPlaybackMixer.getInstance().isMasterMuteOn());
+
+	}
+	
+	@Override
+	protected String[] getArgs() {
+		return null;
+	}
+	
+	public Subject getObservedSubject() {
+		return ApolloPlaybackMixer.getInstance();
+	}
+
+	
+	@Override
+	protected void onParentStateChanged(int eventType) {
+		super.onParentStateChanged(eventType);
+		
+		switch (eventType) {
+
+		case ItemParentStateChangedEvent.EVENT_TYPE_REMOVED:
+		case ItemParentStateChangedEvent.EVENT_TYPE_REMOVED_VIA_OVERLAY:
+		case ItemParentStateChangedEvent.EVENT_TYPE_HIDDEN:
+			ApolloPlaybackMixer.getInstance().removeObserver(this);
+			break;
+
+		case ItemParentStateChangedEvent.EVENT_TYPE_ADDED:
+		case ItemParentStateChangedEvent.EVENT_TYPE_ADDED_VIA_OVERLAY:
+		case ItemParentStateChangedEvent.EVENT_TYPE_SHOWN:
+		case ItemParentStateChangedEvent.EVENT_TYPE_SHOWN_VIA_OVERLAY:
+			ApolloPlaybackMixer.getInstance().addObserver(this);
+			break;
+
+		}
+	}
+
+	public void modelChanged(Subject source, SubjectChangedEvent event) {
+
+		if (event.getID() == ApolloSubjectChangedEvent.VOLUME) {
+			
+			vcButton.setVolumeValue(
+					(int)(ApolloPlaybackMixer.getInstance().getMasterVolume() * 100.0f)
+					);
+			
+		} else if (event.getID() == ApolloSubjectChangedEvent.MUTE) {
+
+			vcButton.setMuted(ApolloPlaybackMixer.getInstance().isMasterMuteOn());
+
+		} 
+		
+	}
+
+	public void setObservedSubject(Subject parent) {
+	}
+
+}
Index: /trunk/src_apollo/org/apollo/widgets/SampleRecorder.java
===================================================================
--- /trunk/src_apollo/org/apollo/widgets/SampleRecorder.java	(revision 315)
+++ /trunk/src_apollo/org/apollo/widgets/SampleRecorder.java	(revision 315)
@@ -0,0 +1,916 @@
+package org.apollo.widgets;
+
+import java.awt.Color;
+import java.awt.Dimension;
+import java.awt.FontMetrics;
+import java.awt.Graphics;
+import java.awt.Graphics2D;
+import java.awt.GridBagConstraints;
+import java.awt.GridBagLayout;
+import java.awt.Rectangle;
+import java.awt.event.ActionEvent;
+import java.awt.event.ActionListener;
+import java.awt.geom.AffineTransform;
+import java.awt.geom.Rectangle2D;
+import java.awt.image.BufferedImage;
+import java.io.ByteArrayOutputStream;
+import java.io.IOException;
+import java.io.PipedInputStream;
+
+import javax.sound.sampled.AudioFormat;
+import javax.sound.sampled.LineUnavailableException;
+import javax.swing.JButton;
+import javax.swing.JLabel;
+import javax.swing.JPanel;
+import javax.swing.JSpinner;
+import javax.swing.SpinnerModel;
+import javax.swing.SpinnerNumberModel;
+import javax.swing.SwingUtilities;
+
+import org.apollo.audio.ApolloSubjectChangedEvent;
+import org.apollo.audio.AudioCapturePipe;
+import org.apollo.audio.RecordManager;
+import org.apollo.audio.SampledAudioManager;
+import org.apollo.audio.util.FrameLayoutDaemon;
+import org.apollo.audio.util.MultiTrackPlaybackController;
+import org.apollo.audio.util.SoundDesk;
+import org.apollo.audio.util.Timeline;
+import org.apollo.audio.util.MultiTrackPlaybackController.MultitrackLoadListener;
+import org.apollo.gui.PeakTroughWaveFormRenderer;
+import org.apollo.gui.WaveFormRenderer;
+import org.apollo.io.IconRepository;
+import org.apollo.mvc.Observer;
+import org.apollo.mvc.Subject;
+import org.apollo.mvc.SubjectChangedEvent;
+import org.apollo.util.AudioMath;
+import org.apollo.util.AudioSystemLog;
+import org.apollo.util.TrackNameCreator;
+import org.expeditee.gui.DisplayIO;
+import org.expeditee.gui.Frame;
+import org.expeditee.gui.FrameGraphics;
+import org.expeditee.items.ItemParentStateChangedEvent;
+import org.expeditee.items.Text;
+import org.expeditee.items.widgets.InteractiveWidget;
+
+public class SampleRecorder extends InteractiveWidget 
+	implements ActionListener, Observer, MultitrackLoadListener {
+	
+	private enum WidgetState {
+		Ready, // Waiting to record
+		CountingDown,
+		LoadingPlayback,
+		Recording, // ...
+		Finalizing, // Capture stopped, reading last bytes from pipe
+		Finished // Removed from expeditee world, spawned audio track widget in place of...
+	}
+
+	private Subject observeredSubject = null;
+	
+	private boolean shouldPlayback = false;
+	private WidgetState state = null;
+	private long recordStartTime = 0; // system time
+	private RecordedByteReader audioByteReader = null;
+	private CountDownTimer countdownThread = null;
+	private String playbackFrameName;
+	private long initiationTime = -1;
+	
+	private JButton recordButton;
+	private JButton recordSynchedButton; // record while playing
+	private JButton stopButton;
+	private JSpinner countDownSpinner;
+	private JLabel countDownSpinnerLabel;
+	private JLabel statusLabel;
+	
+	private AnimatedSampleGraph sampleGraph;
+	private boolean hasExplicityStopped = false;
+	
+	private final static int BUTTON_HEIGHT = 50;
+	private final static int LABEL_HEIGHT = 30;
+	private final static int COUNTDOWN_SETTINGS_HEIGHT = 30;
+	private final static int HORO_SPACING = 2;
+	private final static int VERT_SPACING = 0;
+	
+	private final static int MAX_COUNTDOWN_TIME = 60;
+	
+	private static final Color GRAPH_BACKCOLOR = Color.BLACK;
+	private static final Color GRAPH_WAVECOLOR = Color.GREEN;
+	
+	private final static int RENDER_POINTS_PER_SECOND = 20; // how many points to render each second
+	
+	public SampleRecorder(Text source, String[] args) {
+		super(source, new JPanel(new GridBagLayout()),
+				AnimatedSampleGraph.GRAPH_WIDTH + (2 * HORO_SPACING), 
+				AnimatedSampleGraph.GRAPH_WIDTH + (2 * HORO_SPACING),
+				COUNTDOWN_SETTINGS_HEIGHT + BUTTON_HEIGHT + LABEL_HEIGHT + AnimatedSampleGraph.GRAPH_HEIGHT + (4 * VERT_SPACING), 
+				COUNTDOWN_SETTINGS_HEIGHT + BUTTON_HEIGHT + LABEL_HEIGHT + AnimatedSampleGraph.GRAPH_HEIGHT + (4 * VERT_SPACING));
+		
+		// Create gui layout
+		recordButton = new JButton();
+		recordButton.setIcon(IconRepository.getIcon("record.png"));
+		recordButton.addActionListener(this);
+		recordButton.setPreferredSize(new Dimension(AnimatedSampleGraph.GRAPH_WIDTH / 2, BUTTON_HEIGHT));
+		
+		recordSynchedButton = new JButton();
+		recordSynchedButton.setIcon(IconRepository.getIcon("recordtime.png"));
+		recordSynchedButton.addActionListener(this);
+		recordSynchedButton.setPreferredSize(new Dimension(AnimatedSampleGraph.GRAPH_WIDTH / 2, BUTTON_HEIGHT));
+
+		stopButton = new JButton();
+		stopButton.setIcon(IconRepository.getIcon("stop.png"));
+		stopButton.addActionListener(this);
+		stopButton.setPreferredSize(new Dimension(AnimatedSampleGraph.GRAPH_WIDTH, BUTTON_HEIGHT));
+		stopButton.setVisible(false);
+
+		sampleGraph = new AnimatedSampleGraph();
+		
+		statusLabel = new JLabel();
+		statusLabel.setPreferredSize(new Dimension(AnimatedSampleGraph.GRAPH_WIDTH, LABEL_HEIGHT));
+		statusLabel.setHorizontalAlignment(JLabel.CENTER);
+		statusLabel.setVerticalAlignment(JLabel.CENTER);
+		
+		SpinnerModel model =
+	        new SpinnerNumberModel(0, 0, MAX_COUNTDOWN_TIME, 1);
+		countDownSpinner = new JSpinner(model);
+		countDownSpinner.setPreferredSize(new Dimension(50, COUNTDOWN_SETTINGS_HEIGHT));
+		
+		countDownSpinnerLabel = new JLabel("Count down:");
+		countDownSpinnerLabel.setPreferredSize(new Dimension(AnimatedSampleGraph.GRAPH_WIDTH - 50, COUNTDOWN_SETTINGS_HEIGHT));
+
+		// Layout GUI
+		GridBagConstraints c;
+		
+		JPanel countdownPane = new JPanel(new GridBagLayout());
+		countdownPane.setPreferredSize(new Dimension(AnimatedSampleGraph.GRAPH_WIDTH, COUNTDOWN_SETTINGS_HEIGHT));
+		
+		c = new GridBagConstraints();
+		c.gridx = 0;
+		c.gridy = 0;
+		c.weightx = 1.0f;
+		c.fill = GridBagConstraints.BOTH;
+		countdownPane.add(countDownSpinnerLabel, c);
+		
+		c = new GridBagConstraints();
+		c.gridx = 1;
+		c.gridy = 0;
+		c.fill = GridBagConstraints.BOTH;
+		countdownPane.add(countDownSpinner, c);
+		
+		JPanel buttonPane = new JPanel(new GridBagLayout());
+		buttonPane.setPreferredSize(new Dimension(AnimatedSampleGraph.GRAPH_WIDTH, BUTTON_HEIGHT));
+
+		c = new GridBagConstraints();
+		c.gridx = 0;
+		c.gridy = 0;
+		c.fill = GridBagConstraints.BOTH;
+		buttonPane.add(recordButton, c);
+		
+		c = new GridBagConstraints();
+		c.gridx = 1;
+		c.gridy = 0;
+		c.fill = GridBagConstraints.BOTH;
+		buttonPane.add(recordSynchedButton, c);
+		
+		c = new GridBagConstraints();
+		c.gridx = 0;
+		c.gridy = 0;
+		c.gridwidth = 2;
+		c.fill = GridBagConstraints.BOTH;
+		buttonPane.add(stopButton, c);
+
+		
+		// Assemble
+		
+		c = new GridBagConstraints();
+		c.gridx = 0;
+		c.gridy = 0;
+		c.fill = GridBagConstraints.BOTH;
+		_swingComponent.add(buttonPane, c);
+		
+		c = new GridBagConstraints();
+		c.gridx = 0;
+		c.gridy = 1;
+		c.fill = GridBagConstraints.BOTH;
+		_swingComponent.add(countdownPane, c);
+		
+		c = new GridBagConstraints();
+		c.gridx = 0;
+		c.gridy = 2;
+		c.fill = GridBagConstraints.BOTH;
+		_swingComponent.add(sampleGraph, c);
+		
+		c = new GridBagConstraints();
+		c.gridx = 0;
+		c.gridy = 3;
+		c.fill = GridBagConstraints.BOTH;
+		_swingComponent.add(statusLabel, c);
+		
+		_swingComponent.doLayout();
+
+		setState(WidgetState.Ready, "Ready");
+	}
+	
+	/**
+	 * All of the widget logic is centarlized here - according to the new state transition
+	 * Must be in AWT thread
+	 * 
+	 * @param newState
+	 */
+	private void setState(WidgetState newState, String status) {
+		if (state == newState) return;
+		WidgetState oldState = state;
+		state = newState;
+		
+		sampleGraph.alternateText = null;
+		sampleGraph.invalidateGraph();
+		
+		statusLabel.setText(status);
+
+		if (newState == WidgetState.Ready) {
+
+			recordButton.setVisible(true);
+			recordSynchedButton.setVisible(true);
+			stopButton.setVisible(false);
+			
+			if (oldState != null) sampleGraph.clear();
+			
+		} else if (newState == WidgetState.CountingDown) {
+			assert(oldState == WidgetState.Ready);
+			
+			assert (countdownThread == null || !countdownThread.isAlive());
+			
+			int countDown = (Integer)countDownSpinner.getValue();
+			
+			if (countDown == 0) {
+				commenceRecording(shouldPlayback);
+			} else {
+
+				recordButton.setVisible(false);
+				recordSynchedButton.setVisible(false);
+				stopButton.setVisible(true);
+				stopButton.setEnabled(true);
+	
+				sampleGraph.alternateText = Integer.toString(countDown);
+				sampleGraph.alternateTextColor = (countDown > 3) ? Color.WHITE : Color.RED;
+				sampleGraph.invalidateGraph();
+				FrameGraphics.refresh(true);
+				
+				countdownThread = new CountDownTimer(countDown);
+				countdownThread.start();
+			}
+			
+		} else if (newState == WidgetState.LoadingPlayback) {
+			assert(oldState == WidgetState.Ready || oldState == WidgetState.CountingDown);
+			
+			recordButton.setVisible(false);
+			recordSynchedButton.setVisible(false);
+			stopButton.setVisible(true);
+			stopButton.setEnabled(false);
+			
+			// TODO: Cancel load on users demand.
+
+			
+		} else if (newState == WidgetState.Recording) {
+			assert(oldState == WidgetState.Ready || 
+					oldState == WidgetState.CountingDown || 
+					oldState == WidgetState.LoadingPlayback);
+			
+			hasExplicityStopped = false;
+			
+			recordButton.setVisible(false);
+			recordSynchedButton.setVisible(false);
+			stopButton.setVisible(true);
+			stopButton.setEnabled(true);
+
+			setStatusLabelToRecordTime();
+			
+		} else if (newState == WidgetState.Finalizing) { // wait for pipe to finish reading bytes
+			
+			// This state can be skipped if pipe has finished before
+			// stop event captured.
+			
+			recordButton.setVisible(false);
+			recordSynchedButton.setVisible(false);
+			stopButton.setVisible(true);
+			stopButton.setEnabled(false);
+			
+			// Thread reading from pipe will finish and trigger the finished
+			
+		} else if (newState == WidgetState.Finished) {
+			
+			// The widget could have been removed while recording or finializing.
+			// In such cases then do not do anything as the user has suggested
+			// that the want away with it all...
+			if (!hasExplicityStopped) {
+				
+				// Reset the state
+				setState(WidgetState.Ready, "Ready"); 
+				
+			} else {
+
+				// Remove this temporary widget
+				removeSelf();
+			
+				// Spawn an audio track using the actual bytes and audio format buffered from
+				// the pipe. This will load instantly, saving of bytes is its responsibility.
+				 if (audioByteReader != null && audioByteReader.bufferedAudioBytes != null
+						 && audioByteReader.bufferedAudioBytes.size() > 0) {
+					 
+					 // Get frame to anchor track to
+					 Frame targetFrame = getParentFrame();
+					 if (targetFrame == null) {
+						 targetFrame = DisplayIO.getCurrentFrame();
+					 }
+					 
+					 assert(targetFrame != null);
+					 
+					 if (!shouldPlayback) initiationTime = -1;
+					 
+					 SampledTrackEvo4 trackWidget = SampledTrackEvo4.createFromMemory(
+							 audioByteReader.bufferedAudioBytes.toByteArray(), 
+							 audioByteReader.audioFormat, 
+							 targetFrame, 
+							 getX(), 
+							 getY(),
+							 initiationTime, 
+							 TrackNameCreator.getNameCopy(targetFrame.getTitle() + "_"),
+							 null);
+					 
+					 targetFrame.addAllItems(trackWidget.getItems());
+	
+					 
+				 }
+			}
+			
+		}
+		
+		// Ensure that if not recording or loading playback then stop/cancel multiplaybackl controller events
+		// that are commenced via this widget.
+		if (
+				newState != WidgetState.LoadingPlayback && newState != WidgetState.Recording
+				&& (oldState == WidgetState.LoadingPlayback || oldState == WidgetState.Recording)
+				&& playbackFrameName != null && shouldPlayback
+				&& MultiTrackPlaybackController.getInstance().isCurrentPlaybackSubject(
+						playbackFrameName, 
+						FramePlayer.FRAME_PLAYERMASTER_CHANNEL_ID)) {
+			
+			MultiTrackPlaybackController.getInstance().cancelLoad(
+				playbackFrameName, 
+				FramePlayer.FRAME_PLAYERMASTER_CHANNEL_ID);
+			
+			MultiTrackPlaybackController.getInstance().stopPlayback();
+		}
+
+	}
+	
+	private void setStatusLabelToRecordTime() {
+		long elapsed = (System.currentTimeMillis() - recordStartTime);
+		elapsed /= 1000;
+		//statusLabel.setText("Recorded " + elapsed + " seconds");
+		statusLabel.setText(elapsed + " seconds");
+	}
+
+	/**
+	 * {@inheritDoc} 
+	 * SampleRecorderWidget is really stateless - they are means to capture audio only...
+	 * That is, temporary.
+	 */
+	@Override
+	protected String[] getArgs() {
+		return null;
+	}
+	
+	@Override
+	protected void onParentStateChanged(int eventType) {
+		super.onParentStateChanged(eventType);
+
+		switch (eventType) {
+		case ItemParentStateChangedEvent.EVENT_TYPE_HIDDEN:
+		case ItemParentStateChangedEvent.EVENT_TYPE_REMOVED:
+		case ItemParentStateChangedEvent.EVENT_TYPE_REMOVED_VIA_OVERLAY:
+
+			if (state == WidgetState.Recording) {
+				// This will change the state of this widget
+				RecordManager.getInstance().stopCapturing();
+				
+			} else if (state == WidgetState.CountingDown) {
+				countdownThread.abortCountdown();
+				
+			} else if (state == WidgetState.LoadingPlayback && playbackFrameName != null) {
+				MultiTrackPlaybackController.getInstance().cancelLoad(
+						playbackFrameName, 
+						FramePlayer.FRAME_PLAYERMASTER_CHANNEL_ID);
+			}
+			
+			// Ensure that this can be disposed
+			RecordManager.getInstance().removeObserver(this);
+			MultiTrackPlaybackController.getInstance().removeObserver(this);
+			break;
+
+		case ItemParentStateChangedEvent.EVENT_TYPE_ADDED:
+		case ItemParentStateChangedEvent.EVENT_TYPE_ADDED_VIA_OVERLAY:
+		case ItemParentStateChangedEvent.EVENT_TYPE_SHOWN:
+		case ItemParentStateChangedEvent.EVENT_TYPE_SHOWN_VIA_OVERLAY:
+			RecordManager.getInstance().addObserver(this);
+			MultiTrackPlaybackController.getInstance().addObserver(this);
+			break;
+			
+		}
+
+	}
+
+	public Subject getObservedSubject() {
+		return observeredSubject;
+	}
+
+	public void setObservedSubject(Subject parent) {
+		observeredSubject = parent;
+	}
+
+	public void modelChanged(Subject source, SubjectChangedEvent event) { // subject messages
+
+		switch (event.getID()) {
+		
+		case ApolloSubjectChangedEvent.CAPTURE_STARTED:
+			if (event.getState() == this) {
+				// Change to recording state
+				recordStartTime = System.currentTimeMillis();
+				setState(WidgetState.Recording, "Record started");
+			}
+			break;
+
+			
+		case ApolloSubjectChangedEvent.CAPTURE_STOPPED:
+			
+			if (state == WidgetState.Recording) {
+				
+			//	assert (event.getState() == this);
+				setState(WidgetState.Finalizing, "finalizing");
+			}
+			break;
+			
+		case ApolloSubjectChangedEvent.PLAYBACK_STARTED: // from multi playback controller
+
+			if (state == WidgetState.LoadingPlayback && playbackFrameName != null &&
+					MultiTrackPlaybackController.getInstance().isCurrentPlaybackSubject(
+					playbackFrameName, FramePlayer.FRAME_PLAYERMASTER_CHANNEL_ID)) {
+				commenceRecording(false);
+			}
+			break;
+
+		}
+		
+	}
+
+
+	public void multiplaybackLoadStatusUpdate(int id, Object state) {
+	
+		if (state != WidgetState.LoadingPlayback) return;
+		String abortMessage = null;
+		
+		switch(id) {
+		case MultitrackLoadListener.LOAD_CANCELLED:
+			abortMessage = "Playback/Record cancelled"; 
+			break;
+		case MultitrackLoadListener.LOAD_COMPLETE:
+			break;
+		case MultitrackLoadListener.LOAD_FAILED_BAD_GRAPH:
+			abortMessage = "Graph contains loops";
+			((Exception)state).printStackTrace();
+			break;
+		case MultitrackLoadListener.LOAD_FAILED_GENERIC:
+			abortMessage = "Unexpected error";
+			((Exception)state).printStackTrace();
+			break;
+		case MultitrackLoadListener.LOAD_FAILED_PLAYBACK:
+			abortMessage = "Graph contains loops";
+			break;
+		case MultitrackLoadListener.NOTHING_TO_PLAY:
+			abortMessage = "Nothing to play"; // could be due to user slecting empty space
+			break;
+		case MultitrackLoadListener.TRACK_LOAD_FAILED_IO:
+			// This is special... the loader does not abort... and it tries to load more.
+			((Exception)state).printStackTrace();
+			break;
+		case MultitrackLoadListener.TRACK_LOADED:
+			break;
+
+		}
+		
+		if (abortMessage != null) {
+			AudioSystemLog.println("Aborted playback - " + abortMessage);
+			setState(WidgetState.Ready, abortMessage);
+		}
+
+	}
+
+	public void actionPerformed(ActionEvent e) { // For action button
+		
+		
+		if (e.getSource() == stopButton) {
+			assert (state == WidgetState.Recording || state == WidgetState.CountingDown);
+			
+			if (state == WidgetState.CountingDown) {
+				assert (countdownThread != null);
+				countdownThread.abortCountdown();
+			} else {
+				hasExplicityStopped = true;
+				RecordManager.getInstance().stopCapturing();
+			}
+			
+		} else if (e.getSource() == recordButton || e.getSource() == recordSynchedButton) {
+			assert (state == WidgetState.Ready);
+			shouldPlayback = (e.getSource() == recordSynchedButton);
+			setState(WidgetState.CountingDown, "Counting down...");
+		}
+
+
+	}
+	
+	/**
+	 * Reads bytes asynchronously from a given pipe until it is finished or an exception occurs.
+	 * Once finished excecution, the widget state will be set to finished.
+	 * 
+	 * The bytes read are buffered, rendered and sent to the AnimatedSampleGraph for drawing.
+	 * The status label is also updated according to the record time. 
+	 * 
+	 * @author Brook Novak
+	 *
+	 */
+	private class RecordedByteReader extends Thread {
+		
+		private PipedInputStream pin;
+		private AudioFormat audioFormat;
+		private int bufferSize;
+		private int aggregationSize;
+		public ByteArrayOutputStream bufferedAudioBytes = null; // not null if started
+		private DoUpdateLabel updateLabelTask = new DoUpdateLabel();
+		private WaveFormRenderer waveFormRenderer = null;
+		
+		RecordedByteReader(AudioCapturePipe pipe)  {
+			assert(pipe != null);
+			this.pin = pipe.getPin();
+			this.audioFormat = pipe.getAudioFormat();
+			this.bufferSize = pipe.getBufferSize();
+			waveFormRenderer = new PeakTroughWaveFormRenderer(audioFormat);
+			
+			// Aggregate size depends on samplerate
+			aggregationSize = (int)(audioFormat.getFrameRate() / RENDER_POINTS_PER_SECOND);
+		}
+
+		@Override
+		public void run() {
+
+			bufferedAudioBytes = new ByteArrayOutputStream();
+			byte[] buffer = new byte[bufferSize];
+			int len = 0;
+			int bytesBuffered;
+			
+			int lastUpdateStateTime = 0;
+
+			try {
+				while(true) {
+					
+					bytesBuffered = 0;
+					
+					while (bytesBuffered < buffer.length) { // Full the buffer (unless reaches uneven end)
+						len = pin.read(buffer, bytesBuffered, buffer.length - bytesBuffered);
+						if (len == -1) break; // done
+						bytesBuffered += len; 
+					}
+
+					if (bytesBuffered > 0) {
+
+						// Buffer bytes
+						bufferedAudioBytes.write(buffer, 0, bytesBuffered);
+						
+						// Render bytes
+						float[] waveforms = waveFormRenderer.getSampleAmplitudes(
+								buffer, 
+								0, 
+								bytesBuffered / audioFormat.getFrameSize(), 
+								aggregationSize);
+							
+						// Send renderings to graph for drawing
+						SwingUtilities.invokeLater(new DoRenderGraph(waveforms));
+
+					}
+					
+					if (len == -1) break; // done
+					
+					// For every second elapsed, update status
+					if ((System.currentTimeMillis() - lastUpdateStateTime) >= 1000) {
+						SwingUtilities.invokeLater(updateLabelTask);
+					}
+				}
+			} catch (IOException e) {
+				e.printStackTrace();
+				
+			} finally {
+				
+				try {
+					pin.close();
+				} catch (IOException e) {
+					e.printStackTrace();
+				}
+				
+				try {
+					bufferedAudioBytes.close();
+				} catch (IOException e) {
+					e.printStackTrace();
+				}
+				
+				// Ensure that will enter finalized state
+				SwingUtilities.invokeLater(new DoFinished());
+				
+			}
+
+		}
+	}
+	
+	class DoFinished implements Runnable {
+		public void run() {
+			setState(WidgetState.Finished, "Finished");
+		}
+	}
+
+	class DoUpdateLabel implements Runnable {
+		public void run() {
+			setStatusLabelToRecordTime();
+		}
+	}
+	
+	class DoRenderGraph implements Runnable {
+		private float[] waveForms;
+		public DoRenderGraph(float[] waveForms) {
+			this.waveForms = waveForms;
+		}
+		public void run() {
+			assert (audioByteReader != null);
+			sampleGraph.updateVisualization(waveForms);
+		}
+	}
+	
+	/**
+	 * The state will change eventually (or instantly is failed)
+	 * 
+	 * @param withPlayback
+	 * 		True to commence frame and enter in a loading state.
+	 */
+	private void commenceRecording(boolean withPlayback) {
+		
+		if (withPlayback) {
+			
+			assert (state != WidgetState.LoadingPlayback);
+			assert (state != WidgetState.Recording);
+			
+			setState(WidgetState.LoadingPlayback, "Loading tracks...");
+			
+			Frame currentFrame = DisplayIO.getCurrentFrame();
+			if (currentFrame == null || currentFrame.getName() == null) {
+				
+				setState(WidgetState.Ready, "No frame to play");
+				return;
+			}
+			
+			playbackFrameName = currentFrame.getName();
+			
+
+			Timeline tl = FrameLayoutDaemon.getInstance().getLastComputedTimeline();
+			
+			if (tl == null || FrameLayoutDaemon.getInstance().getTimelineOwner() == null ||
+					FrameLayoutDaemon.getInstance().getTimelineOwner() != currentFrame) {
+				tl = FrameLayoutDaemon.inferTimeline(currentFrame);
+			}
+			
+			if (tl != null) {
+		
+				initiationTime = tl.getMSTimeAtX(getX());
+				
+				// Clamp
+				if (initiationTime < tl.getFirstInitiationTime())
+					initiationTime = tl.getFirstInitiationTime();
+				
+				else if (initiationTime > (tl.getFirstInitiationTime() + tl.getRunningTime())) 
+					initiationTime  = tl.getFirstInitiationTime() + tl.getRunningTime();
+
+				int startFrame = AudioMath.millisecondsToFrames(
+						initiationTime - tl.getFirstInitiationTime(),  
+						SampledAudioManager.getInstance().getDefaultPlaybackFormat());
+				
+				// To ensure that the frame channels are not in use
+				MultiTrackPlaybackController.getInstance().stopPlayback();
+				SoundDesk.getInstance().freeChannels(FramePlayer.FRAME_PLAYERMASTER_CHANNEL_ID);
+
+				FramePlayer.playFrame(
+					this, 
+					currentFrame.getName(), 
+					false, 
+					0, 
+					startFrame, 
+					Integer.MAX_VALUE);
+				
+			} else {
+				commenceRecording(false); // without playback
+				return;
+			}
+			
+		} else {
+	
+			try {
+				
+				// Start capturing
+				AudioCapturePipe pipe = RecordManager.getInstance().captureAudio(this);
+				
+				// Read bytes asynchronously ... buffer and render visual feedback
+				audioByteReader = new RecordedByteReader(pipe);
+				audioByteReader.start();
+				
+			} catch (LineUnavailableException ex) {
+				AudioSystemLog.printException("Failed to commence audio capture via record widget", ex);
+				setState(WidgetState.Ready, "Bad Device");
+			} catch (IOException ex) {
+				AudioSystemLog.printException("Failed to commence audio capture via record widget", ex);
+				setState(WidgetState.Ready, "Failed");
+			}
+		}
+	}
+	
+	/**
+	 * Renders a waveform to a panel at realtime
+	 */
+	private class AnimatedSampleGraph extends JPanel {
+		
+		protected static final long serialVersionUID = 0L;
+		
+		static final int GRAPH_WIDTH = 140;
+		static final int GRAPH_HEIGHT = 80;
+		static final int HALF_GRAPH_HEIGHT = GRAPH_HEIGHT / 2;
+		static final int SAMPLE_PIXEL_SPACING = 1; 
+
+		private BufferedImage imageBuffer;
+		private Graphics2D imageGraphics;
+		private int lastSampleHeight = HALF_GRAPH_HEIGHT;
+		
+		private String alternateText = null;
+		private Color alternateTextColor = Color.WHITE;
+
+		AnimatedSampleGraph() {
+			imageBuffer = new BufferedImage(GRAPH_WIDTH, GRAPH_HEIGHT, BufferedImage.TYPE_INT_RGB);
+			imageGraphics = (Graphics2D)imageBuffer.getGraphics();
+			imageGraphics.setColor(GRAPH_BACKCOLOR);
+			imageGraphics.fillRect(0, 0, GRAPH_WIDTH, GRAPH_HEIGHT);
+			setPreferredSize(new Dimension(GRAPH_WIDTH, GRAPH_HEIGHT));
+		}
+		
+		/**
+		 * Clears the graph and invlaidates itself
+		 */
+		public void clear() {
+			imageGraphics.setColor(GRAPH_BACKCOLOR);
+			imageGraphics.fillRect(0, 0, GRAPH_WIDTH, GRAPH_HEIGHT);
+			invalidateGraph();
+		}
+
+		@Override
+		public void paint(Graphics g) {
+			
+			if (alternateText != null) {
+				
+				g.setColor(Color.BLACK);
+				g.fillRect(0, 0, getWidth(), getHeight());
+				
+				g.setFont(TrackWidgetCommons.FREESPACE_TRACKNAME_FONT);
+				g.setColor(alternateTextColor);
+				
+				// Center track name
+				FontMetrics fm   = g.getFontMetrics(TrackWidgetCommons.FREESPACE_TRACKNAME_FONT);
+				Rectangle2D rect = fm.getStringBounds(alternateText, g);
+				
+				g.drawString(
+						alternateText, 
+						(int)((getWidth() - rect.getWidth()) / 2), 
+						(int)((getHeight() - rect.getHeight()) / 2) + (int)rect.getHeight()
+						);
+				
+			} else {
+				g.drawImage(imageBuffer, 0, 0, null);
+			}
+		}
+		
+		/**
+		 * Renders the audio bytes to the graph
+		 * @param audioBytes
+		 */
+		public void updateVisualization(float[] waveForms) {
+
+			if (waveForms == null || waveForms.length == 0) return;
+			
+			int pixelWidth = waveForms.length * SAMPLE_PIXEL_SPACING;
+		
+			// Translate buffer back pixelWidth pixels
+			AffineTransform transform = new AffineTransform();
+			transform.translate(-pixelWidth, 0.0);
+			//transform.translate(pixelWidth, 0.0);
+			//AffineTransformOp op = new AffineTransformOp(transform, AffineTransformOp.TYPE_BILINEAR);
+			//imageBuffer = op.filter(imageBuffer, null);
+			imageGraphics.drawImage(imageBuffer, transform, this);
+			
+			// Render backcolor
+			imageGraphics.setColor(GRAPH_BACKCOLOR);
+			imageGraphics.fillRect(GRAPH_WIDTH - pixelWidth, 0, pixelWidth, GRAPH_HEIGHT);
+			
+			// Render wave forms from lastSampleHeight
+			imageGraphics.setColor(GRAPH_WAVECOLOR);
+			int currentPixelX = GRAPH_WIDTH - pixelWidth;
+
+			for (int i = 0; i < waveForms.length; i++) {
+				
+				int currentHeight = HALF_GRAPH_HEIGHT + (int)(waveForms[i] * HALF_GRAPH_HEIGHT);
+				
+				// Draw a line
+				imageGraphics.drawLine(currentPixelX - 1, lastSampleHeight, currentPixelX, currentHeight);
+				
+				currentPixelX += SAMPLE_PIXEL_SPACING;
+				lastSampleHeight = currentHeight;
+			}
+			
+			invalidateGraph();
+		}
+		
+		private void invalidateGraph() {
+
+	        // For fastest refreshing - invalidate directy via expedtiee
+	        Rectangle dirty = this.getBounds();
+	        dirty.translate(SampleRecorder.this.getX(), SampleRecorder.this.getY());
+	        FrameGraphics.invalidateArea(dirty); 
+	        FrameGraphics.refresh(true); 
+	        
+		}
+		
+	}
+		
+	/**
+	 * Updates the timer / widget state.
+	 * 
+	 * @author Brook Novak
+	 *
+	 */
+	private class CountDownTimer extends Thread {
+		
+		private int currentCountdown;
+		private boolean returnToReadyState = false;
+		
+		public CountDownTimer(int countdownSecs) {
+			assert(countdownSecs > 0);
+			currentCountdown = countdownSecs;
+		}
+		
+		public void abortCountdown() {
+			interrupt();
+		}
+		
+		public void run() {
+
+			try {
+				
+				while (currentCountdown > 0) {
+					
+					if (interrupted()) { 
+						returnToReadyState = true;
+						break;
+					}
+					
+					sleep(1000);
+					currentCountdown--;
+					
+					// Update graph
+					SwingUtilities.invokeLater(new Runnable() {
+						public void run() {
+							sampleGraph.alternateText = Integer.toString(currentCountdown);
+							sampleGraph.alternateTextColor = (currentCountdown > 3) ?
+										Color.WHITE : Color.RED;
+							sampleGraph.invalidateGraph();
+							FrameGraphics.refresh(true);
+						}
+					});
+					
+				}
+				
+			} catch (InterruptedException e) {
+				returnToReadyState = true;
+			}
+			
+			SwingUtilities.invokeLater(new Runnable() {
+				public void run() {
+					if (returnToReadyState) {
+						setState(WidgetState.Ready, "Ready");
+					} else {
+						commenceRecording(shouldPlayback);
+					}
+				}
+			});
+
+		}
+	}
+	
+}
Index: /trunk/src_apollo/org/apollo/widgets/SampledTrackEvo4.java
===================================================================
--- /trunk/src_apollo/org/apollo/widgets/SampledTrackEvo4.java	(revision 315)
+++ /trunk/src_apollo/org/apollo/widgets/SampledTrackEvo4.java	(revision 315)
@@ -0,0 +1,1869 @@
+package org.apollo.widgets;
+
+import java.awt.BasicStroke;
+import java.awt.Color;
+import java.awt.FontMetrics;
+import java.awt.Graphics;
+import java.awt.Graphics2D;
+import java.awt.Point;
+import java.awt.Rectangle;
+import java.awt.Shape;
+import java.awt.Stroke;
+import java.awt.event.ActionEvent;
+import java.awt.event.KeyEvent;
+import java.awt.event.KeyListener;
+import java.awt.event.MouseEvent;
+import java.awt.event.MouseListener;
+import java.awt.event.MouseMotionListener;
+import java.awt.geom.Rectangle2D;
+import java.io.File;
+import java.io.IOException;
+import java.lang.reflect.InvocationTargetException;
+import java.util.LinkedList;
+import java.util.List;
+
+import javax.sound.sampled.AudioFormat;
+import javax.sound.sampled.LineUnavailableException;
+import javax.sound.sampled.UnsupportedAudioFileException;
+import javax.swing.SwingUtilities;
+
+import org.apollo.audio.ApolloPlaybackMixer;
+import org.apollo.audio.ApolloSubjectChangedEvent;
+import org.apollo.audio.SampledTrackModel;
+import org.apollo.audio.TrackSequence;
+import org.apollo.audio.structure.AudioStructureModel;
+import org.apollo.audio.structure.TrackGraphNode;
+import org.apollo.audio.util.FrameLayoutDaemon;
+import org.apollo.audio.util.SoundDesk;
+import org.apollo.audio.util.Timeline;
+import org.apollo.audio.util.TrackMixSubject;
+import org.apollo.gui.EditableSampledTrackGraphView;
+import org.apollo.gui.ExpandedTrackManager;
+import org.apollo.gui.PlaybackControlPopup;
+import org.apollo.gui.SampledTrackGraphView;
+import org.apollo.gui.SampledTrackGraphView.EffecientInvalidator;
+import org.apollo.io.AudioIO;
+import org.apollo.io.AudioPathManager;
+import org.apollo.io.IconRepository;
+import org.apollo.io.AudioIO.AudioFileLoader;
+import org.apollo.items.EmulatedTextItem;
+import org.apollo.items.EmulatedTextItem.TextChangeListener;
+import org.apollo.mvc.Observer;
+import org.apollo.mvc.Subject;
+import org.apollo.mvc.SubjectChangedEvent;
+import org.apollo.util.AudioMath;
+import org.apollo.util.PopupReaper;
+import org.apollo.util.TrackModelHandler;
+import org.apollo.util.TrackModelLoadManager;
+import org.apollo.util.TrackNameCreator;
+import org.expeditee.gui.Browser;
+import org.expeditee.gui.DisplayIO;
+import org.expeditee.gui.Frame;
+import org.expeditee.gui.FrameGraphics;
+import org.expeditee.gui.MouseEventRouter;
+import org.expeditee.gui.PopupManager;
+import org.expeditee.items.ItemParentStateChangedEvent;
+import org.expeditee.items.ItemUtils;
+import org.expeditee.items.Text;
+import org.expeditee.items.widgets.HeavyDutyInteractiveWidget;
+import org.expeditee.items.widgets.InteractiveWidget;
+import org.expeditee.items.widgets.InteractiveWidgetInitialisationFailedException;
+import org.expeditee.items.widgets.InteractiveWidgetNotAvailableException;
+
+/**
+ * The sampled track widgets in apollo.
+ * 
+ * @author Brook Novak
+ *
+ */
+public class SampledTrackEvo4 extends HeavyDutyInteractiveWidget 
+	implements TrackModelHandler, EffecientInvalidator, Observer {
+
+	/** The observered subject. Can be null */
+	private SampledTrackModel trackModel = null;
+	
+	/** Used for the loading phase. Can change serveal times of a lifetime. for example after import and save it becomes local */
+	private String loadFilenameArgument = null; // preproceeds with ARG_IMPORT_TAG if importing.
+	
+	private String localFileName; // Immutable - assigned on constuction
+	
+	/** Used for dumping audio to a temp file when deleted - to free memory.
+	 * Protocol: Set when the widget is deleted. Unset when loaded. */
+	private File recoveryFile = null;
+	
+	private boolean shouldOmitAudioIndexing = false;
+	
+	private EditableSampledTrackGraphView fulltrackView;
+	private EmulatedTextItem nameLabel; 
+	private PlaybackPopup playbackControlPopup = null;
+	private TrackMixSubject trackMix; // immutable - like local filename
+	
+	/** The amount of load bar that is allocated to file loading */
+	private static final float FILE_LOADING_PERCENT_RANGE = 0.95f;
+
+	/** For parsing arguments */
+	public static final String ARG_IMPORT_TAG = "if=";
+	
+	/** For parsing metadata */
+	public static final String META_LOCALNAME_TAG = "ln=";
+	
+	private static final Stroke FREESPACE_OUTLINING = new BasicStroke(2.0f);
+	
+	private static final Color SEMI_TRANSPARENT_FREESPACE_BACKCOLOR = new Color(
+			FREESPACE_BACKCOLOR.getRed(), FREESPACE_BACKCOLOR.getGreen(),
+			FREESPACE_BACKCOLOR.getBlue(), 128);
+	
+	
+	/** If a track widget has a data string thaty contains META_SHOULD_INDEX_AUDIO_TAG, then
+	 * the track should not be indexed for searching the audio.... default isdoes not exist
+	 */
+	public static final String META_DONT_INDEX_AUDIO_TAG = "dontindexaudio";
+
+	/**
+	 * Constructor used for loading directly from given bytes.
+	 * 
+	 * @param source
+	 * 
+	 * @param audioBytes
+	 * 
+	 * @param format
+	 * 
+	 * @param mixTemplate
+	 * 		Can be null
+	 * 
+	 */
+	private SampledTrackEvo4(Text source, byte[] audioBytes, AudioFormat format, TrackMixSubject mixTemplate) {
+		
+		super(source, new EditableSampledTrackGraphView(), 
+				100, -1, 
+				FrameLayoutDaemon.TRACK_WIDGET_HEIGHT, FrameLayoutDaemon.TRACK_WIDGET_HEIGHT, 
+				TrackWidgetCommons.CACHE_DEPTH,
+				true);
+
+		// Must set upon construction - always
+		localFileName = AudioPathManager.generateLocateFileName("wav");
+		updateData(META_LOCALNAME_TAG, META_LOCALNAME_TAG + localFileName);
+		
+		trackModel = new SampledTrackModel(audioBytes, format, localFileName);
+		
+		// Ensure that the model is marked as modiefied so that it will save
+		trackModel.setAudioModifiedFlag(true);
+		
+		trackModel.setName(getStrippedDataString(TrackWidgetCommons.META_NAME_TAG));
+
+		// Create the immutable mix subject
+		if (mixTemplate == null)
+			trackMix = SoundDesk.getInstance().getOrCreateMix(SoundDesk.createPureLocalChannelID(this));
+		
+		else trackMix = SoundDesk.getInstance().createMix(
+				SoundDesk.createPureLocalChannelID(this), 
+				mixTemplate.getVolume(), 
+				mixTemplate.isMuted(),
+				true);
+				
+		// Keep meta as constant as possible for best results
+		updateData(TrackWidgetCommons.META_RUNNINGMSTIME_TAG, TrackWidgetCommons.META_RUNNINGMSTIME_TAG + getRunningMSTimeFromRawAudio());
+		
+		createGUI();
+		
+		initObservers();
+		
+	}
+
+	/**
+	 * Constructor called by Expeditee. Eventually loads audio from file.
+	 * 
+	 * @param source
+	 * 
+	 * @param args
+	 * 		Can have a ARG_IMPORT_TAG argument...
+	 */
+	public SampledTrackEvo4(Text source, String[] args) {
+		super(source, new EditableSampledTrackGraphView(), 
+				100, -1, 
+				FrameLayoutDaemon.TRACK_WIDGET_HEIGHT, FrameLayoutDaemon.TRACK_WIDGET_HEIGHT, 
+				TrackWidgetCommons.CACHE_DEPTH);
+		
+		// Read the metadata
+		localFileName = getStrippedDataString(META_LOCALNAME_TAG);
+
+		// Ensure the local filename is assigned - even if file does not exist...
+		// Also it could be importing a file...
+		if (localFileName == null) {
+			localFileName = AudioPathManager.generateLocateFileName("wav");
+			updateData(META_LOCALNAME_TAG, META_LOCALNAME_TAG + localFileName);
+		}
+
+		trackMix = SoundDesk.getInstance().getOrCreateMix(SoundDesk.createPureLocalChannelID(this));
+
+		loadFilenameArgument = localFileName;
+		if (args != null) { // parse args
+			for (String arg : args) {
+				if (arg != null && arg.length() > ARG_IMPORT_TAG.length()) {
+					loadFilenameArgument = arg;
+				}
+			}
+		}
+		
+		createGUI();
+
+	}
+	
+	private void initObservers() {
+		
+		// Listen for model events
+		trackModel.addObserver(this);
+		trackModel.addObserver(fulltrackView);
+		fulltrackView.setMix(trackMix); // use the same mix/channel for playback
+		ExpandedTrackManager.getInstance().addObserver(this);
+		
+		// Show graph as being selected if expanded / pending to expand.
+		if (ExpandedTrackManager.getInstance().isTrackInExpansionSelection(trackModel)) {
+			fulltrackView.setBackColor(new Color(100, 100, 100), new Color(120, 120, 120));
+		} else {
+			fulltrackView.setBackColor(SampledTrackGraphView.DEFAULT_BACKGROUND_COLOR, SampledTrackGraphView.DEFAULT_BACKGROUND_HIGHTLIGHTS_COLOR);
+		}
+
+	}
+
+	private void createGUI() {
+		
+		// Set widget as a fixed size widget- using width from last recorded width in the meta data.
+		int width = getStrippedDataInt(TrackWidgetCommons.META_LAST_WIDTH_TAG, -1);
+		if (width >= 0 && width < FrameLayoutDaemon.MIN_TRACK_WIDGET_WIDTH) {
+			width = FrameLayoutDaemon.MIN_TRACK_WIDGET_WIDTH;
+		}
+		
+		if (width < 0) {
+			setSize(-1, -1, 
+					FrameLayoutDaemon.TRACK_WIDGET_HEIGHT, FrameLayoutDaemon.TRACK_WIDGET_HEIGHT, 
+					FrameLayoutDaemon.TRACK_WIDGET_DEFAULT_WIDTH, FrameLayoutDaemon.TRACK_WIDGET_HEIGHT);
+		} else {
+			setSize(width, width, 
+					FrameLayoutDaemon.TRACK_WIDGET_HEIGHT, FrameLayoutDaemon.TRACK_WIDGET_HEIGHT, 
+					width, FrameLayoutDaemon.TRACK_WIDGET_HEIGHT);
+		}
+		
+		shouldOmitAudioIndexing = containsDataTrimmedIgnoreCase(META_DONT_INDEX_AUDIO_TAG);
+
+		playbackControlPopup = new PlaybackPopup();
+		
+		fulltrackView = (EditableSampledTrackGraphView)_swingComponent;
+		fulltrackView.setAlwaysFullView(true);
+		
+		/*fulltrackView.setInvalidator(new EffecientInvalidator() {
+			public void onGraphDirty(SampledTrackGraphView graph, Rectangle dirty) {
+		        dirty.translate(getX(), getY() - 1);
+		        FrameGraphics.invalidateArea(dirty); 
+		        FrameGraphics.refresh(true); 
+			}
+		});*/
+		
+		// Auto-show playback popup.
+		// Block messages if the track is expanded
+		fulltrackView.addMouseMotionListener(new MouseMotionListener() {
+
+			public void mouseDragged(MouseEvent e) {
+				if (nameLabel != null) {
+					if (nameLabel.onMouseDragged(e)) {
+						e.consume();
+					}
+				}
+			}
+
+			public void mouseMoved(MouseEvent e) {
+
+				if (nameLabel != null) {
+					nameLabel.onMouseMoved(e, fulltrackView);
+				}
+				
+				if (playbackControlPopup == null) {
+					playbackControlPopup = new PlaybackPopup();
+				} 
+
+				// Only show popup iff there is nothing expanded / expanding.
+				if (!PopupManager.getInstance().isShowing(playbackControlPopup) &&
+						!ExpandedTrackManager.getInstance().doesExpandedTrackExist()) {	
+					
+					// Get rid of all popups
+					PopupManager.getInstance().hideAutohidePopups();
+					
+					Rectangle animationSource = _swingComponent.getBounds();
+					
+					// Determine where popup should show
+					int x = SampledTrackEvo4.this.getX();
+					int y = SampledTrackEvo4.this.getY() - playbackControlPopup.getHeight() - 2; // by default show above
+					
+					// I get sick.dizzy from the popup expanding from the whole thing...
+					animationSource.height = 1;
+					animationSource.width = Math.min(animationSource.width, playbackControlPopup.getWidth());
+					
+					if (y < 0) {
+						y = SampledTrackEvo4.this.getY() + SampledTrackEvo4.this.getHeight() + 2;
+						animationSource.y = y - 2;
+					} 
+
+					// Animate the popup
+					PopupManager.getInstance().showPopup(
+							playbackControlPopup, 
+							new Point(x, y), 
+							fulltrackView,
+							PopupManager.getInstance().new ExpandShrinkAnimator(
+									animationSource,
+									Color.LIGHT_GRAY));
+					
+					PopupReaper.getInstance().initPopupLifetime(
+							playbackControlPopup, 
+							PopupManager.getInstance().new ExpandShrinkAnimator(
+									animationSource,
+									Color.LIGHT_GRAY), 
+									TrackWidgetCommons.POPUP_LIFETIME);
+					
+				} else {
+					PopupReaper.getInstance().revivePopup(playbackControlPopup, TrackWidgetCommons.POPUP_LIFETIME);
+				}
+				
+			}
+
+		});
+		
+		// Expand on double click
+		// Block messages if the track is expanded
+		fulltrackView.addMouseListener(new MouseListener() {
+
+			public void mouseClicked(MouseEvent e) {
+				if (trackModel == null) return;
+				
+				if (nameLabel != null) {
+					if (nameLabel.onMouseClicked(e)) {
+						e.consume();
+						return;
+					}
+				}
+				
+				if (e.getClickCount() >= 2) {
+					
+					expand(e.isControlDown());
+					
+				}
+
+			}
+
+			public void mouseEntered(MouseEvent e) {
+			}
+
+			public void mouseExited(MouseEvent e) {
+			}
+
+			public void mousePressed(MouseEvent e) {
+				if (nameLabel != null) {
+					if (nameLabel.onMousePressed(e)) {
+						e.consume();
+						return;
+					}
+				}
+				
+				// Consume events if track is selected for expansion
+				if (ExpandedTrackManager.getInstance().isTrackInExpansionSelection(trackModel) &&
+						e.getButton() != MouseEvent.BUTTON1) { // but allow selection only
+					e.consume();
+				}
+			}
+
+			public void mouseReleased(MouseEvent e) {
+				if (nameLabel != null) {
+					if (nameLabel.onMouseReleased(e)) {
+						e.consume();
+					}
+				}
+				
+				// Consume events if track is selected for expansion
+				if (ExpandedTrackManager.getInstance().isTrackInExpansionSelection(trackModel)) {
+					e.consume();
+				}
+				
+		
+			}
+		
+		});
+		
+		fulltrackView.addKeyListener(new KeyListener() {
+
+			public void keyPressed(KeyEvent e) {
+				if (nameLabel != null) {
+					if (nameLabel.onKeyPressed(e, fulltrackView)) {
+						e.consume();
+					}
+				}
+			}
+
+			public void keyReleased(KeyEvent e) {
+				if (nameLabel != null) {
+					if (nameLabel.onKeyReleased(e, fulltrackView)) {
+						e.consume();
+					}
+				}
+				
+				// Toggle indexing
+				if (e.isControlDown() && e.getKeyCode() == KeyEvent.VK_I) {
+					setShouldOmitIndexAudio(!shouldOmitIndexAudio());
+					FrameGraphics.refresh(true);
+				}
+
+			}
+
+			public void keyTyped(KeyEvent e) {
+			}
+			
+		});
+		
+		
+		
+		nameLabel = new EmulatedTextItem(_swingComponent, new Point(10, 20));
+		nameLabel.setBackgroundColor(Color.WHITE);	
+		
+		String metaName = getStrippedDataString(TrackWidgetCommons.META_NAME_TAG);
+		if (metaName == null) metaName = "Untitled";
+		nameLabel.setText(metaName); 
+
+		nameLabel.addTextChangeListener(new TextChangeListener() { //  a little bit loopy!
+
+			public void onTextChanged(Object source, String newLabel) {
+				if (trackModel != null && !nameLabel.getText().equals(trackModel.getName())) {
+					trackModel.setName(nameLabel.getText());
+					SampledTrackEvo4.this.updateData(TrackWidgetCommons.META_NAME_TAG, trackModel.getName());
+					
+				}
+			}
+
+		});
+		
+		// Make sure the above mouse listeners get first serve
+		fulltrackView.reAddListeners();
+		
+		// Make sure border color is correct
+		updateBorderColor();
+		setWidgetEdgeThickness(TrackWidgetCommons.STOPPED_TRACK_EDGE_THICKNESS);
+
+	}
+
+	/**
+	 * Creates a {@link SampledTrackEvo4} instantly from audio bytes in memory and adds it to a given frame.
+	 * @param audioBytes
+	 * 			The audio samples.
+	 * 
+	 * @param format
+	 * 			The format of the audio samples.
+	 * 
+	 * @param targetFrame
+	 * 			The frame that the widget will reside.
+	 * 
+	 * @param x
+	 * 
+	 * @param y
+	 * 
+	 * @param specificInitTime
+	 * 		Negative to determine init time from x position. Positive to override
+	 * 		and determine x position from given init time
+	 * 
+	 * @param name
+	 * 			The name of the new track. If null then a default name will be used.
+	 * 
+	 * @param mixTemplate 
+	 * 			The mix data to clone for the new sampled tracks mix.
+	 * 			If null then default mix settings will be used.
+	 * 
+	 * @return The {@link SampledTrackEvo4} instance added to the given frame.
+	 * 
+	 */
+	public static SampledTrackEvo4 createFromMemory(
+			byte[] audioBytes, 
+			AudioFormat format,
+			Frame targetFrame,
+			int x, int y,
+			long specificInitTime,
+			String name,
+			TrackMixSubject mixTemplate) {
+		
+		assert (targetFrame != null);
+		assert (audioBytes != null);
+		assert (format != null);
+		
+		Text source = new Text(targetFrame.getNextItemID());
+		source.setParent(targetFrame);
+		
+		
+		
+		long runningtime = AudioMath.framesToMilliseconds(
+				audioBytes.length / format.getFrameSize(), format);
+		
+		
+		Timeline tl = FrameLayoutDaemon.getInstance().getTimeline(targetFrame);
+		
+		int width = -1;
+		if (tl != null) {
+			width = (int)((double)runningtime / tl.getTimePerPixel());
+		}	
+		
+		// Clamp width to something reasonable.
+		if (width > 0 && width < FrameLayoutDaemon.MIN_TRACK_WIDGET_WIDTH) width = FrameLayoutDaemon.MIN_TRACK_WIDGET_WIDTH;
+
+		if (specificInitTime < 0) {
+			
+			if (tl == null) specificInitTime = 0;
+			else specificInitTime = tl.getMSTimeAtX(x);
+			
+		} else if (tl != null) { // if defined a specific ms time and a timeline is avialable
+			
+			x = tl.getXAtMSTime(specificInitTime);
+		} 
+		
+		source.setPosition(x, y);
+			
+		// Setup metadata
+		LinkedList<String> data = new LinkedList<String>();
+
+		data.add(TrackWidgetCommons.META_INITIATIONTIME_TAG + specificInitTime);
+		data.add(TrackWidgetCommons.META_LAST_WIDTH_TAG + width); // although layout manager will handle, just to quick set
+		if (name != null) data.add(TrackWidgetCommons.META_NAME_TAG + name);
+		
+		source.setData(data);
+		
+		SampledTrackEvo4 strack = new SampledTrackEvo4(source, audioBytes, format, mixTemplate);
+
+		return strack;
+	}
+	
+	
+	/**
+	 * Creates a {@link SampledTrackEvo4} from file - which is tyo be imported into apollos
+	 * 
+	 * @param targetFrame
+	 * 			The frame that the widget will reside.
+	 * 
+	 * @param file
+	 * 			The audio file to import
+	 * 
+	 * @return The {@link SampledTrackEvo4} instance added to the given frame.
+	 * 
+	 */
+	public static SampledTrackEvo4 createFromFile(File file, Frame targetFrame, int x, int y) {
+		assert (targetFrame != null);
+		assert (file != null);
+		
+	
+		String[] args = new String[] { 
+				ARG_IMPORT_TAG + file.getAbsolutePath()
+				};
+		
+		Text source = new Text(
+				targetFrame.getNextItemID(),
+				ItemUtils.GetTag(ItemUtils.TAG_IWIDGET) + ":" + formatArgs(args));
+		
+		source.setParent(targetFrame);
+		
+		source.setPosition(x, y);
+		
+		long initTime = FrameLayoutDaemon.getInstance().getMSAtX(x, targetFrame);
+
+		// Setup metadata
+		LinkedList<String> data = new LinkedList<String>();
+
+		data.add(TrackWidgetCommons.META_INITIATIONTIME_TAG + initTime);
+		data.add(TrackWidgetCommons.META_LAST_WIDTH_TAG + FrameLayoutDaemon.TRACK_WIDGET_DEFAULT_WIDTH); // once loaded then the layout will handle proper size
+		data.add(TrackWidgetCommons.META_NAME_TAG + file.getName());
+		
+		source.setData(data);
+
+		SampledTrackEvo4 strack = new SampledTrackEvo4(source, args);
+
+		return strack;
+	}
+
+	
+	@Override
+	protected String[] getArgs() {
+		return null;
+	}
+
+	@Override
+	protected List<String> getData() {
+
+		List<String> data = new LinkedList<String>();
+		
+		data.add(META_LOCALNAME_TAG + localFileName);
+		
+		if (shouldOmitAudioIndexing) data.add(META_DONT_INDEX_AUDIO_TAG);
+		
+		String lastName = getName();
+		
+		if (lastName != null)
+			data.add(TrackWidgetCommons.META_NAME_TAG + lastName);
+		
+		data.add(TrackWidgetCommons.META_LAST_WIDTH_TAG + getWidth());
+		
+		Long initTime = null;
+		
+		Frame f = getParentFrame();
+		if (f != null) {
+			TrackGraphNode tinf = AudioStructureModel.getInstance().getTrackGraphInfo(localFileName, f.getName());
+			if (tinf != null) {
+				initTime = tinf.getInitiationTime();
+			}
+		}
+		
+		if (initTime == null) initTime = getInitiationTimeFromMeta(); // old meta
+		if (initTime == null) initTime = 0L;
+
+		data.add(TrackWidgetCommons.META_INITIATIONTIME_TAG + initTime);
+		
+
+		return data;
+	}
+	
+	
+	@Override
+	public InteractiveWidget copy() 
+		throws InteractiveWidgetNotAvailableException, InteractiveWidgetInitialisationFailedException {
+		
+		if (trackModel == null) {
+			return super.copy();
+			
+		} else {
+
+			return SampledTrackEvo4.createFromMemory(
+					trackModel.getAllAudioBytesCopy(),
+					trackModel.getFormat(), 
+					getSource().getParentOrCurrentFrame(), 
+					getX(), 
+					getY(),
+					-1,
+					TrackNameCreator.getNameCopy(trackModel.getName()),
+					trackMix);
+		}
+		
+	}
+
+	@Override
+	public int getLoadDelayTime() {
+		return 0;
+	}
+
+	@Override
+	protected float loadWidgetData() {
+		
+		// Load audio from file
+		File f = null;
+		boolean isImporting = false;
+		
+		if (recoveryFile != null) { // are we recovering?
+			
+			setLoadScreenMessage("Recovering audio file...");
+			
+			if (recoveryFile.exists()) f = recoveryFile;
+		}
+		
+		if (f == null) { // must be importing or loading from local repository
+
+			if (loadFilenameArgument != null) {
+				if (loadFilenameArgument.startsWith(ARG_IMPORT_TAG)) { // importing a file?
+					
+					setLoadScreenMessage("Importing audio file...");
+					
+					isImporting = true;
+					f = new File(loadFilenameArgument.substring(ARG_IMPORT_TAG.length()));
+					
+				} else {
+					assert(loadFilenameArgument == localFileName);
+					
+					setLoadScreenMessage("Loading audio file...");
+					
+					f = new File(
+							AudioPathManager.AUDIO_HOME_DIRECTORY + loadFilenameArgument);
+				}
+				
+				if (!f.exists() || !f.isFile()) f = null;
+			} 
+			
+		}
+
+		if (f == null) {
+			if (recoveryFile != null) {
+				setLoadScreenMessage("Recovery file missing");
+			} else {
+				setLoadScreenMessage("File missing");
+			}
+			
+			return LOAD_STATE_FAILED;
+		}
+		
+		if (trackModel != null && trackModel.getFilepath() != null &&
+				trackModel.equals(f.getPath())) {
+			
+			// already have loaded
+			assert(!isImporting);
+			
+		} else {
+
+			try {
+				
+				trackModel = TrackModelLoadManager.getInstance().load(f.getPath(), localFileName, this, false);
+				
+				if (trackModel == null) { // load operation canceled
+					assert(hasCancelBeenRequested()); 
+					return LOAD_STATE_INCOMPLETED;
+					
+				} else if(isImporting) { // ensure that file path is null - since not yet saved
+					trackModel.setFilepath(null);
+					
+				} else if (recoveryFile != null) {
+					
+					// If recovering - might be recovering an existing track that had been
+					// saved to the repository .. thus re-use the old file
+					trackModel.setFilepath(AudioPathManager.AUDIO_HOME_DIRECTORY + localFileName);
+				}
+				
+			} catch (IOException e) {
+				e.printStackTrace();
+				setLoadScreenMessage("Failed to load audio file");
+				return LOAD_STATE_FAILED;
+				
+			} catch (UnsupportedAudioFileException e) {
+				e.printStackTrace();
+				setLoadScreenMessage("Format not supported");
+				return LOAD_STATE_FAILED;
+			} catch (OutOfMemoryError e) {
+				e.printStackTrace();
+				setLoadScreenMessage("Out of memory");
+				return LOAD_STATE_FAILED;
+			}
+			
+		}
+		
+		// If was imported / recovered, then set as being modified
+		if (isImporting || recoveryFile != null) {
+			trackModel.setAudioModifiedFlag(true);
+		}
+		
+		// Set the name for this track
+		String name = getStrippedDataString(TrackWidgetCommons.META_NAME_TAG);
+		if (name != null) 
+			trackModel.setName(name);
+		
+		initObservers(); // sets default name if non set
+		
+		// If was recovering - get rid of temp data
+		if (recoveryFile != null) {
+			recoveryFile.delete();
+			recoveryFile = null; // ensure that out of a recovery state
+		}
+		
+		// Keep meta as constant as possible for best results
+		updateData(TrackWidgetCommons.META_RUNNINGMSTIME_TAG, TrackWidgetCommons.META_RUNNINGMSTIME_TAG + getRunningMSTimeFromRawAudio());
+		
+		// Must make sure that this track is on the track graph model
+		try {
+			SwingUtilities.invokeAndWait(new Runnable() {
+				
+				public void run() {
+					
+					Frame parent = getParentFrame();
+					String pfname = (parent != null) ?  parent.getName() : null;
+					
+					TrackGraphNode tinf = AudioStructureModel.getInstance().getTrackGraphInfo(localFileName, pfname);
+					if (tinf == null) {
+
+						// Determine new initation time according to position
+						long initTime = (parent != null) ? 
+								FrameLayoutDaemon.getInstance().getMSAtX(getX(), parent)
+								: 0;
+
+						// Keep TrackGraphModel consistant
+						AudioStructureModel.getInstance().onTrackWidgetAnchored(
+								localFileName, 
+								pfname, 
+								initTime, 
+								AudioMath.framesToMilliseconds(trackModel.getFrameCount(), trackModel.getFormat()),
+								getName());
+						
+					}
+				}
+			});
+		} catch (InterruptedException e) {
+			e.printStackTrace();
+		} catch (InvocationTargetException e) {
+			e.printStackTrace();
+		}
+		
+		
+		// Notify layout manager - not really needed but to be extra safe force the daemon
+		// to be super consistant
+		FrameLayoutDaemon.getInstance().forceRecheck();
+
+		return LOAD_STATE_COMPLETED;
+	}
+	
+	/**
+	 * Used by save manager
+	 */
+	public boolean doesNeedSaving() {
+		return (trackModel != null && (trackModel.isAudioModified() || trackModel.getFilepath() == null));
+	}
+
+	/**
+	 * Used by save manager
+	 */
+	public String getSaveName() {
+		
+		if (trackModel != null && trackModel.getName() != null)
+			return "Sampled Track: " + trackModel.getName();
+		else return "A Sampled Track";
+		
+	}
+
+	/**
+	 * Saves the audio bytes to file
+	 */
+	@Override
+	protected void saveWidgetData() {
+		
+		if (trackModel == null) return; // nothing to save
+		
+		// If saving for the file time then get a filename
+		if (trackModel.getFilepath() == null) {
+			trackModel.setFilepath(AudioPathManager.AUDIO_HOME_DIRECTORY + localFileName);
+			loadFilenameArgument = localFileName; // set to now local, next load will be local
+		}
+		
+		// Save audio bytes.
+		try {
+			
+			AudioIO.savePCMAudioToWaveFile(
+					trackModel.getFilepath(), 
+					trackModel.getAllAudioBytes(), // Safe: arrays are immutable
+					trackModel.getFormat());
+			
+			// Reset modified flag
+			trackModel.setAudioModifiedFlag(false);
+			
+		} catch (IOException e) {
+			e.printStackTrace();
+		} catch (UnsupportedAudioFileException e) {
+			e.printStackTrace();
+		}
+
+		// Ensure audio bytes can be collected if this has expired
+		if (isExpired()) {
+
+			try {
+				SwingUtilities.invokeAndWait(new Runnable() {
+					public void run() {
+						releaseMemory(true);
+					}
+				});
+			} catch (InterruptedException e) {
+				e.printStackTrace();
+			} catch (InvocationTargetException e) {
+				e.printStackTrace();
+			}
+			
+		}
+		
+	
+	}
+
+	@Override
+	public void onDelete() {
+		super.onDelete();
+		// Its nice to keep everything layed out as much as possible
+		FrameLayoutDaemon.getInstance().resumeLayout(this);
+	}
+
+	@Override
+	protected void unloadWidgetData() {
+
+		// Still needs to save?
+		if (doesNeedSaving() || trackModel == null)
+			return; // Release memory later when saved
+
+		try {
+			// Release memory - on swing thread to avoid nullified model data wil painting / editing.
+			SwingUtilities.invokeAndWait(new Runnable() {
+				public void run() {
+					releaseMemory(true);
+				}
+			});
+		} catch (InterruptedException e) {
+			e.printStackTrace();
+		} catch (InvocationTargetException e) {
+			e.printStackTrace();
+		}
+
+		
+	}
+	
+	@Override
+	protected void tempUnloadWidgetData() {
+
+		// Need unloading?
+		if (trackModel == null) return;
+		
+		// Get rid of old temporary files
+		if (recoveryFile != null && recoveryFile.exists()) {
+			recoveryFile.delete();
+		}
+		
+		// Dump memory if needs saving
+		if (trackModel.isAudioModified() || trackModel.getFilepath() == null) {
+
+			try {
+				// Always be unique
+				String uniqueID = AudioPathManager.generateLocateFileName("wav");
+				recoveryFile = File.createTempFile("APOLLO_BACKUP" + uniqueID, null);
+				
+			} catch (IOException e) {
+				e.printStackTrace();
+				return; // cannot dump ... must keep in memory.
+			}
+
+			// Dump audio to file
+			try {
+				AudioIO.savePCMAudioToWaveFile(
+						recoveryFile.getAbsolutePath(), 
+						trackModel.getAllAudioBytes(), 
+						trackModel.getFormat());
+			} catch (IOException e1) {
+				e1.printStackTrace();
+				return; // cannot dump ... must keep in memory.
+			} catch (UnsupportedAudioFileException e1) {
+				e1.printStackTrace();
+				return; // cannot dump ... must keep in memory.
+			}
+		}
+		
+		try {
+			// Release memory - on swing thread to avoid nullified model data wil painting / editing.
+			SwingUtilities.invokeAndWait(new Runnable() {
+				public void run() {
+					releaseMemory(false);
+				}
+			});
+		} catch (InterruptedException e) {
+			e.printStackTrace();
+		} catch (InvocationTargetException e) {
+			e.printStackTrace();
+		}
+
+	}
+
+	/**
+	 * To be called by the swing thread only
+	 * This is nessessary to avoid runtime memory leaks!!
+	 */
+	private void releaseMemory(boolean onlyIfExpired) {
+		
+		if (!onlyIfExpired || (onlyIfExpired && isExpired())) {
+			
+			if (trackModel != null) {
+				
+				trackModel.removeObserver(fulltrackView);
+				trackModel.removeObserver(this);
+				trackModel = null;
+				
+				assert (fulltrackView.getObservedSubject() == null);
+				assert (fulltrackView.getMix() == null);
+			}
+			
+			fulltrackView.releaseBuffer();
+			
+			ExpandedTrackManager.getInstance().removeObserver(this);
+
+		}
+		
+	}
+
+	/**
+	 * Global Track model re-use ....
+	 */
+	public SampledTrackModel getSharedSampledTrackModel(String localfilename) {
+		if (trackModel != null &&
+				trackModel.getLocalFilename().equals(localfilename)) {
+			assert(localfilename.equals(localFileName));
+			return trackModel;
+		}
+		return null;
+
+	}
+	
+	/**
+	 * @see #getAudioFormat()
+	 * 
+	 * @return
+	 * 		The audio bytes for this track widget. Null if not loaded.
+	 */
+	public byte[] getAudioBytes() {
+		return (trackModel != null) ? trackModel.getAllAudioBytes() : null;
+	}
+	
+	/**
+	 * @see #getAudioBytes()
+	 * 
+	 * @return
+	 * 		The audio format of the audio bytes for this track widget. Null if not loaded.
+	 */
+	public AudioFormat getAudioFormat() {
+		return (trackModel != null) ? trackModel.getFormat() : null;
+	}
+	
+	/**
+	 * <b>Warning</b> if the path happens to be a path of a recovery file
+	 * then it is deleted if the widget is reloaded.
+	 * 
+	 * @return 
+	 * 		A full filepath to load the audio from. Never null. The path	
+	 * 		may lead to a out of date file or no file.
+	 */
+	public String getLatestSavedAudioPath() {
+		
+		if (recoveryFile != null && recoveryFile.exists()) {
+			return recoveryFile.getAbsolutePath();
+		}
+			
+		return AudioPathManager.AUDIO_HOME_DIRECTORY + localFileName;
+		
+		
+	}
+
+	public void onGraphDirty(SampledTrackGraphView graph, Rectangle dirty) {
+        dirty.translate(getX(), getY() - 1);
+        FrameGraphics.invalidateArea(dirty); 
+        FrameGraphics.refresh(true);
+	}
+
+	public Subject getObservedSubject() {
+		return null;
+	}
+	
+
+	@Override
+	protected void onSizeChanged() {
+		super.onSizeChanged();
+		// Keep meta as constant as possible for best reults
+		updateData(TrackWidgetCommons.META_LAST_WIDTH_TAG, TrackWidgetCommons.META_LAST_WIDTH_TAG + getWidth());
+	}
+
+	/**
+	 * Responds to model changed events by updating the GUI ...
+	 */
+	public void modelChanged(Subject source, SubjectChangedEvent event) {
+		
+		// If the expansion selection has changed - check to see if was for this
+		if (source == ExpandedTrackManager.getInstance()) {
+			
+			// Show graph as being selected if expanded / pending to expand.
+			if (trackModel != null &&
+					ExpandedTrackManager.getInstance().isTrackInExpansionSelection(trackModel)) {
+				fulltrackView.setBackColor(new Color(100, 100, 100), new Color(120, 120, 120));
+			} else {
+				fulltrackView.setBackColor(SampledTrackGraphView.DEFAULT_BACKGROUND_COLOR, SampledTrackGraphView.DEFAULT_BACKGROUND_HIGHTLIGHTS_COLOR);
+			}
+			
+			return;
+		}
+		
+		Frame parent = null;
+
+		switch (event.getID()) {
+		
+			case ApolloSubjectChangedEvent.LOAD_STATUS_REPORT:
+				// If this widget is loading - then update the load status
+				if (isInLoadProgress()) {
+					
+					// If the load has been cancelled, then cancel the loader
+					if (hasCancelBeenRequested()) {
+						
+						AudioFileLoader loader = (AudioFileLoader)source;
+						loader.cancelLoad();
+						
+					} else {
+						float perc = ((Float)event.getState()).floatValue();
+						if (perc > 1.0f) perc = 1.0f;
+						perc *= FILE_LOADING_PERCENT_RANGE; // Dont stretch load to all of bar - still will have more work to do
+						updateLoadPercentage(perc);
+					}
+					
+				}
+				
+				break;
+				
+			case ApolloSubjectChangedEvent.NAME_CHANGED:
+				if (trackModel != null && !nameLabel.getText().equals(trackModel.getName())) {
+					nameLabel.setText(trackModel.getName());
+				}
+				
+				// Get graph model consistant
+				parent = getParentFrame();
+				String pfname = (parent != null) ?  parent.getName() : null;
+				
+				AudioStructureModel.getInstance().onTrackWidgetNameChanged(
+						localFileName, 
+						pfname, 
+						getName());
+						
+				break;
+				
+			case ApolloSubjectChangedEvent.AUDIO_INSERTED:
+			case ApolloSubjectChangedEvent.AUDIO_REMOVED:
+				
+				long newRunningTime = getRunningMSTimeFromRawAudio();
+				assert(newRunningTime > 0);
+				
+				long oldRunningTime = getRunningMSTimeFromMeta();
+				
+				// Keep meta as constant as possible for best reults
+				updateData(TrackWidgetCommons.META_RUNNINGMSTIME_TAG, TrackWidgetCommons.META_RUNNINGMSTIME_TAG + newRunningTime);
+
+				if (trackModel != null) {
+					
+					parent = getParentFrame();
+					
+					// Keep TrackGraphModel consistant
+					AudioStructureModel.getInstance().onTrackWidgetAudioEdited(
+							localFileName,
+							(parent != null) ? parent.getName() : null, 
+							newRunningTime);
+					
+					if (trackModel.getSelectionStart() == 0 && oldRunningTime > newRunningTime) {
+						long inittime = getInitiationTimeFromMeta();
+						inittime += (oldRunningTime - newRunningTime);
+						updateData(TrackWidgetCommons.META_INITIATIONTIME_TAG, 
+								TrackWidgetCommons.META_INITIATIONTIME_TAG + inittime);
+						
+						AudioStructureModel.getInstance().onTrackWidgetRemoved(localFileName, (parent != null) ? parent.getName() : null);
+						AudioStructureModel.getInstance().onTrackWidgetAnchored(localFileName, (parent != null) ? parent.getName() : null, 
+								inittime, newRunningTime, getName());
+					}
+					
+				}
+				
+				break;
+				
+
+		}
+
+	}
+	
+	public void setObservedSubject(Subject parent) {
+	}
+
+	@Override
+	public void paintInFreeSpace(Graphics g) {
+		paintInFreeSpace(g, false);
+	}
+	
+	public void paintInFreeSpace(Graphics g, boolean isAtFinalPass) { 
+
+		if (isLoaded()) { 
+			
+			
+			if (ExpandedTrackManager.getInstance().isAnyExpandedTrackVisible() &&
+					!isAtFinalPass) {
+				// If a expanded track is in view .. then must render lastly
+				return;
+			} 
+		
+			// Check to see if dragging over a EditableSampledTrackGraphView
+			MouseEvent me = MouseEventRouter.getCurrentMouseEvent();
+			if (me != null) {
+			
+				if (me.getComponent() != fulltrackView && 
+						me.getComponent() instanceof EditableSampledTrackGraphView &&
+						!((EditableSampledTrackGraphView)me.getComponent()).isPlaying()) {
+					
+
+					Point containerPoint = SwingUtilities.convertPoint(me.getComponent(), 
+							new Point(0,0), Browser._theBrowser.getContentPane());
+					
+					Shape clipBackUp = g.getClip();
+					g.setClip(null);
+					
+					g.setColor(Color.ORANGE);
+					((Graphics2D)g).setStroke(EditableSampledTrackGraphView.GRAPH_BAR_STROKE);
+					g.drawLine(
+							containerPoint.x + me.getX(),
+							containerPoint.y, 
+							containerPoint.x + me.getX(),
+							containerPoint.y + me.getComponent().getHeight());
+					
+					FrameGraphics.invalidateArea(new Rectangle(
+							containerPoint.x + me.getX(), 
+							containerPoint.y, 
+							1,
+							containerPoint.y + me.getComponent().getHeight() + 1));
+			
+					// Restore clip
+					g.setClip(clipBackUp);
+					
+					g.setColor(SEMI_TRANSPARENT_FREESPACE_BACKCOLOR);
+					g.fillRect(getX(), getY(), getWidth(), getHeight());
+					
+					if (isAtFinalPass) { // final pass does not draw the borders... so must manually draw them
+						g.setColor(Color.BLACK);
+						((Graphics2D)g).setStroke(FREESPACE_OUTLINING);
+						g.drawRect(getX(), getY(), getWidth(), getHeight());
+					}
+		
+					return;
+				}
+				
+			}
+		
+			
+			
+		}
+		
+		super.paintInFreeSpace(g);
+		
+		if (isLoaded()) {
+			
+			Shape clipBackUp = g.getClip();
+			Rectangle tmpClip = (clipBackUp != null) ? clipBackUp.getBounds() : 
+				new Rectangle(0, 0, 
+						Browser._theBrowser.getContentPane().getWidth(), 
+						Browser._theBrowser.getContentPane().getHeight());
+				
+			g.setClip(tmpClip.intersection(getBounds()));
+		
+			// Draw the name
+			String name = getName();
+			if (name == null) name = "Unnamed";
+
+			g.setFont(TrackWidgetCommons.FREESPACE_TRACKNAME_FONT);
+			g.setColor(TrackWidgetCommons.FREESPACE_TRACKNAME_TEXT_COLOR);
+			
+			// Center track name
+			FontMetrics fm   = g.getFontMetrics(TrackWidgetCommons.FREESPACE_TRACKNAME_FONT);
+			Rectangle2D rect = fm.getStringBounds(name, g);
+			
+			g.drawString(
+					name, 
+					this.getX() + (int)((getWidth() - rect.getWidth()) / 2), 
+					this.getY() + (int)((getHeight() - rect.getHeight()) / 2) + (int)rect.getHeight()
+					);
+			
+			g.setClip(clipBackUp);
+		
+		}
+		
+	}
+	
+	
+
+	@Override
+	public void paint(Graphics g) {
+		super.paint(g);
+		
+		if (isLoaded() && nameLabel != null) {
+			nameLabel.paint(g);
+			
+			if (shouldOmitIndexAudio()) {
+				
+				int shiftOffset = SoundDesk.getInstance().isPlaying(trackMix.getChannelID()) ?
+						-20 : 0;
+				
+				IconRepository.getIcon("omitindexed.png").paintIcon(
+						_swingComponent, 
+						g, 
+						getX() + getWidth() - EditableSampledTrackGraphView.LOCK_ICON_CORNER_OFFSET + shiftOffset, 
+						getY() + EditableSampledTrackGraphView.LOCK_ICON_CORNER_OFFSET - 16);
+				
+			}
+		}
+		
+	}
+	
+	private MouseEvent lastInsertME = null;
+	
+	@Override
+	protected void onParentStateChanged(int eventType) {
+		super.onParentStateChanged(eventType);
+
+		Frame parent = null;
+
+		switch (eventType) {
+
+			// Logic for injecting audio tracks into EditableSampledTrackGraphView's
+			case ItemParentStateChangedEvent.EVENT_TYPE_ADDED:
+			case ItemParentStateChangedEvent.EVENT_TYPE_ADDED_VIA_OVERLAY:
+
+				// Resume any layouts suspended by this track widget
+				FrameLayoutDaemon.getInstance().resumeLayout(this);
+				
+				if (trackModel != null) {
+
+					parent = getParentFrame();
+
+					// Determine new initation time according to anchored position
+					long initTime = (parent != null) ? 
+							FrameLayoutDaemon.getInstance().getMSAtX(getX(), parent)
+							: 0;
+
+					// Keep TrackGraphModel consistant
+					AudioStructureModel.getInstance().onTrackWidgetAnchored(
+							localFileName, 
+							(parent != null) ? parent.getName() : null, 
+							initTime, 
+							AudioMath.framesToMilliseconds(trackModel.getFrameCount(), trackModel.getFormat()),
+							getName());
+					
+					
+					MouseEvent me = MouseEventRouter.getCurrentMouseEvent();
+					
+					if (me != null && me != lastInsertME &&
+							(me.getButton() == MouseEvent.BUTTON2 ||
+									me.getButton() == MouseEvent.BUTTON3)) {
+						
+						// Although widgets filter out multiple parent changed events per mouse event - 
+						// it also considers the event type, and because this it remove itself from the frame/freespace
+						// it jumbles up the filtering so that this will be invoked for each corner.
+						lastInsertME = me;
+					
+						if (me.getComponent() != fulltrackView && 
+								me.getComponent() instanceof EditableSampledTrackGraphView) {
+
+							EditableSampledTrackGraphView editableSampledTrack = 
+								(EditableSampledTrackGraphView)me.getComponent();
+
+							// Inject this into the widget
+							try {
+								injectAudio(editableSampledTrack, me.getX(), true);
+							} catch (IOException ex) {
+								ex.printStackTrace();
+							}
+
+						}
+							
+					} else if (me == lastInsertME) {
+						// Note due to a injection removing this widget while in the midst of
+						// anchoring, the widget parent event filtering will not work thus
+						// must keep removing self until the mouse event has done.
+						removeSelf();
+					}
+				}
+				
+			case ItemParentStateChangedEvent.EVENT_TYPE_SHOWN:
+			case ItemParentStateChangedEvent.EVENT_TYPE_SHOWN_VIA_OVERLAY:
+
+				
+/*
+				// Listen for volume or mute changed events
+				trackMix.addObserver(this);
+				
+				// Listen for solo events and track sequence creation events
+				MixDesk.getInstance().addObserver(this);
+				*/
+				
+			break; 
+			
+
+			case ItemParentStateChangedEvent.EVENT_TYPE_REMOVED:
+			case ItemParentStateChangedEvent.EVENT_TYPE_REMOVED_VIA_OVERLAY:
+
+				// If yanking this from the frame into free space suspend the layout daemon
+				// for this frame
+				if (MouseEventRouter.getCurrentMouseEvent() != null &&
+						MouseEventRouter.getCurrentMouseEvent().getButton() == MouseEvent.BUTTON2 &&
+						MouseEventRouter.getCurrentMouseEvent() != lastInsertME) {
+					Frame suspended = DisplayIO.getCurrentFrame();
+					if (suspended != null) {
+						FrameLayoutDaemon.getInstance().suspendLayout(suspended, this);
+					}
+				}
+				
+				if (trackModel != null) {
+					parent = getParentFrame();
+					// Keep TrackGraphModel consistant
+					AudioStructureModel.getInstance().onTrackWidgetRemoved(localFileName, 
+							(parent != null) ? parent.getName() : null);
+				}
+				
+			case ItemParentStateChangedEvent.EVENT_TYPE_HIDDEN:
+				/*
+				trackMix.removeObserver(this);
+				MixDesk.getInstance().removeObserver(this);*/
+				
+				break; 
+				
+		}
+
+	}
+
+	/**
+	 * Injects this widgets bytes into a EditableSampledTrackGraphView
+	 * and removes this widget from freespace/parent-frame.
+	 * 
+	 * Doesn't inject bytes if the target EditableSampledTrackGraphView is in a playing state.
+	 * 
+	 * @param target
+	 * 		The target EditableSampledTrackGraphView to inject this widgets bytes into
+	 * 
+	 * @param graphX
+	 * 		The X pixel in the target's graph. Must be in valid range.
+	 * 
+	 * @param destroySelf
+	 * 		If want to destroy this widget
+	 * 
+	 * @throws IOException
+	 * 		If the insert failed ... can occur if bytes need to be
+	 * 		converted into targets format.
+	 */
+	public void injectAudio(EditableSampledTrackGraphView target, int graphX, boolean destroySelf)
+		throws IOException {
+		assert(target != null);
+		assert(graphX >= 0);
+		assert(graphX <= target.getWidth());
+		
+		// Cannot inject into EditableSampledTrackGraphView's while playing, although
+		// won't break anything, this would be confusing for the user.
+		if (target.isPlaying()) return;
+		
+		// Inject the audio at the graph poistion
+		int insertFramePoint = target.frameAtX(graphX);
+
+		// Inject audio 
+		target.insertAudio(
+				trackModel.getAllAudioBytes(), 
+				trackModel.getFormat(), 
+				insertFramePoint); 
+
+		// Note: if removed from free space .. then there should be no
+		// more references to this item therefore the memory will be freed
+		// eventually after this invoke
+		if (destroySelf) removeSelf();
+	}
+	
+	/**
+	 * 
+	 * @return
+	 * 		The unique loval filename for this track. Auto-assigned - and rememered. Never null.
+	 */
+	public String getLocalFileName() {
+		return localFileName;
+	}
+
+	/**
+	 * Determines the running time from the raw audio in memory.
+	 * 
+	 * @return
+	 * 		The running time or this track in MS.
+	 * 		-1 if not loaded.
+	 */
+	public long getRunningMSTimeFromRawAudio()
+	{
+		if (this.trackModel != null) {
+			return AudioMath.framesToMilliseconds(trackModel.getFrameCount(), trackModel.getFormat());
+		}
+		
+		return -1;
+	}
+	
+	/**
+	 * Determines the running time from the meta data.
+	 * 
+	 * @return
+	 * 		The running time or this track in MS.
+	 * 		-1 if unavilable.
+	 */
+	public long getRunningMSTimeFromMeta() {
+		return getStrippedDataLong(TrackWidgetCommons.META_RUNNINGMSTIME_TAG, new Long(-1));
+	}
+	
+	/**
+	 * @return
+	 * 		The name given to this widget... can be null.
+	 */
+	public String getName() {
+		if (this.trackModel != null)
+			return trackModel.getName();
+		else if (this.nameLabel != null) {
+			return nameLabel.getText();
+		} 
+		return getStrippedDataString(TrackWidgetCommons.META_NAME_TAG);
+	}
+	
+	/**
+	 * Determines the initiation time from the meta data.
+	 * 
+	 * @return
+	 * 		The initiation time or this track in MS.
+	 * 		null if unavilable.
+	 */
+	public Long getInitiationTimeFromMeta() {
+		return getStrippedDataLong(TrackWidgetCommons.META_INITIATIONTIME_TAG, null);
+	}
+	
+	private void updateBorderColor() {
+		
+		// Get border color currently used
+		Color oldC = getSource().getBorderColor();
+		
+		Color newC = TrackWidgetCommons.getBorderColor(
+				SoundDesk.getInstance().isSolo(trackMix.getChannelID()), 
+				trackMix.isMuted());
+		
+		// Update the color
+		if (!newC.equals(oldC)) {
+			setWidgetEdgeColor(newC);
+		}
+	}
+	
+	/**
+	 * State icons live at the top right corner
+	 *
+	 */
+	private void invalidateStateIcons() {
+		this.invalidateSelf(); // TODO
+	}
+	
+	private void setShouldOmitIndexAudio(boolean shouldOmit) {
+		this.shouldOmitAudioIndexing = shouldOmit;
+		if (!shouldOmit) {
+			removeData(META_DONT_INDEX_AUDIO_TAG);
+		} else {
+			addDataIfCaseInsensitiveNotExists(META_DONT_INDEX_AUDIO_TAG);
+		}
+		invalidateStateIcons();
+	}
+	
+	public boolean shouldOmitIndexAudio() {
+		return shouldOmitAudioIndexing;
+	}
+	
+//	/**
+//	 * Invalidates.
+//	 * @param shouldLayout
+//	 */
+//	private void setShouldLayout(boolean shouldLayout) {
+//		
+//		if (shouldLayout) {
+//			removeData(TrackWidgetCommons.META_OMIT_LAYOUT_TAG);
+//		} else {
+//			addDataIfCaseInsensitiveNotExists(TrackWidgetCommons.META_OMIT_LAYOUT_TAG);
+//		}
+//		
+//		invalidateStateIcons();
+//	}
+//	
+
+//	
+//	public boolean shouldLayout() {
+//		
+//		return (!containsDataTrimmedIgnoreCase(TrackWidgetCommons.META_OMIT_LAYOUT_TAG));
+//	}
+	
+	/**
+	 * Doesn't expand if not anchored ... 
+	 * @param addToExpand
+	 */
+	private void expand(boolean addToExpand) {
+		
+		Frame parent = getParentFrame();
+		String pfname = (parent != null) ?  parent.getName() : null;
+		if (pfname == null) return;
+
+		if (!ExpandedTrackManager.getInstance().isTrackInExpansionSelection(trackModel)) {
+			
+			if (addToExpand) {
+				
+				// Show the expanded view for this track once control has been released
+				ExpandedTrackManager.getInstance().addTrackToSelection(
+						trackModel,
+						pfname,
+						_swingComponent.getBounds(),
+						trackMix);
+
+			} else {
+
+				// Get rid of all popups
+				PopupManager.getInstance().hideAutohidePopups();
+				
+				int start = trackModel.getSelectionStart();
+				int length = trackModel.getSelectionLength();
+				
+				if (length <= 1) {
+					start = 0;
+					length = trackModel.getFrameCount();
+				}
+				
+				// Show the expanded view for this track
+				ExpandedTrackManager.getInstance().expandSingleTrack(
+						trackModel, 
+						_swingComponent.getBounds(),
+						trackMix,
+						pfname,
+						start,
+						length
+						);
+			
+			}
+			
+		} else {
+			
+			// Take away track from being selected.
+			ExpandedTrackManager.getInstance().removeTrackFromSelection(trackModel);
+		}
+		
+	}
+
+	/**
+	 * The small popup for common actions.
+	 * 
+	 * @author Brook Novak
+	 *
+	 */
+	private class PlaybackPopup extends PlaybackControlPopup implements Observer {
+
+		private static final long serialVersionUID = 1L;
+		
+		public PlaybackPopup() {
+			miscButton.setActionCommand("expand");
+			miscButton.setIcon(IconRepository.getIcon("expand.png"));
+			miscButton.setToolTipText("Expand");
+
+		}
+
+		@Override
+		public void onHide() {
+			super.onHide();
+			
+			// Listen for volume or mute changed events
+			trackMix.removeObserver(this);
+			
+			// Listen for solo events and track sequence creation events
+			SoundDesk.getInstance().removeObserver(this);
+		}
+
+		@Override
+		public void onShow() {
+			super.onShow();
+			// Listen for volume or mute changed events
+			trackMix.addObserver(this);
+			
+			// Listen for solo events and track sequence creation events
+			SoundDesk.getInstance().addObserver(this);
+			updateVolume((int)(100 * trackMix.getVolume()));
+			updateMute(trackMix.isMuted());
+			updateSolo(SoundDesk.getInstance().isSolo(trackMix.getChannelID()));
+		}
+
+		public void actionPerformed(ActionEvent e) {
+			if (trackModel == null) return;
+			
+			if (e.getSource() == playPauseButton) {
+
+				try {
+	
+					if (!SoundDesk.getInstance().isPlaying(trackMix.getChannelID())) { // play / resume
+						
+						int startFrame = -1, endFrame = -1;
+						
+						// Resume playback?
+						if (SoundDesk.getInstance().isPaused(trackMix.getChannelID())) {
+							startFrame = SoundDesk.getInstance().getLastPlayedFramePosition(trackMix.getChannelID());
+							if (startFrame >= 0 && startFrame < trackModel.getFrameCount()) {
+				
+								// The user may have edited the audio track and reselected it
+								// since the last pause. Thus select an appropriate end frame
+								endFrame = (trackModel.getSelectionLength() > 1) ?
+										trackModel.getSelectionStart() + trackModel.getSelectionLength():
+											trackModel.getFrameCount() - 1;
+									
+								// Changed selection? it play range invalid?
+								if (endFrame <= startFrame || startFrame < trackModel.getSelectionStart()) {
+									startFrame = -1; // Play new selection (see below)
+								
+								} else if (endFrame >= trackModel.getFrameCount()) {
+									endFrame = trackModel.getFrameCount() - 1;
+								}		
+								
+							}
+						}
+						
+						// Play from beginning of selection to end of selection
+						if (startFrame < 0) {
+							startFrame = trackModel.getSelectionStart();
+							endFrame = (trackModel.getSelectionLength() > 1) ?
+									startFrame + trackModel.getSelectionLength():
+										trackModel.getFrameCount() - 1;
+						}
+	
+						// Safety clamp:
+						if (endFrame >= trackModel.getFrameCount()) {
+							endFrame = trackModel.getFrameCount() - 1;
+						}	
+						
+						if (startFrame < endFrame) {
+							SoundDesk.getInstance().playSampledTrackModel(
+									trackModel, 
+									trackMix.getChannelID(), 
+									startFrame, 
+									endFrame, 
+									0);
+						}
+	
+					} else { // pause
+						
+						TrackSequence ts = SoundDesk.getInstance().getTrackSequence(trackMix.getChannelID());
+
+						if (ts != null &&
+								ts.isPlaying()) {
+							
+							// Mark channel as paused.
+							SoundDesk.getInstance().setPaused(trackMix.getChannelID(), true);
+							
+							// Stop playback for this channel
+							ApolloPlaybackMixer.getInstance().stop(ts);
+							
+						}
+						
+					}
+					
+				} catch (LineUnavailableException e1) {
+					e1.printStackTrace();
+				}
+				
+			} else if (e.getSource() == stopButton) {
+				
+				TrackSequence ts = SoundDesk.getInstance().getTrackSequence(trackMix.getChannelID());
+
+				// reset any paused mark
+				SoundDesk.getInstance().setPaused(trackMix.getChannelID(), false);
+				
+				if (ts != null &&
+						ts.isPlaying()) {
+					// Stop playback
+					ApolloPlaybackMixer.getInstance().stop(ts);
+				}
+				
+			} else if (e.getSource() == rewindButton) {
+				
+				trackModel.setSelection(0, 0);
+				SoundDesk.getInstance().setPaused(trackMix.getChannelID(), false);
+				
+			} else if (e.getSource() == miscButton) {
+				expand(false);
+			}
+		}
+		
+		public Subject getObservedSubject() {
+			return null;
+		}
+
+		public void setObservedSubject(Subject parent) {
+		}
+
+		/**
+		 * Receives events from the track model OR from the observed track sequence.
+		 */
+		public void modelChanged(Subject source, SubjectChangedEvent event) {
+
+			// Synch GUI with track state
+			switch (event.getID()) {
+			
+			case ApolloSubjectChangedEvent.TRACK_SEQUENCE_CREATED: // from sound desk
+				
+				if (event.getState().equals(trackMix.getChannelID())) {
+					// The channel being played is the same as this one ...
+					// even if the track model is unloaded must enter into a playing state
+					// if the created track sequence will play
+					TrackSequence ts = SoundDesk.getInstance().getTrackSequence(trackMix.getChannelID());
+					assert(ts != null);
+					assert(!ts.hasFinished());
+					assert(!ts.isPlaying());
+					ts.addObserver(this);
+				}
+
+				break;
+				
+			case ApolloSubjectChangedEvent.PLAYBACK_STARTED: // From observed track sequence
+				stopButton.setEnabled(true);
+				rewindButton.setEnabled(false);
+				playPauseButton.setIcon(IconRepository.getIcon("pause.png"));
+				
+				invalidateStateIcons();
+
+				SampledTrackEvo4.this.setWidgetEdgeThickness(TrackWidgetCommons.PLAYING_TRACK_EDGE_THICKNESS);
+				//FrameGraphics.refresh(true);
+					break;
+ 
+				case ApolloSubjectChangedEvent.PLAYBACK_STOPPED: // From observed track sequence
+				
+				invalidateStateIcons();
+					
+				rewindButton.setEnabled(true);
+				stopButton.setEnabled(false);
+				playPauseButton.setIcon(IconRepository.getIcon("play.png"));
+				
+				// Note:
+				// No need to remove self from observing the dead track since the track references this
+				// and will get garbage collected
+				
+				SampledTrackEvo4.this.setWidgetEdgeThickness(TrackWidgetCommons.STOPPED_TRACK_EDGE_THICKNESS);
+
+				break;
+				
+			case ApolloSubjectChangedEvent.PAUSE_MARK_CHANGED: // When stopped or paused
+				/*
+				if (ae.getState().equals(trackMix.getChannelID())) {
+					
+					if (MixDesk.getInstance().isPaused(trackMix.getChannelID())) {
+						// Do nothing .. the paused mark is set prior to a stop
+					} else {
+						// Esnure that the GUI represents a stopped state
+						stopButton.setEnabled(false);
+						playPauseButton.setIcon(IconRepository.getIcon("play.png"));
+					}
+					
+				}*/
+				
+				break;
+				
+			case ApolloSubjectChangedEvent.VOLUME: // From obseved track mix
+				updateVolume((int)(100 * trackMix.getVolume()));
+				break;
+				
+			case ApolloSubjectChangedEvent.MUTE: // From obseved track mix
+				updateMute(trackMix.isMuted());
+				updateBorderColor();
+				break;
+				
+			case ApolloSubjectChangedEvent.SOLO_PREFIX_CHANGED: // From mix desk
+				updateSolo(SoundDesk.getInstance().isSolo(trackMix.getChannelID()));
+				updateBorderColor();
+				break;
+			}
+			
+	
+		
+		}
+
+		@Override
+		protected void volumeChanged() {
+			trackMix.setVolume(((float)volumeSlider.getValue()) / 100.0f);
+		}
+
+		@Override
+		protected void muteChanged() {
+			trackMix.setMuted(muteButton.isSelected());
+		}
+
+		@Override
+		protected void soloChanged() {
+			SoundDesk.getInstance().setSoloIDPrefix(soloButton.isSelected() ?
+					trackMix.getChannelID() : null
+					);
+		}
+
+	}
+	
+}
Index: /trunk/src_apollo/org/apollo/widgets/SearchRecorder.java
===================================================================
--- /trunk/src_apollo/org/apollo/widgets/SearchRecorder.java	(revision 315)
+++ /trunk/src_apollo/org/apollo/widgets/SearchRecorder.java	(revision 315)
@@ -0,0 +1,420 @@
+package org.apollo.widgets;
+
+import java.awt.BorderLayout;
+import java.awt.Dimension;
+import java.awt.GridBagConstraints;
+import java.awt.GridBagLayout;
+import java.awt.event.ActionEvent;
+import java.awt.event.ActionListener;
+import java.io.ByteArrayOutputStream;
+import java.io.IOException;
+import java.io.PipedInputStream;
+
+import javax.sound.sampled.AudioFormat;
+import javax.sound.sampled.LineUnavailableException;
+import javax.swing.JButton;
+import javax.swing.JLabel;
+import javax.swing.JPanel;
+import javax.swing.SwingUtilities;
+
+import org.apollo.agents.MelodySearch;
+import org.apollo.audio.ApolloSubjectChangedEvent;
+import org.apollo.audio.AudioCapturePipe;
+import org.apollo.audio.RecordManager;
+import org.apollo.audio.SampledAudioManager;
+import org.apollo.audio.util.MultiTrackPlaybackController;
+import org.apollo.io.IconRepository;
+import org.apollo.mvc.Observer;
+import org.apollo.mvc.Subject;
+import org.apollo.mvc.SubjectChangedEvent;
+import org.apollo.util.AudioSystemLog;
+import org.expeditee.actions.Actions;
+import org.expeditee.gui.DisplayIO;
+import org.expeditee.gui.Frame;
+import org.expeditee.items.ItemParentStateChangedEvent;
+import org.expeditee.items.Text;
+import org.expeditee.items.widgets.InteractiveWidget;
+
+public class SearchRecorder extends InteractiveWidget 
+	implements ActionListener, Observer {
+	
+	private enum WidgetState {
+		Ready, // Waiting to record a querry
+		Recording, // ...
+		Finalizing, // Capture stopped, reading last bytes from pipe
+	}
+	
+	private WidgetState state = null;
+	private RecordedByteReader audioByteReader = null;
+	private long recordStartTime = 0; // system time
+	
+	private JButton recordButton;
+	private JButton commitButton; // stop recording and commence search
+	private JButton cancelButton;
+	JLabel statusLabel;
+
+	private boolean hasExplicityStopped = false;
+	
+	private final static int BUTTON_SIZE = 50;
+	private final static int LABEL_HEIGHT = 30;
+	
+	public SearchRecorder(Text source, String[] args) {
+		super(source, new JPanel(new BorderLayout()),
+				BUTTON_SIZE * 2, 
+				BUTTON_SIZE * 2,
+				BUTTON_SIZE + LABEL_HEIGHT, 
+				BUTTON_SIZE + LABEL_HEIGHT);
+		
+		// Create gui layout
+		recordButton = new JButton();
+		recordButton.setIcon(IconRepository.getIcon("searchmel.png"));
+		recordButton.setToolTipText("Search for audio from live recording");
+		recordButton.addActionListener(this);
+		recordButton.setPreferredSize(new Dimension(BUTTON_SIZE * 2, BUTTON_SIZE));
+
+		commitButton = new JButton();
+		commitButton.setIcon(IconRepository.getIcon("commitquery.png"));
+		commitButton.addActionListener(this);
+		commitButton.setToolTipText("GO!");
+		commitButton.setPreferredSize(new Dimension(BUTTON_SIZE, BUTTON_SIZE));
+		commitButton.setVisible(false);
+		
+		cancelButton = new JButton();
+		cancelButton.setToolTipText("Cancel");
+		cancelButton.setIcon(IconRepository.getIcon("cancel.png"));
+		cancelButton.addActionListener(this);
+		cancelButton.setPreferredSize(new Dimension(BUTTON_SIZE, BUTTON_SIZE));
+
+		
+		statusLabel = new JLabel();
+		statusLabel.setPreferredSize(new Dimension(BUTTON_SIZE * 2, LABEL_HEIGHT));
+		statusLabel.setHorizontalAlignment(JLabel.CENTER);
+		statusLabel.setVerticalAlignment(JLabel.CENTER);
+		
+		// Layout GUI
+		GridBagConstraints c;
+		
+		JPanel buttonPane = new JPanel(new GridBagLayout());
+		buttonPane.setPreferredSize(new Dimension(BUTTON_SIZE * 2, BUTTON_SIZE));
+		
+		c = new GridBagConstraints();
+		c.gridx = 0;
+		c.gridy = 0;
+		c.gridwidth = 2;
+		c.fill = GridBagConstraints.BOTH;
+		buttonPane.add(recordButton, c);
+		
+		c = new GridBagConstraints();
+		c.gridx = 0;
+		c.gridy = 0;
+		c.fill = GridBagConstraints.BOTH;
+		buttonPane.add(commitButton, c);
+		
+		c = new GridBagConstraints();
+		c.gridx = 1;
+		c.gridy = 0;
+		c.fill = GridBagConstraints.BOTH;
+		buttonPane.add(cancelButton, c);
+		
+		// Assemble
+		_swingComponent.add(buttonPane, BorderLayout.CENTER);
+		_swingComponent.add(statusLabel, BorderLayout.SOUTH);
+		
+		_swingComponent.doLayout();
+
+		setState(WidgetState.Ready, "Record Query");
+	}
+	
+	/**
+	 * All of the widget logic is centarlized here - according to the new state transition
+	 * Must be in AWT thread
+	 * 
+	 * @param newState
+	 */
+	private void setState(WidgetState newState, String status) {
+		if (state == newState) return;
+		WidgetState oldState = state;
+		state = newState;
+		
+		statusLabel.setText(status);
+
+		if (newState == WidgetState.Ready) {
+
+			recordButton.setVisible(true);
+			cancelButton.setVisible(false);
+			commitButton.setVisible(false);
+			
+			// Ensure that this is observing the record model
+			SampledAudioManager.getInstance().addObserver(this);
+			
+		} else if (newState == WidgetState.Recording) {
+			assert(oldState == WidgetState.Ready);
+			
+			hasExplicityStopped = false;
+			
+			recordButton.setVisible(false);
+			cancelButton.setVisible(true);
+			commitButton.setVisible(true);
+			cancelButton.setEnabled(true);
+			commitButton.setEnabled(true);
+
+			setStatusLabelToRecordTime();
+			
+		} else if (newState == WidgetState.Finalizing) { // wait for pipe to finish reading bytes
+			
+			// This state can be skipped if pipe has finished before
+			// stop event captured.
+			recordButton.setVisible(false);
+			cancelButton.setVisible(true);
+			commitButton.setVisible(true);
+			cancelButton.setEnabled(false);
+			commitButton.setEnabled(false);
+			
+			// Thread reading from pipe will finish and trigger the finished
+			
+		} else assert(false);
+
+	}
+	
+	private void setStatusLabelToRecordTime() {
+		long elapsed = (System.currentTimeMillis() - recordStartTime);
+		elapsed /= 1000;
+		//statusLabel.setText("Recorded " + elapsed + " seconds");
+		statusLabel.setText("query: " + elapsed + " secs");
+	}
+
+	/**
+	 * {@inheritDoc} 
+	 * SampleRecorderWidget is really stateless - they are means to capture audio only...
+	 * That is, temporary.
+	 */
+	@Override
+	protected String[] getArgs() {
+		return null;
+	}
+	
+	@Override
+	protected void onParentStateChanged(int eventType) {
+		super.onParentStateChanged(eventType);
+
+		switch (eventType) {
+		case ItemParentStateChangedEvent.EVENT_TYPE_HIDDEN:
+		case ItemParentStateChangedEvent.EVENT_TYPE_REMOVED:
+		case ItemParentStateChangedEvent.EVENT_TYPE_REMOVED_VIA_OVERLAY:
+
+			if (state == WidgetState.Recording) {
+				// This will change the state of this widget
+				RecordManager.getInstance().stopCapturing();
+			}
+			
+			// Ensure that this can be disposed
+			RecordManager.getInstance().removeObserver(this);
+			SampledAudioManager.getInstance().removeObserver(this);
+			break;
+
+		case ItemParentStateChangedEvent.EVENT_TYPE_ADDED:
+		case ItemParentStateChangedEvent.EVENT_TYPE_ADDED_VIA_OVERLAY:
+		case ItemParentStateChangedEvent.EVENT_TYPE_SHOWN:
+		case ItemParentStateChangedEvent.EVENT_TYPE_SHOWN_VIA_OVERLAY:
+			RecordManager.getInstance().addObserver(this);
+			MultiTrackPlaybackController.getInstance().addObserver(this);
+			SampledAudioManager.getInstance().addObserver(this);
+			break;
+			
+		}
+
+	}
+
+	public Subject getObservedSubject() {
+		return null;
+	}
+
+	public void setObservedSubject(Subject parent) {
+	}
+
+	public void modelChanged(Subject source, SubjectChangedEvent event) { // subject messages
+
+		switch (event.getID()) {
+		
+		case ApolloSubjectChangedEvent.CAPTURE_STARTED:
+			if (event.getState() == this) {
+				// Change to recording state
+				recordStartTime = System.currentTimeMillis();
+				setState(WidgetState.Recording, "Recording query");
+			}
+			break;
+
+			
+		case ApolloSubjectChangedEvent.CAPTURE_STOPPED:
+			
+			if (state == WidgetState.Recording) {
+				setState(WidgetState.Finalizing, "finalizing");
+			}
+			break;
+			
+
+		}
+		
+	}
+
+	public void actionPerformed(ActionEvent e) { // For action button
+		
+		
+		if (e.getSource() == commitButton) {
+			assert (state == WidgetState.Recording);
+			hasExplicityStopped = true;
+			RecordManager.getInstance().stopCapturing();
+
+		} else if (e.getSource() == recordButton) {
+			assert (state == WidgetState.Ready);
+
+			try {
+				
+				// Start capturing
+				AudioCapturePipe pipe = RecordManager.getInstance().captureAudio(this);
+				
+				// Read bytes asynchronously ... buffer and render visual feedback
+				audioByteReader = new RecordedByteReader(pipe);
+				audioByteReader.start();
+				
+			} catch (LineUnavailableException ex) {
+				AudioSystemLog.printException("Failed to commence audio capture via record widget", ex);
+				setState(WidgetState.Ready, "Bad Device");
+			} catch (IOException ex) {
+				AudioSystemLog.printException("Failed to commence audio capture via record widget", ex);
+				setState(WidgetState.Ready, "Failed");
+			}
+			
+		} else if (e.getSource() == cancelButton) {
+			assert (state == WidgetState.Recording);
+			hasExplicityStopped = false; // not needed but to make clear
+			RecordManager.getInstance().stopCapturing();
+		}
+
+
+	}
+	
+	/**
+	 * Reads bytes asynchronously from a given pipe until it is finished or an exception occurs.
+	 * Once finished excecution, the widget state will be set to finished.
+	 * 
+	 * The bytes read are buffered, rendered and sent to the AnimatedSampleGraph for drawing.
+	 * The status label is also updated according to the record time. 
+	 * 
+	 * @author Brook Novak
+	 *
+	 */
+	private class RecordedByteReader extends Thread {
+		
+		private PipedInputStream pin;
+		private AudioFormat audioFormat;
+		private int bufferSize;
+		public ByteArrayOutputStream bufferedAudioBytes = null; // not null if started
+		private DoUpdateLabel updateLabelTask = new DoUpdateLabel();;
+		
+		RecordedByteReader(AudioCapturePipe pipe)  {
+			assert(pipe != null);
+			this.pin = pipe.getPin();
+			this.audioFormat = pipe.getAudioFormat();
+			this.bufferSize = pipe.getBufferSize();
+		}
+
+		@Override
+		public void run() {
+
+			bufferedAudioBytes = new ByteArrayOutputStream();
+			byte[] buffer = new byte[bufferSize];
+			int len = 0;
+			int bytesBuffered;
+			
+			int lastUpdateStateTime = 0;
+
+			try {
+				while(true) {
+					
+					bytesBuffered = 0;
+					
+					while (bytesBuffered < buffer.length) { // Full the buffer (unless reaches uneven end)
+						len = pin.read(buffer, bytesBuffered, buffer.length - bytesBuffered);
+						if (len == -1) break; // done
+						bytesBuffered += len; 
+					}
+
+					if (bytesBuffered > 0) {
+						// Buffer bytes
+						bufferedAudioBytes.write(buffer, 0, bytesBuffered);
+					}
+					
+					if (len == -1) break; // done
+					
+					// For every second elapsed, update status
+					if ((System.currentTimeMillis() - lastUpdateStateTime) >= 1000) {
+						SwingUtilities.invokeLater(updateLabelTask);
+					}
+				}
+				
+			} catch (IOException e) {
+				e.printStackTrace();
+			} finally {
+				
+				try {
+					pin.close();
+				} catch (IOException e) {
+					e.printStackTrace();
+				}
+				
+				try {
+					bufferedAudioBytes.close();
+				} catch (IOException e) {
+					e.printStackTrace();
+				}
+				
+				// Ensure that will enter finalized state
+				SwingUtilities.invokeLater(new RecoringFinalizer());
+				
+			}
+
+		}
+	}
+	
+	class RecoringFinalizer implements Runnable {
+		
+		public void run() {
+			
+			if (hasExplicityStopped) {
+				
+				Frame sourceFrame = DisplayIO.getCurrentFrame();
+				
+				 if (sourceFrame != null &&
+						 audioByteReader != null && audioByteReader.bufferedAudioBytes != null
+						 && audioByteReader.bufferedAudioBytes.size() > 0) {
+					 
+					// Create melody search agent - setting raw audio for query data
+					MelodySearch melSearchAgent = new MelodySearch();
+					melSearchAgent.useRawAudio(
+							audioByteReader.bufferedAudioBytes.toByteArray(), 
+							 audioByteReader.audioFormat);
+					
+					Text launcherItem = new Text(sourceFrame.getNextItemID(), "Audio Query");
+
+					// Run the melody search agent using recorded audio
+					Actions.LaunchAgent(
+							melSearchAgent, 
+							sourceFrame, 
+							launcherItem);
+				}
+				
+				
+			}
+	
+			setState(WidgetState.Ready, "Record Query");
+		}
+	}
+
+	class DoUpdateLabel implements Runnable {
+		public void run() {
+			setStatusLabelToRecordTime();
+		}
+	}
+
+}
Index: /trunk/src_apollo/org/apollo/widgets/TrackWidgetCommons.java
===================================================================
--- /trunk/src_apollo/org/apollo/widgets/TrackWidgetCommons.java	(revision 315)
+++ /trunk/src_apollo/org/apollo/widgets/TrackWidgetCommons.java	(revision 315)
@@ -0,0 +1,50 @@
+package org.apollo.widgets;
+
+import java.awt.Color;
+import java.awt.Font;
+
+public final class TrackWidgetCommons {
+	private TrackWidgetCommons() {
+	}
+	
+	/** For parsing metadata */
+	public static final String META_NAME_TAG = "name=";
+	public static final String META_INITIATIONTIME_TAG = "it=";
+	public static final String META_RUNNINGMSTIME_TAG = "rt=";
+	public static final String META_LAST_WIDTH_TAG = "lw=";
+	public static final String META_OMIT_LAYOUT_TAG = "dontlayout";
+	
+	public static final int CACHE_DEPTH = 1;
+	
+	public static final Color FREESPACE_TRACKNAME_TEXT_COLOR = Color.WHITE;
+	public static final Font FREESPACE_TRACKNAME_FONT = new Font("Arial", Font.BOLD, 20);
+	
+	public static final int POPUP_LIFETIME = 1500;
+	
+	public static final Color STANDARD_TRACK_EDGE_COLOR = Color.BLACK;
+	public static final Color MUTED_TRACK_EDGE_COLOR = Color.GRAY;
+	public static final Color SOLO_TRACK_EDGE_COLOR = Color.RED;
+	public static final Color MUTED_SOLO_TRACK_EDGE_COLOR = new Color(255, 168, 168);
+	
+	public static final float STOPPED_TRACK_EDGE_THICKNESS = 1.0f;
+	public static final float PLAYING_TRACK_EDGE_THICKNESS = 4.0f;
+	
+
+	public static Color getBorderColor(boolean isSolo, boolean isMuted) {
+		
+		Color newC = null;
+		
+		if (isSolo && isMuted) {
+			newC = TrackWidgetCommons.MUTED_SOLO_TRACK_EDGE_COLOR;
+		} else if (isSolo) {
+			newC = TrackWidgetCommons.SOLO_TRACK_EDGE_COLOR;
+		} else if (isMuted) {
+			newC = TrackWidgetCommons.MUTED_TRACK_EDGE_COLOR;
+		} else {
+			newC = TrackWidgetCommons.STANDARD_TRACK_EDGE_COLOR;
+		}
+		
+		return newC;
+	
+	}
+}
