Why is splitting projects into multiple files so hard?

The topic says it all... I cannot for the life of me figure out how to split my code into multiple .h/.c files...

For the most part my code is all self contained only relying on the Midi.h class. In order to make use of midi input, you have to subclass the Midi class. This works perfectly when all in one file with the appropriate #include <Midi.h>

However, when I try to move my subclass into its own two files mymidi.c/h I am getting various odd errors when I try to compile.. Such as:

/mymidi.h:13: undefined reference to `vtable for MyMidi' (note: this happens only when the class in mymidi.c is COMMENTED OUT, as in no actual code, just the header)

The following error happens when the class in mymidi.c IS NOT commented out... There's a few pages of these types of errors.

C:\Program Files\Arduino\hardware\arduino\cores\arduino/WString.h:28: error: expected '=', ',', ';', 'asm' or '__attribute__' before 'String'
C:\Program Files\Arduino\hardware\arduino\cores\arduino/WString.h:99: error: expected '=', ',', ';', 'asm' or '__attribute__' before ':' token
C:\Program Files\Arduino\hardware\arduino\cores\arduino/WString.h:106: error: expected '=', ',', ';', 'asm' or '__attribute__' before 'operator'

How can it be this awkward? Granted I come from a PHP background, where it is as simple as an include() and bang! the included file is inserted inline... Don't get me started about the IDE's seriously weird handling of multiple .pde files... Concatenate them alphabetically?!? That just doesn't make sense to me whatsoever...

Any help would be wonderful :slight_smile:

Since you'll likely want code:
This code works perfectly when in the main pde file.

mymidi.h

#ifndef MYMIDI_H
#define MYMIDI_H
#include <Midi.h>
class MyMidi : public Midi {
  public:

  MyMidi(HardwareSerial &s) : Midi(s) {}
  
  void handleNoteOn(unsigned int channel, unsigned int note, unsigned int velocity){};
  void handleNoteOff(unsigned int channel, unsigned int note, unsigned int velocity){};
  void handleControlChange(unsigned int channel, unsigned int controller, unsigned int value){};
};
#endif

mymidi.c

#include "mymidi.h"

class MyMidi : public Midi {
  public:

  MyMidi(HardwareSerial &s) : Midi(s) {}
  
  void handleNoteOn(unsigned int channel, unsigned int note, unsigned int velocity) {
    sendNoteOn(channel, note, velocity); // midi thru
    if (note == CH1_BEAT_NOTE && velocity == 127) digitalWrite(LED_CH1_BEAT, LOW); // thump!
    if (note == CH2_BEAT_NOTE && velocity == 127) digitalWrite(LED_CH2_BEAT, LOW); // thump!
  }
  void handleNoteOff(unsigned int channel, unsigned int note, unsigned int velocity){
    sendNoteOff(channel, note, velocity); // midi thru
    if (note == CH1_BEAT_NOTE && velocity == 0) digitalWrite(LED_CH1_BEAT, HIGH); // no thump!
    if (note == CH2_BEAT_NOTE && velocity == 0) digitalWrite(LED_CH2_BEAT, HIGH); // no thump!
  }

  void handleControlChange(unsigned int channel, unsigned int controller, unsigned int value) {
    sendControlChange(channel, controller, value); // midi thru
    if (channel != 4) return; // we only are about channel 4 atm
    if (controller == XFADER_CC) {
      unsigned int graphCh1 = 0;
      bitSet(graphCh1,map(value,0,127,0,NUM_LEDS_XF-1));
      digitalWrite(LATCH_XFADER, LOW);
      shiftOut(DATA_XFADER, CLOCK_XFADER, MSBFIRST, highByte(graphCh1));
      shiftOut(DATA_XFADER, CLOCK_XFADER, MSBFIRST, lowByte(graphCh1));
      digitalWrite(LATCH_XFADER, HIGH);
    }
  }
};

However, when I try to move my subclass into its own two files mymidi.c/h I am getting various odd errors when I try to compile.

Perhaps you should be sub-classing mymidi in a .cpp file, instead of a .c file. Different compilers get invoked. The .cpp compiler knows about classes. The .c compiler does not.

Ok, good point.. learn something new every day... however when i renamed and compiled, I get

mymidi.cpp:2: error: redefinition of 'class MyMidi'
mymidi.h:3: error: previous definition of 'class MyMidi'

The definition of the class goes in the .h file. The implementation of the class goes in the .cpp file.

Remove all the { and } from the header file.
Remove the class statement and public keyword from the source file.

Header:

#ifndef MYMIDI_H
#define MYMIDI_H
#include <Midi.h>
class MyMidi : public Midi {
  public:

  MyMidi(HardwareSerial &s);
  
  void handleNoteOn(unsigned int channel, unsigned int note, unsigned int velocity);
  void handleNoteOff(unsigned int channel, unsigned int note, unsigned int velocity);
  void handleControlChange(unsigned int channel, unsigned int controller, unsigned int value);
};
#endif

Source:

MyMidi(HardwareSerial &s) : Midi(s) {}
  
  void handleNoteOn(unsigned int channel, unsigned int note, unsigned int velocity) {
    sendNoteOn(channel, note, velocity); // midi thru
    if (note == CH1_BEAT_NOTE && velocity == 127) digitalWrite(LED_CH1_BEAT, LOW); // thump!
    if (note == CH2_BEAT_NOTE && velocity == 127) digitalWrite(LED_CH2_BEAT, LOW); // thump!
  }
  void handleNoteOff(unsigned int channel, unsigned int note, unsigned int velocity){
    sendNoteOff(channel, note, velocity); // midi thru
    if (note == CH1_BEAT_NOTE && velocity == 0) digitalWrite(LED_CH1_BEAT, HIGH); // no thump!
    if (note == CH2_BEAT_NOTE && velocity == 0) digitalWrite(LED_CH2_BEAT, HIGH); // no thump!
  }

  void handleControlChange(unsigned int channel, unsigned int controller, unsigned int value) {
    sendControlChange(channel, controller, value); // midi thru
    if (channel != 4) return; // we only are about channel 4 atm
    if (controller == XFADER_CC) {
      unsigned int graphCh1 = 0;
      bitSet(graphCh1,map(value,0,127,0,NUM_LEDS_XF-1));
      digitalWrite(LATCH_XFADER, LOW);
      shiftOut(DATA_XFADER, CLOCK_XFADER, MSBFIRST, highByte(graphCh1));
      shiftOut(DATA_XFADER, CLOCK_XFADER, MSBFIRST, lowByte(graphCh1));
      digitalWrite(LATCH_XFADER, HIGH);
    }
  }
}

Okay, did that...

mymidi.cpp:2: error: expected `)' before '&' token
mymidi.cpp: In function 'void handleNoteOn(unsigned int, unsigned int, unsigned int)':
mymidi.cpp:5: error: 'sendNoteOn' was not declared in this scope
mymidi.cpp:6: error: 'CH1_BEAT_NOTE' was not declared in this scope
mymidi.cpp:6: error: 'LED_CH1_BEAT' was not declared in this scope
mymidi.cpp:6: error: 'LOW' was not declared in this scope
mymidi.cpp:6: error: 'digitalWrite' was not declared in this scope
mymidi.cpp:7: error: 'CH2_BEAT_NOTE' was not declared in this scope
mymidi.cpp:7: error: 'LED_CH2_BEAT' was not declared in this scope
mymidi.cpp:7: error: 'LOW' was not declared in this scope
mymidi.cpp:7: error: 'digitalWrite' was not declared in this scope
mymidi.cpp: In function 'void handleNoteOff(unsigned int, unsigned int, unsigned int)':
mymidi.cpp:10: error: 'sendNoteOff' was not declared in this scope
mymidi.cpp:11: error: 'CH1_BEAT_NOTE' was not declared in this scope
mymidi.cpp:11: error: 'LED_CH1_BEAT' was not declared in this scope
mymidi.cpp:11: error: 'HIGH' was not declared in this scope
mymidi.cpp:11: error: 'digitalWrite' was not declared in this scope
mymidi.cpp:12: error: 'CH2_BEAT_NOTE' was not declared in this scope
mymidi.cpp:12: error: 'LED_CH2_BEAT' was not declared in this scope
mymidi.cpp:12: error: 'HIGH' was not declared in this scope
mymidi.cpp:12: error: 'digitalWrite' was not declared in this scope
mymidi.cpp: In function 'void handleControlChange(unsigned int, unsigned int, unsigned int)':
mymidi.cpp:16: error: 'sendControlChange' was not declared in this scope
mymidi.cpp:18: error: 'XFADER_CC' was not declared in this scope
mymidi.cpp:20: error: 'NUM_LEDS_XF' was not declared in this scope
mymidi.cpp:20: error: 'map' was not declared in this scope
mymidi.cpp:20: error: 'bitSet' was not declared in this scope
mymidi.cpp:21: error: 'LATCH_XFADER' was not declared in this scope
mymidi.cpp:21: error: 'LOW' was not declared in this scope
mymidi.cpp:21: error: 'digitalWrite' was not declared in this scope
mymidi.cpp:22: error: 'DATA_XFADER' was not declared in this scope
mymidi.cpp:22: error: 'CLOCK_XFADER' was not declared in this scope
mymidi.cpp:22: error: 'MSBFIRST' was not declared in this scope
mymidi.cpp:22: error: 'highByte' was not declared in this scope
mymidi.cpp:22: error: 'shiftOut' was not declared in this scope
mymidi.cpp:23: error: 'lowByte' was not declared in this scope
mymidi.cpp:24: error: 'HIGH' was not declared in this scope
mymidi.cpp: At global scope:
mymidi.cpp:27: error: expected declaration before '}' token

I added my constants header, so all the defines went away, I also added WProgram.h, and some of the others went away, but I'm still left with:

mymidi.cpp:4: error: expected `)' before '&' token
mymidi.cpp: In function 'void handleNoteOn(unsigned int, unsigned int, unsigned int)':
mymidi.cpp:7: error: 'sendNoteOn' was not declared in this scope
mymidi.cpp: In function 'void handleNoteOff(unsigned int, unsigned int, unsigned int)':
mymidi.cpp:12: error: 'sendNoteOff' was not declared in this scope
mymidi.cpp: In function 'void handleControlChange(unsigned int, unsigned int, unsigned int)':
mymidi.cpp:18: error: 'sendControlChange' was not declared in this scope
mymidi.cpp: At global scope:
mymidi.cpp:29: error: expected declaration before '}' token

I don't understand why it cant find them, all those functions are in the parent class... Unless I am SERIOUSLY misunderstanding how OOP works in C++

The proper format for defining a class method is:

return_type ClassName::methodName(params){
code for method
}

Rinse and repeat for all methods of the class.

Ok awesome!

Thanks to you guys, I now have my code split into logical files.. I have one final issue though, I have one function that calls my midi sub-class:

void midi_stream_note(const uint8_t pitch, const bool onoff) {
#ifdef DEBUG
  uint8_t command = ((onoff)? 0x90 : 0x80);
  Serial.print("cmd:");Serial.print(command >> 4,HEX);   // command type 0..15
  Serial.print(" ch:");Serial.print((g_midi_channel & 0x0f),DEC);  // channel 0..15
  Serial.print(" nt:");Serial.print(pitch & 0x7f,DEC);   // note 0..127
  Serial.print(" d3:");Serial.println(g_midi_velocity & 0x7f,HEX); // velocity 0..127
#else
  if (onoff) {
    midi.sendNoteOn(MIDI_CHANNEL, pitch & 0x7f, 0x7f);
  } else {
    midi.sendNoteOff(MIDI_CHANNEL, pitch & 0x7f, 0x7f);
  }
#endif
}

I have the midi class instantiated at the top of the main pde as follows:

MyMidi midi(Serial);

It compiles with the midi_stream_note in the main sketch, but when I try to move it to the midi.cpp file, it fails to compile complaining that 'midi' is not declared in this scope. How would I get this working? Where do I instantiate the class? I also have my main task function that is staying in the main sketch file that also calls midi... so I need to have access to it in midi.cpp and in the main sketch file...

It compiles with the midi_stream_note in the main sketch, but when I try to move it to the midi.cpp file, it fails to compile complaining that 'midi' is not declared in this scope. How would I get this working?

In the cpp file, the instance that the method is being called for is known. So, you shouldn't be using midi.sendNoteOn() in the midi_stream_note() function. It should be just sendNoteOn().

Nadda....

Same error. midi_stream_note is not part of the class... so (based on my php oop knowledge how would it know where to find sendNoteOn unless I specify which class?

In php (yeah, i know... php), you always have to reference the class whose method you are calling, no matter where you are calling it from (even inside the class, you use $this->)

midi_stream_note() is not defined in the header file, or implemented in the source file.

In PHP, the reference to the instance is using the $this-> mechanism. In C++, it would be done using this->, but, this-> is implied if omitted, so, usually, it is omitted.

It is though...

midi.h

#ifndef MYMIDI_H
#define MYMIDI_H
#include <Midi.h>
class MyMidi : public Midi {
  public:

  MyMidi(HardwareSerial &s);
  
  void handleNoteOn(unsigned int channel, unsigned int note, unsigned int velocity);
  void handleNoteOff(unsigned int channel, unsigned int note, unsigned int velocity);
  void handleControlChange(unsigned int channel, unsigned int controller, unsigned int value);
};

extern uint8_t g_midi_channel;
extern uint8_t g_midi_velocity;
extern uint8_t kNoteMap[32];

void midi_stream_note(const uint8_t pitch, const bool onoff);
uint8_t midi_fourbanks_key_to_note(const uint8_t keynum);
#endif

midi.cpp

#include <WProgram.h>
#include "constants.h"
#include "midi.h"
#include "key.h"

uint8_t g_midi_channel = 3;      // MIDI channel to listen and send on (0..15)
uint8_t g_midi_velocity = 127;     // Default velocity for NoteOn (0..127)
uint8_t kNoteMap[32] = {
  12, 13, 14, 15,
   8,  9, 10, 11,
   4,  5,  6,  7,
   0,  1,  2,  3,
  16, 17, 18, 19,
  20, 21, 22, 23,
  24, 25, 26, 27,
  28, 29, 30, 31,
};

MyMidi::MyMidi(HardwareSerial &s) : Midi(s) {}
  
void MyMidi::handleNoteOn(unsigned int channel, unsigned int note, unsigned int velocity) {
  sendNoteOn(channel, note, velocity); // midi thru
  if (note == CH1_BEAT_NOTE && velocity == 127) digitalWrite(LED_CH1_BEAT, LOW); // thump!
  if (note == CH2_BEAT_NOTE && velocity == 127) digitalWrite(LED_CH2_BEAT, LOW); // thump!
}
void MyMidi::handleNoteOff(unsigned int channel, unsigned int note, unsigned int velocity){
  sendNoteOff(channel, note, velocity); // midi thru
  if (note == CH1_BEAT_NOTE && velocity == 0) digitalWrite(LED_CH1_BEAT, HIGH); // no thump!
  if (note == CH2_BEAT_NOTE && velocity == 0) digitalWrite(LED_CH2_BEAT, HIGH); // no thump!
}

void MyMidi::handleControlChange(unsigned int channel, unsigned int controller, unsigned int value) {
  sendControlChange(channel, controller, value); // midi thru
  if (channel != 4) return; // we only are about channel 4 atm
  if (controller == XFADER_CC) {
    unsigned int graphCh1 = 0;
    bitSet(graphCh1,map(value,0,127,0,NUM_LEDS_XF-1));
    digitalWrite(LATCH_XFADER, LOW);
    shiftOut(DATA_XFADER, CLOCK_XFADER, MSBFIRST, highByte(graphCh1));
    shiftOut(DATA_XFADER, CLOCK_XFADER, MSBFIRST, lowByte(graphCh1));
    digitalWrite(LATCH_XFADER, HIGH);
  }
}

void midi_stream_note(const uint8_t pitch, const bool onoff) {
#ifdef DEBUG
  uint8_t command = ((onoff)? 0x90 : 0x80);
  Serial.print("cmd:");Serial.print(command >> 4,HEX);   // command type 0..15
  Serial.print(" ch:");Serial.print((g_midi_channel & 0x0f),DEC);  // channel 0..15
  Serial.print(" nt:");Serial.print(pitch & 0x7f,DEC);   // note 0..127
  Serial.print(" d3:");Serial.println(g_midi_velocity & 0x7f,HEX); // velocity 0..127
#else
  if (onoff) {
    sendNoteOn(MIDI_CHANNEL, pitch & 0x7f, 0x7f);
  } else {
    sendNoteOff(MIDI_CHANNEL, pitch & 0x7f, 0x7f);
  }
#endif
}

uint8_t midi_fourbanks_key_to_note(const uint8_t keynum) {
  uint8_t banksize = 0;
  banksize = 12;
  uint8_t offset = MIDI_BASE_NOTE + g_key_bank_selected * banksize;
  return kNoteMap[keynum] + offset;
}

Ok, so I did get it to compile by moving the midiStreamNote into the midi class itself... To be honest, it probably belongs in there anyway...

However I would still like to know why it was happening and what I would need to do to avoid it. This is the beginnings of a much much larger project, so I will certainly encounter situations like this again.

Are there any decent resources online about including external files in c++?

It is though...

No, it was declared in the header file, as a global function. It was implemented in the source file, but not as a member of the class, so it can't call class members. The function was being called as a member of a class, when that was not how it was defined.

so a single cpp file can only have one class in it? all functions are considered part of the class?

Perhaps I am misunderstanding... I want to split my midi related stuff out of my main sketch into a midi.cpp file... part of this midi related stuff is the class, but not all of it. Am I to understand that I cannot have a global function and a class in a single cpp file?

so a single cpp file can only have one class in it? all functions are considered part of the class?

No and no. If the function is declared inside a class, it must be implemented as a class method, using the
ReturnType ClassName::MethodName(ArgType arg)
format.

If the function is declared outside the class, it is a global method, but it does not have access to variables defined in other files, unless the extern keyword is used to import the variable into the file scope.

Perhaps I am misunderstanding... I want to split my midi related stuff out of my main sketch into a midi.cpp file... part of this midi related stuff is the class, but not all of it. Am I to understand that I cannot have a global function and a class in a single cpp file?

You can have non-class members in the file. But, those non-class members can not call methods in a class without an instance of that class. If the instance is declared in another file, that instance must be imported using the extern keyword.

ok... the light bulb is getting brighter...

extern MyMidi midi(Serial);

in midi.cpp would take care of things then right?

extern MyMidi midi(Serial);

in midi.cpp would take care of things then right?

Close. The variable midi is declared and initialized in one file using

MyMidi midi(Serial);

To reference that already initialized variable in another file:

extern MyMidi midi;