Architecture for complex-ish project

I am building a rather complex portable synth with daisy (inspired by OP-1). Currently, I have a working prototype that has features like tracks with note patterns, multiple sound engines, multiple FX, Ui screens to render all that, keyboard and midi input sources, etc. While it functionally works well, the software architecture design is quite bad:

  • Pointer to an Application class is passed to UI classes, which are then used to access things like Clock, active track, reading/updating engine configuration, etc;
  • Application class acts as a god object, it is basically the entry point to all the business/synth logic, so whenever I need to read or tweak anything, it involves multi-layer access: app->GetActiveTrack()->GetEngine()->GetEnvConfig()
  • I do use the event queue, but only for user inputs (keypress, encoders, midi)
  • Inheritance and dynamic polymorphism (virtual methods) are used quite often: Engine implementations, Fx types, Voice implementations, UI screens, etc.
  • It is getting harder to make changes and add new features.
  • It feels like all the above contribute to the code size.

Does anyone have suggestions, or ideas on how to design such an architecture? Maybe you know some good examples of OSS apps, that I could refer to?

Had some initial discussions in slack

Hi brbrr!

My drum sequencer BassMate might give you some food for thought (GitHub - ukmaker/BassMate). Ignore the fact that it’s not currently written for Daisy Seed. I’ll be getting back to that once I’ve mastered the QSPI boot loader and figured out how to get decent performance out of AnalogBassDrum which currently seems to want 90% CPU.

I’m using the Model View Presenter pattern here more or less. So the Model class holds most of the hardware; the Presenter mediates between hardware and the view, manages state etc; the View manages the display (including the LEDS on the NeoTrellis which is also really part of the Model but I’ve put in the Presenter for now… need to move it I think).

The key for me is that the controller makes it explicit what happens and when. I always find the MVC pattern with events and callbacks all over the place to be a bit of a nightmare. Good for desktop UIs, not so good for embedded.

Also I like using Miro to help dump my thoughts and notes in one place - you might like it too. Miro | Online Whiteboard for Visual Collaboration

Object-Oriented Design Heuristics

I use UML for OOD design. But in English this means labeling boxes that contain similar functionality and create clean testable interfaces. Make each C++ class a product as if you are going to market it.

Learn some simple design patterns like the singleton (good for embedded systems without a heap).

Since configuration is a pervasive feature of all objects, perhaps have each configurable object register its name, configurable items, callbacks etc with a God config object. Then ConfigGod->save().