/** CSC220W20Androidassn4ClientParson to allow selectable scale, tonic on client, start of 2020 year. CSC220F19Androidassn4ClientParson, D. Parson, November 2019, CSC220, derived from CSC220F19MIDIassn3parson. ENTER STUDENT NAME HERE: Dr. Parson's solution Planetarium-oriented assignment 4, creating a remote control on Android for server sketch CSC220F19MIDIassn4Server. INTERACTION: Mouse location with mousePressed variable determines velocity & pitch of NOTE_ON. 'b' toggles painting the perimeter of the dome (bounding circle). UP and DOWN 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 = 16 ; // 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 = 1 ; // STUDENT: CHANGE controlEffectData2 0 to 127 for how much of controlEffectNumber to use. final int controlEffectData2 = 127 ; // DEGREE OF CONTROL MESSAGE effect // 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 */ // THE FOLLOWING MUST BE SAME ON CLIENT AND SERVER // 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, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11}, {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 {0, 2, 4, 7, 9}, // Major pentatonic: DO RE MI SO LA (next DO not shown) {0, 2, 3, 7, 8} // Minor pentatonic }; final String [] scaleName = {"chromatic", "major", "minor", "majpenta", "minpenta"}; 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 = 3 ; // Don't go to 0, not needed. // MIDI STATE VARIABLES (For assn3 we are keeping these constant.): // scaleix and tonic changed from final to volatile 12/30/2019 so client menus can change them. volatile int scaleix = 1 ; // index into scales and scaleName, cycle using 's' and 'S' volatile int tonic = 7 ; // index into scales array, cycle using 't' and 'T' // ABOVE SAME ON CLIENT, SERVER // Note objects volatile Note [] notes = null ; // allocate in setupNotes(). 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(P2D); // We are popping up 3D shapes when a note is hit. // size(762,1024,P2D); // Android / 2 with portrait orientation // size(1024,762,P2D); // Android Galaxy tablet / 2 with landscape orientation // size(1280,762,P2D); // Expand towards Android Galaxy width. 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 ; setupNotes(); // 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); // We are only using 1 Musician in assignment 3, more later. startOSC(); orientation(LANDSCAPE); } void setupNotes() { // call this after every tonic or scaleix change, initially from setup() // setup() places the Note object positions and other properties: notes = new Note[ (scales[scaleix].length * octaveCount + 1) * volumeLevels ]; int noteindex = 0 ; // index into the notes[] array. int volstep = 120 / (volumeLevels+1) ; // do not make Notes for the 0-velocity level int notesize = 3 * 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); int [] xyloc = mapXY(octstep * scales[scaleix].length + notestep, 0, (octaveCount) * scales[scaleix].length, volumeLevels-velix-1, 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); int [] xyloc = mapXY(octaveCount * scales[scaleix].length , 0, (octaveCount) * scales[scaleix].length, volumeLevels-velix-1, volumeLevels); newnote = new Note(pitch, vel, xyloc[0], xyloc[1], notesize, notesize, hue, 99, 99 * vel / 127); notes[noteindex++] = newnote ; } } void draw() { background(0, 0, 0); // black for planetarium pollLooper(); if (! connectionReady()) { // STUDENT ADD THIS SECTION FOR ASSN4. return ; } pushMatrix(); translate(domeTranslateX, domeTranslateY); // 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 %) noFill(); ellipseMode(CENTER); rectMode(CENTER); shapeMode(CENTER); imageMode(CENTER); Note toplay = null ; // Added 11/29/2019 for closest note float todist = Float.MAX_VALUE; if (mousePressed) { int mx = mouseX - domeTranslateX ; int my = mouseY - domeTranslateY ; float distance = dist(0, 0, mx, my); // Added 12/30/2019, only sound if in circle. if (distance <= domeWidthHeight/2) { for (Note n : notes) { distance = dist(n.x, n.y, mx, my); if (toplay == null || distance < todist) { toplay = n ; todist = distance ; } } } } for (Note n : notes) { pushMatrix(); // Make sure location and appearance settings get restored. pushStyle(); // Sometimes custom playNote solutions leave their fill() etc. intact. for (int m = 0 ; m < /* musicians.length */ 1 ; m++) { // Reserve channel 0 for testing only. // println("DEBUG m " + m + " who " + musicians[m] + " what " + n); musicians[m].playNote(n, toplay == n, toplay == n, oscClientMidiChan); } popStyle(); popMatrix(); } if (isBoundingCircle) { stroke(0, 0, 99); noFill(); strokeWeight(2); circle(0, 0, domeWidthHeight); } /// Use 2D clipping only when globalShearXSpeed != 0 because it hides strokes. 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); 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]); } // Variant of overlap that takes x,y as first argument pair and 4-element bounding box as second boolean isWithin(int x, int y, int [] bb1) { 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. /* REPLACE WITH POLAR LAYOUT 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. */ float [] cartesian = polarToCartesian(1.1*(velocityStep+1.0)/(velocityStepsTotal+1.0), radians(360.0 * (pitch-minpitch) / (maxpitch-minpitch+1))); int [] result = cartesianToPhysical(cartesian[0], cartesian[1]); // float [] debug = physicalToCartesian(result[0], result[1]); // println("DEBUG PC " + cartesian[0] + "," + cartesian[1] + " -> " + result[0] + "," + result[1] + " -> " + debug[0] + "," + debug[1]); result[0] -= width/2 ; result[1] -= height/2 ; return result ; } /** * 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); } } }