Previous page: Daily Crap 2009-05-07
Next page: Daily Crap 2009-05-09


Scale-finder

Scale-finder is a Processing sketch for finding scales containing given notes. You click on a few note names and scales without them disappear. Click them again to deselect.

12 major and minor scales are shown, major on the left, minor on the right. I didn't label them because I didn't care which scales they were for what I was doing (writing a song), but they're in order, going up by half-steps from C.

One font is required, put it in a data subdirectory: Monospaced-12.vlw.

You could also download everything (the code on this page is slightly different; some unused bits are taken out): scalefinder.tar.gz.

Or, if you're on OS X and just want to use the thing, here's a binary: scalefinder-app.tar.gz.

The code is uncharacteristically structured this time because I finally decided that I had rewritten the same hacks with arrays for too long. It's time for some objects. My two favorite performance languages are ChucK and Processing (Java), so unfortunately there is no Ruby in sight. This is the Java source for the scale-finder sketch, organized into reusable chunks by utility.

// Code

// This whole section should be copy-pastable to a Processing sketch.

// Scales

interface Scale {
  boolean contains(int note);
}

class OffsetScale implements Scale {
  int base;
  Scale scale;

  public OffsetScale(int base, Scale scale) {
    this.base = base;
    this.scale = scale;
  }

  public boolean contains(int note) {
    return scale.contains(note - base);
  }
}

class OctaveScale implements Scale {
  private int[] scale, scale_diffs;

  public OctaveScale(int[] scale) {
    this.scale = scale.clone();
    scale_diffs = calculate_diffs(scale);
  }

  // make an array of step sizes (simplifies contains())
  private int[] calculate_diffs(int[] scale) {
    int[] diffs = new int[scale.length];
    for(int i = 0; i < scale.length - 1; i++)
      diffs[i] = scale[i+1] - scale[i];
    diffs[diffs.length-1] = 12 - scale[diffs.length-1];
    return diffs;
  }

  public boolean contains(int note) {
    int peek = 0;
    while(note < 0) note += 12;
    while(note > 0) {
      note -= scale_diffs[peek];
      peek = (peek + 1) % scale_diffs.length;
    }
    return note == 0;
  }
}

Scale major = new OctaveScale(new int[] { 0, 2, 4, 5, 7, 9, 11 });
Scale minor = new OctaveScale(new int[] { 0, 2, 3, 5, 7, 8, 11 });

// Note the major and minor scale definitions at the end.
// They are in C by default, but other scales can be made
// by wrapping them in an OffsetScale

// Chords (several pressed notes)

interface Chord {
  boolean pressed(int note);
}

class KeyedChord implements Chord {
  private HashSet notes = new HashSet();

  public void press(int note) {
    Integer inote = new Integer(note);
    if(!notes.contains(inote))
      notes.add(inote);
  }

  public void unpress(int note) {
    notes.remove(new Integer(note));
  }

  public void toggle(int note) {
    if(pressed(note))
      unpress(note);
    else
      press(note);
  }

  public boolean pressed(int note) {
    return notes.contains(new Integer(note));
  }
}

class ScaleChord implements Chord {
  private Scale scale;

  public ScaleChord(Scale scale) {
    this.scale = scale;
  }

  public boolean pressed(int note) {
    return scale.contains(note);
  }
}

// Geometry! (what?)

// A computer graphics course has gotten me to start thinking with vectors
// For better or for worse, I have ported part of the library,
// which was actually nicer in C++ due to operator overloading

class Point2 {
  private float x, y;

  public Point2(float x, float y) {
    this.x = x;
    this.y = y;
  }

  public float X() { return x; }
  public float Y() { return y; }

  public Vector2 minus(Point2 p) { return new Vector2(x - p.x, y - p.y); }
  public Point2 plus(Vector2 v) { return new Point2(x + v.X(), y + v.Y()); }

  public String toString() { return "[" + x + ", " + y + "]"; }
}

class Vector2 {
  private float x, y;

  public Vector2(float x, float y) {
    this.x = x;
    this.y = y;
  }

  public float X() { return x; }
  public float Y() { return y; }
  public float Length() { return mag(x, y); }

  public Vector2 times(float factor) { return new Vector2(x * factor, y * factor); }
  public Vector2 divide(float factor) { return new Vector2(x / factor, y / factor); }

  public String toString() { return "<" + x + ", " + y + ">"; }
}

class Rect2 {
  private Point2 min, max;

  public Rect2(float minx, float miny, float maxx, float maxy) {
    this.min = new Point2(minx, miny);
    this.max = new Point2(maxx, maxy);
  }

  public Rect2(Point2 min, Point2 max) {
    this.min = min;
    this.max = max;
  }

  public Point2 Min() { return min; }
  public Point2 Max() { return max; }
  public float XMin() { return min.X(); }
  public float YMin() { return min.Y(); }
  public float XMax() { return max.X(); }
  public float YMax() { return max.Y(); }
  public float XLength() { return max.X() - min.X(); }
  public float YLength() { return max.Y() - min.Y(); }

  public Point2 Centroid() { return min.plus(max.minus(min).divide(2)); }

  public Rect2 YFlip() { return new Rect2(min.X(), max.Y(), max.X(), min.Y()); }
  public Rect2 XFlip() { return new Rect2(max.X(), min.Y(), min.X(), max.Y()); }

  public boolean Contains(Point2 p) {
    return p.X() >= min.X() && p.Y() >= min.Y() &&
           p.X() <= max.X() && p.Y() <= max.Y();
  }

  public Rect2[] XSplit(int divisions) {
    Rect2[] rects = new Rect2[divisions];
    for(int i = 0; i < divisions; i++)
      rects[i] = new Rect2( (i / (float) divisions) * XLength() + min.X(), min.Y(),
                           ((i + 1) / (float) divisions) * XLength() + min.X(), max.Y());
    return rects;
  }

  public Rect2[] YSplit(int divisions) {
    Rect2[] rects = new Rect2[divisions];
    for(int i = 0; i < divisions; i++)
      rects[i] = new Rect2( min.X(), (i / (float) divisions) * YLength() + min.Y(), max.X(),
                                    ((i + 1) / (float) divisions) * YLength() + min.Y());
    return rects;
  }

  public void Draw() { rect(min.X(), min.Y(), XLength(), YLength()); }

  public String toString() { return "rect(" + min + " - " + max + ")"; }
}

// Drawing a piano scale

class PianoWidget {
  private Rect2 frame;
  private int min_note, max_note;

  private Rect2[] key_rects;

  private Vector2 boxup, boxright;

  public PianoWidget(Rect2 frame, int min_note, int max_note) {
    this.frame = frame;
    this.min_note = min_note;
    this.max_note = max_note;

    key_rects = frame.YSplit(NumNotes());
  }

  public int MinNote() { return min_note; }
  public int MaxNote() { return max_note; }
  public int NumNotes() { return max_note - min_note + 1; }

  public boolean IsHovering(Point2 p) { return frame.Contains(p); }
  public int HoverNote(Point2 p) {
    for(int i = min_note; i <= max_note; i++)
      if(KeyRect(i).Contains(p)) {
        println(KeyRect(i) + " contained " + p);
        return i;
      }
    return -1; // error
  }

  public Rect2 KeyRect(int note) {
    if(note < min_note || note > max_note)
      return null;
    return key_rects[(key_rects.length - 1) - (note - min_note)];
  }

  public void Draw(Chord chord) {
    for(int i = 0; i < NumNotes(); i++) {
      Rect2 rect = KeyRect(min_note + i);
      Point2 center = rect.Centroid();

      stroke(0);
      fill(chord.pressed(min_note + i) ? 0 : 255);
      rect.Draw();

      stroke(128);
      fill(128);
      String caption = note_name(min_note + i);
      text(caption, center.X() - textWidth(caption) / 2, center.Y() + 6);
    }
  }
}

// A section just for one function

String note_name(int note) {
  String names[] = new String[] { "C", "C#/Db", "D", "D#/Eb", "E", "F", "F#/Gb", "G", "G#/Ab", "A", "A#/Bb", "B" };
  return names[note % 12];
}

// The Sketch

// Thankfully, all that code means that the sketch is pretty short.

PFont font;

PianoWidget piano;
KeyedChord chord = new KeyedChord();

Scale scalematrix[];
PianoWidget scalepianos[];

int min_note = 21 + 12 * 2;
int max_note = 108 - 12 * 2;

void setup() {
  size(1000, 650);
  frameRate(10);

  font = loadFont("Monospaced-12.vlw");
  textFont(font, 12);

  piano = new PianoWidget(new Rect2(10, 10, 60, 640), min_note, max_note);

  scalematrix = new Scale[24];
  scalepianos = new PianoWidget[24];
  Rect2[] rects = new Rect2(70, 10, width - 10, height - 10).XSplit(24);
  for(int i = 0; i < 12; i++) scalematrix[i] = new OffsetScale(i, major);
  for(int i = 0; i < 12; i++) scalematrix[i+12] = new OffsetScale(i, minor);
  for(int i = 0; i < 24; i++) scalepianos[i] = new PianoWidget(rects[i], min_note, max_note);
}

void draw() {
  background(30, 30, 60);
  piano.Draw(chord);

  for(int i = 0; i < scalematrix.length; i++) {
    boolean draw = true;
    for(int note = min_note; note <= max_note; note++) {
      if(chord.pressed(note) && !scalematrix[i].contains(note)) {
        draw = false;
        break;
      }
    }
    if(draw)
      scalepianos[i].Draw(new ScaleChord(scalematrix[i]));
  }
}

void mousePressed() {
  Point2 mouse = new Point2(mouseX, mouseY);
  if(piano.IsHovering(mouse)) {
    chord.toggle(piano.HoverNote(mouse));
  }
}

Comments

Click here to view the comments on this post.