Hallo zusammen,
ich bin nun schon einige Zeit dabei eine C++ Library zur umfassenden
MIDI-Fernsteuerung eines Kemper Profiling Amps (ein digitaler E-Gitarren
Verstärker mit massig Funktionen die man steuern kann) zu entwickeln.
Ich hole ganz kurz zu den Hintergründen aus bevor ich zur eigentlichen
Programmierungsfrage komme um meine Überlegung verständlicher zu machen.
Wer mag darf den Abschnitt auch überspringen ;)
--------- Die Hintergründe -----------------------
Die Library, welche ich programmiere ist so ausgelegt, dass sie mit
nahezu vollständig gleicher API und gleichen Funktionen sowohl auf
PC-Architekturen als auch auf Mikrocontrollern (aktuell nur mit dem
Arduino-Framework - prinzipiell aber alles an Hardware mit UART-Port
wofür es einen C++ Compiler gibt) läuft. Das erreiche ich, indem ich
eine Cross-Plattform-fähige MIDI-Library entwickelt habe, welche
wiederum mit vollständig gleicher API die MIDI-Kommunikation über die
jeweilige Hardwareschnittstelle der Wahl realisiert, das ist z.B. für
Arduino ein HardareSerial oder SofwareSerial, für MacOS ein
CoreMIDI-basiertes USB-MIDI-Inferface, diverse weitere Implementierungen
sind in Planung.
Das hat den Vorteil, dass die Arbeit von mir und vllt auch. der
Community für mehrere Projekte auf völlig verschiedenen Plattformen
genutzt werden kann und dass ich entspannt die Ansteuerung einzelner
Features auf dem PC entwickeln und debuggen kann und diese durch den
hübschen Hardware Abstraction Layer nahezu ohne Probleme auf Anhieb auch
auf dem Mikrocontroller laufen.
Nebenbei ist die ganze Geschichte Header-Only gehalten, so dass ein
einziges #include-Statement reicht um alles zum laufen und kompilieren
zu bekommen!
Dabei achte ich stets auf möglichst mikrocontroller-freundlich
gestalteten Code und muss relativ wenig Performance-Einbußen in Kauf
nehmen.
Nun hat der Amp einen Haufen ansprechbarer Parameter, welche Funktionen
von Effekten Steuern, die abhängig vom aktuell geladenen Preset sich
möglicherweise in den 8 virtuellen Effektslots befinden. Ist nun aber
ein bestimmter Effekt nicht geladen, geht ein Steuerbefehl an diesen
Effekt in diesem Slot ins leere.
Da dies so viele Parameter sind, würde ich ungern in der Hauptklasse
dafür jeweils eine eigene setter- und getter-Methode implementieren.
Stattdessen fände ich es sehr angenehm, mir eine weitere
Effekt-Basisklasse zu erstellen und viele Klassen für die jeweiligen
spezifischen Effekttypen, welche von der Basisklasse erben und die
Ansprache der jeweils relevanten Parameter abstrahieren. Dazu würde ich
dann gerne in der Hauptklasse jeweils Methoden implementieren nach der
Art
1 | StompTypeA *getStompTypeAInstance()
|
welche mir in dem Fall, dass ein solcher Effekt im aktuellen Setup
geladen ist einen Pointer auf die passende Instanz geben um diesen
Effekt zu kontrollieren oder falls ein solcher Effekt nicht geladen ist,
einen Nullpointer zurück geben.
Nach jeden Preset-Wechsel scanne ich eh einmal die Effekt-Slots durch,
weiß also welche Effekte jeweils vorhanden sind. Wäre ich nun
ausschließlich auf dem PC unterwegs, würde ich mir dann jeweils auf dem
Heap eine Instanz der passenden Effekt-Klasse anlegen und gut wäre es.
Nun will ich aber Mikrocontoller-kompatibel völlig ohne Heap auskommen,
also möchte ich die Infos über die geladenen Effekte gerne in einem 8
Felder großem Stack-Array ablegen. Dies hat mich nach längerem hin- und
her überlegen zu folgender Idee getrieben:
--------------- Die eigentliche Frage: ------------------------
Ich habe eine Basisklasse und eine gewisse Anzahl an Klassen die davon
erben. Konkret in dieser Art:
1 | class StompBase {
|
2 |
|
3 | public:
|
4 |
|
5 | /**
|
6 | * Will be called once in the constructor of the Amp class
|
7 | */
|
8 | void setFixedPropertiesPtr (NRPNPage slotPage, KemperProfilingAmp *amp) {
|
9 | this->slotPage = slotPage;
|
10 | this->amp = amp;
|
11 | }
|
12 |
|
13 | /**
|
14 | * Will be called after each rig update to set the stomp type in that stomp slot
|
15 | */
|
16 | void setStompType (StompType stompType) {
|
17 | this->stompType = stompType;
|
18 | }
|
19 |
|
20 | StompType getStompType() {
|
21 | return stompType;
|
22 | }
|
23 |
|
24 | /**
|
25 | * Should be overriden by derived class to determine if the assumed stomp is still present in that slot
|
26 | * @return
|
27 | */
|
28 | virtual bool isStillValid() {return false; };
|
29 |
|
30 | protected:
|
31 | StompType stompType = Empty;
|
32 | NRPNPage slotPage = PageUninitialized;
|
33 | KemperProfilingAmp *amp;
|
34 | };
|
35 |
|
36 |
|
37 | class WahStomp : public StompBase {
|
38 |
|
39 | public:
|
40 |
|
41 | /**
|
42 | * Checks if there still is a Wah stomp in this slot
|
43 | */
|
44 | bool isStillValid() override {
|
45 | if ((stompType == StompType::Wah) && (slotPage != PageUninitialized)) {
|
46 | return true;
|
47 | }
|
48 | return false;
|
49 | }
|
50 |
|
51 | /**
|
52 | * Sets the Wah range parameter
|
53 | */
|
54 | void setRange (uint8_t range) {
|
55 |
|
56 | if ((amp->lastNRPNPage != slotPage) || (amp->lastNRPNParameter != WahRange)) {
|
57 | amp->setNewNRPNParameter (slotPage, WahRange);
|
58 | }
|
59 |
|
60 | amp->sendControlChange (119, range);
|
61 | }
|
62 |
|
63 | // ... and a lot more...
|
64 | };
|
65 |
|
66 | class PhaserVibeStomp : public StompBase {
|
67 |
|
68 | public:
|
69 |
|
70 | /**
|
71 | * Checks if there still is a PhaserVibe stomp in this slot
|
72 | */
|
73 | bool isStillValid() override {
|
74 | if ((stompType == VintageChorus) && (slotPage != PageUninitialized)) {
|
75 | return true;
|
76 | }
|
77 | return false;
|
78 | }
|
79 |
|
80 | /**
|
81 | * Sets the PhaserVibe modPahserStages parameter
|
82 | */
|
83 | void setModPhaserStages (uint8_t modPhaserStages) {
|
84 |
|
85 | if ((amp->lastNRPNPage != slotPage) || (amp->lastNRPNParameter != ModPhaserStages)) {
|
86 | amp->setNewNRPNParameter (slotPage, ModPhaserStages);
|
87 | }
|
88 |
|
89 | amp->sendControlChange (119, modPhaserStages);
|
90 | }
|
91 |
|
92 | // ... and a lot more...
|
93 | };
|
Nun bestehen alle Klassen die davon erben immer ausschließlich aus
Member Funktionen und fügen der Klasse keine einzige Member Variable
hinzu. Wenn ich mir das vorstelle, müsste doch nun das Speicher-Layout
von StompBase und beliebigen davon abgeleiteten Klassen nach der oben
beschriebenen Machart exakt gleich aussehen. Demnach müsste ein Downcast
von
z.B. zu
doch problemlos funktionieren. ODER? Genau an diesem Punkt bin ich mir
nicht ganz sicher und hätte gerne etwas einblick von jemanden der ganz
tief im Compiler-Thema drin sitzt und mir sagen kann ob das immer
funktionieren wird.
Falls das nämlich funktioniert, würde ich in der Hauptklasse so etwas
implementieren:
1 | /**
|
2 | * This array gets updated with the stomp types after each rig change
|
3 | */
|
4 | StompBase stompsInCurrentRig[8];
|
5 |
|
6 | /**
|
7 | * Returns a pointer to a WahStomp instance if one exists, returns a nullptr otherwise
|
8 | */
|
9 | WahStomp *getWahStompInstance() {
|
10 | for (int i = 0; i < 8; i++) {
|
11 | if (stompsInCurrentRig[i].getStompType() == StompType::Wah) {
|
12 | return (WahStomp*) &stompsInCurrentRig[i];
|
13 | }
|
14 | }
|
15 |
|
16 | return nullptr;
|
17 | }
|
Damit könnte ich das im oberen abschnitt beschriebene Ziel hübsch
(zumindest nach Außen hin :D) erreichen und dabei völlig
Mikrocontroller-kompatibel ausschließlich auf dem Stack unterwegs
sein...
Falls jemand einen Vorschlag hat nach außen hin das gleiche zu erreichen
ohne solch einen fiesen Downcast-Hack freue ich mich auch!