/* * Created on Feb 2, 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.sequencer.model; import javax.sound.midi.InvalidMidiDataException; import javax.sound.midi.MidiMessage; import javax.sound.midi.SysexMessage; import com.frinika.global.Toolbox; import java.util.*; /** * Abstract superclass for SysexMacros. * * @author Jens Gulden */ public abstract class AbstractSysexMacro implements SysexMacro { public final static String SYSEX_MACRO_PACKAGE = "com.frinika.sequencer.midi.sysex"; /** * Generic usage message. Should be overwritten by subclasses. * * @return Usage message string. */ public String usage() { return "Usage: <macro-name> <param1> <param2> ..."; } /** * Tries to find a responsible macro-parser class for a given * sysex macro string. The first word of the string is considered * the macro name, which should be available as a class in * com.frinika.sequencer.midi.sysex, e.g.: * "roland 1000 11 22 33" will try to load the macro class * com.frinika.sequencer.midi.sysex.Roland. * * @param macro * @return */ public static SysexMacro findMacro(String s) { SysexMacro macro; String ww = Toolbox.firstWord(s); String w = Toolbox.capitalize(ww); try { Class cl; try { cl = Class.forName(SYSEX_MACRO_PACKAGE + "." + w); } catch (ClassNotFoundException cnfe) { cl = Class.forName(ww); // maybe fully qualified classname } Object o = cl.newInstance(); macro = (SysexMacro)o; } catch (InstantiationException ie) { ie.printStackTrace(); return null; } catch (IllegalAccessException iae) { iae.printStackTrace(); return null; } catch (ClassNotFoundException cnfe) { // not found: maybe raw data try { byte b = parseByte(w, 16); } catch (InvalidMidiDataException imde) { // not a number: macro not found return null; } // parse as raw data macro = new com.frinika.sequencer.midi.sysex.Sysex(); } return macro; } /** * Entry method called from SysexEvent. * Default implementation assumes a single sysex message to be parsed * and thus delegates to parse(String). * @param macro * @return */ public MidiMessage[] parseMessages(String macro) throws InvalidMidiDataException { byte[] data = parse(macro); SysexMessage syxm = new SysexMessage(); syxm.setMessage(data, data.length); MidiMessage[] mm = { syxm }; return mm; } /** * The default implementation skipps the macro-name, * then calls parse(StringTokenizer st). * @param macro * @return */ public byte[] parse(String macro) throws InvalidMidiDataException { StringTokenizer st = new StringTokenizer(macro); st.nextToken(); // skip macro name return parse(st); } /** * The default implementation extracts individual blank-seperated parameters (not comma-seperated), * then calls parse(String[] args). * @param st * @return */ public byte[] parse(StringTokenizer st) throws InvalidMidiDataException { int size = st.countTokens(); String[] args = new String[size]; for (int i = 0; i < size; i++) { args[i] = st.nextToken(); } return parse(args); } /** * The default implementation treats all args as decimal number values, * then calls parse(int[] args) * @param args * @return */ public byte[] parse(String[] args) throws InvalidMidiDataException { int[] a = new int[args.length]; for (int i = 0; i < args.length; i++) { a[i] = parseIntArg(args[i], i); } return parse(a); } /** * Might be overwritten if other formats than decimal are to be parsed as args. * @param arg * @param index * @return */ public int parseIntArg(String arg, int index) throws InvalidMidiDataException { return parseInt(arg, 10); } /** * The default implementation throws a runtime error, so at last this method * must be overwritten by subclasses. * @param args * @return */ public byte[] parse(int[] args) throws InvalidMidiDataException { throw new AbstractMethodError("parse(int[] args) or another parse-method must be implemented by sublcasses of SysexMacro"); } // --- Tools --- /** * Same as splitWords, but without first word (i.e. without macro name itself, * just parameters). * @param s * @return */ public static String[] splitArgs(String s) { String[] w = Toolbox.splitWords(s); String[] args = new String[w.length-1]; System.arraycopy(w, 1, args, 0, args.length); return args; } /** * Parses a single byte-string. By default a hex-value is assumed, but decimal * values can be given if preceeded by a tilde (~). * @param s * @return */ protected static int parseInt(String s, int defaultRadix) throws InvalidMidiDataException { int radix = defaultRadix; if (s.startsWith("~")) { radix = 10; s = s.substring(1); } else if (s.startsWith("0x")) { radix = 16; s = s.substring(2); } try { return Integer.parseInt(s, radix); } catch (NumberFormatException nfe) { throw new InvalidMidiDataException(nfe.getMessage()); } } protected static int parseInt(String s, int defaultRadix, int min, int max) throws InvalidMidiDataException { int i = parseInt(s, defaultRadix); if ((i < min) || (i > max)) { throw new InvalidMidiDataException("value "+i+" is not valid, expected range "+min+"..."+max); } return i; } protected static byte parseByte(String s, int defaultRadix) throws InvalidMidiDataException { int i = parseInt(s, defaultRadix); if (i < 0 || i > 255) { throw new InvalidMidiDataException("value "+i+" is not a byte"); } return (byte)i; } protected int parseType(String s, String[] list) throws InvalidMidiDataException { //try { // int num = parseInt(s, 10); // return num; //} catch (NumberFormatException nfe) { for (int i = 0; i < list.length; i++) { if (s.equalsIgnoreCase(list[i])) { return i; } } throw new InvalidMidiDataException("Value '"+s+"' is not available in the list '"+Toolbox.joinStrings(list, ",")+"'."); //} } protected void error(String msg) throws InvalidMidiDataException { throw new InvalidMidiDataException(msg+"\n"+usage()); } }