Skip to content

Commit

Permalink
sound effects (work in progress)
Browse files Browse the repository at this point in the history
- implemented mono_at and poly_at
- enabled poly_at and port_ctrl using .note()
- enabled effects within patterns
- for poly_at and port_ctrl in patterns:
  - using note index as parameter for .note()
- added mono_mode and poly_mode
- added more unit tests
- #28
  • Loading branch information
truj committed Jan 22, 2024
1 parent df9511f commit a086fb0
Show file tree
Hide file tree
Showing 21 changed files with 1,732 additions and 87 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 = 1704405921;
public static final int COMMIT_TIME = 1705915601;

/** Branch name. Automatically changed by precommit.pl */
public static final String BRANCH = "sound-effects";
Expand Down
16 changes: 15 additions & 1 deletion src/org/midica/config/Dict.java
Original file line number Diff line number Diff line change
Expand Up @@ -250,6 +250,8 @@ public class Dict {
public static final String SYNTAX_CC_5D_EFF3_DEP = "CC_5D_EFF3_DEP";
public static final String SYNTAX_CC_5E_EFF4_DEP = "CC_5E_EFF4_DEP";
public static final String SYNTAX_CC_5F_EFF4_DEP = "CC_5F_EFF4_DEP";
public static final String SYNTAX_CC_7E_MONO_MODE = "CC_7E_MONO_MODE";
public static final String SYNTAX_CC_7F_POLY_MODE = "CC_7F_POLY_MODE";
public static final String SYNTAX_RPN_0_PITCH_BEND_R = "RPN_0_PITCH_BEND_R";
public static final String SYNTAX_RPN_1_FINE_TUNE = "RPN_1_FINE_TUNE";
public static final String SYNTAX_RPN_2_COARSE_TUNE = "RPN_2_COARSE_TUNE";
Expand Down Expand Up @@ -1757,6 +1759,9 @@ public class Dict {
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";
Expand Down Expand Up @@ -3018,6 +3023,8 @@ private static void initLanguageEnglish() {
set( SYNTAX_CC_5D_EFF3_DEP, "Controller 93 (0x5D): Effect 3 Depth (Chorus)" );
set( SYNTAX_CC_5E_EFF4_DEP, "Controller 94 (0x5E): Effect 4 Depth (Celeste/Detune)" );
set( SYNTAX_CC_5F_EFF4_DEP, "Controller 95 (0x5F): Effect 5 Depth (Phaser)" );
set( SYNTAX_CC_7E_MONO_MODE, "Controller 95 (0x7E): Mono Mode ON (Poly OFF)" );
set( SYNTAX_CC_7F_POLY_MODE, "Controller 95 (0x7F): Poly Mode ON (Mono OFF)" );
set( SYNTAX_RPN_0_PITCH_BEND_R, "RPN 0 (0x0000): Pitch Bend Range" );
set( SYNTAX_RPN_1_FINE_TUNE, "RPN 1 (0x0001): Channel Fine Tune" );
set( SYNTAX_RPN_2_COARSE_TUNE, "RPN 2 (0x0002): Channel Coarse Tune" );
Expand Down Expand Up @@ -3571,6 +3578,9 @@ private static void initLanguageEnglish() {
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." );
Expand Down Expand Up @@ -4382,7 +4392,7 @@ public static void initSyntax() {
setSyntax( SYNTAX_CH_A_POLY_AT, "poly_at" );
setSyntax( SYNTAX_CH_D_MONO_AT, "mono_at" );
setSyntax( SYNTAX_CH_E_PITCH_BEND, "bend" );
setSyntax( SYNTAX_CC_01_MOD, "modulation" );
setSyntax( SYNTAX_CC_01_MOD, "mod" );
setSyntax( SYNTAX_CC_02_BREATH, "breath" );
setSyntax( SYNTAX_CC_04_FOOT, "foot" );
setSyntax( SYNTAX_CC_05_PORT_TIME, "port_time" );
Expand Down Expand Up @@ -4412,6 +4422,8 @@ public static void initSyntax() {
setSyntax( SYNTAX_CC_5D_EFF3_DEP, "chorus" );
setSyntax( SYNTAX_CC_5E_EFF4_DEP, "detune" );
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" );
Expand Down Expand Up @@ -4626,6 +4638,8 @@ else if (Config.CBX_SYNTAX_UPPER.equals(configuredSyntax)) {
addSyntaxForInfoView( SYNTAX_CC_5D_EFF3_DEP );
addSyntaxForInfoView( SYNTAX_CC_5E_EFF4_DEP );
addSyntaxForInfoView( SYNTAX_CC_5F_EFF4_DEP );
addSyntaxForInfoView( SYNTAX_CC_7E_MONO_MODE );
addSyntaxForInfoView( SYNTAX_CC_7F_POLY_MODE );
addSyntaxForInfoView( SYNTAX_RPN_0_PITCH_BEND_R );
addSyntaxForInfoView( SYNTAX_RPN_1_FINE_TUNE );
addSyntaxForInfoView( SYNTAX_RPN_2_COARSE_TUNE );
Expand Down
200 changes: 145 additions & 55 deletions src/org/midica/file/read/Effect.java
Original file line number Diff line number Diff line change
Expand Up @@ -50,6 +50,7 @@ public class Effect {

private static Pattern flowPattern = null;
private static Pattern noteOrRestPattern = null;
private static Pattern noteIndexPattern = null;
private static Pattern intPattern = null;
private static Pattern periodsPattern = null;

Expand Down Expand Up @@ -132,6 +133,8 @@ public static void init(MidicaPLParser rootParser) {
effectNames.add( MidicaPLParser.CC_5D_EFF3_DEP );
effectNames.add( MidicaPLParser.CC_5E_EFF4_DEP );
effectNames.add( MidicaPLParser.CC_5F_EFF4_DEP );
effectNames.add( MidicaPLParser.CC_7E_MONO_MODE );
effectNames.add( MidicaPLParser.CC_7F_POLY_MODE );
effectNames.add( MidicaPLParser.RPN_0_PITCH_BEND_R );
effectNames.add( MidicaPLParser.RPN_1_FINE_TUNE );
effectNames.add( MidicaPLParser.RPN_2_COARSE_TUNE );
Expand Down Expand Up @@ -178,6 +181,8 @@ public static void init(MidicaPLParser rootParser) {
ctrlNameToNumber.put( MidicaPLParser.CC_5D_EFF3_DEP, 0x5D );
ctrlNameToNumber.put( MidicaPLParser.CC_5E_EFF4_DEP, 0x5E );
ctrlNameToNumber.put( MidicaPLParser.CC_5F_EFF4_DEP, 0x5F );
ctrlNameToNumber.put( MidicaPLParser.CC_7E_MONO_MODE, 0x7E );
ctrlNameToNumber.put( MidicaPLParser.CC_7F_POLY_MODE, 0x7F );

// init structures for RPN-based effects
rpnNameToNumber.put( MidicaPLParser.RPN_0_PITCH_BEND_R, 0x0000 );
Expand All @@ -197,49 +202,69 @@ public static void init(MidicaPLParser rootParser) {
flowElementNames.add(MidicaPLParser.FL_DOUBLE);

// compile regex patterns
String flowRegex
= "\\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+(?:\\.\\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)
+ ")?"
+ "$";
periodsPattern = Pattern.compile(periodsRegex);
{
// flow pattern
String flowRegex
= "\\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);

// note index pattern (to replace .note(idx) by .note(name))
String noteIndexRegex
= "\\G" // end of previous match
+ "(.*?)" // anything (group 1)
+ Pattern.quote(MidicaPLParser.FL_DOT) // '.'
+ Pattern.quote(MidicaPLParser.FUNC_NOTE) // note
+ Pattern.quote(MidicaPLParser.PARAM_OPEN) // (
+ "(\\S*?)" // parameter (group 2)
+ Pattern.quote(MidicaPLParser.PARAM_CLOSE); // )
noteIndexPattern = Pattern.compile(noteIndexRegex);

// note/rest pattern
noteOrRestPattern = Pattern.compile("^[0-9]|" + Pattern.quote(MidicaPLParser.REST) + "$");

// int pattern (for function parameters)
String intRegex = "^"
+ "(\\-?\\d+)" // direct int value (group 1)
+ "|"
+ "(?:"
+ "(\\-?\\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);

// periods pattern (for function parameters)
String periodsRegex = "^"
+ "(\\-?\\d+(?:\\.\\d*))" // int or float (group 1)
+ "(" // percent (group 2)
+ Pattern.quote(MidicaPLParser.EFF_PERCENT)
+ ")?"
+ "$";
periodsPattern = Pattern.compile(periodsRegex);
}
}

/**
Expand Down Expand Up @@ -308,10 +333,6 @@ public static boolean applyFlowIfPossible(int channel, String flowStr, String le
String numberLsb = m.group(4);
String paramStr = m.group(5);

// flow or something else?
if (null == elemName) {
return false;
}
if (flowElementNames.contains(elemName))
looksLikeFlow = true;
else
Expand Down Expand Up @@ -376,6 +397,57 @@ public static void closeFlowIfPossible() {
flow = null;
}

/**
* Translates the pattern indices inside the given effect flow to notes.
*
* Needed for pattern calls.
*
* @param flowStr the effect flow to be translated
* @param noteNumbers the note numbers
* @return the translated flow
* @throws ParseException if an index is not a positive integer or too high for the given noteNumbers.
*/
public static String translatePatternIndices(String flowStr, Integer[] noteNumbers) throws ParseException {

String translated = "";
Matcher m = noteIndexPattern.matcher(flowStr);
int lastMatchOffset = 0;
while (m.find()) {
lastMatchOffset = m.end();
String beforeStr = m.group(1);
String indexStr = m.group(2);
try {
int index = Integer.parseInt(indexStr);
int note = noteNumbers[index];
translated += beforeStr + MidicaPLParser.FL_DOT + MidicaPLParser.FUNC_NOTE
+ MidicaPLParser.PARAM_OPEN + note + MidicaPLParser.PARAM_CLOSE;

}
catch (NumberFormatException e) {
String noteElem = MidicaPLParser.FL_DOT + MidicaPLParser.FUNC_NOTE
+ MidicaPLParser.PARAM_OPEN + indexStr + MidicaPLParser.PARAM_CLOSE;
throw new ParseException(
String.format(Dict.get(Dict.ERROR_FL_NOTE_PAT_IDX_NAN), indexStr, noteElem)
);
}
catch (IndexOutOfBoundsException e) {
String noteElem = MidicaPLParser.FL_DOT + MidicaPLParser.FUNC_NOTE
+ MidicaPLParser.PARAM_OPEN + indexStr + MidicaPLParser.PARAM_CLOSE;
throw new ParseException(
String.format(Dict.get(Dict.ERROR_FL_NOTE_PAT_IDX_TOO_HIGH), indexStr, noteElem)
);
}
}

// unmatched characters left?
if (lastMatchOffset != flowStr.length()) {
String remainder = flowStr.substring(lastMatchOffset);
translated += remainder;
}

return translated;
}

/**
* Parses a generic controller or (N)RPN number, assigned in a flow.
*
Expand Down Expand Up @@ -569,14 +641,24 @@ private static void applyFunction(String funcName, String[] params) throws Parse
// for all other functions we need the effect type
int valueType = flow.getValueType(funcName);

// note required but not set?
if (flow.needsNote() && flow.getNote() < 0)
throw new ParseException(Dict.get(Dict.ERROR_FL_NOTE_NOT_SET) + funcName);

// on()/off() - boolean functions
if (MidicaPLParser.FUNC_ON.equals(funcName) || MidicaPLParser.FUNC_OFF.equals(funcName)) {
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;
if (MidicaPLParser.FUNC_ON.equals(funcName)) {

// special case: allow on() for ANY / NONE (but then use a predefined value)
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));
Expand Down Expand Up @@ -649,20 +731,28 @@ private static void setValue(int[] values) throws ParseException {
if (2 == values.length) {

// pitch bend?
if (0xE0 == flow.getEffectNumber()) {
if (0xE0 == effectNum) {
// special case: set(0) for MSB only - set the LSB to 0 - to get no bend at all
if (0x40 == values[1])
SequenceCreator.addMessageChannelEffect(effectNum, channel, 0, values[1], tick);
else
// use the MSB in both positions - cover the whole range
SequenceCreator.addMessageChannelEffect(effectNum, channel, values[1], values[1], tick);
return;
}
else {
// TODO: mono_at, poly_at
// MSB first, LSB second

// mono_at?
if (0xD0 == effectNum) {
SequenceCreator.addMessageChannelEffect(effectNum, channel, values[1], 0, tick);
return;
}

// poly_at?
if (0xA0 == effectNum) {
int note = flow.getNote();
SequenceCreator.addMessageChannelEffect(effectNum, channel, note, values[1], tick);
return;
}
return;
}
else if (3 == values.length) {
// pitch bend: lsb first, msb second
Expand Down
Loading

0 comments on commit a086fb0

Please sign in to comment.