Skip to content

Commit

Permalink
Enabled half tone steps for channel coarse/fine tuning
Browse files Browse the repository at this point in the history
- added half tone steps for coarse/fine tuning
- renamed some effects
- made portamento unit tests more audible
- bugfix in message classifier
- #28
  • Loading branch information
truj committed Feb 4, 2024
1 parent 19380a1 commit 70d59be
Show file tree
Hide file tree
Showing 36 changed files with 752 additions and 275 deletions.
Binary file modified midica.jar
Binary file not shown.
2 changes: 1 addition & 1 deletion src/org/midica/Midica.java
Original file line number Diff line number Diff line change
Expand Up @@ -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 = 1706045002;
public static final int COMMIT_TIME = 1707065035;

/** Branch name. Automatically changed by precommit.pl */
public static final String BRANCH = "sound-effects";
Expand Down
40 changes: 16 additions & 24 deletions src/org/midica/config/Dict.java
Original file line number Diff line number Diff line change
Expand Up @@ -1344,8 +1344,8 @@ public class Dict {
public static final String MSG4_C_GEN_PURP_CTRL_3_LSB = "msg4_c_gen_purp_3_lsb";
public static final String MSG4_C_GEN_PURP_CTRL_4_LSB = "msg4_c_gen_purp_4_lsb";
public static final String MSG4_RPN_PITCH_BEND_SENS = "msg4_rpn_pitch_band_sens"; // MSG5_C_RPN_[MSB|LSB]
public static final String MSG4_RPN_MASTER_FINE_TUN = "msg4_rpn_master_fine_tuning"; // MSG5_C_RPN_[MSB|LSB]
public static final String MSG4_RPN_MASTER_COARSE_TUN = "msg4_rpn_master_coarse_tuning"; // MSG5_C_RPN_[MSB|LSB]
public static final String MSG4_RPN_CHANNEL_FINE_TUN = "msg4_rpn_channel_fine_tuning"; // MSG5_C_RPN_[MSB|LSB]
public static final String MSG4_RPN_CHANNEL_COARSE_TUN = "msg4_rpn_channel_coarse_tuning"; // MSG5_C_RPN_[MSB|LSB]
public static final String MSG4_RPN_TUN_PROG_CHANGE = "msg4_rpn_tuning_program_change"; // MSG5_C_RPN_[MSB|LSB]
public static final String MSG4_RPN_TUN_BANK_SELECT = "msg4_rpn_tuning_bank_select"; // MSG5_C_RPN_[MSB|LSB]
public static final String MSG4_RPN_MOD_DEPTH_RANGE = "msg4_rpn_mod_depth_range"; // MSG5_C_RPN_[MSB|LSB]
Expand Down Expand Up @@ -1749,18 +1749,14 @@ public class Dict {
public static final String ERROR_FL_PARAMS_NOT_ALLOWED = "error_fl_params_not_allowed";
public static final String ERROR_FL_WRONG_PARAM_NUM = "error_fl_wrong_param_num";
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_FL_NOTE_NOT_SET = "error_fl_note_not_set";
public static final String ERROR_FL_NOTE_PAT_IDX_NAN = "error_fl_note_pat_idx_nan";
public static final String ERROR_FL_NOTE_PAT_IDX_TOO_HIGH = "error_fl_note_pat_idx_too_high";
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_NOT_SUPPORTED_BY_EFF = "error_func_not_supported_by_eff";
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_PERCENT_FORBIDDEN = "error_func_percent_forbidden";
public static final String ERROR_FUNC_BROKEN_HALFTONE = "error_func_broken_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";
Expand Down Expand Up @@ -3158,8 +3154,8 @@ private static void initLanguageEnglish() {
set( MSG4_C_GEN_PURP_CTRL_3_LSB, "LSB (General Purpose Controller 3)" );
set( MSG4_C_GEN_PURP_CTRL_4_LSB, "LSB (General Purpose Controller 4)" );
set( MSG4_RPN_PITCH_BEND_SENS, "Pitch Bend Sensitivity" );
set( MSG4_RPN_MASTER_FINE_TUN, "Master Fine Tuning (in Cents)" );
set( MSG4_RPN_MASTER_COARSE_TUN, "Master Coarse Tuning (in Half Steps)" );
set( MSG4_RPN_CHANNEL_FINE_TUN, "Channel Fine Tuning (in Cents)" );
set( MSG4_RPN_CHANNEL_COARSE_TUN, "Channel Coarse Tuning (in Halftone Steps)" );
set( MSG4_RPN_TUN_PROG_CHANGE, "Tuning Program Change" );
set( MSG4_RPN_TUN_BANK_SELECT, "Tuning Bank Select" );
set( MSG4_RPN_MOD_DEPTH_RANGE, "Modulation Depth Range" );
Expand Down Expand Up @@ -3561,18 +3557,14 @@ private static void initLanguageEnglish() {
set( ERROR_FL_PARAMS_NOT_ALLOWED, "Effect flow element '%s' does not support parameters." );
set( ERROR_FL_WRONG_PARAM_NUM, "Wrong number of parameters for function '%s'. Expected: %d. Got: %d. Parameters: '%s'" );
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_FL_NOTE_NOT_SET, "The chosen effect needs a note to be set before you can use this function: " );
set( ERROR_FL_NOTE_PAT_IDX_NAN, "pattern index '%s' is not a number in this element: %s" );
set( ERROR_FL_NOTE_PAT_IDX_TOO_HIGH, "pattern index '%s' too high in this element: %s" );
set( ERROR_FUNC_TYPE_NOT_BOOL, "Effect type not 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_NOT_SUPPORTED_BY_EFF, "The chosen effect does not support this function: " );
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_PERCENT_FORBIDDEN, "The chosen effect does not allow percentage values. Invalid parameter: " );
set( ERROR_FUNC_BROKEN_HALFTONE, "The chosen effect does not allow broken halftones. Invalid parameter: " );
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)<br>"
+ "Consider to increase the pitch bend range before setting this value." );
Expand Down Expand Up @@ -4410,12 +4402,12 @@ public static void initSyntax() {
setSyntax( SYNTAX_CC_5F_EFF4_DEP, "phaser" );
setSyntax( SYNTAX_CC_7E_MONO_MODE, "mono_mode" );
setSyntax( SYNTAX_CC_7F_POLY_MODE, "poly_mode" );
setSyntax( SYNTAX_RPN_0_PITCH_BEND_R, "pitch_bend_range" );
setSyntax( SYNTAX_RPN_1_FINE_TUNE, "fine_tune" );
setSyntax( SYNTAX_RPN_2_COARSE_TUNE, "coarse_tune" );
setSyntax( SYNTAX_RPN_3_TUNING_PROG, "tuning_program" );
setSyntax( SYNTAX_RPN_4_TUNING_BANK, "tuning_bank" );
setSyntax( SYNTAX_RPN_5_MOD_DEPTH_R, "mod_depth_range" );
setSyntax( SYNTAX_RPN_0_PITCH_BEND_R, "bend_range" );
setSyntax( SYNTAX_RPN_1_FINE_TUNE, "fine_tune" );
setSyntax( SYNTAX_RPN_2_COARSE_TUNE, "coarse_tune" );
setSyntax( SYNTAX_RPN_3_TUNING_PROG, "tuning_prog" );
setSyntax( SYNTAX_RPN_4_TUNING_BANK, "tuning_bank" );
setSyntax( SYNTAX_RPN_5_MOD_DEPTH_R, "mod_range" );

// switch to lower/upper, if needed
if (Config.CBX_SYNTAX_LOWER.equals(configuredSyntax)) {
Expand Down
117 changes: 77 additions & 40 deletions src/org/midica/file/read/Effect.java
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@
package org.midica.file.read;

import java.util.ArrayList;
import java.util.Collection;
import java.util.HashMap;
import java.util.HashSet;
import java.util.List;
Expand Down Expand Up @@ -631,10 +632,38 @@ private static void applyFunction(String funcName, String[] params) throws Parse
return;
}

// All other functions are not supported by all effect types.
// Check if the function is supported.
boolean isSupported = false;
Collection<Integer> supportedFunctions = flow.getSupportedFunctions(funcName);
if (MidicaPLParser.FUNC_ON.equals(funcName)) {
if (supportedFunctions.contains(EffectFlow.FUNC_TYPE_ON)) {
isSupported = true;
}
}
else if (MidicaPLParser.FUNC_OFF.equals(funcName)) {
if (supportedFunctions.contains(EffectFlow.FUNC_TYPE_OFF))
isSupported = true;
}
else if (MidicaPLParser.FUNC_SET.equals(funcName)) {
if (supportedFunctions.contains(EffectFlow.FUNC_TYPE_SET))
isSupported = true;
}
else if (MidicaPLParser.FUNC_NOTE.equals(funcName)) {
if (supportedFunctions.contains(EffectFlow.FUNC_TYPE_NOTE))
isSupported = true;
}
else if (supportedFunctions.contains(EffectFlow.FUNC_TYPE_CONT)) {
isSupported = true;
}
if (!isSupported)
throw new ParseException(Dict.get(Dict.ERROR_FUNC_NOT_SUPPORTED_BY_EFF) + funcName);

// note()
if (MidicaPLParser.FUNC_NOTE.equals(funcName)) {

int note = parser.parseNote(params[0]);
flow.setNote(note, funcName);
flow.setNote(note);
return;
}

Expand All @@ -656,27 +685,13 @@ private static void applyFunction(String funcName, String[] params) throws Parse
if (valueType == EffectFlow.TYPE_NONE || valueType == EffectFlow.TYPE_ANY) {
value = flow.getDefaultValueForOn(valueType);
}
else if (valueType != EffectFlow.TYPE_BOOLEAN) {
throw new ParseException(String.format(Dict.get(Dict.ERROR_FUNC_TYPE_NOT_BOOL), funcName));
}
}
else if (valueType != EffectFlow.TYPE_BOOLEAN && valueType != EffectFlow.TYPE_ANY) {
throw new ParseException(String.format(Dict.get(Dict.ERROR_FUNC_TYPE_NOT_BOOL), funcName));
}

setValue(new int[] {value, value});

return;
}

// non-boolean function for a boolean effect?
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)) {
int[] values = parseIntParam(params[0]);
Expand All @@ -689,13 +704,6 @@ else if (valueType != EffectFlow.TYPE_BOOLEAN && valueType != EffectFlow.TYPE_AN
|| MidicaPLParser.FUNC_SIN.equals(funcName) || MidicaPLParser.FUNC_COS.equals(funcName)
|| MidicaPLParser.FUNC_NSIN.equals(funcName) || MidicaPLParser.FUNC_NCOS.equals(funcName)) {

// don't allow (N)RPN
int effectType = flow.getEffectType();
if (EffectFlow.EFF_TYPE_RPN == effectType)
throw new ParseException(String.format(Dict.get(Dict.ERROR_FL_CONT_RPN), funcName));
if (EffectFlow.EFF_TYPE_NRPN == effectType)
throw new ParseException(String.format(Dict.get(Dict.ERROR_FL_CONT_NRPN), funcName));

applyContinousFunction(funcName, params);

return;
Expand Down Expand Up @@ -989,7 +997,7 @@ private static int[] parseIntParam(String valueStr) throws ParseException {
// get range of the sound effect
int min = flow.getMin();
int max = flow.getMax();
boolean needHalfTones = flow.mustUseHalfToneSteps();
boolean supportsPercent = flow.supportsPercentage();
boolean canUseHalfTones = flow.supportsHalfToneSteps();
boolean isMsbLsb = false;

Expand All @@ -1011,14 +1019,18 @@ private static int[] parseIntParam(String valueStr) throws ParseException {
}
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.
// check percentage input
if (!supportsPercent)
throw new ParseException(Dict.get(Dict.ERROR_FUNC_PERCENT_FORBIDDEN) + valueStr);
if (percent > 100)
throw new ParseException(String.format(Dict.get(Dict.ERROR_FUNC_VAL_GREATER_MAX), valueStr, 100 + MidicaPLParser.EFF_PERCENT));
if (percent < -100 && min < 0)
throw new ParseException(String.format(Dict.get(Dict.ERROR_FUNC_VAL_LOWER_MIN), valueStr, -100 + MidicaPLParser.EFF_PERCENT));
if (percent < 0 && 0 == min)
throw new ParseException(String.format(Dict.get(Dict.ERROR_FUNC_VAL_LOWER_MIN), valueStr, min));
throw new ParseException(String.format(Dict.get(Dict.ERROR_FUNC_VAL_LOWER_MIN), valueStr, 0 + MidicaPLParser.EFF_PERCENT));

// calculate value
if (percent < 0)
// theoretically: value = percent * -min / 100
value = (int) ((percent * -min * 10 - 100 * 5) / (100 * 10));
Expand Down Expand Up @@ -1048,7 +1060,7 @@ else if (msbStr != null) {
value = msb * 128 + lsb;
}
else {
throw new ParseException(Dict.get(Dict.ERROR_FUNC_NO_NUMBER) + valueStr);
throw new FatalParseException(Dict.get(Dict.ERROR_FUNC_NO_NUMBER) + valueStr);
}
}
}
Expand All @@ -1061,7 +1073,7 @@ else if (msbStr != 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 && !needHalfTones && !isMsbLsb)
if (value > max && !isMsbLsb)
throw new ParseException(String.format(Dict.get(Dict.ERROR_FUNC_VAL_GREATER_MAX), valueStr, max));

// adjust the actual MIDI value for signed types
Expand All @@ -1075,7 +1087,7 @@ else if (msbStr != null) {

// find out how many bytes are needed
int byteCount = 1;
if (EffectFlow.TYPE_MSB == valueType || EffectFlow.TYPE_MSB_SIGNED == valueType || EffectFlow.TYPE_MSB_HALFTONES == valueType) {
if (EffectFlow.TYPE_MSB == valueType || EffectFlow.TYPE_MSB_SIGNED == valueType) {
if (flow.isDouble())
byteCount = 2;
}
Expand All @@ -1094,7 +1106,7 @@ else if (msbStr != null) {
}

/**
* Parses half-tone parameters (flowt or int).
* Parses half-tone parameters (float or int).
*
* This is used for one of the following effect types:
*
Expand All @@ -1105,7 +1117,6 @@ else if (msbStr != null) {
* @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();
Expand All @@ -1115,6 +1126,7 @@ private static int parseHalfToneSteps(String halfToneStr) throws ParseException
// pitch bend range
if (0x0000 == effNum) {

// check range
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));
Expand All @@ -1130,6 +1142,38 @@ private static int parseHalfToneSteps(String halfToneStr) throws ParseException
return msb * 128 + lsb;
}

// channel coarse tune
if (0x0002 == effNum) {

// don't allow broken values
if (halfToneSteps != Math.round(halfToneSteps))
throw new ParseException(Dict.get(Dict.ERROR_FUNC_BROKEN_HALFTONE) + halfToneStr);

// only one byte allowed
return Math.round(halfToneSteps);
}

// From here on, we have either channel fine tune or pitch bend.
// Both are signed MSBs that can have up to 2 bytes.
// get max value
int max;
if (flow.isDouble())
max = halfToneSteps < 0 ? 8192 : 8191;
else
max = halfToneSteps < 0 ? 64 : 63;

// channel fine tune
if (0x0001 == effNum) {

// not between +/-1.0?
if (halfToneSteps < -1.0)
throw new ParseException(String.format(Dict.get(Dict.ERROR_FUNC_VAL_LOWER_MIN), halfToneStr, -1.0));
if (halfToneSteps > 1.0)
throw new ParseException(String.format(Dict.get(Dict.ERROR_FUNC_VAL_GREATER_MAX), halfToneStr, 1.0));

return Math.round(halfToneSteps * max);
}

// pitch bend
if (0xE0 == effNum) {

Expand All @@ -1143,13 +1187,6 @@ private static int parseHalfToneSteps(String halfToneStr) throws ParseException
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));
}

Expand Down
Loading

0 comments on commit 70d59be

Please sign in to comment.