← 3.17 Final drum machine code - standards compliant version | 4.1 Algorithmic music learning outcomes → |
note : bugue si mis en pause je crois (va vouloir rattraper toutes les notes non jouées entre le moment mis en pause et la reprise)
exercice.html
<!DOCTYPE html> <html> <head> <meta charset="utf-8"> <title>exercice : drum machine</title> </head> <body> <canvas id="impulsions" nx="button" label="impulsions"></canvas> <canvas id="matrix" nx="matrix" label="séquenceur" width="500" height="500"></canvas> <script src="nexusUI.js" charset="utf-8"></script> <script src="Samples_set.js" charset="utf-8"></script> <script src="Sequenceur.js" charset="utf-8"></script> <script src="impulseur.js" charset="utf-8"></script> <script src="main.js" charset="utf-8"></script> </body> </html>
Samples_set.js
function Samples_set ( contexte ) { /* Set de samples permettant de charger simplement des samples et de les jouer */ this.contexte = contexte; this.samples_set = {}; } Samples_set.prototype.charger = function ( nom, url ) { /* Charge le sample et l'ajoute au set String, String -> Void */ // on créé une référence à this (notre occurence de Sample_set) var self = this; // on créé l'objet requête var requete = new XMLHttpRequest(); // que l'on paramètre : // 1 : requête GET, url de la requête, requête asynchrone : true requete.open('GET', url, true); // 2 : type de réponse requete.responseType = 'arraybuffer'; // 3 : écouteur onload et fonction à exécuter alors requete.onload = function () { // les données audio var donnees_audio = requete.response; // sont passées pour décodage au contexte audio contexte.decodeAudioData( donnees_audio, function( buffer ) { // on ajoute le buffer à notre objet instruments self.samples_set[nom] = buffer; }); }; // on envoie la requête requete.send(); }; Samples_set.prototype.jouer = function ( nom, instant ) { /* Joue le sample à un instant t s'il est trouvé String, Number -> Void */ // si le sample existe, a été chargé if ( this.samples_set[nom] ) { try { //on créé un BufferSource var player = contexte.createBufferSource(); // on y charge notre sample player.buffer = this.samples_set[nom]; // on spécifie qu'on ne le joue pas en boucle player.loop = false; // on le connecte au contexte player.connect(this.contexte.destination); // on le lancera à l'instant t player.start( instant ); // en cas d'erreur } catch (e) { console.error('erreur : Samples_set.jouer :', e); } } };
Sequenceur.js
function Sequenceur ( contexte, base_rythmique, taille, marge ) { /* on renseigne une base rythmique en secondes, une intervalle en secondes pour le rafraichissement, et une taille. Number, Object, Number, Number, Number, Number */ /* les paramètres */ // le contexte audio this.contexte = contexte; // l'élément matrix d'ui this.matrix; // la base rythmique, une étape toutes les n secondes this.base_rythmique = base_rythmique; // le nombre d'étapes this.taille = taille; // la marge qu'on se donne pour le traitement this.marge = marge; /* l'objet séquenceur */ // l'objet séquenceur this.sequenceur = {}; // les séquences this.sequenceur.sequences = []; // les sets d'instruments this.sequenceur.sets = []; /* variables de travail on travaillera en complément de manière local avec les variables locales t : le timecode actuel à l'éxécution t_max_a_traite : limite temporelle */ // compteur tops this.compteur_tops = 0; // le précédent top traité this.timecode_top_precedent; // le prochain top à traiter this.timecode_top_suivant = 0; this.tmp; } Sequenceur.prototype.ajouter_sequence = function ( samples_set, nom ) { /* Permet d'ajouter une ligne au séquenceur, liée à une fonction jouant ledit instrument Array, Function -> Void */ // on créé une nouvelle séquence var sequence = []; // que l'on dimensionne et initialise for (var i = 0; i < this.taille; i++) { sequence.push(0); } // on l'ajoute au séquenceur this.sequenceur.sequences.push( sequence ); // ainsi que la référence à l'instrument this.sequenceur.sets.push( { samples_set: samples_set, nom: nom} ); if ( matrix ) { matrix.row = this.row(); } }; Sequenceur.prototype.impulsion = function ( frequence ) { /* Lorsque le séquenceur reçoit une impulsion, il regarde le temps actuel, la limite jusqu'où il va traiter le temps, et tant qu'il trouve des timecode dans cette fenêtre à traiter, il les traite La limite se calcule à partir du paramètre en entrée (le nombre de ms entre les impulsions en entrée) et la marge prévue pour éviter les problèmes dus à setInterval. À noter : on pourrait avoir des problèmes en cas de réduction de la fréquence Number -> Void */ var t = this.contexte.currentTime; var t_max_a_traiter = t + ( frequence * ( 1 + this.marge ) ); // on met à jour l'ui si applicable if ( matrix !== undefined ) { this.matrix.jumpToCol( (this.compteur_tops - 1) % this.taille ); } else { console.log('matrix not defined');} // tant que le prochain timecode est dans l'intervalle while ( t_max_a_traiter >= this.timecode_top_suivant ) { this.traiter_etape(); } }; Sequenceur.prototype.traiter_etape = function () { /* on traite une étape : on prend le prochain timecode, et pour chaque séquence, on regarde s'il y a a quelque chose à déclarer (et si c'est le cas on déclare), puis on met à jour les timecode précédent/suivant ainsi que l'étape */ // pour tous les sets, // on détermine l'étape vis à vis de la taille du contrôleur console.log('traiter_etape()'); var etape = this.compteur_tops % this.taille; var t = this.timecode_top_suivant; // on traite les différents sets for (var i = 0; i < this.sequenceur.sequences.length; i++) { if ( this.sequenceur.sequences[ i ][ etape % this.taille ] ) { this.sequenceur.sets[ i ].samples_set.jouer( this.sequenceur.sets[ i ].nom, t ); console.log('jouer', this.sequenceur.sets[ i ].nom, t); } } // on met à jour les timecode this.timecode_top_precedent = this.timecode_top_suivant; this.timecode_top_suivant += this.base_rythmique; // on met à jour le compteur de tops this.compteur_tops++; }; Sequenceur.prototype.entree = function ( data ) { this.sequenceur.sequences[ data.row ][ data.col ] = data.level; }; Sequenceur.prototype.matrix = function ( matrix ) { /* configure un élément matrix comme ui */ this.matrix = matrix; }; Sequenceur.prototype.col = function () { /* Renvoie le nombre de colonnes du séquenceur */ return this.taille; }; Sequenceur.prototype.row = function () { /* Renvoie le nombre de rangs du séquenceur */ return this.sequenceur.sequences.length; }; Sequenceur.prototype.dessiner = function () { var output };
impulseur.js
var impulseur = (function(){ var composant = {}; var liste_sequenceurs = []; var etape = 0; var duree_intervalle = 100; // millisecondes var intervalle; var actif = false; composant.ajouter_sequenceur = function ( sequenceur ) { /* */ liste_sequenceurs.push( sequenceur ); } function impulsion () { /* envoie */ console.log( 'impulsion envoyée' ); for (var i = 0; i < liste_sequenceurs.length; i++) { liste_sequenceurs[i].impulsion( duree_intervalle / 1000 ); } } composant.duree_intervalle = function ( _duree_intervalle ) { duree_intervalle = _duree_intervalle * 1000; } composant.lancer = function () { /* lance les impulsions */ actif = true; intervalle = window.setInterval( impulsion, duree_intervalle ); } composant.stopper = function () { /* stoppe les impulsions */ actif = false; window.clearInterval ( intervalle ); } composant.actif = function () { return actif; } return composant; })();
main.js
var contexte_audio = window.AudioContext || window.webkitAudioContext; var contexte = new contexte_audio(); /* sample set */ var batterie = new Samples_set( contexte ); batterie.charger( 'HT0D0', 'HT0D0.wav'); batterie.charger( 'HT0D3', 'HT0D3.wav'); batterie.charger( 'HT0D7', 'HT0D7.wav'); batterie.charger( 'HT0DA', 'HT0DA.wav'); batterie.charger( 'HT3D0', 'HT3D0.wav'); batterie.charger( 'HT3D3', 'HT3D3.wav'); batterie.charger( 'HT3D7', 'HT3D7.wav'); batterie.charger( 'HT3DA', 'HT3DA.wav'); batterie.charger( 'HT7D0', 'HT7D0.wav'); batterie.charger( 'HT7D3', 'HT7D3.wav'); batterie.charger( 'HT7D7', 'HT7D7.wav'); batterie.charger( 'HT7DA', 'HT7DA.wav'); batterie.charger( 'HTAD0', 'HTAD0.wav'); batterie.charger( 'HTAD3', 'HTAD3.wav'); batterie.charger( 'HTAD7', 'HTAD7.wav'); batterie.charger( 'HTADA', 'HTADA.wav'); /* */ var intervalle_duree = 0.1; var base_rythmique = .25; var marge = 0.5; var sequenceur = new Sequenceur ( contexte, base_rythmique, 8, marge ); impulseur.ajouter_sequenceur( sequenceur ); impulseur.duree_intervalle( intervalle_duree ); sequenceur.ajouter_sequence( batterie, 'HT0D0'); sequenceur.ajouter_sequence( batterie, 'HT0D3'); sequenceur.ajouter_sequence( batterie, 'HT0D7'); sequenceur.ajouter_sequence( batterie, 'HT0DA'); sequenceur.ajouter_sequence( batterie, 'HT3D0'); sequenceur.ajouter_sequence( batterie, 'HT3D3'); sequenceur.ajouter_sequence( batterie, 'HT3D7'); sequenceur.ajouter_sequence( batterie, 'HT3DA'); sequenceur.ajouter_sequence( batterie, 'HT7D0'); sequenceur.ajouter_sequence( batterie, 'HT7D3'); sequenceur.ajouter_sequence( batterie, 'HT7D7'); sequenceur.ajouter_sequence( batterie, 'HT7DA'); sequenceur.ajouter_sequence( batterie, 'HTAD0'); sequenceur.ajouter_sequence( batterie, 'HTAD3'); sequenceur.ajouter_sequence( batterie, 'HTAD7'); sequenceur.ajouter_sequence( batterie, 'HTADA'); var matrix, impulsions; nx.onload = function () { matrix.row = sequenceur.row(); matrix.col = sequenceur.col(); matrix.init(); sequenceur.matrix( matrix ); impulsions.on('*', function ( data ) { if ( data.press == 1) { if ( impulseur.actif() ) { impulseur.stopper(); } else { impulseur.lancer(); } } }); matrix.on('*', function ( data ) { // comme ligne 122 on utilise jumpToCol, cela fire un event // on ne veut toutefois que la ligne suivante ne s'exécute que si on est dans le cas d'un événement qui refile data.row, data.col, data.level, on fait un test if ( data.row !== undefined ) { //console.log(data); sequenceur.entree(data); // sequenceur[data.row][data.col] = data.level; } }); };