SMG : Procédural Musical Generator



SMG : Procédural Musical Generator


By Erixoff 2022

General




Traitement analyse






On invoque l'interpreteur sur des fichiers texte ou directement en ligne.
Le texte est analysé, et transformé en bytecode. Les noms d'objets sont « transformés » (associés) en Id uniques.
Typiquement on donne un fichier texte, qui, une fois analysé, enrichira l'espace d'exécution (sandbox ?) avec les variables, constantes, fonctions et objets (base, et instances).
Le bytecode en instructions/données prêtes à être exécutées, idéalement, on pourrait structurer le bytecode de façon à pouvoir être écrit dans un fichier brut avec peu de traitements.
Par exemple :


SMG langage:


Description du langage:


Utiliser un langage simple (texte) type assembleur pour décrire les actions, conditions et en général, transformé en bytecode (brut) pour l'exécution.
Opcode + opérandes
Avantage : simple à implémenter et permet de tout décrire assez simplement par le truchement de registres et autres
On décrit les actions entrée/cycle/sortie
On décrit les conditions avec calcul d'une valeur de retour booléenne
SMG langage -> compilation -> bytecode -> execution par la machine virtuelle
Le langage texte en clair est « proche » du bytecode.

Exemple langage SMG Mnémonique/Opcode:

Start stop pattern #x // demarre le pattern à la prochaine mesure
Load register A with X
Get register
Load Into B
Add A,B
Etc
Penser aux constantes, chaînes et fonctions macro

On peut aussi décrire carrément la FSM d'une layer ou piste avec le langage et des define pour les états et transitions.
Define state entry action, state24, [from state44]
Xxx
End define
Define transition, state1, state4
If A>X, 22 // comparaison, résultat dans B
// valeur de retour est B
End define

Fichier:
Élaboration : Le fichier sera lu au démarrage et analysé pour créer les FSM (listes de transitions/actions) et variables et constantes (parsing des DEF (déclarations statiques)
Exécution : Puis le code main est exécuté; les objets créées et les FSM sont déroulées. Le code interprété métier associées aux actions (et condition de transition) est alors exécuté interprété au fil de l'eau, selon les états.

Exemple de fichier:
PMG_VERSION;1.2 // ESA: utile pour la compatibilité !
// définitions statiques
// ESA : Chaque objet dans PMG a une FSM à sa disposition, ici on va s'en servir, notamment.
DEF OBJECT; Object_Album
DEF STATE; State1_Intro
DEF ENTRYACTION
START_FSM; Object_song; calm // démarrage fsm song avec 1 paramètre pour indiquer la « couleur »
WAIT_END_FSM; Object_Song
END DEF
DEF TRANSITION; State1_intro;State2
Xxx
SET;transition_condition_value; true
END DEF
END DEF
DEF STATE; state2_Song
Xxx
END DEF
DEF STATE; state3_Intermede
Xxx
END DEF
SET_INIT; State1_intro

DEF FSM Object_Song:
DEF state; STATE1
Def entry action
START_FSM; Object_Section; sunny
END DEF
END DEF

// Dynamique (exécution)
// main
NEW; Object_Album
NEW; Object_Song
NEW; Object_Section
NEW; etc
START_FSM; Object_Album

Comment stocker le bytecode:
Le langage sous forme bytecode exécutable :
  • OpCode + Nb fixe d'octets (et pour les chaînes de caractères, on utilise un sorte de pointeur)
  • OpCode + Nb illimité d'octets ?

Finalement, on peut définir un pattern soit en dur (avec une liste d'événements note) soit avec du code.
Write pattern, pat13, eventNote, time1, vel4, duration4

Contextes d'éxécution:
Il faut un contexte global contenant:
  • les objets (déclarés et définis/instantiés)
  • constantes globales
  • Les registres généraux
  • Le numero d'instruction

Typage :
Faut-il typer les registres ? TODO ESA

Guidelines:


Complexité:

Il faut limiter la compléxité générale pour éviter un trop long, lourd, complexe et buggy développement :
  • Pas de variables, on utilise uniquement des registres


Attributs/Propriétés:

Tout ce qui est uniquement interne à PMG, c.a.d. non visible de l'extérieur, on le déclare en champ fixe dans la structure interne C, car il n'a pas vocation à être être dynamique et etendable.
Tout le reste est en attribut modulable (liste de d'attribut)

Objet


Générique

Tout dans PMG est objet.
Une chanson, un pattern, … une note, etc. C'est la même structure C de base.
Décrire les patterns, songs, pistes, albums avec une liste d'évènements, enfin surtout les objets contenant des items musicaux comme les patterns / pistes. Car c'est ce qu'on envoie au constructeur pour générer le buffer de notes et CC en sortie
Un événement est daté (ou conditionné ?…)
Notion générique d'objet :
Un objet est défini par :
  • Des attributs (liste de couples label/valeur, tous ne sont pas obligatoirement définis) :
    • Transpose
    • Timbre
    • Canal MIDI
    • Volume
    • Durée
  • Des propriétés
  • Des données métier (= du contenu) = 1 ou plusieurs listes de données (événements, ou autre)
  • Du code (pourquoi pas plusieurs sections, on pourrait avoir du code pour la construction, pour l'exécution nominale, l'arrêt …?)
En fait, un pattern, c'est par commodité un ensemble de notes que l'on peut éventuellement répéter, mais c'est une vue de l'esprit : on nomme cet objet ainsi par commodité, il pourrait contenir d'autres types d'événements ou code et s'appellerait par exemple song.

On peut ainsi aussi définir des choses simples :
  • une liste de patterns qui sont lues en séquence linéaire toute simple : état 1, action Start Pattern X, transition vers état 2 = nb mesures 4, entrée état 2 -> action start pattern Y
  • Des patterns lus en parallèle (comme des pistes) : un état puis 3 états en parallèle, chacun lançant un pattern et pouvant synchroniser leur fin sur une fin commune ou bien séparée
Structure objet:


Structure attribute d'un objet:


Structure des patterns :
Liste d'actions musicales : note on,off; CC;
Datation : en relatif (mesures ou temps) par rapport au début du pattern (pas bcp d'intérêt en absolu)

Définition/Création/instanciation des objets:

A la définition de l'objet, on crée une version statique de la structure interne Object en remplissant avec le contenu de la définition
A la création / instanciation, on créé une nouvelle structure (alloc) par duplication de celle de base. Dans la structure, on a un espace de contexte d'exécution pour la FSM machine virtuelle

Langage SMG vs liste d'evenements musicaux:
Je fais une distinction entre code, et événements (notes, CC, etc) datés. Est-ce qu'il y a lieu de faire ? Ou bien 1 event = 1 objet
On pourrait aussi tout décomposer en objet : par exemple, le code associé à un objet A pourrait être un objet A1, utile ?

Edition non destructive:

Un pattern est un objet dédié au stockage de notes.
Il peut avoir les propriétés suivantes :
  • Transpose
  • Repeatable
  • Etc
C'est bien de pouvoir attribuer une transposition à un pattern, mais plutôt que de le faire en dur, en prenant la propriété Transpose et le transposer par le logiciel, c'est plus puissant d'appliquer un code (associés à l'objet) avant chaque exécution du pattern. Ce code prend en compte toutes les propriétés qui l'intéressent et modifie le contenu de sa liste d'événements (notes, CC, etc). C'est comme les photos sur iPhone. On a ainsi le pattern original brut, et sa version customisée; et les 2 cohabitent.

Les FSM sont génériques. On instante une FSM et on s'en sert typiquement pour l'album, une autre pour les chansons, etc.
Chaque FSM gère une couche. La FSM Album démarre une FSM Song à chaque chanson.
Use case : A FINIR !!! Décrire un exemple pour gérer plusieurs pistes en parallèle


Génération du flux de sortie


Mixage:

Lorsque l'on demande le démarrage d'un pattern (quand l'instruction START_PATTERN est exécuté de manière générale), l'ordonnanceur place la réf du pattern dans une liste. Il y a le pattern en cours de lecture/execution, et le(s) prochains pattern(s), sachant que ce sont plutôt des listes d'events, et qu'une liste à 1 dimension suffit pour gérer le parallélisme, car ce sont des listes d'événements datés. Il faut donc lire les patterns afin de construire le « pattern brut » de sortie, c.a.d. une sortie de pattern buffer.

Mixer tous les patterns en cours d'exécution vers un buffer pour envoyer au séquenceur midi.

C'est au moment du mixage qu'on fait l'Execute de l'objet Pattern afin d'appliquer les altérations comme transpose, swing ou tout autre chose.

L'avantage de la généricité des objets est qu'on peut définir un pattern de batterie, un pattern de basse, un pattern de mélodie avec le même type d'objet, et il suffit de les jouer ensemble (en parallèle) pour qu'ils alimentent le buffet de sortie, une fois mixé.


Gestion des timbres

Il faut pouvoir associer un timbre avec une référence (un nom par exemple) à une façon de le sélectionner par le périphérique de sortie. En gros, en interne, à un nom de timbre, on associe plusieurs attributs (par exemple : type de générateur, chemin, Bank, program Number)
On pourrait même y associer des attributs PMG comme une probabilité.


Précision / quantification


Pour certaines musiques (amblent, par exemple), on pourrait imaginer que la précision temporelle n'est pas primordiale, et on pourrait donc laisser la possibilité de déclencher des notes (ou plus généralement des événements) depuis le langage, plutôt que de passer par un pattern dans lequel on écrit les events puis qu'on demande de lire (en décalé par bufferisation). Ça a l'avantage de simplifier l'écriture du programme.


Notes


User friendly & Powerful :


Il est important que le logiciel puisse être simple à utiliser au départ. Simple à utiliser pour faire des choses simples (comme une liste de patterns enchainée séquenciellement). Cela afin de ne pas rebuter les potentiels utilisateurs. Si c'est trop lourd des de le départ, ça ne donne pas envie d'y passer bcp de temps.
J'ai souvent passé du temps sur des logiciels pour rien: trop compliqués, ou bien pas assez puissant.
Il faut pouvoir faire des choses « par défaut » avec peu de difficulté, si c'est simple dans le principe, c'est mieux pour l'utilisation et le codage/maintenance.



[ Éric - mobile ]

Comments