1. Introduction

Le jeu vidéo est un média assez répandu, se jouant habituellement à l'aide d'une manette, ou d'un clavier. Cependant, certains d'entre eux se contrôle de façon plus original, à l'aide d'un volant, d'une caméra ou d'un instrument de musique, dans des jeux musicaux. C'est cette dernière catégorie qui nous intéresse ici. Le but est de recréer une version de la manette du jeu Taiko no Tatsujin, manette en forme de tambour traditionnel japonais (Taiko). Les règles sont les suivantes : Le joueur doit frapper sur le Taiko sur le rythme d’une musique. Le rythme est symbolisé par des notes qui apparaissent à l’écran et selon leur couleur il faut frapper au centre, si la note est rouge, ou sur les côtés du tambour, si la note est bleue. Cependant, une vidéo est plus parlante. L'objectif du projet est de réalisé un tambour qui, à base de capteurs de vibrations, est capable de localiser un impact sur une surface circulaire et de pouvoir déterminer si le joueur à frappé au centre ou sur les côtés. Ensuite le but est d'envoyer cette information, sous forme d'inputs de clavier, à un ordinateur sur lequel tournera une version du jeu pour PC (comme le mode Taiko du jeu Osu!). Cette manette existe déjà en salle d'arcade, comme ce qui peut être constaté sur la vidéo précédente, de plus une version réduite, utilisable chez soi se trouve sur le marché. Cependant, ce produit n'est compatible qu'avec les consoles possédant une version du jeu, tandis que mon projet vise à créer un modèle pouvant être utilisé sur un ordinateur.

2. Matériel et méthodes

2.1 Matériel

  • 1 Raspberry Pi Zero W (Le Pi 3 ne fonctionne pas car il ne possède pas d'USB OTG permettant d'envoyer des "ordres" dans les deux sens.)
  • 4 Piezo Knock Sensors, le modèle utilisé est le MEAS 1006015-1, dont la documentation est disponible ici.
  • 4 Résistances de 1MOhm chacune.
  • 4 Diode Zener avec un seuil d'effet avalanche à 5.1v
  • 1 ADC (Analog to digital converter), le modèle utilisé est le MCP3008.
  • 1 Breadboard pour la connexion des éléments, même si à la fin les composants peuvent aussi être soudés.
  • Des fils électriques.
  • 1 Surface circulaire en liège, le modèle utilisé est un sous-plats de 22.5 cm de diamètre, disponible ici.
  • 1 Paire de baguettes de batterie.
  • 1 câble micro USB vers USB pour l'alimentation et le communication du Raspberry.

2.2 Méthode

2.2.1 Montage

Les capteurs utilisés sont composés de matériaux piézoélectriques, c'est-à-dire qu'ils produisent une tension s'ils sont déformés par une action mécanique, tel une vibration, comme c'est le cas ici. Le signal produit est analogique, car ils s'agit de valeurs de tensions multiples, or le Raspberry ne peut lire que des valeurs digitales sur ses pins, soit une tension haute, soit une tension basse. C'est pourquoi il faut passer par un ADC, permettant d'interpréter les valeurs de tensions émises par les capteurs.

Ensuite il faut relier les 4 capteurs au MCP3008. Les piezos produisent un courant au voltage proportionnel à l'intensité de la vibration et proportionnel à la résistance, c'est pourquoi on en utilise une de 1MOhm, pour avoir des valeurs assez hautes, donc détectables. Cependant il ne faut pas que la tension de sortie ne monte trop haut, car cela pourrait endommager l'ADC. On prévoit donc une diode Zener, qui laisse passer le courant à la terre si la tension dépasse un certain seuil, ici 5.1v. Le montage est le suivant : Piezo.png

Figure 2.1 Montage d'un capteur de vibration.

Il suffit de répéter ce montage pour chaque capteur. Il faut encore placer les capteurs sous la surface du tambour, comme ceci :

photo_2019-05-16_21-58-25.jpg Figure 2.2 Placement des capteurs sous la surface

Les capteurs sont numérotés de 0 à 3 dans le sens horaire, quand on regarde depuis le dessus. (Donc sens antihoraire sur la photo de dessus), le capteur 0 est en haut. Les piezos sont fixés sur des supports, pour ne pas à avoir à les placer sur la breadbord directement, ce qui donne ceci :

photo_2019-05-16_21-58-17.jpg Figure 2.3 Fixation d'un capteur

Le câblage, avec la résistance et la diode, se fait sur la breadboard, comme ceci :

photo_2019-05-16_21-57-50.jpg Figure 2.4 Montage sur la breadboard

2.2.2 Localisation des coups

Le but est d’abord de pouvoir lire l'intensité de chaque capteur, et de comparer le temps qu'ils mettent pour détecter un coup, car les vibrations n'arrivent pas en même temps. Tout d'abord il faut pouvoir accéder à l'ADC, à l'aide du code suivant :

import numpy as np
import time
# Import SPI library and MCP3008 (ADC) library
import Adafruit_GPIO.SPI as SPI
import Adafruit_MCP3008.MCP3008 as MCP3008

# Hardware SPI configuration
SPI_PORT = 0
SPI_DEVICE = 0
mcp = MCP3008(spi=SPI.SpiDev(SPI_PORT, SPI_DEVICE))

Numpy va servir à stocker les valeurs sous formes de matrices, tandis que time sera utilisé pour calculer les différences de temps. L'ADC est référencé par la variable mcp. Pour lire une valeur d'un des channels de l'ADC il suffit d'appeler la fonction suivante, qui retourne un entier entre 0 et 1023 de la tension à un des channel :

mcp.read_adc(channel)

Il faut ensuite pouvoir lire les valeurs des capteurs, de la façon suivante :

#This function return an array if there is a hit, with the intensity and the time difference between the detection of each sensor
def ReadValues(sensorsNumber):
    firstSensorIdx = 0
    isMeasuring = False
    values = np.zeros((4,2))
    initialTime = 0

    # Check if there is a hit
    for i in range(sensorsNumber):
        piezoValue = mcp.read_adc(i)
        if(piezoValue > HIT_THRESHOLD):
            isMeasuring = True
            initialTime = time.time()
            values[i,1] = piezoValue
            firstSensorIdx = i
            break

    # If the is a hit, starts the measure by checking every sensor until they all have return a value
    while isMeasuring:
        for i in range(sensorsNumber):
            piezoValue = mcp.read_adc(i)
            if(values[i,1] == 0 and piezoValue > HIT_THRESHOLD):
                values[i,1] = piezoValue
                values[i,0] = time.time() - initialTime

        if(np.count_nonzero(values[:,1]) == sensorsNumber or time.time() - initialTime >= T_MAX):
            time.sleep(0.05)
            return values

Il faut noter que la mesure ne peut pas se faire à un seul moment, car les capteurs ne détectent pas tous en même temps la vibration, car celle-ci se propage dans le matériau. C'est pourquoi il faut tout d'abord vérifier, dans la première boucle for, si un des piezo détecte quelque chose. Si oui alors la boucle while se lance, ici le code regarde chaque capteurs n'ayant encore rien détecté, et s'ils affichent une valeur supérieur au seuil, on l'enregistre et on calcul la différence de temps entre la mesure du premier capteur et le nouveau. Les deux mesures (temps et intensité) sont ensuite enregistrées dans le tableau values. Cette boucle prend fin si tous les capteurs ont retourné une mesure, ou si le temps maximal, défini par la constante T_MAX, est atteint, cela évite de faire une mesure à l'infinie dans le cas où un capteur ne détecte rien. On ajoute aussi un time.sleep() avant de retourner les valeurs, car les capteurs avaient tendance à rapidement retourner une deuxième mesure, car la vibration fait des aller-retour et peut donc à nouveau être détectée.

Il faut maintenant, pour localiser le coup, soit comparer les différences de temps de détection, soit comparer les différentes intensités. Tout d'abord regardons avec les intensités, à l'aide du code suivant :

# This function only works with 4 sensors
def EstimateLocationWithIntensity(sensorsLocation, sensorsValue):

    if np.size(sensorsLocation, 0) != np.size(sensorsValue, 0) and np.size(sensorsValue,0) != 4:
        print("Error : The number of sensors in the two arrays is not the same or not equal to 4")
        return

    # This function only work if every sensors have return an intensity
    if np.count_nonzero(sensorsValue[:,1]) != np.size(sensorsValue, 0):
        print("Error : One of the sensor has not return a value")
        return

    x_coord = (sensorsValue[1,1] - sensorsValue[3,1]) * ((sensorsLocation[1,0]-sensorsLocation[3,0])/2) / I_MAX

    y_coord = (sensorsValue[0,1] - sensorsValue[2,1]) * ((sensorsLocation[0,1]-sensorsLocation[2,1])/2) / I_MAX

    return np.array([[x_coord,y_coord]])

On vérifie, dans les deux premier if, qu'il n'y a que 4 capteurs, et qu'ils ont tous lu une valeur, car sinon la fonction ne marche pas. De plus il faut que les 4 capteurs soient sur les axes, et que l'origine des axes soit le centre du tambour. On peut donc, pour calculer la coordonnée x, prendre les deux capteurs opposés, sur l'axe des x, et mesurer la différence entre les deux intensités. Cette valeur est proportionnel à la composante x du point inconnu. Il faut maintenant mettre cette valeur à l'échelle, en la divisant par l'écart d'intensité maximum. Ce dernier est à déterminer, en regardant la différence maximum mesurable, en frappant plusieurs fois directement au niveau d'un capteur, cette valeur est très approximative. Il faut encore multiplier le tout par la distance entre un capteur et le centre du tambour, qui dans notre cas vaut 11,25. On répète le processus avec les deux piezo sur l'axe des y.

On cherche ensuite une autre méthode pour estimer la position du coup, à l'aide des différences de temps mis par les capteurs pour détecter une valeur. Le code permettant de prendre une mesure permet aussi de calculer le temps supplémentaire que prend l'onde pour un atteindre un second capteur, après avoir déjà été détectée par un premier capteur. En multipliant ce temps par la vitesse du son dans le liège, on obtient la distance supplémentaire séparant le second capteur du point inconnu, par rapport à la distance séparant ce point du premier capteur. Visuellement cela donne : Capteur.PNG Figure 2.5 Visualisation des informations données par les différences de temps.

Le point P(x;y), correspondant à la localisation du coup doit se trouver à égal distance du premier capteur et du cercle vert. Pour calculer la distance entre P et le cercle on a :

CodeCogsEqn(14).png
Équation 2.1 Distance entre un point(x;y) et le cercle de rayon RAYON et de centre (Cx;Cy)

Pour calculer la distance avec un point :

CodeCogsEqn(12).png Équation 2.2 Distance entre un point(x;y) et un capteur

Le point inconnu est situé à égal distance des deux, il respecte donc l'équation suivante :

CodeCogsEqn(15).png Équation 2.3 Équation de l'hyperbole à équidistance du cercle et du point

On devrait obtenir une hyperbole contenant le point que nous cherchons, il faut donc répéter le processus en comparant un autre capteur avec celui de référence (celui ayant détecté en premier). On obtient une deuxième hyperbole, et l'intersection des deux correspond à la position du coup[1]. Il y a possiblement plusieurs solutions, mais il faut uniquement prendre l'intersection qui se situe sur le tambour. Je n'ai malheureusement pas réussi à implémenter cette fonction. De plus en utilisant 450 m/s[2] pour la vitesse du son dans le liège, on tombe sur des résultats incohérents. Par exemple, le temps séparant le capteur 0 du capteur 2 est souvent autour des 0.01 secondes, ce qui équivaudrait à 4,5 mètres parcouru, or les deux capteurs sont éloignés de 22.5 centimètres. On peut supposer qu'il faut prendre en compte le temps de calculs et des mesures du raspberry, qui est significatif. Pour pallier à ça on pourrait trouver une autre constante de la vitesse du son, qui prendrait aussi en compte ce temps supplémentaire.

Il existe une troisième solution, beaucoup plus simple, mais moins intéressante. Il suffit de placer un capteur sous chaque zones où le joueur est susceptible de frapper (deux aux centres, deux sur les bords supérieurs), et de regarder quel capteur détecte quelque chose en premier, ou le quel retourne la plus haute valeur d'intensité.

2.2.3 Conversion en inputs de claviers

Pour cette partie, la méthode de iSticktoit a été utilisée[3]. Cela ne fonctionne qu'avec un Raspberry Pi zero, car celui-ci possède un port micro-usb On-The-Go (OTG). Avec le protocol USB de base, les appareils communiquent sur un schéma maître-esclave, avec donc un des deux qui donne des ordres et l'autre qui les exécute. Avec la norme OTG, cette relation est supprimée, chaque appareil peu communiquer dans les deux sens[4]. Il faut d'abord depuis le Raspberry, exécuter ces trois commandes, qui permettent d'activer certains drivers. :

pi@raspberrypi:~ $ echo "dtoverlay=dwc2" | sudo tee -a /boot/config.txt
pi@raspberrypi:~ $ echo "dwc2" | sudo tee -a /etc/modules
pi@raspberrypi:~ $ sudo echo "libcomposite" | sudo tee -a /etc/modules

Elles ont pour effet de rajouter à la fin du fichier, en deuxième partie de commande, le résultat de la première commande. Par exemple la première ligne rajoute dtoverlay=dwc2 à la fin du fichier config.txt. Il faut ensuite créer un fichier de configuration et le rendre exécutable, comme ceci :

pi@raspberrypi:~ $ sudo touch /usr/bin/isticktoit_usb
pi@raspberrypi:~ $ sudo chmod +x /usr/bin/isticktoit_usb

Celui-ci doit être rempli de la manière suivante :

#!/bin/bash
cd /sys/kernel/config/usb_gadget/
mkdir -p isticktoit
cd isticktoit
echo 0x1d6b > idVendor # Linux Foundation
echo 0x0104 > idProduct # Multifunction Composite Gadget
echo 0x0100 > bcdDevice # v1.0.0
echo 0x0200 > bcdUSB # USB2
mkdir -p strings/0x409
echo "fedcba9876543210" > strings/0x409/serialnumber
echo "Tobias Girstmair" > strings/0x409/manufacturer
echo "iSticktoit.net USB Device" > strings/0x409/product
mkdir -p configs/c.1/strings/0x409
echo "Config 1: ECM network" > configs/c.1/strings/0x409/configuration
echo 250 > configs/c.1/MaxPower

# Add functions here
mkdir -p functions/hid.usb0
echo 1 > functions/hid.usb0/protocol
echo 1 > functions/hid.usb0/subclass
echo 8 > functions/hid.usb0/report_length
echo -ne \\x05\\x01\\x09\\x06\\xa1\\x01\\x05\\x07\\x19\\xe0\\x29\\xe7\\x15\\x00\\x25\\x01
                \\x75\\x01\\x95\\x08\\x81\\x02\\x95\\x01\\x75\\x08\\x81\\x03\\x95\\x05\\x75\\x01
                \\x05\\x08\\x19\\x01\\x29\\x05\\x91\\x02\\x95\\x01\\x75\\x03\\x91\\x03\\x95\\x06
                \\x75\\x08\\x15\\x00\\x25\\x65\\x05\\x07\\x19\\x00\\x29\\x65\\x81\\x00\\xc0 
          > functions/hid.usb0/report_desc
ln -s functions/hid.usb0 configs/c.1/
# End functions

ls /sys/class/udc > UDC

Comme celui-ci doit être relancé à chaque démarrage, on modifie le fichier /etc/rc.local, en ajoutant ceci avant le exit0 :

/usr/bin/isticktoit_usb

Les commandes exécutées dans le fichier rc.local le sont à chaque fois que le raspberry pi boot. Je reprécise encore que ces étapes sont issues d'un guide, le code ne m'appartient donc pas. Ensuite, dans le script ayant besoin d'envoyer des inputs, il faut les écrire dans le fichier /dev/hidg0, comme ceci :

# This function write the inputs that are send by the usb otg port as keyboard inputs
# It must be executed as root
def SendKeyboardInputs(input):
    with open('/dev/hidg0', 'rb+') as fd:
        fd.write(input.encode())

Maintenant on peut vérifier dans quelle partie du tambour un point se situe, en regardant si, selon ses coordonnées, il est dans un cercle de rayon définie par la constante RAYON. Ce qui donne le code suivant :

# This function send a keyboard input by the usb otg port
def OutputKey(xLocation, yLocation):

    # If it's in the center
    if(math.sqrt(xLocation**2 + yLocation**2)<=RAYON):
        # Right
        if(xLocation >= 0):
            SendKeyboardInputs(J)
        # Left
        else:
            SendKeyboardInputs(F)
    # If it's in the right edge
    elif(xLocation >= 0):
        SendKeyboardInputs(K)
    # Left
    else:
        SendKeyboardInputs(D)

    # Release keys
    SendKeyboardInputs(RELEASE_KEY)

Le centre du cercle étant en (0;0), il suffit de calculer la norme du vecteur de l'origine au point, ce qui vaut à la racine de l'addition du carré des deux composantes. Les constantes J, F, K, D et RELEASE_KEY ont été définies en début de code. On peut ainsi relier le Raspberry Pi à un autre ordinateur et jouer à une version pc du jeu Taiko no Tatsujin, comme le mode Taiko du jeu osu! ou TJA Player 3. Il suffit de brancher le Raspberry et de lancer en tant que root le fichier suivant : IntensityDetection.py, cependant il faut faire attention car de base exécuter la commande python avec sudo éxecutera le code dans un autre environnement, il faut donc rajouter -E dans la commande, juste après sudo, comme par exemple :

sudo -E python IntensityDetection.py 

3. Résultats

Tout d'abord, il n'y a des résultats qu'avec la détection à l’aide de l'intensité, étant donné que je n'ai pas réussi à implémenter le code pour la détection à l'aide de la différence de temps. Pour évaluer la précision du Taiko, j'ai ajouté un script Evaluate.py qui print les touches de claviers normalement envoyées par le câble USB. Il suffit ensuite de répéter le même schéma de coup, c'est à dire le suivant : frapper au centre droit (j), puis au centre gauche (f), puis au bord gauche (d) et enfin dans le bord droit (k), puis de recommencer. Il faut ensuite récupérer les valeurs soit dans un fichier à part, soit en copiant dans le terminal, et de mettre le tout en forme dans un tableur, dont voici le lien. Sur 108 mesures, la précision est de 22,22 %.

Les mesures pour obtenir I_MAX sont disponibles sur une deuxièmes feuilles Google sheet, au même lien. On obtient une moyenne de 368,8 avec un écart type de 226,5758642, avec uniquement 10 mesures.

En partant du fait que la détection par le temps se base sur une approximation de la vitesse du son dans le liège, pour tenir compte du temps de calculs, on peut estimer que les résultats obtenus par cette méthode auraient aussi été très imprécis.

Il y a un autre point, plutôt inattendu, à relever. Les piezos ont tendance à se retirer de leurs supports lorsqu'ils sont soumis à une longues séries de vibration, ce qui arrive assez souvent.

La partie électronique, quant à ellle, fonctionne. Les valeurs peuvent être lue clairement, et, même si cela n'a pas été volontairement testé, l'ADC n'a pas été endommagé. On peut espérer que les diodes zener sont opérationnels.

4. Discussion

Les résultats sont peu convaincants, la fiabilité de ce prototype est faible, de nombreux éléments peuvent en être la cause. Tout d'abord, au niveau du matériel. Le plateau est peut être trop petit, avec une surface plus grande on se serait attendu à avoir plus de variation au niveau des intensités et des intervalles de temps, ce qui aurait affiné la localisation. Ensuite au niveau du montage, mes talents de bricolages étant limités, les piezo sont fixés de façon peu soignée et peu précise. Cela peut facilement fausser les mesures, certain capteurs pouvant plus vibrer que d'autre. Ensuite pour les mesures de temps, il est possible que l'ADC ne soit pas assez performants pour fournir des valeurs assez rapidement, comme le suggérait un forum. Ainsi, les mesures de temps sont affectées par le temps de détection et de calculs. Cela ce confirme quand on regarde les valeurs retournées, et qu'on les multiplient par la vitesse du son dans le liège. On obtient des distances impossibles, qui dépassent largement les dimensions du taiko. Pour la méthode. Avec les intensités, la formule se base sur une constante I_MAX, correspondant à intervalle d'intensité entre deux capteurs opposés maximale. Cette valeur a été obtenue expérimentalement, cependant elle est complétement approximative, avec un coefficient de variation de 61,4 % environ. On peut donc déjà déterminer que ceci fausse grandement les résultats. Ensuite on aurait du utiliser un procédé similaire pour le temps, en trouvant une vitesse de propagation de l'onde moyenne, en tenant compte du temps de calcul et lecture. Vu les résultats obtenus pour la constante d'intensité, on peut supposer qu'on aurait eu des problèmes similaire avec la vitesse.

Pour les capteurs qui se décroche, ont peut déduire que ce montage n'est pas le plus optimale, ce qui rejoint l'hypothèse comme quoi les piezos n'étaient pas tous dans les même conditions.

La partie montage électronique fonctionne, n'ayant jamais fait d'analogique avant, ceci est déjà un nouveau point aquis.

5. Conclusion

Ce projet est maintenant terminé, et même si les résultats ne sont pas excellents il semble possible d'améliorer le système pour le rendre efficace. Le but est partiellement atteint, cependant il semble évident que si la seul fonction de ce tambour est de jouer au jeu Taiko no Tatsujin, il est plus simple de ne pas essayer de localiser précisément de coup, mais juste de placer un capteur sous chaque surface, susceptible d'être frappée par le joueur. Cependant on pourrait utiliser ce projet pour une tout autre utilisation. On pourrait faire une batterie jouant des sons différents selon le lieu où l'on frappe. Ou bien encore une cible de tir, détectant automatiquement la localisation de l'impact. En bref, ce projet pourrait avoir un certain impact, s'il fonctionnait.

Je tient, avant de mettre le point final, à remercier ma famille et mes voisins, qui ont dû apprécier d'entendre quelqu’un frapper sur un tambour en liège à deux heures du matin.

Références

Notes

[1] Somolinos J.A., López A., Morales R., Morón C. A New Self-Calibrated Procedure for Impact Detection and Location on Flat Surfaces. Sensors (30 mai 2013), https://www.ncbi.nlm.nih.gov/pmc/articles/PMC3715253/

[2] The Engineering ToolBox, Speed of Sound in common Solids, Page consultée le 15 mai 2019 à partir de https://www.engineeringtoolbox.com/sound-speed-solids-d_713.html

[3] Composite USB Gadgets on the Raspberry Pi Zero. (22 février 2016). iFixtoit. Page consultée le 8 mai 2019 à partir de http://www.isticktoit.net/?p=1383

[4] USB On-The-Go. (13 avril 2019). Wikipédia, l'encyclopédie libre. Page consultée le 13 avril 2019 à partir de http://fr.wikipedia.org/w/index.php?title=USB_On-The-Go&oldid=158417795.