/* * Created on Feb 22, 2006 * * Copyright (c) 2006 P.J.Leonard * * 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 */ package com.frinika.sequencer.model; import java.awt.Color; import java.awt.Graphics2D; import java.awt.Rectangle; import java.awt.event.KeyEvent; import java.util.*; import javax.sound.midi.MidiEvent; import javax.sound.midi.Track; import javax.sound.midi.ShortMessage; import javax.swing.JMenuItem; import javax.swing.JPopupMenu; import javax.swing.KeyStroke; import com.frinika.gui.OptionsEditor; import com.frinika.project.gui.ProjectFrame; import com.frinika.sequencer.FrinikaTrackWrapper; import com.frinika.sequencer.gui.menu.RepeatAction; import com.frinika.sequencer.gui.menu.SplitSelectedPartsAction; import com.frinika.sequencer.gui.partview.PartView; import com.frinika.sequencer.model.tempo.TempoList; /** * Contains a List of MultiEvents. * Belongs to a lane. * * @author Paul * */ public class MidiPart extends Part implements EditHistoryRecorder<MultiEvent> { private static final long serialVersionUID = 1L; String name; TreeSet<MultiEvent> multiEvents = new TreeSet<MultiEvent>(); Collection<CommitListener> commitListeners = null; // Jens transient TreeSet<MultiEventEndTickComparable> multiEventEndTickComparables = new TreeSet<MultiEventEndTickComparable>(); public MidiPart() { } /** * Constructor for an MidiPart. * @param lane */ public MidiPart(MidiLane lane) { super(lane); } /** * Rebuild the bounds from the multievent startTicks * */ public void setBoundsFromEvents() { if(multiEvents.size()>0) { setStartTick(multiEvents.first().getStartTick()); if(multiEventEndTickComparables.size()>0) setEndTick(multiEventEndTickComparables.last().getMultiEvent().getEndTick()); else // This should really not happen - but happens for old projects with notes without end ticks (and imported midi files) setEndTick(multiEvents.last().getStartTick()); } else { System.err.println(" Warning attempt to set bounds for an empty part"); setStartTick(0); setEndTick(1); } } /** * Import Midi events into a Part from a section of track. * * Notes with start ticks in the range and inserted and the end events will be found and added. * (So end events may be outside the bounds) * * @param startTickArg start tick (inclusive) * @param endTickArg end tick (exclusive) */ public void importFromMidiTrack(long startTickArg,long endTickArg) { HashMap<Integer,NoteEvent> pendingNoteEvents = new HashMap<Integer,NoteEvent>(); FrinikaTrackWrapper track=((MidiLane) lane).getTrack(); for(int n=0;n<track.size();n++) { MidiEvent event = track.get(n); // Check if note event try { if(event.getMessage() instanceof ShortMessage) { ShortMessage shm = (ShortMessage)event.getMessage(); if(shm.getCommand() == ShortMessage.NOTE_ON || shm.getCommand() == ShortMessage.NOTE_OFF) { // Note off if(shm.getCommand() == ShortMessage.NOTE_OFF || shm.getData2()==0) { // Generate a note event NoteEvent noteEvent = pendingNoteEvents.get(shm.getChannel() << 8 | shm.getData1()); if (noteEvent == null) { System.err.println("NoteOff event without start event, PLEASE FIX ME in MidiPart "); continue; } noteEvent.setEndEvent(event); pendingNoteEvents.remove(shm.getChannel() << 8 | shm.getData1()); multiEvents.add(noteEvent); } else { // Note on if (event.getTick() >=startTickArg && event.getTick() < endTickArg){ pendingNoteEvents.put(shm.getChannel() << 8 | shm.getData1(), new NoteEvent(this,event)); } } } else if(shm.getCommand() == ShortMessage.CONTROL_CHANGE) { if (event.getTick() >=startTickArg && event.getTick() < endTickArg) multiEvents.add(new ControllerEvent(this,event.getTick(),shm.getData1(),shm.getData2())); } else if(shm.getCommand() == ShortMessage.PITCH_BEND) { if (event.getTick() >=startTickArg && event.getTick() < endTickArg) multiEvents.add(new PitchBendEvent(this,event.getTick(),((shm.getData1()) | (shm.getData2() << 7)) & 0x7fff)); } else if(shm.getCommand() == ShortMessage.PROGRAM_CHANGE) { System.out.println(" Discarding program change event "); } // TODO Sysex messages here if (event.getTick() >= endTickArg && pendingNoteEvents.size()==0) break; } } catch(Exception e) { e.printStackTrace(); } } if (pendingNoteEvents.size() != 0 ) { System.err.println(" Some notes did not have a noteoff event "); } for (MultiEvent e:multiEvents) { e.zombie=false; if (e instanceof NoteEvent) { ((NoteEvent)e).validate(); } } rebuildMultiEventEndTickComparables(); setBoundsFromEvents(); } /** * Import Midi events into a Part from a section of track. * * Notes with start ticks in the range and inserted and the end events will be found and added. * (So end events may be outside the bounds) * * @param startTickArg start tick (inclusive) * @param endTickArg end tick (exclusive) */ public void importFromMidiTrack(Track track,long startTickArg,long endTickArg) { HashMap<Integer,NoteEvent> pendingNoteEvents = new HashMap<Integer,NoteEvent>(); // FrinikaTrackWrapper track=((MidiLane) lane).getTrack(); for(int n=0;n<track.size();n++) { MidiEvent event = track.get(n); // Check if note event try { if(event.getMessage() instanceof ShortMessage) { ShortMessage shm = (ShortMessage)event.getMessage(); if(shm.getCommand() == ShortMessage.NOTE_ON || shm.getCommand() == ShortMessage.NOTE_OFF) { // Note off if(shm.getCommand() == ShortMessage.NOTE_OFF || shm.getData2()==0) { // Generate a note event NoteEvent noteEvent = pendingNoteEvents.get(shm.getChannel() << 8 | shm.getData1()); if (noteEvent == null) { System.err.println("NoteOff event without start event, PLEASE FIX ME in MidiPart "); continue; } noteEvent.setEndEvent(event); pendingNoteEvents.remove(shm.getChannel() << 8 | shm.getData1()); multiEvents.add(noteEvent); } else { // Note on if (event.getTick() >=startTickArg && event.getTick() < endTickArg){ pendingNoteEvents.put(shm.getChannel() << 8 | shm.getData1(), new NoteEvent(this,event)); } } } else if(shm.getCommand() == ShortMessage.CONTROL_CHANGE) { if (event.getTick() >=startTickArg && event.getTick() < endTickArg) multiEvents.add(new ControllerEvent(this,event.getTick(),shm.getData1(),shm.getData2())); } else if(shm.getCommand() == ShortMessage.PITCH_BEND) { if (event.getTick() >=startTickArg && event.getTick() < endTickArg) multiEvents.add(new PitchBendEvent(this,event.getTick(),((shm.getData1()) | (shm.getData2() << 7)) & 0x7fff)); } // TODO Sysex messages here if (event.getTick() >= endTickArg && pendingNoteEvents.size()==0) break; } } catch(Exception e) { e.printStackTrace(); } } if (pendingNoteEvents.size() != 0 ) { System.err.println(" Some notes did not have a noteoff event "); } for (MultiEvent e:multiEvents) { e.zombie=false; if (e instanceof NoteEvent) { ((NoteEvent)e).validate(); } } rebuildMultiEventEndTickComparables(); setBoundsFromEvents(); } public String getName() { return name; } public void setName(String name) { this.name = name; } /** * Add a MultiEvent to the track. * The client is responsible for adjusting the Part bounds is need be. * * @param ev * @return */ public void add(MultiEvent ev) { ev.part = this; ev.commitAdd(); // TODO this should not be here ? /* if (startTick > ev.getStartTick()) startTick = ev.getStartTick(); if (endTick < ev.getEndTick()) endTick = ev.getEndTick(); */ multiEvents.add(ev); if(multiEventEndTickComparables != null) multiEventEndTickComparables.add(ev.getMultiEventEndTickComparable()); setChanged(); if (lane != null) getEditHistoryContainer().push(this,EditHistoryRecordableAction.EDIT_HISTORY_TYPE_ADD, ev); // System. out.println("added "+ev.toString()); } /** * Remove a MultiEvent from the track * * @param multiEvent * @return */ public void remove(MultiEvent multiEvent) { multiEvent.commitRemove(); multiEvents.remove(multiEvent); if(multiEventEndTickComparables != null) multiEventEndTickComparables.remove(multiEvent.getMultiEventEndTickComparable()); lane.project.getMultiEventSelection().removeSelected(multiEvent); setChanged(); getEditHistoryContainer().push(this,EditHistoryRecordableAction.EDIT_HISTORY_TYPE_REMOVE, multiEvent); // System. out.println("removed "+multiEvent.toString()); } /* protected void attachMultiEvents() { if (multiEvents == null ) return; for (MultiEvent e:multiEvents) { e.commitAdd(); } }*/ public void commitEventsRemove() { if (multiEvents == null ) return; for (MultiEvent e:multiEvents) { if (!e.isZombie()) e.commitRemove(); } } // /** // * 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 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 multiEvents.subSet(new SubsetMultiEvent(startTick),new SubsetMultiEvent(endTick)); } public FrinikaTrackWrapper getTrack() { return ((MidiLane) lane).getTrack(); } /* * public EditHistoryContainer getEditHistoryContainer() { return * track.getEditHistoryContainer(); } * * public Sequence getSequence() { return track.getSequence(); } */ public int getMidiChannel() { return getTrack().getMidiChannel(); } public EditHistoryContainer getEditHistoryContainer() { return getLane().getProject().getEditHistoryContainer(); } /** * Make sure part is detached before calling this then reattach after * This operation does not change the database rep so we do not call setChanged() * */ @Override protected void moveItemsBy(long deltaTick) { Vector<MultiEvent> list=new Vector<MultiEvent>(); for(MultiEvent ev:multiEvents) { list.add(ev); } for (MultiEvent ev:list) { long newTick = deltaTick+ev.getStartTick(); remove(ev); ev.setStartTick(newTick); add(ev); } } public void moveContentsBy(double dTick,Lane dstLane) { setStartTick (getStartTick() + dTick); setEndTick(getEndTick() + dTick); commitEventsRemove(); if (dstLane != lane) { lane.getParts().remove(this); dstLane.getParts().add(this); lane=dstLane; } for (MultiEvent ev : multiEvents) { long newTick = (long)(dTick + ev.getStartTick()); ev.setStartTick(newTick); } commitEventsAdd(); } public void restoreFromClone(EditHistoryRecordable o) { MidiPart clone=(MidiPart)o; lane=clone.lane; setStartTick(clone.getStartTick()); setEndTick(clone.getEndTick()); // selection selected=false; // clone.selected; } @Override public Object clone() throws CloneNotSupportedException { Part clone=new MidiPart(); clone.lane= lane; clone.setStartTick(getStartTick()); clone.setEndTick(getEndTick()); clone.selected = false; // database history fields //clone.editParent=editParent; //clone.rootPart=rootPart; //clone.partResourceId=partResourceId; return clone; } @Override /** * Generate native MIDI event out of generic Frinika MultiEvents */ public void commitEventsAdd() { if (multiEvents == null ) return; // System.out.println("Committing " + multiEvents.size() + " events"); for(MultiEvent multiEvent : multiEvents) { long tick=multiEvent.getStartTick(); if (tick >= getStartTick() && tick < getEndTick()) multiEvent.commitAdd(); } } @Override public void copyBy(double deltaTick,Lane dst) { MidiPart part = new MidiPart((MidiLane)dst); Collection<MultiEvent> events=getMultiEvents(); part.setStartTick(getStartTick()+deltaTick); part.setEndTick(getEndTick()+deltaTick); for (MultiEvent ev:events) { try { MultiEvent newEv=(MultiEvent)ev.clone(); double newTick = deltaTick+ev.getStartTick(); newEv.setStartTick((long)newTick); part.add(newEv); } catch (CloneNotSupportedException e) { // TODO Auto-generated catch block e.printStackTrace(); } } part.rootPart=rootPart; part.partResourceId=partResourceId; part.editParent=editParent; } public Selectable deepCopy(Selectable parent) { MidiPart clone; if (parent != null) { clone = (MidiPart) ((MidiLane)parent).createPart(); }else { clone=new MidiPart(); } clone.setStartTick(getStartTick()); clone.setEndTick(getEndTick()); if (parent == null) { clone.lane=lane; } clone.name="Copy of "+name; clone.color=color; clone.rootPart=this.rootPart; clone.partResourceId=this.partResourceId; clone.editParent=this.editParent; for (MultiEvent ev:multiEvents) { clone.multiEvents.add((MultiEvent)ev.deepCopy(clone)); } return clone; } public void deepMove(long tick) { Collection<MultiEvent> events=getMultiEvents(); for (MultiEvent ev:events) { ev.deepMove(tick); } setStartTick(getStartTick()+tick); setEndTick(getEndTick() +tick); } public void rebuildMultiEventEndTickComparables() { multiEventEndTickComparables = new TreeSet<MultiEventEndTickComparable>(); for(MultiEvent multiEvent : multiEvents) { multiEventEndTickComparables.add(multiEvent.getMultiEventEndTickComparable()); } //setBoundsFromEvents(); } /** * deprecated *//* public Rectangle getEventBounds() { Rectangle rect=new Rectangle(); rect.x=(int) startTick; rect.width=(int) (endTick-startTick); int low=128; int high=0; for(MultiEvent ev:multiEvents) { if (!ev.isZombie() && (ev instanceof NoteEvent)){ int pit=((NoteEvent)ev).getNote(); if (pit > high) high=pit; if (pit < low) low=pit; } } rect.y=low; rect.height=high-low; // TODO Auto-generated method stub return rect; } */ public int[] getPitchRange() { int low=128; int high=0; for(MultiEvent ev:multiEvents) { if (!ev.isZombie() && (ev instanceof NoteEvent)){ int pit=((NoteEvent)ev).getNote(); if (pit > high) high=pit; if (pit < low) low=pit; } } int [] ret={low,high}; return ret; } /** * Commit the MultiEvents as MidiEvents to a Sequencers Track event list. */ public void onLoad() { // System.out.println(" On load 1"); commitEventsAdd(); // System.out.println(" On load 2"); rebuildMultiEventEndTickComparables(); // System.out.println(" On load 3"); } // @Override // public void attach() { // // TODO Auto-generated method stub // // } // // @Override // public void detach() { // // TODO Auto-generated method stub // // } public void drawThumbNail(Graphics2D g, Rectangle rect,PartView panel) { TempoList tl=lane.getProject().getTempoList(); for (MultiEvent e : getMultiEvents()) { double x = tl.getTimeAtTick(e.getStartTick()); if (e instanceof NoteEvent) { double w = tl.getTimeAtTick(e.getEndTick()); int note = ((NoteEvent) e).getNote(); int y = (int) ((rect.y + (rect.height * (128 - note)) / 128.0)); if (e.isZombie()) g.setColor(Color.WHITE); else g.setColor(Color.BLACK); g.drawLine((int)panel.userToScreen(x), y, (int)panel.userToScreen(w), y); } } } public void addCommitListener(CommitListener l) { if (commitListeners == null) { // auto-init commitListeners = new HashSet<CommitListener>(); } commitListeners.add(l); } public void removeCommitListener(CommitListener l) { commitListeners.remove(l); } void fireCommitAdd(MultiEvent event) { if (commitListeners == null) return; for (CommitListener l : commitListeners) { l.commitAddPerformed(event); } } void fireCommitRemove(MultiEvent event) { if (commitListeners == null) return; for (CommitListener l : commitListeners) { l.commitRemovePerformed(event); } } // --- context menu ------------------------------------------------------ /** * Fills the part's context menu with menu-items. * * @param popup */ @Override protected void initContextMenu(final ProjectFrame frame, JPopupMenu popup) { JMenuItem item = new JMenuItem(new RepeatAction(frame)); //item.setText(item.getText()+"..."); // hack item.setMnemonic(KeyEvent.VK_R); item.setAccelerator(KeyStroke.getKeyStroke(KeyEvent.VK_R, 0)); popup.add(item); item = new JMenuItem(new SplitSelectedPartsAction(frame)); popup.add(item); super.initContextMenu(frame, popup); } // --- properties panel -------------------------------------------------- /** * Create PropertiesPanel. * * @param frame * @return */ @Override protected OptionsEditor createPropertiesPanel(ProjectFrame frame) { return new MidiPartPropertiesPanel(frame); } // --- inner class --- /** * Instance returned via createProperitesPanel(). * * This is an example how type-specific Properties-Panels can be built. * Currently, this just inherits all defaults. */ protected class MidiPartPropertiesPanel extends PropertiesPanel { /** * Constructor. * * @param frame */ protected MidiPartPropertiesPanel(ProjectFrame frame) { super(frame); } /** * Fills the panel with gui elements for editing the part's properties. */ @Override protected void initComponents() { super.initComponents(); // do additional things here //GridBagConstraints gc = new GridBagConstraints(); //gc.gridwidth = GridBagConstraints.REMAINDER; //this.add(new JLabel("MIDI-PART TEST"), gc); } /** * Refreshes the GUI so that it reflects the model's current state. */ @Override public void refresh() { super.refresh(); // do additional things here } /** * Updates the model so that it contains the values set by the user. */ @Override public void update() { super.update(); // do additional things here } } }