miércoles, 15 de abril de 2020

Javascript aplicado (2). Aplicación de tutorial de música sin instalaciones ni http server!

0. Introducción

Todo lo que se anuncia tiene sus limitaciones. Y este post las tiene... y muchas.

En principio, sin web server, no sé como acceder a guardar ficheros, o accede a una BD con simple html y javascript....

En fin esta simple aplicación es para entrenarte en reconocer intervalos musicales. Se han copiado muchas cosas de otras fuentes. Pero al final ha valido la pena, pues se copian los ficheros en una carpeta y se abre el fichero "index.html" en el navegador y a funcionar. No requiere instalaciones previas. Y se pueden copiar las dependecias de "cdn" a dicha carpeta y no haría falta conexión a internet.

Debo dar las gracias a Philip Shim tal como hice en el anterior post.

He perdido mucho tiempo con el dichoso Tone.js, pues mi conocimiento de javascript es bastante pobre, y el tema de la "asincronicidad", si no caes al principio te vuelves loco.

Queremos hacer un entrenador de reconocimiento de intervalos, para ello se genera un sonido de intervalo al azar que hay que adivinar. Se dispone de un selector de los intervalos a probar, y unas ayudas de repetición del audio del intervalo o refrescar la escucha de intervalos ejemplos y se aporta también el piano de nuestro querido Philip Shim.

Como la cosa se hacia larga, se ha partido el código en varios ficheros:
  1. "index.html" que es el fichero de partida que hace referencia a los scrips.
  2. "style.js" para dar estilo a la hoja.
  3. "script.js" que és fichero que contiene el código principal de la carga del programa y eventos.
  4. "general-utils" funcionalidades básicas como manejo de arrays y tiempos de espera
  5. "interval-utils.js" que contiene constantes relativas a los intervalos.
  6. "component-utils" funcionalidades para crear componentes mediante javascript
  7. "music-utils-edu" con llamnadas a "tone.js" para tocar notas e intervalos. Hace alguna referencia no usada a Web Audio (sobre la que se base tone.js)
Veamos la pantalla que se quiera implementar




Comencemos con los ficheros:

1. index.html 

Solo destacar que :

  • las líneas de botones y componentes que reaccionan con el usuario se han hecho con la etiqueta de listas no ordenadas "<ul>" (de de Philip Shim) a la que se le añaden los controles mediante javascript en tiempo de carga del formulario. La línea 14 contiene la actuación del evento "onload", al que le decimos que rellene el formulario y seleccionamos las ocvtavas entre 3 y 5.
  • Se ha añadido una parte de ayuda que es modal.A partir de la línea 58
  • Hay un botón para marcar/desmarcar los intervalos seccionados línea 20
  • En la líne 22 se cargarán los intrvalos a seleccionar el test
  • en la 24 para ver si son ascdendentes o descendentes y la selección de duración de las notas, volumen, y el rango de octavas.
  • Las líneas 37 y 28 contienen botones para oir los intervalos típicos para refrescar la memoria.
  • La línea 32 aqarece el piano de Philip Shim.
  • De las líneas 34 a 41 aparecen los marcadares de intentos, aciertos y fallos
  • La línea 43 tiene la selección de respuesta y botones de aceptar, nuevo intento y repetir intervalo. 



  1
  2
  3
  4
  5
  6
  7
  8
  9
 10
 11
 12
 13
 14
 15
 16
 17
 18
 19
 20
 21
 22
 23
 24
 25
 26
 27
 28
 29
 30
 31
 32
 33
 34
 35
 36
 37
 38
 39
 40
 41
 42
 43
 44
 45
 46
 47
 48
 49
 50
 51
 52
 53
 54
 55
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
<!DOCTYPE html>
<html lang="en" >
 <head>
  <!--<meta charset="UTF-8">-->
  <meta charset="UTF-8">
    <!--
  <meta charset="UTF-8" http-equiv="Content-Security-Policy" content="default-src *; style-src 'self' 'unsafe-inline'; script-src 'self' 'unsafe-inline' 'unsafe-eval' http://www.google.com">
 -->
  <title>Aprende a reconocer intervalos</title>
  <link rel="stylesheet" href="./style.css">

 </head>

 <body onload="fillAll(3,5);">

  <p class="centered">
   <!-- Trigger/Open The Modal -->
   <button class="simple-btn simple-btn-edu" id="myHelp">Ayuda!</button>
   <label  class="titol titol-cabecera" id="tttselec">Selecciona intervalos del test.</label>
   <button class="simple-btn" onclick="marcarTodo()">(Marca/Desmarca) Todo</button>
  </p>
  <ul id ="selection"></ul>

  <ul id ="selectionasc"></ul>

  <h2> Escucha los intervalos si no te acuerdas. </h2>
  <ul id ="buttonsasc"></ul>
  <ul id ="buttonsdes"></ul>


  <h2> Un piano.. siempre va bien!</h2>
  <ul id ="pianoedu"></ul>

  <p>

   <label class="titol" id="tselec">Selecciona bien la respuesta!:</label>
   <label class="titol titol-marcador" id="marcadores">Marcadores:</label>
   <input type="text"  class="marc"               id="intentos" disabled="true" value="0" >
   <input type="text"  class="marc marc-aciertos" id="aciertos" disabled="true" value="0" >
   <input type="text"  class="marc marc-fallos"   id="fallos"   disabled="true" value="0" >
  </p>

  <ul id ="respuesta"></ul><br>

  <input type="text"  class="message"     id="message"     value="¿Empezamos?" >


    <!-------------------------------------------------------------------------------------------->
  <!-- The Modal help -->
  <div id="myModal" class="modal">

    <!-- Modal content -->
    <div class="modal-content">
      <div class="modal-header">
        <span class="close">&times;</span>
        <h2>Ayuda</h2>
      </div>
      <div class="modal-body">
        <p class="mybold" >1. Selecciona:</p>
        <p>1.1 Los <em>intervalos</em> que quieras practicar (Desde unísono a Octava justa)</p>
        <p>1.2 Si los quieres <em>acendentes</em> y/o <em>descendentes</em></p>
        <p>1.3 La figura que indica la <em>duración</em> de las notas (redonda, blanca,...)</p>
        <p>1.4 El <em>volumen</em>. Si no lo oyes, dale mas volumen!</p>
        <p>1.5 El <em>rango de octavas</em>. (Se recomienda valores intermedios 3 a 5)</p>
        <br>
        <p class="mybold" >2. Puedes escuchar ejemplos de intervalos tantas veces como quieras:</p>
        <p>2.1 Haz click sobre cualquier boton por ejemplo <em>"2M"</em> y escucha un intervalo.</p>
        <br>
        <p class="mybold" >3. Puede utilizar un piano virtual:</p>
        <p>3.1 Puedes hacer click sobre cualquier tecla del piano de la pantalla y sonará.</p>
        <p>3.2 Puedes tocar las notas de las 2 primeras octavas con el <em>teclado del ordenador</em>.</p>
        <p>3.3 Por ejemplo la tecla <em>"q"</em> activa el Do-3</p>
        <br>
        <p class="mybold" >4. Comprueba tus avances con los marcadores de intentos, aciertos y fallos</p>
        <br>
        <p class="mybold" >5. Para empezar a practicar:</p>
        <p>5.1 Haz click sobre el botón <em>"Nuevo Intento"</em></p>
        <p>5.2 Oirás un intervalo que tienes que adivinar</p>
        <p>5.3 Selecciona un intervalo i si es ascendente/descendente (abajo a la izquierda)</p>
        <p>5.4 Si lo tiene claro, dale al botón <em>"OK"</em></p>
        <p>5.5 Con el botón <em>"Repetir Audio"</em>, puedes volver a oir el intervalo</p>

      </div>
      <div class="modal-footer">
        <h3>Suerte y disfruta!</h3>
      </div>
    </div>

  </div>

   <!-- <script src='https://unpkg.com/tone@latest/build/Tone.js'></script> -->
  <script  src="./Tone.js"></script>
  <!--<script  src="./Tone.js.map"></script>-->
  <script  src="./interval-utils.js"></script>
         <script  src="./general-utils.js"></script>
  <script  src="./component-utils.js"></script>
  <!--<script  src="./music-utils.js"></script>-->
  <script  src="./music-utils-edu.js"></script>
                <script  src="./modal-help.js"></script>

  <script  src="./script.js"></script>
 </body>
</html>

Al final se muestran las referencias a ficheros javascrip (líneas 91 a 101).

Veamos ahora el fichero de esilo:


2. style.css

No voy  a explicar mucho, solo indicar que se utilizan muchas clases para reaprovechar las clases básicas y añadir las clases que marcan diferencias. Por eso hay componentes que tienen varias clases CSS.La primera línea  comentada  es para importar algun tipo de fuente y pero para no tener problemas CORS, nio se pueden importar ficheros del CDN:


  1
  2
  3
  4
  5
  6
  7
  8
  9
 10
 11
 12
 13
 14
 15
 16
 17
 18
 19
 20
 21
 22
 23
 24
 25
 26
 27
 28
 29
 30
 31
 32
 33
 34
 35
 36
 37
 38
 39
 40
 41
 42
 43
 44
 45
 46
 47
 48
 49
 50
 51
 52
 53
 54
 55
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
/*
@import url("https://fonts.googleapis.com/css?family=Roboto:400,700&display=swap");
*/
h2 {
 color: DarkBlue;
 text-align: center
}
.flex {
  display: flex;
}

ul .main {
  position: relative;
  width: 70px;
  height: 30px;
  border: 1px solid black;
  border-right: none;
  background: #fffff0;
  border-radius: 5px;
  box-shadow: 0px 3px 5px #666;
  cursor: pointer;
  display: flex;
  justify-content: center;
  align-items: flex-end;
  padding-bottom: 10px;
  font-weight: bold;
}

ul .main-check {
 border:none;
 padding:10px;
 font-size: 125%;
 /*box-shadow:none;*/
 background:LightCyan;
}
ul .main-resp {
  width: auto;
  padding-left: 30px;
  height: 60px;
  font-size: 150%;
  /*margin-right:30px;*/
  background:LightCyan;
  border:none;
}




ul .main-duration {
 /*width:350px;*/
 width:50%;
 padding-left: 30px;
}
ul .main-volume {
 width:300px;
 padding-left: 10px;
}

ul .main-rango {
 width:400px;
 padding-left: 10px;
}

ul .main-key {
  width: 60px;
  height: 120px; /*180px;*/
  font-size:100%;
}

ul .main-key:last-child {
  border-right: 1px solid black;
}

ul .main-blackkey {
  position: absolute;
  top: -1px;
  left: 37.5px;
  width: 45px;
  height: 80px; /*120px;*/
  background: black;
  z-index: 999;
  color: white;
}

ul .bt {
 font-weight: bold;
 font-size: 100%;
 border-radius: 5px;
    box-shadow: 0px 3px 5px #666;
}

ul .chk {
 font-size: 100%;
 margin-left: 10px;
 margin-right: 10px;
 color: blue;
}

ul .sel {
 font-weight: bold;
 font-size: 100%;
 padding:5px;
 margin-left:5px;
 margin-right:5px;

 color:blue;
 border-radius: 5px;
}

ul .sel-resp {
 color:HotPink;
}

input[type=checkbox]:hover {
    box-shadow:0px 0px 10px #1300ff;
}

select:hover {
    box-shadow:0px 0px 10px #1300ff;
}

button:active:hover:not([disabled])  {
    box-shadow:0px 0px 10px #1300ff;
}

.simple-btn {
 border-radius: 5px;
 font-size: 90%;
 color: blue;
 box-shadow: 0px 3px 5px #666;
}
.simple-btn-edu {
 font-size: 125%;
}

.centered{
 text-align: center;
}


/*=====================================================*/

body {
  font-family: 'Roboto', sans-serif;
}


ul {
  list-style: none;
  display: flex;
}




ul .active {
  box-shadow: 0px 1px 3px #666;
}

.titol {
 font-weight: bold;
    font-size:150%;
 color: DarkBlue;
 margin-left: 40px;
}
.titol-marcador {
 margin-left: 60px;
}
.titol-cabecera {
 margin-left: none;

}

.marc {
 font-weight: bold;
  margin-left:15px;
 font-size:150%;
 color: black;
 border: 1px solid black;
  border-radius: 5px;
 width: 50px;
 text-align: right;
 padding: 5px;
 box-shadow: 0px 3px 5px #666;
}

.marc-aciertos {
 color: blue;
}

.marc-fallos {
 color: red;
}

.btn{
 font-weight: bold;
    font-size:95%;
 border-radius: 5px;
 height: 50px;
 padding: 5px;
 margin-left:5px;
 margin-right:5px;
 justify-content: center;
 box-shadow: 0px 3px 5px #666;
}
.btn-rep {
 color: sandybrown;
}

.btn-ok {
 font-size:125%;
 color: blue;
}

.bt-next {
}

button:disabled,
button[disabled]{
  font-style: italic;
  color: DarkGray;
  opacity: 0.3;
}


.message {
 font-weight: bold;
 font-size: 125%;
 margin-left: 40px;
 margin-right: 10px;
 color: blue;
 width:95%;
 height:30px;
 border-radius: 5px;
 box-shadow: 0px 3px 5px #666;
}

/************************************************************************************
*     HELP MODAL FORM
************************************************************************************/

.modal {
  display: none; /* Hidden by default */
  position: fixed; /* Stay in place */
  z-index: 1; /* Sit on top */
  padding-top: 100px; /* Location of the box */
  left: 0;
  top: 0;
  width: 100%; /* Full width */
  height: 100%; /* Full height */
  overflow: auto; /* Enable scroll if needed */
  background-color: rgb(0,0,0); /* Fallback color */
  background-color: rgba(0,0,0,0.4); /* Black w/ opacity */
}

/* Modal Content */
.modal-content {
  position: relative;
  background-color: #fefefe;
  margin: auto;
  padding: 0;
  border: 1px solid #888;
  width: 80%;
 height: 70%;
  box-shadow: 0 4px 8px 0 rgba(0,0,0,0.2),0 6px 20px 0 rgba(0,0,0,0.19);
  -webkit-animation-name: animatetop;
  -webkit-animation-duration: 0.4s;
  animation-name: animatetop;
  animation-duration: 0.4s;
 overflow: scroll;
}

.mybold {
 font-weight: bold;
}

/* Add Animation */
@-webkit-keyframes animatetop {
  from {top:-300px; opacity:0}
  to {top:0; opacity:1}
}

@keyframes animatetop {
  from {top:-300px; opacity:0}
  to {top:0; opacity:1}
}

/* The Close Button */
.close {
  color: white;
  float: right;
  font-size: 28px;
  font-weight: bold;
}

.close:hover,
.close:focus {
  color: #000;
  text-decoration: none;
  cursor: pointer;
}

.modal-header {
  padding: 2px 16px;
  background-color: #5cb85c;
  color: white;
}

.modal-body {padding: 2px 16px;}

.modal-footer {
  padding: 2px 16px;
  background-color: #5cb85c;
  color: white;
}

Veamos el fichero responsable de organizar todo:

3. script.js

A partir de la línea 190 nos encargamos de manejar el piano de Philip Shim.

La línea 166 aparece la función fillAll() que es la que se encarga de crear los controles y asignar eventos en la carga del formulario (onload event).  en ella se llaman a funciones que aparecen desde las primeras líneas y construyen el formulario (checkboxes, selectores ...) y normalmente se agregan aun bloque <li> de un <ul>. Es decirt cada <ul> representa una línea de controles gemelos y a dicha linea le añadimos elementos <li> que cargan con los controles.



  1
  2
  3
  4
  5
  6
  7
  8
  9
 10
 11
 12
 13
 14
 15
 16
 17
 18
 19
 20
 21
 22
 23
 24
 25
 26
 27
 28
 29
 30
 31
 32
 33
 34
 35
 36
 37
 38
 39
 40
 41
 42
 43
 44
 45
 46
 47
 48
 49
 50
 51
 52
 53
 54
 55
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
const checkboxes=[ ];
const checkboxesAsc=[ ];
const radios=[ ];

/**
* Selectioin of intervals
*
*/
function fillSelection() {
 var aText=getNthArrayElement(intervals,"-",0);

 for (var i=0; i<=12; i++) {
  checkboxes.push(createCheckBox("ch"+i ,"chk", "checked"));
  addLitoUlSimple("selection", "sel"+i, "main main-check", aText[i], checkboxes[i]);
 }
}

/**
* Selectioin of intervals ascending/descending
*
*/
function fillSelectionAsc() {
 for (var i=0; i<=1; i++) {
  checkboxesAsc.push(createCheckBox("ch"+i ,"chk", "checked"));
  addLitoUlSimple("selectionasc", "selasc"+i, "main main-check", ascen[i], checkboxesAsc[i]);
 }
}

/**
* Selection of duration of notes
*
*/
function fillDuration() {
 var selDuration=createSelect("selDuration",duration,"sel")
 selDuration.selectedIndex = 2;
 addLitoUlSimple("selectionasc", "dur", "main main-check main-duration", "Duracion notas:", selDuration);
}

/**
* Selectioin of volume of notes (0,100)
*/
function fillVolume() {
  var selVolume=createSelect("selVolume",volume,"sel")
  addLitoUlSimple("selectionasc", "dur", "main main-check main-volume ", "Volumen:", selVolume);
  selVolume.addEventListener('change', e=> changeVolume());
   selVolume.selectedIndex = 7;
}

function fillOctaves() {
  var selOctaveFrom=createSelect("seloctavefrom",octaves,"sel")
  selOctaveFrom.addEventListener('change', e=> getOctaves());
   selOctaveFrom.selectedIndex = 3;
  var li=addLitoUlSimple("selectionasc", "dur", "main main-check main-rango ", "Octavas rango:", selOctaveFrom);

  var selOctaveTo=createSelect("seloctaveto",octaves,"sel")
  selOctaveTo.addEventListener('change', e=> getOctaves());
   selOctaveTo.selectedIndex = 5;
  li.appendChild(selOctaveTo);
}

/*
function fillVolumeTxt() {
 var txtVolume=createTextBox("txtVolume","sel")
 txtVolume.value= 0;
 addLitoUlSimple("selectionasc", "dur", "main main-check main-volume ", "", txtVolume);
}
*/
/**
* Fill buttons to hear intervals examples
*
*/
function fillButtons() {
 var aText=getNthArrayElement(intervals,"-",0);
  for (var i=0; i<=12; i++) {
    var button = createButton("btasc-"+i ,aText[i]+ascChar, "bt");

    //button.addEventListener("click", e=> playDemoInterval(j, 1)); //No guarda la i
  button.addEventListener("click", e=> playDemoInterval(e.target.id.split('-')[1], 0));
    var li=addLitoUlSimple("buttonsasc", "lbtasc"+i, "main main-check", "", button);
 }

  for (var i=0; i<=12; i++) {
    var button = createButton("btdes-"+i ,aText[i]+desChar, "bt");
    button.addEventListener("click", e=> playDemoInterval(+e.target.id.split('-')[1], 1));
  var li=addLitoUlSimple("buttonsdes", "lbtdes"+i, "main main-check", "", button);
 }
}


/**
* Fill Piano from octaveDesde to octaveHasta
*
*/
function fillPianoEdu(octaveDesde,octaveHasta) {
 for (var j=octaveDesde; j<=octaveHasta; j++) {
  fillPianoEduOctave(j);
 }
}

/**
* Add a piano with one octave
*
*/
function fillPianoEduOctave(octave) {
 var aInt1=getNthArrayElement(intervals,"-",1);
 var aInt2=getNthArrayElement(intervals,"-",2);
 var aInt3=getNthArrayElement(intervals,"-",3);
 var aInt4=getNthArrayElement(intervals,"-",4);

 for (var i=0; i<=11; i++) {
  var note=aInt2[i]+octave;
  var ifSharp=note.includes("#");
  var noteE=aInt1[i]+octave;

  if (!ifSharp) {
   var nextNote=aInt2[i+1]+octave;
   var ifSharpNext=nextNote.includes("#");
   var tecla=note;
   if (octave==3) tecla= aInt3[i];
   if (octave==4) tecla= aInt4[i];

   var div=null;
   if (ifSharpNext) {
    var nextNoteE=aInt1[i+1]+octave;
    var tecla1=nextNote;
    if (octave==3) tecla1= aInt3[i+1];
    if (octave==4) tecla1= aInt4[i+1];
    div=createDiv("divPi"+i, tecla1, " main main-blackkey", "data-note",nextNoteE);
      }

   var li = addLitoUl("pianoedu", "ed"+i, "main main-key", tecla, div,"data-note",noteE)
      if (note.includes("Do"))  li.style.backgroundColor ="LightYellow";
         }

 }
}


/**
* Get the selectors of intervals and asc/desc
*/
function fillRespuesta() {
 var aInt0=getNthArrayElement(intervals,"-",0);
 var selInt=createSelect("selInt",aInt0,"sel sel-resp");
 var selAsc=createSelect("selAsc",ascen,"sel sel-resp");
 var li = addLitoUlSimple("respuesta", "resp", "main main-resp", "Intervalo:", selInt);
 li.appendChild(selAsc);

  // Fill buttons
 var btOK=createButton("btok","OK", "btn btn-ok", true);
 btOK.addEventListener("click", e=> checkAnswer());
 li.appendChild(btOK);

 var btNext=createButton("btnext","Nuevo Intento", "btn btn-next", false);
 btNext.addEventListener("click", e=> nextPlay());
 li.appendChild(btNext);

 var btRep=createButton("btrep","Repetir Audio", "btn btn-rep", true);
 btRep.addEventListener("click", e=> repeatAudition());
 li.appendChild(btRep);
}

/**
* Fills everything
*/
function fillAll(octaveDesde, octaveHasta) {
 fillSelection();
 fillSelectionAsc();
 fillDuration();
 fillVolume();
 fillOctaves();
 //fillVolumeTxt()
 fillButtons();
  fillPianoEdu(octaveDesde,octaveHasta);
 fillRespuesta();
 //fillRespButtons();
 changeVolume();
 getOctaves();
}

function marcarTodo() {
 var check= (checkboxes[0].checked) ? false : true;
 for (var i=0; i<checkboxes.length; i++){
  checkboxes[i].checked=check;
 }
}
// Piano activate keys
const pianoEdu = document.getElementById("pianoedu");

pianoEdu.addEventListener("mousedown", e => {
  //changeVolume();
  synth.triggerAttack(e.target.dataset.note);
});


pianoEdu.addEventListener("mouseup", e => {
  synth.triggerRelease();
});


// Play in the keyboard
document.addEventListener("keydown", e => {
  var k=e.key;
  //changeVolume();
  console.log("addEventListener.keydown.tecla=" + k + "  volumejn="+synth.volume.value );

    for (var i=0; i<12; i++) {
   if (intervals[i].split('-')[3].includes(k)) {
     return synth.triggerAttack(intervals[i].split('-')[1]+"3");
   }
   else if (intervals[i].split('-')[4].includes(k)) {
     return synth.triggerAttack(intervals[i].split('-')[1]+"4");
   }
 }

  return
})  ;

 document.addEventListener("keyup", e => {
  var k=e.key;

 for (var i=0; i<12; i++) {
   if (intervals[i].split('-')[3].includes(k) ||
       intervals[i].split('-')[4].includes(k)) {
     synth.triggerRelease();
   }
   }

 });


4. general-utils.js

Se ha buscado como obtener los símbolos de las flechitas ascendentes o descendentes y se han marcado como constante (ascChar y desChar)

En javascript y en concreto en tone.js tenmos un problema (o virtud según como se mire), que si queremos tocar 2 notas, si no esperamos a que la primera nota termine, se pone en marcha a tocar la segunda nota también.

En chrome se puede solucionar implementando la función wait que se dedica a esperar tanto tiempo como la duración de la nota que le damos (línea 44). Pero en Mozilla no funciona. Para ello hemos tenido que recurrir a la función window.setTimeout() que ejecuta asíncronamente una función al cabo de cierto tiempo.

También tenemos una función de números aleatorios para obtener los intervalos aleatorios a preguntar.

Para los arrays de varias dimensiones, se ha practicado una función que devuelve una parte del array.


 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
//Flechas de subida y bajada para marcar intervalos ascendentes o descendentes
const ascChar ='\u2191';
const desChar ='\u2193';


/**
* Get the nth array element of
*    An array with elements separated with a separator
*
* array = arrString
* separator= separator
*
* examples:
*
* arrTest=["1-a-A","2-b,B","3-c-C","4-d-D"]
*
* getNthArrayElement(arrTest,"-",0) = ["1","2","3","4"]
* getNthArrayElement(arrTest,"-",1) = ["a","b","c","d"]
*
*/
function getNthArrayElement(arrString,separator,n) {
 var a=[ ];
 for (var i=0; i<arrString.length; i++) {
                a1=arrString[i];
  b=a1.split('-');
  c=(b.length>n ? b[n] : '');
  a.push(c);
 }
 return a;
}

/**
* Get a random number between min and max inclusive
*/
 function getRandomInt(min, max) {
    min = Math.ceil(min);
    max = Math.floor(max);
    return Math.floor(Math.random() * (max - min + 1)) + min;
}

/**
*  Espera mseg
*/
function wait(ms){
   var start = new Date().getTime();
   var end = start;
   while(end < start + ms) {
     end = new Date().getTime();
  }
}


5. interval-utils.js


Aquí tenemos constantes de canciones para asociar a intervalos


 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
const ascIntervalMusics= [
  "0.La Tarara",                         //0
  "2m.Tiburon",                          //1
  "2M.Cumpleaños Feliz,Fray Geroni",                //2
  "3m.Canción de cuna de Brahms",        //3
  "3M.Ya vienen los reyes magos, For he's a Jolly Good Fellow,",     //4
  "4J.We wish you a merry christmas,El padrino, Marcha nupcial (ja s'han casat)",    //5
  "4A.Maria.LosSimpsons",                //6
  "5J.Star Wars, Twinkle little star, Así hablo Zaratusta",   //7
  "6m.Manha, Mocita la del clavel",                            //8
  "6M.La traviata",                      //9
  "7m.The winner takes it all, Mi jaca", //10
  "7M.Superman"                        , //11
  "8J.Somewhere over the reainbow, Singing in the rain"       //12
];

const desIntervalMusics= [
  "0.La Tarara",                         //0
  "2m.Tiburon. Septiminio(erase una vez)",                          //1
  "2M.Va pensiero Nabuco, Yesterday, Si yo fuera rico",                 //2
  "3m.Puente rio Kwai, Hey Hude don't let me down",        //3
  "3M.Rellotge, Bethoven 5ª sinfonia",     //4
  "4J.Himno españa",    //5
  "4A.",                //6
  "5J.Flinstones",   //7solutions[0].equalsIgnoreCase(""+
  "6m.Love story",                            //8
  "6M.Nobody knows the trouble",                      //9
  "7m.Un americano en Paris (tii,ta,ta)", //10
  "7M.Cole Porter I love you "                        , //11
  "8J Dartacan(eran uno dos y tres)"       //12
];


6. component-utils


Hay utilidades para:

  • asignar múltiples clases CSS a un objeto (línea 5)
  • crear varios tipos de componentes html (select, checkbox, textbox, button, div, li)
  • asignar algunas propiedades (disable, value)
  • otras (incrementar contadores en textbox)


  1
  2
  3
  4
  5
  6
  7
  8
  9
 10
 11
 12
 13
 14
 15
 16
 17
 18
 19
 20
 21
 22
 23
 24
 25
 26
 27
 28
 29
 30
 31
 32
 33
 34
 35
 36
 37
 38
 39
 40
 41
 42
 43
 44
 45
 46
 47
 48
 49
 50
 51
 52
 53
 54
 55
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
/**
* Add classes separated by space
*/
function addClassEdu(myComp, myClasses) {
 arrCls=myClasses.trim().split(/\s+/);
 for (var i=0; i<arrCls.length; i++) myComp.classList.add(arrCls[i]);
}


/**
* Create a select element with:
* id= id_
* css class = cssClass
* options = the array of string "arrText"
*/
function createSelect(id_,arrText,cssClass) {
 var myComp=document.createElement("select");
 myComp.id=id_;
 //myComp.classList.add(cssClass);
 addClassEdu(myComp, cssClass);
 for (var i=0; i<arrText.length; i++) {
  var opt=document.createElement("option");
  opt.text=arrText[i];
  myComp.add(opt);
 }
 return myComp;
}

/**
* Create a checkbox element with:
* id= id_
* css class = cssClass
*/
function createCheckBox(id_,cssClass,sChecked) {
 var myComp=document.createElement("input");
 myComp.type="checkbox";
 myComp.name=id_;
 myComp.id=id_;
 myComp.checked=sChecked;
 //myComp.classList.add(cssClass);
 addClassEdu(myComp, cssClass);

 return myComp;
}

/**
* Create a textbox element with:
* id= id_
* css class = cssClass
*/
function createTextBox(id_,cssClass) {
 var myComp=document.createElement("input");
 myComp.type="text";
 myComp.name=id_;
 myComp.id=id_;
 addClassEdu(myComp, cssClass);

 return myComp;
}



/**
* Create a button element with:
* id= id_
* css class = cssClass
*/
function createButton(id_,text, cssClass, disabled) {
 var myComp=document.createElement("button");
 myComp.type="button";
 myComp.id=id_;
 myComp.disabled=disabled;
 myComp.appendChild(document.createTextNode(text));
 //myComp.classList.add(cssClass);
 addClassEdu(myComp, cssClass);

 return myComp;
}

/**
* Create a div element with:
*   id= id_
*   css class = cssClass
*   attr:       attribute (data-note)
*   attVal:     value of the attribute (C4)
*/
function createDiv(id_,text, cssClass, attr, attVal ) {
 var myComp=document.createElement("div");
 myComp.id=id_;
 myComp.appendChild(document.createTextNode(text));
 //myComp.classList.add(cssClass);
 addClassEdu(myComp, cssClass);
 if (attr.length>0) myComp.setAttribute(attr, attVal);
 return myComp;
}

/**
* Add a li elelement to an unordered list ul
* arguments:
*   ulId:       id of the ul
*   liId:       new id of the li
*   liCssClass: css class of the li
*   liText:     text for the li
*   liChild:    child component to add to the li
*/
function addLitoUlSimple(ulId, liId, cssClass, text, liChild) {
 var ul = document.getElementById(ulId);
 var li = document.createElement("li");
 //li.classList.add(cssClass);
 addClassEdu(li, cssClass);
 li.setAttribute("id", liId);
 if (text.length>0) li.appendChild(document.createTextNode(text));
 //li.appendChild(document.createTextNode(text));
 if (liChild!=null)li.appendChild(liChild);
 li.appendChild(liChild);
 ul.appendChild(li);
 return li;
}


/**
* Add a li elelement to an unordered list ul
* arguments:
*   ulId:       id of the ul
*   liId:       new id of the li
*   liCssClass: css class of the li
*   liText:     text for the li
*   liChild:    child component to add to the li
*   attr:       attribute (data-note)
*   attVal:     value of the attribute (C4)
*/
function addLitoUl(ulId, liId, cssClass, text, liChild, attr, attVal) {
 var ul = document.getElementById(ulId);
 var li = document.createElement("li");
 //li.classList.add(cssClass);
 addClassEdu(li, cssClass);
 li.setAttribute("id", liId);
 if (text.length>0) li.appendChild(document.createTextNode(text));
 if (liChild!=null)li.appendChild(liChild);
 li.setAttribute(attr, attVal);
 ul.appendChild(li);
 return li;
}

/** increment counter
*
*/
function disableComponent(idComponent, disabled){
  var myComp= document.getElementById(idComponent);
  myComp.disabled=disabled;
}


/**
*  increment a textBox that stores a counter
*/
function incrementCounter(idMarcador){
  var txtMarcador= document.getElementById(idMarcador);
  txtMarcador.value++;
}

/**
* Fill textbox with message
*
*/
function fillTextBox(id, value, color) {
  var myComp= document.getElementById(id);
  myComp.value=value;
  myComp.style.color = color;
}


7. music-utils

En general se hace:

  • Coger las referencias a elementos de otros javascripts en concreto a Tone.Synth() y Tone.Synth.toMaster()
  • Definimos los inervalos, notas en nomenclatura inglesa y castellana, y el mapeo del teclado al piano.
  • Definimos otras constantes como la duración de notas en base a figuras (blanca, negra, corchea..) volumen, octavas desde hasta--
  • Conversión del volumen de sonido a escala logarítmica (simil adecibelios)
  • Tocar intervalos.
  • Recogida de valores de los controles (duración, volumen, rango de octavas, respuestas..)
  • Lógica de negocio del juego (nuevas jugada, verificar resultado, tocar intervalos de ejemplo)
En fin espero que os haya gustado, aunque un poco largo.


  1
  2
  3
  4
  5
  6
  7
  8
  9
 10
 11
 12
 13
 14
 15
 16
 17
 18
 19
 20
 21
 22
 23
 24
 25
 26
 27
 28
 29
 30
 31
 32
 33
 34
 35
 36
 37
 38
 39
 40
 41
 42
 43
 44
 45
 46
 47
 48
 49
 50
 51
 52
 53
 54
 55
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
/*******************************************************************************************************************
/**
* @see: https://odino.org/emit-a-beeping-sound-with-javascript/
* @see: https://stackoverflow.com/questions/39200994/how-to-play-a-specific-frequency-with-javascript
* @see: https://pages.mtu.edu/~suits/notefreqs.html
*
* @see: https://www.javascripture.com/OscillatorNode
*
*/
//const audioContext=new AudioContext() ;// browsers limit the number of concurrent audio contexts, so you better re-use'em
//var audioContext = new (window.AudioContext || window.webkitAudioContext)();

// Tone JS Synthetizer
const synth = new Tone.Synth();
// synth.oscillator.type = "sine";
synth.toMaster();

const intervals = [
      "Un-C-Do-q-z",         //0
   "2m-C#-Do#-2-s",        //1
   "2M-D-Re-w-x",        //2
   "3m-D#-Re#-3-d",        //3
   "3M-E-Mi-e-c",        //4
   "4J-F-Fa-r-v",        //5
   "4A-F#-Fa#-5-g",        //6
   "5J-G-Sol-t-b",        //7
   "6m-G#-Sol#-6-h",        //8
   "6M-A-La-y-n",        //9
   "7m-A#-La#-7-j",        //10
   "7M-B-Si-u-m",        //11
   "8J-C-Do"         //12
];

const ascen=["Asc","Desc"]
const duration=["Redonda","Blanca","Negra","Corchea","Semicorchea"];

//const volume=[100, 70, 40, 10, 7, 4, 1, 0.7, 0.4, 0.1, 0.07, 0.04, 0.01, 0.007, 0.004, 0.001];
const volume=[20, 15, 12, 11, 10, 9, 8, 7, 6, 5, 4, 3, 2 ];
const fqLa4=440; // Frecuencia La4

const octaves = [0,1,2,3,4,5,6,7,8,9,10];
var octaveFrom =2;
var octaveTo =5;

// Variable that stores the last interval to play
var aa={};

/**
* Obtenemos la frecuencia de una nota en nomenclatura inglesa
* Web audio solo trabaja en frecuencia
*  nota: "A3", "C4".....
*/
function getFrequency(note) {
 var notes=getNthArrayElement(intervals,"-",1) ; //Get array of music notes
 var notat=note.trim();
 var octave=notat.substr(notat.length-1,1);
 var nota1=notat.substring(0,notat.length-1);
 var found=false;
 var i=-1;
 while (! found && i<12){
  if (notes[++i].localeCompare(nota1)==0) found=true;
 }
 var freq=fqLa4*(2**((i-9)/12));
 freq=freq*(2**(octave-4));
 return freq;
}
//==========================TONEJS =================================

/**
*  Plays a note in Tone.js
*  note1: "A4", "B3", "C#3" ...
*  note2: "A4", "B3", "C#3" ...
*  duration (ms)
*  noecho: stop playing, avoiding residual vibration
*/
function playInterval(note1, note2, myDuration, noecho) {
  synth.triggerAttackRelease(note1, myDuration/1000);
  window.setTimeout(playSecondTone, myDuration, note2, myDuration);
  if (noecho)window.setTimeout(removeEcho, myDuration*2);
}

/**
Plays the second note of the Interval {
*  note2 : note to play
*  duration (ms)
*/
function playSecondTone(note, myDuration) {
  synth.triggerAttackRelease(note, myDuration/1000);
}

/**
* Play a note of small duration without any volumen
* and removes remaining vibrations
*/
function removeEcho() {
  synth.volume.value = "-Infinity";
  changeVolume();
}

/**
* Convert volume in WebAudio to Tonejs
*
* WebAudio equals to Tone
* =======            ========
*      100            100
*       50             25
*       25              0
*       0      "-Infinite"
*/
function convertVolumeWAudiotoTone(volume) {
 if (volume==0) return "-Infinity";
 var a = 100*Math.log(volume/10)/Math.log(10);
 //console.log ("convertVolumeWAudiotoTone.volumen.js="+a + "  volumen="+ volume);
 return a;
}

/**
* Get selected duration from form in miliseconds
*/
function getDurationms() {
  var select=document.getElementById("selDuration");
  //var opc = select.options[select.selectedIndex].value;
  var opc = select.selectedIndex;
  return 4/(2**opc)*1000 ;
}

/**
* Get selected volume in %
*/
function getVolume() {
  var select=document.getElementById("selVolume");
  return select.options[select.selectedIndex].value;
}

/**
* Get Octaves
*/
function getOctaves() {
  var selectFrom=document.getElementById("seloctavefrom");
    octaveFrom= selectFrom.options[selectFrom.selectedIndex].value;
    var selectTo=document.getElementById("seloctaveto");
    octaveTo= selectTo.options[selectTo.selectedIndex].value;
}
/**
* Change Volum =
*/
function changeVolume() {
 synth.volume.value = convertVolumeWAudiotoTone(getVolume());
 //console.log("changeVolume.vol="+getVolume());
}

/**
* Get the selected duration in nth of a whole note (redonda)
*   1n , 2n, 3n, 4n, 8n,
*/
function getDurationNth() {
  var select=document.getElementById("selDuration");
  //var opc = select.options[select.selectedIndex].value;
  var opc = select.selectedIndex;
  return ""+(2**opc)+"n" ;
}

/**
* Fill the variable "aa" with the info of the interval to play:
*
*  note1
*  note2
*  asc: 0/1 for ascending/descending
*/
function getNotesInterval() {
 var arrInts=[ ];
 var arrAsc= [ ];

 for (var i=0; i<13; i++) if (checkboxes[i].checked) arrInts.push(i);
 for (var i=0; i<2; i++)   if (checkboxesAsc[i].checked) arrAsc.push(i);

 var interval=getRandomInt(0, arrInts.length-1);  //Posion relativa
 interval=arrInts[interval];  //Posicion absoluta

  var asc=getRandomInt(0, arrAsc.length-1);
  asc=arrAsc[asc];

  var arrNot1=getRandomNote();
 var arrNot2=getInterval2ndNote(arrNot1[0],arrNot1[1], interval);
 aa=getNotesIntervalFromNotePos(arrNot1[0], arrNot1[1],arrNot2[0], arrNot2[1], interval, asc);
  console.log("getNotesInterval.aa="+aa);
}

/**
* Grts tje second note of an interval
*
*/
function getInterval2ndNote(nota1, octave1, interval){
  var a=[ ];
  var octave2=octave1;

 var nota2=+interval+nota1;
 if (nota2>11) {
  nota2=+nota2-12;
  octave2=+octave1+1;
 }
  a.push(+nota2);
  a.push(+octave2);
  return a;

}
/**
* get notes in English notation like "A4", "B#3" ..
* params:
*  nota1:   int position of first note (0..11)
*  octave1: int octave of first note (0..6)
*  nota2:   int position of second note (0..11)
*  octave2: int octave of second note (0..6)
*  interval: Not used but needed to fill aa structure
*/
function getNotesIntervalFromNotePos(nota1, octave1,nota2, octave2, interval, asc) {
   var notes=getNthArrayElement(intervals,"-",1) ; //Get array of music notes
   var bb={};
  bb.nota1=notes[nota1]+octave1;
  bb.nota2=notes[nota2]+octave2;
  bb.interval=interval;
  bb.asc=asc;

   return bb;
}

/**
* Get a random note
*   return an array of integer: first = note number, second=octave
*
*/
function getRandomNote() {
  var a=[ ];
  a.push(getRandomInt(0, 11));   //Nota);
  a.push(getRandomInt(octaveFrom, octaveTo)); // octaves
  return a;
}


/**
* Next play
*/
function nextPlay() {

   //Enable/disable Buttons
   disableComponent("btok"  ,false);
   disableComponent("btnext",true);
   disableComponent("btrep" ,false);

   getNotesInterval(); // get new interval to play into variable "aa"
   repeatAudition(); // Play interval
   incrementCounter("intentos"); //Increment attempts "intentos"
}

/**
* Play an interval
*
*/
function playIntervalByValue(bb){
  if (bb.asc==0) playInterval(bb.nota1, bb.nota2,  getDurationms(), true);
  else           playInterval(bb.nota2, bb.nota1,  getDurationms(), true);
  console.log("playIntervalByValue.nota1="+bb.nota1 + " nota2="+bb.nota2 + " interval="+bb.interval +" asc="+bb.asc);
}
/**
* Repeat  audition
*/
function repeatAudition() {
  playIntervalByValue(aa);
}


/**
* Check Answer
*/
function checkAnswer() {

 //Enable/disable Buttons
 disableComponent("btok"  ,true);
 disableComponent("btnext",false);
 disableComponent("btrep" ,false);


 var aText=getNthArrayElement(intervals,"-",0);

 var selAns=document.getElementById("selInt");
 var intUsr=selAns.selectedIndex;

 var selAnsAsc=document.getElementById("selAsc");
 var ascUsr=selAnsAsc.selectedIndex;

 //Acertamos
  console.log("checkAnswer.aa.interval="+aa.interval + " intUsr=" + intUsr);
 if (intUsr==aa.interval && (ascUsr==aa.asc || intUsr == 0) ) {
    incrementCounter("aciertos");
    fillTextBox("message", "¡Correcto! ", "blue");
  //Fallamos
  } else {
    incrementCounter("fallos");
    fillTextBox("message", "¡Noo!  La respuesta correcta es: "+ aText[aa.interval] + "-"+ ascen[aa.asc], "red");
 }

}
/**
* Function that plays an interval whose first note is C4
* It is triggered by buttons
*/
function playDemoInterval(interval, asc) {
  nota1=0;     //C4
  octave1=4;
  var arrNot2=getInterval2ndNote(nota1, octave1, interval);
 var nota2=arrNot2[0];     //Position nota
 var octave2=arrNot2[1];   //octave
  console.log("playDemoInterval.1.nota1:" + nota1 + " nota2=" + nota2);
  var bb=getNotesIntervalFromNotePos(nota1, octave1,nota2, octave2, interval, asc);
  console.log("playDemoInterval.2.bb="+ bb);
  var myMessage= (asc==0) ? ascIntervalMusics[interval] : desIntervalMusics[interval];
  console.log("playDemoInterval.3.interval:" + interval + " asc="+ asc +"  mensaje="+ myMessage + " ascInterv:" + ascIntervalMusics[interval]);
  fillTextBox("message", myMessage, "deepskyblue");
  playIntervalByValue(bb);

}



No hay comentarios :

Publicar un comentario