// Class Note manages the display and NOTE_ON/NOTE_OFF of a constant-pitch, // constant-velocity note on the visual "keyboard". class Note { int pitch ; // first argument for NOTE_ON and NOTE OFF int velocity ; // second argument for NOTE_ON and NOTE OFF int x, y ; // x,y coordinates relative to 0,0 in center of display int wide, high ; // width, height of this shape int Hue, Sat, Brt ; // Hue, Saturation, Brightness of this Note object. int musicalScaleStep ; float velix, volLevels ; final PShape shp ; int nesting = 0 ; int nestSpeed = 1 ; final int nestLimit = 10 ; final int nestDelayLimit = 1 ; int nestDelay = nestDelayLimit ; Map isSounding = new HashMap(); // Map channel to whether it is sounding or not on each channel, initially not. int noteson = 0 ; // across all channels Note(int pitch, int velocity, int x, int y, int wide, int high, int Hue, int Sat, int Brt, int musicalScaleStep, int velix, int volLevels) { // Copy parameters into data fields. this.pitch = pitch ; this.velocity = velocity ; this.x = x ; // Prefix class data field with "this." when function parameter or this.y = y ; // local variable has the same variable name. "this." is this object. this.wide = wide ; this.high = high ; this.Hue = Hue ; this.Sat = Sat ; this.Brt = Brt ; this.musicalScaleStep = musicalScaleStep ; this.velix = velix ; this.volLevels = volLevels ; shp = SVGshapes[musicalScaleStep % SVGshapes.length]; for (int c = 0 ; c < 16 ; c++) { // There are up to 16 channels (instruments) in MIDI. isSounding.put(c, false); // Each channel is NOT sounding at the start. } } // Display the Note, NOTE_ON when isBeingSounded on channel, NOTE_OFF when not. // STUDENT: Extend this function to add parameters tonic and scaleix // to this function. Add code after the final popMatrix() below; // see STUDENT comment at the bottom of this function. void display(boolean isBeingSounded, int channel) { pushMatrix(); pushStyle(); translate(x, y); try { if (isBeingSounded || isSounding.get(channel)) { fill(Hue, 99, 99); tint(Hue, 99, 99); if (isBeingSounded) { stroke(0, 0, 0); } else { stroke(Hue,99,99); } } else { fill(Hue, 99, 99 /* Brt */); // Brt is dimmer for lower-velocity notes. tint(Hue, 99, 99 /*Brt*/); stroke(Hue,99,99); } // rect(0, 0, wide, high); float scaler = ((127-velocity) / 64.0) + 3; // float scalerBIG = scaler * 10.0 ; image(hexagon, 0, 0, wide * scaler, high * scaler); int minny = min(width, height) ; if (isBeingSounded || isSounding.get(channel) || noteson > 0) { pushMatrix(); shp.setStrokeWeight(1); shp.setFill(color(Hue, 99, 99, 2)); // Draw nested translucent shapes, full screen, at the center. for (float sc = 1.0 ; sc > 0.1 ; sc -= 0.25) { shape(shp, -x, -y, minny*sc, minny*sc); } stroke(0,0,0); // black fill(Hue, 99, 99); tint(Hue, 99, 99); float tallness ; if (velix == 0) { tallness = 100.0 ; } else if (velix == 1) { tallness = 550.0 ; } else { tallness = 875.0 ; } noStroke(); float xoffset = wide * scaler * (random(-0.1, 0.1)); float yoffset = high * scaler * (random(-0.1, 0.1)); int tr = 1 ; float nestScaleHeight = (nesting+1.0) / nestLimit ; LinkedList TR = new LinkedList (); for (float sc = 2.0 ; sc <= tallness * nestScaleHeight ; sc += 20.0, tr += 1) { if (tr > 15) { tr = 15 ; } TR.add(tr); } //int lasttr = 0 ; // PShape shp = SVGshapes[musicalScaleStep % SVGshapes.length]; //shp.setStroke(color(0,0,0)); shp.setStrokeWeight(1); shp.setFill(color(Hue, 99, 99)); while (TR.size() > 0) { int trelem = TR.pollFirst(); //lasttr = trelem ; translate(0, 0, trelem); // cumulative increases in Z // println("TR " + trelem); ; shape(shp, 0 + xoffset, 0 + yoffset, wide * scaler, high * scaler); // scale(1.02, 1.02, 1); // Experiment - make bigger as they go up. } //translate(0, 0, lasttr); // Nest the topmost one. shp.setStrokeWeight(1); for (int nst = 0 ; nst < nesting ; nst++) { scale(.85); shape(shp, 0 + xoffset, 0 + yoffset, wide * scaler, high * scaler); } nestDelay-- ; if (nestDelay <= 0) { nestDelay = nestDelayLimit ; nesting += nestSpeed ; if (nesting > nestLimit || nesting == 0) { nestSpeed = - nestSpeed ; } } popMatrix(); if (! isSounding.get(channel)) { ShortMessage newMessage = new ShortMessage() ; int v = (killfade < 0) ? velocity : round(killfade * velocity); newMessage.setMessage(ShortMessage.NOTE_ON, channel, pitch, v); receiver.send(newMessage, -1L); // send it now // if (isOSCmessage) { isSounding.put(channel, true); // } noteson++ ; } } if (isSounding.get(channel) && ! isBeingSounded) { ShortMessage newMessage = new ShortMessage() ; newMessage.setMessage(ShortMessage.NOTE_OFF, channel, pitch, velocity); receiver.send(newMessage, -1L); // send it now isSounding.put(channel, false); noteson-- ; } } catch (InvalidMidiDataException dx) { System.err.println("MIDI ERROR: " + dx.getMessage()); // Error messages go here. } popStyle(); popMatrix(); // STUDENT (THESE INSTRUCTIONS ADDRESS NOTES, AND THEN GRAPHICS): // IF TONIC and SCALEX ARE BOTH > 0, THEN DO THE FOLLOWING: // Call function mapNoteToChord() that appears at the bottom // of this tab and store the returned Note [] in a local variable. // For each of the non-null Note objects in the returned array: // Call its Note.display function, passing parameters isBeingSounded // and channel as they are passed into this function, and passing // tonic and scaleix as -1 values. Otherwise, each chord Note would // play *ITS* chord Notes, in an infinite recursion. // Use pushStyle() before this block of code and popStyle() after to // save the current stroke color & weight via pushStyle(), and popStyle() // to restore it. // Draw a thick line from the current Note to its opposite across the // center -- from x, y to -x, -y at Z coordinate 0 in the current Hue -- // and do the same for the Note objects returned by mapNoteToChord(), // using their x and y. I will post a video of chord playing. } // We will need this only if we get to moving Note objects in assn4. // Note that my rotateBB function is available if we do 2D rotation. int [] get2DBoundingBox() { int [] result = new int [4]; result[0] = x - wide/2 ; result[1] = y - high/2 ; result[2] = x + wide/2 ; result[3] = y + high/2 ; return result; } int getX() { return x ; } int getY() { return y ; } } // Added by Parson Groundhog Day 2020 for CSC480 Assignment 1 // STUDENT: You must figure out how to use mapNoteToChord(). // WARNINGS: Return value will not be null, but it may have a .length of 0, // and if non-0, some of its elements may be null because they are out of the // server's range of Note objects for upper octaves. // DO NOT MAKE ANY CHANGES TO FUNCTION mapNoteToChord(). // You may add comments if you like, to help you remember how it works. Note [] mapNoteToChord(int pitch, int velocity, int tonic, int scaleix) { if (scaleix < 0 || scaleix > scales.length || tonic < 0 || tonic > 127) { return new Note[0]; } int pitchOffsetFromTonic = (pitch - tonic + 12) % 12 ; int [][] intervals = chords[scaleix]; int [] entry = intervals[pitchOffsetFromTonic % intervals.length]; Note [] result = new Note [ entry.length ] ; for (int i = 0 ; i < entry.length ; i++) { NoteKey key = new NoteKey(pitch + entry[i], velocity); result[i] = notemap.get(key); } return result ; }