diff --git a/midica.jar b/midica.jar
index 540b044..7b9e6b6 100644
Binary files a/midica.jar and b/midica.jar differ
diff --git a/src/org/midica/Midica.java b/src/org/midica/Midica.java
index a751e41..82986be 100644
--- a/src/org/midica/Midica.java
+++ b/src/org/midica/Midica.java
@@ -36,7 +36,7 @@ public class Midica {
private static final int VERSION_MINOR = 11;
/** UNIX timestamp of the last commit */
- public static final int COMMIT_TIME = 1703690090;
+ public static final int COMMIT_TIME = 1704361659;
/** Branch name. Automatically changed by precommit.pl */
public static final String BRANCH = "sound-effects";
diff --git a/src/org/midica/config/Dict.java b/src/org/midica/config/Dict.java
index da8ef1f..f88afac 100644
--- a/src/org/midica/config/Dict.java
+++ b/src/org/midica/config/Dict.java
@@ -204,6 +204,7 @@ public class Dict {
public static final String SYNTAX_FL_RPN = "FL_RPN";
public static final String SYNTAX_FL_NRPN = "FL_NRPN";
public static final String SYNTAX_FL_ASSIGNER = "FL_ASSIGNER";
+ public static final String SYNTAX_FL_GEN_NUM_SEP = "FL_GEN_NUM_SEP";
public static final String SYNTAX_FUNC_SET = "FUNC_SET";
public static final String SYNTAX_FUNC_ON = "FUNC_ON";
public static final String SYNTAX_FUNC_OFF = "FUNC_OFF";
@@ -1693,7 +1694,6 @@ public class Dict {
public static final String ERROR_GLOBAL_NUM_OF_ARGS = "error_global_num_of_args";
public static final String ERROR_UNKNOWN_GLOBAL_CMD = "error_unknown_global_cmd: ";
public static final String ERROR_UNKNOWN_COMMAND_ID = "error_unknown_command_id";
- public static final String ERROR_MIDI_PROBLEM = "error_midi_problem";
public static final String ERROR_CH_CMD_NUM_OF_ARGS = "error_ch_num_of_args";
public static final String ERROR_CANT_PARSE_OPTIONS = "error_cant_parse_options";
public static final String ERROR_OPTION_NEEDS_VAL = "error_option_needs_val";
@@ -1746,6 +1746,7 @@ public class Dict {
public static final String ERROR_FL_NOT_OPEN = "error_fl_not_open";
public static final String ERROR_FL_MISSING_DOT = "error_fl_missing_dot";
public static final String ERROR_FL_NUMBER_NOT_ALLOWED = "error_fl_number_not_allowed";
+ public static final String ERROR_FL_NUM_SEP_NOT_ALLOWED = "error_fl_num_sep_not_allowed";
public static final String ERROR_FL_UNMATCHED_REMAINDER = "error_fl_unmatched_remainder";
public static final String ERROR_FL_NUMBER_MISSING = "error_fl_number_missing";
public static final String ERROR_FL_NUMBER_TOO_HIGH = "error_fl_number_too_high";
@@ -1755,10 +1756,18 @@ public class Dict {
public static final String ERROR_FL_EMPTY_PARAM = "error_fl_empty_param";
public static final String ERROR_FL_CONT_RPN = "error_fl_cont_rpn";
public static final String ERROR_FL_CONT_NRPN = "error_fl_cont_nrpn";
+ public static final String ERROR_FL_NOTE_NOT_SUPP = "error_fl_note_not_supp";
public static final String ERROR_FUNC_TYPE_NOT_BOOL = "error_func_type_not_bool";
public static final String ERROR_FUNC_TYPE_BOOL = "error_func_type_bool";
+ public static final String ERROR_FUNC_TYPE_NONE = "error_func_type_none";
public static final String ERROR_FUNC_VAL_LOWER_MIN = "error_func_val_lower_min";
public static final String ERROR_FUNC_VAL_GREATER_MAX = "error_func_val_greater_max";
+ public static final String ERROR_FUNC_NEED_HALFTONE = "error_func_need_halftone";
+ public static final String ERROR_FUNC_HALFTONE_NOT_ALLOWED = "error_func_halftone_not_allowed";
+ public static final String ERROR_FUNC_HALFTONE_GT_RANGE = "error_func_halftone_gt_range";
+ public static final String ERROR_FUNC_MSB_LSB_NEEDS_DOUBLE = "error_func_msb_lsb_needs_double";
+ public static final String ERROR_FUNC_MSB_TOO_HIGH = "error_func_msb_too_high";
+ public static final String ERROR_FUNC_LSB_TOO_HIGH = "error_func_lsb_too_high";
public static final String ERROR_FUNC_NO_NUMBER = "error_func_no_number";
public static final String ERROR_FUNC_PERIODS_NEG = "error_func_periods_neg";
public static final String ERROR_FUNC_PERIODS_NO_NUMBER = "error_func_periods_no_number";
@@ -2963,6 +2972,7 @@ private static void initLanguageEnglish() {
set( SYNTAX_FL_RPN, "Generic RPN (flow element)" );
set( SYNTAX_FL_NRPN, "Generic NRPN (flow element)" );
set( SYNTAX_FL_ASSIGNER, "asssigner for flow elements, e.g. Ctrl/(N)RPN number or note name" );
+ set( SYNTAX_FL_GEN_NUM_SEP, "MSB/LSB separator for generic (N)RPN numbers in an effect flow" );
set( SYNTAX_FUNC_SET, "set function" );
set( SYNTAX_FUNC_ON, "function to enable a binary effect" );
set( SYNTAX_FUNC_OFF, "function to disable a binary effect" );
@@ -3364,7 +3374,7 @@ private static void initLanguageEnglish() {
set( MSG_DESC_F_UNKNOWN_TONALITY, "Unknown tonality" );
// UiControler + PlayerControler
- set( ERROR_IN_LINE, "parsing error in file:
%s
line: %s
" );
+ set( ERROR_IN_LINE, "parsing error in file:
%s
line: %s
" );
// SoundbankParser
set( UNKNOWN_SOUND_EXT, "Allowed file extensions: *.sf2 or *.dls"
@@ -3475,7 +3485,7 @@ private static void initLanguageEnglish() {
set( ERROR_CALL_DUPLICATE_PARAM_NAME, "duplicate parameter name: " );
set( ERROR_CALL_PARAM_MORE_ASSIGNERS, "named parameter must not contain more than one assign symbol: " );
set( ERROR_COMPACT_INVALID_OPTION, "option invalid for compact commands: %s - Erroneous part: %s" );
- set( ERROR_COMPACT_PAT_CALL_WITH_OPT, "Pattern call contains additional data: %s
"
+ set( ERROR_COMPACT_PAT_CALL_WITH_OPT, "Pattern call contains additional data: %s
"
+ "Pattern call: %s
"
+ "(Forgotten whitespace after call? Failed to close parameter list? Faulty whitespace in parameter list?)" );
set( ERROR_INVALID_TIME_DENOM, "invalid denominator in time signature: " );
@@ -3498,7 +3508,6 @@ private static void initLanguageEnglish() {
set( ERROR_GLOBAL_NUM_OF_ARGS, "wrong number of arguments in global command" );
set( ERROR_UNKNOWN_GLOBAL_CMD, "unknown global command: " );
set( ERROR_UNKNOWN_COMMAND_ID, "Unknown command ID: " );
- set( ERROR_MIDI_PROBLEM, "Midi Problem!
" );
set( ERROR_CH_CMD_NUM_OF_ARGS, "wrong number of arguments in channel command" );
set( ERROR_CANT_PARSE_OPTIONS, "cannot parse options: " );
set( ERROR_OPTION_NEEDS_VAL, "option needs value: " );
@@ -3551,6 +3560,7 @@ private static void initLanguageEnglish() {
set( ERROR_FL_NOT_OPEN, "An effect flow is not yet open. Don't use '%s' to start a flow." );
set( ERROR_FL_MISSING_DOT, "Effect flow elements must be separated with '%s'" );
set( ERROR_FL_NUMBER_NOT_ALLOWED, "A generic number is not allowed for this effect flow element: " );
+ set( ERROR_FL_NUM_SEP_NOT_ALLOWED, "The gereric number has only one byte. MSB/LSB not allowed for element: " );
set( ERROR_FL_UNMATCHED_REMAINDER, "Effect flow ends with an invalid remainder: " );
set( ERROR_FL_NUMBER_MISSING, "Effect flow element '%s' needs to be assigned with a generic number" );
set( ERROR_FL_NUMBER_TOO_HIGH, "Generic number %s too high for element %s. Maximum is %d." );
@@ -3560,10 +3570,23 @@ private static void initLanguageEnglish() {
set( ERROR_FL_EMPTY_PARAM, "Empty parameter not allowed. Parameters: " );
set( ERROR_FL_CONT_RPN, "Effect function '%s' is not allowed for RPN-based effects." );
set( ERROR_FL_CONT_NRPN, "Effect function '%s' is not allowed for NRPN-based effects." );
+ set( ERROR_FL_NOTE_NOT_SUPP, "A note is not supported for the chosen effect type. Invalid element: " );
set( ERROR_FUNC_TYPE_NOT_BOOL, "Effect type not boolean. Function '%s' is not allowed." );
- set( ERROR_FUNC_TYPE_BOOL, "Effect type is boolean. Function '%s' is not allowed." );
+ set( ERROR_FUNC_TYPE_BOOL, "Value type is 'boolean'. Function '%s' is not allowed." );
+ set( ERROR_FUNC_TYPE_NONE, "Value type is 'none'. Function '%s' is not allowed." );
set( ERROR_FUNC_VAL_LOWER_MIN, "Parameter '%s' is smaller than the minimum value (%s)" );
set( ERROR_FUNC_VAL_GREATER_MAX, "Parameter '%s' is greater than the maximum value (%s)" );
+ set( ERROR_FUNC_NEED_HALFTONE, "Effect needs half-tones as parameter. Parameter not accepted: " );
+ set( ERROR_FUNC_HALFTONE_NOT_ALLOWED, "Parameter '%s' not allowed. The effect type does not support half tone steps." );
+ set( ERROR_FUNC_HALFTONE_GT_RANGE, "Half-tone parameter '%s' exceeds the current pitch bend range (%s)
"
+ + "Consider to increase the pitch bend range before setting this value." );
+ set( ERROR_FUNC_MSB_LSB_NEEDS_DOUBLE, "MSB/LSB parameters can only be used with double precision.
"
+ + "Parameter not accepted: %s.
"
+ + "Consider using '%s'." );
+ set( ERROR_FUNC_MSB_TOO_HIGH, "The MSB part of the following parameter is too high: '%s'
"
+ + "MSB not accepted: '%s'" );
+ set( ERROR_FUNC_LSB_TOO_HIGH, "The LSB part of the following parameter is too high: '%s'
"
+ + "LSB not accepted: '%s'" );
set( ERROR_FUNC_NO_NUMBER, "Parameter is not a valid number or percentage value: " );
set( ERROR_FUNC_PERIODS_NEG, "The 'periods' parameter cannot be negative: " );
set( ERROR_FUNC_PERIODS_NO_NUMBER, "The 'periods' parameter is not a valid number: " );
@@ -3622,7 +3645,7 @@ private static void initLanguageEnglish() {
set( TITLE_WAIT, "Please Wait" );
set( WAIT_PARSE_MPL, "Parsing the MidicaPL file..." );
set( WAIT_PARSE_MID, "Parsing the MIDI file..." );
- set( WAIT_PARSE_SB, "Parsing the Soundbank" );
+ set( WAIT_PARSE_SB, "Parsing the Soundbank" );
set( WAIT_PARSE_URL, "Downloading the Soundbank" );
set( WAIT_PARSE_FOREIGN, "Importing the file using %s" );
set( WAIT_REPARSE, "Reloading the File" );
@@ -4343,6 +4366,7 @@ public static void initSyntax() {
setSyntax( SYNTAX_FL_RPN, "rpn" );
setSyntax( SYNTAX_FL_NRPN, "nrpn" );
setSyntax( SYNTAX_FL_ASSIGNER, "=" );
+ setSyntax( SYNTAX_FL_GEN_NUM_SEP, "/" );
setSyntax( SYNTAX_FUNC_SET, "set" );
setSyntax( SYNTAX_FUNC_ON, "on" );
setSyntax( SYNTAX_FUNC_OFF, "off" );
@@ -4556,6 +4580,7 @@ else if (Config.CBX_SYNTAX_UPPER.equals(configuredSyntax)) {
addSyntaxForInfoView( SYNTAX_FL_RPN );
addSyntaxForInfoView( SYNTAX_FL_NRPN );
addSyntaxForInfoView( SYNTAX_FL_ASSIGNER );
+ addSyntaxForInfoView( SYNTAX_FL_GEN_NUM_SEP );
addSyntaxForInfoView( SYNTAX_FUNC_SET );
addSyntaxForInfoView( SYNTAX_FUNC_ON );
addSyntaxForInfoView( SYNTAX_FUNC_OFF );
diff --git a/src/org/midica/file/read/CommandOption.java b/src/org/midica/file/read/CommandOption.java
index e73ee19..67649bc 100644
--- a/src/org/midica/file/read/CommandOption.java
+++ b/src/org/midica/file/read/CommandOption.java
@@ -78,7 +78,7 @@ else if (MidicaPLParser.OPT_ELSE.equals(name)) {
}
else {
// should never happen
- throw new ParseException("Invalid option name: " + name + ". Please report.");
+ throw new FatalParseException("Invalid option name: " + name + ".");
}
}
diff --git a/src/org/midica/file/read/Effect.java b/src/org/midica/file/read/Effect.java
index e1a68cd..aa3610c 100644
--- a/src/org/midica/file/read/Effect.java
+++ b/src/org/midica/file/read/Effect.java
@@ -7,10 +7,14 @@
package org.midica.file.read;
+import java.util.ArrayList;
import java.util.HashMap;
import java.util.HashSet;
+import java.util.List;
import java.util.Map;
+import java.util.Map.Entry;
import java.util.Set;
+import java.util.TreeMap;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
@@ -27,10 +31,14 @@
*/
public class Effect {
+ private static final long RPN_DISTANCE = 10;
+
private static MidicaPLParser parser = null;
private static EffectFlow flow = null;
+ private static List> pitchBendRangeByChannel;
+
private static Set functionNames;
private static Set effectNames;
@@ -54,6 +62,18 @@ public class Effect {
public static void init(MidicaPLParser rootParser) {
parser = rootParser;
+ // create and initialize special structures
+ {
+ // pitch bend range by channel/tick
+ pitchBendRangeByChannel = new ArrayList<>();
+ for (int channel = 0; channel < 16; channel++) {
+ TreeMap rangeMap = new TreeMap<>();
+ rangeMap.put(0L, 2f); // default: 2.0
+ pitchBendRangeByChannel.add(rangeMap);
+ }
+ }
+
+ // create structures
functionToParamCount = new HashMap<>();
functionNames = new HashSet<>();
effectNames = new HashSet<>();
@@ -62,6 +82,7 @@ public static void init(MidicaPLParser rootParser) {
rpnNameToNumber = new HashMap<>();
flowElementNames = new HashSet<>();
+ // init structures for functions
functionToParamCount.put( MidicaPLParser.FUNC_SET, 1 );
functionToParamCount.put( MidicaPLParser.FUNC_ON, 0 );
functionToParamCount.put( MidicaPLParser.FUNC_OFF, 0 );
@@ -77,6 +98,7 @@ public static void init(MidicaPLParser rootParser) {
functionNames.add(key);
}
+ // init structures for effects
effectNames.add( MidicaPLParser.CH_A_POLY_AT );
effectNames.add( MidicaPLParser.CH_D_MONO_AT );
effectNames.add( MidicaPLParser.CH_E_PITCH_BEND );
@@ -116,15 +138,16 @@ public static void init(MidicaPLParser rootParser) {
effectNames.add( MidicaPLParser.RPN_3_TUNING_PROG );
effectNames.add( MidicaPLParser.RPN_4_TUNING_BANK );
effectNames.add( MidicaPLParser.RPN_5_MOD_DEPTH_R );
+ effectNames.add( MidicaPLParser.FL_CTRL ); // generic controller with number
+ effectNames.add( MidicaPLParser.FL_RPN ); // generic RPN with number or MSB/LSB
+ effectNames.add( MidicaPLParser.FL_NRPN ); // generic NRPN with number or MSB/LSB
- effectNames.add( MidicaPLParser.FL_CTRL );
- effectNames.add( MidicaPLParser.FL_RPN );
- effectNames.add( MidicaPLParser.FL_NRPN );
-
+ // init structures for channel-based effects
channelMsgNameToNumber.put( MidicaPLParser.CH_A_POLY_AT, 0xA0 );
channelMsgNameToNumber.put( MidicaPLParser.CH_D_MONO_AT, 0xD0 );
channelMsgNameToNumber.put( MidicaPLParser.CH_E_PITCH_BEND, 0xE0 );
+ // init structures for controller-based effects
ctrlNameToNumber.put( MidicaPLParser.CC_01_MOD, 0x01 );
ctrlNameToNumber.put( MidicaPLParser.CC_02_BREATH, 0x02 );
ctrlNameToNumber.put( MidicaPLParser.CC_04_FOOT, 0x04 );
@@ -156,6 +179,7 @@ public static void init(MidicaPLParser rootParser) {
ctrlNameToNumber.put( MidicaPLParser.CC_5E_EFF4_DEP, 0x5E );
ctrlNameToNumber.put( MidicaPLParser.CC_5F_EFF4_DEP, 0x5F );
+ // init structures for RPN-based effects
rpnNameToNumber.put( MidicaPLParser.RPN_0_PITCH_BEND_R, 0x0000 );
rpnNameToNumber.put( MidicaPLParser.RPN_1_FINE_TUNE, 0x0001 );
rpnNameToNumber.put( MidicaPLParser.RPN_2_COARSE_TUNE, 0x0002 );
@@ -163,7 +187,7 @@ public static void init(MidicaPLParser rootParser) {
rpnNameToNumber.put( MidicaPLParser.RPN_4_TUNING_BANK, 0x0004 );
rpnNameToNumber.put( MidicaPLParser.RPN_5_MOD_DEPTH_R, 0x0005 );
- // flow element names (effect names, function names, or other possible flow elements)
+ // init flow element names (effect names, function names, or other possible flow elements)
for (String effectName : effectNames) {
flowElementNames.add(effectName);
}
@@ -174,29 +198,45 @@ public static void init(MidicaPLParser rootParser) {
// compile regex patterns
String flowRegex
- = "\\G" // end of previous match
- +"(^|" + Pattern.quote(MidicaPLParser.FL_DOT) + ")" // begin or '.'
- + "(\\w+)(?:" + Pattern.quote(MidicaPLParser.FL_ASSIGNER) + "(\\d+))?" // flow element without parameters
- + "(?:"
- + Pattern.quote(MidicaPLParser.PARAM_OPEN) // (
- + "(\\S*?)" // function parameters
- + Pattern.quote(MidicaPLParser.PARAM_CLOSE) // )
- + ")?"; // optional
+ = "\\G" // end of previous match
+ + "(^|" + Pattern.quote(MidicaPLParser.FL_DOT) + ")?" // begin or '.'
+ + "(\\w+)" // name
+ + "(?:" // generic number (optional)
+ + Pattern.quote(MidicaPLParser.FL_ASSIGNER)
+ + "(\\d+)" // MSB or whole number
+ + "(?:"
+ + Pattern.quote(MidicaPLParser.FL_GEN_NUM_SEP)
+ + "(\\d+)" // LSB of generic number
+ + ")?"
+ + ")?"
+ + "(?:" // parameters (optional)
+ + Pattern.quote(MidicaPLParser.PARAM_OPEN) // (
+ + "(\\S*?)" // function parameters
+ + Pattern.quote(MidicaPLParser.PARAM_CLOSE) // )
+ + ")?"; // optional
flowPattern = Pattern.compile(flowRegex);
noteOrRestPattern = Pattern.compile("^[0-9]|" + Pattern.quote(MidicaPLParser.REST) + "$");
String intRegex = "^"
- + "(\\-?\\d+)" // direct int value (group 1)
+ + "(\\-?\\d+)" // direct int value (group 1)
+ "|"
+ "(?:"
- + "(\\-?\\d+(\\.\\d+)?)" // percentage value (group 2)
- + Pattern.quote(MidicaPLParser.EFF_PERCENT)
+ + "(\\-?\\d+(?:\\.\\d+)?)" // percentage value (group 2)
+ + Pattern.quote(MidicaPLParser.EFF_PERCENT)
+ + ")"
+ + "|"
+ + "(\\-?\\d+\\.\\d+)" // half-tone-steps (group 3)
+ + "|"
+ + "(?:"
+ + "(\\d+)" // MSB (group 4)
+ + Pattern.quote(MidicaPLParser.FL_GEN_NUM_SEP)
+ + "(\\d+)" // LSB (group 5)
+ ")"
+ "$";
intPattern = Pattern.compile(intRegex);
String periodsRegex = "^"
+ "(\\-?\\d+(?:\\.\\d*))" // int or float (group 1)
+ "(" // percent (group 2)
- + Pattern.quote(MidicaPLParser.EFF_PERCENT)
+ + Pattern.quote(MidicaPLParser.EFF_PERCENT)
+ ")?"
+ "$";
periodsPattern = Pattern.compile(periodsRegex);
@@ -242,6 +282,10 @@ public static boolean isFlow(String flow) {
*/
public static boolean applyFlowIfPossible(int channel, String flowStr, String lengthStr) throws ParseException {
+ // flow active but for a different channel? - close flow
+ if (flow != null && channel != flow.getChannel())
+ closeFlowIfPossible();
+
// looks like normal note or rest?
if (noteOrRestPattern.matcher(flowStr).find()) {
return false;
@@ -261,7 +305,8 @@ public static boolean applyFlowIfPossible(int channel, String flowStr, String le
String dot = m.group(1);
String elemName = m.group(2);
String numberStr = m.group(3);
- String paramStr = m.group(4);
+ String numberLsb = m.group(4);
+ String paramStr = m.group(5);
// flow or something else?
if (null == elemName) {
@@ -272,9 +317,9 @@ public static boolean applyFlowIfPossible(int channel, String flowStr, String le
else
throw new ParseException(Dict.get(Dict.ERROR_FL_UNKNOWN_ELEMENT) + elemName);
- // starts with dot? - flow must be open
- if (dot.length() > 0) {
- if (null == flowStr)
+ // starts with dot? - flow must be open (from the same channel)
+ if (dot != null && dot.length() > 0) {
+ if (null == flow)
throw new ParseException(String.format(Dict.get(Dict.ERROR_FL_NOT_OPEN), MidicaPLParser.FL_DOT));
}
else {
@@ -288,10 +333,12 @@ public static boolean applyFlowIfPossible(int channel, String flowStr, String le
// check number
int number = -1;
if (MidicaPLParser.FL_CTRL.equals(elemName)) {
- number = parseGenericNumber(numberStr, 0x7F, elemName);
+ if (numberLsb != null)
+ throw new ParseException(Dict.get(Dict.ERROR_FL_NUM_SEP_NOT_ALLOWED) + elemName);
+ number = parseGenericNumber(numberStr, numberLsb, 0x7F, elemName);
}
else if (MidicaPLParser.FL_RPN.equals(elemName) || MidicaPLParser.FL_NRPN.equals(elemName)) {
- number = parseGenericNumber(numberStr, 0x3FFF, elemName);
+ number = parseGenericNumber(numberStr, numberLsb, 0x3FFF, elemName);
}
else if (numberStr != null) {
throw new ParseException(Dict.get(Dict.ERROR_FL_NUMBER_NOT_ALLOWED) + elemName);
@@ -332,22 +379,35 @@ public static void closeFlowIfPossible() {
/**
* Parses a generic controller or (N)RPN number, assigned in a flow.
*
- * @param numberStr The number to be parsed.
+ * Needed by one of the following elements:
+ *
+ * - .ctrl=...
+ * - .rpn=...
+ * - .nrpn=...
+ *
+ * @param numberStr The MSB or whole number to be parsed.
+ * @param numberLsb The LSB, if numberStr is an MSB, or **null** if numberStr is the whole 14-bit number.
* @param maxNum The maximum allowed number.
* @param elemName Flow element name (for error messages).
* @return the parsed number
* @throws ParseException if the number cannot be parsed or is too high.
*/
- private static int parseGenericNumber(String numberStr, int maxNum, String elemName) throws ParseException {
+ private static int parseGenericNumber(String numberStr, String numberLsb, int maxNum, String elemName) throws ParseException {
// no number provided?
if (null == numberStr)
throw new ParseException(String.format(Dict.get(Dict.ERROR_FL_NUMBER_MISSING), elemName));
try {
- // parse the number
+ // parse the number or MSB
int number = Integer.parseInt(numberStr);
+ // LSB available? - parse it
+ if (numberLsb != null) {
+ int lsb = Integer.parseInt(numberLsb);
+ number = number * 128 + lsb;
+ }
+
// number higher then allowed by the controller or (n)rpn?
if (number > maxNum)
throw new ParseException(String.format(Dict.get(Dict.ERROR_FL_NUMBER_TOO_HIGH), numberStr, elemName, maxNum));
@@ -372,23 +432,6 @@ private static int parseGenericNumber(String numberStr, int maxNum, String elemN
*/
private static void applyFlowElement(String elemName, int number, String paramStr) throws ParseException {
- // check number
- if (MidicaPLParser.FL_CTRL.equals(elemName)) {
- if (number < 0)
- throw new ParseException("Number missing for element '" + elemName + "'. This should not happen. Please report.");
- else if (number > 127)
- throw new ParseException("Number for element '" + elemName + "' is higher than 127. This should not happen. Please report.");
- }
- else if (MidicaPLParser.FL_RPN.equals(elemName) || MidicaPLParser.FL_RPN.equals(elemName)) {
- if (number < 0)
- throw new ParseException("Number missing for element '" + elemName + "'. This should not happen. Please report.");
- else if (number > 16383)
- throw new ParseException("Number for element '" + elemName + "' is higher than 16383. This should not happen. Please report.");
- }
- else if (number > -1) {
- throw new ParseException("Number missing for element '" + elemName + "' not allowed. This should not happen. Please report.");
- }
-
// check presence of params
if (functionNames.contains(elemName)) {
if (paramStr == null && ! MidicaPLParser.FUNC_WAIT.equals(elemName))
@@ -434,7 +477,7 @@ else if (rpnNameToNumber.containsKey(elemName)) {
effectNumber = rpnNameToNumber.get(elemName);
}
else {
- throw new ParseException("Don't know what to do with effect '" + elemName + "'. This should not happen. Please report.");
+ throw new FatalParseException("Don't know what to do with effect '" + elemName + "'.");
}
}
@@ -449,20 +492,19 @@ else if (rpnNameToNumber.containsKey(elemName)) {
// unpack parameters - special case: treat 'wait' like 'wait()'
String[] params;
- if ((null == paramStr || paramStr.isEmpty()) && MidicaPLParser.FUNC_WAIT.equals(elemName))
- params = new String[] {};
+ if (paramStr != null && paramStr.isEmpty())
+ params = new String[] {}; // special case: Functions without any parameter.
+ else if ((null == paramStr || paramStr.isEmpty()) && MidicaPLParser.FUNC_WAIT.equals(elemName))
+ params = new String[] {}; // special case: wait/wait() without parameter
else
params = paramStr.split(Pattern.quote(MidicaPLParser.PARAM_SEPARATOR), -1);
// check number of parameters
Integer expectedCount = functionToParamCount.get(elemName);
if (null == expectedCount) {
- throw new ParseException("Expected parameter count unknown for function '" + elemName + "'. This should not happen. Please report.");
- }
- if (0 == expectedCount && 1 == params.length && paramStr.isEmpty()) {
- // OK. Special case for functions without any parameter.
+ throw new FatalParseException("Expected parameter count unknown for function '" + elemName + "'.");
}
- else if (0 == params.length && MidicaPLParser.FUNC_WAIT.equals(elemName)) {
+ if (0 == params.length && MidicaPLParser.FUNC_WAIT.equals(elemName)) {
// OK. Special case for wait() without parameter
}
else {
@@ -490,7 +532,7 @@ else if (0 == params.length && MidicaPLParser.FUNC_WAIT.equals(elemName)) {
return;
}
- throw new ParseException("Don't know what to do with flow element '" + elemName + "'. This should not happen. Please report.");
+ throw new FatalParseException("Don't know what to do with flow element '" + elemName + "'.");
}
/**
@@ -520,7 +562,7 @@ private static void applyFunction(String funcName, String[] params) throws Parse
// note()
if (MidicaPLParser.FUNC_NOTE.equals(funcName)) {
int note = parser.parseNote(params[0]);
- flow.setNote(note);
+ flow.setNote(note, funcName);
return;
}
@@ -529,20 +571,29 @@ private static void applyFunction(String funcName, String[] params) throws Parse
// on()/off() - boolean functions
if (MidicaPLParser.FUNC_ON.equals(funcName) || MidicaPLParser.FUNC_OFF.equals(funcName)) {
- if (valueType != EffectFlow.TYPE_BOOLEAN && valueType != EffectFlow.TYPE_ANY) {
+ int value = MidicaPLParser.FUNC_ON.equals(funcName) ? 127 : 0;
+
+ // check type
+ if (valueType == EffectFlow.TYPE_NONE && MidicaPLParser.FUNC_ON.equals(funcName)) {
+ // special case: allow on() for type NONE (but then use value=0)
+ value = 0;
+ }
+ else if (valueType != EffectFlow.TYPE_BOOLEAN && valueType != EffectFlow.TYPE_ANY) {
throw new ParseException(String.format(Dict.get(Dict.ERROR_FUNC_TYPE_NOT_BOOL), funcName));
}
- int value = MidicaPLParser.FUNC_ON.equals(funcName) ? 127 : 0;
setValue(new int[] {value, value});
return;
}
// non-boolean function for a boolean effect?
- if (EffectFlow.TYPE_BOOLEAN == valueType) {
+ if (EffectFlow.TYPE_BOOLEAN == valueType)
throw new ParseException(String.format(Dict.get(Dict.ERROR_FUNC_TYPE_BOOL), funcName));
- }
+
+ // non-boolean function for type 'none'
+ if (EffectFlow.TYPE_NONE == valueType)
+ throw new ParseException(String.format(Dict.get(Dict.ERROR_FUNC_TYPE_NONE), funcName));
// set()
if (MidicaPLParser.FUNC_SET.equals(funcName)) {
@@ -568,7 +619,7 @@ private static void applyFunction(String funcName, String[] params) throws Parse
return;
}
- throw new ParseException("Don't know what to do with function '" + funcName + "'. This should not happen. Please report.");
+ throw new FatalParseException("Don't know what to do with function '" + funcName + "'.");
}
/**
@@ -595,11 +646,7 @@ private static void setValue(int[] values) throws ParseException {
try {
if (EffectFlow.EFF_TYPE_CHANNEL == effectType) {
- if (0 == values.length) {
- SequenceCreator.addMessageChannelEffect(effectNum, channel, 0, 0, tick);
- return;
- }
- else if (2 == values.length) {
+ if (2 == values.length) {
// pitch bend?
if (0xE0 == flow.getEffectNumber()) {
@@ -624,11 +671,7 @@ else if (3 == values.length) {
}
}
else if (EffectFlow.EFF_TYPE_CTRL == effectType) {
- if (0 == values.length) {
- SequenceCreator.addMessageCtrl(effectNum, channel, 0, tick);
- return;
- }
- else if (2 == values.length) {
+ if (2 == values.length) {
SequenceCreator.addMessageCtrl(effectNum, channel, values[1], tick);
return;
}
@@ -639,18 +682,91 @@ else if (3 == values.length) {
}
}
else if (EffectFlow.EFF_TYPE_RPN == effectType) {
- // TODO: implement
+ setRpnOrNrpn(true, effectNum, values);
+ return;
}
else if (EffectFlow.EFF_TYPE_NRPN == effectType) {
- // TODO: implement
+ setRpnOrNrpn(false, effectNum, values);
+ return;
}
else {
- throw new ParseException("Unknown effect type: " + effectType + ". This should not happen. Please report.");
+ throw new FatalParseException("Unknown effect type: " + effectType + ".");
}
- throw new ParseException("Unknown effect type/number/byte-count combination: " + effectType + "/" + effectNum + "/" + values.length + ". This should not happen. Please report.");
+ throw new FatalParseException("Unknown effect type/number/byte-count combination: " + effectType + "/" + effectNum + "/" + values.length + ".");
+ }
+ catch (InvalidMidiDataException e) {
+ throw new FatalParseException("Invalid MIDI data when trying to apply effect " + effectType + "/" + effectNum + ".");
+ }
+ }
+
+ /**
+ * Writes an RPN or NRPN to the MIDI sequence.
+ *
+ * The given **values** array has the same format as the one returned
+ * by {@link #parseIntParam(String)}. It contains 2 or 3 bytes.
+ *
+ * The first byte is always the raw value.
+ *
+ * The second byte is either the MSB (if there is a third byte) or the only value.
+ *
+ * If the third value is available, it contains the LSB.
+ *
+ * @param isRpn **true** to create an RPN, **false** to create an NRPN.
+ * @param effectNum 14-bit number of the RPN or NRPN
+ * @param values see above
+ * @throws ParseException if there is an unexpected problem.
+ */
+ private static void setRpnOrNrpn(boolean isRpn, int effectNum, int[] values) throws ParseException {
+
+ long tick = flow.getCurrentTick();
+ int channel = flow.getChannel();
+
+ int rpnMsb = isRpn ? 0x65 : 0x63;
+ int rpnLsb = isRpn ? 0x64 : 0x62;
+
+ int effectMsb = effectNum >> 7;
+ int effectLsb = effectNum & 0x7F;
+
+ long tick1 = tick - 3 * RPN_DISTANCE;
+ long tick2 = tick - 2 * RPN_DISTANCE;
+ long tick3 = tick - RPN_DISTANCE;
+ long tick4 = tick;
+ long tick5 = tick + RPN_DISTANCE;
+ if (tick1 < 0) tick1 = 0;
+ if (tick2 < 0) tick2 = 0;
+ if (tick3 < 0) tick3 = 0;
+
+ try {
+ // (N)RPN MSB / LSB
+ SequenceCreator.addMessageCtrl(rpnMsb, channel, effectMsb, tick1);
+ SequenceCreator.addMessageCtrl(rpnLsb, channel, effectLsb, tick2);
+
+ // data entry MSB / LSB
+ SequenceCreator.addMessageCtrl(0x06, channel, values[1], tick3);
+ if (values.length > 2)
+ SequenceCreator.addMessageCtrl(0x26, channel, values[2], tick4);
+
+ // (N)RPN reset MSB / LSB
+ SequenceCreator.addMessageCtrl(rpnMsb, channel, 0x7F, tick5);
+ SequenceCreator.addMessageCtrl(rpnLsb, channel, 0x7F, tick5);
}
catch (InvalidMidiDataException e) {
- throw new ParseException("Invalid MIDI data when trying to apply effect " + effectType + "/" + effectNum + ". This should not happen. Please report.");
+ throw new FatalParseException("Invalid MIDI data when trying to apply (N)RPN " + effectNum + ".");
+ }
+
+ // special case: pitch bend range
+ if (isRpn && 0x0000 == flow.getEffectNumber()) {
+ TreeMap rangeMap = pitchBendRangeByChannel.get(channel);
+
+ // get range in half tone steps
+ float halfToneSteps = values[1];
+ if (values.length > 2) {
+ float cents = values[2] / 100f;
+ halfToneSteps += cents;
+ }
+
+ // remember pitch bend range
+ rangeMap.put(tick, halfToneSteps);
}
}
@@ -715,7 +831,6 @@ private static void applyContinousFunction(String function, String[] params) thr
}
// calculate the value for each tick
- System.err.println("_____");
for (long tick = 0; tick <= tickDiff; tick++) {
int[] setValues = new int[byteCount];
@@ -733,7 +848,6 @@ private static void applyContinousFunction(String function, String[] params) thr
value = value < 0 ? value * negDiff : value * posDiff;
value = Math.round(value) + middleVal;
setValues[0] = (int) value;
- System.err.println(setValues[0]); // TODO: delete
}
// calculate MSB / LSB
@@ -765,12 +879,12 @@ private static void applyContinousFunction(String function, String[] params) thr
}
/**
- * Parses a numeric or percentage function parameter, checks it against the
+ * Parses a numeric or percentage or float or MSB/LSB function parameter, checks it against the
* sound effect's min/max and returns the resulting value byte(s).
*
* The returned array consists of the following bytes:
*
- * - first byte: the complete value
+ * - first byte: the complete value (up to 14 bits)
* - second byte: first data byte (or MSB)
* - third byte: second data byte (or LSB)
*
@@ -782,32 +896,39 @@ private static void applyContinousFunction(String function, String[] params) thr
*/
private static int[] parseIntParam(String valueStr) throws ParseException {
- // TODO: handle TYPE_NONE
-
// get range of the sound effect
int min = flow.getMin();
int max = flow.getMax();
+ boolean needHalfTones = flow.mustUseHalfToneSteps();
+ boolean canUseHalfTones = flow.supportsHalfToneSteps();
+ boolean isMsbLsb = false;
// parse the parameter
Integer value = null;
try {
Matcher m = intPattern.matcher(valueStr);
if (m.matches()) {
- String intStr = m.group(1);
- String percentStr = m.group(2);
- //String floatStr = m.group(3); // TODO: something to make pitch bend easier to use
+ String intStr = m.group(1);
+ String percentStr = m.group(2);
+ String halfToneStr = m.group(3);
+ String msbStr = m.group(4);
+ String lsbStr = m.group(5);
if (intStr != null) {
- value = Integer.parseInt(intStr);
+ if (canUseHalfTones)
+ value = parseHalfToneSteps(intStr);
+ else
+ value = Integer.parseInt(intStr);
}
else if (percentStr != null) {
float percent = Float.parseFloat(percentStr);
+ if (needHalfTones)
+ throw new ParseException(Dict.get(Dict.ERROR_FUNC_NEED_HALFTONE) + valueStr);
// A negative percentage with a minimum of 0 should NOT evaluate to 0
// but throw an exception instead.
if (percent < 0 && 0 == min)
throw new ParseException(String.format(Dict.get(Dict.ERROR_FUNC_VAL_LOWER_MIN), valueStr, min));
- // TODO: check
if (percent < 0)
// theoretically: value = percent * -min / 100
value = (int) ((percent * -min * 10 - 100 * 5) / (100 * 10));
@@ -815,6 +936,27 @@ else if (percentStr != null) {
// theoretically: value = percent * max / 100
value = (int) ((percent * max * 10 + 100 * 5) / (100 * 10));
}
+ else if (halfToneStr != null) {
+ if (!canUseHalfTones)
+ throw new ParseException(String.format(Dict.get(Dict.ERROR_FUNC_HALFTONE_NOT_ALLOWED), valueStr));
+
+ value = parseHalfToneSteps(halfToneStr);
+ }
+ else if (msbStr != null) {
+ isMsbLsb = true;
+ if (!flow.isDouble())
+ throw new ParseException(String.format(
+ Dict.get(Dict.ERROR_FUNC_MSB_LSB_NEEDS_DOUBLE), valueStr, MidicaPLParser.FL_DOUBLE));
+ int msb = Integer.parseInt(msbStr);
+ int lsb = Integer.parseInt(lsbStr);
+ if (msb > 127)
+ throw new ParseException(String.format(
+ Dict.get(Dict.ERROR_FUNC_MSB_TOO_HIGH), valueStr, msbStr));
+ if (lsb > 127)
+ throw new ParseException(String.format(
+ Dict.get(Dict.ERROR_FUNC_LSB_TOO_HIGH), valueStr, lsbStr));
+ value = msb * 128 + lsb;
+ }
else {
throw new ParseException(Dict.get(Dict.ERROR_FUNC_NO_NUMBER) + valueStr);
}
@@ -829,18 +971,12 @@ else if (percentStr != null) {
// check value against min / max
if (value < min)
throw new ParseException(String.format(Dict.get(Dict.ERROR_FUNC_VAL_LOWER_MIN), valueStr, min));
- if (value > max)
+ if (value > max && !needHalfTones && !isMsbLsb)
throw new ParseException(String.format(Dict.get(Dict.ERROR_FUNC_VAL_GREATER_MAX), valueStr, max));
// adjust the actual MIDI value for signed types
int valueType = flow.getValueType(valueStr);
- if (EffectFlow.TYPE_BYTE_SIGNED == valueType) {
- value += 64;
- }
- if (EffectFlow.TYPE_DOUBLE_SIGNED == valueType) {
- value += 8192;
- }
- if (EffectFlow.TYPE_MSB_SIGNED == valueType) {
+ if (EffectFlow.TYPE_MSB_SIGNED == valueType && !isMsbLsb) {
if (flow.isDouble())
value += 8192;
else
@@ -849,26 +985,126 @@ else if (percentStr != null) {
// find out how many bytes are needed
int byteCount = 1;
- if (EffectFlow.TYPE_DOUBLE == valueType || EffectFlow.TYPE_DOUBLE_SIGNED == valueType) {
- byteCount = 2;
- }
- else if (EffectFlow.TYPE_MSB == valueType || EffectFlow.TYPE_MSB_SIGNED == valueType) {
+ if (EffectFlow.TYPE_MSB == valueType || EffectFlow.TYPE_MSB_SIGNED == valueType || EffectFlow.TYPE_MSB_HALFTONES == valueType) {
if (flow.isDouble())
byteCount = 2;
}
- // handle 0 and 1 byte
- if (0 == byteCount) {
- return new int[] {};
- }
- else if (1 == byteCount) {
+ // handle 1 byte
+ if (1 == byteCount)
return new int[] {value, value};
- }
// handle 2 bytes
int msb = value >> 7;
int lsb = value & 0x7F;
- return new int[] {value, msb, lsb};
+ int[] values = new int[] {value, msb, lsb};
+ adjustRoundingEdgeCaseForPitchBendRange(values);
+
+ return values;
+ }
+
+ /**
+ * Parses half-tone parameters (flowt or int).
+ *
+ * This is used for one of the following effect types:
+ *
+ * - pitch bend range
+ * - pitch bend
+ *
+ * @param halfToneStr the half tone steps parameter
+ * @return the value that the effect type needs.
+ * @throws ParseException
+ */
+ // TODO: also use for channel coarse/fine tuning?
+ private static int parseHalfToneSteps(String halfToneStr) throws ParseException {
+
+ int effNum = flow.getEffectNumber();
+
+ float halfToneSteps = Float.parseFloat(halfToneStr);
+
+ // pitch bend range
+ if (0x0000 == effNum) {
+
+ float max = flow.isDouble() ? 127.99f : 127;
+ if (halfToneSteps > max)
+ throw new ParseException(String.format(Dict.get(Dict.ERROR_FUNC_VAL_GREATER_MAX), halfToneStr, max));
+
+ // only one byte?
+ if (!flow.isDouble())
+ return Math.round(halfToneSteps);
+
+ // 2 bytes
+ int msb = (int) halfToneSteps;
+ float remainder = halfToneSteps - msb;
+ int lsb = Math.round(remainder * 100);
+ return msb * 128 + lsb;
+ }
+
+ // pitch bend
+ if (0xE0 == effNum) {
+
+ // get current pitch bend range
+ int channel = flow.getChannel();
+ long tick = flow.getCurrentTick();
+ Entry entry = pitchBendRangeByChannel.get(channel).floorEntry(tick);
+ float range = entry.getValue();
+
+ // range exceeded?
+ if (Math.abs(halfToneSteps) > range)
+ throw new ParseException(String.format(Dict.get(Dict.ERROR_FUNC_HALFTONE_GT_RANGE), halfToneStr, range));
+
+ // get max value
+ int max;
+ if (flow.isDouble())
+ max = halfToneSteps < 0 ? 8192 : 8191;
+ else
+ max = halfToneSteps < 0 ? 64 : 63;
+
+ return Math.round(max * (halfToneSteps / range));
+ }
+
+ // this should never be reached
+ int elemType = flow.getEffectType();
+ throw new FatalParseException("Don't know what to do with half tone steps of effect '" + elemType + "/" + effNum + "'.");
+ }
+
+ /**
+ * Checks and corrects rounding errors for half tone parameters with rounding errors.
+ *
+ * Only applies for:
+ *
+ * - pitch bend range
+ *
+ * Does nothing for other effect types.
+ *
+ * Fixes an edge case with MSB and LSB where the rounded LSB value is 100.
+ * Example: 4.997
+ *
+ * Here the MSB must be set to 5 and the LSB must be set to 0.
+ *
+ * @param values the values as returned by {@link #parseIntParam(String)}
+ */
+ private static void adjustRoundingEdgeCaseForPitchBendRange(int[] values) {
+
+ // not pitch bend range?
+ if (!flow.supportsHalfToneSteps())
+ return;
+ if (flow.getEffectNumber() != 0x0000)
+ return;
+
+ // the current flow is a pitch bend range RPN
+ int msb = values[1];
+ int lsb = values[2];
+
+ // fix rounding issue.
+ // Example: 4.997 ==> MSB = 4, LSB = 100
+ // Fix: ==> MSB = 5, LSB = 0
+ if (lsb >= 100) {
+ msb++;
+ lsb = 0;
+ values[1] = msb;
+ values[2] = lsb;
+ }
}
/**
@@ -891,6 +1127,9 @@ private static float parsePeriodsParam(String valueStr) throws ParseException {
if (periods < 0) {
throw new ParseException(Dict.get(Dict.ERROR_FUNC_PERIODS_NEG) + valueStr);
}
+ if (Float.isInfinite(periods)) {
+ throw new ParseException(Dict.get(Dict.ERROR_FUNC_PERIODS_NO_NUMBER) + valueStr);
+ }
if (percentStr != null) {
periods /= 100;
}
diff --git a/src/org/midica/file/read/EffectFlow.java b/src/org/midica/file/read/EffectFlow.java
index 8b1c612..f954457 100644
--- a/src/org/midica/file/read/EffectFlow.java
+++ b/src/org/midica/file/read/EffectFlow.java
@@ -28,12 +28,10 @@ public class EffectFlow {
public static final int TYPE_BOOLEAN = 1; // 0=off, 127=on
public static final int TYPE_MSB = 3; // default: 0 - 127, double: 0 - 16383
public static final int TYPE_MSB_SIGNED = 5; // default: -64 - 63, double: -8192 - 8191
+ public static final int TYPE_MSB_HALFTONES = 7; // default: 0 - 127, double: 0 - 127.99
public static final int TYPE_BYTE = 11; // 0 - 127
- public static final int TYPE_DOUBLE = 13; // 0 - 16383
public static final int TYPE_ANY = 15; // anything that fits in 7 bits
public static final int TYPE_NONE = 17; // no value allowed (using 0 internally)
- public static final int TYPE_BYTE_SIGNED = 19; // -64 - 63
- public static final int TYPE_DOUBLE_SIGNED = 21; // -8192 - 8191
// effect types
public static final int EFF_TYPE_CHANNEL = 1;
@@ -133,6 +131,54 @@ public int getEffectNumber() {
return effectNumber;
}
+ /**
+ * Determines if the current effect type supports half-tone-steps as parameters.
+ *
+ * Examples:
+ *
+ * - pitch bend
+ * - pitch bend range
+ *
+ * @return **true** if half tone steps are supported, otherwise **false**.
+ */
+ public boolean supportsHalfToneSteps() {
+
+ if (EFF_TYPE_CHANNEL == effectType) {
+
+ // pitch bend
+ if (0xE0 == effectNumber)
+ return true;
+ }
+ else if (EFF_TYPE_RPN == effectType) {
+
+ // pitch bend range
+ if (0x0000 == effectNumber)
+ return true;
+ }
+
+ // TODO: how do we treat channel coarse/fine tuning?
+
+ return false;
+ }
+
+ /**
+ * Determines if the current effect type supports ONLY half-tone-steps as parameters.
+ * That means, normal values are **not** accepted.
+ *
+ * This is the case for the **pitch bend range**.
+ *
+ * @return **true** if only half-tone-steps are accepted as parameters.
+ */
+ public boolean mustUseHalfToneSteps() {
+
+ // pitch bend range
+ if (EFF_TYPE_RPN == effectType && 0x0000 == effectNumber) {
+ return true;
+ }
+
+ return false;
+ }
+
/**
* Applies a **length(...)** function call in the flow.
*
@@ -146,9 +192,27 @@ public void setLength(String lengthStr) throws ParseException {
/**
* Sets the note in the flow.
*
- * @param note note number (0 - 127)
+ * @param note note number (0 - 127)
+ * @param elemName element name that caused the call (only used for error messages)
+ * @throws ParseException if the effect is not set or doesn't support a note.
*/
- public void setNote(int note) throws ParseException {
+ public void setNote(int note, String elemName) throws ParseException {
+
+ // effect not yet set?
+ if (effectNumber < 0)
+ throw new ParseException(Dict.get(Dict.ERROR_FL_EFF_NOT_SET) + elemName);
+
+ // check if effect type supports a note
+ boolean isNoteSupported = false;
+ if (EFF_TYPE_CHANNEL == effectType && 0xA0 == effectNumber) // poly_at
+ isNoteSupported = true;
+ else if (EFF_TYPE_CTRL == effectType && 0x54 == effectNumber) // portamento ctrl
+ isNoteSupported = true;
+
+ // note not supported?
+ if (!isNoteSupported)
+ throw new ParseException(Dict.get(Dict.ERROR_FL_NOTE_NOT_SUPP) + elemName);
+
this.note = note;
}
@@ -161,7 +225,7 @@ public void setNote(int note) throws ParseException {
public void setDouble() throws ParseException {
int valueType = getValueType(MidicaPLParser.FL_DOUBLE);
- if (TYPE_MSB == valueType || TYPE_MSB_SIGNED == valueType) {
+ if (TYPE_MSB == valueType || TYPE_MSB_SIGNED == valueType || TYPE_MSB_HALFTONES == valueType) {
isDouble = true;
return;
}
@@ -231,12 +295,10 @@ public long getFutureTick() {
* - {@link #TYPE_BOOLEAN}
* - {@link #TYPE_MSB}
* - {@link #TYPE_MSB_SIGNED}
+ * - {@link #TYPE_MSB_HALFTONES}
* - {@link #TYPE_BYTE}
- * - {@link #TYPE_DOUBLE}
* - {@link #TYPE_ANY}
* - {@link #TYPE_NONE}
- * - {@link #TYPE_BYTE_SIGNED}
- * - {@link #TYPE_DOUBLE_SIGNED}
*
* @param elemName element name that caused the call (only used for error messages)
* @return see above
@@ -268,7 +330,7 @@ else if (EFF_TYPE_NRPN == effectType) {
// not found?
if (null == valueType)
- throw new ParseException("Unknown effect number '" + effectNumber + "' for effect type '" + typeStr + "'. This should not happen. Please report.");
+ throw new FatalParseException("Unknown effect number '" + effectNumber + "' for effect type '" + typeStr + "'.");
return valueType;
}
@@ -301,7 +363,7 @@ else if (EFF_TYPE_NRPN == effectType) {
}
if (null == min)
- throw new ParseException("Unknown min value for '" + effectType + "/" + effectNumber + ". This should not happen. Please report.");
+ throw new FatalParseException("Unknown min value for '" + effectType + "/" + effectNumber + ".");
return min;
}
@@ -331,13 +393,17 @@ else if (EFF_TYPE_NRPN == effectType) {
// handle MSB / LSB
if (isDouble) {
if (127 == max)
- return 16383;
+ max = 16383;
if (63 == max)
- return 8191;
+ max = 8191;
+
+ // special case: pitch bend range
+ if (TYPE_MSB_HALFTONES == rpnToType.get(effectNumber))
+ max = 128;
}
if (null == max)
- throw new ParseException("Unknown max value for '" + effectType + "/" + effectNumber + ". This should not happen. Please report.");
+ throw new FatalParseException("Unknown max value for '" + effectType + "/" + effectNumber + ".");
return max;
}
@@ -365,7 +431,7 @@ else if (EFF_TYPE_NRPN == effectType) {
}
if (null == def) {
- throw new ParseException("Unknown default value for '" + effectType + "/" + effectNumber + ". This should not happen. Please report.");
+ throw new FatalParseException("Unknown default value for '" + effectType + "/" + effectNumber + ".");
}
return def;
@@ -449,12 +515,18 @@ else if (EFF_TYPE_NRPN == effectType) {
/////////////////////////////
for (int i = 0x0000; i < 0x4000; i++) {
- rpnToType.put(i, TYPE_DOUBLE);
+ rpnToType.put(i, TYPE_MSB);
rpnToDefault.put(i, 0);
- nrpnToType.put(i, TYPE_DOUBLE);
+ nrpnToType.put(i, TYPE_MSB);
nrpnToDefault.put(i, 0);
}
+ // exceptions (type)
+ rpnToType.put(0x0000, TYPE_MSB_HALFTONES); // pitch bend range
+ rpnToType.put(0x0001, TYPE_MSB_SIGNED); // channel fine tuning
+ rpnToType.put(0x0002, TYPE_MSB_SIGNED); // channel coarse tuning
+ rpnToType.put(0x7F7F, TYPE_NONE); // reset RPN
+
// min / max
applyDefaultMinAndMax(rpnToType, rpnToDefault, rpnToMin, rpnToMax);
applyDefaultMinAndMax(nrpnToType, nrpnToDefault, nrpnToMin, nrpnToMax);
@@ -464,11 +536,6 @@ else if (EFF_TYPE_NRPN == effectType) {
rpnToDefault.put(0x0001, 0x4000); // channel fine tuning
rpnToDefault.put(0x0002, 0x4000); // channel coarse tuning
rpnToDefault.put(0x0005, 0x0040); // modulation depth range
-
- // exceptions (type)
- rpnToType.put(0x0001, TYPE_DOUBLE_SIGNED); // channel fine tuning
- rpnToType.put(0x0002, TYPE_DOUBLE_SIGNED); // channel coarse tuning
- rpnToType.put(0x7F7F, TYPE_NONE); // reset RPN
}
/**
@@ -484,22 +551,14 @@ private static void applyDefaultMinAndMax(Map typeStructure, M
for (int number : typeStructure.keySet()) {
int type = typeStructure.get(number);
defaultStructure.put(number, 0);
- if (TYPE_BYTE == type || TYPE_MSB == type || TYPE_ANY == type) {
+ if (TYPE_BYTE == type || TYPE_MSB == type || TYPE_MSB_HALFTONES == type || TYPE_ANY == type) {
minStructure.put(number, 0);
maxStructure.put(number, 127);
}
- else if (TYPE_BYTE_SIGNED == type || TYPE_MSB_SIGNED == type) {
+ else if (TYPE_MSB_SIGNED == type) {
minStructure.put(number, -64);
maxStructure.put(number, 63);
}
- else if (TYPE_DOUBLE == type) {
- minStructure.put(number, 0);
- maxStructure.put(number, 16383);
- }
- else if (TYPE_DOUBLE_SIGNED == type) {
- minStructure.put(number, -8192);
- maxStructure.put(number, 8191);
- }
}
}
}
diff --git a/src/org/midica/file/read/FatalParseException.java b/src/org/midica/file/read/FatalParseException.java
new file mode 100644
index 0000000..288c5ab
--- /dev/null
+++ b/src/org/midica/file/read/FatalParseException.java
@@ -0,0 +1,41 @@
+/*
+ * This Source Code Form is subject to the terms of the
+ * Mozilla Public License, v. 2.0.
+ * If a copy of the MPL was not distributed with this file,
+ * You can obtain one at http://mozilla.org/MPL/2.0/.
+ */
+
+package org.midica.file.read;
+
+import javax.sound.midi.InvalidMidiDataException;
+
+/**
+ * Exceptions of this class can be thrown if a really unexpected error occurs while parsing a file.
+ *
+ * This is used for exceptions caused by problems in the Midica source code itself.
+ *
+ * @author Jan Trukenmüller
+ */
+public class FatalParseException extends ParseException {
+
+ private static final long serialVersionUID = 1L;
+
+ private static final String suffix = "
This should never happen. Please file a bug report.";
+
+ /**
+ * Throws an exception including a detail message.
+ *
+ * @param message
+ */
+ public FatalParseException(String message) {
+ super(message + suffix);
+ }
+
+ /**
+ * Throws an exception caused by an InvalidMidiDataException.
+ * @param e
+ */
+ public FatalParseException(InvalidMidiDataException e) {
+ super("Invalid MIDI data: " + e.getMessage() + suffix);
+ }
+}
diff --git a/src/org/midica/file/read/MidicaPLParser.java b/src/org/midica/file/read/MidicaPLParser.java
index 29b9c44..9b386e3 100644
--- a/src/org/midica/file/read/MidicaPLParser.java
+++ b/src/org/midica/file/read/MidicaPLParser.java
@@ -231,6 +231,7 @@ public class MidicaPLParser extends SequenceParser {
public static String FL_RPN = null;
public static String FL_NRPN = null;
public static String FL_ASSIGNER = null;
+ public static String FL_GEN_NUM_SEP = null;
public static String FUNC_SET = null;
public static String FUNC_ON = null;
public static String FUNC_OFF = null;
@@ -513,6 +514,7 @@ public static void refreshSyntax() {
FL_RPN = Dict.getSyntax( Dict.SYNTAX_FL_RPN );
FL_NRPN = Dict.getSyntax( Dict.SYNTAX_FL_NRPN );
FL_ASSIGNER = Dict.getSyntax( Dict.SYNTAX_FL_ASSIGNER );
+ FL_GEN_NUM_SEP = Dict.getSyntax( Dict.SYNTAX_FL_GEN_NUM_SEP );
FUNC_SET = Dict.getSyntax( Dict.SYNTAX_FUNC_SET );
FUNC_ON = Dict.getSyntax( Dict.SYNTAX_FUNC_ON );
FUNC_OFF = Dict.getSyntax( Dict.SYNTAX_FUNC_OFF );
@@ -1062,6 +1064,7 @@ private void parseTokens(String[] tokens) throws ParseException {
// continue or not - decide depending on parsing run and current mode
boolean mustIgnore = mustIgnore(tokens[0]);
if (mustIgnore) {
+ Effect.closeFlowIfPossible();
return;
}
@@ -1117,9 +1120,10 @@ else if (isBlock) {
nestableBlkStack.peek().add(tokens); // add to block
}
parseGlobalCmd(tokens, isFake);
+ Effect.closeFlowIfPossible();
}
- // channel or instruments command
+ // lowlevel channel or instruments command
else if (tokens[0].matches("^\\d+$")) {
if (!isFunct) {
checkInstrumentsParsed();
@@ -1144,7 +1148,9 @@ else if (tokens[0].matches("^\\d+$")) {
currentFunction.add(String.join(" ", tokens)); // add to function
else if (isBlock)
nestableBlkStack.peek().add(tokens); // add to block
+ Effect.closeFlowIfPossible();
parsePatternCall(tokens, isFake);
+ Effect.closeFlowIfPossible();
return;
}
}
@@ -1181,6 +1187,7 @@ else if (VAR.equals(tokens[0])) {
else if (isBlock)
nestableBlkStack.peek().add(tokens); // add to block
parseVAR(tokens, isFake);
+ Effect.closeFlowIfPossible();
}
// line begins with variable?
@@ -1208,6 +1215,7 @@ else if (isBlock)
nestableBlkStack.peek().add(tokens); // add to block
parseSingleLineInstrumentSwitch(tokens, isFake);
+ Effect.closeFlowIfPossible();
}
// call a function
@@ -1221,7 +1229,9 @@ else if (isBlock)
nestableBlkStack.peek().add(tokens); // add to block
}
+ Effect.closeFlowIfPossible();
parseCALL(tokens, isFake);
+ Effect.closeFlowIfPossible();
}
// nestable block commands
@@ -1232,7 +1242,9 @@ else if (BLOCK_OPEN.equals(tokens[0]) || BLOCK_CLOSE.equals(tokens[0])) {
if (isFunct)
currentFunction.add(String.join(" ", tokens));
+ Effect.closeFlowIfPossible();
parseBLOCK(tokens, isFunct);
+ Effect.closeFlowIfPossible();
}
// mode command (instruments, function, meta, soft karaoke, end)
@@ -1242,7 +1254,9 @@ else if (INSTRUMENTS.equals(tokens[0])
|| META.equals(tokens[0])
|| META_SOFT_KARAOKE.equals(tokens[0])
|| END.equals(tokens[0])) {
+ Effect.closeFlowIfPossible();
parseModeCmd(tokens);
+ Effect.closeFlowIfPossible();
return;
}
@@ -1260,6 +1274,7 @@ else if (MODE_SOFT_KARAOKE == currentMode) {
// define, chord, const, include, soundbank
else {
parseRootLevelCmd(tokens);
+ Effect.closeFlowIfPossible();
}
}
@@ -1337,10 +1352,7 @@ else if ( varPattern.matcher(cmd).matches() ) {
// - the variable should have been replaced already; or
// - an exception for an undefined variable should have been thrown already.
if (isDefaultParsRun && 0 == nestableBlkDepth && currentMode == MODE_DEFAULT) {
- throw new ParseException(
- Dict.get(Dict.ERROR_UNKNOWN_CMD) + cmd
- + "\n This should not happen. Please report."
- );
+ throw new FatalParseException(Dict.get(Dict.ERROR_UNKNOWN_CMD) + cmd);
}
}
else {
@@ -1945,10 +1957,7 @@ else if (META_SOFT_KARAOKE.equals(cmd)) {
// other - may never happen
else {
- throw new ParseException(
- "Invalid Command " + cmd + " found."
- + "This should not happen. Please report."
- );
+ throw new FatalParseException("Invalid Command " + cmd + " found.");
}
}
@@ -2620,7 +2629,7 @@ private void parsePatternCall(String[] tokens, boolean isFake) throws ParseExcep
outerOptStr = patCallMatcher.group(4);
}
else {
- throw new ParseException("Pattern Call Error\nThis should not happen. Please report.");
+ throw new FatalParseException("Pattern Call Error.");
}
// get channel and pattern content
@@ -3105,7 +3114,7 @@ else if (COMPACT_CHANNEL.equals(tokens[0])) {
}
}
else if (0 == tokens.length) {
- throw new ParseException("Pattern Error\nThis should not happen. Please report.");
+ throw new FatalParseException("Pattern Error");
}
// add line to pattern
@@ -3220,9 +3229,9 @@ else if (iPat == 2)
*
* @param syllable the syllable to be unescaped.
* @param tick the tick when the syllable occurs.
- * @throws ParseException if a MIDI problem occurs.
+ * @throws FatalParseException if a MIDI problem occurs.
*/
- private void applySyllable(String syllable, long tick) throws ParseException {
+ private void applySyllable(String syllable, long tick) throws FatalParseException {
syllable = syllable.replace(LYRICS_SPACE, " ");
syllable = syllable.replace(LYRICS_COMMA, ",");
if (!isSoftKaraoke) {
@@ -3237,7 +3246,7 @@ private void applySyllable(String syllable, long tick) throws ParseException {
SequenceCreator.addMessageLyrics(syllable, tick, false);
}
catch (InvalidMidiDataException e) {
- throw new ParseException(Dict.get(Dict.ERROR_MIDI_PROBLEM) + e.getMessage());
+ throw new FatalParseException(e);
}
}
@@ -3279,7 +3288,7 @@ else if (parts.length == 1) {
if (condMatcher.find())
operator = condMatcher.group(1);
else
- throw new ParseException("invalid operator\nThis should not happen. Please report.");
+ throw new FatalParseException("invalid operator.");
// check for forbidden whitespaces
Matcher wsFirstMatcher = whitespace.matcher(first);
@@ -3345,7 +3354,7 @@ else if (isCondCheckParsRun && "".equals(candidate))
return candidateList.contains(first);
}
else
- throw new ParseException("invalid operator (" + operator + ")\nThis should not happen. Please report.");
+ throw new FatalParseException("invalid operator (" + operator + ")");
}
}
@@ -3750,6 +3759,7 @@ else if (3 == tokens.length) {
else if ( Dict.SYNTAX_FL_RPN.equals(cmdId) ) FL_RPN = cmdName;
else if ( Dict.SYNTAX_FL_NRPN.equals(cmdId) ) FL_NRPN = cmdName;
else if ( Dict.SYNTAX_FL_ASSIGNER.equals(cmdId) ) FL_ASSIGNER = cmdName;
+ else if ( Dict.SYNTAX_FL_GEN_NUM_SEP.equals(cmdId) ) FL_GEN_NUM_SEP = cmdName;
else if ( Dict.SYNTAX_FUNC_SET.equals(cmdId) ) FUNC_SET = cmdName;
else if ( Dict.SYNTAX_FUNC_ON.equals(cmdId) ) FUNC_ON = cmdName;
else if ( Dict.SYNTAX_FUNC_OFF.equals(cmdId) ) FUNC_OFF = cmdName;
@@ -4206,7 +4216,7 @@ else if (bankMSB > 127) {
SequenceCreator.initChannel(channel, instrNum, instrName, tick);
}
catch (InvalidMidiDataException e) {
- throw new ParseException(Dict.get(Dict.ERROR_MIDI_PROBLEM) + e.getMessage());
+ throw new FatalParseException(e);
}
}
else {
@@ -4444,7 +4454,7 @@ else if (flatPattern.matcher(noteName).find())
}
}
catch (InvalidMidiDataException e) {
- throw new ParseException(Dict.get(Dict.ERROR_MIDI_PROBLEM) + e.getMessage());
+ throw new FatalParseException(e);
}
}
@@ -4460,7 +4470,7 @@ public int getCompactChannel(String token) throws ParseException {
if (matcher.matches())
return toChannel(matcher.group(1));
else
- throw new ParseException("Unable to determine channel from compact command.\n This should not happen. Please report.");
+ throw new FatalParseException("Unable to determine channel from compact command.");
}
/**
@@ -4476,10 +4486,7 @@ public int getCompactChannel(String token) throws ParseException {
public String[] reorganizeCompactCmd(String[] tokens) throws ParseException {
if (tokens.length > 3) {
- throw new ParseException(
- "More than 3 tokens in a compact command."
- + "\n This should not happen. Please report."
- );
+ throw new FatalParseException("More than 3 tokens in a compact command.");
}
if (3 == tokens.length) {
String[] newTokens = new String[2];
@@ -4655,7 +4662,7 @@ else if (barLineMatcher.matches()) {
adjustCompactNoteLength(channel, null, false);
}
- // create normal channel command
+ // create lowlevel channel command
if (null == chCmdTokens) {
chCmdTokens = new String[] {
channel + "",
@@ -4699,9 +4706,7 @@ else if (barLineMatcher.matches()) {
return;
}
- throw new ParseException(
- "Compact Channel Command invalid\n This should not happen. Please report."
- );
+ throw new FatalParseException("Compact Channel Command invalid.");
}
}
@@ -4803,6 +4808,9 @@ private void parseChannelCmd(String[] tokens, boolean isFake) throws ParseExcept
if (Effect.applyFlowIfPossible(channel, tokens[1], durationStr)) {
return;
}
+ else {
+ Effect.closeFlowIfPossible();
+ }
}
// note or rest
@@ -4968,7 +4976,7 @@ else if (currentDuration > tremolo) {
}
}
catch (InvalidMidiDataException e) {
- throw new ParseException(Dict.get(Dict.ERROR_MIDI_PROBLEM) + e.getMessage());
+ throw new FatalParseException(e);
}
if (multiple) {
@@ -5533,9 +5541,9 @@ else if (2 == limits.length) {
* Thereby all undefined channels will be initialized with a fake instrument
* so that the data structures work.
*
- * @throws ParseException if something went wrong.
+ * @throws FatalParseException if a MIDI problem occurs.
*/
- private void postprocessInstrumentsIfNotYetDone() throws ParseException {
+ private void postprocessInstrumentsIfNotYetDone() throws FatalParseException {
// postprocessing already done?
if (instrumentsParsed)
@@ -5590,7 +5598,7 @@ private void postprocessInstrumentsIfNotYetDone() throws ParseException {
}
}
catch (InvalidMidiDataException e) {
- throw new ParseException(Dict.get(Dict.ERROR_MIDI_PROBLEM) + e.getMessage());
+ throw new FatalParseException(e);
}
instrumentsParsed = true;
@@ -5601,9 +5609,9 @@ private void postprocessInstrumentsIfNotYetDone() throws ParseException {
*
* Sets all defined meta events in the MIDI sequence.
*
- * @throws ParseException if something went wrong.
+ * @throws FatalParseException if a MIDI problem occurs.
*/
- private void postprocessMeta() throws ParseException {
+ private void postprocessMeta() throws FatalParseException {
try {
// copyright
StringBuilder copyright = metaInfo.get("copyright");
@@ -5665,7 +5673,7 @@ private void postprocessMeta() throws ParseException {
}
}
catch (InvalidMidiDataException e) {
- throw new ParseException(Dict.get(Dict.ERROR_MIDI_PROBLEM) + e.getMessage());
+ throw new FatalParseException(e);
}
}
diff --git a/src/org/midica/midi/SequenceCreator.java b/src/org/midica/midi/SequenceCreator.java
index 8ea7681..fabca04 100644
--- a/src/org/midica/midi/SequenceCreator.java
+++ b/src/org/midica/midi/SequenceCreator.java
@@ -512,32 +512,6 @@ public static void addMessageCtrl(int ctrl, int channel, int value, long tick) t
tracks[channel + NUM_META_TRACKS].add(event);
}
- /**
- * Adds a sound effect based on an RPN.
- *
- * @param rpn Number of the RPN.
- * @param channel Channel number from 0 to 15.
- * @param value Value to be set.
- * @param tick MIDI tick.
- * @throws InvalidMidiDataException
- */
- public static void addMessageRpn(int rpn, int channel, int value, long tick) throws InvalidMidiDataException {
- // TODO: implement
- }
-
- /**
- * Adds a sound effect based on an NRPN.
- *
- * @param nrpn Number of the RPN.
- * @param channel Channel number from 0 to 15.
- * @param value Value to be set.
- * @param tick MIDI tick.
- * @throws InvalidMidiDataException
- */
- public static void addMessageNrpn(int nrpn, int channel, int value, long tick) throws InvalidMidiDataException {
- // TODO: implement
- }
-
/**
* Adds a channel-dependent generic message.
* This is called by the {@link MidiParser} to add messages that are not handled by another
diff --git a/src/org/midica/ui/model/SingleMessage.java b/src/org/midica/ui/model/SingleMessage.java
index 6987163..4ae0789 100644
--- a/src/org/midica/ui/model/SingleMessage.java
+++ b/src/org/midica/ui/model/SingleMessage.java
@@ -250,6 +250,20 @@ public String toString() {
// probably a SHORT message (used in MidicaPLParserTest)
if (getOption(IMessageType.OPT_CHANNEL) != null && getOption(IMessageType.OPT_SUMMARY) != null) {
+
+ // CTRL change?
+ if (getOption(IMessageType.OPT_STATUS_BYTE).toString().startsWith("B")) {
+ String ctrlByte = Integer.toHexString((byte) getOption(IMessageType.OPT_CONTROLLER)).toUpperCase();
+ if (1 == ctrlByte.length())
+ ctrlByte = "0" + ctrlByte;
+ return (Long) getOption(IMessageType.OPT_TICK)
+ + "/" + (Integer) getOption(IMessageType.OPT_CHANNEL)
+ + "/" + (String) getOption(IMessageType.OPT_STATUS_BYTE)
+ + "-" + ctrlByte
+ + "/" + (String) getOption(IMessageType.OPT_SUMMARY);
+ }
+
+ // something else
return (Long) getOption(IMessageType.OPT_TICK)
+ "/" + (Integer) getOption(IMessageType.OPT_CHANNEL)
+ "/" + (String) getOption(IMessageType.OPT_STATUS_BYTE)
diff --git a/test/org/midica/file/read/MidicaPLParserTest.java b/test/org/midica/file/read/MidicaPLParserTest.java
index 637f579..8338f25 100644
--- a/test/org/midica/file/read/MidicaPLParserTest.java
+++ b/test/org/midica/file/read/MidicaPLParserTest.java
@@ -2814,6 +2814,298 @@ void testParseFilesFailing() {
assertEquals( 5, e.getLineNumber() );
assertEquals( "1: (tr=/16) d", e.getLineContent() );
assertTrue( e.getMessage().startsWith(Dict.get(Dict.ERROR_OTO_DUPLICATE_TREMOLO)));
+
+// if (1==1) return; // TODO: delete...
+
+ e = assertThrows( ParseException.class, () -> parse(getFailingFile("eff-unknown-flow-elem-1")) );
+ assertEquals( 4, e.getLineNumber() );
+ assertEquals( "0: volll.keep.set(50)", e.getLineContent() );
+ assertTrue( e.getMessage().startsWith(Dict.get(Dict.ERROR_UNKNOWN_NOTE)));
+
+ e = assertThrows( ParseException.class, () -> parse(getFailingFile("eff-unknown-flow-elem-2")) );
+ assertEquals( 4, e.getLineNumber() );
+ assertEquals( "0: vol.keeeeeep.set(50)", e.getLineContent() );
+ assertTrue( e.getMessage().startsWith(Dict.get(Dict.ERROR_FL_UNKNOWN_ELEMENT)));
+
+ e = assertThrows( ParseException.class, () -> parse(getFailingFile("eff-flow-broken-by-var")) );
+ assertEquals( 6, e.getLineNumber() );
+ assertEquals( "0: .set(50)", e.getLineContent() );
+ assertTrue( e.getMessage().startsWith(String.format(Dict.get(Dict.ERROR_FL_NOT_OPEN), ".")));
+
+ e = assertThrows( ParseException.class, () -> parse(getFailingFile("eff-flow-broken-by-const")) );
+ assertEquals( 6, e.getLineNumber() );
+ assertEquals( "0: .set(50)", e.getLineContent() );
+ assertTrue( e.getMessage().startsWith(String.format(Dict.get(Dict.ERROR_FL_NOT_OPEN), ".")));
+
+ e = assertThrows( ParseException.class, () -> parse(getFailingFile("eff-flow-broken-by-call")) );
+ assertEquals( 6, e.getLineNumber() );
+ assertEquals( "0: .set(50)", e.getLineContent() );
+ assertTrue( e.getMessage().startsWith(String.format(Dict.get(Dict.ERROR_FL_NOT_OPEN), ".")));
+
+ e = assertThrows( ParseException.class, () -> parse(getFailingFile("eff-flow-broken-by-function")) );
+ assertEquals( 7, e.getLineNumber() );
+ assertEquals( "0: .set(50)", e.getLineContent() );
+ assertTrue( e.getMessage().startsWith(String.format(Dict.get(Dict.ERROR_FL_NOT_OPEN), ".")));
+
+ e = assertThrows( ParseException.class, () -> parse(getFailingFile("eff-flow-broken-by-note")) );
+ assertEquals( 6, e.getLineNumber() );
+ assertEquals( "0: .set(50)", e.getLineContent() );
+ assertTrue( e.getMessage().startsWith(String.format(Dict.get(Dict.ERROR_FL_NOT_OPEN), ".")));
+
+ e = assertThrows( ParseException.class, () -> parse(getFailingFile("eff-flow-broken-by-other-channel")) );
+ assertEquals( 6, e.getLineNumber() );
+ assertEquals( "0: .set(50)", e.getLineContent() );
+ assertTrue( e.getMessage().startsWith(String.format(Dict.get(Dict.ERROR_FL_NOT_OPEN), ".")));
+
+ e = assertThrows( ParseException.class, () -> parse(getFailingFile("eff-flow-missing-dot-1")) );
+ assertEquals( 5, e.getLineNumber() );
+ assertEquals( "0: wait().set(50)", e.getLineContent() );
+ assertTrue( e.getMessage().startsWith(Dict.get(Dict.ERROR_FL_EFF_NOT_SET) + "set"));
+
+ e = assertThrows( ParseException.class, () -> parse(getFailingFile("eff-flow-missing-dot-2")) );
+ assertEquals( 4, e.getLineNumber() );
+ assertEquals( "0: vol.wait()set(50)", e.getLineContent() );
+ assertTrue( e.getMessage().startsWith(String.format(Dict.get(Dict.ERROR_FL_MISSING_DOT), ".")));
+
+ e = assertThrows( ParseException.class, () -> parse(getFailingFile("eff-flow-non-generic-with-num")) );
+ assertEquals( 4, e.getLineNumber() );
+ assertEquals( "0: vol=30.set(50)", e.getLineContent() );
+ assertTrue( e.getMessage().startsWith(Dict.get(Dict.ERROR_FL_NUMBER_NOT_ALLOWED) + "vol"));
+
+ e = assertThrows( ParseException.class, () -> parse(getFailingFile("eff-flow-rpn-without-num")) );
+ assertEquals( 4, e.getLineNumber() );
+ assertEquals( "0: rpn.set(50)", e.getLineContent() );
+ assertTrue( e.getMessage().startsWith(String.format(Dict.get(Dict.ERROR_FL_NUMBER_MISSING), "rpn")));
+
+ e = assertThrows( ParseException.class, () -> parse(getFailingFile("eff-flow-rpn-without-num")) );
+ assertEquals( 4, e.getLineNumber() );
+ assertEquals( "0: rpn.set(50)", e.getLineContent() );
+ assertTrue( e.getMessage().startsWith(String.format(Dict.get(Dict.ERROR_FL_NUMBER_MISSING), "rpn")));
+
+ e = assertThrows( ParseException.class, () -> parse(getFailingFile("eff-flow-nrpn-num-too-high")) );
+ assertEquals( 4, e.getLineNumber() );
+ assertEquals( "0: nrpn=999999999999999.set(50)", e.getLineContent() );
+ assertTrue( e.getMessage().startsWith(String.format(Dict.get(Dict.ERROR_FL_NUMBER_TOO_HIGH), "999999999999999", "nrpn", 16383)));
+
+ e = assertThrows( ParseException.class, () -> parse(getFailingFile("eff-flow-ctrl-num-too-high")) );
+ assertEquals( 4, e.getLineNumber() );
+ assertEquals( "0: ctrl=128.set(50)", e.getLineContent() );
+ assertTrue( e.getMessage().startsWith(String.format(Dict.get(Dict.ERROR_FL_NUMBER_TOO_HIGH), 128, "ctrl", 127)));
+
+ e = assertThrows( ParseException.class, () -> parse(getFailingFile("eff-flow-ctrl-with-lsb")) );
+ assertEquals( 4, e.getLineNumber() );
+ assertEquals( "0: ctrl=0/11.set(50)", e.getLineContent() );
+ assertTrue( e.getMessage().startsWith(Dict.get(Dict.ERROR_FL_NUM_SEP_NOT_ALLOWED) + "ctrl"));
+
+ e = assertThrows( ParseException.class, () -> parse(getFailingFile("eff-flow-double-with-params")) );
+ assertEquals( 4, e.getLineNumber() );
+ assertEquals( "0: vol.double().set(50)", e.getLineContent() );
+ assertTrue( e.getMessage().startsWith(String.format(Dict.get(Dict.ERROR_FL_PARAMS_NOT_ALLOWED), "double")));
+
+ e = assertThrows( ParseException.class, () -> parse(getFailingFile("eff-flow-double-for-boolean")) );
+ assertEquals( 4, e.getLineNumber() );
+ assertEquals( "0: hold.double.on()", e.getLineContent() );
+ assertTrue( e.getMessage().startsWith(String.format(Dict.get(Dict.ERROR_FL_DOUBLE_NOT_SUPPORTED), "double")));
+
+ e = assertThrows( ParseException.class, () -> parse(getFailingFile("eff-flow-double-for-single")) );
+ assertEquals( 4, e.getLineNumber() );
+ assertEquals( "0: chorus.double.set(50)", e.getLineContent() );
+ assertTrue( e.getMessage().startsWith(String.format(Dict.get(Dict.ERROR_FL_DOUBLE_NOT_SUPPORTED), "double")));
+
+ e = assertThrows( ParseException.class, () -> parse(getFailingFile("eff-func-without-params")) );
+ assertEquals( 4, e.getLineNumber() );
+ assertEquals( "0: vol.set", e.getLineContent() );
+ assertTrue( e.getMessage().startsWith(String.format(Dict.get(Dict.ERROR_FL_PARAMS_REQUIRED), "set")));
+
+ e = assertThrows( ParseException.class, () -> parse(getFailingFile("eff-func-wrong-param-count-1")) );
+ assertEquals( 4, e.getLineNumber() );
+ assertEquals( "0: vol.set(30,40)", e.getLineContent() );
+ assertTrue( e.getMessage().startsWith(String.format(Dict.get(Dict.ERROR_FL_WRONG_PARAM_NUM), "set", 1, 2, "30,40")));
+
+ e = assertThrows( ParseException.class, () -> parse(getFailingFile("eff-func-wrong-param-count-2")) );
+ assertEquals( 4, e.getLineNumber() );
+ assertEquals( "0: vol.wait(4,8)", e.getLineContent() );
+ assertTrue( e.getMessage().startsWith(String.format(Dict.get(Dict.ERROR_FL_WRONG_PARAM_NUM), "wait", 1, 2, "4,8")));
+
+ e = assertThrows( ParseException.class, () -> parse(getFailingFile("eff-func-wrong-param-count-3")) );
+ assertEquals( 4, e.getLineNumber() );
+ assertEquals( "0: vol.set()", e.getLineContent() );
+ assertTrue( e.getMessage().startsWith(String.format(Dict.get(Dict.ERROR_FL_WRONG_PARAM_NUM), "set", 1, 0, "")));
+
+ e = assertThrows( ParseException.class, () -> parse(getFailingFile("eff-flow-remainder-1")) );
+ assertEquals( 4, e.getLineNumber() );
+ assertEquals( "0: vol.wait().wait().set(50).test", e.getLineContent() );
+ assertTrue( e.getMessage().startsWith(Dict.get(Dict.ERROR_FL_UNKNOWN_ELEMENT) + "test"));
+
+ e = assertThrows( ParseException.class, () -> parse(getFailingFile("eff-flow-remainder-2")) );
+ assertEquals( 4, e.getLineNumber() );
+ assertEquals( "0: vol.wait().wait().set(50).wait-for-me", e.getLineContent() );
+ assertTrue( e.getMessage().startsWith(Dict.get(Dict.ERROR_FL_UNMATCHED_REMAINDER) + "-for-me"));
+
+ e = assertThrows( ParseException.class, () -> parse(getFailingFile("eff-func-empty-param")) );
+ assertEquals( 4, e.getLineNumber() );
+ assertEquals( "0: vol.sin(0,,100%)", e.getLineContent() );
+ assertTrue( e.getMessage().startsWith(String.format(Dict.get(Dict.ERROR_FL_EMPTY_PARAM), "0,,100%")));
+
+ e = assertThrows( ParseException.class, () -> parse(getFailingFile("eff-func-bool-with-numeric-1")) );
+ assertEquals( 4, e.getLineNumber() );
+ assertEquals( "0: vol.on()", e.getLineContent() );
+ assertTrue( e.getMessage().startsWith(String.format(Dict.get(Dict.ERROR_FUNC_TYPE_NOT_BOOL), "on")));
+
+ e = assertThrows( ParseException.class, () -> parse(getFailingFile("eff-func-bool-with-numeric-2")) );
+ assertEquals( 4, e.getLineNumber() );
+ assertEquals( "0: chorus.off()", e.getLineContent() );
+ assertTrue( e.getMessage().startsWith(String.format(Dict.get(Dict.ERROR_FUNC_TYPE_NOT_BOOL), "off")));
+
+ e = assertThrows( ParseException.class, () -> parse(getFailingFile("eff-func-numeric-for-bool")) );
+ assertEquals( 4, e.getLineNumber() );
+ assertEquals( "0: legato.set(0)", e.getLineContent() );
+ assertTrue( e.getMessage().startsWith(String.format(Dict.get(Dict.ERROR_FUNC_TYPE_BOOL), "set")));
+
+ e = assertThrows( ParseException.class, () -> parse(getFailingFile("eff-func-cont-rpn")) );
+ assertEquals( 4, e.getLineNumber() );
+ assertEquals( "0: pitch_bend_range.line(1,12)", e.getLineContent() );
+ assertTrue( e.getMessage().startsWith(String.format(Dict.get(Dict.ERROR_FL_CONT_RPN), "line")));
+
+ e = assertThrows( ParseException.class, () -> parse(getFailingFile("eff-func-cont-nrpn")) );
+ assertEquals( 4, e.getLineNumber() );
+ assertEquals( "0: nrpn=123.line(1,12)", e.getLineContent() );
+ assertTrue( e.getMessage().startsWith(String.format(Dict.get(Dict.ERROR_FL_CONT_NRPN), "line")));
+
+ e = assertThrows( ParseException.class, () -> parse(getFailingFile("eff-func-param-invalid-1")) );
+ assertEquals( 4, e.getLineNumber() );
+ assertEquals( "0: vol.line(1,9999999999999)", e.getLineContent() );
+ assertTrue( e.getMessage().startsWith(Dict.get(Dict.ERROR_FUNC_NO_NUMBER) + "9999999999999"));
+
+ e = assertThrows( ParseException.class, () -> parse(getFailingFile("eff-func-param-invalid-2")) );
+ assertEquals( 4, e.getLineNumber() );
+ assertEquals( "0: vol.line(1,0x7F)", e.getLineContent() );
+ assertTrue( e.getMessage().startsWith(Dict.get(Dict.ERROR_FUNC_NO_NUMBER) + "0x7F"));
+
+ e = assertThrows( ParseException.class, () -> parse(getFailingFile("eff-func-param-too-low-1")) );
+ assertEquals( 4, e.getLineNumber() );
+ assertEquals( "0: balance.line(63,-65)", e.getLineContent() );
+ assertTrue( e.getMessage().startsWith(String.format(Dict.get(Dict.ERROR_FUNC_VAL_LOWER_MIN), -65, -64)));
+
+ e = assertThrows( ParseException.class, () -> parse(getFailingFile("eff-func-param-too-low-2")) );
+ assertEquals( 4, e.getLineNumber() );
+ assertEquals( "0: vol.line(1,-0.000001%)", e.getLineContent() );
+ assertTrue( e.getMessage().startsWith(String.format(Dict.get(Dict.ERROR_FUNC_VAL_LOWER_MIN), "-0.000001%", 0)));
+
+ e = assertThrows( ParseException.class, () -> parse(getFailingFile("eff-func-param-too-high-1")) );
+ assertEquals( 4, e.getLineNumber() );
+ assertEquals( "0: vol.line(1,128)", e.getLineContent() );
+ assertTrue( e.getMessage().startsWith(String.format(Dict.get(Dict.ERROR_FUNC_VAL_GREATER_MAX), 128, 127)));
+
+ e = assertThrows( ParseException.class, () -> parse(getFailingFile("eff-func-param-too-high-2")) );
+ assertEquals( 4, e.getLineNumber() );
+ assertEquals( "0: balance.double.line(1,8192)", e.getLineContent() );
+ assertTrue( e.getMessage().startsWith(String.format(Dict.get(Dict.ERROR_FUNC_VAL_GREATER_MAX), 8192, 8191)));
+
+ e = assertThrows( ParseException.class, () -> parse(getFailingFile("eff-func-param-periods-nan-1")) );
+ assertEquals( 4, e.getLineNumber() );
+ assertEquals( "0: vol.sin(0,100%,1.2.3)", e.getLineContent() );
+ assertTrue( e.getMessage().startsWith(Dict.get(Dict.ERROR_FUNC_PERIODS_NO_NUMBER) + "1.2.3"));
+
+ e = assertThrows( ParseException.class, () -> parse(getFailingFile("eff-func-param-periods-nan-2")) );
+ assertEquals( 4, e.getLineNumber() );
+ assertEquals( "0: vol.sin(0,100%,999999999999999999999999999999999999999.0)", e.getLineContent() );
+ assertTrue( e.getMessage().startsWith(Dict.get(Dict.ERROR_FUNC_PERIODS_NO_NUMBER) + "999999999999999999999999999999999999999.0"));
+
+ e = assertThrows( ParseException.class, () -> parse(getFailingFile("eff-func-param-periods-neg")) );
+ assertEquals( 4, e.getLineNumber() );
+ assertEquals( "0: vol.sin(0,100%,-1.0)", e.getLineContent() );
+ assertTrue( e.getMessage().startsWith(Dict.get(Dict.ERROR_FUNC_PERIODS_NEG) + "-1.0"));
+
+ e = assertThrows( ParseException.class, () -> parse(getFailingFile("eff-flow-eff-not-set-1")) );
+ assertEquals( 4, e.getLineNumber() );
+ assertEquals( "0: wait().set(50)", e.getLineContent() );
+ assertTrue( e.getMessage().startsWith(Dict.get(Dict.ERROR_FL_EFF_NOT_SET) + "set"));
+
+ e = assertThrows( ParseException.class, () -> parse(getFailingFile("eff-flow-eff-not-set-2")) );
+ assertEquals( 4, e.getLineNumber() );
+ assertEquals( "0: wait().double.vol.set(50)", e.getLineContent() );
+ assertTrue( e.getMessage().startsWith(Dict.get(Dict.ERROR_FL_EFF_NOT_SET) + "double"));
+
+ e = assertThrows( ParseException.class, () -> parse(getFailingFile("eff-flow-eff-already-set")) );
+ assertEquals( 4, e.getLineNumber() );
+ assertEquals( "0: vol.wait().vol.set(50)", e.getLineContent() );
+ assertTrue( e.getMessage().startsWith(Dict.get(Dict.ERROR_FL_EFF_ALREADY_SET)));
+
+ e = assertThrows( ParseException.class, () -> parse(getFailingFile("eff-func-halftone-for-vol")) );
+ assertEquals( 4, e.getLineNumber() );
+ assertEquals( "0: vol.set(12.0)", e.getLineContent() );
+ assertTrue( e.getMessage().startsWith(String.format(Dict.get(Dict.ERROR_FUNC_HALFTONE_NOT_ALLOWED), "12.0")));
+
+ e = assertThrows( ParseException.class, () -> parse(getFailingFile("eff-func-pbr-halftone-gt-max-1")) );
+ assertEquals( 4, e.getLineNumber() );
+ assertEquals( "0: pitch_bend_range.set(129.0)", e.getLineContent() );
+ assertTrue( e.getMessage().startsWith(String.format(Dict.get(Dict.ERROR_FUNC_VAL_GREATER_MAX), 129.0f, 127f)));
+
+ e = assertThrows( ParseException.class, () -> parse(getFailingFile("eff-func-pbr-halftone-gt-max-2")) );
+ assertEquals( 4, e.getLineNumber() );
+ assertEquals( "0: pitch_bend_range.double.set(127.997)", e.getLineContent() );
+ assertTrue( e.getMessage().startsWith(String.format(Dict.get(Dict.ERROR_FUNC_VAL_GREATER_MAX), 127.997f, 127.99f)));
+
+ e = assertThrows( ParseException.class, () -> parse(getFailingFile("eff-func-pbr-halftone-gt-max-3")) );
+ assertEquals( 4, e.getLineNumber() );
+ assertEquals( "0: pitch_bend_range.set(127.0001)", e.getLineContent() );
+ assertTrue( e.getMessage().startsWith(String.format(Dict.get(Dict.ERROR_FUNC_VAL_GREATER_MAX), 127.0001f, 127f)));
+
+ e = assertThrows( ParseException.class, () -> parse(getFailingFile("eff-func-pbr-halftone-gt-max-4")) );
+ assertEquals( 4, e.getLineNumber() );
+ assertEquals( "0: pitch_bend_range.double.set(127.997)", e.getLineContent() );
+ assertTrue( e.getMessage().startsWith(String.format(Dict.get(Dict.ERROR_FUNC_VAL_GREATER_MAX), 127.997, 127.99f)));
+
+ e = assertThrows( ParseException.class, () -> parse(getFailingFile("eff-func-pbr-with-percent")) );
+ assertEquals( 4, e.getLineNumber() );
+ assertEquals( "0: pitch_bend_range.set(12.0%)", e.getLineContent() );
+ assertTrue( e.getMessage().startsWith(Dict.get(Dict.ERROR_FUNC_NEED_HALFTONE) + "12.0%"));
+
+ e = assertThrows( ParseException.class, () -> parse(getFailingFile("eff-func-bend-gt-range")) );
+ assertEquals( 4, e.getLineNumber() );
+ assertEquals( "0: bend.wait.set(2.3)", e.getLineContent() );
+ assertTrue( e.getMessage().startsWith(String.format(Dict.get(Dict.ERROR_FUNC_HALFTONE_GT_RANGE), 2.3f, 2.0f)));
+
+ e = assertThrows( ParseException.class, () -> parse(getFailingFile("eff-flow-note-invalid")) );
+ assertEquals( 4, e.getLineNumber() );
+ assertEquals( "0: mono_at.note(c+6).set(123)", e.getLineContent() );
+ assertTrue( e.getMessage().startsWith(Dict.get(Dict.ERROR_UNKNOWN_NOTE) + "c+6"));
+
+ e = assertThrows( ParseException.class, () -> parse(getFailingFile("eff-flow-note-without-effect")) );
+ assertEquals( 4, e.getLineNumber() );
+ assertEquals( "0: note(c).vol.set(100%)", e.getLineContent() );
+ assertTrue( e.getMessage().startsWith(Dict.get(Dict.ERROR_FL_EFF_NOT_SET) + "note"));
+
+ e = assertThrows( ParseException.class, () -> parse(getFailingFile("eff-flow-note-not-allowed")) );
+ assertEquals( 4, e.getLineNumber() );
+ assertEquals( "0: vol.note(c).set(100%)", e.getLineContent() );
+ assertTrue( e.getMessage().startsWith(Dict.get(Dict.ERROR_FL_NOTE_NOT_SUPP) + "note"));
+
+ e = assertThrows( ParseException.class, () -> parse(getFailingFile("eff-flow-numeric-for-none")) );
+ assertEquals( 4, e.getLineNumber() );
+ assertEquals( "0: ctrl=123.wait.set(12)", e.getLineContent() );
+ assertTrue( e.getMessage().startsWith(String.format(Dict.get(Dict.ERROR_FUNC_TYPE_NONE), "set")));
+
+ e = assertThrows( ParseException.class, () -> parse(getFailingFile("eff-flow-off-for-none")) );
+ assertEquals( 4, e.getLineNumber() );
+ assertEquals( "0: ctrl=123.wait.off()", e.getLineContent() );
+ assertTrue( e.getMessage().startsWith(String.format(Dict.get(Dict.ERROR_FUNC_TYPE_NOT_BOOL), "off")));
+
+ e = assertThrows( ParseException.class, () -> parse(getFailingFile("eff-func-msblsb-without-double")) );
+ assertEquals( 4, e.getLineNumber() );
+ assertEquals( "0: vol.set(12/30)", e.getLineContent() );
+ assertTrue( e.getMessage().startsWith(String.format(Dict.get(Dict.ERROR_FUNC_MSB_LSB_NEEDS_DOUBLE), "12/30", "double")));
+
+ e = assertThrows( ParseException.class, () -> parse(getFailingFile("eff-func-msb-too-high")) );
+ assertEquals( 4, e.getLineNumber() );
+ assertEquals( "0: vol.double.set(128/30)", e.getLineContent() );
+ assertTrue( e.getMessage().startsWith(String.format(Dict.get(Dict.ERROR_FUNC_MSB_TOO_HIGH), "128/30", "128")));
+
+ e = assertThrows( ParseException.class, () -> parse(getFailingFile("eff-func-lsb-too-high")) );
+ assertEquals( 4, e.getLineNumber() );
+ assertEquals( "0: vol.double.set(30/128)", e.getLineContent() );
+ assertTrue( e.getMessage().startsWith(String.format(Dict.get(Dict.ERROR_FUNC_LSB_TOO_HIGH), "30/128", "128")));
}
/**
diff --git a/test/org/midica/testfiles/failing/eff-flow-broken-by-call.midica b/test/org/midica/testfiles/failing/eff-flow-broken-by-call.midica
new file mode 100644
index 0000000..b072989
--- /dev/null
+++ b/test/org/midica/testfiles/failing/eff-flow-broken-by-call.midica
@@ -0,0 +1,9 @@
+INCLUDE inc/instruments.midica
+
+
+0: vol.wait()
+CALL func
+0: .set(50)
+
+FUNCTION func
+END
\ No newline at end of file
diff --git a/test/org/midica/testfiles/failing/eff-flow-broken-by-const.midica b/test/org/midica/testfiles/failing/eff-flow-broken-by-const.midica
new file mode 100644
index 0000000..8454964
--- /dev/null
+++ b/test/org/midica/testfiles/failing/eff-flow-broken-by-const.midica
@@ -0,0 +1,7 @@
+INCLUDE inc/instruments.midica
+
+
+0: vol.wait()
+CONST $x = y
+0: .set(50)
+
diff --git a/test/org/midica/testfiles/failing/eff-flow-broken-by-function.midica b/test/org/midica/testfiles/failing/eff-flow-broken-by-function.midica
new file mode 100644
index 0000000..189b692
--- /dev/null
+++ b/test/org/midica/testfiles/failing/eff-flow-broken-by-function.midica
@@ -0,0 +1,9 @@
+INCLUDE inc/instruments.midica
+
+
+0: vol.wait()
+FUNCTION func
+END
+0: .set(50)
+
+CALL func
diff --git a/test/org/midica/testfiles/failing/eff-flow-broken-by-note.midica b/test/org/midica/testfiles/failing/eff-flow-broken-by-note.midica
new file mode 100644
index 0000000..98b53db
--- /dev/null
+++ b/test/org/midica/testfiles/failing/eff-flow-broken-by-note.midica
@@ -0,0 +1,7 @@
+INCLUDE inc/instruments.midica
+
+
+0: vol.wait()
+0: c
+0: .set(50)
+
diff --git a/test/org/midica/testfiles/failing/eff-flow-broken-by-other-channel.midica b/test/org/midica/testfiles/failing/eff-flow-broken-by-other-channel.midica
new file mode 100644
index 0000000..9fab4ae
--- /dev/null
+++ b/test/org/midica/testfiles/failing/eff-flow-broken-by-other-channel.midica
@@ -0,0 +1,7 @@
+INCLUDE inc/instruments.midica
+
+
+0: vol.wait()
+1: balance.wait()
+0: .set(50)
+
diff --git a/test/org/midica/testfiles/failing/eff-flow-broken-by-var.midica b/test/org/midica/testfiles/failing/eff-flow-broken-by-var.midica
new file mode 100644
index 0000000..b65d12a
--- /dev/null
+++ b/test/org/midica/testfiles/failing/eff-flow-broken-by-var.midica
@@ -0,0 +1,7 @@
+INCLUDE inc/instruments.midica
+
+
+0: vol.double
+VAR $x = y
+0: .set(50)
+
diff --git a/test/org/midica/testfiles/failing/eff-flow-ctrl-num-too-high.midica b/test/org/midica/testfiles/failing/eff-flow-ctrl-num-too-high.midica
new file mode 100644
index 0000000..803bb9d
--- /dev/null
+++ b/test/org/midica/testfiles/failing/eff-flow-ctrl-num-too-high.midica
@@ -0,0 +1,4 @@
+INCLUDE inc/instruments.midica
+
+
+0: ctrl=128.set(50)
diff --git a/test/org/midica/testfiles/failing/eff-flow-ctrl-with-lsb.midica b/test/org/midica/testfiles/failing/eff-flow-ctrl-with-lsb.midica
new file mode 100644
index 0000000..0cf7dbb
--- /dev/null
+++ b/test/org/midica/testfiles/failing/eff-flow-ctrl-with-lsb.midica
@@ -0,0 +1,4 @@
+INCLUDE inc/instruments.midica
+
+
+0: ctrl=0/11.set(50)
diff --git a/test/org/midica/testfiles/failing/eff-flow-double-for-boolean.midica b/test/org/midica/testfiles/failing/eff-flow-double-for-boolean.midica
new file mode 100644
index 0000000..9342ff9
--- /dev/null
+++ b/test/org/midica/testfiles/failing/eff-flow-double-for-boolean.midica
@@ -0,0 +1,4 @@
+INCLUDE inc/instruments.midica
+
+
+0: hold.double.on()
diff --git a/test/org/midica/testfiles/failing/eff-flow-double-for-single.midica b/test/org/midica/testfiles/failing/eff-flow-double-for-single.midica
new file mode 100644
index 0000000..7da4eb9
--- /dev/null
+++ b/test/org/midica/testfiles/failing/eff-flow-double-for-single.midica
@@ -0,0 +1,4 @@
+INCLUDE inc/instruments.midica
+
+
+0: chorus.double.set(50)
diff --git a/test/org/midica/testfiles/failing/eff-flow-double-with-params.midica b/test/org/midica/testfiles/failing/eff-flow-double-with-params.midica
new file mode 100644
index 0000000..9fd9e87
--- /dev/null
+++ b/test/org/midica/testfiles/failing/eff-flow-double-with-params.midica
@@ -0,0 +1,4 @@
+INCLUDE inc/instruments.midica
+
+
+0: vol.double().set(50)
diff --git a/test/org/midica/testfiles/failing/eff-flow-eff-already-set.midica b/test/org/midica/testfiles/failing/eff-flow-eff-already-set.midica
new file mode 100644
index 0000000..0548dfd
--- /dev/null
+++ b/test/org/midica/testfiles/failing/eff-flow-eff-already-set.midica
@@ -0,0 +1,5 @@
+INCLUDE inc/instruments.midica
+
+
+0: vol.wait().vol.set(50)
+
diff --git a/test/org/midica/testfiles/failing/eff-flow-eff-not-set-1.midica b/test/org/midica/testfiles/failing/eff-flow-eff-not-set-1.midica
new file mode 100644
index 0000000..f09cefc
--- /dev/null
+++ b/test/org/midica/testfiles/failing/eff-flow-eff-not-set-1.midica
@@ -0,0 +1,5 @@
+INCLUDE inc/instruments.midica
+
+
+0: wait().set(50)
+
diff --git a/test/org/midica/testfiles/failing/eff-flow-eff-not-set-2.midica b/test/org/midica/testfiles/failing/eff-flow-eff-not-set-2.midica
new file mode 100644
index 0000000..d781176
--- /dev/null
+++ b/test/org/midica/testfiles/failing/eff-flow-eff-not-set-2.midica
@@ -0,0 +1,5 @@
+INCLUDE inc/instruments.midica
+
+
+0: wait().double.vol.set(50)
+
diff --git a/test/org/midica/testfiles/failing/eff-flow-missing-dot-1.midica b/test/org/midica/testfiles/failing/eff-flow-missing-dot-1.midica
new file mode 100644
index 0000000..a1be711
--- /dev/null
+++ b/test/org/midica/testfiles/failing/eff-flow-missing-dot-1.midica
@@ -0,0 +1,6 @@
+INCLUDE inc/instruments.midica
+
+
+0: vol.wait()
+0: wait().set(50)
+
diff --git a/test/org/midica/testfiles/failing/eff-flow-missing-dot-2.midica b/test/org/midica/testfiles/failing/eff-flow-missing-dot-2.midica
new file mode 100644
index 0000000..03a6df9
--- /dev/null
+++ b/test/org/midica/testfiles/failing/eff-flow-missing-dot-2.midica
@@ -0,0 +1,5 @@
+INCLUDE inc/instruments.midica
+
+
+0: vol.wait()set(50)
+
diff --git a/test/org/midica/testfiles/failing/eff-flow-non-generic-with-num.midica b/test/org/midica/testfiles/failing/eff-flow-non-generic-with-num.midica
new file mode 100644
index 0000000..df2f045
--- /dev/null
+++ b/test/org/midica/testfiles/failing/eff-flow-non-generic-with-num.midica
@@ -0,0 +1,4 @@
+INCLUDE inc/instruments.midica
+
+
+0: vol=30.set(50)
diff --git a/test/org/midica/testfiles/failing/eff-flow-note-invalid.midica b/test/org/midica/testfiles/failing/eff-flow-note-invalid.midica
new file mode 100644
index 0000000..a09fc4e
--- /dev/null
+++ b/test/org/midica/testfiles/failing/eff-flow-note-invalid.midica
@@ -0,0 +1,4 @@
+INCLUDE inc/instruments.midica
+
+
+0: mono_at.note(c+6).set(123)
diff --git a/test/org/midica/testfiles/failing/eff-flow-note-not-allowed.midica b/test/org/midica/testfiles/failing/eff-flow-note-not-allowed.midica
new file mode 100644
index 0000000..f35d529
--- /dev/null
+++ b/test/org/midica/testfiles/failing/eff-flow-note-not-allowed.midica
@@ -0,0 +1,4 @@
+INCLUDE inc/instruments.midica
+
+
+0: vol.note(c).set(100%)
diff --git a/test/org/midica/testfiles/failing/eff-flow-note-without-effect.midica b/test/org/midica/testfiles/failing/eff-flow-note-without-effect.midica
new file mode 100644
index 0000000..73372c0
--- /dev/null
+++ b/test/org/midica/testfiles/failing/eff-flow-note-without-effect.midica
@@ -0,0 +1,4 @@
+INCLUDE inc/instruments.midica
+
+
+0: note(c).vol.set(100%)
diff --git a/test/org/midica/testfiles/failing/eff-flow-nrpn-num-too-high.midica b/test/org/midica/testfiles/failing/eff-flow-nrpn-num-too-high.midica
new file mode 100644
index 0000000..1a6757c
--- /dev/null
+++ b/test/org/midica/testfiles/failing/eff-flow-nrpn-num-too-high.midica
@@ -0,0 +1,4 @@
+INCLUDE inc/instruments.midica
+
+
+0: nrpn=999999999999999.set(50)
diff --git a/test/org/midica/testfiles/failing/eff-flow-numeric-for-none.midica b/test/org/midica/testfiles/failing/eff-flow-numeric-for-none.midica
new file mode 100644
index 0000000..38bffd1
--- /dev/null
+++ b/test/org/midica/testfiles/failing/eff-flow-numeric-for-none.midica
@@ -0,0 +1,4 @@
+INCLUDE inc/instruments.midica
+
+
+0: ctrl=123.wait.set(12)
diff --git a/test/org/midica/testfiles/failing/eff-flow-off-for-none.midica b/test/org/midica/testfiles/failing/eff-flow-off-for-none.midica
new file mode 100644
index 0000000..5c50ae6
--- /dev/null
+++ b/test/org/midica/testfiles/failing/eff-flow-off-for-none.midica
@@ -0,0 +1,4 @@
+INCLUDE inc/instruments.midica
+
+
+0: ctrl=123.wait.off()
diff --git a/test/org/midica/testfiles/failing/eff-flow-remainder-1.midica b/test/org/midica/testfiles/failing/eff-flow-remainder-1.midica
new file mode 100644
index 0000000..7d03447
--- /dev/null
+++ b/test/org/midica/testfiles/failing/eff-flow-remainder-1.midica
@@ -0,0 +1,5 @@
+INCLUDE inc/instruments.midica
+
+
+0: vol.wait().wait().set(50).test
+
diff --git a/test/org/midica/testfiles/failing/eff-flow-remainder-2.midica b/test/org/midica/testfiles/failing/eff-flow-remainder-2.midica
new file mode 100644
index 0000000..f07c8a8
--- /dev/null
+++ b/test/org/midica/testfiles/failing/eff-flow-remainder-2.midica
@@ -0,0 +1,5 @@
+INCLUDE inc/instruments.midica
+
+
+0: vol.wait().wait().set(50).wait-for-me
+
diff --git a/test/org/midica/testfiles/failing/eff-flow-rpn-num-too-high.midica b/test/org/midica/testfiles/failing/eff-flow-rpn-num-too-high.midica
new file mode 100644
index 0000000..1db3017
--- /dev/null
+++ b/test/org/midica/testfiles/failing/eff-flow-rpn-num-too-high.midica
@@ -0,0 +1,4 @@
+INCLUDE inc/instruments.midica
+
+
+0: rpn=16384.set(50)
diff --git a/test/org/midica/testfiles/failing/eff-flow-rpn-without-num.midica b/test/org/midica/testfiles/failing/eff-flow-rpn-without-num.midica
new file mode 100644
index 0000000..b2dd27f
--- /dev/null
+++ b/test/org/midica/testfiles/failing/eff-flow-rpn-without-num.midica
@@ -0,0 +1,4 @@
+INCLUDE inc/instruments.midica
+
+
+0: rpn.set(50)
diff --git a/test/org/midica/testfiles/failing/eff-func-bend-gt-range.midica b/test/org/midica/testfiles/failing/eff-func-bend-gt-range.midica
new file mode 100644
index 0000000..78a6efd
--- /dev/null
+++ b/test/org/midica/testfiles/failing/eff-func-bend-gt-range.midica
@@ -0,0 +1,4 @@
+INCLUDE inc/instruments.midica
+
+0: pitch_bend_range.set(2.0)
+0: bend.wait.set(2.3)
diff --git a/test/org/midica/testfiles/failing/eff-func-bool-with-numeric-1.midica b/test/org/midica/testfiles/failing/eff-func-bool-with-numeric-1.midica
new file mode 100644
index 0000000..d10e635
--- /dev/null
+++ b/test/org/midica/testfiles/failing/eff-func-bool-with-numeric-1.midica
@@ -0,0 +1,4 @@
+INCLUDE inc/instruments.midica
+
+
+0: vol.on()
diff --git a/test/org/midica/testfiles/failing/eff-func-bool-with-numeric-2.midica b/test/org/midica/testfiles/failing/eff-func-bool-with-numeric-2.midica
new file mode 100644
index 0000000..f18fa73
--- /dev/null
+++ b/test/org/midica/testfiles/failing/eff-func-bool-with-numeric-2.midica
@@ -0,0 +1,4 @@
+INCLUDE inc/instruments.midica
+
+
+0: chorus.off()
diff --git a/test/org/midica/testfiles/failing/eff-func-cont-nrpn.midica b/test/org/midica/testfiles/failing/eff-func-cont-nrpn.midica
new file mode 100644
index 0000000..433b8a4
--- /dev/null
+++ b/test/org/midica/testfiles/failing/eff-func-cont-nrpn.midica
@@ -0,0 +1,4 @@
+INCLUDE inc/instruments.midica
+
+
+0: nrpn=123.line(1,12)
diff --git a/test/org/midica/testfiles/failing/eff-func-cont-rpn.midica b/test/org/midica/testfiles/failing/eff-func-cont-rpn.midica
new file mode 100644
index 0000000..f3f0a64
--- /dev/null
+++ b/test/org/midica/testfiles/failing/eff-func-cont-rpn.midica
@@ -0,0 +1,4 @@
+INCLUDE inc/instruments.midica
+
+
+0: pitch_bend_range.line(1,12)
diff --git a/test/org/midica/testfiles/failing/eff-func-empty-param.midica b/test/org/midica/testfiles/failing/eff-func-empty-param.midica
new file mode 100644
index 0000000..01dcc14
--- /dev/null
+++ b/test/org/midica/testfiles/failing/eff-func-empty-param.midica
@@ -0,0 +1,4 @@
+INCLUDE inc/instruments.midica
+
+
+0: vol.sin(0,,100%)
diff --git a/test/org/midica/testfiles/failing/eff-func-halftone-for-vol.midica b/test/org/midica/testfiles/failing/eff-func-halftone-for-vol.midica
new file mode 100644
index 0000000..45d621c
--- /dev/null
+++ b/test/org/midica/testfiles/failing/eff-func-halftone-for-vol.midica
@@ -0,0 +1,4 @@
+INCLUDE inc/instruments.midica
+
+
+0: vol.set(12.0)
diff --git a/test/org/midica/testfiles/failing/eff-func-lsb-too-high.midica b/test/org/midica/testfiles/failing/eff-func-lsb-too-high.midica
new file mode 100644
index 0000000..8b6fc31
--- /dev/null
+++ b/test/org/midica/testfiles/failing/eff-func-lsb-too-high.midica
@@ -0,0 +1,4 @@
+INCLUDE inc/instruments.midica
+
+
+0: vol.double.set(30/128)
diff --git a/test/org/midica/testfiles/failing/eff-func-msb-too-high.midica b/test/org/midica/testfiles/failing/eff-func-msb-too-high.midica
new file mode 100644
index 0000000..9d0bd12
--- /dev/null
+++ b/test/org/midica/testfiles/failing/eff-func-msb-too-high.midica
@@ -0,0 +1,4 @@
+INCLUDE inc/instruments.midica
+
+
+0: vol.double.set(128/30)
diff --git a/test/org/midica/testfiles/failing/eff-func-msblsb-without-double.midica b/test/org/midica/testfiles/failing/eff-func-msblsb-without-double.midica
new file mode 100644
index 0000000..d3b28e1
--- /dev/null
+++ b/test/org/midica/testfiles/failing/eff-func-msblsb-without-double.midica
@@ -0,0 +1,4 @@
+INCLUDE inc/instruments.midica
+
+
+0: vol.set(12/30)
diff --git a/test/org/midica/testfiles/failing/eff-func-numeric-for-bool.midica b/test/org/midica/testfiles/failing/eff-func-numeric-for-bool.midica
new file mode 100644
index 0000000..986c266
--- /dev/null
+++ b/test/org/midica/testfiles/failing/eff-func-numeric-for-bool.midica
@@ -0,0 +1,4 @@
+INCLUDE inc/instruments.midica
+
+
+0: legato.set(0)
diff --git a/test/org/midica/testfiles/failing/eff-func-param-invalid-1.midica b/test/org/midica/testfiles/failing/eff-func-param-invalid-1.midica
new file mode 100644
index 0000000..472c656
--- /dev/null
+++ b/test/org/midica/testfiles/failing/eff-func-param-invalid-1.midica
@@ -0,0 +1,4 @@
+INCLUDE inc/instruments.midica
+
+
+0: vol.line(1,9999999999999)
diff --git a/test/org/midica/testfiles/failing/eff-func-param-invalid-2.midica b/test/org/midica/testfiles/failing/eff-func-param-invalid-2.midica
new file mode 100644
index 0000000..82f29d1
--- /dev/null
+++ b/test/org/midica/testfiles/failing/eff-func-param-invalid-2.midica
@@ -0,0 +1,4 @@
+INCLUDE inc/instruments.midica
+
+
+0: vol.line(1,0x7F)
diff --git a/test/org/midica/testfiles/failing/eff-func-param-periods-nan-1.midica b/test/org/midica/testfiles/failing/eff-func-param-periods-nan-1.midica
new file mode 100644
index 0000000..e9d4b11
--- /dev/null
+++ b/test/org/midica/testfiles/failing/eff-func-param-periods-nan-1.midica
@@ -0,0 +1,4 @@
+INCLUDE inc/instruments.midica
+
+
+0: vol.sin(0,100%,1.2.3)
diff --git a/test/org/midica/testfiles/failing/eff-func-param-periods-nan-2.midica b/test/org/midica/testfiles/failing/eff-func-param-periods-nan-2.midica
new file mode 100644
index 0000000..b805237
--- /dev/null
+++ b/test/org/midica/testfiles/failing/eff-func-param-periods-nan-2.midica
@@ -0,0 +1,4 @@
+INCLUDE inc/instruments.midica
+
+
+0: vol.sin(0,100%,999999999999999999999999999999999999999.0)
diff --git a/test/org/midica/testfiles/failing/eff-func-param-periods-neg.midica b/test/org/midica/testfiles/failing/eff-func-param-periods-neg.midica
new file mode 100644
index 0000000..52e5c9b
--- /dev/null
+++ b/test/org/midica/testfiles/failing/eff-func-param-periods-neg.midica
@@ -0,0 +1,4 @@
+INCLUDE inc/instruments.midica
+
+
+0: vol.sin(0,100%,-1.0)
diff --git a/test/org/midica/testfiles/failing/eff-func-param-too-high-1.midica b/test/org/midica/testfiles/failing/eff-func-param-too-high-1.midica
new file mode 100644
index 0000000..998ae92
--- /dev/null
+++ b/test/org/midica/testfiles/failing/eff-func-param-too-high-1.midica
@@ -0,0 +1,4 @@
+INCLUDE inc/instruments.midica
+
+
+0: vol.line(1,128)
diff --git a/test/org/midica/testfiles/failing/eff-func-param-too-high-2.midica b/test/org/midica/testfiles/failing/eff-func-param-too-high-2.midica
new file mode 100644
index 0000000..16d137b
--- /dev/null
+++ b/test/org/midica/testfiles/failing/eff-func-param-too-high-2.midica
@@ -0,0 +1,4 @@
+INCLUDE inc/instruments.midica
+
+
+0: balance.double.line(1,8192)
diff --git a/test/org/midica/testfiles/failing/eff-func-param-too-low-1.midica b/test/org/midica/testfiles/failing/eff-func-param-too-low-1.midica
new file mode 100644
index 0000000..3f2fd15
--- /dev/null
+++ b/test/org/midica/testfiles/failing/eff-func-param-too-low-1.midica
@@ -0,0 +1,4 @@
+INCLUDE inc/instruments.midica
+
+
+0: balance.line(63,-65)
diff --git a/test/org/midica/testfiles/failing/eff-func-param-too-low-2.midica b/test/org/midica/testfiles/failing/eff-func-param-too-low-2.midica
new file mode 100644
index 0000000..b19da55
--- /dev/null
+++ b/test/org/midica/testfiles/failing/eff-func-param-too-low-2.midica
@@ -0,0 +1,4 @@
+INCLUDE inc/instruments.midica
+
+
+0: vol.line(1,-0.000001%)
diff --git a/test/org/midica/testfiles/failing/eff-func-pbr-halftone-gt-max-1.midica b/test/org/midica/testfiles/failing/eff-func-pbr-halftone-gt-max-1.midica
new file mode 100644
index 0000000..fa726cf
--- /dev/null
+++ b/test/org/midica/testfiles/failing/eff-func-pbr-halftone-gt-max-1.midica
@@ -0,0 +1,4 @@
+INCLUDE inc/instruments.midica
+
+
+0: pitch_bend_range.set(129.0)
diff --git a/test/org/midica/testfiles/failing/eff-func-pbr-halftone-gt-max-2.midica b/test/org/midica/testfiles/failing/eff-func-pbr-halftone-gt-max-2.midica
new file mode 100644
index 0000000..83a51a2
--- /dev/null
+++ b/test/org/midica/testfiles/failing/eff-func-pbr-halftone-gt-max-2.midica
@@ -0,0 +1,4 @@
+INCLUDE inc/instruments.midica
+
+
+0: pitch_bend_range.double.set(127.997)
diff --git a/test/org/midica/testfiles/failing/eff-func-pbr-halftone-gt-max-3.midica b/test/org/midica/testfiles/failing/eff-func-pbr-halftone-gt-max-3.midica
new file mode 100644
index 0000000..4b0e692
--- /dev/null
+++ b/test/org/midica/testfiles/failing/eff-func-pbr-halftone-gt-max-3.midica
@@ -0,0 +1,4 @@
+INCLUDE inc/instruments.midica
+
+
+0: pitch_bend_range.set(127.0001)
diff --git a/test/org/midica/testfiles/failing/eff-func-pbr-halftone-gt-max-4.midica b/test/org/midica/testfiles/failing/eff-func-pbr-halftone-gt-max-4.midica
new file mode 100644
index 0000000..83a51a2
--- /dev/null
+++ b/test/org/midica/testfiles/failing/eff-func-pbr-halftone-gt-max-4.midica
@@ -0,0 +1,4 @@
+INCLUDE inc/instruments.midica
+
+
+0: pitch_bend_range.double.set(127.997)
diff --git a/test/org/midica/testfiles/failing/eff-func-pbr-with-percent.midica b/test/org/midica/testfiles/failing/eff-func-pbr-with-percent.midica
new file mode 100644
index 0000000..a4980ab
--- /dev/null
+++ b/test/org/midica/testfiles/failing/eff-func-pbr-with-percent.midica
@@ -0,0 +1,4 @@
+INCLUDE inc/instruments.midica
+
+
+0: pitch_bend_range.set(12.0%)
diff --git a/test/org/midica/testfiles/failing/eff-func-without-params.midica b/test/org/midica/testfiles/failing/eff-func-without-params.midica
new file mode 100644
index 0000000..7715a79
--- /dev/null
+++ b/test/org/midica/testfiles/failing/eff-func-without-params.midica
@@ -0,0 +1,4 @@
+INCLUDE inc/instruments.midica
+
+
+0: vol.set
diff --git a/test/org/midica/testfiles/failing/eff-func-wrong-param-count-1.midica b/test/org/midica/testfiles/failing/eff-func-wrong-param-count-1.midica
new file mode 100644
index 0000000..7f1bfc1
--- /dev/null
+++ b/test/org/midica/testfiles/failing/eff-func-wrong-param-count-1.midica
@@ -0,0 +1,4 @@
+INCLUDE inc/instruments.midica
+
+
+0: vol.set(30,40)
diff --git a/test/org/midica/testfiles/failing/eff-func-wrong-param-count-2.midica b/test/org/midica/testfiles/failing/eff-func-wrong-param-count-2.midica
new file mode 100644
index 0000000..23c1af1
--- /dev/null
+++ b/test/org/midica/testfiles/failing/eff-func-wrong-param-count-2.midica
@@ -0,0 +1,4 @@
+INCLUDE inc/instruments.midica
+
+
+0: vol.wait(4,8)
diff --git a/test/org/midica/testfiles/failing/eff-func-wrong-param-count-3.midica b/test/org/midica/testfiles/failing/eff-func-wrong-param-count-3.midica
new file mode 100644
index 0000000..6f9ac0e
--- /dev/null
+++ b/test/org/midica/testfiles/failing/eff-func-wrong-param-count-3.midica
@@ -0,0 +1,4 @@
+INCLUDE inc/instruments.midica
+
+
+0: vol.set()
diff --git a/test/org/midica/testfiles/failing/eff-unknown-flow-elem-1.midica b/test/org/midica/testfiles/failing/eff-unknown-flow-elem-1.midica
new file mode 100644
index 0000000..ecc127b
--- /dev/null
+++ b/test/org/midica/testfiles/failing/eff-unknown-flow-elem-1.midica
@@ -0,0 +1,5 @@
+INCLUDE inc/instruments.midica
+
+
+0: volll.keep.set(50)
+
diff --git a/test/org/midica/testfiles/failing/eff-unknown-flow-elem-2.midica b/test/org/midica/testfiles/failing/eff-unknown-flow-elem-2.midica
new file mode 100644
index 0000000..97f40a0
--- /dev/null
+++ b/test/org/midica/testfiles/failing/eff-unknown-flow-elem-2.midica
@@ -0,0 +1,4 @@
+INCLUDE inc/instruments.midica
+
+
+0: vol.keeeeeep.set(50)
diff --git a/test/org/midica/testfiles/working/effects-1.midica b/test/org/midica/testfiles/working/effects-1.midica
new file mode 100644
index 0000000..57ea329
--- /dev/null
+++ b/test/org/midica/testfiles/working/effects-1.midica
@@ -0,0 +1,127 @@
+INCLUDE inc/instruments.midica
+
+////////////////////////////////////////
+// channel 0: CTRL / MSB (vol == Expression)
+////////////////////////////////////////
+
+// set, wait, length
+0: vol.wait.set(10).wait().set(20) .wait(/8) .set(30)
+0: .wait.wait.set(40)
+0: .length(/2).wait.set(50) .wait .set(60)
+
+// %
+0: .length(64).wait.set(0%)
+0: .wait.set(100.000%)
+
+// double
+0: .double.wait.set(100%)
+
+////////////////////////////////////////
+// channel 1: CTRL / MSB_SIGNED (balance)
+////////////////////////////////////////
+
+// set, wait, length
+1: balance.length(64).wait.set(-64)
+1: .wait.set(63)
+1: .wait.set(0)
+1: .wait.set(-100%)
+1: .wait.set(100%)
+1: .wait.set(0%)
+
+// double
+1: .double.wait.set(-100%)
+1: .wait.set(100%)
+1: .wait.set(0%)
+1: .wait.set(-8192)
+1: .wait.set(8191)
+1: .wait.set(0)
+
+////////////////////////////////////////
+// channel 2: CTRL / BYTE (chorus)
+////////////////////////////////////////
+
+2: chorus.length(64).wait.set(0)
+2: .wait.set(127)
+2: .wait.set(0)
+2: .wait.set(100%)
+2: .wait.set(0%)
+2: .wait.set(50%)
+
+////////////////////////////////////////
+// channel 3: CTRL / BOOLEAN (hold)
+////////////////////////////////////////
+
+3: hold.length(64).wait.on()
+3: .wait.off()
+
+////////////////////////////////////////
+// channel 4: CTRL / ANY (0x50 == 80)
+////////////////////////////////////////
+
+4: length(64).wait.ctrl=80.on()
+4: .wait.off()
+4: .wait.set(100%)
+4: .wait.set(0%)
+4: .wait.set(50%)
+4: .wait.set(127)
+4: .wait.set(0)
+
+////////////////////////////////////////
+// channel 5: CTRL / NONE (0x7B == 123 == all notes off)
+////////////////////////////////////////
+
+5: ctrl=123.length(64).wait.on()
+
+////////////////////////////////////////
+// channel 6: RPN / MSB (modulation depth range)
+////////////////////////////////////////
+
+6: mod_depth_range.length(32).wait.set(0)
+6: .wait.set(127)
+6: .wait.set(0%)
+6: .wait.set(100%)
+
+// double
+6: .double.wait.set(0)
+6: .wait.set(16383)
+6: .wait.set(0%)
+6: .wait.set(50%)
+6: .wait.set(100%)
+
+6: c c
+6: rpn=0/5.set(50%)
+6: c
+6: rpn=5.set(0%)
+
+////////////////////////////////////////
+// channel 7: RPN / MSB_SIGNED (channel coarse tuning)
+////////////////////////////////////////
+
+7: coarse_tune.length(32).wait.set(0)
+7: .wait.set(63)
+7: .wait.set(-64)
+7: .wait.set(100%)
+7: .wait.set(0%)
+7: .wait.set(-100%)
+
+// double
+7: .double.wait.set(0)
+7: .wait.set(8191)
+7: .wait.set(-8192)
+7: .wait.set(0%)
+7: .wait.set(100%)
+7: .wait.set(-100%)
+
+7: c c
+7: rpn=0/2.set(50%)
+7: c
+7: rpn=2.set(0%)
+
+////////////////////////////////////////
+// channel 8: NRPN / 4/7 (+ RPNs)
+////////////////////////////////////////
+
+8: nrpn=4/7.length(32).wait.double.set(100%)
+8: c
+8: nrpn=519.length(32).wait.double.set(50%) // MSB * 128 + LSB == 128 * 4 + 7 == 512 + 7 == 519
+