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.Mutable; import org.apollo.util.TextItemSearchResult; import org.apollo.widgets.LinkedTrack; import org.apollo.widgets.SampledTrack; 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. * * * Thread safe convention: * 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 graphRoots = new HashSet(); // SHARED RESOURCE /** All overdubbed frames loaded in memory. MAP = Framename -> Overdub instance */ private Map allOverdubbedFrames = new HashMap(); // 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; } /** * MUST NOT BE IN THE EXPEDITEE THREAD! OTHERWISE WILL DEFINITELY DEADLOCK * * 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) - * However - 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. * * MUST NOT NE IN THE EXPEDITEE THREAD! OTHERWISE WILL DEFINITELY DEADLOCK * * @param rootFrameName * Must not be null. Mustn't be a link - must be the framename * * @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 newGraph = new HashMap (); 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 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 tracks = null; /* VirtualFilename -> LinkedTrackModelData. */ Map 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 += SampledTrack.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 results = ExpediteeFileTextSearch.prefixSearch( fullPath, new String[] {trackPrefix, linkedTrackPrefix}); // Parse search results tracks = new HashMap(); linkedTracks = new HashMap(); for (TextItemSearchResult result : results) { // Track widget if (result.text.startsWith(trackPrefix)) { String name = null; String localFileName = null; Mutable.Long initiationTime = null; for (String data : result.data) { // read data lines data = data.trim(); if (data.startsWith(SampledTrack.META_LOCALNAME_TAG) && data.length() > SampledTrack.META_LOCALNAME_TAG.length()) { localFileName = data.substring(SampledTrack.META_LOCALNAME_TAG.length()); } else if (data.startsWith(TrackWidgetCommons.META_INITIATIONTIME_TAG) && data.length() > TrackWidgetCommons.META_INITIATIONTIME_TAG.length()) { try { initiationTime = Mutable.createMutableLong(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, result.position.y)); // 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) { Mutable.Long 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 = Mutable.createMutableLong(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, result.position.y)); // 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 = Mutable.createMutableLong(oframe.getFirstInitiationTime()); // Remember: initiation times are relative, so setting to zero // could be obscure. } oframe.addTrack(new TrackGraphNode( tmodel.initiationTime.value, localFilename, tmodel.runningTimeMS, tmodel.name, tmodel.ypos)); } // 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 = Mutable.createMutableLong(oframe.getFirstInitiationTime()); // Remember: initiation times are relative, so setting to zero // could be obscure. } oframe.addLinkedTrack(new LinkedTracksGraphNode( linkedTrackModel.initiationTime.value, od, virtualFilename, linkedTrackModel.name, linkedTrackModel.ypos)); } return oframe; } /** * * @author Brook Novak * */ private class TrackModelData { TrackModelData(Mutable.Long initiationTime, long runningTimeMS, String name, int ypos) { this.initiationTime = initiationTime; this.runningTimeMS = runningTimeMS; this.name = name; this.ypos = ypos; } /** Null if unavilable */ Mutable.Long initiationTime; /** negative if unavailable */ long runningTimeMS; /** Can be null. Trackname */ String name; int ypos; } /** * * @author Brook Novak * */ private class LinkedTrackModelData { LinkedTrackModelData(Mutable.Long initiationTime, String frameLink, String name, int ypos) { assert(frameLink != null); this.initiationTime = initiationTime; this.frameLink = frameLink; this.name = name; this.ypos = ypos; } /** Null if unavilable */ Mutable.Long initiationTime; /** The framename */ String frameLink; /** Can be null. Trackname */ String name; /** The y pixel position */ int ypos; } /** * 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 tracks = null; /** VirtualFilename -> LinkedTrackModelData. Not null if frame was in memory. */ Map 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(); linkedTracks = new HashMap(); for (InteractiveWidget iw : rootFrame.getInteractiveWidgets()) { if (iw instanceof SampledTrack) { SampledTrack sampledTrackWidget = (SampledTrack)iw; TrackGraphNode tinf = AudioStructureModel.getInstance().getTrackGraphInfo( sampledTrackWidget.getLocalFileName(), rootFrameName); Mutable.Long initTime = (tinf != null) ? Mutable.createMutableLong(tinf.getInitiationTime()) : sampledTrackWidget.getInitiationTimeFromMeta(); long runningTime = sampledTrackWidget.getRunningMSTimeFromRawAudio(); if (runningTime <= 0) runningTime = -1; // signify to load from file tracks.put(sampledTrackWidget.getLocalFileName(), new TrackModelData( initTime, runningTime, sampledTrackWidget.getName(), sampledTrackWidget.getY())); // 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); Mutable.Long initTime = (ltinf != null) ? Mutable.createMutableLong(ltinf.getInitiationTime()) : linkedTrackWidget.getInitiationTimeFromMeta(); // Don't consider track-links without any links if (linkedTrackWidget.getLink() != null) { linkedTracks.put(linkedTrackWidget.getVirtualFilename(), new LinkedTrackModelData( initTime, linkedTrackWidget.getLink(), linkedTrackWidget.getName(), linkedTrackWidget.getY())); } } } } } } /** * 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 newGraph; private boolean abortedCommit = false; /** If not empty after the run, then the commit */ Stack loopTrace = new Stack(); NewGraphCommitor(OverdubbedFrame rootODFrame, Map 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 redundantRoots = new LinkedList(); 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 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 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. * MUST BE ON THE SWING THREAD * * @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 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. MUST BE ON EXPEDITEE THREAD * * @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. MUST BE ON EXPEDITEE THREAD * * @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; } /** * MUST BE ON SWING THREAD. * 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()); } } /** * MUST BE ON SWING THREAD. * 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)); } /** * MUST BE ON SWING THREAD. * 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 ypos * The Y-pixel position of the track. * * @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, int ypos) { 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, ypos); 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)); } /** * MUST BE ON SWING THREAD. * 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)); } /** * MUST BE ON SWING THREAD. * 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)); } /** * MUST BE ON SWING THREAD. * Keeps model consistent with expeditee. * * @param localFilename * Must not be null. * * @param parentFrameName * Can be null. * * @param initTime * The initiation time in ms * * @param yPos * The y pixel position. * */ public void onTrackWidgetPositionChanged(String localFilename, String parentFrameName, long initTime, int yPos) { boolean doNotify = false; AbstractTrackGraphNode 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 name in model if (odframe != null) { // is loaded? tinf = odframe.getTrack(localFilename); if (tinf != null && (tinf.getInitiationTime() != initTime || tinf.getYPixelPosition() != yPos)) { tinf.setInitiationTime(initTime); tinf.setYPixelPosition(yPos); // 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_POSITION_CHANGED, tinf)); } /** * MUST BE ON SWING THREAD. * Keeps model consistent with expeditee. * * @param virtualFilename * Must not be null. * * @param parentFrameName * Can be null. * * @param initTime * The initiation time in ms * * @param yPos * The y pixel position. * */ public void onLinkedTrackWidgetPositionChanged(String virtualFilename, String parentFrameName, long initTime, int yPos) { boolean doNotify = false; AbstractTrackGraphNode tinf = null; 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? tinf = odframe.getLinkedTrack(virtualFilename); if (tinf != null && (tinf.getInitiationTime() != initTime || tinf.getYPixelPosition() != yPos)) { tinf.setInitiationTime(initTime); tinf.setYPixelPosition(yPos); // 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_POSITION_CHANGED, tinf)); } /** * MUST BE ON SWING THREAD. * 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); if (tinf != null && 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)); } /** * MUST BE ON SWING THREAD. * 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... * * @param ypos * The Y-pixel position of the track. * */ public void onLinkedTrackWidgetAnchored( String virtualFilename, String parentFrameName, long newInitiationTime, String absoluteLinkedFrame, String name, int ypos) { 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, ypos)); } // 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)); } /** * MUST BE ON SWING THREAD. * 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. * * Also 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. * * MUST NOT BE ON SWING THREAD - OR MAY DEADLOCK * * @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 updateQueue = new LinkedList(); // 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, int ypos) { super(Mutable.createMutableLong(initiationTime), absoluteLink, name, ypos); 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.value, linkedODFrame, updateData.virtualFilename, updateData.name, updateData.ypos); // Check that the graph will be loop free parentFrame.addLinkedTrack(linktInf); // ADDING TEMPORARILY // Check from parent frame Stack loopTrace = new Stack(); 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 } }