view src/uk/ac/qmul/eecs/depic/daw/ClipList.java @ 4:473da40f3d39 tip

added html formatting to Daw/package-info.java
author Fiore Martin <f.martin@qmul.ac.uk>
date Thu, 25 Feb 2016 17:50:09 +0000
parents 629262395647
children
line wrap: on
line source
/*  
 Cross-Modal DAW Prototype - Prototype of a simple Cross-Modal Digital Audio Workstation.

 Copyright (C) 2015  Queen Mary University of London (http://depic.eecs.qmul.ac.uk/)
	
 This program is free software: you can redistribute it and/or modify
 it under the terms of the GNU General Public License as published by
 the Free Software Foundation, either version 3 of the License, or
 (at your option) any later version.

 This program is distributed in the hope that it will be useful,
 but WITHOUT ANY WARRANTY; without even the implied warranty of
 MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
 GNU General Public License for more details.

 You should have received a copy of the GNU General Public License
 along with this program.  If not, see <http://www.gnu.org/licenses/>.
*/
package uk.ac.qmul.eecs.depic.daw;

import java.util.ArrayList;
import java.util.Collection;
import java.util.Collections;
import java.util.Iterator;
import java.util.LinkedList;
import java.util.List;
import java.util.Map;

/**
 * 
 * A list of Clips 
 *
 */
public class ClipList extends  ArrayList<Clip> {
	private static final long serialVersionUID = 1L;
	private int scaleFactor;
	private List<Integer> starts;
	private Map<Sample,WavePeaks> peaksMap;
	private List<SoundWaveListener> changeListeners;
	private ClipListEditor clipListEditor;
	
	public ClipList(int scaleFactor, Map<Sample,WavePeaks> peaksMap,SoundWave wave) {
		this.scaleFactor = scaleFactor;
		this.peaksMap = peaksMap;
		changeListeners = new ArrayList<>(1);
		starts = new ArrayList<>();
		clipListEditor = new ClipListEditor();
	}
	
	/**
	 * Adds a new {@code SampleSelection}. The order of selections is kept (ordered insert).  
	 * 
	 * @param s the new selection to add
	 */

	@Override
	public boolean add(Clip s){
		int index = Collections.binarySearch(starts, s.getStart());
		
		if(index < 0)
			index = ~index;
		
		starts.add(index, s.getStart());
		super.add(index,s);
		return true;
	}
	
	/**
	 * Unsupported method
	 * 
	 * @throws UnsupportedOperationException
	 * 
	 */
	@Override
	public void add(int index, Clip element){
		throw new UnsupportedOperationException();
	}
	
	@Override
	public boolean addAll(Collection<? extends Clip > collection){
		for(Clip c : collection){
			add(c);
		}
		return (!collection.isEmpty());
	}
	
	@Override
	public boolean addAll(int index, Collection<? extends Clip> c){
		throw new UnsupportedOperationException();
	}
	
	@Override
	public boolean 	removeAll(Collection<?> c){
		boolean changed = false;
		for( Object o : c){
			boolean removed = remove(o);
			changed = changed || removed; 
		}
		return changed;
	}
	
	@Override 
	public boolean retainAll(Collection<?> collection){
		List<Clip> newList = new ArrayList<>(size());
		for(Clip c : this){
			if(collection.contains(c)){
				newList.add(c);
			}
		}
		
		if(newList.size() == size())
			return false;
		
		clear();
		addAll(newList);
		return true;
	}

	@Override
	public boolean remove(Object c){
		boolean removed = super.remove(c); 
		if(removed){
			starts.remove(((Clip)c).getStart()); // FIXME boh
		}
		return removed;
	}
	
	
	public void clear(){
		super.clear();
		starts.clear();
	}
	
	public void addSoundWaveListener(SoundWaveListener l ){
		changeListeners.add(l);
	}
	
	public boolean removeSoundWaveListener(SoundWaveListener l ){
		return changeListeners.remove(l);
	}
	
	protected void fireSoundWaveListeners(SoundWaveEvent evt){
		for(SoundWaveListener l : changeListeners){
			l.update(evt);
		}
	}
	
	/**
	 * Returns the length of the SoundWave 
	 * 
	 * @return
	 */
	public int getLength() {
		if(isEmpty())
			return 0;
		else {
			int max = get(0).getEnd();
			for(int i=1; i< size(); i++){
				if(max < get(i).getEnd()){
					max = get(i).getEnd();
				}
			}
			
			return max+1;
		}
	}

	public float getLengthMs(){
		if(isEmpty())
			return 0.0f;
		else{
			float max = get(0).getEndTimeMs();
			for(int i=1; i< size(); i++){
				if(max < get(i).getEndTimeMs()){
					max = get(i).getEndTimeMs();
				}
			}
			
			return max;
		}
	}
	
	public List<Clip> getClipsAt(int pos){ // FIXME maybe find a better algorithm
		if(isEmpty())
			return Collections.emptyList();
		
		List<Clip> clipsAt = new ArrayList<>(size());
		for(Clip s : this){
			/* take advantage of the fact that ClipList is ordered by start */
			if(s.getStart() > pos){
				break;
			}else if(s.contains(pos)){
				clipsAt.add(s);
			}
		}
		
//		if(selections.get(chachedIndex).contains((int)p)){
//			return selections.get(chachedIndex);
//		}else{
//			for(int i=0;i<selections.size();i++){
//				if(selections.get(i).contains(i)){
//					chachedIndex = i;
//					return selections.get(chachedIndex);
//				}
//			}
//		}
		return clipsAt;
	}
	
	public List<Clip> getClipsAtTime(float time){
		if(isEmpty()){
			return Collections.emptyList();
		}
		
		List<Clip> clipsAt = new ArrayList<>(size());
		for(Clip s : this){
			/* clips are ordered by start (and hence startTimeMs) so next clips will all have *
			 * startTimeMs greater than time. Further check is therefore useless              */
			if(s.getStartTimeMs() > time){
				break;
			}else if(s.containsTime(time)){
				clipsAt.add(s);
			}
		}
		return clipsAt;
	}
	
	/**
	 * changes the scale factor of this clip list. All the contained clips will be rescaled according to the new scale factor.
	 * 
	 * @param newScaleFactor
	 */
	public void setScaleFactor(int newScaleFactor){
		for(int i=0; i<size(); i++){
			Clip clip = get(i);
			/* use the Selection static method to convert the clip start and end to the new scale factor values */
			Selection converted = Selection.convertFactor(new Selection(clip.getStart(),clip.getEnd(),scaleFactor), newScaleFactor);
			/* replace the old clip with an update scale factor one */
			set(i, new Clip(
					converted.getStart(),
					converted.getEnd(),
					clip.getSample(),
					Selection.convertFactor(new Selection(clip.getSampleStart(),clip.getEnd(),scaleFactor), newScaleFactor).getStart(),
					clip.getSample().getLength()/peaksMap.get(clip.getSample()).withScaleFactor(newScaleFactor).size())); 
		}
		
		scaleFactor = newScaleFactor;
	}
	
	public int getScaleFactor(){
		return scaleFactor;
	}
	
	public SoundWaveEditor getClipEditor () {
		return clipListEditor;
	}
	
	/* returns a new list obtained by splitting this at the given position */
	private static List<Clip> splitList(int pos, List<Clip> list){
		List<Clip> newList = new LinkedList<>();
		
		for(int i=0; i<list.size(); i++){
			Clip c = list.get(i);
			/* split around getStart() */
			if(c.contains(pos)){
				Clip [] split = c.split(pos);
				newList.add(split[0]);
				if(split.length == 2)
					newList.add(split[1]);
			}else{
				newList.add(c);
			}
		}
		
		return newList;
	}
	
	private class ClipListEditor implements SoundWaveEditor {
		List<Clip> copiedClips;
		Selection copySelection;
		boolean copied;
		
		ClipListEditor(){
			copiedClips = new LinkedList<>();
			copySelection = Selection.ZERO_SELECTION;
		}
		
		@Override
		public boolean cut(SoundWave wave, Selection selection) {
			if(selection == null || selection.getStart() == selection.getEnd()){
				return false;
			}
			
			/* cut also copies in memory */
			copySelection = selection;
			
			/* splits this clipList on start and end of selection and 
			 * saves in copiedClips the clips within the selection   */
			
			/* get the split list, splitting by selection start */
			List<Clip> splitList = ClipList.splitList(selection.getStart(),ClipList.this);
			/* substitute this list with the split list */
			ClipList.this.clear();
			ClipList.this.addAll(splitList);
			
			/* get the split list, splitting by selection end */
			splitList = ClipList.splitList(selection.getEnd(),ClipList.this);
			/* substitute this list with the split list */
			ClipList.this.clear();
			ClipList.this.addAll(splitList);

			copiedClips.clear();
			for(Clip c : ClipList.this){
				/* when a clip is split at position, position will go with the right split           * 
				 * this is why getStart() must be grater and equal and getEnd() lower but not equal  */
				if(c.getStart() >= selection.getStart() && c.getEnd() < selection.getEnd()){
					copiedClips.add(c);
				}
			}
			copied = removeAll(copiedClips);
			
			if(copied){
				fireSoundWaveListeners(new SoundWaveEvent(wave,SoundWaveEvent.CUT,copiedClips));
			}
			return copied; 
		}
	
		@Override
		public boolean copy(SoundWave wave, Selection selection) {
			if(selection == null || selection.getStart() == selection.getEnd()){
				return false;
			}
			/* keep track of the selection used to copy */
			copySelection = selection;
			
			/* splits on the selection start */
			List<Clip> splitList = splitList(selection.getStart(),ClipList.this);
			Iterator<Clip> iterator = splitList.iterator();
			
			/* removes all the clips at the left of selection start */
			while(iterator.hasNext()){
				Clip c = iterator.next();
				if(c.getStart() < selection.getStart()){ // not equal because when splitting at position 
					iterator.remove();                   // position will go with the right split 
				}
			}
			
			/* splits what's left at the selection end */
			splitList = splitList(selection.getEnd(),splitList);
			iterator = splitList.iterator();
			
			/* removes all the clips at the right of selectino end */
			while(iterator.hasNext()){
				Clip c = iterator.next();
				if(c.getEnd() >= selection.getEnd()){
					iterator.remove();
				}
			}
			
			copiedClips = splitList;
			
			if(splitList.isEmpty()){
				return false;
			}else{
				fireSoundWaveListeners(new SoundWaveEvent(wave,SoundWaveEvent.COPY,copiedClips));
				return true;
			}
		}
	
		@Override
		public boolean paste(SoundWave wave, int position) {
			if(copiedClips.isEmpty()){
				return false;
			}

			/* shift the clips to the new position */
			for(Clip c : copiedClips){
				/* shift the clip to the new position. Keeps into account the position relative *
				 * to the original copy selection (c.getStart() - copySelection.getStart()).    *
				 * This is to have the clips original to be pasted as they were in the original *
				 * selection, avoid them to all be to be placed with start at position          */
				Clip newClip = new Clip(c);
				newClip.setStartAt(position + c.getStart() - copySelection.getStart());
				
				/* add the new clips after shifting them to the new position */
				add(newClip);
			}
			
			fireSoundWaveListeners(new SoundWaveEvent(wave,SoundWaveEvent.PASTE,copiedClips));
			return true;
		}
		
		@Override
		public boolean insert(SoundWave wave, int position){
			if(copiedClips.isEmpty()){
				return false;
			}
			
			/* split the list at position */
			List<Clip> splitList = ClipList.splitList(position, ClipList.this);
			
			/* shift right all the clips at the right of position */
			for(Clip c : splitList){
				if(c.getStart() >= position){ // >= because when split at position, position will go to the right split 
					c.shift((int)copySelection.lenght());
				}
			}
			
			/* insert the copied clips */
			for(Clip c : copiedClips){
				/* shift the clip to the new position. Keeps into account the position relative *
				 * to the original copy selection (c.getStart() - copySelection.getStart()).    *
				 * This is to have the clips original to be pasted as they were in the original *
				 * selection, avoid them to all be to be placed with start at position          */
				Clip newClip = new Clip(c);
				newClip.setStartAt(position + c.getStart() - copySelection.getStart());
				
				/* add the new clips after shifting them to the new position */
				splitList.add(newClip);
			}
			
			/* update this clip list with the new list */
			ClipList.this.clear();
			ClipList.this.addAll(splitList);
			
			fireSoundWaveListeners(new SoundWaveEvent(wave,SoundWaveEvent.INSERT,copiedClips));
			return true;
		}
	}
}