Journal Un tap tempo en ligne de commande

Posté par (page perso) . Licence CC by-sa.
Tags :
38
19
fév.
2018

Bonjour à tous,

j'aimerai vous présenter mon dernier petit projet : TapTempo.

C'est un détecteur de tempo en ligne de commande. L'utilisateur frappe une touche en cadence régulière et le programme en déduit le tempo correspondant. Il est affiché en nombre de battements équivalent par minute (ou BPM en anglais).

La plupart des logiciels audio-numériques ainsi que beaucoup de d'instruments électroniques ont cette fonctionnalité, mais ça me rebutait de lancer un gros logiciel ou de me déplacer vers mon synthé pour vérifier le tempo d'un morceau. Alors TapTempo est né !

Le scénario que je me suis fixé est de pourvoir simplement taper taptempo dans un terminal, de frapper une touche régulièrement (tout en écoutant un morceau de musique par exemple) et d'obtenir le tempo correspondant à chaque frappe :

> taptempo
Appuyer sur la touche entrée en cadence (q pour quitter).

[Appuyer encore sur la touche entrée pour lancer le calcul du tempo...]
Tempo : 62 bpm
Tempo : 71 bpm
Tempo : 76 bpm
Tempo : 78 bpm
Tempo : 80 bpm
Tempo : 86 bpm
Tempo : 87 bpm
Tempo : 80 bpm  q
Au revoir !

Le calcul s'effectue sur les 5 dernières frappes (ou moins si l'utilisateur n'a pas encore frappé 5 fois la touche entrée) pour limiter l'effet des micro-variations d'une frappe à l'autre. Le nombre de frappes enregistrés est remis à zéro au bout de 5 secondes sans interaction avec l'utilisateur. Ceci permet d'éviter que le premier affichage du tempo ne soit erroné dans le cas où le dernier intervalle de temps est très grand à cause de l'inactivité de l'utilisateur.

Ces options peuvent être changées via les arguments du programme :

  -h, --help            affiche ce message d'aide
  -p, --precision       changer le nombre de décimale du tempo à afficher
                        la valeur par défaut est 0 décimales, le max est 5 décimales
  -r, --reset-time      changer le temps en seconde de remise à zéro du calcul
                        la valeur par défaut est 5 secondes
  -s, --sample-size     changer le nombre d'échantillons nécessaires au calcul du tempo
                        la valeur par défaut est 5 échantillons
  -v, --version         afficher la version

Concernant le développement à proprement parler, c'est du C++ orienté objet, langage avec lequel je suis le plus à l'aise.
Lors de mes recherches j'ai hésité à utiliser ncurses mais ça m'a paru trop complexe pour ce que je souhaitais réaliser. J'utilise donc directement le terminal. Ceci a comme inconvénients d'afficher ce que l'utilisateur tape et de forcer l'utilisation de la touche entrée pour frapper le tempo. L'avantage c'est la simplicité, pas de dépendances, principe KISS.

De plus j'en ai profité pour mettre en place des outils et des bonnes pratiques issues des logiciels libres:
- licence GPL-3.0+
- génération du projet avec CMake
- traduction en français avec gettext, et potentiellement dans d'autres langues.
- analyse des paramètres d'entrée avec getopt
- génération d'un paquet pour Debian, avec une page de manuel
- génération d'un exécutable Windows via MSYS2 avec mingw-bundledlls pour inclure les dépendances

J'anticipe le seul bémol actuel : le code est hébergé par GitHub, qui n'est pas une forge libre.

N'hésitez pas à tester et à envoyer vos remarques !

  • # Pas mal !

    Posté par . Évalué à 3.

    Effectivement ça marche bien et c'est une bonne idée ! Sa pourra sûrement me servir quand je veux trouver le rythme d'une chanson.

  • # Très bonne idée !

    Posté par (page perso) . Évalué à 7.

    Ce genre d'appli en ligne de commande est une très bonne idée, vu que l'interaction utilisateur est de toute façon très limitée.
    En revanche, GPL-3.0+ je trouve ça un peu restrictif pour un outil relativement simple. Tu dois être dans les 300 lignes de code C++, c'est à peu près dans dans ce cas là que la FSF recommande l'Apache-2.0.

    Perso j'ai commencé un petit métronome en GTK+, j'ajouterai sans doute une fonction de détection des BPM, c'est effectivement très utile (mais pas de rapport avec la question de la licence, je compte recoder la fonctionnalité moi même ;-)). Pour le build system j'ai pris Meson (et apprécié la rapidité de configuration sous Windows par rapport à CMake), et pour la partie ligne de commande j'utilise en général les fonctionnalités de la glib.

    J'avais testé la compatibilité Windows sous MSYS2, mais je ne connaissais pas mingw-bundledlls. Ça a l'air pas mal mais seulement adapté aux projets comme les tiens avec des dépendances purement à des bibliothèques. Ça ne résoud pas les dépendances à des ressources (configuration, icônes, thèmes). Une autre possibilité pour créer un bundle est de créer un package MSYS2 pour ton appli et de l'installer avec ses dépendances dans un préfixe spécifique. Tu n'as ensuite plus qu'à livrer le dossier en question. Je n'ai pas encore essayé en pratique mais ça se fait.

    Et je ne dirai rien sur github ;). Merci d'avoir partagé ton projet :)

    • [^] # Re: Très bonne idée !

      Posté par . Évalué à 4.

      Tu dois être dans les 300 lignes de code C++, c'est à peu près dans dans ce cas là que la FSF recommande l'Apache-2.0.

      Aux fils des corrections/évolutions il y aura peu être plus de lignes de code, donc bon…

      • [^] # Re: Très bonne idée !

        Posté par (page perso) . Évalué à 7.

        Il me semble qu'il y a aussi un caractère d'innovation mis en avant par la FSF. Par tu peux mettre une bibliothèque qui fait des choses basiques en GPL-3, mais en pratique ça ne sert pas à grand chose, ça freine la redistribution alors qu'il y a plein d'alternatives qui font la même chose (ou mieux) sous des licenses plus libérales (BSD, MIT). Mais après chacun décide de faire ce qu'il veut avec son code :).

    • [^] # Re: Très bonne idée !

      Posté par (page perso) . Évalué à 3. Dernière modification le 19/02/18 à 16:52.

      Je ne connaissais cette recommandation de la FSF, merci pour ce retour !

              $ cloc .
                    23 text files.
                    23 unique files.
                    21 files ignored.
      
              https://github.com/AlDanial/cloc v 1.66  T=0.02 s (362.2 files/s, 27769.9 lines/s)
              -------------------------------------------------------------------------------
              Language                     files          blank        comment           code
              -------------------------------------------------------------------------------
              C++                              3             44             48            230
              Python                           1             29             24             90
              Bourne Shell                     1             15             15             40
              CMake                            1             11             18             39
              C/C++ Header                     2             14             29             38
              make                             1              2              1              3
              -------------------------------------------------------------------------------
              SUM:                             9            115            135            440
              -------------------------------------------------------------------------------
      

      Qu'est-ce qui compte ? les 230 lignes de C++ ou les 440 lignes au total ? ;-)

      • [^] # Re: Très bonne idée !

        Posté par (page perso) . Évalué à 3.

        C'est toi l'auteur, c'est toi qui vois ;-). Je suis pas sûr que la FSF oblige qui que ce soit à utiliser une licence à 299 LOC et une autre à 301 LOC donc c'est laissé à ton appréciation :-p

  • # dynamique sonore

    Posté par (page perso) . Évalué à 4.

    C'est un poil hors sujet, mais connaissez vous un outil similaire pour calculer la dynamique (en temps réel ou sur des fichiers son) ?

  • # Pas besoin de ncurses

    Posté par (page perso) . Évalué à 10.

    Pas besoin de ncurses pour cacher les touches et utiliser autre chose que entrée. Ça peut se faire avec tcsetattr(), en désactivant le mode 'canonical' et les différents ECHO.

    Typiquement:

    struct termios tios = {0};
    tcgetattr(0, &tios);
    tios.c_lflag &= ~(ECHO|ECHONL|ICANON);
    tcsetattr(0, TCSANOW, &tios);
    Si le but est d'être compatible windows, il faut cependant mettre ça sous ifdef (par exemple _MSC_VER) car c'est différent là-bas.

    Le reste a très peu besoin de changer, mais un peu quand même.

    Sinon le fichier main.c a des fins de ligne windows contrairement aux autres fichiers.

  • # Question et info

    Posté par (page perso) . Évalué à 6.

    J'utilise généralement https://www.all8.com/tools/bpm.htm qui marche bien.

    Question: pourquoi se limiter au 5 dernières frappes? Autant prendre la moyennes de toutes les frappes (en limitant à 100, si tu veux).
    Sur des chansons à 200+bpm comme https://www.all8.com/tools/bpm.htm, 5 frappes, c'est pas idéal, je trouve.

    • [^] # Re: Question et info

      Posté par (page perso) . Évalué à 4.

      Les deux approches ont leurs avantages et inconvénients. Sur 5 frappes tu es moins précis mais tu peux gérer plus facilement les variations dans un morceau. Sur tout l'historique, c'est bien si ton morceau est joué au métronome mais si le tempo est fluctuant, tu lisses et t'éloignes du tempo original. À ce moment là les 5 dernières frappes est plus utile.

      Titre de l'image

      Source: http://blog.fixyourmix.com/category/audio/audio-myths/

      Après peut être que les frappes des n dernières secondes plutôt que les n dernières frappes serait plus précis, car ça gère le cas des tempos rapides > 200bpm.

    • [^] # Re: Question et info

      Posté par (page perso) . Évalué à 2.

      Tu peux utiliser l'option -s ou --sample-size pour prendre en compte plus d'échantillons dans le calcul.

  • # Petite question d'implémentation

    Posté par (page perso) . Évalué à 5.

    D'abord, félicitations pour ce petit soft bien sympa.
    Ensuite, j'ai une petite question sûrement idiote mais pourquoi as-tu stocké les timestamps de chaque appui de touche pour ne finalement prendre que la première et la dernière valeur et calculer une moyenne ?
    N'était-il pas plus simple de ne garder que le nombre d'appuis et le premier et le dernier horodatage ?
    J'ai dû louper un truc :D

    • [^] # Re: Petite question d'implémentation

      Posté par (page perso) . Évalué à 4.

      Merci pour tes félicitations.

      Le calcul du tempo est glissant sur les 5 dernières frappes, donc on doit garder les 5 derniers timestamps car lorsqu'un nouveau timestamp arrive (une touche a été pressée), on décale la liste des timestamp. Le 4éme devient le 5éme, le 3éme le 4éme, etc. et le nouveau devient le 1er.

      Un petit schéma vaut mieux qu'un grand discours :

      On a 5 frappes enregistrées, le calcul du tempo se base sur t et t-4:

      +------------+---+----+---+----+--------------------------> temps
                  t-4  t-3  t-2 t-1  t
      

      Une nouvelle frappe arrive (t+1), le calcul se base maintenant sur t+1 et t-3:

      +------------+---+----+---+----+---+----------------------> temps
                (t-4)  t-3  t-2 t-1  t   t+1
      

      Donc on est obligé de garder l'historique des timestamps sinon on aurait perdu t-3.

      Dans l'implémentation, les timestamps sont enregistrés dans une file (std::queue) on ajoute au début la nouvelle frappe et on jette la plus ancienne (t-4 ici).

      J'espère que cela a répondu à tes questions :)

      • [^] # Re: Petite question d'implémentation

        Posté par (page perso) . Évalué à 4. Dernière modification le 22/02/18 à 13:16.

        Merci de l'explication, j'avais pas tout compris effectivement et c'est dommage puisque je me suis amusé à commencer une implémentation à l'identique en Ada ;)
        Du coup, il vaut mieux comprendre :D

        En tout cas, c'est finalement très logique et ça explique bien les paramètres que l'on peut passer.

        Normalement, en Ada, il ne me reste que la gestion des options (autre que le simple argc, argv) et l'internationalisation car ce sont des trucs qu je n'ai jamais bidouillé.

  • # J'ai trouvé ce projet marrant

    Posté par (page perso) . Évalué à 3. Dernière modification le 24/02/18 à 17:26.

    Et je me suis amusé à le traduire en Rust (pour le feun):

    https://gitlab.com/Boiethios/tempotap

    J'ai juste ignoré la traduction et ce qui concerne l'installation.

    (Concernant la licence, je n'y connais pas grand' chose, j'espère que je n'ai pas fait d'erreur. J'ai mis le texte de la GPL V3 et j'ai indiqué la provenance du code).

    • [^] # Re: J'ai trouvé ce projet marrant

      Posté par (page perso) . Évalué à 3.

      Bravo pour cette traduction :)

      Le code a l'air plus compact, c'est une caractéristique de Rust ?

      Je vois que tu as rajouté la gestion d'erreur si l'écriture sur le terminal échoue. Bonne idée, il faut que je le rajoute !

      • [^] # Re: J'ai trouvé ce projet marrant

        Posté par (page perso) . Évalué à 3.

        J'aurais du mal à détailler toutes les caractéristiques du Rust (ou de n'importe quel langage) en quelques lignes, mais je dirais que si le code est plus concis, c'est dû d'une part aux macros procédurales qui permettent de générer n'importe quel code à la compilation:

        #[derive(Debug, StructOpt)]
        struct Params {
            #[structopt(short = "p", long = "precision", default_value = "0")]
            /// set the decimal precision of the tempo display [max: 5]
            precision: usize,
            #[structopt(short = "r", long = "reset-time", default_value = "5")]
            /// set the time in second to reset the computation
            reset_time: u64,
            #[structopt(short = "s", long = "sample-size", default_value = "5")]
            /// set the number of samples needed to compute the tempo
            sample_size: usize,
        }
        

        Ce code va générer tout le code nécessaire à la gestion des paramètres en ligne de commande par exemple. Ça a la même utilité que les templates en C++, mais c'est plus puissant (tu peux réécrire tout l'arbre syntaxique du code sur lequel s'applique la macro).

        La deuxième raison est que le code est orienté expression (comme un langage fonctionnel). On peut écrire des choses comme:

        fn are_close(a: f64, b: f64) -> bool {
            (a - b).abs() < 0.0001
        }
        

        Si j'ai géré le cas d'erreur c'est que c'est obligatoire en Rust (sinon ça génère un warning). En soi, je ne suis pas sûr que ce soit super utile, j'imagine qu'il y a peu de chance que l'écriture sur le terminal échoue.

        • [^] # Re: J'ai trouvé ce projet marrant

          Posté par (page perso) . Évalué à 3.

          Effectivement ça a l'air puissant ces macros. A voir pour la lisibilité sur des cas plus complexes.

          Si j'ai géré le cas d'erreur c'est que c'est obligatoire en Rust (sinon ça génère un warning). En soi, je ne suis pas sûr que ce soit super utile, j'imagine qu'il y a peu de chance que l'écriture sur le terminal échoue.

          Je ne sais pas ce que fait exactement std::io::stdout().flush().expect("Error: cannot flush"); mais si ça écrit sur le terminal lors d'une erreur d'écriture, ça sent la boucle infinie :-)

  • # une version python 2.7

    Posté par (page perso) . Évalué à 0.

    #!/usr/bin/python
    # -*- coding: utf-8 -*
    import sys,termios,tty,datetime
    
    def getKey():
        fd = sys.stdin.fileno()
        old_settings = termios.tcgetattr(fd)
        try:
            tty.setraw(fd)
            ch = sys.stdin.read(1)
        finally:
            termios.tcsetattr(fd, termios.TCSADRAIN, old_settings)
        return ch
    
    print "tapTempo : press any key (q for quit)"
    t=[]
    while getKey()!="q":
        t.append( datetime.datetime.now() )
    
    ll=[ (j-i).microseconds for i, j in zip(t[:-1], t[1:]) ][-5:]
    print "BPM:",60000000*len(ll)/sum(ll)
    
  • # Bientôt dans votre distribution

    Posté par . Évalué à 4.

    On dirait que TapTempo prend son envol : https://bugs.debian.org/cgi-bin/bugreport.cgi?bug=893306

Suivre le flux des commentaires

Note : les commentaires appartiennent à ceux qui les ont postés. Nous n'en sommes pas responsables.