Skip to content

Smart Piano (moteur de jeu), aide à apprendre le piano (avec un clavier MIDI) optimisée par apprentissage automatique

License

Notifications You must be signed in to change notification settings

ProjetISIE/SmartPianoEngine

Repository files navigation

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++).

Modes de Jeu

  1. Notes note : Reconnaissance de notes individuelles

    • Le joueur doit jouer la note affichée
  2. Accords chord : Accords simples (sans renversement)

    • Le joueur doit jouer les 3 notes de l'accord dans n'importe quel ordre
  3. Accords renversés inversed : Accords avec renversements

    • Le joueur doit jouer l'accord, mais il est renversé

Gammes Supportées

  • c : Do
  • d : Ré
  • e : Mi
  • f : Fa
  • g : Sol
  • a : La
  • b : Si

Modes Supportés

  • maj : Majeur
  • min : Mineur

Matériel

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.

Dépannage et Résolution des Problèmes

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

Outillage

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

Compilation & Exécution

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 nombre N de threads correspondant au nombre de cœurs de processeur avec -jN (ex. cmake --build build -j4) ou --jobs N pour nix (ex. nix build .#cross --jobs 4)

Test Manuel

Il est possible de tester le moteur de jeu manuellement avec un client UDS tel que socat.

socat - UNIX-CONNECT:/tmp/smartpiano.sock

Puis 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

Tests Automatiques

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.

Conventions de Code

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 Doxygen

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.

Nommage des Symboles (casse, tirets)

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 du Code (sauts de ligne, espaces)

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 type

Autres

Utilisation 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
    1. Attributs privés
    2. Éventuels attributs publics
    3. Éventuelles méthodes privées
    4. Méthodes publiques
  • Utilisation de this-> pour identifier attributs
    • 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};
  • Méthodes de moins de trois lignes dans le .hpp uniquement
    • 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 imbrications if multiples
    • Privilégier les switch case lorsque possible
  • 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-*,

Journalisation

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

Auteurs & Licence

  • Fankam Jisele
  • Fauré Guilhem

Initiateur du projet : Mahut Vivien

L’entièreté de Smart Piano est sous GNU GPL v3, voir LICENSE.

Contribution

Respecter les instructions des sections précédentes pour contribuer, en particulier celles des Conventions de Code.

Ajout d’un Mode de Jeu

  1. Créer une classe héritant de IGameMode
  2. Implémenter start(), play(), stop()
  3. Enregistrer dans GameEngine::createGameMode()

Ajout d’un Transport

  1. Créer une classe héritant de ITransport
  2. Implémenter les méthodes de communication
  3. Injecter dans le main.cpp

Architecture

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()).

  • NoteGame Mode reconnaissance de notes individuelles
  • ChordGame Mode accords simples ou renversés

ITransport Interface définissant la communication bidirectionnelle client-serveur.

  • UdsTransport Implémentation via Unix Domain Socket avec sérialisation/parsing de messages selon le protocole défini
  • Message Structure immuable représentant un message du protocole (type + champs clé-valeur)

IMidiInput Interface pour la lecture MIDI.

  • RtMidiInput Implémentation utilisant la bibliothèque RtMidi avec traitement asynchrone et conversion MIDI vers Note

Composants Musicaux

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.

Utilitaires

Logger Système de journalisation thread-safe avec rotation automatique de fichiers logs (standard et erreurs), formatage avec timestamps.

Diagramme de Flux

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)

About

Smart Piano (moteur de jeu), aide à apprendre le piano (avec un clavier MIDI) optimisée par apprentissage automatique

Topics

Resources

License

Stars

Watchers

Forks

Releases

No releases published

Packages

No packages published

Contributors 2

  •  
  •