| lang |
|---|
fr |
- Smart Piano (moteur de jeu)
Smart Piano est un système aidant à progresser au piano en s'entrainant à en jouer d’une manière optimisant l’apprentissage grâce à des exercices intelligents.
L’utilisateur interagit via un clavier MIDI connecté au dispositif Smart Piano, sur lequel fonctionne l’application.
L'application propose plusieurs modes de jeu (notes, accords…) et évalue la performance en temps réel pour proposer les exercices les plus propices à faire progresser.
Ce projet, développé dans le cadre du cursus de Polytech Tours, vise à être
une plateforme ayant un intérêt pédagogique à la fois sur le piano et sur le
développement (C++).
-
Notes
note: Reconnaissance de notes individuelles- Le joueur doit jouer la note affichée
-
Accords
chord: Accords simples (sans renversement)- Le joueur doit jouer les 3 notes de l'accord dans n'importe quel ordre
-
Accords renversés
inversed: Accords avec renversements- Le joueur doit jouer l'accord, mais il est renversé
c: Dod: Rée: Mif: Fag: Sola: Lab: Si
maj: Majeurmin: Mineur
Smart Piano a été conçu pour fonctionner avec :
- Raspberry Pi 4
- Sous Raspberry Pi OS
- MicroSD de 32 Go
- Écran tactile connecté à la Raspberry Pi (via HDMI)
- Clavier MIDI standard (ex. SWISSONIC EasyKeys49)
Il est néanmoins possible que l'application fonctionne sur d'autres systèmes d’exploitations, architectures ou configurations, sans garantie.
| Problème | Solution |
|---|---|
| Aucune note n'est détectée | Vérifier que le clavier MIDI est bien branché et reconnu avec aconnect -l |
| Connexion au MDJ impossible | S’assurer que le moteur de jeu est bien lancé : ./engine |
| L'application plante | Relancer l'application, voire redémarrer la Raspberry Pi |
| Fonction | Outil |
|---|---|
| Compilation C++ | Clang |
| Système de build | CMake (+ Ninja) |
| Dépendances et environnement | Nix |
| Versionnage et collaboration | Git hébergé sur GitHub |
| Tests automatisés | doctest |
| Couverture de code | llvm-cov |
| Assistance langage C++ | clangd (LSP) |
| Documentation depuis le code | Doxygen |
| Formatage du C++ | clang-format |
| Contrôle qualité C++ | clang-tidy |
| Débogage C++ | lldb |
| Test manuel communication socket | socat |
| Éditeur de code | VS Code, Helix… |
Ce projet utilise Nix pour télécharger les (bonnes versions des) dépendances,
configurer l’environnement, et permettre in-fine d’effectuer des compilations
(croisées) reproductibles. L’environnement Nix est défini dans
flake.nix et s’active avec la commande nix flake develop
(nix doit être installé) ou plus simplement via direnv (qui doit aussi
être installé séparément).
Pour compiler le projet, il est possible (pour tester) d’utiliser CMake
(cmake --build build) directement depuis un environnement Nix activé, mais
la solution préconisée (car reproductible) est d’utiliser nix build ; ou
nix build .#cross pour compiler en ciblant l’architecture de la Raspberry Pi 4
(ARM64).
Le serveur peut être lancé avec ./result/bin/main, ou ./build/main si
compilé avec CMake (ou automatiquement après un build avec
cmake --build build --target run). Le moteur démarre et écoute sur
/tmp/smartpiano.sock.
Pour accélérer les opérations impliquant
cmake, indiquer le nombreNde threads correspondant au nombre de cœurs de processeur avec-jN(ex.cmake --build build -j4) ou--jobs Npournix(ex.nix build .#cross --jobs 4)
Il est possible de tester le moteur de jeu manuellement avec un client UDS tel
que socat.
socat - UNIX-CONNECT:/tmp/smartpiano.sockPuis envoyer des commandes :
config
game=note
scale=c
mode=maj
Le moteur répond (normalement) avec :
ack
status=ok
Puis envoyer ready pour commencer :
ready
Le moteur envoie un « challenge » :
note
note=c4
id=1
Jouer la note sur le clavier MIDI, le moteur répond :
result
id=1
correct=c4
duration=1234
Les tests unitaires et tests d’intégration peuvent être exécutés manuellement
avec la commande cmake --build build --target tests.
Ils sont automatiquement exécutés lors des builds avec Nix.
De plus, il est possible de générer un rapport de couverture de code avec
cmake --build build --target coverage, puis d’en visualiser un résumé avec
llvm-cov report build/src/main -instr-profile=build/coverage.profdata -ignore-filename-regex="test/.*".
Sur la branche principale main, tous les tests automatiques (unitaires,
intégration) doivent passer parfaitement, avec une couverture de 100% des
fonctions et d’au moins 90% des lignes de code. Il faut s’assurer qu’une
branche répond à ces critères avant de la fusionner dans main.
L’objectif est de couvrir 100% des lignes de codes, mais certains cas peuvent
être trop difficiles à simuler en tests automatiques, auquel cas, ils doivent
être commentés comme tel.
Norme utilisée du langage C++ la plus récente (stable), C++23. Utilisation de
ses fonctionnalités modernes et respect des meilleures pratiques. Par exemple,
std::println() est préféré à std::cout << ou std::cerr <<.
Documentation des attributs, méthodes et fonctions en français (correct),
suivant la syntaxe Doxygen, comportant au moins un (concis) premier paragraphe
expliquant rapidement la raison d’être de la fonction, ainsi qu’une ligne
@param par paramètre, et @return si son type de retour n’est pas void.
const std::string message; ///< Message intéressant
/**
* Ne fais rien, mis à part être une fonction d’exemple
* @param firstArg Le premier argument
* @param secondArg Le second argument
*/
void myFunc(uint32_t firstArg, uint16_t secondArg);Ne pas commenter les éléments évidents tels que les getters/setters simples ou constructeurs/destructeurs simples.
Avertissements de non-respect de la convention par clang-tidy selon les règles
de nommage définies dans le fichier .clang-tidy.
CheckOptions.readability-identifier-naming:
EnumConstantCase: UPPER_CASE
ConstexprVariableCase: UPPER_CASE
GlobalConstantCase: UPPER_CASE
ClassCase: CamelCase
StructCase: CamelCase
EnumCase: CamelCase
FunctionCase: camelBack
GlobalFunctionCase: camelBack
VariableCase: camelBack
GlobalVariableCase: camelBack
ParameterCase: camelBack
NamespaceCase: lower_case| Symbole | Convention |
|---|---|
| Enum constante | UPPER_CASE (MY_ENUM) |
| Constexpr | UPPER_CASE (MY_CONSTEXPR) |
| Constante globale | UPPER_CASE (MY_CONST) |
| Classe | CamelCase (MyClass) |
| Struct | CamelCase (MyStruct) |
| Enum | CamelCase (MyEnum) |
| Fonction | camelBack (MyMethod) |
| Fonction globale | camelBack (MyFunc) |
| Variable / Objet | camelBack (myVar) |
| Variable globale | camelBack (myGlobalVar) |
| Paramètre | camelBack (myParam) |
| Espace de nommage | snake_case (my_namespace) |
| Type C (à éviter) | snake_case suivi de _t |
Formatage automatique avec clang-format selon la convention de code de LLVM
(l’organisation derrière Clang) avec quelques ajustements pour rendre le code
plus compact (définis dans le fichier .clang-format).
BasedOnStyle: LLVM # Se baser sur le style « officiel » de Clang
IndentWidth: 4 # Indenter fortement pour décourager trop de sous-imbrication
---
Language: Cpp # Règles spécifiques au C++ (le projet pourrait utiliser plusieurs langages)
AllowShortBlocksOnASingleLine: Empty # Code plus compact, ex. {}
AllowShortCaseExpressionOnASingleLine: true # Code plus compact, ex. switch(a) { case 1: … }
AllowShortCaseLabelsOnASingleLine: true # Code plus compact, ex. case 1: …
AllowShortIfStatementsOnASingleLine: AllIfsAndElse # Code plus compact, ex. if (a) { … } else { … }
AllowShortLoopsOnASingleLine: true # Code plus compact, ex. for (…) { … }
DerivePointerAlignment: false # Tout le temps…
PointerAlignment: Left # afficher le marquer de pointeur * collé au typeUtilisation des entiers de taille strictement définie uint8_t, uint16_t,
uint32_t et uint64_t de la bibliothèque <stdint.h> au lieu des char,
short, int et long variant selon la plateforme ou l’environnement.
- Structure des classes en quatre blocs
- Attributs privés
- Éventuels attributs publics
- Éventuelles méthodes privées
- Méthodes publiques
- Utilisation de
this->pour identifier attributs- Pas de préfixe ou postfixe tel que
_
- Pas de préfixe ou postfixe tel que
- Pas d’attributs initialisés avec valeurs littérales dans le constructeur
- Initialiser à la déclaration, ex.
type name{value};
- Initialiser à la déclaration, ex.
- Méthodes de moins de trois lignes dans le
.hppuniquement- Ex. getters/setters simples, constructeurs/destructeurs simples
- Commentaires et strings concis
- Pas de verbe ni déterminants si compréhensible sans
- Pas de points finaux
- Pas d’espace avant les
:
- Pas de blocs
{}pour une seule instruction - Limiter les lignes vides au sein d’une même méthode
- Préférer la séparation en blocs logiques avec des commentaires
- Limiter l’imbrication des blocs
- Préférer les retours anticipés (
early return) - Préférer les ET
&&aux imbricationsifmultiples - Privilégier les
switch caselorsque possible
- Préférer les retours anticipés (
- Toujours expliciter strictement le comportement des attributs et des méthodes
const,noexcept,override,final,nodiscard, …
Vérification automatique de nombreuses règles de qualité de code par
clang-tidy (définies dans .clang-tidy).
Checks: >
bugprone-*,
cert-*,
clang-analyzer-*,
clang-diagnostic-*,
concurrency-*,
modernize-*,
performance-*,
readability-*,Le moteur génère deux fichiers de journaux (« logs ») qu’il est possible d’utiliser pour vérifier le bon fonctionnement de Smart Piano ou comprendre l’origine des erreurs.
smartpiano.log: Logs normaux (tout ce qu’il se passe)smartpiano.err.log: Logs d'erreurs
- Fankam Jisele
- Fauré Guilhem
Initiateur du projet : Mahut Vivien
L’entièreté de Smart Piano est sous GNU GPL v3, voir LICENSE.
Respecter les instructions des sections précédentes pour contribuer, en particulier celles des Conventions de Code.
- Créer une classe héritant de
IGameMode - Implémenter
start(),play(),stop() - Enregistrer dans
GameEngine::createGameMode()
- Créer une classe héritant de
ITransport - Implémenter les méthodes de communication
- Injecter dans le
main.cpp
Voir PROTOCOL pour la spécification complète du protocole de communication entre le moteur de jeu et l'interface utilisateur.
L'architecture suit le principe d'inversion de dépendances avec des
interfaces abstraites (IGameMode, ITransport, IMidiInput) permettant de
découpler les composants et faciliter les tests et l'extensibilité.
main: Point d'entrée de l'application. Configure la
journalisation, instancie le transport (UDS) et l'entrée MIDI (RtMidi), crée le
moteur de jeu puis lance la boucle d'événements principale.
GameEngine: Orchestrateur central coordonne le
transport, l'entrée MIDI et les modes de jeu. Gère le cycle complet d'une
session : connexion client, réception configuration, création du mode approprié,
exécution de la partie.
IGameMode Interface définissant le contrat pour tous
les modes de jeu (start(), play(), stop()).
ITransport Interface définissant la communication
bidirectionnelle client-serveur.
UdsTransportImplémentation via Unix Domain Socket avec sérialisation/parsing de messages selon le protocole définiMessageStructure immuable représentant un message du protocole (type + champs clé-valeur)
IMidiInput Interface pour la lecture MIDI.
RtMidiInputImplémentation utilisant la bibliothèque RtMidi avec traitement asynchrone et conversion MIDI versNote
Note Classe immuable représentant une note musicale en
notation standard (lettre a-g + altération optionnelle + octave 0-8).
ChordRepository Référentiel statique contenant
tous les accords musicaux mappés par tonalité et degré avec leurs notes MIDI
correspondantes (ex: Do majeur I = C4, E4, G4).
ChallengeFactory Générateur aléatoire de notes
et d'accords (simples ou avec renversements) selon une gamme et un mode musical
donné.
AnswerValidator Validateur spécialisé qui
compare notes et accords joués vs attendus, avec support des renversements
d'accords.
Logger Système de journalisation thread-safe avec
rotation automatique de fichiers logs (standard et erreurs), formatage avec
timestamps.
main.cpp
└─> GameEngine(transport, midi)
├─> transport.start()
└─> run() [boucle principale]
├─> transport.waitForClient()
├─> transport.receive() → Message config
├─> createGameMode(config) → IGameMode
│ ├─> NoteGame
│ └─> ChordGame
└─> gameMode.play(transport, midi)
├─> ChallengeFactory.generate() → Challenge
├─> transport.send(challenge)
├─> midi.readNotes() → Notes jouées
├─> AnswerValidator.validate() → Résultat
└─> transport.send(result)