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 +