/**
* Generates and combines pitch and rhythm sequences under user control. The combined
* phrases are then arranged to form a melody, which is given a rating by means of a
* slider. The phrases and melody can be played and, if desired, discarded by pressing
* 'dislike'. Phrases that are replaced but not disliked are assumed to be liked.
*
* @author Ray Whorley
* @version 1.4, 22nd June 2020
*/
/**
*Reuse of the code is welcomed, and governed by the GNU General Public License Version 3.
*/
import java.util.*;
import java.io.*;
import java.nio.file.*;
import javax.sound.midi.*;
/**
* Creates and plays MIDI files, and deals with all file handling.
*/
private Music music = new Music();
/**
* Calculates musical pitch transition probabilities and generates pitch sequences by
* random sampling of the probability distributions.
*/
private Generator generate = new Generator();
/**
* Slider used for rating a melody prior to saving.
*/
private Slider rating;
/**
* Array of buttons indicating pitch phrase number or "empty pitches".
*/
private PitchButton[] pitch = new PitchButton[4];
/**
* Array of buttons for generating pitch phrases.
*/
private PitchButton[] genPitch = new PitchButton[4];
/**
* Array of buttons for playing pitch phrases.
*/
private PitchButton[] playPitch = new PitchButton[4];
/**
* Array of buttons for discarding pitch phrases to a "dislike" folder.
*/
private PitchButton[] dislikePitch = new PitchButton[4];
/**
* Array of buttons indicating rhythm phrase number or "empty rhythm".
*/
private RhythmButton[] rhythm = new RhythmButton[4];
/**
* Array of buttons for generating rhythm phrases.
*/
private RhythmButton[] genRhythm = new RhythmButton[4];
/**
* Array of buttons for playing rhythm phrases.
*/
private RhythmButton[] playRhythm = new RhythmButton[4];
/**
* Array of buttons for discarding rhythm phrases to a "dislike" folder.
*/
private RhythmButton[] dislikeRhythm = new RhythmButton[4];
/**
* Array of buttons indicating phrase number or "empty phrase".
*/
private CombineButton[] phrase = new CombineButton[4];
/**
* Array of buttons for combining pitch and rhythm phrases.
*/
private CombineButton[] combine = new CombineButton[4];
/**
* Array of buttons for playing combined pitch/rhythm phrases.
*/
private CombineButton[] playCombine = new CombineButton[4];
/**
* Array of buttons for discarding combined pitch/rhythm phrases to a "dislike" folder.
*/
private CombineButton[] dislikeCombine = new CombineButton[4];
/**
* Array of buttons indicating phrase number or "empty phrase". Touching a button copies
* a combined pitch/rhythm phrase to the melody.
*/
private MelodyButton[] melody = new MelodyButton[4];
/**
* A button for saving the melody and its rating.
*/
private MelodyButton[] saveMelody = new MelodyButton[1];
/**
* A button for playing the melody.
*/
private MelodyButton[] playMelody = new MelodyButton[1];
/**
* A button for clearing the melody.
*/
private MelodyButton[] clearMelody = new MelodyButton[1];
/**
* Background colour of the screen.
*/
private color backgroundColour;
/**
* Ratio of actual screen width to an arbitrary screen width. Used for adapting the GUI
* to different screen sizes and aspect ratios.
*/
private float wFactor;
/**
* Ratio of actual screen height to an arbitrary screen height. Used for adapting the
* GUI to different screen sizes and aspect ratios.
*/
private float hFactor;
/**
* The number of notes in a musical phrase.
*/
private int numNotes = 16;
/**
* Pitch phrase number, incremented as pitch phrases are generated.
*/
private int pitchPhraseCount = 0;
/**
* Rhythm phrase number, incremented as rhythm phrases are generated.
*/
private int rhythmPhraseCount = 0;
/**
* Combined pitch/rhythm phrase number, incremented as phrases are combined.
*/
private int combinePhraseCount = 0;
/**
* Melody number, incremented each time a melody is saved.
*/
private int melodyCount = 1;
/**
* Representation of a pitch phrase, initially empty.
*/
private String pitchPhrase = "";
/**
* Representation of a rhythm phrase, initially empty.
*/
private String rhythmPhrase = "";
/**
* A pitch phrase array, initially empty.
*/
private String[] pPhraseArray = new String[4];
/**
* A rhythm phrase array, initially empty.
*/
private String[] rPhraseArray = new String[4];
/**
* Array of handcrafted rhythms.
*/
private String[] rhythmArray = new String[24];
/**
* Sets screen size and colour; instantiates Slider and Button objects; and sets up an
* array of rhythms.
*/
public void setup() {
size(450, 800);
wFactor = width/270.0;
hFactor = height/540.0;
backgroundColour = color(0);
rating = new Slider(123, 465);
for (int i = 0; i < 4; i++) {
pitch[i] = new PitchButton(0.0479, 0.055 + i*0.06, 69, 15, 170, 0, 0, 8, 255,
"empty pitches", 0.078, 0.075 + i*0.06, 0, 255, 0, 0,
"", 0.066);
genPitch[i] = new PitchButton(0.343, 0.055 + i*0.06, 63, 15, 170, 0, 0, 8, 255,
"generate", 0.397, 0.075 + i*0.06, 255, 0, 0, 255,
"generating...", 0.365);
playPitch[i] = new PitchButton(0.615, 0.055 + i*0.06, 41, 15, 0, 0, 170, 8, 255,
"play", 0.663, 0.075 + i*0.06, 0, 0, 255, 255,
"playing...", 0.621);
dislikePitch[i] = new PitchButton(0.805, 0.055 + i*0.06, 41, 15, 170, 0, 0, 8, 255,
"dislike", 0.833, 0.075 + i*0.06, 255, 0, 0, 255,
"moving...", 0.808);
rhythm[i] = new RhythmButton(0.0479, 0.332 + i*0.06, 69, 15, 170, 0, 0, 8, 255,
"empty rhythm", 0.078, 0.352 + i*0.06, 0, 255, 0, 0,
"", 0.053);
genRhythm[i] = new RhythmButton(0.343, 0.332 + i*0.06, 63, 15, 170, 0, 0, 8, 255,
"generate", 0.397, 0.352 + i*0.06, 255, 0, 0, 255,
"generating...", 0.365);
playRhythm[i] = new RhythmButton(0.615, 0.332 + i*0.06, 41, 15, 0, 0, 170, 8, 255,
"play", 0.663, 0.352 + i*0.06, 0, 0, 255, 255,
"playing...", 0.621);
dislikeRhythm[i] = new RhythmButton(0.805, 0.332 + i*0.06, 41, 15, 170, 0, 0, 8, 255,
"dislike", 0.833, 0.352 + i*0.06, 255, 0, 0, 255,
"moving...", 0.808);
phrase[i] = new CombineButton(0.0479, 0.609 + i*0.06, 69, 15, 170, 0, 0, 8, 255,
"empty phrase", 0.081, 0.629 + i*0.06, 0, 255, 0, 0,
"", 0.082);
combine[i] = new CombineButton(0.343, 0.609 + i*0.06, 63, 15, 170, 0, 0, 8, 255,
"combine", 0.40, 0.629 + i*0.06, 255, 0, 0, 255,
"combining...", 0.365);
playCombine[i] = new CombineButton(0.615, 0.609 + i*0.06, 41, 15, 0, 0, 170, 8, 255,
"play", 0.663, 0.629 + i*0.06, 0, 0, 255, 255,
"playing...", 0.621);
dislikeCombine[i] = new CombineButton(0.805, 0.609 + i*0.06, 41, 15, 170, 0, 0, 8,
255, "dislike", 0.833, 0.629 + i*0.06, 255, 0,
0, 255, "moving...", 0.808);
melody[i] = new MelodyButton(0.05 + i*0.229, 0.886, 60, 15, 170, 0, 0, 8, 255,
"empty phrase", 0.067 + i*0.229, 0.906, 255, 0, 0, 255,
"moving...", 0.095 + i*0.229);
}
saveMelody[0] = new MelodyButton(0.409, 0.946, 41, 15, 0, 170, 0, 8, 255, "save",
0.452, 0.966, 0, 255, 0, 0, "saving...", 0.421);
playMelody[0] = new MelodyButton(0.604, 0.946, 41, 15, 0, 0, 170, 8, 255, "play",
0.653, 0.966, 0, 0, 255, 255, "playing...", 0.612);
clearMelody[0] = new MelodyButton(0.799, 0.946, 41, 15, 170, 0, 0, 8, 255, "clear",
0.84, 0.966, 255, 0, 0, 255, "clearing...", 0.803);
rhythmArray[0] = "24 24 24 24 24 24 24 24 24 24 24 24 24 24 24 24 ";
rhythmArray[1] = "48 24 24 24 12 12 24 24 24 12 12 24 12 12 48 48 ";
rhythmArray[2] = "24 12 12 24 24 48 24 24 24 12 12 24 12 12 48 48 ";
rhythmArray[3] = "36 12 24 24 36 12 24 24 24 24 24 24 36 12 24 24 ";
rhythmArray[4] = "48 36 12 24 12 12 36 12 24 12 12 36 12 24 24 48 ";
rhythmArray[5] = "48 48 12 12 12 12 48 12 12 12 12 24 24 36 12 48 ";
rhythmArray[6] = "48 48 18 6 18 6 24 24 24 24 12 12 12 12 48 48 ";
rhythmArray[7] = "24 24 48 24 24 48 12 12 12 12 12 12 12 12 48 48 ";
rhythmArray[8] = "12 12 12 12 24 24 24 24 48 12 12 12 12 24 24 96 ";
rhythmArray[9] = "24 24 12 12 24 24 24 12 12 24 36 12 24 24 48 48 ";
rhythmArray[10] = "24 12 12 12 12 24 24 24 48 24 12 12 12 12 24 96 ";
rhythmArray[11] = "24 12 12 12 12 24 36 12 24 24 36 12 12 12 24 96 ";
rhythmArray[12] = "24 12 12 24 24 24 24 48 24 12 12 24 24 24 24 48 ";
rhythmArray[13] = "24 12 12 24 24 24 24 36 12 24 24 24 24 36 12 48 ";
rhythmArray[14] = "24 12 12 24 24 36 12 48 24 12 12 24 24 36 12 48 ";
rhythmArray[15] = "24 12 12 24 24 36 12 24 24 24 24 24 24 36 12 48 ";
rhythmArray[16] = "24 24 24 24 24 12 12 12 12 24 24 24 24 24 48 48 ";
rhythmArray[17] = "24 24 24 24 24 24 24 24 24 24 24 24 36 12 24 24 ";
rhythmArray[18] = "36 12 24 24 36 12 24 24 24 12 12 24 24 36 12 48 ";
rhythmArray[19] = "36 12 36 12 36 12 12 12 24 36 12 36 12 36 12 48 ";
rhythmArray[20] = "36 12 24 24 24 24 24 24 36 12 24 24 24 24 24 24 ";
rhythmArray[21] = "24 24 24 24 12 24 12 24 24 24 24 12 24 12 48 48 ";
rhythmArray[22] = "36 12 12 24 12 24 24 36 12 36 12 12 24 12 48 48 ";
rhythmArray[23] = "36 12 12 24 12 36 12 24 24 24 24 12 24 12 48 48 ";
}
/**
* Draws the graphical user interface on the screen. The GUI comprises text, lines,
* buttons and slider. It is divided into four sections: pitch, rhythm, combined
* pitch/rhythm and melody. There are buttons representing musical phrases; and buttons
* for generating, playing and discarding (disliking) phrases. There is also a button
* for saving a melody, and a slider for rating a melody. This method is called
* regularly to register updates caused by touching buttons and the slider.
*/
public void draw() {
background(backgroundColour);
noFill();
stroke(255, 190, 0);
rect(width*0.02, height*0.045, width*0.96, height*0.233);
rect(width*0.02, height*0.322, width*0.96, height*0.233);
rect(width*0.02, height*0.599, width*0.96, height*0.233);
line(width*0.02, height*0.876, width*0.38, height*0.876);
line(width*0.38, height*0.837, width*0.98, height*0.837);
line(width*0.02, height*0.988, width*0.98, height*0.988);
line(width*0.02, height*0.876, width*0.02, height*0.988);
line(width*0.98, height*0.837, width*0.98, height*0.988);
line(width*0.38, height*0.837, width*0.38, height*0.876);
fill(255, 190, 0);
textSize(round(11*width/270));
text("pitch phrases", 0.02*width, 0.035*height);
text("rhythm phrases", 0.02*width, 0.312*height);
text("combined phrases", 0.02*width, 0.589*height);
text("complete melody", 0.02*width, 0.866*height);
resetPlay(pitch, playPitch, "pitchPlay", "pitch", "pitches");
resetPlay(rhythm, playRhythm, "rhythmPlay", "rhythm", "rhythms");
resetPlay(phrase, playCombine, "combinePlay", "combine", "combined");
resetPlay(melody, playMelody, "melodyPlay", "melody", "melody");
resetGen(pitch, genPitch, pitchPhraseCount);
resetGen(rhythm, genRhythm, rhythmPhraseCount);
resetCombine();
resetMelody();
resetDislike(pitch, dislikePitch);
resetDislike(rhythm, dislikeRhythm);
resetDislike(phrase, dislikeCombine);
resetSave();
resetClear();
for (int i = 0; i < 4; i++) {
pitch[i].drawButton();
genPitch[i].drawButton();
playPitch[i].drawButton();
dislikePitch[i].drawButton();
rhythm[i].drawButton();
genRhythm[i].drawButton();
playRhythm[i].drawButton();
dislikeRhythm[i].drawButton();
phrase[i].drawButton();
combine[i].drawButton();
playCombine[i].drawButton();
dislikeCombine[i].drawButton();
melody[i].drawButton();
}
saveMelody[0].drawButton();
playMelody[0].drawButton();
clearMelody[0].drawButton();
rating.drawSlider();
}
/**
* The various portions of the GUI are checked to ascertain where the touch occurred
* (and therefore what action is required). Touch is synonymous with mouse press.
*/
public void mousePressed() {
pitchSection();
rhythmSection();
combinedSection();
melodySection();
}
/**
* Slider horizonal position on the screen and rating displayed on the slider are
* updated if the slider is being touched while the finger is moving across the screen.
* Finger moving is synonymous with mouse dragging.
*/
public void mouseDragged() {
if (rating.onSlider() && mouseX >= rating.getLeft() && mouseX <= rating.getRight()) {
rating.setSliderX(mouseX - round(8.5*wFactor));
}
for (int i = 0; i < 121; i += 12) {
if (rating.onSlider() && mouseX < rating.getLeft() + round((i + 8.5)*wFactor)
&& mouseX > rating.getLeft() + round((i - 8.5)*wFactor)) {
rating.setRating("" + i/12);
break;
}
}
}
/**
* The slider is centered on the nearest vertical line on the scale when screen touching
* ceases. Cessation of touch is synonymous with mouse release.
*/
public void mouseReleased() {
for (int i = 0; i < 11; i++) {
if (Integer.parseInt(rating.getRating()) == i) {
rating.setSliderX(rating.getLeft() + round((i*12 - 9.0)*wFactor));
break;
}
}
}
/**
* The various parts of the pitch section of the GUI are checked to ascertain where a
* touch occurred (and therefore what action is required).
*/
private void pitchSection() {
if (select(pitch)) {
} else if (generatePitch()) {
} else if (play(pitch, playPitch)) {
} else {
dislike(pitch, dislikePitch, "pitchMove", "pitch", "pitches");
}
}
/**
* The various parts of the rhythm section of the GUI are checked to ascertain where a
* touch occurred (and therefore what action is required).
*/
private void rhythmSection() {
if (select(rhythm)) {
} else if (generateRhythm()) {
} else if (play(rhythm, playRhythm)) {
} else {
dislike(rhythm, dislikeRhythm, "rhythmMove", "rhythm", "rhythms");
}
}
/**
* The various parts of the combined pitch/rhythm section of the GUI are checked to
* ascertain where a touch occurred (and therefore what action is required).
*/
private void combinedSection() {
if (select(phrase)) {
} else if (combine()) {
} else if (play(phrase, playCombine)) {
} else {
dislike(phrase, dislikeCombine, "combineMove", "combine", "combined");
}
}
/**
* The various parts of the melody section of the GUI are checked to ascertain where a
* touch occurred (and therefore what action is required).
*/
private void melodySection() {
if (copyPhrase()) {
} else if (playMelody[0].onButton() && melody[0].getFileNumber() > 0) {
play(melody, playMelody);
} else if (saveMelody[0].onButton() && melody[0].getFileNumber() > 0) {
saveMelody();
} else if (clearMelody[0].onButton()&& melody[0].getFileNumber() > 0) {
clearMelody();
}
}
/**
* Tests whether or not a phrase button (in an array of such buttons) is being touched
* while it is a particular shade of green (i.e. while a phrase number is displayed on
* the button). If so, its buttonClicked and buttonPressed data fields are set to true,
* and buttonClicked is set to false for the other buttons in the array. Has the effect
* of ensuring that only one button in the array is selected at any one time.
*
* @param button An array of Button objects representing phrase buttons.
* @return true if an unselected green button (displaying a phrase number) is
* being touched, false otherwise.
*/
private boolean select(Button[] button) {
for (int i = 0; i < 4; i++) {
if (button[i].onButton() && button[i].getButtonGreen() == 170) {
button[i].buttonClicked = true;
button[i].buttonPressed = true;
button[i].setButtonTextPressed(button[i].getButtonText());
for (int k = 0; k < 4; k++) {
if (i != k) {
button[k].buttonClicked = false;
}
}
return true;
}
}
return false;
}
/**
* Tests whether or not a pitch generation button (in an array of such buttons) is being
* touched while the associated pitch phrase is unselected. If so, the pitch generation
* button's buttonClicked and buttonPressed data fields are set to true, a pitch phrase
* is generated, and the pitch phrase is saved as a MIDI file (the rhythm phrase used
* here comprises crotchets only). If the pitch phrase is rejected on the basis of
* rearrangement distance, however, new pitch phrases are generated until accepted.
*
* @return true if a pitch generation button is being touched while the associated pitch
* phrase is unselected, false otherwise.
*/
private boolean generatePitch() {
for (int i = 0; i < pitch.length; i++) {
if (genPitch[i].onButton() && !pitch[i].buttonClicked) {
pitchPhraseCount++;
genPitch[i].buttonClicked = true;
genPitch[i].buttonPressed = true;
boolean accepted = true;
int rejectCount = 0;
while (true) {
generate.run();
try {
accepted = music.update("pitchFile", "pitch", "pitches", "" +
pitchPhraseCount, "" + ++rejectCount, pitchPhrase,
rhythmPhrase, pPhraseArray, rPhraseArray, "", 1);
} catch (IOException e) {
}
if (accepted)
break;
}
pitch[i].setFileNumber(pitchPhraseCount);
pitch[i].setPitchPhrase(pitchPhrase);
return true;
}
}
return false;
}
/**
* Tests whether or not a play button (in an array of such buttons) is being touched
* while its associated phrase button is displaying a phrase number. If so, the play
* button's buttonClicked and buttonPressed data fields are set to true.
*
* @param button An array of Button objects representing phrase buttons.
* @param playButton An array of Button objects representing play buttons.
* @return true if a play button is being touched while its associated phrase
* button is displaying a phrase number, false otherwise.
*/
private boolean play(Button[] button, Button[] playButton) {
if (!playing()) {
for (int i = 0; i < button.length; i++) {
if (playButton[i].onButton() && button[i].getFileNumber() > 0) {
playButton[i].buttonClicked = true;
playButton[i].buttonPressed = true;
playButton[i].selected = true;
return true;
}
}
}
return false;
}
/**
* Tests whether or not any play button is selected (i.e. that a phrase or melody is
* currently being played).
*
* @return true if a play button is selected, false otherwise.
*/
private boolean playing() {
for (int i = 0; i < playPitch.length; i++) {
if (playPitch[i].selected || playRhythm[i].selected || playCombine[i].selected) {
return true;
}
}
if (playMelody[0].selected) {
return true;
} else {
return false;
}
}
/**
* Tests whether or not a dislike button (in an array of such buttons) is being touched
* while its associated unselected phrase button is displaying a phrase number. If so,
* the dislike button's buttonClicked and buttonPressed data fields are set to true,
* and the relevant MIDI file is moved to a dislike folder.
*
* @param button An array of Button objects representing phrase buttons.
* @param dislikeButton An array of Button objects representing dislike buttons.
* @param str The action required ("pitchMove", "rhythmMove" or "combineMove").
* @param fileType The file type ("pitch", "rhythm" or "combine").
* @param folder The relevant folder ("pitches", "rhythms" or "combined").
*
*/
private void dislike(Button[] button, Button[] dislikeButton, String str,
String fileType, String folder) {
for (int i = 0; i < button.length; i++) {
if (dislikeButton[i].onButton() && button[i].getFileNumber() > 0
&& !button[i].buttonClicked) {
dislikeButton[i].buttonClicked = true;
dislikeButton[i].buttonPressed = true;
try {
music.update(str, fileType, folder, "" + button[i].getFileNumber(), "0",
pitchPhrase, rhythmPhrase, pPhraseArray, rPhraseArray, "", 1);
} catch (IOException e) {
}
}
}
}
/**
* Tests whether or not a rhythm generation button (in an array of such buttons) is
* being touched while the associated rhythm phrase is unselected. If so, the rhythm
* generation button's buttonClicked and buttonPressed data fields are set to true, a
* rhythm phrase is randomly selected, and the rhythm phrase is saved as a MIDI file
* (the pitch phrase used here comprises middle Cs only). If the rhythm phrase is
* rejected on the basis of rearrangement distance, however, new rhythm phrases are
* randomly selected until accepted.
*
* @return true if a rhythm generation button is being touched while the associated
rhythm phrase is unselected, false otherwise.
*/
private boolean generateRhythm() {
for (int i = 0; i < rhythm.length; i++) {
if (genRhythm[i].onButton() && !rhythm[i].buttonClicked) {
rhythmPhraseCount++;
genRhythm[i].buttonClicked = true;
genRhythm[i].buttonPressed = true;
pitchPhrase = "60 60 60 60 60 60 60 60 60 60 60 60 60 60 60 60 ";
boolean accepted = true;
int rejectCount = 0;
while (true) {
double ran = 24*Math.random();
rhythmPhrase = rhythmArray[(int)ran];
try {
accepted = music.update("rhythmFile", "rhythm", "rhythms", "" +
rhythmPhraseCount, "" + ++rejectCount, pitchPhrase,
rhythmPhrase, pPhraseArray, rPhraseArray, "", 1);
} catch (IOException e) {
}
if (accepted)
break;
}
rhythm[i].setFileNumber(rhythmPhraseCount);
rhythm[i].setRhythmPhrase(rhythmPhrase);
return true;
}
}
return false;
}
/**
* Tests whether or not a pitch/rhythm combination button (in an array of such buttons)
* is being touched. If so, the relevant pitch and rhythm phrases are retrieved. If both
* are retrieved successfully the combine button's buttonClicked and buttonPressed data
* fields are set to true, and the combined pitch/rhythm phrase is saved as a MIDI file.
*
* @return true if a pitch/rhythm combination button is being touched, false otherwise.
*/
private boolean combine() {
for (int i = 0; i < combine.length; i++) {
if (combine[i].onButton()) {
pitchPhrase = getPitchPhrase(pitch);
rhythmPhrase = getRhythmPhrase(rhythm);
if (!pitchPhrase.equals("") && !rhythmPhrase.equals("")) {
combinePhraseCount++;
combine[i].buttonClicked = true;
combine[i].buttonPressed = true;
try {
music.update("combineFile", "combine", "combined", "" + combinePhraseCount,
"0", pitchPhrase, rhythmPhrase, pPhraseArray, rPhraseArray, "", 1);
} catch (IOException e) {
}
phrase[i].setFileNumber(combinePhraseCount);
phrase[i].setPitchPhrase(pitchPhrase);
phrase[i].setRhythmPhrase(rhythmPhrase);
}
return true;
}
}
return false;
}
/**
* Tests whether or not a melody phrase button (in an array of such buttons) is being
* touched. If so, The relevant pitch and rhythm phrases are retrieved. If both are
* retrieved successfully the melody button's buttonClicked and buttonPressed data
* fields are set to true, and the new combined phrase is added to the melody and saved
* as a MIDI file.
*
* @return true if a melody phrase button is being touched, false otherwise.
*/
private boolean copyPhrase() {
for (int i = 0; i < melody.length; i++) {
if (melody[i].onButton()) {
pitchPhrase = getPitchPhrase(phrase);
rhythmPhrase = getRhythmPhrase(phrase);
if (!pitchPhrase.equals("") && !rhythmPhrase.equals("")) {
melody[i].buttonClicked = true;
melody[i].buttonPressed = true;
melody[i].setPitchPhrase(pitchPhrase);
melody[i].setRhythmPhrase(rhythmPhrase);
for(int k = 0; k < melody.length; k++) {
melody[k].setFileNumber(melodyCount);
pPhraseArray[k] = getPitchPhrase(melody[k]);
rPhraseArray[k] = getRhythmPhrase(melody[k]);
}
try {
music.update("melodyFile", "melody", "melody", "" + melodyCount, "0",
pitchPhrase, rhythmPhrase, pPhraseArray, rPhraseArray, "", 4);
} catch (IOException e) {
}
}
return true;
}
}
return false;
}
/**
* The save button's buttonClicked and buttonPressed data fields are set to true, the
* melody rating is saved to a file, the previous melody becomes unchangeable, and the
* melody number is incremented such that a new melody can be started. The melody
* buttons are not reset to their initial state, however, unless the "clear" button is
* pressed. This allows for several related melodies to be created and saved.
*/
private void saveMelody() {
saveMelody[0].buttonClicked = true;
saveMelody[0].buttonPressed = true;
try {
music.update("saveRating", "save", "", "" + melodyCount, "0", pitchPhrase,
rhythmPhrase, pPhraseArray, rPhraseArray, rating.getRating(), 1);
} catch (IOException e) {
}
melodyCount++;
}
/**
* The clear button's buttonClicked and buttonPressed data fields are set to true. The
* melody buttons and this button are reset to their initial state elsewhere.
*/
private void clearMelody() {
clearMelody[0].buttonClicked = true;
clearMelody[0].buttonPressed = true;
}
/**
* If the clickedAndPressed method returns false, but one of the buttons has been
* clicked, the relevant MIDI file is played and the button's buttonClicked data field
* is set to false. This causes the button to be drawn in its unselected state.
*
* @param button An array of Button objects representing phrase buttons.
* @param play An array of Button objects representing play buttons.
* @param str The action required ("pitchPlay", "rhythmPlay", "combinePlay" or
* "melodyPlay").
* @param fileType The file type ("pitch", "rhythm", "combine" or "melody").
* @param folder The relevant folder ("pitches", "rhythms", "combined" or "melody").
*/
private void resetPlay(Button[] button, Button[] play, String str, String fileType,
String folder) {
if (clickedAndPressed(play)) {
} else {
for (int i = 0; i < play.length; i++) {
if (play[i].buttonClicked) {
try {
music.update(str, fileType, folder, "" + button[i].getFileNumber(), "0",
pitchPhrase, rhythmPhrase, pPhraseArray, rPhraseArray, "", 1);
} catch (IOException e) {
}
play[i].buttonClicked = false;
break;
}
}
}
}
/**
* If one of the buttons is deemed to be clicked and pressed, the button's buttonPressed
* data field is set to false. With buttonClicked remaining true, the button is drawn in
* its selected state.
*
* @param button An array of Button objects.
* @return true if a button is clicked and pressed, otherwise false.
*/
private boolean clickedAndPressed(Button[] button) {
for (int i = 0; i < button.length; i++) {
if (button[i].buttonClicked && button[i].buttonPressed) {
button[i].buttonPressed = false;
return true;
}
}
return false;
}
/**
* If the clickedAndPressed method returns false, but one of the generation buttons has
* been clicked, after a 0.5s pause the generation button's buttonClicked data field is
* set to false. If the associated phrase button is selected, its buttonClicked data
* field is also set to false. This causes the buttons to be drawn in their unselected
* state.
*
* @param phrase An array of Button objects representing phrase buttons.
* @param gen An array of Button objects representing phrase generation buttons.
* @param count The phrase number (e.g. for file 1.mid, count = 1).
*/
private void resetGen(Button[] phrase, Button[] gen, int count) {
if (clickedAndPressed(gen)) {
} else {
for (int i = 0; i < gen.length; i++) {
if (gen[i].buttonClicked) {
delay(500);
phrase[i].setButtonColour(0, 170, 0);
phrase[i].setButtonText(count);
phrase[i].setTextX(count);
gen[i].buttonClicked = false;
if (phrase[i].selected) {
phrase[i].buttonClicked = false;
}
break;
}
}
}
}
/**
* If the clickedAndPressed method returns false, but one of the combine buttons has
* been clicked, after a 0.5s pause the phrase and combine buttons' buttonClicked data
* fields are set to false. In addition, all pitch and rhythm phrase buttons'
* buttonClicked data fields are set to false. This causes the buttons to be drawn in
* their unselected state.
*/
private void resetCombine() {
if (clickedAndPressed(combine)) {
} else {
for (int i = 0; i < combine.length; i++) {
if (combine[i].buttonClicked) {
delay(500);
phrase[i].setButtonColour(0, 170, 0);
phrase[i].setButtonText(combinePhraseCount);
phrase[i].setTextX(combinePhraseCount);
phrase[i].buttonClicked = false;
combine[i].buttonClicked = false;
for (int k = 0; k < pitch.length; k++) {
pitch[k].buttonClicked = false;
rhythm[k].buttonClicked = false;
}
break;
}
}
}
}
/**
* If the clickedAndPressed method returns false, but one of the melody phrase buttons
* has been clicked, after a 0.5s pause the melody phrase button's buttonClicked data
* field is set to false. This causes the button to be drawn in its unselected state.
* In addition, all combined pitch/rhythm buttons are unselected.
*/
private void resetMelody() {
if (clickedAndPressed(melody)) {
} else {
for (int i = 0; i < melody.length; i++) {
if (melody[i].buttonClicked) {
delay(500);
melody[i].setButtonColour(0, 170, 0);
melody[i].buttonClicked = false;
melody[i].setButtonText(getText(phrase));
melody[i].setTextX(getPhraseNumber(phrase));
unselect(phrase);
break;
}
}
}
}
/**
* If a button is selected its text is returned, otherwise "" is returned.
*
* @param phr An array of Button objects.
* @return Button text of a selected button, otherwise "".
*/
private String getText(Button[] phr) {
for (int i = 0; i < phr.length; i++) {
if (phrase[i].selected) {
return phrase[i].getButtonText();
}
}
return "";
}
/**
* Selected buttons have their buttonClicked data fields set to false. This causes the
* buttons to be drawn in their unselected state.
*
* @param b An array of Button objects.
*/
private void unselect(Button[] b) {
for (int i = 0; i < b.length; i++) {
if (b[i].selected) {
b[i].buttonClicked = false;
}
}
}
/**
* If the clickedAndPressed method returns false, but one of the dislike buttons has
* been clicked, after a 0.5s pause the dislike button's buttonClicked data field is
* set to false. This causes the button to be drawn in its unselected state.
*
* @param phrase An array of Button objects representing phrase buttons.
* @param dislike An array of Button objects representing dislike buttons.
*/
private void resetDislike(Button[] phrase, Button[] dislike) {
if (clickedAndPressed(dislike)) {
} else {
for (int i = 0; i < phrase.length; i++) {
if (dislike[i].buttonClicked) {
delay(500);
phrase[i].setFileNumber(0);
phrase[i].setButtonColour(170, 0, 0);
phrase[i].setButtonText(0);
phrase[i].setTextX(0);
dislike[i].buttonClicked = false;
break;
}
}
}
}
/**
* If the clickedAndPressed method returns false, but the save button has been clicked,
* after a 0.5s pause the save button's buttonClicked data field is set to false. This
* causes the button to be drawn in its unselected state.
*/
private void resetSave() {
if (clickedAndPressed(saveMelody)) {
} else if (saveMelody[0].buttonClicked) {
delay(500);
saveMelody[0].buttonClicked = false;
}
}
/**
* If the clickedAndPressed method returns false, but one of the dislike buttons has
* been clicked, after a 0.5s pause the melody buttons are reset and the dislike
* button's buttonClicked data field is set to false. This causes the buttons to be
* drawn in their unselected state.
*/
private void resetClear() {
if (clickedAndPressed(clearMelody)) {
} else if (clearMelody[0].buttonClicked) {
delay(500);
for (int i = 0; i < melody.length; i++) {
melody[i].setFileNumber(0);
melody[i].setButtonColour(170, 0, 0);
melody[i].setButtonText(0);
melody[i].setTextX(0);
melody[i].setPitchPhrase("");
melody[i].setRhythmPhrase("");
}
clearMelody[0].buttonClicked = false;
}
}
/**
* The relevant phrase number is retrieved and returned if a button is selected,
* otherwise 0 is returned.
*
* @param phrase An array of Button objects representing pitch phrase buttons.
* @return An integer representing a phrase number if a button is selected,
* otherwise 0.
*/
private int getPhraseNumber(Button[] phrase) {
for (int i = 0; i < phrase.length; i++) {
if (phrase[i].selected) {
return phrase[i].getFileNumber();
}
}
return 0;
}
/**
* The relevant pitch phrase is retrieved. If the retrieval is successful it is
* returned, otherwise "0 " is returned.
*
* @param phrase A Button object representing a pitch phrase button.
* @return A string representing a pitch phrase if it exists, otherwise "0 ".
*/
private String getPitchPhrase(Button phrase) {
String pitchPhrase = phrase.getPitchPhrase();
if (pitchPhrase.equals("")) {
pitchPhrase = "0 ";
}
return pitchPhrase;
}
/**
* The relevant pitch phrase is retrieved and returned if a button is selected,
* otherwise "" is returned.
*
* @param phrase An array of Button objects representing pitch phrase buttons.
* @return A string representing a pitch phrase if a button is selected,
* otherwise "".
*/
private String getPitchPhrase(Button[] phrase) {
for (int i = 0; i < phrase.length; i++) {
if (phrase[i].selected) {
return phrase[i].getPitchPhrase();
}
}
return "";
}
/**
* The relevant rhythm phrase is retrieved. If the retrieval is successful it is
* returned, otherwise "96 " is returned.
*
* @param phrase A Button object representing a rhythm phrase button.
* @return A string representing a rhythm phrase if it exists, otherwise "96 ".
*/
private String getRhythmPhrase(Button phrase) {
String rhythmPhrase = phrase.getRhythmPhrase();
if (rhythmPhrase.equals("")) {
rhythmPhrase = "96 ";
}
return rhythmPhrase;
}
/**
* The relevant rhythm phrase is retrieved and returned if a button is selected,
* otherwise "" is returned.
*
* @param phrase An array of Button objects representing rhythm phrase buttons.
* @return A string representing a rhythm phrase if a button is selected,
* otherwise "".
*/
private String getRhythmPhrase(Button[] phrase) {
for (int i = 0; i < phrase.length; i++) {
if (phrase[i].selected) {
return phrase[i].getRhythmPhrase();
}
}
return "";
}
/**
* Slider used for rating a melody from 0 to 10. The rating displayed on the slider
* changes as it traverses the scale.
*
* @author Ray Whorley
* @version 1.3, 30th March 2020
*/
class Slider {
/**
* Horizontal coordinate of the scale on the screen.
*/
private int x;
/**
* Vertical coordinate of the scale on the screen.
*/
private int y;
/**
* Horizontal coordinate of the slider (top left) on the screen.
*/
private int sliderX;
/**
* Vertical coordinate of the slider (top left) on the screen.
*/
private int sliderY;
/**
* Horizontal dimension of the slider.
*/
private int sliderWidth;
/**
* Vertical dinension of the slider.
*/
private int sliderHeight;
/**
* Rating displayed on the slider, with initial value of 5.
*/
private String rating = "5";
/**
* Creates a new Slider instance from integer arguments representing the horizontal
* and vertical positions of the scale on the screen.
*
* @param positionX An integer representing the horizontal position of the scale on
* the screen.
* @param positionY An integer representing the vertical position of the scale on
* the screen.
*/
public Slider(int positionX, int positionY) {
x = positionX;
y = positionY;
sliderX = round((x + 51)*wFactor);
sliderY = round((y - 8)*hFactor);
sliderWidth = round(17*wFactor);
sliderHeight = round(17*hFactor);
}
/**
* Draws the slider on the screen, including updates to the rating as it is moved.
*/
public void drawSlider() {
int xScale = round(x*wFactor);
int yScale = round(y*hFactor);
int scaleWidth = round(120*wFactor);
int scaleHeight = round(2*hFactor);
fill(0, 170, 0); // Horizontal scale line.
stroke(0);
rect(xScale, yScale, scaleWidth, scaleHeight);
stroke(0, 170, 0); // Eleven vertical scale lines representing 0 to 10.
for (int i = 0; i < 121; i += 12) {
line((x + i)*wFactor, (y - 2)*hFactor, (x + i)*wFactor, (y + 5)*hFactor);
}
stroke(0); // Slider.
fill(0, 190, 0);
rect(sliderX, sliderY, sliderWidth, sliderHeight);
fill(255); // Slider text.
textSize(round(12*wFactor));
if (rating.equals("10")) {
text(rating, sliderX + round(1.0*wFactor), round((y + 5.5)*hFactor));
} else {
text(rating, sliderX + round(5.5*wFactor), round((y + 5.5)*hFactor));
}
}
/**
* Tests whether or not the slider is being touched. Touch position and mouse position
* are synonymous.
*
* @return true if the slider is being touched, false otherwise.
*/
public boolean onSlider() {
if (mouseX >= sliderX && mouseX <= sliderX + sliderWidth &&
mouseY >= sliderY && mouseY <= sliderY + sliderHeight) {
return true;
} else {
return false;
}
}
/**
* Sets the horizontal position of the slider on the screen.
*
* @param x An integer representing the horizontal position of the slider on the
* screen.
*/
public void setSliderX(int x) {
sliderX = x;
}
/**
* Returns the left-hand position of the scale on the screen.
*
* @return An integer representing the left-hand position of the scale on the screen.
*/
public int getLeft() {
return round(x*wFactor);
}
/**
* Returns the right-hand position of the scale on the screen.
*
* @return An integer representing the right-hand position of the scale on the screen.
*/
public int getRight() {
return round((x + 120)*wFactor);
}
/**
* Sets the numerical rating to be displayed on the slider.
*
* @param s A string representing the numerical rating to be displayed on the slider.
*/
public void setRating(String s) {
rating = s;
}
/**
* Returns the numerical rating displayed on the slider.
*
* @return A string representing the numerical rating displayed on the slider.
*/
public String getRating() {
return rating;
}
}
/**
* Button can be coloured, sized, positioned and its text specified by the class user.
* When touched (pressed) its text and brightness changes, it appears to move downwards,
* and an action is initiated depending on the type of button ("generate", "play",
* etc.).
*
* @author Ray Whorley
* @version 1.3, 30th March 2020
*/
abstract class Button {
/**
* The normal colour of the button.
*/
private color buttonColour;
/**
* Horizontal coordinate of the top left-hand corner of the button on the screen.
*/
private int buttonX;
/**
* Vertical coordinate of the top left-hand corner of the button on the screen.
*/
private int buttonY;
/**
* Width of the button.
*/
private int buttonWidth;
/**
* Height of the button.
*/
private int buttonHeight;
/**
* Red component of the normal button colour.
*/
private int buttonRed;
/**
* Green component of the normal button colour.
*/
private int buttonGreen;
/**
* Blue component of the normal button colour.
*/
private int buttonBlue;
/**
* Button font size.
*/
private int buttonFontSize;
/**
* Normal button text colour.
*/
private int buttonTextColour;
/**
* Vertical coordinate of the button text on the screen.
*/
private int textY;
/**
* Red component of the button when it is pressed.
*/
private int buttonRedPressed;
/**
* Green component of the button when it is pressed.
*/
private int buttonGreenPressed;
/**
* Blue component of the button when it is pressed.
*/
private int buttonBluePressed;
/**
* Button text colour when it is pressed.
*/
private int buttonTextColourPressed;
/**
* MIDI file number currently associated with the button.
*/
private int fileNumber = 0;
/**
* Button text when the button is pressed.
*/
private String buttonTextPressed;
/**
* Representation of the pitch phrase currently associated with the button.
*/
private String pitchPhrase = "";
/**
* Representation of the rhythm phrase currently associated with the button.
*/
private String rhythmPhrase = "";
/**
* Horizontal coordinate of the button text on the screen.
*/
protected int textX;
/**
* Horizontal coordinate of the button text on the screen when the button is pressed.
*/
protected int textXpressed;
/**
* true if button is pressed, false otherwise.
*/
protected boolean buttonPressed = false;
/**
* true if button is clicked, false otherwise.
*/
protected boolean buttonClicked = false;
/**
* true if button is selected, false otherwise.
*/
protected boolean selected = false;
/**
* Normal button text.
*/
protected String buttonText;
/**
* Creates a new Button instance from the following parameters.
*
* @param bX Fraction of the screen width at which the top
* left-hand corner of the button is situated.
* @param bY Fraction of the screen height at which the top
* left-hand corner of the button is situated.
* @param bWidth Button width.
* @param bHeight Button height.
* @param bRed Red component of the normal button colour.
* @param bGreen Green component of the normal button colour.
* @param bBlue Blue component of the normal button colour.
* @param bFontSize Button font size.
* @param bTextColour Normal button text colour (greyscale).
* @param bText Normal button text.
* @param tX Fraction of the screen width at which the button
* text is situated.
* @param tY Fraction of the screen height at which the button
* text is situated.
* @param bRedPressed Red component of the button colour when pressed.
* @param bGreenPressed Green component of the button colour when pressed.
* @param bBluePressed Blue component of the button colour when pressed.
* @param bTextColourPressed Button text colour (greyscale) when pressed.
* @param bTextPressed Button text when pressed.
* @param tXpressed Fraction of the screen width at which the button
* text is situated when pressed.
*/
public Button(float bX, float bY, int bWidth, int bHeight, int bRed, int bGreen,
int bBlue, int bFontSize, int bTextColour, String bText, float tX,
float tY, int bRedPressed, int bGreenPressed, int bBluePressed,
int bTextColourPressed, String bTextPressed, float tXpressed) {
buttonX = round(bX*width);
buttonY = round(bY*height);
buttonWidth = round(bWidth*wFactor);
buttonHeight = round(bHeight*hFactor);
buttonRed = bRed;
buttonGreen = bGreen;
buttonBlue = bBlue;
buttonFontSize = round(bFontSize*wFactor);
buttonTextColour = bTextColour;
buttonText = bText;
textX = round(tX*width);
textY = round(tY*height);
buttonRedPressed = bRedPressed;
buttonGreenPressed = bGreenPressed;
buttonBluePressed = bBluePressed;
buttonTextColourPressed = bTextColourPressed;
buttonTextPressed = bTextPressed;
textXpressed = round(tXpressed*width);
}
/**
* Draws the button on the screen, including updates as the button is touched
* (pressed) and updates (as applicable) when another button is touched. Such updates
* include change of colour, text, brightness and position (the appearance of moving
* downwards).
*/
public void drawButton() {
noFill();
if (buttonClicked) {
selected = true;
buttonColour = color(buttonRedPressed, buttonGreenPressed, buttonBluePressed);
fill(buttonColour);
stroke(0);
rect(buttonX - 3, buttonY + 3, buttonWidth, buttonHeight);
fill(buttonTextColourPressed);
textSize(buttonFontSize);
text(buttonTextPressed, textXpressed, textY + 3);
if (buttonRed >= 170 || buttonTextPressed.equals("moving...")) {
stroke(212, 158, 0);
} else if (buttonBlue >= 170) {
stroke(0, 158, 212);
} else if (buttonGreen >= 170) {
stroke(0, 212, 158);
}
line(buttonX - 3, buttonY + 3, buttonX + buttonWidth - 4, buttonY + 3);
if (buttonRed >= 170 || buttonTextPressed.equals("moving...")) {
stroke(255, 190, 0);
} else if (buttonBlue >= 170) {
stroke(0, 190, 255);
} else if (buttonGreen >= 170) {
stroke(0, 255, 190);
}
line(buttonX + buttonWidth - 4, buttonY + 3,
buttonX + buttonWidth - 4, buttonY + buttonHeight + 3);
} else {
selected = false;
buttonColour = color(buttonRed, buttonGreen, buttonBlue);
fill(buttonColour);
stroke(0);
rect(buttonX, buttonY, buttonWidth, buttonHeight);
fill(buttonTextColour);
textSize(buttonFontSize);
text(buttonText, textX, textY);
if (buttonRed >= 170) {
stroke(255, 190, 0);
} else if (buttonBlue >= 170) {
stroke(0, 190, 255);
} else if (buttonGreen >= 170) {
stroke(0, 255, 190);
}
line(buttonX, buttonY + 2, buttonX, buttonY + buttonHeight);
line(buttonX - 1, buttonY + 3, buttonX - 1, buttonY + buttonHeight + 1);
line(buttonX - 2, buttonY + 4, buttonX - 2, buttonY + buttonHeight + 2);
if (buttonRed >= 170) {
stroke(212, 158, 0);
} else if (buttonBlue >= 170) {
stroke(0, 158, 212);
} else if (buttonGreen >= 170) {
stroke(0, 212, 158);
}
line(buttonX, buttonY + buttonHeight,
buttonX + buttonWidth - 1, buttonY + buttonHeight);
line(buttonX - 1, buttonY + buttonHeight + 1,
buttonX + buttonWidth - 2, buttonY + buttonHeight + 1);
line(buttonX - 2, buttonY + buttonHeight + 2,
buttonX + buttonWidth - 3, buttonY + buttonHeight + 2);
}
}
/**
* Tests whether or not the button is being touched. Touch position and mouse position
* are synonymous.
*
* @return true if the button is being touched, false otherwise.
*/
public boolean onButton() {
if (mouseX >= buttonX && mouseX <= buttonX + buttonWidth &&
mouseY >= buttonY && mouseY <= buttonY + buttonHeight) {
return true;
} else {
return false;
}
}
/**
* Sets the number used to form a MIDI file name (e.g. 1.mid).
*
* @param i An integer representing a number used in a file name.
*/
public void setFileNumber(int i) {
fileNumber = i;
}
/**
* Returns the number used to form a MIDI file name (e.g. 1.mid).
*
* @return An integer representing a number used in a file name.
*/
public int getFileNumber() {
return fileNumber;
}
/**
* Returns the green component of the current button colour.
*
* @return An integer representing the green component of the current button colour.
*/
public int getButtonGreen() {
return buttonGreen;
}
/**
* Sets the normal button text according to the value of the argument.
*
* @param i An integer representing a phrase number (0 if empty phrase).
*/
public abstract void setButtonText(int i);
/**
* Sets the normal button text.
*
* @param s A string representing the normal button text.
*/
public void setButtonText(String s) {
buttonText = s;
}
/**
* Returns the normal button text.
*
* @return A string representing the normal button text.
*/
public String getButtonText() {
return buttonText;
}
/**
* Sets the button text when pressed.
*
* @param s A string representing the button text when pressed.
*/
public void setButtonTextPressed(String s) {
buttonTextPressed = s;
}
/**
* Sets the horizontal position of the screen at which the button text is situated,
* according to the value of the argument.
*
* @param i An integer representing a phrase number (0 if empty phrase).
*/
public abstract void setTextX(int i);
/**
* Sets the normal button colour.
*
* @param r An integer representing the red component of the normal button colour.
* @param g An integer representing the green component of the normal button colour.
* @param b An integer representing the blue component of the normal button colour.
*/
public void setButtonColour(int r, int g, int b) {
buttonRed = r;
buttonGreen = g;
buttonBlue = b;
}
/**
* Sets the current pitch phrase.
*
* @param str A string representing the current pitch phrase.
*/
public void setPitchPhrase(String str){
pitchPhrase = str;
}
/**
* Returns the current pitch phrase.
*
* @return A string representing the current pitch phrase.
*/
public String getPitchPhrase() {
return pitchPhrase;
}
/**
* Sets the current rhythm phrase.
*
* @param str A string representing the current rhythm phrase.
*/
public void setRhythmPhrase(String str){
rhythmPhrase = str;
}
/**
* Returns the current rhythm phrase.
*
* @return A string representing the current rhythm phrase.
*/
public String getRhythmPhrase() {
return rhythmPhrase;
}
}
/**
* PitchButton can be coloured, sized, positioned and its text specified by the class
* user. When touched (pressed) its text and brightness changes, it appears to move
* downwards, and an action is initiated depending on the type of button ("generate",
* "play", etc.).
*
* @author Ray Whorley
* @version 1.3, 30th March 2020
*/
class PitchButton extends Button {
/**
* Creates a new Button instance from the following parameters.
*
* @param bX Fraction of the screen width at which the top
* left-hand corner of the button is situated.
* @param bY Fraction of the screen height at which the top
* left-hand corner of the button is situated.
* @param bWidth Button width.
* @param bHeight Button height.
* @param bRed Red component of the normal button colour.
* @param bGreen Green component of the normal button colour.
* @param bBlue Blue component of the normal button colour.
* @param bFontSize Button font size.
* @param bTextColour Normal button text colour (greyscale).
* @param bText Normal button text.
* @param tX Fraction of the screen width at which the button
* text is situated.
* @param tY Fraction of the screen height at which the button
* text is situated.
* @param bRedPressed Red component of the button colour when pressed.
* @param bGreenPressed Green component of the button colour when pressed.
* @param bBluePressed Blue component of the button colour when pressed.
* @param bTextColourPressed Button text colour (greyscale) when pressed.
* @param bTextPressed Button text when pressed.
* @param tXpressed Fraction of the screen width at which the button
* text is situated when pressed.
*/
public PitchButton(float bX, float bY, int bWidth, int bHeight, int bRed, int bGreen,
int bBlue, int bFontSize, int bTextColour, String bText, float tX,
float tY, int bRedPressed, int bGreenPressed, int bBluePressed,
int bTextColourPressed, String bTextPressed, float tXpressed) {
super(bX, bY, bWidth, bHeight, bRed, bGreen, bBlue, bFontSize, bTextColour, bText,
tX, tY, bRedPressed, bGreenPressed, bBluePressed, bTextColourPressed,
bTextPressed, tXpressed);
}
/**
* Sets the normal button text according to the value of the argument.
*
* @param i An integer representing a phrase number (0 if empty pitches).
*/
public void setButtonText(int i) {
if (i > 0) {
buttonText = "pitch phrase " + i;
} else {
buttonText = "empty pitches";
}
}
/**
* Sets the horizontal position of the screen at which the button text is situated,
* according to the value of the argument (textXpressed also updated).
*
* @param i An integer representing a phrase number (0 if empty pitches).
*/
public void setTextX(int i) {
if (i > 0 && i < 10) {
textX = round(0.077*width);
} else if (i > 9) {
textX = round(0.068*width);
} else {
textX = round(0.078*width);
}
textXpressed = textX - 3;
}
}
/**
* RhythmButton can be coloured, sized, positioned and its text specified by the class
* user. When touched (pressed) its text and brightness changes, it appears to move
* downwards, and an action is initiated depending on the type of button ("generate",
* "play", etc.).
*
* @author Ray Whorley
* @version 1.3, 30th March 2020
*/
class RhythmButton extends Button {
/**
* Creates a new Button instance from the following parameters.
*
* @param bX Fraction of the screen width at which the top
* left-hand corner of the button is situated.
* @param bY Fraction of the screen height at which the top
* left-hand corner of the button is situated.
* @param bWidth Button width.
* @param bHeight Button height.
* @param bRed Red component of the normal button colour.
* @param bGreen Green component of the normal button colour.
* @param bBlue Blue component of the normal button colour.
* @param bFontSize Button font size.
* @param bTextColour Normal button text colour (greyscale).
* @param bText Normal button text.
* @param tX Fraction of the screen width at which the button
* text is situated.
* @param tY Fraction of the screen height at which the button
* text is situated.
* @param bRedPressed Red component of the button colour when pressed.
* @param bGreenPressed Green component of the button colour when pressed.
* @param bBluePressed Blue component of the button colour when pressed.
* @param bTextColourPressed Button text colour (greyscale) when pressed.
* @param bTextPressed Button text when pressed.
* @param tXpressed Fraction of the screen width at which the button
* text is situated when pressed.
*/
public RhythmButton(float bX, float bY, int bWidth, int bHeight, int bRed, int bGreen,
int bBlue, int bFontSize, int bTextColour, String bText, float tX,
float tY, int bRedPressed, int bGreenPressed, int bBluePressed,
int bTextColourPressed, String bTextPressed, float tXpressed) {
super(bX, bY, bWidth, bHeight, bRed, bGreen, bBlue, bFontSize, bTextColour, bText,
tX, tY, bRedPressed, bGreenPressed, bBluePressed, bTextColourPressed,
bTextPressed, tXpressed);
}
/**
* Sets the normal button text according to the value of the argument.
*
* @param i An integer representing a phrase number (0 if empty rhythm).
*/
public void setButtonText(int i) {
if (i > 0) {
buttonText = "rhythm phrase " + i;
} else {
buttonText = "empty rhythm";
}
}
/**
* Sets the horizontal position of the screen at which the button text is situated,
* according to the value of the argument (textXpressed also updated).
*
* @param i An integer representing a phrase number (0 if empty rhythm).
*/
public void setTextX(int i) {
if (i > 0 && i < 10) {
textX = round(0.062*width);
} else if (i > 9) {
textX = round(0.052*width);
} else {
textX = round(0.078*width);
}
textXpressed = textX - 3;
}
}
/**
* CombineButton can be coloured, sized, positioned and its text specified by the class
* user. When touched (pressed) its text and brightness changes, it appears to move
* downwards, and an action is initiated depending on the type of button ("generate",
* "play", etc.).
*
* @author Ray Whorley
* @version 1.3, 30th March 2020
*/
class CombineButton extends Button {
/**
* Creates a new Button instance from the following parameters.
*
* @param bX Fraction of the screen width at which the top
* left-hand corner of the button is situated.
* @param bY Fraction of the screen height at which the top
* left-hand corner of the button is situated.
* @param bWidth Button width.
* @param bHeight Button height.
* @param bRed Red component of the normal button colour.
* @param bGreen Green component of the normal button colour.
* @param bBlue Blue component of the normal button colour.
* @param bFontSize Button font size.
* @param bTextColour Normal button text colour (greyscale).
* @param bText Normal button text.
* @param tX Fraction of the screen width at which the button
* text is situated.
* @param tY Fraction of the screen height at which the button
* text is situated.
* @param bRedPressed Red component of the button colour when pressed.
* @param bGreenPressed Green component of the button colour when pressed.
* @param bBluePressed Blue component of the button colour when pressed.
* @param bTextColourPressed Button text colour (greyscale) when pressed.
* @param bTextPressed Button text when pressed.
* @param tXpressed Fraction of the screen width at which the button
* text is situated when pressed.
*/
public CombineButton(float bX, float bY, int bWidth, int bHeight, int bRed, int bGreen,
int bBlue, int bFontSize, int bTextColour, String bText, float tX,
float tY, int bRedPressed, int bGreenPressed, int bBluePressed,
int bTextColourPressed, String bTextPressed, float tXpressed) {
super(bX, bY, bWidth, bHeight, bRed, bGreen, bBlue, bFontSize, bTextColour, bText,
tX, tY, bRedPressed, bGreenPressed, bBluePressed, bTextColourPressed,
bTextPressed, tXpressed);
}
/**
* Sets the normal button text according to the value of the argument.
*
* @param i An integer representing a phrase number (0 if empty phrase).
*/
public void setButtonText(int i) {
if (i > 0) {
buttonText = "full phrase " + i;
} else {
buttonText = "empty phrase";
}
}
/**
* Sets the horizontal position of the screen at which the button text is situated,
* according to the value of the argument (textXpressed also updated).
*
* @param i An integer representing a phrase number (0 if empty phrase).
*/
public void setTextX(int i) {
if (i > 0 && i < 10) {
textX = round(0.089*width);
} else if (i > 9) {
textX = round(0.082*width);
} else {
textX = round(0.081*width);
}
textXpressed = textX - 3;
}
}
/**
* MelodyButton can be coloured, sized, positioned and its text specified by the class
* user. When touched (pressed) its text and brightness changes, it appears to move
* downwards, and an action is initiated depending on the type of button ("play",
* "save", etc.).
*
* @author Ray Whorley
* @version 1.3, 30th March 2020
*/
class MelodyButton extends Button {
/**
* Creates a new Button instance from the following parameters.
*
* @param bX Fraction of the screen width at which the top
* left-hand corner of the button is situated.
* @param bY Fraction of the screen height at which the top
* left-hand corner of the button is situated.
* @param bWidth Button width.
* @param bHeight Button height.
* @param bRed Red component of the normal button colour.
* @param bGreen Green component of the normal button colour.
* @param bBlue Blue component of the normal button colour.
* @param bFontSize Button font size.
* @param bTextColour Normal button text colour (greyscale).
* @param bText Normal button text.
* @param tX Fraction of the screen width at which the button
* text is situated.
* @param tY Fraction of the screen height at which the button
* text is situated.
* @param bRedPressed Red component of the button colour when pressed.
* @param bGreenPressed Green component of the button colour when pressed.
* @param bBluePressed Blue component of the button colour when pressed.
* @param bTextColourPressed Button text colour (greyscale) when pressed.
* @param bTextPressed Button text when pressed.
* @param tXpressed Fraction of the screen width at which the button
* text is situated when pressed.
*/
public MelodyButton(float bX, float bY, int bWidth, int bHeight, int bRed, int bGreen,
int bBlue, int bFontSize, int bTextColour, String bText, float tX,
float tY, int bRedPressed, int bGreenPressed, int bBluePressed,
int bTextColourPressed, String bTextPressed, float tXpressed) {
super(bX, bY, bWidth, bHeight, bRed, bGreen, bBlue, bFontSize, bTextColour, bText,
tX, tY, bRedPressed, bGreenPressed, bBluePressed, bTextColourPressed,
bTextPressed, tXpressed);
}
/**
* Sets the normal button text. The argument is ignored.
*
* @param i An integer representing a phrase number (0 if empty phrase).
*/
public void setButtonText(int i) {
buttonText = "empty phrase";
}
/**
* Sets the horizontal position of the screen at which the button text is situated,
* according to the value of the argument.
*
* @param i An integer representing a phrase number (0 if empty phrase).
*/
public void setTextX(int i) {
for (int k = 0; k < melody.length; k++) {
if (this.equals(melody[k])) {
if (i > 0 && i < 10) {
textX = round((0.075 + 0.229*k)*width);
} else if (i > 9) {
textX = round((0.067 + 0.229*k)*width);
} else {
textX = round((0.067 + 0.229*k)*width);
}
}
}
}
}
/**
Builds on generator2.js, taken from the supplementary material of
Milne, Andrew J.; Laney, Robin and Sharp, David B. (2016). Testing a spectral model
of tonal affinity with microtonal melodies and inharmonic spectra. Musicae
Scientiae, 20(4) pp. 465-494.
Calculates musical pitch transition probabilities and generates pitch sequences by
random sampling of the probability distributions.
@author Ray Whorley
@version 1.4, 22nd June 2020
*/
class Generator {
/**
* The length of the chain of generating intervals (see beta, below).
*/
private int r = 12;
/**
* The size of the interval of repetition in cents. 1200 cents is the standard
* octave.
*/
private int alpha = 1200;
/**
* The size of the generating interval (700 cents gives 12-TET, 300 cents gives 4-TET,
* 720 cents gives 5-TET, etc.). 12-TET is twelve-tone equal temperament, which is the
* standard tuning system. 700 cents is a perfect fifth.
*/
private int beta = 700;
/**
* Minimum value in 'for' loops in the constructor (-6 for r = 12).
*/
private int jkmin = (r + 1)/2 - r;
/**
* Maximum value in 'for' loops in the constructor (5 for r = 12).
*/
private int jkmax = r - (r + 2)/2;
/**
* Previous note1, used to determine when note1 changes value (note1 is the first note
* of a transition from one note to another).
*/
private int oldNote1 = 100000;
/**
* Used as an index in certain arrays.
*/
private int count = 0;
/**
* Array of note2s (note2 is the second note of a transition from one note to
* another).
*/
private int[] note2Array = new int[144];
/**
* Constant used in the determination of factor1.
*/
private double s_1 = 6.0d;
/**
* Constant used in the determination of factor2.
*/
private double s_2 = 6.5d;
/**
* Constant used in the determination of factor3.
*/
private double s_3 = 1200.0d;
/**
* Constant used in the determination of factor4.
*/
private double s_4 = 1000.0d;
/**
* Sum of note2 probabilities. Used to normalise distributions.
*/
private double sum = 0.0d;
/**
* Note2 probability factor.
*/
private double factor1;
/**
* Note2 probability factor.
*/
private double factor2;
/**
* Note2 probability factor.
*/
private double factor3;
/**
* Note2 probability factor.
*/
private double factor4;
/**
* Array of note2 probabilities.
*/
private double[] pArray = new double[144];
/**
* A probability distribution: note2s (integer) are mapped to their probability
* (double).
*/
private HashMap distrib = new HashMap();
/**
* Note1s (integer) are mapped to their note2 probability distributions (as distrib
* above).
*/
private HashMap> distns
= new HashMap>();
/**
* Creates a new Generator instance and calculates note transition probability
* distributions.
*/
public Generator() {
for (int j_1 = jkmin; j_1 <= jkmax; j_1 = j_1+1)
for (int k_1 = jkmin; k_1 <= jkmax; k_1 = k_1+1)
for (int j_2 = jkmin; j_2 <= jkmax; j_2 = j_2+1)
for (int k_2 = jkmin; k_2 <= jkmax; k_2 = k_2+1) {
double mu_1 = (double)jkmin + (double)(r-1)/2.0d;
int mu_2 = k_1;
int mu_3 = 1200;
int mu_4 = j_1*alpha + k_1*beta;
double factor1A = 1.0d - Math.abs((double)k_2 - mu_1)/s_1;
if (factor1A >= 0 && factor1A <= 1)
factor1 = factor1A;
else
factor1 = 0.0d;
double factor2A = 1.0 - Math.abs((double)(k_2 - mu_2))/s_2;
if (factor2A >= 0 && factor2A <= 1)
factor2 = factor2A;
else
factor2 = 0.0d;
double factor3A = 1.0 - Math.abs((double)(j_2*alpha + k_2*beta - mu_3))/s_3;
if (factor3A >= 0 && factor3A <= 1)
factor3 = factor3A;
else
factor3 = 0.0d;
double factor4A = 1.0 - Math.abs((double)(j_2*alpha + k_2*beta - mu_4))/s_4;
if (factor4A >= 0 && factor4A <= 1)
factor4 = factor4A;
else
factor4 = 0.0d;
int note1 = 60 + (j_1*alpha + k_1*beta)/100;
int note2 = 60 + (j_2*alpha + k_2*beta)/100;
HashMap distn = new HashMap();
if (note1 != oldNote1) {
if (sum > 0.0d)
for (int i = 0; i < pArray.length; i++) {
if (pArray[i] > 0.0d) {
pArray[i] = pArray[i]/sum;
distn.put(note2Array[i], pArray[i]);
}
}
distns.put(oldNote1, distn);
oldNote1 = note1;
count = 0;
sum = 0.0d;
}
pArray[count] = factor1 * factor2 * factor3 * factor4;
note2Array[count] = note2;
sum += pArray[count];
count++;
}
}
/**
* Generates a pitch sequence by random sampling of note transition probability
* distributions (except for three consecutive pitches that are copied between random
* non-overlapping and non-overflowing sequence positions).
*/
public void run() {
double ran = 11*Math.random();
int[] firstNote = {59, 60, 62, 64, 65, 67, 69, 71, 72, 74, 76};
int note = firstNote[(int)ran];
int from, to;
int finalClusterNote = 0;
String cluster = "";
pitchPhrase = note + " ";
rhythmPhrase = 24 + " ";
do {
from = (int)((numNotes - 2)*Math.random());
to = (int)((numNotes - 2)*Math.random());
} while (Math.abs(to - from) < 3);
if (to - from < 0) {
int temp = from;
from = to;
to = temp;
}
if (from == 0) {
cluster = note + " ";
}
for (int i = 1; i < numNotes; i++) {
if (i == to) {
rhythmPhrase += "24 24 24 ";
pitchPhrase += cluster;
note = finalClusterNote;
i += 2;
} else {
double probSum = 0.0d, newProbSum = 0.0d;
distrib = distns.get(note); // Distn. conditional on previous note in phrase.
rhythmPhrase += "24 ";
ran = Math.random();
HashSet notes = new HashSet(distrib.keySet());
for (Iterator j = notes.iterator(); j.hasNext();) {
note = (Integer)j.next();
newProbSum += distrib.get(note);
if (ran > probSum && ran <= newProbSum) {
pitchPhrase += note + " ";
break;
}
probSum = newProbSum;
}
if (i - from > -1 && i - from < 3) {
cluster += note + " ";
if (i - from == 2) {
finalClusterNote = note;
}
}
}
}
}
}
/**
* All MIDI processing and file handling takes place in this class.
*
* @author Ray Whorley
* @version 5.0, 19th June 2020
*/
class Music
{/**
* Initial value of the minimum Levenshtein or rearrangement distance between
* a musical phrase and an array of such phrases that are liked.
*/
private double minLike = 1000.0d;
/**
* Initial value of the minimum Levenshtein or rearrangement distance between
* a musical phrase and an array of such phrases that are disliked.
*/
private double minDislike = 1001.0d;
/**
* The initial part of the path for saving MIDI files.
*/
private String dataDirectory = "./OpenMelodyOutput/";
/**
* Constructs a MIDI sequence and saves it as a MIDI file.
*/
private Output output = new Output(dataDirectory);
/**
* Relative path for saving melody ratings.
*/
private Path ratingsPath = Paths.get("OpenMelodyOutput/melody/like/ratings.txt");
/**
* File for saving melody ratings.
*/
private File ratings = new File(ratingsPath.toAbsolutePath().toString());
/**
* Keeps a record of liked and disliked pitch sequences.
*/
private int[][][] pitchSeq = new int[2][100][1];
/**
* Keeps a record of liked and disliked interval sequences (derived from
* pitch sequences).
*/
private int[][][] intervalSeq = new int[2][100][1];
/**
* Keeps a record of liked and disliked contour sequences (derived from pitch
* sequences).
*/
private int[][][] contourSeq = new int[2][100][1];
/**
* Keeps a record of liked and disliked 5-value contour sequences (derived
* from pitch sequences).
*/
private int[][][] fiveValueContourSeq = new int[2][100][1];
/**
* Keeps a record of liked and disliked in-scale sequences (w.r.t C major,
* derived from pitch sequences).
*/
private int[][][] inScaleSeq = new int[2][100][1];
/**
* Keeps a record of liked and disliked contour linked with in-scale
* sequences (derived from pitch sequences).
*/
private int[][][] contourInScaleSeq = new int[2][100][1];
/**
* Keeps a record of liked and disliked 5-value contour linked with in-scale
* sequences (derived from pitch sequences).
*/
private int[][][] fiveValueContourInScaleSeq = new int[2][100][1];
/**
* Keeps a record of liked and disliked interval linked with in-scale
* sequences (derived from pitch sequences).
*/
private int[][][] intervalInScaleSeq = new int[2][100][1];
/**
* Keeps a record of liked and disliked tessitura sequences (derived from
* pitch sequences).
*/
private int[][][] tessituraSeq = new int[2][100][1];
/**
* Keeps a record of liked and disliked tessitura change sequences (derived
* from pitch sequences).
*/
private int[][][] tessituraChangeSeq = new int[2][100][1];
/**
* Keeps a record of liked and disliked scale degree sequences (derived
* from pitch sequences).
*/
private int[][][] scaleDegreeSeq = new int[2][100][1];
/**
* Keeps a record of liked and disliked rhythm sequences.
*/
private int[][][] rhythmSeq = new int[2][100][1];
/**
* Keeps a record of liked and disliked durationDurRatio sequences.
*/
private int[][][] durationDurRatioSeq = new int[2][100][1];
/**
* Value of minimun Levenshtein/rearrangement distance from disliked phrases
* above which the generated pitch phrase is definitely not rejected.
*/
private double pitchAcceptValue;
/**
* MIDI files are saved, played or moved. In the latter case a file is moved from
* folder "like" to folder "dislike." MIDI files are of pitch phrases, rhythm
* phrases, combined pitch and rhythm phrases, and complete melodies.
*/
public Music()
{ try
{ int[][] p = new int[1][];
int[][] r = new int[1][];
pitchAcceptValue = 3.25d;
// Initialise arrays.
for (int i = 0; i < rhythmSeq.length; i++)
for (int j = 0; j < rhythmSeq[0].length; j++)
{ pitchSeq[i][j][0] = -2000;
intervalSeq[i][j][0] = -2000;
contourSeq[i][j][0] = -2000;
fiveValueContourSeq[i][j][0] = -2000;
inScaleSeq[i][j][0] = -2000;
contourInScaleSeq[i][j][0] = -2000;
fiveValueContourInScaleSeq[i][j][0] = -2000;
intervalInScaleSeq[i][j][0] = -2000;
tessituraSeq[i][j][0] = -2000;
tessituraChangeSeq[i][j][0] = -2000;
scaleDegreeSeq[i][j][0] = -2000;
rhythmSeq[i][j][0] = -2000;
durationDurRatioSeq[i][j][0] = -2000;
}
// Play short phrase on start up so that first real phrase plays
// without stuttering.
p[0] = getPhrase("60 64 67 64 60 ");
r[0] = getPhrase("24 24 24 24 96 ");
output.toFile("open", p, r, "pitch", "like");
Path path = Paths.get("OpenMelodyOutput/pitches/like/open.mid");
File midiFile = new File(path.toAbsolutePath().toString());
play(midiFile);
Files.delete(path.toAbsolutePath());
}
catch (IOException e)
{ System.out.println(e);
}
}
/**
* Saves a MIDI file, plays a MIDI file, or moves a MIDI file to "dislike" folder.
* Files may be for pitches, rhythms, complete (combined) phrases or complete melodies.
* Generated phrases may be rejected on the basis of rearrangement distance, with MIDI
* files sent to folder "auto-reject."
*
* @param str Indicates file saving, playing or moving;
* e.g. "pitchFile", "rhythmPlay" and "combineMove".
* @param fileType Possible values are "pitch", "rhythm, "combine" and "melody".
* @param folder Possible values are "pitches", "rhythms", "combined"
* and "melody".
* @param fileNum MIDI file number.
* @param rejectNum Rejected MIDI file number.
* @param pitchPhrase String representation of a pitch phrase.
* @param rhythmPhrase String representation of a rhythm phrase.
* @param pPhraseArray Array of pitch phrases.
* @param rPhraseArray Array of rhythm phrases.
* @param rating Melody rating from slider.
* @param arrayLength Usually 1 (single phrase), but 4 for melodies (four phrases).
* @throws IOException This exception can emerge because the method deals with files.
* @see IOException
*/
public boolean update(String str, String fileType, String folder, String fileNum,
String rejectNum, String pitchPhrase, String rhythmPhrase, String[] pPhraseArray,
String[] rPhraseArray, String rating, int arrayLength)
throws IOException
{ boolean accepted = true;
if (str.equals(fileType + "File"))
{ int fNum = Integer.parseInt(fileNum);
int[][] pitch = new int[arrayLength][];
int[][] rhythm = new int[arrayLength][];
if (arrayLength == 1)
for (int i = 0; i < arrayLength; i++)
{ pitch[i] = getPhrase(pitchPhrase);
rhythm[i] = getPhrase(rhythmPhrase);
}
else
for (int i = 0; i < arrayLength; i++)
{ pitch[i] = getPhrase(pPhraseArray[i]);
rhythm[i] = getPhrase(rPhraseArray[i]);
}
if (fileType.equals("melody"))
{ for (int i = 0; i < arrayLength; i++)
{ int[][] p = new int[1][];
int[][] r = new int[1][];
p[0] = pitch[i];
r[0] = rhythm[i];
output.toFile(i + 1 + "", p, r, fileType, "phrases");
}
}
else if (fileType.equals("pitch"))
{ minLike = 1000.0d;
minDislike = 1001.0d;
int[] interval = getIntervals(pitch[0]);
int[] contour = getContour(pitch[0]);
int[] fiveValueContour = getFiveValueContour(pitch[0]);
int[] inScale = getInScale(pitch[0]);
int[] contourInScale = getContourInScale(contour, inScale);
int[] fiveValueContourInScale
= getContourInScale(fiveValueContour, inScale);
int[] intervalInScale = getContourInScale(interval, inScale);
int[] tessitura = getTessitura(pitch[0]);
int[] tessituraChange = getTessituraChange(tessitura);
int[] scaleDegree = getScaleDegree(pitch[0]);
int[][] phrase = new int[4][];
int[][][][] phrases = new int[4][][][];
//phrase[0] = pitch[0];
//phrases[0] = pitchSeq;
//phrase[0] = interval;
//phrases[0] = intervalSeq;
//phrase[0] = contour;
//phrases[0] = contourSeq;
//phrase[0] = fiveValueContour;
//phrases[0] = fiveValueContourSeq;
//phrase[0] = inScale;
//phrases[0] = inScaleSeq;
//phrase[0] = contourInScale;
//phrases[0] = contourInScaleSeq;
//phrase[0] = fiveValueContourInScale;
//phrases[0] = fiveValueContourInScaleSeq;
//phrase[0] = intervalInScale;
//phrases[0] = intervalInScaleSeq;
//phrase[0] = contourInScale;
//phrase[1] = fiveValueContourInScale;
//phrases[0] = contourInScaleSeq;
//phrases[1] = fiveValueContourInScaleSeq;
//phrase[0] = tessitura;
//phrases[0] = tessituraSeq;
//phrase[0] = tessituraChange;
//phrases[0] = tessituraChangeSeq;
//phrase[0] = scaleDegree;
//phrases[0] = scaleDegreeSeq;
phrase[0] = fiveValueContour;
phrase[1] = inScale;
phrase[2] = tessituraChange;
phrase[3] = scaleDegree;
phrases[0] = fiveValueContourSeq;
phrases[1] = inScaleSeq;
phrases[2] = tessituraChangeSeq;
phrases[3] = scaleDegreeSeq;
//minLike = getMin(minLike, 0, phrase, phrases);
//minDislike = getMin(minDislike, 1, phrase, phrases);
minLike = getMinRearrange(minLike, 0, phrase, phrases);
minDislike = getMinRearrange(minDislike, 1, phrase, phrases);
if (minLike < minDislike || minDislike > pitchAcceptValue)
{ //pitchSeq[0][fNum] = pitch[0];
//pitchSeq[1][fNum][0] = -1000;
//intervalSeq[0][fNum] = interval;
//intervalSeq[1][fNum][0] = -1000;
//contourSeq[0][fNum] = contour;
//contourSeq[1][fNum][0] = -1000;
fiveValueContourSeq[0][fNum] = fiveValueContour;
fiveValueContourSeq[1][fNum][0] = -1000;
inScaleSeq[0][fNum] = inScale;
inScaleSeq[1][fNum][0] = -1000;
//contourInScaleSeq[0][fNum] = contourInScale;
//contourInScaleSeq[1][fNum][0] = -1000;
//fiveValueContourInScaleSeq[0][fNum] = fiveValueContourInScale;
//fiveValueContourInScaleSeq[1][fNum][0] = -1000;
//intervalInScaleSeq[0][fNum] = intervalInScale;
//intervalInScaleSeq[1][fNum][0] = -1000;
//tessituraSeq[0][fNum] = tessitura;
//tessituraSeq[1][fNum][0] = -1000;
tessituraChangeSeq[0][fNum] = tessituraChange;
tessituraChangeSeq[1][fNum][0] = -1000;
scaleDegreeSeq[0][fNum] = scaleDegree;
scaleDegreeSeq[1][fNum][0] = -1000;
}
}
else if (fileType.equals("rhythm"))
{ minLike = 1000.0d;
minDislike = 1001.0d;
int[] durationDurRatio = getDurationDurRatio(rhythm[0]);
int[][] phrase = new int[1][];
int[][][][] phrases = new int[1][][][];
//phrase[0] = rhythm[0];
//phrases[0] = rhythmSeq;
phrase[0] = durationDurRatio;
phrases[0] = durationDurRatioSeq;
//minLike = getMin(minLike, 0, phrase, phrases);
//minDislike = getMin(minDislike, 1, phrase, phrases);
minLike = getMinRearrange(minLike, 0, phrase, phrases);
minDislike = getMinRearrange(minDislike, 1, phrase, phrases);
if (minLike < minDislike || minDislike > 4.0d)
{ //rhythmSeq[0][fNum] = rhythm[0];
//rhythmSeq[1][fNum][0] = -1000;
durationDurRatioSeq[0][fNum] = durationDurRatio;
durationDurRatioSeq[1][fNum][0] = -1000;
}
}
// if (!fileType.equals("pools"))
if (fileType.equals("combine") || fileType.equals("melody"))
{ output.toFile(fileNum, pitch, rhythm, fileType, "like");
}
else if (rejectNum.equals("0") || minLike < minDislike ||
(fileType.equals("pitch") && minDislike > pitchAcceptValue)
|| (fileType.equals("rhythm") && minDislike > 4.0d))
{ output.toFile(fileNum, pitch, rhythm, fileType, "like");
}
else
{ String fileName = fileNum + "_" + rejectNum;
output.toFile(fileName, pitch, rhythm,
fileType, "auto-rejected");
accepted = false;
}
}
else if (str.equals(fileType + "Play"))
{ if (fileType.equals("melody"))
{ for (int i = 1; i < 5; i++)
{ Path path = Paths.get("OpenMelodyOutput/" + folder + "/phrases/"
+ i + ".mid");
File midiFile = new File(path.toAbsolutePath().toString());
play(midiFile);
pause(500);
}
}
else
{ Path path = Paths.get("OpenMelodyOutput/" + folder + "/like/"
+ fileNum + ".mid");
File midiFile = new File(path.toAbsolutePath().toString());
play(midiFile);
}
}
else if (str.equals(fileType + "Move"))
{ int fNum = Integer.parseInt(fileNum);
Path from = Paths.get("OpenMelodyOutput/" + folder + "/like/"
+ fileNum + ".mid");
Path to = Paths.get("OpenMelodyOutput/" + folder + "/dislike/"
+ fileNum + ".mid");
Files.move(from.toAbsolutePath(), to.toAbsolutePath());
if (fileType.equals("pitch"))
{ //pitchSeq[1][fNum] = pitchSeq[0][fNum];
//pitchSeq[0][fNum] = new int[1];
//pitchSeq[0][fNum][0] = -1000;
//intervalSeq[1][fNum] = intervalSeq[0][fNum];
//intervalSeq[0][fNum] = new int[1];
//intervalSeq[0][fNum][0] = -1000;
//contourSeq[1][fNum] = contourSeq[0][fNum];
//contourSeq[0][fNum] = new int[1];
//contourSeq[0][fNum][0] = -1000;
fiveValueContourSeq[1][fNum] = fiveValueContourSeq[0][fNum];
fiveValueContourSeq[0][fNum] = new int[1];
fiveValueContourSeq[0][fNum][0] = -1000;
inScaleSeq[1][fNum] = inScaleSeq[0][fNum];
inScaleSeq[0][fNum] = new int[1];
inScaleSeq[0][fNum][0] = -1000;
//contourInScaleSeq[1][fNum] = contourInScaleSeq[0][fNum];
//contourInScaleSeq[0][fNum] = new int[1];
//contourInScaleSeq[0][fNum][0] = -1000;
//fiveValueContourInScaleSeq[1][fNum]
// = fiveValueContourInScaleSeq[0][fNum];
//fiveValueContourInScaleSeq[0][fNum] = new int[1];
//fiveValueContourInScaleSeq[0][fNum][0] = -1000;
//intervalInScaleSeq[1][fNum] = intervalInScaleSeq[0][fNum];
//intervalInScaleSeq[0][fNum] = new int[1];
//intervalInScaleSeq[0][fNum][0] = -1000;
//tessituraSeq[1][fNum] = tessituraSeq[0][fNum];
//tessituraSeq[0][fNum] = new int[1];
//tessituraSeq[0][fNum][0] = -1000;
tessituraChangeSeq[1][fNum] = tessituraChangeSeq[0][fNum];
tessituraChangeSeq[0][fNum] = new int[1];
tessituraChangeSeq[0][fNum][0] = -1000;
scaleDegreeSeq[1][fNum] = scaleDegreeSeq[0][fNum];
scaleDegreeSeq[0][fNum] = new int[1];
scaleDegreeSeq[0][fNum][0] = -1000;
}
else if (fileType.equals("rhythm"))
{ //rhythmSeq[1][fNum] = rhythmSeq[0][fNum];
//rhythmSeq[0][fNum] = new int[1];
//rhythmSeq[0][fNum][0] = -1000;
durationDurRatioSeq[1][fNum] = durationDurRatioSeq[0][fNum];
durationDurRatioSeq[0][fNum] = new int[1];
durationDurRatioSeq[0][fNum][0] = -1000;
}
}
else if (str.equals(fileType + "Rating"))
{ FileWriter writer = new FileWriter(ratings, true);
if (rating.equals("10"))
writer.write(fileNum + ".mid" + " " + rating + "\n");
else
writer.write(fileNum + ".mid" + " " + rating + "\n");
writer.close();
}
else if (str.equals("pitches")) {}
else if (str.equals("get pitches") && !fileType.equals("save")) {}
else if (str.equals("unselect pitch")) {}
else if (str.equals("rhythm")) {}
else if (str.equals("get rhythm") && !fileType.equals("save"))
{ if (!pitchPhrase.equals("") && !rhythmPhrase.equals(""))
{ pitchPhrase = "";
rhythmPhrase = "";
}
}
return accepted;
}
/**
* A string comprising pitch values or note lengths separated by spaces is
* converted to an array of integers.
*
* @param str A string comprising space separated numbers.
* @return An integer array of pitch values or note lengths for a musical phrase.
*/
private int[] getPhrase(String str)
{ String num = "";
int count = 0;
int[] temp = new int[50]; // Arbitrary length.
for (int i = 0; i < str.length(); i++)
{ if (str.charAt(i) != ' ' && str.charAt(i) != '\n')
num += str.charAt(i);
else
{ temp[count] = Integer.parseInt(num);
num = "";
count++;
}
}
int[] phrase = new int[count];
for (int i = 0; i < count; i++)
phrase[i] = temp[i];
return phrase;
}
/**
* Converts a pitch sequence into an interval sequence.
*
* @param pitch An integer array representing a pitch sequence.
* @return An integer array representing an interval sequence.
*/
private int[] getIntervals(int[] pitch)
{ int[] interval = new int[pitch.length - 1];
for (int i = 1; i < pitch.length; i++)
interval[i - 1] = pitch[i] - pitch[i - 1];
return interval;
}
/**
* Converts a pitch sequence into a contour sequence.
*
* @param pitch An integer array representing a pitch sequence.
* @return An integer array representing a contour sequence.
*/
private int[] getContour(int[] pitch)
{ int[] contour = new int[pitch.length - 1];
for (int i = 1; i < pitch.length; i++)
{ if (pitch[i] - pitch[i - 1] > 0)
contour[i - 1] = 1;
else if (pitch[i] - pitch[i - 1] == 0)
contour[i - 1] = 0;
else
contour[i - 1] = -1;
}
return contour;
}
/**
* Converts a pitch sequence into a 5-value contour sequence.
*
* @param pitch An integer array representing a pitch sequence.
* @return An integer array representing a 5-value contour sequence.
*/
private int[] getFiveValueContour(int[] pitch)
{ int[] fiveValueContour = new int[pitch.length - 1];
for (int i = 1; i < pitch.length; i++)
{ if (pitch[i] - pitch[i - 1] > 2)
fiveValueContour[i - 1] = 2;
else if (pitch[i] - pitch[i - 1] < 3 && pitch[i] - pitch[i - 1] > 0)
fiveValueContour[i - 1] = 1;
else if (pitch[i] - pitch[i - 1] == 0)
fiveValueContour[i - 1] = 0;
else if (pitch[i] - pitch[i - 1] < -2)
fiveValueContour[i - 1] = -2;
else
fiveValueContour[i - 1] = -1;
}
return fiveValueContour;
}
/**
* Converts a pitch sequence into an in-scale sequence.
*
* @param pitch An integer array representing a pitch sequence.
* @return An integer array representing an in-scale sequence.
*/
private int[] getInScale(int[] pitch)
{ int[] inScale = new int[pitch.length];
int key = getKey(pitch);
for (int i = 0; i < pitch.length; i++)
{ if (!((pitch[i] - key) % 12 == 0 ||
(pitch[i] - key) % 12 == 2 ||
(pitch[i] - key) % 12 == 4 ||
(pitch[i] - key) % 12 == 5 ||
(pitch[i] - key) % 12 == 7 ||
(pitch[i] - key) % 12 == 9 ||
(pitch[i] - key) % 12 == 11))
inScale[i] = 1;
else
inScale[i] = 0;
}
return inScale;
}
/**
* Finds the most likely key (with the fewest non-scale notes).
*
* @param pitch An integer array representing a pitch sequence.
* @return An integer representing the most likely key
* (0 = C, 1 = C#, etc).
*/
private int getKey(int[] pitch)
{ int minNonScale = 100;
int key = 100;
for (int j = 0; j < 12; j++)
{ int nonScale = 0;
for (int i = 0; i < pitch.length; i++)
{ if (!((pitch[i] - j) % 12 == 0 ||
(pitch[i] - j) % 12 == 2 ||
(pitch[i] - j) % 12 == 4 ||
(pitch[i] - j) % 12 == 5 ||
(pitch[i] - j) % 12 == 7 ||
(pitch[i] - j) % 12 == 9 ||
(pitch[i] - j) % 12 == 11))
nonScale++;
}
if (nonScale < minNonScale)
{ key = j;
minNonScale = nonScale;
}
}
return key;
}
/**
* Converts contour (including 5-value contour and interval) and in-scale
* sequences into a linked contour x in-scale sequence.
*
* @param contour An integer array representing a contour sequence.
* @return An integer array representing a linked contour x in-scale
* sequence.
*/
private int[] getContourInScale(int[] contour, int[] inScale)
{ int[] contourInScale = new int[inScale.length];
for (int i = 0; i < inScale.length; i++)
{ if (i == 0)
contourInScale[i] = inScale[i];
else
contourInScale[i] = contour[i - 1]*10 + inScale[i];
}
return contourInScale;
}
/**
* Converts a pitch sequence into a tessitura sequence. Pitches are
* categorised as low (< E4), high (> Eb5) or middling.
*
* @param pitch An integer array representing a pitch sequence.
* @return An integer array representing a tessitura sequence.
*/
private int[] getTessitura(int[] pitch)
{ int[] tessitura = new int[pitch.length];
for (int i = 0; i < pitch.length; i++)
{ if (pitch[i] < 64)
tessitura[i] = -1;
else if (pitch[i] > 75)
tessitura[i] = 1;
else
tessitura[i] = 0;
}
return tessitura;
}
/**
* Converts a tessitura sequence into a tessitura change sequence.
*
* @param tessitura An integer array representing a tessitura sequence.
* @return An integer array representing a tessitura change
* sequence.
*/
private int[] getTessituraChange(int[] tessitura)
{ int[] tessituraChange = new int[tessitura.length - 1];
for (int i = 1; i < tessitura.length; i++)
tessituraChange[i - 1] = tessitura[i - 1]*10 + tessitura[i];
return tessituraChange;
}
/**
* Converts a pitch sequence into a scale degree sequence.
*
* @param pitch An integer array representing a pitch sequence.
* @return An integer array representing a scale degree sequence.
*/
private int[] getScaleDegree(int[] pitch)
{ int[] scaleDegree = new int[pitch.length];
int key = getKey(pitch);
for (int i = 0; i < pitch.length; i++)
scaleDegree[i] = (pitch[i] - key) % 12;
return scaleDegree;
}
/**
* Converts a duration sequence into a linked duration x duration ratio
* sequence.
*
* @param duration An integer array representing a duration sequence.
* @return An integer array representing a linked duration x
* duration ratio sequence.
*/
private int[] getDurationDurRatio(int[] duration)
{ int[] durationDurRatio = new int[duration.length - 1];
for (int i = 1; i < duration.length; i++)
durationDurRatio[i - 1] = duration[i - 1]*100 + duration[i];
return durationDurRatio;
}
/**
* Calculates the Levenshtein distance between a musical phrase and all such
* phrases that are either liked or disliked (not both), and returns the
* minimum distance.
*
* @param min An integer representing the starting value of minimum
* Levenshtein distance.
* @param index An integer representing an array index: 0 for "like"
* and 1 for "dislike".
* @param phrase An integer array representing a musical phrase in one or
* ways.
* @param seq An array of liked and disliked musical phrases, represented
* in one or more ways.
* @return Minimum Levenshtein distance between a musical phrase
* and all such phrases that are either liked or disliked.
*/
private double getMin(double min, int index, int[][] phrase, int[][][][] seq)
{ int i = 1;
while (seq[0][index][i][0] != -2000)
{ if (seq[0][index][i][0] > -1000)
{ double sum = 0.0d;
for (int k = 0; k < phrase.length; k++)
sum += levenshteinDistance(phrase[k], seq[k][index][i]);
min = Math.min(min, sum/(double)phrase.length);
}
i++;
}
return min;
}
/**
* Calculates the rearrangement distance between a musical phrase and all
* such phrases that are either liked or disliked (not both), and returns the
* minimum distance.
*
* @param min An integer representing the starting value of minimum
* rearrangement distance.
* @param index An integer representing an array index: 0 for "like"
* and 1 for "dislike".
* @param phrase An integer array representing a musical phrase in one or
* ways.
* @param seq An array of liked and disliked musical phrases, represented
* in one or more ways.
* @return Minimum rearrangement distance between a musical phrase
* and all such phrases that are either liked or disliked.
*/
private double getMinRearrange(double min, int index,
int[][] phrase, int[][][][] seq)
{ int i = 1;
while (seq[0][index][i][0] != -2000)
{ if (seq[0][index][i][0] > -1000)
{ double sum = 0.0d;
for (int k = 0; k < phrase.length; k++)
sum += rearrangementDistance(phrase[k], seq[k][index][i]);
min = Math.min(min, sum/(double)phrase.length);
}
i++;
}
return min;
}
/**
* A Sequencer object plays a MIDI file.
*
* @param midiFile A File object containing MIDI data (.mid).
*/
private void play(File midiFile)
{ try
{ Sequencer sequencer = MidiSystem.getSequencer();
sequencer.open();
Sequence midiSeq = MidiSystem.getSequence(midiFile);
sequencer.setSequence(midiSeq);
sequencer.start();
while (sequencer.isRunning())
pause(500);
sequencer.stop();
sequencer.close();
}
catch(MidiUnavailableException e)
{ System.out.println(e);
}
catch(InvalidMidiDataException e)
{ System.out.println(e);
}
catch(IOException e)
{ System.out.println(e);
}
}
/**
* Pauses processing.
*
* @param time An integer representing the length of the pause in ms.
*/
private void pause(int time)
{ try
{ Thread.sleep(time);
}
catch(InterruptedException e)
{ System.out.println(e);
}
}
/**
* An inplementation of the Wagner-Fischer algorithm:
* Wagner, Robert A. and Fischer, Michael J. (1974). The String-to-String
* Correction Problem. Journal of the ACM, 21(1) pp. 168-173.
*
* Calculates the Levenshtein distance between two musical phrases.
*
* @param s An integer array representation of a pitch or rhythm phrase,
* which is a sequence of basic (such as Pitch) or derived (such as
* Interval) viewpoint elements.
* @param t An integer array representation of a pitch or rhythm phrase,
* which is a sequence of basic (such as Pitch) or derived (such as
* Interval) viewpoint elements.
* @return The Levenshtein distance between two musical phrases.
*/
private double levenshteinDistance(int[] s, int[] t)
{ int substCost = 0;
// For all i and j, d[i][j] will hold the Levenshtein distance between
// the first i characters of s and the first j characters of t.
int[][] d = new int[s.length + 1][t.length + 1];
// Set each element in d to zero.
for (int i = 0; i < d.length; i++)
for (int j = 0; j < d[i].length; j++)
d[i][j] = 0;
// Source prefixes can be transformed into
// empty string by dropping all characters.
for (int i = 1; i < d.length; i++)
d[i][0] = i;
// Target prefixes can be reached from empty
// source prefix by inserting every character.
for (int j = 1; j < d[0].length; j++)
d[0][j] = j;
for (int j = 1; j < d[0].length; j++)
for (int i = 1; i < d.length; i++)
{ if (s[i - 1] == t[j - 1])
substCost = 0;
else
substCost = 1;
int temp = Math.min(d[i - 1][j] + 1, // deletion
d[i][j - 1] + 1); // insertion
d[i][j] = Math.min(temp, d[i - 1][j - 1] + substCost); // subst.
}
return (double)d[s.length][t.length];
}
/**
* Calculates the rearrangement distance between two musical phrases.
*
* @param s An integer array representation of a pitch or rhythm phrase,
* which is a sequence of basic (such as Pitch) or derived (such as
* Interval) viewpoint elements.
* @param t An integer array representation of a pitch or rhythm phrase,
* which is a sequence of basic (such as Pitch) or derived (such as
* Interval) viewpoint elements.
* @return The rearrangement distance between two musical phrases.
*/
private double rearrangementDistance(int[] s, int[] t)
{ int distance = s.length;
// Array element set to true if an element in the
// source sequence is found in the target sequence.
boolean[] found = new boolean[s.length];
// Set each element in 'found' to false.
for (int i = 0; i < found.length; i++)
found[i] = false;
for (int i = 0; i < s.length; i++)
for (int j = 0; j < t.length; j++)
if (s[i] == t[j] && !found[j])
{ found[j] = true;
distance--;
break;
}
return (double)distance;
}
}
/**
* Constructs a MIDI sequence and saves it as a MIDI file.
*
* @author Ray Whorley
* @version 5.0, 16th June 2020
*/
class Output
{/**
* Length of event, message and short message arrays.
*/
private final int LEN = 300; // TODO: could be much lower.
/**
* MIDI status for Note On. Indicates that a note is beginning to sound.
*/
private final int NOTE_ON = 144;
/**
* MIDI value for velocity (note loudness).
*/
private final int VELOCITY = 96;
/**
* MIDI value for velocity (note loudness) used for first beat of bar.
*/
private final int ACCENT = 111;
/**
* MIDI status for Program Change (change of instrumental sound).
*/
private final int PROGRAM_CHANGE = 192;
/**
* Indicates the beat of the bar to start on.
*/
private final int TICK_OFFSET = 0*32/3; // Start on first beat of bar.
/**
* MIDI track used for pitch only phrase, rhythm only phrase,
* phrase with pitch and rhythm combined, and complete melody.
*/
private Track melodyTrack;
/**
* Array of MIDI events, which comprise a MIDI message and a starting time.
*/
private MidiEvent[] melodyEvent = new MidiEvent[LEN];
/**
* Array of MIDI messages (for example, note start and finish). A MIDI short
* message is created first, and then assigned to a MIDI message.
*/
private MidiMessage[] melodyMessage = new MidiMessage[LEN];
/**
* Array of MIDI short messages (for example, note start and finish). Each
* short message is assigned to a MIDI message.
*/
private ShortMessage[] melodyShortMessage = new ShortMessage[LEN];
/**
* A MIDI event starting time.
*/
private long tick;
/**
* The initial part of the path for saving MIDI files.
*/
private String dataDirectory = "";
/**
* Creates a new Output instance from a string argument representing the first
* part of a path.
*
* @param directory a string representing the first part of a path
*/
public Output(String directory)
{ dataDirectory = directory;
}
/**
* Constructs a MIDI sequence and saves it as a MIDI file. The sequence
* contains a general track for time signature, key signature and tempo; and a
* melody track for instrument sound and notes.
*
* @param fileNumber A number in string format. Concatinated with ".mid" to
* give a MIDI file name.
* @param pitch An array of integers (MIDI pitch values). Array length 1
for phrase and length 4 for melody.
* @param rhythm An array of integers (note lengths: crotchet = 24).
Array length 1 for phrase and length 4 for melody.
* @param fileType A string with possible values "pitch", "rhythm",
"combine", "pools" and "melody".
*/
public void toFile(String fileNumber, int[][] pitch, int[][] rhythm,
String fileType, String folder)
{ Track generalTrack;
MidiEvent[] generalEvent = new MidiEvent[LEN]; // TODO: can be smaller
MetaMessage[] generalMessage = new MetaMessage[LEN];
int note, noteLength;
int rest = 0;
for (int i = 0; i < LEN; i++)
melodyShortMessage[i] = new ShortMessage();
try
{ Sequence musicSequence = new Sequence(Sequence.PPQ, 256, 2);
generalTrack = musicSequence.createTrack(); // 256 ticks per crotchet
melodyTrack = musicSequence.createTrack(); // and 2 tracks
byte[] timeSig = {(byte)4, (byte)2, (byte)24, (byte)8}; // 4/4
byte[] tempo = {(byte)9, (byte)39, (byte)192}; // crotchet = 100
byte[] keySig = {(byte)0, (byte)0}; // C major
// int type, byte[] data, int length of data
generalMessage[0] = new MetaMessage(88, timeSig, 4);
generalMessage[1] = new MetaMessage(81, tempo, 3);
generalMessage[2] = new MetaMessage(89, keySig, 2);
for (int i = 0; i < 3; i++)
{ generalEvent[i] = new MidiEvent(generalMessage[i], 0);
generalTrack.add(generalEvent[i]);
}
if (fileType.equals("rhythm"))
{ // use percussion sound for rhythm
melodyShortMessage[0].setMessage(PROGRAM_CHANGE, 115, 0);
melodyMessage[0] = melodyShortMessage[0];
melodyEvent[0] = new MidiEvent(melodyMessage[0], 0);
melodyTrack.add(melodyEvent[0]);
}
int count = -1;
for (int k = 0; k < pitch.length; k++)
for (int i = 0; i < pitch[k].length; i++)
{ if (pitch[k][i] == 0)
rest = rhythm[k][i]*32/3;
else
{ count++;
noteLength = rhythm[k][i]*32/3;
note = pitch[k][i];
addMelodyNote(count*2 + 2, note, noteLength, rest);
rest = 0;
}
}
Path path = Paths.get("");
File midiFile = new File("");
if (fileType.equals("pitch"))
{ path = Paths.get("OpenMelodyOutput/pitches/" + folder + "/" + fileNumber
+ ".mid");
midiFile = new File(path.toAbsolutePath().toString());
}
else if (fileType.equals("rhythm"))
{ path = Paths.get("OpenMelodyOutput/rhythms/" + folder + "/" + fileNumber
+ ".mid");
midiFile = new File(path.toAbsolutePath().toString());
}
else if (fileType.equals("combine"))
{ path = Paths.get("OpenMelodyOutput/combined/" + folder + "/" + fileNumber
+ ".mid");
midiFile = new File(path.toAbsolutePath().toString());
}
else if (fileType.equals("melody"))
{ path = Paths.get("OpenMelodyOutput/melody/" + folder + "/" + fileNumber
+ ".mid");
midiFile = new File(path.toAbsolutePath().toString());
}
MidiSystem.write(musicSequence, 1, midiFile); // Type 1 file.
}
catch(InvalidMidiDataException e)
{ System.out.println(e);
}
catch(IOException e)
{ System.out.println(e);
}
}
/**
* Adds note start and finish events to the melody track. In the case of note
* finish, Note On with zero velocity is preferred to Note Off. Notes on first
* beat of bar are played slightly louder. A bar's rest replaces phrase(s)
* missing from melody.
*
* @param i Array index used for MIDI short message, message and
* event arrays.
* @param note MIDI pitch value.
* @param noteLength Note length (crotchet = 24*32/3).
* @param rest A bar's rest (96*32/3 for 4/4), which replaces phrase(s)
* missing from melody.
* @throws InvalidMidiDataException This exception can emerge because the
* method deals with MIDI data.
*/
private void addMelodyNote(int i, int note,
int noteLength, int rest) throws InvalidMidiDataException
{ tick = (i == 2 ? TICK_OFFSET + rest : melodyEvent[i-1].getTick() + rest);
if (tick % (96*32/3) == 0)
melodyShortMessage[i].setMessage(NOTE_ON, note, ACCENT);
else
melodyShortMessage[i].setMessage(NOTE_ON, note, VELOCITY);
melodyMessage[i] = melodyShortMessage[i];
melodyEvent[i] = new MidiEvent(melodyMessage[i], tick);
melodyTrack.add(melodyEvent[i]);
i++;
tick = melodyEvent[i - 1].getTick() + noteLength;
melodyShortMessage[i].setMessage(NOTE_ON, note, 0); // Velocity = 0
melodyMessage[i] = melodyShortMessage[i];
melodyEvent[i] = new MidiEvent(melodyMessage[i], tick);
melodyTrack.add(melodyEvent[i]);
}
}