Search code examples
enumsarduinohashtableteensy

Implementing enum like functionality with custom types


I'm working on a USB MIDI controller using a Teensy. The controller is a row of 7 buttons, each button is a progression degree and the 7 buttons make up a chord progression. When pressed the device sends a MIDI note on/off message to play a chord.

In my code I have intervals stored in an enum:

/*
 * Intervals
 */
 typedef enum {
   ROOT = 0,
   UNISON = 0,
   DIMINISHED_SECOND = 0,
   MINOR_SECOND = 1,
   AUGMENTED_UNISON = 1,
   HALFSTEP = 1,
   MAJOR_SECOND = 2,
   DIMINISHED_THIRD = 2,
   WHOLESTEP = 2,
   MINOR_THIRD = 3,
   AUGMENTED_SECOND = 3,
   MAJOR_THIRD = 4,
   DIMINISHED_FOURTH = 4,
   PERFECT_FOURTH = 5,
   AUGMENTED_THIRD = 5,
   DIMINISHED_FIFTH = 6,
   AUGMENTED_FOURTH = 6,
   PERFECT_FIFTH = 7,
   DIMINISHED_SIXTH = 7,
   MINOR_SIXTH = 8,
   AUGMENTED_FIFTH = 8,
   MAJOR_SIXTH = 9,
   DIMINISHED_SEVENTH = 9,
   MINOR_SEVENTH = 10,
   AUGMENTED_SIXTH = 10,
   MAJOR_SEVENTH = 11,
   DIMINISHED_OCTAVE = 11,
   PERFECT_OCTAVE = 12,
   AUGMENTED_SEVENTH = 12,
   DIMISHED_NINTH = 12,
   MINOR_NINTH = 13,
   AUGMENTED_OCTAVE = 13,
   MAJOR_NINTH = 14,
   DIMINISHED_TENTH = 14,
   MINOR_TENTH = 15,
   AUGMENTED_NINTH = 15,
   MAJOR_TENTH = 16,
   DIMINISHED_ELEVENTH = 16,
   PERFECT_ELEVENTH = 17,
   AUGMENTED_TENTH = 17,
   DIMINISHED_TWELFTH = 18,
   AUGMENTED_ELEVENTH = 18,
   PERFECT_TWELFTH = 19,
   DIMINISHED_THIRTEENTH = 19,
   MINOR_THIRTEENTH = 20,
   AUGMENTED_TWELFTH = 20,
   MAJOR_THIRTEENTH = 21,
   DIMINISHED_FOURTEENTH = 21,
   MINOR_FOURTEENTH = 22,
   AUGMENTED_THIRTEENTH = 22,
   MAJOR_FOURTEENTH = 23,
   DIMINISHED_FIFTEENTH = 23,
   PERFECT_FIFTEENTH = 24,
   AUGMENTED_FOURTEENTH = 24,
   AUGMENTED_FIFTEENTH = 25
 } INTERVAL;

I also have an array of chords, like so:

struct Chord {
   String name;
   int tones[7];
 };

Chord chords[6] = {
  { "maj", {
    INTERVAL::UNISON,
    INTERVAL::MAJOR_THIRD,
    INTERVAL::PERFECT_FIFTH }
  },
  { "min", {
    INTERVAL::UNISON,
    INTERVAL::MINOR_THIRD,
    INTERVAL::PERFECT_FIFTH }
  },
  { "maj7", {
    INTERVAL::UNISON,
    INTERVAL::MAJOR_THIRD,
    INTERVAL::PERFECT_FIFTH,
    INTERVAL::MAJOR_SEVENTH }
  },
  { "min7", {
    INTERVAL::UNISON,
    INTERVAL::MINOR_THIRD,
    INTERVAL::PERFECT_FIFTH,
    INTERVAL::MINOR_SEVENTH }
  },
  { "maj9", {
    INTERVAL::UNISON,
    INTERVAL::MAJOR_THIRD,
    INTERVAL::PERFECT_FIFTH,
    INTERVAL::MAJOR_SEVENTH,
    INTERVAL::MAJOR_NINTH }
  },
  { "min9", {
    INTERVAL::UNISON,
    INTERVAL::MINOR_THIRD,
    INTERVAL::PERFECT_FIFTH,
    INTERVAL::MINOR_SEVENTH,
    INTERVAL::MINOR_NINTH }
  }
};

I'd like to access the chords in a similar way to the enum of intervals so I can do something like this (psudeocode):

void playChord(Chord chord, int velocity, int channel) {
    int i;
    for(i=0; i<chord.length; i++) {
        usbMIDI.sendNoteOn(chord[i], velocity, channel);
    }
}

playChord(Chord::MAJOR, 127, 1);

I know it's not possible to have an enum of custom types, but is there any way I could get close to this? I've considered using a HashTable, but I'd have to implement it from scratch and I don't fancy that if I can help it.


Solution

  • The point of an enum is that you create a new type that can only take on a fixed set of values. It is appropriate to use an enum for your intervals because there are only so many intervals in actual use, and because creating a new type is more convenient than using integer constants here.

    The story is different for your chords. You already have a type for your chords, so wrapping them in another enum type is not helpful. Also, the number of chords is far less finite. A chord chart I have at hand shows 22 shapes, but that does not include inversions. Your chord struct is far more appropriate than artificially limiting chords with an enum.

    C has two other mechanisms to create “constants” other than enums: preprocessor-defines and static variables.

    With a preprocessor directive, we can define a Chord literal. IIRC struct literals are a C99 thing, previously there could only be initializer literals.

    #define CHORD_MAJOR ((Chord){"maj", {ROOT, MAJOR_THIRD, PERFECT_FIFTH}})
    

    With a static variable, you would declare an object in a header:

    static const Chord chord_major = {"maj", {ROOT, MAJOR_THIRD, PERFECT_FIFTH}};
    

    Note that C does not have a namespace operator like ::. Instead, you have to prefix any possibly clashing identifiers yourself. C++ does have namespaces, but that doesn't affect the points made in this answer.