/** STUDENT NAME: Dale E. Parson CSC480S20MIDIassn1ServerParson is a solution to CSC480 Spring 2020 assn1 server. CSC480S20MIDIassn1Server is derived from CSC220W20MIDIassn4ServerParson, to add Note chords in the server sketch. This is the starting point student server code for Assignment 1 in CSC480 Spring 2020. CSC220W20MIDIassn4ServerParson, enhanced start of 2020 for multiple client scales, by displaying chromatic on the server. CSC220F19MIDIassn4ServerParson, enhanced for December 2, 2019 gig. CSC220F19MIDIassn4Server, D. Parson, November 2019. derived from CSC220F19MIDIassn3, D. Parson, October 2019, CSC220 Planetarium-oriented assignment 4 server, this accepts OSC/UDP messages from student clients running on PCs, laptops, or Androids, and plays them on a MIDI keyboard layout from Dr. Parson's Assignment 3 solution. INTERACTION: Mouse location with mousePressed variable determines velocity & pitch of NOTE_ON. 'b' toggles painting the perimeter of the dome (bounding circle). 'r' increases rotate speed. 't' decreases rotate speed. 's' stops (zeroes) rotate speed. 'z' stops (zeroes) rotate speed and rotate amount. 'R' turns off all sounding notes 'X' 3 times initiates closure of the sketch /* OSC PROTOCOL: all datagrams from client start out with /* a) Connecting to IP of CSC220F19MIDIassn4Server process, sending /* initial signup, and receiving reply. /* CLIENT: SEND DATAGRAM: /csc480S20Midi1/client IPCLIENTADDRSTRING UDPCLIENTPORTINT midichannel pitch velocity tonic scaleix /* midichannel pitch velocity are unused placeholders in above message /* SERVER: RECV DATAGRAM: /csc480S20Midi1/server (dataless acknowledgement) /* b) Client sends proxy NOTEON or NOTEOFF messages to server, no reply. /* The server ignores pitch velocity tonic scaleix on messages other than /* noteon and noteoff; it uses midichannel on clearmine; those parameters /* can be anything, convention is to set them to -1 when not used by the command. /* /csc480S20Midi1/noteon IPCLIENTADDRSTRING UDPCLIENTPORTINT midichannel pitch velocity tonic scaleix /* /csc480S20Midi1/noteoff IPCLIENTADDRSTRING UDPCLIENTPORTINT midichannel pitch velocity tonic scaleix /* /csc480S20Midi1/clearmine IPCLIENTADDRSTRING UDPCLIENTPORTINT midichannel pitch velocity tonic scaleix /* /csc480S20Midi1/clearall IPCLIENTADDRSTRING UDPCLIENTPORTINT midichannel pitch velocity tonic scaleix /* where "clearmine" applies noteoff to midichannel, and /* "clearall" applies noteoff to all sounding notes. **/ // 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 import oscP5.*; import netP5.*; import java.util.* ; import java.util.concurrent.ConcurrentLinkedQueue ; final int midiDeviceIndex = 0 ; // setup() checks for number of devices. Use one for output. final String CommandPrefix = "/csc480S20Midi1/"; String [] SVGs = { "atom7e.4.svg", "atom6e.0.svg", "atom2e.2.svg", "atom4e.1.svg", "atom4e.3.svg" }; PImage hexagon ; //PShape hexatube ; PShape [] SVGshapes = new PShape [ SVGs.length ] ; int killcount = 0 ; final int killlimit = 3 ; float killfade = -1.0 ; final float killdrop = 1.0 / 32.0 ; // OscP5 oscP5 = null; NetAddress myRemoteLocation; /* NoteKey uniquely determines the note to play or silence. */ class NoteKey implements Comparable { // The pitch + velocity gives you the Note object key. final int pitch ; // pitch and velocity are both in the range [0, 127]. final int velocity ; NoteKey(int pitch, int velocity) { this.pitch = pitch ; this.velocity = velocity ; } public boolean equals(Object obj) { if (obj instanceof NoteKey) { NoteKey nobj = (NoteKey) obj ; return ((pitch == nobj.pitch) && (velocity == nobj.velocity)); } return false ; } public int hashCode() { return((velocity << 1) ^ pitch); // XOR hashes bits, pitch may be even or odd } public int compareTo(NoteKey other) { // pitch is the primary sort key, velocity secondary if (pitch < other.pitch) { return -1 ; } else if (pitch > other.pitch) { return 1 ; } else if (velocity < other.velocity) { return -1 ; } else if (velocity > other.velocity) { return 1 ; } else { return 0 ; } } public String toString() { return "NOTE, pitch = " + pitch + ", velocity = " + velocity; } } // NoteKeyChannel used in Set noteson for unsticking stuck notes. class NoteKeyChannel implements Comparable { final NoteKey notekey ; final int channel ; NoteKeyChannel(NoteKey notekey, int channel) { this.notekey = notekey ; this.channel = channel ; } public boolean equals(Object obj) { if (obj instanceof NoteKeyChannel) { NoteKeyChannel nobj = (NoteKeyChannel) obj ; return ((notekey.equals(nobj.notekey) && (channel == nobj.channel))); } return false ; } public int hashCode() { return(notekey.hashCode() ^ channel); // XOR hashes bits, pitch may be even or odd } public int compareTo(NoteKeyChannel other) { int mainCompare = notekey.compareTo(other.notekey); if (mainCompare != 0) { return mainCompare ; } else if (channel < other.channel) { return -1 ; } else if (channel > other.channel) { return 1 ; } else { return 0 ; } } public String toString() { return "NOTEKEYCHAN: " + notekey.toString() + ", chan = " + channel ; } } /* ClientMessage adds the IP info, MIDI channel and noteon|off for NoteKey client device. */ class ClientMessage { final String clientIP ; final int clientPort ; final String command ; // "client" "noteon" "noteoff" final NoteKey notekey ; final int midichannel ; final int mtonic ; final int mscaleix ; ClientMessage(String clientIP, int clientPort, String command, NoteKey notekey, int midichannel, int tonic, int scaleix) { this.clientIP = clientIP ; this.clientPort = clientPort ; this.command = command ; this.notekey = notekey ; this.midichannel = midichannel ; this.mtonic = tonic ; this.mscaleix = scaleix ; } public String toString() { return "DEBUG OSC message, clientIP = " + clientIP + ", clientPort = " + clientPort + " " + command + " " + notekey.toString() + " chan = " + midichannel ; } } final Map notemap = new HashMap(); // add these when building notes final Set noteson = new HashSet(); // This Queue brings client OSC messages to the Processing GUI thread: final ConcurrentLinkedQueue IncomingOSCqueue = new ConcurrentLinkedQueue(); // This set used to keep a set of client IP:port pairs: final Set ClientIPset = Collections.synchronizedSet(new HashSet()); // MIDI OUTPUT DEVICE SELECTION: // 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}, // chromatic {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 int [][][] chords = { // Each deepest entry here is what to add to pitch. // Tonic is the leftmost entry. {{4,7}}, // Use major triads for chromatic scale. {{4,7}}, // Major triads for major scale. {{3,7}}, // Minor triads for minor scale. {{7}, {7}, {7}, {7}, {7}}, // 3rd-skipping intervals for major pentatonic. {{7}, {7}, {7}, {7}, {7}} // 3rd-skipping intervals for minor pentatonic. }; final String [] noteName = {"C", "Db", "D", "Eb", "E", "F", "Gb", "G", "Ab", "A", "Bb", "B"}; final int octaveCount = 5 ; // 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.): // ABOVE SAME ON CLIENT, SERVER, next two can vary per Client. final int tonic = 7 ; // index into scales array, cycle using 't' and 'T' final int scaleix = 0 ; // index into scales and scaleName, cycle using 's' and 'S' // Note objects final Note [] notes = new Note[ (scales[scaleix].length * octaveCount + 1) * volumeLevels ]; final Musician [] musicians = new Musician[16] ; // Up to sixteen musicians final int [] programNumber = new int[16]; // One per channel, figure dedicated numbers out later final int [] controlEffectNumber = new int[16]; // One per channel, figure dedicated numbers out later final int [] controlEffectData2 = new int[16]; // One per channel, figure dedicated numbers out later // MISC. VARIABLES: PImage clippingCircle ; // Only used when globalShearXSpeed != 0 int domeTranslateX, domeTranslateY, domeWidthHeight, domeRadius ; boolean isBoundingCircle = false ; final float rotateSpeedLimit = TWO_PI / (360.0 * 8) ; float rotateSpeed = 0 ; float rotateAmount = 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 ; for (int i = 0 ; i < SVGs.length ; i++) { SVGshapes[i] = loadShape(SVGs[i]); // PNGimgs[i].resize(notesize, notesize); } hexagon = loadImage("hexagon.png"); // hexagon.resize(notesize, notesize); //hexatube = makeCylinder(notesize, notesize, 6, 255, 255, 255); // hexatube.setStroke(color(0,0,0,0)); // 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 = 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, notestep, velix, volumeLevels); notes[noteindex++] = newnote ; notemap.put(new NoteKey(pitch, vel), 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, 0, velix, volumeLevels); notes[noteindex++] = newnote ; notemap.put(new NoteKey(pitch, vel), 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.pixels[pix] = 0x003000000 ; // Set it to erase 3/255 } } } 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: println("DELAY MIDI 1"); // 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. println("DELAY MIDI 2"); 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 for (int m = 0 ; m < musicians.length ; m++) { musicians[m] = new Musician(m * 4, 1, 127, m); ShortMessage noteMessage = new ShortMessage() ; noteMessage.setMessage(ShortMessage.PROGRAM_CHANGE, m, 73 /*(m==0) ? 73 : m * 4*/, 0); // to channel m receiver.send(noteMessage, -1L); // send it now println("DELAY MIDI 3"); } println("DELAY MIDI 4"); } catch (MidiUnavailableException mx) { System.err.println("MIDI UNAVAILABLE"); // Error messages go here. device = null ; receiver = null ; // Do not try to use them. // exit(); } catch (InvalidMidiDataException dx) { System.err.println("MIDI ERROR: " + dx.getMessage()); // Error messages go here. } //musicians[0] = new Musician(0, 0, 0, 0); // We are only using 1 Musician in assignment 3, more later. // oscP5 = new OscP5(this,12001); // Start the OSC/UDP server // myRemoteLocation = new NetAddress("127.0.0.1",12001); /* myRemoteLocation is a NetAddress. a NetAddress takes 2 parameters, * an ip address and a port number. myRemoteLocation is used as parameter in * oscP5.send() when sending osc packets to another computer, device, * application. usage see below. for testing purposes the listening port * and the port of the remote location address are the same, hence you will * send messages back to this sketch. */ background(0, 0, 0); // black for planetarium println("DELAY MIDI 5, width = " + width + ", height = " + height); /* try { Thread.sleep(1); } catch(InterruptedException xxxx) { } */ } void draw() { if (oscP5 == null) { oscP5 = new OscP5(this,12001); // Start the OSC/UDP server myRemoteLocation = new NetAddress("127.0.0.1",12001); } 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 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(); if (killfade > 0.0 && killfade > killdrop) { killfade -= killdrop ; } else if (killfade >= 0.0) { popMatrix(); return ; // exit(); } rotateZ(rotateAmount); processOSCevents(); // Don't do this until after the global 0,0 xlate & scale. rotateAmount += rotateSpeed ; while (rotateAmount > TWO_PI) { rotateAmount -= TWO_PI ; } while (rotateAmount < -TWO_PI) { rotateAmount += TWO_PI ; } 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); 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 ; for (Note n : notes) { float 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, -1, -1); } 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]); } /* incoming osc message are forwarded to the oscEvent method. */ // oscEvent runs in an OSC thread, so send command via a thread-safe queue // STUDENT: Extend this function to copy tonic and scaleix from the OSC // message into the ClientMessage object constructed here. // Make sure to update the args.length test to account for the two new // fields for tonic and scaleix. void oscEvent(OscMessage theOscMessage) { String addrstr = theOscMessage.addrPattern().trim(); Object [] args = theOscMessage.arguments(); if (addrstr != null && addrstr.startsWith(CommandPrefix) && args.length == 7) { String cmd = addrstr.substring(addrstr.lastIndexOf('/')+1).trim(); String clientip = (String) args[0]; int clientport = ((Integer)args[1]).intValue(); int channel = ((Integer)args[2]).intValue(); // println("DEBUG INCOMING MIDI CHANNEL= " + channel); int pitch = ((Integer)args[3]).intValue(); int velocity = ((Integer)args[4]).intValue(); int mytonic = ((Integer)args[5]).intValue(); int myscaleix = ((Integer)args[6]).intValue(); NoteKey n = new NoteKey(pitch, velocity); ClientMessage msg = new ClientMessage(clientip.trim(), clientport, cmd.trim(), n, channel, mytonic, myscaleix); IncomingOSCqueue.add(msg); } else { println("UNKNOWN INCOMING OSC MESSAGE: " + addrstr + ", args.length " + args.length); } } /* class ClientMessage { final String clientIP ; final int clientPort ; final String command ; // "client" "noteon" "noteoff" final NoteKey notekey ; final int midichannel ; final int mtonic ; final int mscaleix */ // processOSCevents() runs in the Processing draw()'s thread // STUDENT: Extend this function to pass tonic and scaleix from the // incoming ClientMessage object to function m.playNote below. void processOSCevents() { while (IncomingOSCqueue.size() > 0) { ClientMessage message = IncomingOSCqueue.poll(); if (message != null) { // println("DEBUG CLIENT CONTACT! " + message.toString()); if ("client".equals(message.command)) { println("DEBUG CLIENT REGISTRATION! " + message.toString()); // TODO register and respond to client device ClientIPset.add(message); // This is the only server-to-client message in Fall 2019 assn4 OscMessage myMessage = new OscMessage(CommandPrefix + "server"); NetAddress cliRemoteLocation = new NetAddress(message.clientIP,message.clientPort); oscP5.send(myMessage, cliRemoteLocation); } else if ("clearmine".equals(message.command)) { clearStuckNotes(message.midichannel); } else if ("clearall".equals(message.command)) { clearStuckNotes(-1); } else { Note n = notemap.get(message.notekey); if (n != null && message.midichannel < 16 && message.midichannel >= 0 && (("noteon".equals(message.command) || "noteoff".equals(message.command)))) { Musician m = musicians[message.midichannel]; /* println("DEBUG, playing musician at channel " + message.midichannel, ", patch " + musicians[message.midichannel].programNumber); */ boolean playing = "noteon".equals(message.command) ; // println("DEBUG playing for client " + message.midichannel + "," + playing); m.playNote(n, playing, message.mtonic, message.mscaleix); NoteKeyChannel nkc = new NoteKeyChannel(message.notekey, message.midichannel); if (playing) { noteson.add(nkc); } else { noteson.remove(nkc); } } else { println("ERROR IN CLIENT MESSAGE: " + message.toString() + ":n = " + n + ", chan = " + message.midichannel + ", cmd = " + message.command); } } } } } /* 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. */ int [] mapXY(int pitch, int minpitch, int maxpitch, int velocityStep, int velocityStepsTotal) { 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]); 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) { } else if (keyCode == DOWN) { } } else if (key == 'b') { isBoundingCircle = ! isBoundingCircle ; // toggle it } else if (key == 'r') { rotateSpeed += rotateSpeedLimit ; } else if (key == 't') { rotateSpeed -= rotateSpeedLimit ; } else if (key == 's') { rotateSpeed = 0 ; } else if (key == 'z') { rotateSpeed = 0 ; rotateAmount = 0 ; } else if (key == 'R') { clearStuckNotes(-1); } else if (key == 'X') { killcount++ ; println("killcount == " + killcount + " out of " + killlimit); if (killcount >= killlimit && killfade < 0.0) { println("setting killfade to 1.0"); killfade = 1.0 ; } } } void clearStuckNotes(int midichan) { if (midichan < 0 || midichan > 15) { // Clear all playing notes. for (NoteKeyChannel nkc : noteson) { // SHUT OFF THE NOTE Note n = notemap.get(nkc.notekey); if (n != null) { n.display(false, nkc.channel, -1, -1); } } noteson.clear(); } else { // Just clear the channel. Set tmpnoteson = new HashSet(noteson); for (NoteKeyChannel nkc : tmpnoteson) { // Copied noteson so we don't mutate the set we are iterating over. Note n = notemap.get(nkc.notekey); if (n != null && nkc.channel == midichan) { n.display(false, nkc.channel, -1, -1); noteson.remove(nkc); } } } } PShape makeCylinder(float radius, float height, int detail, int Red, int Green, int Blue) { textureMode(NORMAL); PShape cyl = createShape(); cyl.beginShape(QUAD_STRIP); cyl.stroke(0); cyl.strokeWeight(1); cyl.fill(Red, Green, Blue); float angle = TWO_PI / detail; for (int i = 0; i <= detail; i++) { float x = sin(i * angle); float z = cos(i * angle); float u = float(i) / detail; cyl.normal(x, 0, z); cyl.vertex(x * radius, -height/2, z * radius, u, 0); cyl.vertex(x * radius, +height/2, z * radius, u, 1); } cyl.endShape(); //cyl.rotateX(HALF_PI); cyl.translate(radius, height/2, 0); // Above code has it offset to LEFT and UP. return cyl; }