/*
* Created on Jul 5, 2005
*
* Copyright (c) 2005 Peter Johan Salomonsen (http://www.petersalomonsen.com)
*
* http://www.frinika.com
*
* This file is part of Frinika.
*
* Frinika 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 2 of the License, or
* (at your option) any later version.
*
* Frinika 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 Frinika; if not, write to the Free Software
* Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
*/
/**
* Must be in the javax.sound.midi package because the constructor is package-private
*/
package com.frinika.sequencer;
import java.util.HashMap;
import java.util.SortedMap;
import java.util.TreeMap;
import java.util.Vector;
import javax.sound.midi.*;
import com.frinika.sequencer.model.EditHistoryContainer;
/**
* An extended version of the Track class in javax.sound.midi. The purpose of the extension
* is to make the Sequencer player able to request all events for a given tick, rather than
* having to perform a binary search, or keeping a track index to get them.
*
* For the track mapping to remain consistent - it is important NOT to modify the MidiEvent
* objects separately. If a MIDI event needs to be changed, it should be removed and then
* added again to this Track.
*
* @author Peter Johan Salomonsen
*/
public class FrinikaTrackWrapper {
private HashMap<Long,Vector<MidiEvent>> tickMap = new HashMap<Long,Vector<MidiEvent>>();
private HashMap<Integer,SortedMap<Long,Integer>> controllerMap = new HashMap<Integer,SortedMap<Long,Integer>>();
private SortedMap<Long,MidiMessage> tempoMap = new TreeMap<Long,MidiMessage>();
Track track;
Vector<MidiMessage> controllerStateMessages;
private FrinikaSequence sequence;
/**
* Indicates that the channel defined in the multievents should not be
* overridden by the channel set in this track
*/
public static final int CHANNEL_FROM_EVENT = -1;
/**
* Target MIDI out device
*/
MidiDevice midiDevice = null;
/**
* Target midi cannel;
*/
int midiChannel = CHANNEL_FROM_EVENT;
/**
* Construct a FrinikaTrackWrapper to encapsulate a JavaSound track.
* @param track The JavaSound track to wrap around
*/
FrinikaTrackWrapper(Track track) {
this.track = track;
for(int n=0;n<track.size();n++)
{
MidiEvent event = track.get(n);
addEventToTickMap(event);
}
controllerStateMessages = new Vector<MidiMessage>();
controllerStateMessages.ensureCapacity(128);
}
/**
*
* clear this track of all events
* except the "META END OF TRACK"
*
* (PJL tempo track rebuild)
*/
public void clear() {
int N=track.size();
for(int n=N-2;n>=0;n--)
{
MidiEvent event = track.get(n);
// assert(!MidiUtils.isMetaEndOfTrack(event.getMessage()));
track.remove(event);
}
tickMap.clear();
controllerMap.clear();
}
/**
* Add a MIDI event to the tickMap so that the TrackWrapper can keep track on which events
* are located on what tick.
* @param event
*/
private void addEventToTickMap(MidiEvent event)
{
if(!tickMap.containsKey(event.getTick()))
{
tickMap.put(event.getTick(),new Vector<MidiEvent>());
}
tickMap.get(event.getTick()).add(event);
addToControllerMap(event);
// addToTempoMap(event);
}
// /**
// * Tempo events will be added to the tempomap, so that the trackwrapper can keep track of
// * controller state at any point of time in the sequence
// * @param event
// */
// private void addToTempoMap(MidiEvent event)
// {
// try
// {
// ShortMessage shm = (ShortMessage)event.getMessage();
//
// if(shm.getCommand()==ShortMessage.CONTROL_CHANGE)
// {
// //System.out.printf("CC ch %h ctrl %d \n",shm.getChannel(),shm.getData1());
//
// int ccKey = ((shm.getChannel()& 0xf) << 8) | (shm.getData1() & 0xff);
//
// SortedMap<Long,Integer> ccValues;
// ccValues = controllerMap.get(ccKey);
// if(ccValues == null)
// {
// ccValues = new TreeMap<Long,Integer>();
// controllerMap.put(ccKey,ccValues);
// }
//
// ccValues.put(event.getTick(),shm.getData2());
//
// //System.out.printf("ch %h ctrl %d ",shm.getChannel(),shm.getData1());
// //System.out.printf("channel %h controller %d ",(ccKey>>8) & 0xf,ccKey & 0xff);
// //System.out.println(ccValues);
// }
// } catch(Exception e) {
// // e.printStackTrace();
// }
// }
//
// /**
// * Remove an event from the tempoMap
// * @param event
// */
// private void removeFromTempoMap(MidiEvent event)
// {
// tempoMap.remove(event.getTick());
// }
//
//
/**
* Controller events will be added to the controlmap, so that the trackwrapper can keep track of
* controller state at any point of time in the sequence
* @param event
*/
private void addToControllerMap(MidiEvent event)
{
try
{
ShortMessage shm = (ShortMessage)event.getMessage();
if(shm.getCommand()==ShortMessage.CONTROL_CHANGE)
{
//System.out.printf("CC ch %h ctrl %d \n",shm.getChannel(),shm.getData1());
int ccKey = ((shm.getChannel()& 0xf) << 8) | (shm.getData1() & 0xff);
SortedMap<Long,Integer> ccValues;
ccValues = controllerMap.get(ccKey);
if(ccValues == null)
{
ccValues = new TreeMap<Long,Integer>();
controllerMap.put(ccKey,ccValues);
}
ccValues.put(event.getTick(),shm.getData2());
//System.out.printf("ch %h ctrl %d ",shm.getChannel(),shm.getData1());
//System.out.printf("channel %h controller %d ",(ccKey>>8) & 0xf,ccKey & 0xff);
//System.out.println(ccValues);
}
} catch(Exception e) {
// e.printStackTrace();
}
}
/**
* Remove an event from the controllerMap
* @param event
*/
private void removeFromControllerMap(MidiEvent event)
{
try
{
ShortMessage shm = (ShortMessage)event.getMessage();
if(shm.getCommand()==ShortMessage.CONTROL_CHANGE)
{
int ccKey = ((shm.getChannel()& 0xf) << 8) | (shm.getData1() & 0xff);
controllerMap.get(ccKey).remove(event.getTick());
}
} catch(Exception e) {}
}
/**
* Add a MidiEvent to the track - note that you should normally use MultiEvents instead of MidiEvents
* @param event
* @return
*/
public synchronized boolean add(MidiEvent event) {
if(track.add(event))
{
addEventToTickMap(event);
return true;
}
else
return false;
}
/**
* Remove a MidiEvent from the track - Note that you should normally use MultiEvents instead of MidiEvents
* @param event
* @return
*/
public synchronized boolean remove(MidiEvent event) {
if(track.remove(event))
{
Vector<MidiEvent> tt=tickMap.get(event.getTick());
if (tt == null) {
System.out.println( "oops tickMap did not have event vector ");
}
tt.remove(event);
removeFromControllerMap(event);
return true;
}
else
return false;
}
/**
* Add a MultiEvent to the track
* @param multiEvent
* @return
*/
/* public void add(MultiEvent multiEvent)
{
// multiEvent.multiEventGroup = defaultGroup;
disableEditHistoryContainer(); // While we add the generated MidiEvents to the track (we don't want to record that as MidiEvents)
multiEvent.commitAdd();
enableEditHistoryContainer();
defaultGroup.multiEvents.add(multiEvent);
if(editHistoryContainer!=null)
editHistoryContainer.push(EditHistoryEntry.EDIT_HISTORY_TYPE_ADD, this, multiEvent);
System.out.println("added "+multiEvent.toString());
}
*/
/**
* Remove a MultiEvent from the track
* @param multiEvent
* @return
*/
/* public void remove(MultiEvent multiEvent)
{
disableEditHistoryContainer(); // While we remove the generated MidiEvents from the track (we don't want to record that as MidiEvents)
multiEvent.commitRemove();
enableEditHistoryContainer();
defaultGroup.multiEvents.remove(multiEvent);
if(editHistoryContainer!=null)
editHistoryContainer.push(EditHistoryEntry.EDIT_HISTORY_TYPE_REMOVE, this, multiEvent);
System.out.println("removed "+multiEvent.toString());
}*/
/**
* Register updates on a MultiEvent
* @param multiEvent
*/
/* public void update(MultiEvent multiEvent)
{
remove(multiEvent);
add(multiEvent);
}
*/
/**
* Returns the multievent array.
* @return
*/
/* public SortedSet<MultiEvent> getMultiEvents()
{
return defaultGroup.multiEvents;
}
*/
/**
* Returns a subset of the multievent array including startTick excluding endTick
* @param startTick
* @param endTick
* @return
*/
/* public SortedSet<MultiEvent> getMultiEventSubset(long startTick, long endTick)
{
return defaultGroup.multiEvents.subSet(new SubsetMultiEvent(startTick),new SubsetMultiEvent(endTick));
}*/
/**
* Return number of MidiEvents in track
* @return
*/
public int size()
{
return track.size();
}
/**
* Return MidiEvent at a given index in the track
* @param index
* @return
*/
public MidiEvent get(int index)
{
return(track.get(index));
}
/**
* Used by the sequencer player to get the messages it should play at a certain position
* @param tick
* @return
*/
public Vector<MidiEvent> getEventsForTick(long tick)
{
return tickMap.get(tick);
}
/**
* Return a list of midimessages in order to restore controller states at a specific tick.
* Used when looping a sequence, or when starting playback in the middle of the song.
* This is a replacement for the chasing mechanism in the JSE RealtimeSequencer implementation.
* @param tick
* @return
*/
public synchronized Vector<MidiMessage> getControllerStateAtTick(long tick)
{
//long timeSpent = System.currentTimeMillis();
controllerStateMessages.clear();
for(int ccKey : controllerMap.keySet())
{
SortedMap<Long,Integer> ccValues = controllerMap.get(ccKey);
try
{
//System.out.printf("channel %h controller %d ",(ccKey>>8) & 0xf,ccKey & 0xff);
//System.out.println(ccValues);
int ccValue = ccValues.get(ccValues.headMap(tick).lastKey());
ShortMessage shm = new ShortMessage();
shm.setMessage(ShortMessage.CONTROL_CHANGE,(ccKey>>8) & 0xf,ccKey & 0xff,ccValue);
//System.out.println(ccKey+" "+((ccKey>>8) & 0xf)+" "+(ccKey & 0xff)+" "+ccValue);
controllerStateMessages.add(shm);
} catch(Exception e) {
}
}
//timeSpent = System.currentTimeMillis()-timeSpent;
//System.out.println("ControllerState chasing time : "+timeSpent+" ms");
return controllerStateMessages;
}
/**
* @return Returns the sequence.
*/
public FrinikaSequence getSequence() {
return sequence;
}
public void setSequence(FrinikaSequence sequence) {
this.sequence = sequence;
}
/**
* @return Returns the midi channel for this track
*/
public int getMidiChannel() {
return midiChannel;
}
/**
* Set the midi channel for this track
* @param midiChannel
*/
public void setMidiChannel(int midiChannel) {
this.midiChannel = midiChannel;
}
/**
* @return Returns the midiDevice.
*/
public MidiDevice getMidiDevice() {
return midiDevice;
}
/**
* @param midiDevice The midiDevice to set.
*/
public void setMidiDevice(MidiDevice midiDevice) {
this.midiDevice = midiDevice;
}
public EditHistoryContainer getEditHistoryContainer()
{
System.err.println(" Edit hostory does not live here any more (FrinikaSequence) ");
return null;
}
public void attachToSequence() {
// TODO should this next if happen ? It does but should it?
if (sequence.frinikaTrackWrappers.contains(this)) return;
sequence.frinikaTrackWrappers.add(this);
}
public void detachFromSequence() {
sequence.frinikaTrackWrappers.remove(this);
}
public long lastTickUsed() { // Jens
int s = track.size();
if (s > 1) {
MidiEvent lastMidiEvent = track.get( s - 2 );
long tick = lastMidiEvent.getTick();
return tick;
} else {
return 0;
}
}
}