/** CSC220F19MIDIassn3, D. Parson, October 2019, CSC220 ENTER STUDENT NAME HERE: DUE DATE: Friday November 8 at 11:59 PM. Planetarium-oriented assignment 3, in which each student will design their custom instrument sound, arrangement of notes on the planetarium display, and graphic used for the not-sounding and sounding notes. Each student must hard code and document their respective instrument (a.k.a. "patch" or PROGRAM) and effect (a.k.a. CONTROLLER) in variables near the top of this sketch. See STUDENT comments (upper case) for student reuirements. The only interaction in assignment 3 is via the mouse. For assignment 4 we will have multiple remote sketches, one per musician (up to 16 at a time), that will sound their notes via the OSC/UDP (Open Sound Control/User Datagram Protocol) protocols. Assignment 3 plays only 1 note at a time. INTERACTION: Mouse location with mousePressed variable determines velocity & pitch of NOTE_ON. 'b' toggles painting the perimeter of the dome (bounding circle). RIGHT and LEFT arrows increment or decrement setShearXSpeed() for all Note objects, and 'z' zeroes out setShearXSpeed() for all Notes. **/ // STUDENT MUST SELECT AN INSTRUMENT FROM http://midi.teragonaudio.com/tutr/gm.htm, // test it out combined with an effect, set programNumber to that number (note // that the web page starts at 1, so subtract 1 in order to start at 0), and // add a comment telling us what instrument is represented by programNumber. // STUDENT: CHANGE programNumber to the instrument you want to play. final int programNumber = 0 ; // PROGRAM NUMBER. // STUDENT MUST SELECT AN EFFECT FROM http://midi.teragonaudio.com/tech/midispec.htm // CONTROLLERS, test it out combined with a programNumber, set controlEffectNumber to that // number, and add a comment telling us what effect is represented by controlEffectNumber. final int controlEffectNumber = 0 ; // STUDENT: CHANGE controlEffectData2 0 to 127 for how much of controlEffectNumber to use. final int controlEffectData2 = 127 ; // DEGREE OF CONTROL MESSAGE effect final int MIDIchannel = 0 ; // Stay at 0 in assn3. Assn4 will assign these to students. // MIDI docs: // http://midi.teragonaudio.com/tech/midispec.htm TYPES OF CONTROL_CHANGE (effects) // http://midi.teragonaudio.com/tutr/gm.htm TYPES OF PROGRAM_CHANGE (INSTRUMENTS) // https://docs.oracle.com/javase/8/docs/api/javax/sound/midi/ShortMessage.html Your only MIDI class. import javax.sound.midi.* ; // Get everything in the Java MIDI (Musical Instrument Digital Interface) package. import java.util.* ; // https://docs.oracle.com/javase/8/docs/api/java/util/HashMap.html // MIDI OUTPUT DEVICE SELECTION: final int midiDeviceIndex = 0 ; // setup() checks for number of devices. Use one for output. // NOTE: A final variable is in fact a constant that cannot be changed. MidiDevice.Info[] midiDeviceInfo = null ; // See javax.sound.midi.MidiSystem and javax.sound.midi.MidiDevice MidiDevice device = null ; // See javax.sound.midi.MidiSystem and javax.sound.midi.MidiDevice Receiver receiver = null ; // javax.sound.midi.Receiver receives your OUTPUT MIDI messages (counterintuitive?) // SEE https://www.midi.org/specifications/item/gm-level-1-sound-set but start at 0, not 1 // MIDI CONSTANTS (final means the code cannot assign into them): final int [][] scales = { // intervals between notes in a scale, normally not all 12 notes {0, 2, 4, 5, 7, 9, 11}, // Major scale: DO RE MI FA SO LA TI (next DO not shown) {0, 2, 3, 5, 7, 8, 10} // Harmonic minor scale: DO RE MI FA SO LA TI }; final String [] scaleName = {"major", "minor"}; final String [] noteName = {"C", "Db", "D", "Eb", "E", "F", "Gb", "G", "Ab", "A", "Bb", "B"}; final int octaveCount = 3 ; // A subset of all available octaves. Save screen space and ears. final int volumeLevels = 5 ; // Don't go to 0, not needed. // MIDI STATE VARIABLES (For assn3 we are keeping these constant.): final int scaleix = 0 ; // index into scales and scaleName, cycle using 's' and 'S' final int tonic = 0 ; // index into scales array, cycle using 't' and 'T' // Note objects final Note [] notes = new Note[ (scales[scaleix].length * octaveCount + 1) * volumeLevels ]; final Musician [] musicians = new Musician[16] ; // Up to sixteen musicians // MISC. VARIABLES: PImage clippingCircle ; // Only used when globalShearXSpeed != 0 int domeTranslateX, domeTranslateY, domeWidthHeight, domeRadius ; boolean isBoundingCircle = true ; float globalShearXSpeed = 0 ; void setup() { // GRAPHICS: fullScreen(P3D); // We are popping up 3D shapes when a note is hit. // size(900,700,P3D); randomSeed(12345); // always gives identical random() sequence for repeat testing. ellipseMode(CENTER); rectMode(CENTER); shapeMode(CENTER); imageMode(CENTER); domeTranslateX = width/2 ; // plot everything relative to center of dome domeTranslateY = height/2 ; domeWidthHeight = min(width, height); domeRadius = domeWidthHeight / 2 ; // setup() places the Note object positions and other properties: int noteindex = 0 ; // index into the notes[] array. int volstep = 120 / (volumeLevels+1) ; // do not make Notes for the 0-velocity level int notesize = domeWidthHeight * 2 / notes.length ; // trial and error int startOctave = (10 - octaveCount) / 2 ; // the margin below the start of notes int minpitch = startOctave * 12 + tonic ; // 12 notes are in an octave int maxpitch = minpitch + octaveCount * 12 ; Note newnote ; for (int octindex = startOctave, octstep = 0 ; octstep < octaveCount ; octindex++, octstep++) { for (int notestep = 0 ; notestep < scales[scaleix].length ; notestep++) { for (int vel = volstep*2, velix = 0 ; vel <= 127 ; vel += volstep, velix++) { int hue = round(360.0 * (float)notestep / (float)scales[scaleix].length); int pitch = octindex * 12 + scales[scaleix][notestep] + tonic ; int [] xyloc = mapXY(pitch, minpitch, maxpitch, velix, volumeLevels); newnote = new Note(pitch, vel, xyloc[0], xyloc[1], notesize, notesize, hue, 99, 99 * vel / 127); notes[noteindex++] = newnote ; } } } // Add on an extra tonic (DO note) above the top octave. int octindex = startOctave + octaveCount ; for (int vel = volstep*2, velix = 0 ; vel <= 127 ; vel += volstep, velix++) { int hue = 0 ; int pitch = octindex * 12 + tonic ; int [] xyloc = mapXY(pitch, minpitch, maxpitch, velix, volumeLevels); newnote = new Note(pitch, vel, xyloc[0], xyloc[1], notesize, notesize, hue, 99, 99 * vel / 127); notes[noteindex++] = newnote ; } // Only use 2D clipping when globalShearXSpeed != 0. int midx = width / 2 ; int midy = height / 2 ; colorMode(RGB, 256, 256, 256, 256); // Just to build clippingCircle clippingCircle = createImage(width, height, ARGB); clippingCircle.loadPixels(); for (int col = 0 ; col < width ; col++) { for (int row = 0 ; row < height ; row++) { int pix = row * width + col ; if (dist(col, row, midx, midy) > domeRadius) { clippingCircle.pixels[pix] = 0x0ff000000 ; // all black, max alpha } else { clippingCircle.pixels[pix] = 0x000000000 ; // min alpha is transparent } } } clippingCircle.updatePixels(); colorMode(HSB, 360, 100, 100, 100); // For HSB, args are Hue (0..359 DEGREE), Saturation, Brightness, Alpha (0..99 %) fill(0, 0, 0); // black for planetarium noStroke(); frameRate(10); // a note every 10th of a second is enough // MIDI: // 1. FIND OUT WHAT MIDI DEVICES ARE AVAILABLE FOR VARIABLE midiDeviceIndex. midiDeviceInfo = MidiSystem.getMidiDeviceInfo(); for (int i = 0 ; i < midiDeviceInfo.length ; i++) { println("MIDI DEVICE NUMBER " + i + " Name: " + midiDeviceInfo[i].getName() + ", Vendor: " + midiDeviceInfo[i].getVendor() + ", Description: " + midiDeviceInfo[i].getDescription()); } // 2. OPEN ONE OF THE MIDI DEVICES UP FOR OUTPUT. try { device = MidiSystem.getMidiDevice(midiDeviceInfo[midiDeviceIndex]); device.open(); // Make sure to close it before this sketch terminates!!! // There should be a way to schedule a method when Processing closes this // sketch, so we can close the device there, but it is not documented for Processing 3. receiver = device.getReceiver(); // NOTE: Either of the above method calls can throw MidiUnavailableException // if there is no available device or if it does not have a Receiver to // which we can send messages. The catch clause intercepts those error messages. // See https://www.midi.org/specifications/item/gm-level-1-sound-set, use programNumber variable ShortMessage noteMessage = new ShortMessage() ; noteMessage.setMessage(ShortMessage.PROGRAM_CHANGE, 0, programNumber, 0); // to channel 0 receiver.send(noteMessage, -1L); // send it now } catch (MidiUnavailableException mx) { System.err.println("MIDI UNAVAILABLE"); // Error messages go here. device = null ; receiver = null ; // Do not try to use them. } catch (InvalidMidiDataException dx) { System.err.println("MIDI ERROR: " + dx.getMessage()); // Error messages go here. } musicians[0] = new Musician(programNumber, controlEffectNumber, controlEffectData2, MIDIchannel); // We are only using 1 Musician in assignment 3, more later. } void draw() { perspective(); // We want a visible 3D effect when a Note is being played. pushMatrix(); translate(domeTranslateX, domeTranslateY, 0); // 0,0,0 point in center of display, top of dome colorMode(HSB, 360, 100, 100, 100); // For HSB, args are Hue (0..359 DEGREE), Saturation, Brightness, Alpha (0..99 %) background(0, 0, 0); // black for planetarium noFill(); ellipseMode(CENTER); rectMode(CENTER); shapeMode(CENTER); imageMode(CENTER); int translatedMouseX = mouseX - domeTranslateX ; // compensate for 0,0 being in middle int translatedMouseY = mouseY - domeTranslateY ; // of the display. for (Note n : notes) { boolean mouseIn = isWithin(translatedMouseX, translatedMouseY, n); boolean ispressed = mouseIn && mousePressed ; pushMatrix(); // Make sure location and appearance settings get restored. pushStyle(); // Sometimes custom playNote solutions leave their fill() etc. intact. musicians[0].playNote(n, mouseIn, ispressed); popStyle(); popMatrix(); } if (isBoundingCircle) { stroke(0, 0, 99); noFill(); strokeWeight(2); circle(0, 0, domeWidthHeight); } // Only use 2D clipping when globalShearXSpeed != 0. if (globalShearXSpeed != 0) { pushMatrix(); noFill(); // Just in case display() sets a fill(). noStroke(); // Just in case display() sets a stroke(). noTint(); // Just in case display() sets a tint(). // Clip the center area using the clippingCircle PNG image, // then clip the rectangular area around it using clip(). imageMode(CENTER); translate(0, 0, 2); // put clipping shapes in front of any graphics image(clippingCircle, 0, 0, width, height); // clip(width/2, height/2, domeWidthHeight, domeWidthHeight); popMatrix(); } popMatrix(); // RESTORE GLOBAL COORDS. } /** overlap checks whether two objects' bounding boxes overlap **/ boolean overlap(Note note1, Note note2) { int [] bb1 = note1.get2DBoundingBox(); int [] bb2 = note2.get2DBoundingBox(); // If bb1 is completely above, below, // left or right of bb2, we have an easy reject. if (bb1[0] > bb2[2] // bb1_left is right of bb2_right || bb1[1] > bb2[3] // bb1_top is below bb2_bottom, now reverse them || bb2[0] > bb1[2] // bb2_left is right of bb1_right || bb2[1] > bb1[3] // bb2_top is below bb1_bottom, now reverse them ) { return false ; } // In this case one contains the other or they overlap. return true ; } // Variant of overlap that takes x,y as first argument pair boolean isWithin(int x, int y, Note note1) { int [] bb1 = note1.get2DBoundingBox(); return (x >= bb1[0] && x <= bb1[2] && y >= bb1[1] && y <= bb1[3]); } // STUDENT REQUIREMENTS FOR mapXY(): You must rewrite mapXY so that: // 1. It always returns a location within the "dome", which is a circle centered // on the sketch's 0,0,0 coordinate, which is the center of the window or projector. // 2. Try to avoid placing two Note objects at the same location. My solution // accomplishes that by using parameters pitch, minpitch, and maxpitch to position // the Notes in a circle around the center of the diplay, and uses velocityStep and // velocityStepsTotal to determine the distance of each Note from the 0,0,0 center. // I will give you pseudocode for my solution. // 3. You may use an approach other than mine in requirement 2 (previous paragraph); // if you do, try to arrange Notes in some logical, regular order by pitch & velocityStep. // NOTE: If you use fewer than all of this function's parameters, you will get // warnings (not errors) about unused parameter(s). Just ignore those warnings. int [] mapXY(int pitch, int minpitch, int maxpitch, int velocityStep, int velocityStepsTotal) { int [] result = new int[2]; result[0] = round(random(-domeWidthHeight / 2, domeWidthHeight / 2)); result[1] = round(random(-domeWidthHeight / 2, domeWidthHeight / 2)); return result ; } /* int [] mapXY(int pitch, int minpitch, int maxpitch, int velocityStep, int velocityStepsTotal) { STUDENT - PSEUDOCODE FOR ARRANGING Note OBJECTS IN CONCENTRIC CIRCLES AS ILLUSTRATED IN THE ASSIGNMENT HANDOUT: 1. CALL polarToCartesian() TO MAP POLAR COORDINATES TO CARTESIAN COORDINATES (float [] polarToCartesian(float radius, float angleInRadians): 1A. The radius parameter is distance from center in the range [0.0, 1.0]. I used the ratio of velocityStep/velocityStepsTotal to come up with this number. Actually, I used (velocityStep+1.0)/(velocityStepsTotal+1.0), so that Notes at the velocityStep==0 step would not pile up in the middle of the screen. After looking at the result, I multiplied that ratio by 1.1 in order to spread notes out. That is not required. 1B. My angleInRadians is radians(360.0 * the ratio pitchDistance / maxDistance), where pitchDistance is the distance from pitch to minpitch, and maxDistance is the distance from maxpitch to minpitch + 1. The "+ 1" keeps the final row of notes from landing on top of the initial row. Note the application of the radians() function to convert degrees to radians. 2. My solution takes the 2 floats returned by polarToCartesian and passes them to cartesianToPhysical() to get screen coordinates, with cartesianToPhysical() assuming that 0,0 is in the upper-left corner of the physical display. 3. My solution subtracts width/2 from the x coordinate returned by cartesianToPhysical, and subtracts height/2 from the y coordinate returned by cartesianToPhysical, because draw() translates the 0,0,0 reference point to the center of the display. 4. My solution returns this array returned by cartesianToPhysical() as modfied by step 3. STUDENTS DO NOT HAVE TO USE THIS APPROACH. ANYTHING THAT SATISFIES THE 3 "STUDENT REQUIREMENTS FOR mapXY()" IS OK. } */ /** * Helper function supplied by Dr. Parson, student can just call it. * rotatePoint takes the unrotated coordinates local to an object's * 0,0 reference location and rotates them by angleDegrees in degrees. * Applying it to global coordinates rotates around the global 0,0 reference * point in the LEFT,TOP corner of the display. x, y are the unrotated coords. * Return value is 2-element array of the rotated x,y values. **/ double [] rotatePoint(double x, double y, double angleDegrees) { double [] result = new double [2]; double angleRadians = Math.toRadians(angleDegrees); // I have kept local variables as doubles instead of floats until the // last possible step. I was seeing rounding errors in the displayed BBs // when they are scaled when using floats. Using doubles for these calculations // appears to have eliminated those occasionally noticeable errors. // SEE: https://en.wikipedia.org/wiki/Rotation_matrix double cosAngle = (Math.cos(angleRadians)); // returns a double double sinAngle = (Math.sin(angleRadians)); result[0] = (x * cosAngle - y * sinAngle) ; result[1] = (x * sinAngle + y * cosAngle); // println("angleD = " + angleDegrees + ", cos = " + cosAngle + ", sin = " + sinAngle + ", x = " + x + ", y = " // + y + ", newx = " + result[0] + ", newy = " + result[1]); return result ; } /** * Helper function supplied by Dr. Parson, student can just call it. * rotateBB takes the (leftx, topy) and (rightx, bottomy) extents of an * unrotated bounding box and determines the leftmost x, uppermost y, * rightmost x, and bottommost y of the rotated BB, and returns these * rotated extents in a 4-element array of coodinates. * rotateBB needs to rotate every corner of the original bounding box * in turn to find the rotated bounding box as min and max values for X and Y. * Parameters: * leftx, topy and rightx, bottomy are the original, unrotated extents. * angle is the angle of rotation in degrees. * scaleXfactor and scaleYfactor are the scalings of the shape with the BB. * referencex,referencey is the "center" 0,0 point within the shape * being rotated, with is also the reference point of the BB, in global coord. * Return 4-element array holds the minx,miny and maxx,maxy rotated extents. * See http://faculty.kutztown.edu/parson/fall2019/RotateBB2D.png **/ int [] rotateBB(double leftx, double topy, double rightx, double bottomy, double angle, double scaleXfactor, double scaleYfactor, double referencex, double referencey) { int [] result = new int [4]; leftx = leftx * scaleXfactor ; rightx = rightx * scaleXfactor ; topy = topy * scaleYfactor ; bottomy = bottomy * scaleYfactor ; double [] ul = rotatePoint(leftx, topy, angle); // rotate each of the 4 corners double [] ll = rotatePoint(leftx, bottomy, angle); double [] ur = rotatePoint(rightx, topy, angle); double [] lr = rotatePoint(rightx, bottomy, angle); double minx = Math.min(ul[0], ll[0]);// find minx,miny and max,maxy from all 4 minx = Math.min(minx, ur[0]); minx = Math.min(minx, lr[0]); double maxx = Math.max(ul[0], ll[0]); maxx = Math.max(maxx, ur[0]); maxx = Math.max(maxx, lr[0]); double miny = Math.min(ul[1], ll[1]); miny = Math.min(miny, ur[1]); miny = Math.min(miny, lr[1]); double maxy = Math.max(ul[1], ll[1]); maxy = Math.max(maxy, ur[1]); maxy = Math.max(maxy, lr[1]); // scale by this shapes scale result[0] = (int)Math.round(referencex + minx) ; // left extreme result[1] = (int)Math.round(referencey + miny); // top result[2] = (int)Math.round(referencex + maxx); // right side result[3] = (int)Math.round(referencey + maxy); // bottom return result ; } void keyPressed() { if (key == CODED) { if (keyCode == UP) { globalShearXSpeed += 1 ; while (globalShearXSpeed >= 360) { globalShearXSpeed -= 360 ; } for (Note n : notes) { n.setShearXSpeed(globalShearXSpeed); } } else if (keyCode == DOWN) { globalShearXSpeed -= 1 ; while (globalShearXSpeed < 0) { globalShearXSpeed += 360 ; } for (Note n : notes) { n.setShearXSpeed(globalShearXSpeed); } } } else if (key == 'b') { isBoundingCircle = ! isBoundingCircle ; // toggle it } else if (key == 'z') { globalShearXSpeed = 0; for (Note n : notes) { n.setShearXSpeed(globalShearXSpeed); } } }