2

So I'm building a MIDI controller. It has a 16-positions selector that lets the user select a "program": a playlist of commands to send to a MIDI instrument.

A push-switch allows the user to scroll through that playlist, running each command in sequence, one at a time.

The way I'm trying to implement this is as follows: Playlist is a pure abstract class and each Program is derived from it. Each Program would have their own methods for each step, an array of pointers to these functions and a public playNext() method that loop() could call whenever the push-button is pressed with program.playNext()

I'm stuck at the selection of a program stage. I guess I could have an array of pointers to each program and call *arrayOfPrograms[currentProgram]->playNext() but that would mean I would have to instantiate all the programs in setup() and keep them alive at all times...

Is there a way I could only instantiate the selected program and still call its playNext() method easily?

Sorry for the noob question. I'm fairly new to this and it gets confusing very quickly ;-) Thanks for your help.

Max

  • You want to be very careful using dynamic objects on such a tiny system. Do you actually need distinct behavior from these objects? If not, consider having just one eternal one and passing it some data. It's not that this is impossible, but on such a tiny system there is very little tolerance for any sequence of operations that could push the memory allocator into fragmentation. This is a world where you have to code with a mind to how things actually work, not just what is most expressive of ideas. – Chris Stratton Aug 22 '17 at 15:06
  • I could indeed have one single "command runner" and pass it data from arrays but then those arrays should exist in RAM at all time, right? I don't know how big these arrays could become, nor how many should be used. I figured that storing each one in its own object would allow for better RAM usage (provided objects get deleted appropriately of course) – Max Aug 22 '17 at 15:22
  • 1
    Where is the data coming from? If internal program flash, you should avoid copying it to RAM at all. If an external source, you should try to load it only some buffering ahead of as you need it. You only have 2K of ram, and that needs to handle everything. – Chris Stratton Aug 22 '17 at 15:39
1

You seem like you are almost there. Allocate the objects using new and free with delete.

// Abstract parent
class Playlist {
    public:
    virtual void playNext() = 0;
    // etc.
};
// Derived classes
class Something : public Playlist
    public:
    void playNext() {
        // your code here
    }
};
class SomethingElse : public Playlist
    public:
    void playNext() {
        // your code here
    }
};
// More derived classes......

// A pointer of the current program
Playlist *play_list = nullptr;

// A factory function to create a Playlist while also freeing previous
Playlist * create_playlist(int which_program) {
    // Important - delete old
    delete play_list;
    play_list = nullptr;
    // There are only 16 types, use a switch or if
    switch (which_program) {
        case 1:
            // Create a new one and save in global ptr
            play_list = new Something;
            break;
        case 2:
            play_list = new SomethingElse;
            break;
        // More cases here.....
    }
    return play_list;
}

[edit]--------------------------

Ok. The issue is that C++ does not have a lot of reflection options. So you can't create a class only by its name (as a string). Here's another solution I came up with. It's not completely automatic as it involves registering each class in setup. But I think it's close to what you want:

class Playlist;  //Forward declaration

// Each class will have a function to create itself
typedef Playlist * (*CreateFunc)();

// The parent class. It also holds a list of all child classes
// Could probably refactor into 2 separate classes
class Playlist{
  public:
  // Need virtual dtor
  virtual ~Playlist() {
    delete last_created;
  }

  // Put all the overridable function declarations here
  virtual void playNext() = 0;

  // This has to be called for each child. (See macro below)
  static void register_class(int id, CreateFunc f) {
    all_maps[num_maps].id = id;
    all_maps[num_maps++].f = f;
  }

  // Factory method to instantiate an object
  static Playlist * create_playlist(int id) {
      // Delete old pointer
      if (nullptr != last_created) {
          delete last_created;
          last_created = nullptr;
      }
      for (int i = 0; i < num_maps; i++) {
          // Search the list for name match
          if (id == all_maps[i].id) {
              // Call the creation func
              return all_maps[i].f();
          }
      }
      return nullptr;
  }
  protected:
  // A struct to hold the class name and a pointer to the creation function
  struct Map {
    int id;
    CreateFunc f;
  };
  static Map all_maps[32];
  static int num_maps;
  static Playlist* last_created;
};
// Static declarations
Playlist::Map Playlist::all_maps[32] = {{0, nullptr}};
int Playlist::num_maps = 0;
Playlist* Playlist::last_created = nullptr;

// Example child class
class Child1 : public Playlist{
  public:
  void playNext() {Serial.println("Child1");}

  // Must have this function and it must be static
  static Playlist * create() {return new Child1;}
};
// Another example
class Child2 : public Playlist{
  public:
  void playNext() {Serial.println("Child2");}
  static Playlist * create() {return new Child2;}
};

// Helper macro to simplify registering
#define REGISTER_PLAYLIST(id, name) Playlist::register_class(id, &name::create)


void setup() {
  Serial.begin(9600);

  // This must be done for all classes
  REGISTER_PLAYLIST(0, Child1);
  REGISTER_PLAYLIST(1, Child2);

  // Example usage
  Playlist*p = Playlist::create_playlist(0);
  p->playNext();
  p = Playlist::create_playlist(1);
  p->playNext();
}

void loop() {

}

I just whipped this up. Probably room for improvement....

  • Thanks Johnny! That clears it up a bit. But I'm still stuck with a switch or an if. That'll be a pita when I implement more playlists. I'd love it if I could find something more convenient... – Max Aug 22 '17 at 14:45
  • @Max C++ does not have much in the way of reflection (finding all classes of a certain type, for example). I've updated with another example I think is closer to what you want. Personally, I prefer the first example for its simplicity. – Johnny Mopp Aug 22 '17 at 16:44
  • Great! Thx a lot. I'll investigate that. – Max Aug 22 '17 at 17:07
  • Well your first suggestion worked great while I was toying around with it and I got to learn quite a lot in the process. Thanks for that ;-) But then I hit another roadblock: because I need the user to select playlists whenever he/she wants, I wrote the switch and therefore the object instantiation in loop()... and I can't find a way to keep the object alive longer than the loop it has been created in. I wish I could just add 'static' to the create_playlist function... – Max Aug 24 '17 at 11:37
  • @Max Not sure I understand. The play_list should be a global variable. Once you create it, it exists until you delete it and should be accessible by all parts of your sketch. – Johnny Mopp Aug 24 '17 at 11:50
0

I'm not fully understanding the way your sketch should work.

However, I think what you need is some kind of Controller object. The controller object instantiates one program. The controller also keeps a Boolean to play the next one (playNext). Once the program finishes, the controller can delete the just played program, checks if the Boolean is set to play the next, instantiate the next program to play and play it.

This results that programs are not depending on each other.

  • 1
    Yes that makes sense. Thanks for the idea. – Max Aug 22 '17 at 12:12
  • But my problem is even noobier: I don't know how to code it! How do you instantiate an object when you only know which one at runtime? – Max Aug 22 '17 at 12:14
  • I don't fully understand ... you can instantiate an object at runtime depending on the switch (or state). – Michel Keijzers Aug 22 '17 at 12:21
  • I'm sure I can. I just don't know how to do it efficiently. A switch case would work for now but isn't there a better way? Later, I'll probably implement a way to select more than 16 programs (more like 99) and the switch will be cumbersome to say the least... – Max Aug 22 '17 at 12:44
  • In that case I would need more about how it works ... you have a 16 knob selector for the programs. And each program has commands. And the knob is to loop the current command? So e.g. suppose program 12 is selected. Program 12 has 20 commands, running command 0, than command 1, etc. When you press the button during command 10, command 10 loops until depressed and command 11 is continued? And what happens after command 19? Command 0 is started again of the same program? – Michel Keijzers Aug 22 '17 at 12:51

Not the answer you're looking for? Browse other questions tagged or ask your own question.