/* * Created on Feb 16, 2007 * * Copyright (c) 2007 Jens Gulden * * 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.project.scripting.javascript; import static com.frinika.localization.CurrentLocale.getMessage; import com.frinika.project.ProjectContainer; import com.frinika.project.gui.ProjectFrame; import com.frinika.project.scripting.FrinikaScriptingEngine; import com.frinika.project.scripting.gui.ScriptingDialog; import com.frinika.project.scripting.javascript.JavascriptScope; import com.frinika.sequencer.FrinikaSequencer; import com.frinika.sequencer.SongPositionListener; import com.frinika.sequencer.gui.transport.StartAction; import com.frinika.sequencer.gui.transport.StopAction; import com.frinika.sequencer.gui.transport.RecordAction; import com.frinika.sequencer.gui.transport.RewindAction; import com.frinika.sequencer.gui.menu.DeleteAction; import com.frinika.sequencer.gui.menu.midi.MidiStepRecordAction; import com.frinika.sequencer.model.MidiLane; import com.frinika.sequencer.model.AudioLane; import com.frinika.sequencer.model.TextLane; import com.frinika.sequencer.model.MidiPart; import com.frinika.sequencer.model.AudioPart; import com.frinika.sequencer.model.TextPart; import com.frinika.sequencer.model.MultiEvent; import com.frinika.sequencer.model.NoteEvent; import com.frinika.sequencer.model.ControllerEvent; import com.frinika.sequencer.model.SysexEvent; import com.frinika.sequencer.model.Ghost; import com.frinika.sequencer.model.util.TimeUtils; import org.mozilla.javascript.Context; import org.mozilla.javascript.FunctionObject; import org.mozilla.javascript.ScriptableObject; import java.awt.Component; import javax.swing.JMenuBar; import javax.swing.JMenu; import javax.swing.JMenuItem; import java.io.File; import java.io.FileReader; import java.io.FileWriter; import java.io.IOException; import java.lang.reflect.Method; import java.util.*; /** * This class provides the bridge between Frinika and the Rhino Javascript * engine. It maps important parts of Frinika's object structure and features to * Javascript variables and functions. * * <h1>JavaScript Reference</h1> * * The Frinika scripting engine uses a JavaScript engine which implements the * ECMAScript for XML (E4X). For detailed information on the * ECMA-script standard and provided language features, refer to the documentation * available at <a href="http://developer.mozilla.org/en/docs/JavaScript_Language_Resources">JavaScript Language Resources</a>: * <ul> * <li><a href="http://www.mozilla.org/js/language/E262-3.pdf">Third revision of the ECMAScript standard, corresponds to JavaScript 1.5</a></li> * <li><a href="http://www.mozilla.org/js/language/ECMA-357.pdf">ECMAScript for XML (E4X)</a></li> * </ul> * * As addition to the core language capabilities provided by the ECMA-script standard, * the following functions, objects and variables are provided as specific * additions to interface between Frinika and JavaScript. * * * <h2>Global Functions</h2> * * Some built-in top-level functions specific to Frinika are available within * JavaScript. Top-level funcitons are not bound to any object, which means they * can be invoked directly using statement as "myfunction(args1, args2)", * without any priop object reference (such as "myObject.myfunction(arg)"). * * <h4><code>print(string)</code></h4> * * Prints a string to the scripting console. * * <h4><code>println(string)</code></h4> * * Prints a string to the scripting console, and issues a line-feed afterwards. * Note: due to internal limitations it is not possible to print the string * "undefined" using println(). If you expect a variable x to carry the value * undefined, use print(x); println(); instead. * * <h4><code>println()</code></h4> * * Issues a line-feed to the scripting console. * * <h4><code>message(string)</code></h4> * * Displays a message in a pop-up-dialog. * * <h4><code>error(string)</code></h4> * * Displays an error-message in a pop-up-dialog. * * <h4><code>confirm(string)</code> (= <code>boolean</code>)</h4> * * Displays a pop-up-dialog and asks the user to click either "Ok" or "Cancel". * If Ok is clicked, the return value will be true. * * <h4><code>prompt(string)</code> (= <code>string</code>)</h4> * * Displays a pop-up-dialog and asks the user to enter a string. * * <h4><code>promptFile(defaultFilename, suffices, saveMode)</code> (= <code>string</code>)</h4> * * Asks the user to select a filename or directory.<br> * defaultFilename may be undefined.<br> * suffices (optional) is a semicolon-seperated string containing possible file suffices to select plus optional textual description (e.g. "jpg JPEG Picture,png Portable Network Graphics,gif,svg Scalable Vector Graphics"). * If saveMode is <code>true</code> and the user chooses a non-existing file without any of these suffices, the first one will automatically be added. * In a special mode, suffices can be set to "<dir>" to indicate the selection of a directory instead of a file.<br> * saveMode (boolean, optional) specifies wether the file requester should be opened for loading or for saving a file, <code>false</code> for loading, <code>true</code> for saving. * * <h4><code>formatTime(ticks)</code> (= <code>string</code>)</h4> * * Returns the a string representing the number of ticks in a human-readable * manner, e.g. "4:02.064". * * <h4><code>parseTime(barBeatsTicksString)</code> (= <code>int</code>)</h4> * * Returns the number of ticks specified by the formatted string. The string's * format is e.g. "4:02.064". * * <h4><code>wait(ms)</code></h4> * * Waits the specified amount of milliseconds. * * <h4><code>waitTicks(ticks)</code></h4> * * Waits the specified amount of ticks. * * <h4><code>type(string)</code> (= <code>int</code>)</h4> * * Returns the type number for a corresponding type name, 1 for "Midi", 2 for * "Audio", 4 for "Text". * * <h4><code>typeName(int)</code> (= <code>string</code>)</h4> * * Returns the type name of a type number, "Midi" for 1, "Audio" for 2, "Text" * for 4. * * <h4><code>note(string)</code> (= <code>int</code>)</h4> * * Returns the number of a note as derived from the specified name. * Example names are: c3, f#5, Gb2, D3 * * <h4><code>noteName(int)</code> (= <code>string</code>)</h4> * * Returns the name of a note. * * <h4><code>fileRead(filename)</code> (= <code>string</code>)</h4> * * Reads a whole file as string. If the file doesn't exist something goes wrong reading, the result is "undefined". * * <h4><code>fileWrite(filename, string) (= <code>string</code>)</code></h4> * * Writes a whole string to a file. If the file previously exists, it will be overwritten.<br> * Returns <code>true</code> if writing was successful, otherwise <code>false</code>. * * <h4><code>fileLen(filename) (= <code>int</code>)</code></h4> * * Returns the length of a file in bytes. * * <h4><code>fileExists(filename) (= <code>boolean</code>)</code></h4> * * Tests whether a file exists. * * <h4><code>fileDelete(filename) (= <code>boolean</code>)</code></h4> * * Deletes a file.<br> * Returns <code>true</code> if deleting was successful, otherwise <code>false</code>. * * <h4><code>shellExecute(cmd[, fork]) (= <code>int</code>)</code></h4> * * Executes a shell command. If <code>fork</code> remains unspecified or is false, this waits until * the onvoked application exits and the corresponding return-code is delivered as result. * If <code>fork</code> is true, this returns immediately after spawning the shell-command and returns 0. * * <h4><code>panic()</code></h4> * * Send full range of notes-off events to all channels on all devices. * * * * <h2>Objects</h2> * * An interface to Frinika's currently loaded project is given some top-level * objects. * * * <h3>Song</h3> * * The <code>song</code> object represents the current project from which the * script is run (called "song", although not every piece of acoustic material * created with a Frinika will of course necessarily be song). Type values are: * MIDI = 1, AUDIO = 2, TEXT = 4. * * * <h4>Object Variables</h4> * * <h4><code>song.filename</code> <emp>(read-only)</emp></h4> * * <h4><code>song.beatsPerMinute</code></h4> * * <h4><code>song.ticksPerBeat <emp>(read-only)</emp></code></h4> * * <h4><code>song.lanes[]</code> <emp>(read-only)</emp></h4> * * <h4><code>song.lanes[x].name</code> <emp>(read-only)</emp></h4> * * <h4><code>song.lanes[x].type</code> <emp>(read-only)</emp></h4> * * <h4><code>song.lanes[x].parts[]</code> <emp>(read-only)</emp></h4> * * <h4><code>song.lanes[x].parts[x].name</code> <emp>(read-only)</emp></h4> * * <h4><code>song.lanes[x].parts[x].type</code> <emp>(read-only)</emp></h4> * * <h4><code>song.lanes[x].parts[x].events[]</code> <emp>(read-only)</emp></h4> * * <h4><code>song.lanes[x].parts[x].notes[]</code> <emp>(read-only)</emp></h4> * * <h4><code>song.lanes[x].parts[x].controllers[]</code> <emp>(read-only)</emp></h4> * * <h4><code>song.lanes[x].parts[x].sysex[]</code> <emp>(read-only)</emp></h4> * * <h4><code>song.lanes[x].parts[x].ghost</code> <emp>(read-only)</emp></h4> * * * <h4>Object Functions</h4> * * <h4><code>song.play()</code></h4> * * * <h4><code>song.playUntil(tick)</code></h4> * * * <h4><code>song.stop()</code></h4> * * * <h4><code>song.save()</code></h4> * * * <h4><code>song.newLane(name, type)</code></h4> * * * <h4><code>song.getLane(name) (= lane)</code></h4> * * * <h4><code>song.lane[x].remove()</code></h4> * * * <h4><code>song.lane[x].newPart(tick, duration)</code></h4> * * * <h4><code>song.lane[x].newPartOfType(tick, duration, type)</code></h4> * * * <h4><code>song.lane[x].part[y].remove()</code></h4> * * * <h4><code>song.createNew()</code></h4> * * * * <h3>Selection</h3> * * The <code>selection</code> object represents the current selection inside * Frinika's project at the time when the script is run. * * <h4>Object Variables</h4> * * <h4><code>selection.events[]</code></h4> * * Array of all currently selected events. Usually, a script will not use this * because it will be interested i events of a certain type only. * * <h4><code>selection.notes[]</code></h4> * * Array of currently selected Midi notes. If no notes are selected, the array * has 0 entries. Typically, a set of selected notes can be changed in a loop * like this: <code> * for (i = 0; i < selection.notes.length; i++) { * selection.notes[i].note = ... * selection.notes[i].velocity = ... * selection.notes[i].duration = ... * selection.notes[i].startTick = ... * } * </code> * Note that notes from the selection-object may be changed by a script as in * the above example (while those accessed via song.lane[x].part[y].note[z] are * read-only). * * <h4><code>selection.controllers[]</code></h4> * * Array of currently selected Midi controllers. If no controllers are selected, * the array has 0 entries. * * * <h4><code>selection.sysex[]</code></h4> * * Array of currently selected Midi system exclusive events. If no system * exclusive events are selected, the array has 0 entries. * * * <h4>Object Functions</h4> * * <h4><code>selection.clear()</code></h4> * * Clears the current selections so that no elements remain selected afterwards. * * * <h3>Menu</h3> * * The <code>menu</code> object represents the menu-bar of Frinika's project- * window. * * <h4>Object Variables</h4> * * <h4><code>menu[menuIndex][itemIndex]</code></h4> * * <h4><code>menu[a][b].label</code> <emp>(read-only)</emp></h4> * * <h4>Object Functions</h4> * * <h4><code>menu[x][y].execute()</code></h4> * * * <h3>Persistent</h3> * * The <code>persistent</code> object allows to store values that will be saved together with the project. * * <h4>Object Functions</h4> * * <h4><code>persistent.put(key, value)</code></h4> * * <h4><code>persistent.get(key)</code></h4> * * * <h3>Global</h3> * * The <code>global</code> object allows to store values that will be saved in the user's homedir as file .frinika-script-global.properties. * These values can be shared across project, i.e. one project can set a value, another project can read it.<br> * Note that setting a value is a costy operation (will cause file-write with each put-operation). * * <h4>Object Functions</h4> * * <h4><code>global.put(key, value)</code></h4> * * <h4><code>global.get(key)</code></h4> * * * @author Jens Gulden */ public class JavascriptScope extends ScriptableObject { public static final int TYPE_UNKNOWN = 1; public static final int TYPE_MIDI = 1; public static final int TYPE_AUDIO = 2; public static final int TYPE_TEXT = 4; private static final String TEXT_DELIM = "\n---\n"; private Context context; private ProjectFrame frame; private ScriptingDialog dialog; private TimeUtils timeUtils; private Map<Object, Object> wrapperCache; /** * Createsa a JavascriptContext. This is a bridge between JavaScript and * Java, application-specific to Frinika. * * @param frame * @param events */ public JavascriptScope(Context context, ProjectFrame frame, SortedSet<MultiEvent> events, ScriptingDialog dialog) { super(); this.context = context; this.frame = frame; this.dialog = dialog; ProjectContainer p = frame.getProjectContainer(); timeUtils = new TimeUtils(p); wrapperCache = new HashMap<Object, Object>(); song = new Song(p); selection = new Selection(events); initMenu(); this.persistent = new PropertiesWrapper( p.getScriptingEngine().getPersistentProperties() ); this.global = new PropertiesWrapper() { @Override public void set(String variable, String value) { FrinikaScriptingEngine.globalPut(variable, value); } @Override public String get(String variable) { return FrinikaScriptingEngine.globalGet(variable); } }; // init Javascript standard objects context.initStandardObjects(this); // init Frinka-specific objects exportField("song", this.song); exportField("selection", this.selection); exportField("menu", this.menu); exportField("persistent", this.persistent); exportField("global", this.global); exportField("MIDI", TYPE_MIDI); exportField("AUDIO", TYPE_AUDIO); exportField("TEXT", TYPE_TEXT); exportMethod("print", new Class[] { String.class }); exportMethod("println", new Class[] { String.class }); // exportMethod( "println", new Class[] { } ); exportMethod("message", new Class[] { String.class }); exportMethod("error", new Class[] { String.class }); exportMethod("confirm", new Class[] { String.class }); exportMethod("prompt", new Class[] { String.class }); exportMethod("promptFile", new Class[] { String.class, String.class, boolean.class }); exportMethod("time", new Class[] { String.class }); exportMethod("formatTime", new Class[] { int.class }); exportMethod("_wait", new Class[] { int.class }); exportMethod("waitTicks", new Class[] { int.class }); exportMethod("type", new Class[] { String.class }); exportMethod("typeName", new Class[] { int.class }); exportMethod("note", new Class[] { String.class }); exportMethod("noteName", new Class[] { int.class }); exportMethod("fileRead", new Class[] { String.class }); exportMethod("fileWrite", new Class[] { String.class, String.class }); exportMethod("fileLen", new Class[] { String.class }); exportMethod("fileExists", new Class[] { String.class }); exportMethod("fileDelete", new Class[] { String.class }); exportMethod("panic", new Class[] { }); exportMethod("shellExecute", new Class[] { String.class, boolean.class }); } @Override public String getClassName() { return "JavascriptScope"; } /** * Exports a Java object as a Javascript variable. * * @param fieldName * @param value */ private void exportField(String fieldName, Object value) { Object variableJS = Context.javaToJS(value, this); ScriptableObject.putProperty(this, fieldName, variableJS); } /** * Exports a Java method as a Javascript (top-level-)function. Only works * with methods in this class (because scope is used as value for 'this' * when invoking methods). * * @param methodName * @param parameterSignature */ private void exportMethod(String methodName, Class[] parameterSignature) { try { Method m = JavascriptScope.class.getMethod(methodName, parameterSignature); while (methodName.charAt(0) == '_') { methodName = methodName.substring(1); } // remove leading _ for JS name FunctionObject functionJS = new FunctionObject(methodName, m, this); ScriptableObject.putProperty(this, methodName, functionJS); } catch (NoSuchMethodException nse) { nse.printStackTrace(); } } // --- fields exposed to JavaScript --- public Object song; public Object selection; public Object[][] menu; public Object persistent; public Object global; // --- methods exposed to JavaScript --- public void print(String s) { System.out.print(s); if (dialog != null) { dialog.print(s); } } public void println(String s) { if (s.equals("undefined")) s = ""; // hack to allow 'null' input - makes of course String // "undefined" unprintable, use "undefined " etc. instead System.out.println(s); if (dialog != null) { dialog.println(s); } } public void message(String s) { System.out.println(s); if (frame != null) { frame.message(s); } } public void error(String s) { System.err.println(s); if (frame != null) { frame.error(s); } } public boolean confirm(String s) { System.out.print(s); if (frame != null) { System.out.println("... Ok."); return frame.confirm(s); } else { System.out.println("... Cancel."); return false; } } public String prompt(String s) { System.out.print(s); String r; if (frame != null) { r = frame.prompt(s); } else { r = null; } System.out.println(" " + r); return r; } public String promptFile(String defaultFilename, String suffices, boolean saveMode) { System.out.print("prompting for " + (saveMode ? "saving" : "loading") + ", default " + defaultFilename + ":"); String r; if (frame != null) { String[][] s = null; boolean directoryMode = false; if (suffices != null) { directoryMode = suffices.equals("<dir>"); if ( ! directoryMode ) { StringTokenizer st = new StringTokenizer(suffices, ";", false); int count = st.countTokens(); s = new String[count][2]; for (int i = 0; i < s.length; i++) { String suf = st.nextToken(); int space = suf.indexOf(' '); if (space == -1) { s[i][0] = suf; s[i][1] = ""; } else { s[i][0] = suf.substring(0, space); s[i][1] = suf.substring(space+1); } } } } else { s = new String[0][0]; } r = frame.promptFile(defaultFilename, s, saveMode, directoryMode); } else { r = null; } System.out.println(" " + r); return r; } public int time(String s) { // parses a time string if ( s.indexOf('.') == -1 ) { // allow missing "0 bars" s = "0." + s; } if ( s.indexOf(':') == -1 ) { // allow missing "0 ticks" s = s + ":000"; } return (int)timeUtils.barBeatTickToTick(s); // int, long not supported by Rhino } public String formatTime(int ticks) { // int, long not supported by Rhino return timeUtils.tickToBarBeatTick(ticks); } public void _wait(int ms) { try { Thread.sleep(ms); } catch (InterruptedException ie) { // nop } } public void waitTicks(int ticks) { long ms = tick2ms(ticks); try { Thread.sleep(ms); } catch (InterruptedException ie) { // nop } } private int tick2ms(int tick) { float bpm = ((Song) song).getBeatsPerMinute(); int ppq = ((Song) song).getTicksPerBeat(); int ms = Math.round(((float) tick / (float) ppq / (float) bpm) * (60 * 1000)); return ms; } public int type(String name) { if (name.equalsIgnoreCase("Midi")) { return TYPE_MIDI; } else if (name.equalsIgnoreCase("Audio")) { return TYPE_AUDIO; } else if (name.equalsIgnoreCase("Text")) { return TYPE_TEXT; } else { return TYPE_UNKNOWN; } } public String typeName(int type) { switch (type) { case TYPE_MIDI: return "Midi"; case TYPE_AUDIO: return "Audio"; case TYPE_TEXT: return "Text"; default: return null; } } public int note(String name) { return MidiStepRecordAction.parseNote(name); } public String noteName(int note) { return MidiStepRecordAction.formatNote(note); } public String fileRead(String filename) { try { FileReader in = new FileReader(filename); StringBuffer sb = new StringBuffer(); char[] c = new char[1024]; int hasRead; do { hasRead = in.read(c); if (hasRead > 0) { sb.append(c, 0, hasRead); } } while (hasRead == c.length); in.close(); return sb.toString(); } catch (IOException ioe) { ioe.printStackTrace(); return null; } } public boolean fileWrite(String filename, String data) { try { FileWriter out = new FileWriter(filename); out.write(data); out.close(); return true; } catch (IOException ioe) { return false; } } public int fileLen(String filename) { File file = new File(filename); return (int)file.length(); } public boolean fileExists(String filename) { File file = new File(filename); return file.exists(); } public boolean fileDelete(String filename) { File file = new File(filename); return file.delete(); } /*public int shellExecute(String command) { }*/ public void panic() { frame.getProjectContainer().getSequencer().panic(); } public int shellExecute(String cmd, boolean fork) { try { Process p = Runtime.getRuntime().exec(cmd); if (fork) { return 0; } else { return p.waitFor(); } } catch (Exception e) { frame.error(e); return -1; } } // --- inner classes (for exposed variables "selection" etc.) --- public class Song { private ProjectContainer p; Song(ProjectContainer p) { this.p = p; } public Object[] getSystemLanes() { return (new Converter(p.getProjectLane().getFamilyLanes()) { protected Object createWrapper(Object o) { return new Lane((com.frinika.sequencer.model.Lane) o); } }).toArray(); } public Object[] getLanes() { return (new Converter(p.getProjectLane().getChildren()) { protected Object createWrapper(Object o) { return new Lane((com.frinika.sequencer.model.Lane) o); } }).toArray(); } public Object[] getMidiLanes() { return (new Converter(p.getLanes(), MidiLane.class) { protected Object createWrapper(Object o) { return new Lane((com.frinika.sequencer.model.Lane) o); } }).toArray(); } public Object[] getAudioLanes() { return (new Converter(p.getLanes(), AudioLane.class) { protected Object createWrapper(Object o) { return new Lane((com.frinika.sequencer.model.Lane) o); } }).toArray(); } public Object[] getTextLanes() { return (new Converter(p.getLanes(), TextLane.class) { protected Object createWrapper(Object o) { return new Lane((com.frinika.sequencer.model.Lane) o); } }).toArray(); } public String getFilename() { File file = p.getProjectFile(); if ((file != null) && (file.isFile())) { return file.getAbsolutePath(); } else { return null; } } public int getTicksPerBeat() { return p.getSequence().getResolution(); } public float getBeatsPerMinute() { return p.getSequencer().getTempoInBPM(); } public void setPosition(int tick) { frame.getProjectContainer().getSequencer().setTickPosition(tick); } public int getPosition() { return (int)frame.getProjectContainer().getSequencer().getTickPosition(); } public void play() { (new StartAction(frame)).actionPerformed(null); /*FrinikaSequencer sequencer = frame.getProjectContainer().getSequencer(); if ( ! sequencer.isRunning() ) { sequencer.start(); }*/ } public void playUntil(final int tick) { FrinikaSequencer sequencer = frame.getProjectContainer().getSequencer(); long current = sequencer.getTickPosition(); if (tick <= current) return; //final boolean playing = true; final Object lock = new Object(); SongPositionListener spl = new SongPositionListener() { public void notifyTickPosition(long t) { FrinikaSequencer sequencer = frame.getProjectContainer().getSequencer(); if (t >= tick) { sequencer.stop(); synchronized (lock) { lock.notify(); } //sequencer.removeSongPositionListener(this); // would lead to ConcurrentModificationException, so see below } } public boolean requiresNotificationOnEachTick() { return true; // but we're not costy } }; sequencer.addSongPositionListener(spl); play(); try { synchronized (lock) { lock.wait(); } } catch (InterruptedException ie) { // nop } synchronized (sequencer) { // sync. because sequencer might otherwise still iterate over listeners, leading to CocurrentModExc sequencer.removeSongPositionListener(spl); } } public void stop() { (new StopAction(frame)).actionPerformed(null); //FrinikaSequencer sequencer = frame.getProjectContainer().getSequencer(); //sequencer.stop(); } public void rewind() { (new RewindAction(frame)).actionPerformed(null); //FrinikaSequencer sequencer = frame.getProjectContainer().getSequencer(); //sequencer.setTickPosition(0); } public void record() { (new RecordAction(frame)).actionPerformed(null); //FrinikaSequencer sequencer = frame.getProjectContainer().getSequencer(); //sequencer.startRecording(); } public void save() { File file = p.getProjectFile(); if (file != null) { try { p.saveProject(file); } catch (Throwable t) { frame.error(t); } } else { // nop (script must test whether filename valid) } } public void saveAs(String filename) { File file = new File(filename); if ((file != null) && (file.isFile())) { try { p.saveProject(file); // itentionally does not set // lastSaved... value of project // like manual saving } catch (Throwable t) { frame.error(t); } } else { frame.error("Invalid filename for saving '" + filename + "'."); } } public void open(String filename) { // opens a new project, but we don't have a reference to it, so no // further automatic scripting from here on the new project try { new ProjectFrame(ProjectContainer .loadProject(new File(filename))); } catch (Throwable t) { frame.error(t); } } public void createNew() { // new project, but same restrictions as with open apply try { new ProjectFrame(new ProjectContainer()); } catch (Exception e) { frame.error(e); } } public Object newLane(String name, int type) { com.frinika.sequencer.model.Lane lane; ProjectContainer project = frame.getProjectContainer(); switch (type) { case TYPE_MIDI: //(new CreateMidiLaneAction(frame)).actionPerformed(null); project.getEditHistoryContainer().mark(getMessage("sequencer.project.add_midi_lane")); lane = project.createMidiLane(); break; case TYPE_AUDIO: //(new CreateAudioLaneAction(frame)).actionPerformed(null); project.getEditHistoryContainer().mark(getMessage("sequencer.project.add_audio_lane")); lane = project.createAudioLane(); break; case TYPE_TEXT: //(new CreateTextLaneAction(frame)).actionPerformed(null); project.getEditHistoryContainer().mark(getMessage("sequencer.project.add_text_lane")); lane = project.createTextLane(); break; default: frame.error("cannot create new lane, unknown type " + type); return null; } lane.setName(name); project.getEditHistoryContainer().notifyEditHistoryListeners(); return convert(lane, new Lane(lane)); } public Object getLane(String name) { Object[] lanes = getSystemLanes(); for (int i = 0; i < lanes.length; i++) { if ( name.equals( ((Lane)lanes[i]).getName() ) ) { return lanes[i]; } } return null; } } public class Lane { private com.frinika.sequencer.model.Lane l; private int type; Lane(com.frinika.sequencer.model.Lane l) { this.l = l; if (l instanceof MidiLane) { type = TYPE_MIDI; } else if (l instanceof AudioLane) { type = TYPE_AUDIO; } else if (l instanceof TextLane) { type = TYPE_TEXT; } else { type = TYPE_UNKNOWN; } } public int getType() { return type; } public int getIndex() { return frame.getProjectContainer().getLanes().indexOf(l); } public String getName() { return l.getName(); } public void setName(String name) { l.setName(name); } public Object[] getParts() { return (new Converter(l.getParts()) { protected Object createWrapper(Object o) { return new Part((com.frinika.sequencer.model.Part) o, Lane.this); } }).toArray(); } public Object newPart(int startTick, int duration) { return newPartOfType(startTick, duration, this.type); } public Object newPartOfType(int startTick, int duration, int type) { ProjectContainer project = frame.getProjectContainer(); if (typeName(type) == null) { // type-value may be left out type = this.getType(); } com.frinika.sequencer.model.Part part; if (type == TYPE_MIDI) { part = new MidiPart(); } else if (type == TYPE_AUDIO) { part = new AudioPart(); } else if (type == TYPE_TEXT) { part = new TextPart((TextLane) l); } else { frame.error("cannot create new part, unknown type " + type); return null; } project.getEditHistoryContainer().mark(getMessage("sequencer.lane.add_part")); part.setStartTick(startTick); part.setEndTick(startTick + duration); l.add(part); project.getEditHistoryContainer().notifyEditHistoryListeners(); return convert(part, new Part(part, (Lane)convert(l, new Lane(l)))); } public Object getPart(int startTick) { Object[] parts = getParts(); for (int i = 0; i < parts.length; i++) { if ( startTick == ((Part)parts[i]).getStartTick() ) { return parts[i]; } } return null; } public void remove() { //l.setSelected(true); frame.getProjectContainer().getLaneSelection().setSelected(l); (new DeleteAction(frame.getProjectContainer())).actionPerformed(null); } // MidiLanes public int getMidiChannel() { if (type == TYPE_MIDI) { return ((MidiLane) l).getMidiChannel(); } else { return -1; } } /* * public void setMidiChannel(int channel) { if (type == TYPE_MIDI) * ((MidiLane)l).setMidiChannel(channel); } } */ public void setMute(boolean b) { if (type == TYPE_MIDI) { ((MidiLane) l).setMute(b); } } public boolean isMute() { if (type == TYPE_MIDI) { return ((MidiLane) l).isMute(); } else { return false; } } public void setSolo(boolean b) { if (type == TYPE_MIDI) { ((MidiLane) l).setSolo(b); } } public boolean isSolo() { if (type == TYPE_MIDI) { return ((MidiLane) l).isSolo(); } else { return false; } } public void setLooped(boolean b) { if (type == TYPE_MIDI) { ((MidiLane) l).getPlayOptions().looped = b; } } public boolean isLooped() { if (type == TYPE_MIDI) { return ((MidiLane) l).getPlayOptions().looped; } else { return false; } } public void setRecording(boolean b) { if (type == TYPE_MIDI) { ((MidiLane) l).setRecording(b); } } public boolean isRecording() { if (type == TYPE_MIDI) { return ((MidiLane) l).isRecording(); } else { return false; } } // TextLane public String getText(String delim) { if (type == TYPE_TEXT) { return ((TextLane) l).getAllText(delim); } else { return null; } } public String getText() { return getText(TEXT_DELIM); } public void setText(String text, String delim) { if (type == TYPE_TEXT) { ((TextLane) l).setAllText(text, delim); } } public void setText(String text) { if (type == TYPE_TEXT) { ((TextLane) l).setAllText(text, TEXT_DELIM); } } } public class Part { private com.frinika.sequencer.model.Part p; private int type; Part(com.frinika.sequencer.model.Part p, Lane parent) { this.p = p; this.lane = parent; if (p instanceof MidiPart) { type = TYPE_MIDI; } else if (p instanceof AudioPart) { type = TYPE_AUDIO; } else if (p instanceof TextPart) { type = TYPE_TEXT; } else { type = TYPE_UNKNOWN; } } public Object lane; public int getType() { return type; } public boolean isGhost() { return (p instanceof Ghost); } public void remove() { //p.setSelected(true); frame.getProjectContainer().getPartSelection().setSelected(p); (new DeleteAction(frame.getProjectContainer())).actionPerformed(null); } public int getStartTick() { return (int)p.getStartTick(); } public void setStartTick(int tick) { p.setStartTick(tick); } public int getEndTick() { return (int)p.getEndTick(); } public void setEndTick(int tick) { p.setEndTick(tick); } public int getDuration() { return (int)p.getDurationInTicks(); } public void setDuration(int tick) { //p.setDuration(tick); setEndTick( getStartTick() + tick ); } // MidiPart public Object[] getNotes() { if (type == TYPE_MIDI) { return (new Converter(((MidiPart) p).getMultiEvents(), NoteEvent.class)).toArray(); } else { return null; } } public Object[] getControllers() { if (type == TYPE_MIDI) { return (new Converter(((MidiPart) p).getMultiEvents(), ControllerEvent.class)).toArray(); } else { return null; } } public void insertNote(int note, int tick, int duration, int velocity) { if (type == TYPE_MIDI) { ProjectContainer project = frame.getProjectContainer(); project.getEditHistoryContainer().mark(getMessage("sequencer.pianoroll.add_note")); NoteEvent newNote = new NoteEvent((MidiPart) p, tick, note, velocity, ((MidiPart) p).getMidiChannel(), duration); ((MidiPart) p).add(newNote); project.getEditHistoryContainer().notifyEditHistoryListeners(); // project.notifyDragEventListeners(newNote); // PianoRoll..repaintItems(); - general problem: repainting } } public void removeNote(int note, int tick) { // TODO test if (type == TYPE_MIDI) { ProjectContainer project = frame.getProjectContainer(); project.getEditHistoryContainer().mark( getMessage("sequencer.pianoroll.add_note")); NoteEvent delNote = findNote(tick, note, (MidiPart) p); if (delNote != null) { ((MidiPart) p).remove(delNote); } project.getEditHistoryContainer().notifyEditHistoryListeners(); } } public int getMidiChannel() { if (type == TYPE_MIDI) { return ((MidiPart) p).getMidiChannel(); } else { return -1; } } // TextPart public String getText() { if (type == TYPE_TEXT) { return ((TextPart) p).getText(); } else { return null; } } public void setText(String text) { if (type == TYPE_TEXT) { ((TextPart) p).setText(text); } } } public class Selection { public int start; // read-only public int end; // read-only public int duration; // read-only public Object part; // read-only public Object lane; // read-only public Object[] notes; public Object[] controllers; public Object[] sysex; Selection(SortedSet<MultiEvent> p) { if ((p != null) && (!p.isEmpty())) { MultiEvent first = p.first(); MultiEvent last = p.last(); start = (int)first.getStartTick(); end = (int)last.getEndTick(); duration = end - start; // lane = new // Lane(((com.frinika.sequencer.model.Part)part).getLane()); lane = new Lane(first.getPart().getLane()); part = new Part(first.getPart(), (Lane) lane); } else { // no events selected, but maybe at least a part? // (multiple part selection not supprted) Collection<com.frinika.sequencer.model.Part> partSelection = frame .getProjectContainer().getPartSelection().getSelected(); if (!partSelection.isEmpty()) { com.frinika.sequencer.model.Part prt = partSelection .iterator().next(); start = (int)prt.getStartTick(); end = (int)prt.getEndTick(); duration = (int)prt.getDurationInTicks(); lane = new Lane(prt.getLane()); part = new Part(prt, (Lane) lane); } else { // no part selected either, at least a lane? Collection<com.frinika.sequencer.model.Lane> laneSelection = frame .getProjectContainer().getLaneSelection() .getSelected(); if (!laneSelection.isEmpty()) { com.frinika.sequencer.model.Lane ln = laneSelection .iterator().next(); start = 0; end = (int)ln.rightTickForMove(); duration = end - start; part = null; lane = new Lane(ln); } } } ArrayList n = new ArrayList(); ArrayList c = new ArrayList(); ArrayList s = new ArrayList(); if (p != null) { for (MultiEvent e : p) { if (e instanceof NoteEvent) { n.add(e); } else if (e instanceof ControllerEvent) { c.add(e); } else if (e instanceof SysexEvent) { s.add(e); } } } notes = new Object[n.size()]; n.toArray(notes); controllers = new Object[c.size()]; c.toArray(controllers); sysex = new Object[s.size()]; s.toArray(sysex); } } public class Menu { private JMenuItem item; private JMenuItem menu; Menu(JMenuItem item, JMenu menu) { this.item = item; this.menu = menu; } public String getLabel() { return item.getText(); } public String getMenuLabel() { return menu.getText(); } public void execute() { item.doClick(); } } public class PropertiesWrapper { private Properties p; PropertiesWrapper() { this(null); } PropertiesWrapper(Properties properties) { this.p = properties; } public void set(String variable, String value) { p.put(variable, value); } public String get(String variable) { return p.getProperty(variable); } } protected void initMenu() { JMenuBar menuBar = frame.getJMenuBar(); Object[][] o = new Object[menuBar.getMenuCount()][]; for (int i = 0; i < o.length; i++) { final JMenu menu = menuBar.getMenu(i); // o[i] = new Object[menu.getMenuComponentCount()]; // for (int j = 0; j < o[i].length; j++) { List<JMenuItem> m = new ArrayList<JMenuItem>(); for (int j = 0; j < menu.getMenuComponentCount(); j++) { Component component = menu.getMenuComponent(j); if (component instanceof JMenuItem) { // JMenuItem menuItem = (JMenuItem)component; // o[i][j] = new Menu(menuItem); m.add((JMenuItem) component); } } o[i] = (new Converter(m) { protected Object createWrapper(Object javaObject) { return new Menu((JMenuItem) javaObject, menu); } }).toArray(); } this.menu = o; } // --- tools --- private class Converter { private Collection c; Converter(Collection c, Class filterType) { super(); if (filterType != null) { this.c = new ArrayList(); for (Object o : c) { if (filterType.isAssignableFrom(o.getClass())) { this.c.add(o); } } } else { this.c = c; } } Converter(Collection c) { this(c, null); } Object[] toArray() { Object[] l = new Object[c.size()]; int i = 0; for (Object q : c) { l[i++] = getWrapper(q); // new Lane(midilane); } return l; } private Object getWrapper(Object javaObject) { Object wrapper = wrapperCache.get(javaObject); // preserve 1:1 idendity between origial objects and wrapper, also faster if (wrapper == null) { wrapper = createWrapper(javaObject); if (wrapper != javaObject) { wrapperCache.put(javaObject, wrapper); } } return wrapper; } protected Object createWrapper(Object javaObject) { return javaObject; // by default don't wrap, ususally overwritten // by subclasses } } private Object convert(Object javaObject, Object newWrapper) { Object wrapper = wrapperCache.get(javaObject); if (wrapper == null) { wrapper = newWrapper; wrapperCache.put(javaObject, wrapper); } return wrapper; } /* * private class LaneConverter extends Converter { * * LaneConverter(Collection c) { super(c); } * * LaneConverter(Collection c, Class filterType) { super(c, filterType); } * * protected Object createWrapper(Object o) { return new * Lane((com.frinika.sequencer.model.Lane)o); } * } */ private static NoteEvent findNote(long tick, int note, MidiPart p) { Collection<MultiEvent> eventsAtTick = p.getMultiEventSubset(tick, tick + 1); if ((eventsAtTick != null) && (!eventsAtTick.isEmpty())) { for (MultiEvent e : eventsAtTick) { if (e instanceof NoteEvent) { NoteEvent n = (NoteEvent) e; if (n.getNote() == note) { return n; } } } } return null; } }