Etendre Csound

Ajouter des générateurs unitaires

Si les générateurs unitaires existants de Csound ne répondent pas à vos besoins, il est relativement aisé d'étendre Csound en écrivant de nouveaux générateurs unitaires en C ou en C++. Le traducteur, le chargeur et le moniteur d'exécution traiteront votre module comme n'importe quel autre module, pourvu que vous suiviez certaines conventions.

Historiquement, on réalisait ceci avec des générateurs unitaires intégrés, c'est-à-dire dont le code est lié statiquement avec le reste de l'exécutable de Csound.

Aujourd'hui, on préfère créer des générateurs unitaires sous forme de greffon. Ce sont des bibliothèques à liaison dynamique (DLL) sous Windows, et des modules chargeables (bibliothèques partagées chargées par dlopen) sous Linux. Csound recherche et charge ces greffons au moment de l'exécution, depuis le chemin défini dans OPCODEDIR. On peut aussi charger des opcodes de greffon depuis la ligne de commande avec l'option --opcode-lib.

L'avantage de cette méthode, naturellement, est que les greffons créés par n'importe quel développeur, n'importe quand, peuvent être utilisés avec des versions de Csound déjà existantes.

Créer un générateur unitaire intégré

Vous avez besoin d'une structure définissant les entrées, les sorties et l'espace de travail, plus du code d'initialisation et du code d'exécution. Mettons un exemple de tout cela dans deux nouveaux fichiers, newgen.h et newgen.c. Les exemples donnés sont pour Csound 5. Pour les versions antérieures, il faut omettre le premier paramètre (CSOUND *csound) dans toutes les fonctions d'opcode.

/* newgen.h  -  définit une structure */

/* Déclare les structures et les fonctions de Csound. */
#include "csoundCore.h"

typedef struct
{
  OPDS h;                                         /* en-tête requis */
  MYFLT *result, *istrt, *incr, *itime, *icontin; /* adr des arg de sortie et d'entrée */
  MYFLT curval, vincr;                            /* espace de données privé */
  long countdown;                                 /* ditto */
} RMP;


/* newgen.c -  code d'initialisation et d'exécution */
/* Déclare les structures et les fonctions de Csound. */
#include "csoundCore.h"
/* Déclare la structure RMP. */
#include "newgen.h"

int rampset (CSOUND *csound, RMP * p)     /* à l'initialisation de la note : */
{
  if (*p->icontin == FL(0.0))
    p->curval = *p->istrt;                /* reçoit si besoin la nouvelle valeur de début */
  p->vincr = *p->incr / csound->esr;      /* fixe l'increment au taux-s par sec. */
  p->countdown = *p->itime * csound->esr; /* compteur pour iduree en secondes */
  return OK;
}

int ramp (CSOUND *csound, RMP * p)        /* pendant l'exécution de la note : */
{
  MYFLT *rsltp = p->result;               /* initialise un pointeur sur le tableau de sortie */
  int nn = csound->ksmps;                 /* taille du tableau donnée par l'orchestre */
  do
    {
      *rsltp++ = p->curval;               /* copie la valeur courante vers la sortie */
      if (--p->countdown > 0)             /* pour les premières iduree secondes, */
        p->curval += p->vincr;            /* incrémenter la valeur */
    }
  while (--nn);
  return OK;
}

Maintenant nous ajoutons ce module à la table du traducteur dans entry1.c, sous le nom d'opcode rampt :

#include "newgen.h"

int rampset(CSOUND *, RMP *), ramp(CSOUND *, RMP *);

/*  opname  dsblksiz  thread    outypes   intypes      iopadr        kopadr      aopadr  */
{  "rampt",  S(RMP),    5,        "a",    "iiio",   (SUBR)rampset, (SUBR)NULL, (SUBR)ramp  },

Finalement, il faut relier Csound avec le nouveau module. Ajoutez le nom du fichier C à la liste libCsoundSources dans le fichier SConstruct :

libCsoundSources = Split('''
Engine/auxfd.c
...
OOps/newgen.c
...
Top/utility.c
''')

Lancez scons comme vous le feriez pour toute autre construction de Csound, et le nouveau module sera intégré dans votre Csound.

Les actions ci-dessus ont ajouté un nouveau générateur au langage Csound. C'est une fonction de rampe linéaire au taux audio qui modifie une valeur d'entrée selon une pente définie par l'utilisateur pour une durée donnée. Cette rampe peut éventuellement continuer depuis la dernière valeur de la note précédente. L'entrée correspondante du manuel de Csound ressemblerait à ceci :

ar rampt idebut, ipente, iduree [, icontin]

idebut -- valeur du début d'une rampe linéaire au taux audio. Eventuellement ignorée s'il y a un drapeau de continuité.

ipente -- pente de la rampe, exprimée comme le taux de changement des y par seconde.

iduree -- durée de la rampe en secondes, après laquelle la valeur est tenue jusqu'à la fin de la note.

icontin (facultatif) -- drapeau de continuité. S'il est à zéro, la rampe démarrera depuis l'entrée idebut. Sinon, la rampe démarrera depuis la dernière valeur de la note précédente. La valeur par défaut est zéro.

Le fichier newgen.h comprend une liste de paramètres de sortie et d'entrée définie sur une ligne. Ce sont les ports par lesquels le nouveau générateur communiquera avec les autres générateurs dans un instrument. La communication se fait par adresse, pas par valeur, et c'est une liste de pointeurs sur des valeurs de type MYFLT (double si la macro USE_DOUBLE est définie, et float autrement). Il n'y a aucune restriction sur les noms, mais les types d'argument d'entrée-sortie sont définis plus loin par des chaînes de caractères dans entry1.c (intypes, outypes). Les types intypes sont habituellement x, a, k, et i, suivant les conventions normales du manuel de Csound ; on trouve aussi o (facultatif, par défaut 0), p (facultatif, par défaut 1). Les types outypes comprennent a, k, i et s (asig ou ksig). Il est important que tous les noms d'argument de la liste se voient attribuer un type d'argument correspondant dans entry1.c. De plus, les arguments de type-i ne sont valides qu'à l'initialisation, et les arguments des autres types ne sont valables que pendant l'exécution. Les lignes suivantes de la structure RMP déclarent l'espace de travail nécessaire pour que le code soit réentrant. Ceci permet d'utiliser le module plusieurs fois dans plusieurs copies d'instrument tout en préservant toutes les données.

Le fichier newgen.c contient deux sous-programmes, appelés chacun avec un pointeur sur l'instance de Csound et un pointeur sur la structure RMP allouée de façon unique et ses données. Les sous-programmes peuvent être de trois sortes : initialisation de note, génération de signal au taux-k, génération de signal au taux-a. Normalement, un module requiert deux de ces sous-programmes : initialisation, et un sous-programme soit de taux-k, soit de taux-a qui sera inséré dans divers listes chaînées de tâches exécutables quand un instrument est activé. Les type de chaînage apparaissent dans entry1.c sous deux formes : noms isub, ksub et asub ; et un index de chaînage qui est la somme de isub=1, ksub=2, asub=4. Le code lui-même peut référencer (mais ça ne devrait être qu'en lecture) les membres publiques de la structure CSOUND définie dans csoundCore.h, dont les plus utiles sont :

        OPARMS  *oparms
        MYFLT   esr                 taux d'échantillonage défini par l'utilisateur
        MYFLT   ekr                 taux de contrôle défini par l'utilisateur
        int     ksmps               ksmps défini par l'utilisateur
        int     nchnls              nchnls défini par l'utilisateur
        int     oparms->odebug      option -v de la ligne de commande
        int     oparms->msglevel    option -m de la ligne de commande
        MYFLT   tpidsr              2 * PI / esr

Tables de fonction

pour accéder aux tables de fonction en mémoire, une aide spéciale est disponible. La nouvelle structure définie doit comprendre un pointeur

FUNC       *ftp;

initialisé par l'instruction

ftp = csound->FTFind(csound, p->ifuncno);

où MYFLT *ifuncno est un argument d'entrée de type-i contenant le numéro de la ftable. La table stockée est alors en ftp->ftable, et d'autres données comme sa longueur, les masques de phase, les convertisseurs cps-incrément, sont aussi accessibles depuis ce pointeur. Voir la structure FUNC dans csoundCore.h, le code de csoundFTFind() dans fgens.c, et le code de oscset() et de koscil() dans OOps/ugens2.c.

Espace Supplémentaire

Parfois les besoins en espace d'un module sont trop grands pour faire partie d'une structure (limite supérieure de 65279 octets, due au paramètre en entier court non-signé dsblksiz et aux codes réservés >= 0xFF00), ou ils dépendent d'une valeur d'argument-i qui n'est pas connue avant l'initialisation. De l'espace supplémentaire peut être alloué dynamiquement et géré proprement en incluant la ligne

AUXCH      auxch;

dans la structure définié (*p), puis en utilisant ce type de code dans le module d'initialisation :

csound->AuxAlloc(csound, npoints * sizeof(MYFLT), &p->auxch);

L'adresse de l'espace auxiliaire est gardée dans une chaîne d'espaces similaires appartenant à cet intrument, et elle est gérée automatiquement lorsque l'instrument est dupliqué ou passé au ramasse-miettes durant l'exécution. L'assignation

void *auxp = p->auxch.auxp;

trouvera les espaces alloués pour une utilisation pendant l'initialisation et pendant l'exécution. Voir la structure LINSEG dans ugens1.h et le code de lsgset() and klnseg() dans OOps/ugens1.c.

Partage de Fichier

Lorsque l'on accède souvent à un fichier externe, ou si on le fait depuis plusieurs endroits, il est souvent efficace de lire le fichier entier dans la mémoire. On accomplit ceci en incluant la ligne

MEMFIL     *mfp;

dans la structure définie (*p), puis en utilisant le style de code suivant dans le module d'initialisation :

p->mfp = csound->ldmemfile(csound, nomfic);

où char *nomfic est une chaîne contenant le nom du fichier requis. Les données lues se trouveront entre

(char *)p->mfp->beginp;  et  (char *)p->mfp->endp;

Les fichiers chargés n'appartiennent pas à un instrument particulier, mais sont automatiquement partagés pour des accès multiples. Voir la structure ADSYN dans ugens3.h et le code de adset() et de adsyn() dans OOps/ugens3.c.

Arguments chaîne de caractères

Pour permettre un argument d'entrée de type chaîne (disons MYFLT *inomfic) dans votre structure définie (*p), assignez-lui le type d'argument S dans entry1.c, et incluez le code suivant dans le module d'initialisation :

strcpy(nomfic, (char*)p->inomfic);

Voir le code pour adset() dans OOps/ugens3.c, lprdset() dans OOps/ugens5.c, et pvset() dans OOps/ugens8.c.

Ajouter un générateur unitaire comme greffon

La procédure pour créer un généteur unitaire comme greffon ressemble beaucoup à celle qui est utilisée pour créer un générateur intégré. Le code du générateur unitaire sera le même à part les différences suivantes.

En supposant à nouveau que votre générateur s'appelle newgen, effectuez les étapes suivantes :

  1. Ecrivez vos fichiers newgen.c et newgen.h comme vous le feriez pour un générateur unitaire intégré. Mettez ces fichiers dans le répertoire csound5/Opcodes.

  2. Mettez #include "csdl.h" dans les sources de votre générateur unitaire, au lieu de #include "csoundCore.h".

  3. Ajoutez vos champs OENTRY et les fonctions d'enregistrement du générateur unitaire au bas de votre fichier C. Exemple (mais vous pouvez avoir autant de générateurs unitaires que vous le voulez dans un greffon) :

    #define S sizeof
    
    static OENTRY localops[] = {
        { "rampt", S(RMP), 5, "a", "iiio", (SUBR)rampset, (SUBR)NULL, (SUBR)ramp  },
    };
    
    /*
     * La macro suivante de csdl.h définit
     * la fonction d'enregistrement d'opcode "csound_opcode_init()"
     * pour la table des opcodes locaux.
     */
    LINKAGE
  4. Ajoutez votre greffon comme nouvelle cible dans la section des opcodes en greffon du fichier de construction SConstruct :

    greffonEnvironment.SharedLibrary('newgen',
        Split('''Opcodes/newgen.c
        Opcodes/un_autre_fichier_utilise_par_newgen.c
        Opcodes/encore_un_autre_fichier_utilise_par_newgen.c'''))
  5. Lancer la construction de Csound de la manière usuelle.

Référence de OENTRY

La structure OENTRY (voir H/csoundCore.h, Engine/entry1.c, et Engine/rdorch.c) contient les champs publiques suivants :

opname, dsblksiz, thread, outypes, intypes, iopadr, kopadr, aopadr
dsblksiz

Il y a deux types d'opcode, polymorphe et non-polymorphe. Pour les opcodes non-polymorphes, le drapeau dsblksiz spécifie la taille de la structure de l'opcode en octets, et les arguments sont toujours passés à l'opcode au même taux. Les opcodes polymorphes peuvent accepter des arguments à des taux différents, et la façon dont ces arguments sont réellement distribués aux autres opcodes est déterminée par le drapeau dsblksiz et les conventions de nommage suivantes (note : la liste suivante est incomplète, voir Engine/entry1.c pour tous les codes spéciaux possibles pour dsblksiz) :

0xffff

Le type du premier argument en sortie détermine quelle fonction de générateur unitaire est réellement appelée : XXX -> XXX.a, XXX.i, ou XXX.k.

0xfffe

Les types des deux premiers arguments en entrée déterminent quelle fonction de générateur unitaire est réellement appelée : XXX -> XXX.aa, XXX.ak, XXX.ka, ou XXX.kk, comme dans le générateur unitaire oscil.

0xfffd

Fait référence à un argument en entrée de type a ou k, comme dans le générateur unitaire peak.

thread

Spécifie le(s) taux utilisé(s) pour appeler les fonctions de générateur unitaire, comme suit :

Tableau 22. Taux d'appel des ugens selon le paramètre thread

0 taux-i ou taux-k (sortie B seulement)
1 taux-i
2 taux-k
3 taux-i et taux-k
4 taux-a
5 taux-i et taux-a
7 taux-i et (taux-k ou taux-a)


outypes

Liste les valeurs de retour des fonctions de générateur unitaire, s'il y en a. Les types permis sont (note : la liste suivante est incomplète, voir Engine/entry1.c pour tous les types possibles en sortie) :

Tableau 23. Liste des types de sortie des ugens

i scalaire de taux-i
k scalaire de taux-k
a vecteur de taux-a
x scalaire de taux-k ou vecteur de taux-a
f type fsig de flux pvoc de taux-f
m arguments multiples en sortie de taux-a


intypes

Liste les arguments, s'il y en a, que prennent les fonctions de générateur unitaire. Les types permis sont (note : la liste suivante est incomplète, voir Engine/entry1.c pour tous les types possibles en entrée) :

Tableau 24. Liste des types d'entrée des ugens

i scalaire de taux-i
k scalaire de taux-k
a vecteur de taux-a
x scalaire de taux-a ou vecteur de taux-a
f type fsig de flux pvoc de taux-f
S Chaîne
B  
l  
m Commence une liste indéfinie d'arguments de taux-i (n'importe quel nombre)
M Commence une liste indéfinie d'arguments (n'importe quel taux, n'importe quel nombre)
N Commence une liste indéfinie d'arguments facultatifs (aux taux-a, -k, -i, ou -S) (n'importe quel nombre impair)
n Commence une liste indéfinie d'arguments au taux-i (n'importe quel nombre impair)
O facultatif au taux-k, 0 par défaut
o facultatif au taux-i, 0 par défaut
p facultatif au taux-i, 1 par défaut
q facultatif au taux-i, 10 par défaut
V facultatif au taux-k, 0.5 par défaut
v facultatif au taux-i, 0.5 par défaut
j facultatif au taux-i,-1 par défaut
h facultatif au taux-i, 127 par défaut
y Commence une liste indéfinie d'arguments au taux-a (n'importe quel nombre)
z Commence une liste indéfinie d'arguments au taux-k (n'importe quel nombre)
Z Commence une liste indéfinie d'argumenents alternant les taux-k et -a (kaka...) (n'importe quel nombre)


iopadr

L'adresse de la fonction du générateur unitaire (de type int (*SUBR)(CSOUND *, void *)) qui est appelée à l'initialisation, ou NULL s'il n'y a pas de fonction.

kopadr

L'adresse de la fonction du générateur unitaire (de type int (*SUBR)(CSOUND *, void *)) qui est appelée au taux-k, ou NULL s'il n'y a pas de fonction.

aopadr

L'adresse de la fonction du générateur unitaire (de type int (*SUBR)(CSOUND *, void *)) qui est appelée au taux-a, ou NULL s'il n'y a pas de fonction.