Journal PyAlsaCap : Python, pointeurs, et cartes sons…

Posté par  (site web personnel) . Licence CC By‑SA.
44
14
mar.
2013

Sommaire

Pour fêter le retour de DLFP après cette trop longue vacance, voici un petit journal pythonesque, mais pas seulement !

Dans mon dernier journal, nous avions joué avec l'introspection Python et l'export de fonction sur D-Bus. Pour ce faire, nous avions généré du Python avec Python !

Cette fois-ci, je propose de combiner le meilleur de Python avec le meilleur de C ! Nous allons jouer avec des pointeurs… depuis un interpréteur Python !

En plus de tout cela, nous ne sommes plus limité à Python 2. Et nous pouvons sortir avec une peau toute neuve, laissant la vieille mue derrière nous !

Pour cela il faut remercier lolop< qui a partagé une excellente idée : attaquer libasound directement depuis Python avec le module ctype. Si vous ne l'aviez pas encore pertinenté, c'est le moment !

Mais cela n'est pas tout, il faut aussi remercier tankey< de nous avoir fait découvrir alsacap !

alsacap est un merveilleux petit programme développé par Volker Schatz. Je ne sais pas pourquoi ce programme ne fait pas partie de alsa-utils, parce que tout en étant un petit bijou, il répond à une problématique légitime et pourtant insatisfaite : décrire le matériel pris en charge par ALSA ! Cela peut paraitre absurde, mais ces informations ne sont accessibles qu'aux développeurs, ALSA ne fournit pas d'interface utilisateur potable pour obtenir ces informations depuis le shell.

Si vous n'avez pas pertinenté tankey< pour cette contribution de grande valeur, vous pouvez le faire en suivant le lien suivant :
lien suivant. ;)

Ce journal est une grande avancé dans la quête que je poursuis : connaître les noms de mes cartes sons, les échantillonnages pris en charge, le nombre de canaux audio et autres informations très utiles, et ce depuis Python ! Nous allons réécrire alsacap en Python !

Le serpent est rusé, dit-on… et moi j'ai chuté !

Python et ctypes

Ctypes est donc une bibliothèque Python qui fournit des types compatibles C et qui permet d'appeler des fonctions appartenant à des bibliothèques C… Avec, on peut donc interfacer C et Python sans compilation !

Pour utiliser une bibliothèque C, rien de plus simple, essayons avec la libc :

In [1]: from ctypes import *

In [2]: libc = cdll.LoadLibrary("libc.so.6")

Désormais, nous voilà avec un objet nommé libc, les fonctions sont tout simplement des éléments de cet objet. Malheureusement, l'introspection ne marche pas sur ce type d'objet, il nous faudra donc, pour connaître les fonctions, nous référer à leur documentation officielle ou bien au code source (vive le logiciel libre ) !

Ainsi, pour écrire sur la sortie standard le caractère « a », nous pouvons appeler la fonction putchar ainsi :

In [3]: libc.putchar(c_char(b'a'))
Out[3]: 97
a

Notre caractère « a » est bien affiché !

Nous découvrons une chose, le type c_char. Le module ctypes fournit une fournée de types de ce type (c_int, c_bool…) qui peuvent être convertis vers et depuis les types Python (int, bool).

Nous découvrons autre chose : le code de retour est numérique, en effet, c'est un entier.
Nous savons que putchar renvoie, si succès, la valeur numérique du caractère passé en paramètre. Mais il existe des fonctions qui renvoient d'autres types ! Et bien nous pouvons spécifier le type attendu avec l'attribut restype. Ce n'est pas très utile, mais nous allons le faire pour putchar, nous allons déclarer que le type de retour est c_char.

In [4]: libc.putchar.restype = c_char
In [5]: libc.putchar(c_char(b'a'))
Out[5]: b'a'
a

Avant de passer aux choses plus intéressantes, attardons-nous un instant pour adresser une parole pleine de bonnes intentions à nos amis du bouchot :

In [6]: libc.printf(c_char_p(b"salut les moules ! \_o< coincoin"))
Out[6]: 32
salut les moules ! \_o< coincoin

Vous aurez remarqué que le nom du type c_char_p termine par les deux caractères _p, ce qui signifie… que c'est un pointeur. Effectivement, nous savons qu'en C, les chaînes de caractères sont des tableaux de char, et que l'adresse du tableau est l'adresse de son premier caractère ! Avoir bon pointeur c'est s'assurer d'avoir toujours bon caractère…

Passage par référence

Pour décrire nos cartes, il va falloir appeler des fonctions de libasound. Puisque alsacap fait tout ce que nous désirons, nous allons l'imiter. Il nous servira de guide spirituel !

Dans alsacap.c nous trouvons très vite une fonction nommée scancards. Dans cette fonction on trouve le code suivant (à peu près) :

int card = -1;
if(snd_card_next(&card) < 0)
    return;
while(card >= 0) {
    /* some code */
    if (snd_card_next(&card) < 0)
        break;
}

Regardons premièrement la forme de ce code, quelque chose nous saute aux yeux. Nous voyons que la fonction snd_card_next prend pour argument le paramètre &card. Nous devrions plutôt dire que la fonction snd_card_next prend pour argument la référence de card, c'est à dire son adresse.

En Python avec ctypes nous pouvons écrire un passage par référence ainsi :

snd_card_next(byref(card))

Maintenant, faisons plus attention au sens de ce code, ce qu'il fait. Nous remarquons que la variable card est initialisée à -1, et qu'ensuite elle passée par référence à la fonction snd_card_next qui doit donc certainement changer sa valeur par celle de la carte suivante. Nous remarquons aussi que cette fonction retourne un entier négatif en cas d'erreur.

Transposons en python, avec un peu de code supplémentaire pour analyser le fonctionnement. Pour simplifier nous ne nous préoccupons pas des erreurs :

In [1]: from ctypes import *

In [2]: libasound = cdll.LoadLibrary("libasound.so.2")

In [3]: card = c_int(-1)

In [4]: libasound.snd_card_next(byref(card))
Out[4]: 0

In [5]: while card.value >= 0:
   ...:         print(str(card.value))
   ...:         libasound.snd_card_next(byref(card))
   ...:
0
29

In [6]: print(str(card.value))
-1

Nous vérifions nos suppositions : la fonction snd_card_next donne la carte suivante à celle donnée en argument. La valeur -1 signifie que la liste a été parcourue et qu'il faut recommencer à zéro (si la première s'appelle zéro).

Nous pouvons donc écrire la fonction GetCards suivante :

def GetCards():
    cards = []
    card = c_int(-1)
    if libasound.snd_card_next(byref(card)) < 0:
        return cards
    while card.value >= 0:
        cards.append(card.value)
        if libasound.snd_card_next(byref(card)) < 0:
            break
    return cards

In [8]: GetCards()
Out[8]: [0, 29]

J'ai effectivement deux cartes sons, hw:0 et hw:29. Première réussite !

C'était trop facile

Dans alsacap, le code qui nous intéresse ensuite est celui-là :

snd_ctl_card_info(handle, info);

Nous lisons un peu plus haut que ces deux variables sont déclarés ainsi :

static snd_ctl_t *handle= NULL;
static snd_ctl_card_info_t *info;

Oh, des pointeurs ! Mais quels sont leurs types ?

Nous trouvons dans /usr/include/alsa/control.h les déclarations suivantes :

/** CTL handle */
typedef struct _snd_ctl snd_ctl_t;
/** CTL card info container */
typedef struct _snd_ctl_card_info snd_ctl_card_info_t;

Ce sont des pointeurs de structures, autrement dit des pointeurs de pointeurs !
Aïe aïe aïe… nous ne pourrons pas utiliser byref(), car nous ne connaissons pas ce qui est pointé ! Surtout nous ne savons pas trop comment déclarer ça dans Python…

En fait, nous ne lirons jamais directement ces structures, nous utiliserons des fonctions pour cela. Par exemple, pour obtenir le nom de la carte son, nous ferons :

snd_ctl_card_info_get_name(info))

C'est rassurant, nous n'avons pas besoin de connaître exactement les structures, il semble qu'il nous faille seulement savoir passer les pointeurs de fonctions en fonctions.

Nous utiliserons donc le type générique c_void_p. Par souci de clarté, nous préfixerons aussi les noms de variables de type C avec les deux caractères c_.

En C nous avions quelque chose comme :

#define HWCARDTEMPL "hw:%d"
#define HWDEVLEN    32
card = 0
snprintf(hwdev, HWDEVLEN, HWCARDTEMPL, card);
snd_ctl_open(&handle, hwdev, 0)
snd_ctl_card_info(handle, info)

Nous allons essayer en Python :

In [1]: from ctypes import *
In [2]: libasound = cdll.LoadLibrary("libasound.so.2")
In [3]: card = 0
In [4]: hwdev = "hw:" + str(card)
In [5]: b_hwdev = create_string_buffer(str.encode(hwdev))
In [6]: c_handle = c_void_p()
In [7]: c_info = c_void_p()
In [8]: libasound.snd_ctl_open(byref(c_handle), b_hwdev, 0)
Out[8]: 0

Bien, ça a l'air de passer ! La valeur retournée aurait été non nulle s'il y avait eu une erreur…

On peut remarquer la conversion de chaîne de caractère entre les types str et bytes, ce sera souvent utile. Aussi, lorsque nous passons une chaîne de type byte, il n'est pas nécessaire de la passer via byref(). ctypes fait cela tout seul.

Continuons…

In [9]: libasound.snd_ctl_card_info(c_handle, c_info)
python3: control.c:234: snd_ctl_card_info: Assertion `ctl && info' failed.
Abandon

En fait non, nous ne continuons pas ! :)

Et là c'est le drame

Nos manipulations de pointeurs semble correctes :

In [6]: handle = c_void_p()
In [7]: info = c_void_p()
In [8]: handle.value

In [9]: libasound.snd_ctl_open(byref(c_handle), b_hwdev, 0)
Out[9]: 0
In [10]: c_handle.value
Out[10]: 51737712

Le pointeur c_handle passe bien de la valeur NULL à une adresse mémoire !

Qu'est ce qui ne va pas ? Nous avons oublié une chose essentielle…

Déjà, nous aurions pu remarquer que nous passions en paramètre un pointeur non initialisé, c_info. Nous cherchons donc où il est initialisé…

Nous trouvons ce code :

snd_ctl_card_info_alloca(&info);

Nous avons donc trouvé ce qui semble être une fonction qui initialise l'adresse vers laquelle le pointeur pointe… Et nous découvrons que ce que cela semble être une fonction d'allocation. En effet, on peut se douter qu'à l'adresse pointée par info, il n'y aura pas qu'un pointeur, mais des tas de données, chaînes de caractères, listes, etc.

Aïe aïe aïe, pour le pointeur nommé handle, comme son nom l'indique, nous n'avions pas besoin de nous soucier de son allocation, c'est un pointeur qui contient une référence vers un objet que nous ne gérons pas nous-même. Pour le pointeur nommé info, c'est tout autre chose !

Et si nous essayons, depuis python, d'appeler la fonction snd_ctl_card_info_alloca ?

Je l'ai tenté pour vous :

In [11]: libasound.snd_ctl_card_info_alloca(byref(c_info))
---------------------------------------------------------------------------
AttributeError                            Traceback (most recent call last)
<ipython-input-15-13b9fd869a33> in <module>()
----> 1 libasound.snd_ctl_card_info_alloca(byref(c_info))
[]
AttributeError: /usr/lib/x86_64-linux-gnu/libasound.so.2: undefined symbol: snd_ctl_card_info_alloca

C'est un cuisant échec.

Je veux mes allocs !

Mais que se passe-t'il ?

Ce ne serait pas une fonction ? mais qu'est-ce donc ?

Dans /usr/include/alsa/control.h je trouve :

#define snd_ctl_card_info_alloca(ptr) __snd_alloca(ptr, snd_ctl_card_info)

Dans /usr/include/alsa/global.h je trouve :

#define __snd_alloca(ptr,type) do { *ptr = (type##_t *) alloca(type##_sizeof()); memset(*ptr, 0, type##_sizeof()); } while (0)

Aïe, ce sont des macros ! Je ne pourrais donc pas appeler ce code depuis Python !
Mais rusons, ce qu'il nous faut savoir, c'est la taille de l'espace mémoire à allouer…

Nous pourrions chercher la définition exacte de la structure, et la déclarer en Python en héritant de l'objet Structure, mais cela serait un trop grand effort sans intérêt. En effet nous ne parcourrons jamais manuellement la structure, nous utiliserons des getters pour cela ! Tout ce qui nous importe, c'est donc la taille de la structure à allouer !

Nous n'allons pas jouer au préprocesseur et interpréter nous même ce code. Nous allons lire le code généré par le préprocesseur lorsqu'il rencontre cette macro d'allocation.

Juste avant la ligne qui nous intéresse, j'ajoute un témoin dans le code source d'alsacap (que je renomme au passage en alsacap-trace.c) :

snd_ctl_card_info_alloca(&info);
printf("THDTHDTHD");

Hop, préprocessons tout cela, et regardons :

gcc -E alsacap-trace.c -lasound | grep -B1 "THDTHDTHD"
  do { *&info = (snd_ctl_card_info_t *) __builtin_alloca (snd_ctl_card_info_sizeof()); memset(*&info, 0, snd_ctl_card_info_sizeof()); } while (0);
  printf("THDTHDTHD");

Nous trouvons la fonction (c'est est bien une cette fois-ci) snd_ctl_card_info_sizeof() qui retourne la taille mémoire qui doit être allouée ! Essayons…

In [12]: libasound.snd_ctl_card_info_sizeof()
Out[12]: 376

Mais comment allouons nous avec Python et ctypes ?
Pour allouer un tableau (au sens de C) d'entiers, la documentation nous indique que nous pouvons faire ainsi :

TenIntegers = c_int * 10

Pour ne pas nous embêter avec le type (nous ne le connaissons pas vraiment, et peut-il être retranscrit ?), nous allons « multiplier » le type c_void_p. Ainsi nous allouerons un espace mémoire dont l'adresse de départ sera de type « pointeur ».

Juste pour tester, tentons de passer un pointeur vers un espace mémoire de taille arbitraire mais conséquente :

In [13]: c_info = (c_void_p * 50)()
In [14]: libasound.snd_ctl_card_info(handle, c_info)
Out[14]: 0

Notre code ne plante pas, et la fonction retourne le code « no problemo ». Mais attention au « je reviendrai », dès qu'on va tenter d'utiliser un getter sur c_info, on va prendre le risque d'un segfault

Nous avons une fonction qui retourne une valeur, mais nous ne pouvons pas « multiplier » cette valeur avec le type, puisque ce type a déjà une taille !

In [15]: sizeof(c_void_p)
Out[15]: 8

Il nous faut donc « multiplier » le type avec la valeur retournée par ladite fonction, divisée par la taille du type.

In [16]: int(libasound.snd_ctl_card_info_sizeof() / sizeof(c_void_p))
Out[16]: 47
In [17]: c_info = (c_void_p * int(libasound.snd_ctl_card_info_sizeof() / sizeof(c_void_p)))()

Nous utilisons int() non parce que la division aurait un reste, mais parce qu'une division retourne un nombre de type float, type qui n'a pas de sens pour une taille mémoire.

Nous avons donc un espace alloué, et il est de bonne taille !
Nous pouvons le vérifier :

In [18]: sizeof(c_info)
Out[18]: 376

Nous allons donc l'utiliser !

Récupérer les valeurs

Remplir la structure c_info de ses valeurs, pour la carte sélectionnée :

In [19]: libasound.snd_ctl_card_info(c_handle, c_info)
Out[19]: 0

Appeler un getter :

In [20]: libasound.snd_ctl_card_info_get_id(c_info)
Out[20]: 52202712

Nous avons une réponse ! Mais c'est une drôle de réponse !

Mais oui c'est une chaîne de caractère dont nous avons obtenu l'adresse ! Il nous faut définir le type de retour de la fonction et ce sera bon…

In [21]: libasound.snd_ctl_card_info_get_id.restype = c_char_p
In [22]: libasound.snd_ctl_card_info_get_id(c_info)
Out[22]: b'Intel'

Et voilà ! Désormais il n'y a plus aucun mystère, aucune surprise, ni dans Python, ni dans ctypes, ni dans libasound… Nous pouvons retranscrire tout alsacap avec ce que nous avons appris !

Headers et malloc en Python ?

C'est le petit souci de ctypes, il nous faut reproduire (en partie) une spécificité du langage compilé qu'est le C, les headers. Aussi, nous sommes liés aux contraintes d'un langage aujourd'hui considéré comme bas-niveau qu'est le C, comme les allocations mémoires…

Par exemple j'ai du écrire ces déclarations :

libasound.snd_ctl_card_info_get_id.restype = c_char_p
libasound.snd_ctl_card_info_get_name.restype = c_char_p
libasound.snd_pcm_info_get_id.restype = c_char_p
libasound.snd_pcm_info_get_name.restype = c_char_p
libasound.snd_pcm_info_get_subdevice_name.restype = c_char_p
libasound.snd_pcm_format_name.restype = c_char_p
libasound.snd_pcm_format_mask_test.restype = c_bool

Ces allocations :

c_info = (c_void_p * int(self.libasound.snd_ctl_card_info_sizeof() / sizeof(c_void_p)))()
c_pcminfo = (c_void_p * int(self.libasound.snd_pcm_info_sizeof() / sizeof(c_void_p)))()
c_pars = (c_void_p * int(self.libasound.snd_pcm_hw_params_sizeof() / sizeof(c_void_p)))()
c_fmask = (c_void_p * int(self.libasound.snd_pcm_format_mask_sizeof() / sizeof(c_void_p)))()

Et puis j'ai reproduit un certain nombre de constantes de libasound :

En C nous avons :

/usr/include/alsa/pcm.h
** PCM sample format */
typedef enum _snd_pcm_format {
    /** Unknown */
    SND_PCM_FORMAT_UNKNOWN = -1,
    /** Signed 8 bit */
    SND_PCM_FORMAT_S8 = 0,
    /** Unsigned 8 bit */
    SND_PCM_FORMAT_U8,
    /** Signed 16 bit Little Endian */
    SND_PCM_FORMAT_S16_LE,
    /** Signed 16 bit Big Endian */
    SND_PCM_FORMAT_S16_BE,
[]
    SND_PCM_FORMAT_U18_3BE,
    SND_PCM_FORMAT_LAST = SND_PCM_FORMAT_U18_3BE,

J'ai donc écrit en Python :

SND_PCM_FORMAT_S8 = c_int(0)
SND_PCM_FORMAT_U8 = c_int(1)
SND_PCM_FORMAT_S16_LE = c_int(2)
SND_PCM_FORMAT_S16_BE = c_int(3)
# […]
SND_PCM_FORMAT_U18_3BE = c_int(43)
SND_PCM_FORMAT_LAST = SND_PCM_FORMAT_U18_3BE

Mais rassurez-vous, je n'ai pas écrit tout cela à la main, sed m'a rendu un bien grand service !

PyAlsaCap

Et voilà, je n'ai plus eu qu'à suivre méthodiquement le code source d'alsacap et à dérouler avec lui la grande boucle qui interroge les cards, puis les devices, puis les subdevices etc.

Cependant, contrairement à alsacap, je n'imprime pas sur la sortie standard ce que je trouve, je remplis une grande structure utilisable depuis Python ! Pour cela j'ai donc écrit la fonction GetCards. Regardons ce qu'elle nous retourn :

In [1]: from pyalsacap import *
In [2]: insp = Inspector()
In [3]: cards = insp.GetCards()
In [4]: cards
Out[4]:
{
    0: {
        'id': 'Intel',
        'name': 'HDA Intel',
        'devices': {
            0: {
                'channels': [2, 2],
                'formats': ['S16_LE'],
                'id': 'AD198x Analog',
                'name': 'AD198x Analog',
                'rate': [8000, 192000],
                'subdevices_available': 1,
                'subdevices': {
                    0: {
                        'name': 'subdevice #0'
                    }
                }
            },
            1: {
                'channels': [2, 2],
                'formats': ['S16_LE'],
                'id': 'AD198x Digital',
                'name': 'AD198x Digital',
                'rate': [44100, 192000],
                'subdevices_available': 1,
                'subdevices': {
                    0: {
                        'name': 'subdevice #0'
                    }
                }
            }
        }
    },
    29: {
        'id': 'ThinkPadEC',
        'name': 'ThinkPad Console Audio Control',
        'devices': {
        }
    }
}

C'est beau !

J'ai ajouté une autre fonction nommée PrintCards qui reparcourt la structure et l'imprime sur la sortie standard. Regardons ce qu'elle nous écrit !

In [5]: desc = Descriptor()

In [6]: desc.PrintCards(cards)
*** Scanning for playback devices ***
Card 0, ID `Intel', name `HDA Intel'
  Device 0, ID `AD198x Analog', name `AD198x Analog', 1 subdevices (1 available
    2 channels, sampling rate 8000..192000 Hz
    Sample formats: S16_LE
      Subdevice 0, name `subdevice #0'
  Device 1, ID `AD198x Digital', name `AD198x Digital', 1 subdevices (1 available)
    2 channels, sampling rate 44100..192000 Hz
    Sample formats: S16_LE
      Subdevice 0, name `subdevice #0'
Card 29, ID `ThinkPadEC', name `ThinkPad Console Audio Control'

Cela nous convient très bien !

Certains demanderont « mais pourquoi deux classes ? ». En fait j'ai fait deux classes parce que je considère que la partie qui imprime n'est utile que lorsque PyAlsaCap est utilisé comme un script (en remplacement d'alsacap). Lorsque nous utilisons PyAlsaCap comme un module (ce pourquoi je l'ai écrit), nous n'avons pas besoin d'importer tout ce qui a trait à l'impression sur écran !

Pour le moment, toutes les fonctionnalité d'alsacap ne sont pas reproduites, nous ne pouvons pas interroger une carte précise, nous pouvons seulement lister toutes les cartes selon une fonction précise (lecture ou enregistrement). Le paramètre -R est donc pour le moment le seul paramètre que prend le script. Pour la fonction GetCards, l'argument équivalent est "capture".

En Python :

In [1]: from pyalsacap import *
In [2]: insp = Inspector()
In [3]: desc = Descriptor()
In [4]: desc.PrintCards(insp.GetCards("capture")
*** Scanning for recording devices ***
Card 0, ID `Intel', name `HDA Intel'
  Device 0, ID `AD198x Analog', name `AD198x Analog', 2 subdevices (2 available
    2 channels, sampling rate 8000..192000 Hz
    Sample formats: S16_LE
      Subdevice 0, name `subdevice #0'
      Subdevice 1, name `subdevice #0'
Card 29, ID `ThinkPadEC', name `ThinkPad Console Audio Control'

Depuis le shell :

$ ./pyalsacap.py -R
*** Scanning for recording devices ***
Card 0, ID `Intel', name `HDA Intel'
  Device 0, ID `AD198x Analog', name `AD198x Analog', 2 subdevices (2 available)
    2 channels, sampling rate 8000..192000 Hz
    Sample formats: S16_LE
      Subdevice 0, name `subdevice #0'
      Subdevice 1, name `subdevice #0'
Card 29, ID `ThinkPadEC', name `ThinkPad Console Audio Control'

À comparer avec la sortie d'alsacap :

$ ./alsacap -R
*** Scanning for recording devices ***
Card 0, ID `Intel', name `HDA Intel'
  Device 0, ID `AD198x Analog', name `AD198x Analog', 2 subdevices (2 available)
    2 channels, sampling rate 8000..192000 Hz
    Sample formats: S16_LE, S32_LE
      Subdevice 0, name `subdevice #0'
      Subdevice 1, name `subdevice #1'
Card 29, ID `ThinkPadEC', name `ThinkPad Console Audio Control'

Le code

Si cela vous intéresse, vous pouvez lire le source de PyAlsaCap à la date du 10 mars.

C'est ce que j'ai écrit pour occuper mon dimanche après-midi. Il est probable que le code évolue et que j'y ajoute quelques fonctions supplémentaires (et peut-être pourra-t-il complètement remplacer alsacap).

Il faudra aussi que j'apprenne à utiliser le module argparse afin de prendre en compte correctement les paramètres de la ligne de commande.

Mais déjà, si PyAlsaCap ne fait pas tout ce que fait alsacap, il fait quelque chose qu'alsacap ne permet pas : être utilisé comme module Python 3 ! Et c'est là son grand avantage : le code de PyAlsaCap n'utilise rien de spécifique à Python 3 par rapport à Python 2, mais il est utilisable dans un projet plus vaste en Python 3, et ce sans compilation.

  • PyAlsaCap est compatible Python 3
  • PyAlsaCap n'a pas d'autres dépendances que Python et libasound2.
  • Contrairement à d'autres modules (python-pyalsa, python-alsaaudio), PyAlsaCap est un module en python qui n'a pas besoin d'être compilé.
  • PyAlsaCap est donc utilisable dans toute distribution GNU/Linux qui fournit Python et libasound, dès aujourd'hui.
  • PyAlsaCap ne fait pas beaucoup de choses, mais c'est peut-être aussi un avantage ! PyAlsaCap liste une sélection de caractéristiques et capacités des cartes audio, et il le fait bien. :)

Conclusion

Python avec ctypes, c'est extrêmement puissant !

Bon, parfois, chercher par quel bout prendre la bibliothèque C pour obtenir ce que l'on souhaite est un peu prise de tête, surtout lorsque le code fait usage de macros !

Parfois la recherche de solution prend des airs de rétro-ingénierie, mais sur du code ouvert, c'est vraiment trop facile. :)

Python avec ctypes, c'est grisant…

Ça m'a rappelé les sensations que j'avais quand je codait en C étant enfant, mais cette fois-ci sans compilation ! Les pointeurs, c'est fun ! Les langages interprétés, c'est fun aussi ! On peut combiner les deux… :o

D'ailleurs je crois que c'est une bonne manière de décrire ctypes : on n'échappe pas au C, il faut y avoir été initié et maîtriser les concepts de bases, mais par contre on évite toute la compilation !

Surtout, surtout, ce qui est super sympa, c'est de pouvoir jouer avec des pointeurs avec un langage interprété, depuis un shell dudit langage… De quoi nous occuper les longues soirées d'hiver, même si l'hiver ne dure pas très longtemps sur la côte d'Azur ! :p

  • # Oui mais ...

    Posté par  . Évalué à 7.

    Très bon journal !!

    Cependant… Arrêtez avec les "t'il" par pitié pour notre belle langue !!
    Ça s'écrit comme ça: "t-il". Ex: "Que se passe-t-il ?"

    Un p'tit lien en sus: http://www.langue-fr.net/spip.php?article194

    Merci et bonjour chez vous !

    • [^] # Re: Oui mais ...

      Posté par  (site web personnel) . Évalué à 4. Dernière modification le 15 mars 2013 à 14:58.

      Ah oui en effet… en plus je suis certain d'en avoir corrigé au moins un parce que je connais mon défaut… mais ce n'est pas assez. ^^

      ce commentaire est sous licence cc by 4 et précédentes

      • [^] # Re: Oui mais ...

        Posté par  . Évalué à 1.

        journal très intéressant malgré une certaine longueur, jouer avec les pointeurs du C en Python c'est franchement osé ;).

        J'ai une question "après cette trop longue vacance" : cette syntaxe est-elle volontaire ?
        sinon elle pique les yeux.

        • [^] # Re: Oui mais ...

          Posté par  (site web personnel) . Évalué à 2.

          J'ai une question "après cette trop longue vacance" : cette syntaxe est-elle volontaire ?

          En fait c'était une allusion à la « vacance du siège pontifical », la panne de LinuxFR étant survenue en même temps que le conclave. C'était une manière fumeuse (hihi) de comparer l'attente du lectorat de LinuxFR avec celle de quelques (millions ? milliards ?) de croyants pour un nouveau pape.

          Dans ce cas précis, on parle bien d'une vacance au singulier, du latin vacare, être sans. Au pluriel, « les vacances » se réfèrent habituellement au repos, hors ce qu'a du vivre le lectorat pendant la panne n'est pas vraiment un repos, mais un manque.

          C'est que l'absence de ce site a du éprouver nombre de ses fidèles ! En tout cas moi j'ai trouvé cela interminable. :)

          J'avais trouvé l'allusion dans l'air du temps (j'en ai glissé d'autres)… mais elle n'est peut-être pas très accessible en effet.

          Pour ceux qui suivent l'actualité, on parle ces jours-ci de la prochaine installation du pape. Y a moyen de faire des blagues sur les gestionnaires de paquet… Dans un domaine proche, on parle de l' érection d'une cathédrale. Il y a bien que les cathos pour utiliser encore de tels mots connotés et qui surtout ne sont plus compris dans ces sens là. ;)

          journal très intéressant malgré une certaine longueur

          J'ai eu un peu peur moi-même de la longueur, j'aurai voulu le faire plus court, mais j'ai trouvé dans ce cas pratique une bonne occasion de présenter ctypes et de lister quelques pièges difficiles.

          Les tutos donnent toujours des cas généraux qu'on ne rencontre jamais dans la réalité. En l'occurrence ici les difficultés principales étaient de manipuler des pointeurs sur des objets que nous ne connaissons pas (!) et dont le type de n'est pas connu (je ne sais toujours pas ce qu'il y a au bout du pointeur *handle, par exemple), et de contourner l'usage de macros et autres usages du préprocesseur : la fausse fonction rencontrée dans le présent journal, les constantes…

          On voit bien la difficulté de dialoguer avec une bibliothèque C quand elle n'est prévue que pour être utilisée depuis un langage compilé. On prend conscience aussi que certaines manières de programmer n'ont pu naître que parce que le concept de compilation le permet. Certains concepts comme le préprocesseur, par exemple, entraînent une différenciation entre les symboles source et les symboles binaires. Je n'aimerai pas devoir travailler avec ctypes sur une bibliothèque dont la compilation source prévoir l'usage d'outils comme moc par exemple. Les langages qui compilent vers le C (comme Vala) posent le même problème.

          Suite à cette expérience, je sais que si je devais développer une bibliothèque en C, j'éviterai au maximum d'utiliser le préprocesseur si je prévois qu'elle puisse être utilisée directement telle quelle sans compilation.

          J'ai donc essayé de faire plus court… mais je n'ai pas réussi à raccourcir sans retirer des clés. ;)

          ce commentaire est sous licence cc by 4 et précédentes

          • [^] # Re: Oui mais ...

            Posté par  . Évalué à 2.

            Je me disais bien que cela ne pouvais qu'être volontaire, du coup même si je connaissais le mot latin j'ai ainsi appris que l'on pouvait le mettre dans cette forme.

            (…)dans un domaine proche, on parle de l' érection d'une cathédrale. Il y a bien que les cathos pour utiliser encore de tels mots connotés et qui surtout ne sont plus compris dans ces sens là. ;)

            d'accord, mais alors : que faut-il penser

            J'ai eu un peu peur moi-même de la longueur,

            mauvaise foi inside

  • # modif

    Posté par  . Évalué à 2. Dernière modification le 20 mars 2013 à 11:33.

    pour info, sous ub***tu, j'ai dû modifier le script pour que cela fonctionne (et le rendre compatible python2&3), cela est dû a une signature différente pour 2 des fonctions de la lib libasound.

    13d12
    < from __future__ import print_function
    155,156d153
    < NULL = c_void_p()
    < 
    214c211
    <                         self.libasound.snd_ctl_close(c_handle)
    ---
    >                         self.libasound.snd_ctl_close(handle)
    256,257c253,254
    <                                             self.libasound.snd_pcm_hw_params_get_rate_min(c_pars, byref(c_min), NULL)
    <                                             self.libasound.snd_pcm_hw_params_get_rate_max(c_pars, byref(c_max), NULL)
    ---
    >                                             self.libasound.snd_pcm_hw_params_get_rate_min(c_pars, byref(c_min))
    >                                             self.libasound.snd_pcm_hw_params_get_rate_max(c_pars, byref(c_max))
    340c337
    <                     print(str(channels[0]) + ".." + str(channels[1]), end='')
    ---
    >                     print(str(channels[0] + ".." + str(channels[1]), end=''))
    
    
    • [^] # Re: modif

      Posté par  (site web personnel) . Évalué à 2.

      C'est étonnant, ce code a été écrit sur ubuntu…

      je suppose que c'est cette partie-là la plus intéressante :

      256,257c253,254
      <                                             self.libasound.snd_pcm_hw_params_get_rate_min(c_pars, byref(c_min), NULL)
      <                                             self.libasound.snd_pcm_hw_params_get_rate_max(c_pars, byref(c_max), NULL)
      ---
      >                                             self.libasound.snd_pcm_hw_params_get_rate_min(c_pars, byref(c_min))
      >                                             self.libasound.snd_pcm_hw_params_get_rate_max(c_pars, byref(c_max))
      
      

      ces deux fonctions ne prennent désormais que deux arguments ? que se passe-t'il si je donne trois arguments à une fonction compilée avec deux ? Parce que chez moi ça marche. ©

      Je regarderai la doc de libasound et remonterai l'info à Volker Schatz pour qu'il corrige aussi son code s'il le faut.

      ce commentaire est sous licence cc by 4 et précédentes

Suivre le flux des commentaires

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