primocode 8.35.0 → 8.36.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -6,6 +6,37 @@
6
6
  tipo "gerada" → o estúdio sintetiza a música na hora, sem arquivo
7
7
 
8
8
  Tudo passa por um nó de saída único para poder ser gravado junto do vídeo.
9
+
10
+ ── POR QUE A MÚSICA GERADA FOI REFEITA ─────────────────────────────────
11
+
12
+ "Música de fundo: escolha de músicas ruim, sem qualidade."
13
+
14
+ A versão anterior tinha um defeito que não se ouve em cinco segundos e não
15
+ se aguenta em dois minutos: TODO COMPASSO ERA IDÊNTICO. O arpejo era uma
16
+ sequência fixa de oito notas, a bateria era a mesma batida, o pad era o
17
+ mesmo acorde, e o filtro não se mexia. Numa peça de trinta segundos aquilo
18
+ é um loop de dois segundos repetido quinze vezes — e é isso que se ouve
19
+ como "sem qualidade", antes de qualquer discussão sobre timbre.
20
+
21
+ Junto vinham três coisas que separam som de programa de som de música:
22
+
23
+ · TUDO SECO. Oscilador → filtro → saída. Sem espaço nenhum, qualquer
24
+ síntese soa como toque de celular antigo. Reverberação é a diferença
25
+ maior por linha de código deste arquivo.
26
+ · TUDO NO CENTRO. Mono. Um pad largo em estéreo soa "produzido" e um pad
27
+ mono soa "gerado", com as mesmas notas.
28
+ · TUDO NA MESMA FORÇA. Nota humana varia de volume e chega alguns
29
+ milissegundos fora do quadrado; nota de máquina não varia nunca.
30
+
31
+ O que NÃO entrou, e é honesto dizer: nada disso vira música boa de verdade.
32
+ São quatro osciladores e um ruído. O alvo aqui é a cama sob uma narração —
33
+ presente, variada e discreta —, não uma faixa para ouvir sozinha.
34
+
35
+ ── O SORTEIO É SEMEADO, E ISSO É OBRIGATÓRIO ───────────────────────────
36
+ A variação toda sai de `_sorteio`, um gerador determinístico semeado pelo
37
+ estilo. Com `Math.random` a mesma peça sairia com uma música diferente a
38
+ cada vez que fosse tocada ou exportada — a pessoa aprova a prévia, exporta,
39
+ e recebe outro vídeo. Aqui o mesmo estilo dá sempre a mesma faixa.
9
40
  ========================================================================== */
10
41
 
11
42
  var ESTILOS_TRILHA = {
@@ -14,42 +45,74 @@ var ESTILOS_TRILHA = {
14
45
  escala: [0, 2, 3, 5, 7, 8, 10],
15
46
  progressao: [0, 5, 3, 4],
16
47
  bateria: true, ondaPad: "sawtooth", ondaArp: "square",
17
- corte: 1600, ganhoPad: 0.055, ganhoArp: 0.05, arpDiv: 2,
48
+ corte: 1600, ganhoPad: 0.05, ganhoArp: 0.045, arpDiv: 2,
49
+ largura: 0.4, reverb: 0.22, setima: true, bombeia: true,
18
50
  },
19
51
  ambiente: {
20
52
  rotulo: "Ambiente", bpm: 68, raiz: 41,
21
53
  escala: [0, 2, 4, 7, 9],
22
54
  progressao: [0, 3, 5, 3],
23
55
  bateria: false, ondaPad: "sawtooth", ondaArp: "sine",
24
- corte: 900, ganhoPad: 0.085, ganhoArp: 0.028, arpDiv: 1,
56
+ corte: 900, ganhoPad: 0.08, ganhoArp: 0.026, arpDiv: 1,
57
+ largura: 0.6, reverb: 0.5, setima: true,
25
58
  },
26
59
  lofi: {
27
60
  rotulo: "Lo-fi", bpm: 76, raiz: 43,
28
61
  escala: [0, 2, 3, 5, 7, 8, 10],
29
62
  progressao: [0, 3, 4, 2],
30
63
  bateria: true, ondaPad: "triangle", ondaArp: "sine",
31
- corte: 1100, ganhoPad: 0.07, ganhoArp: 0.04, arpDiv: 2, solto: true,
64
+ corte: 1100, ganhoPad: 0.065, ganhoArp: 0.038, arpDiv: 2, solto: true,
65
+ largura: 0.35, reverb: 0.34, setima: true,
32
66
  },
33
67
  epico: {
34
68
  rotulo: "Épico", bpm: 88, raiz: 38,
35
69
  escala: [0, 2, 3, 5, 7, 8, 10],
36
70
  progressao: [0, 5, 6, 4],
37
71
  bateria: true, ondaPad: "sawtooth", ondaArp: "triangle",
38
- corte: 2200, ganhoPad: 0.09, ganhoArp: 0.055, arpDiv: 2, oitavaBaixo: true,
72
+ corte: 2200, ganhoPad: 0.085, ganhoArp: 0.05, arpDiv: 2, oitavaBaixo: true,
73
+ largura: 0.5, reverb: 0.42, setima: true, bombeia: true,
39
74
  },
40
75
  alegre: {
41
76
  rotulo: "Alegre", bpm: 124, raiz: 48,
42
77
  escala: [0, 2, 4, 5, 7, 9, 11],
43
78
  progressao: [0, 4, 5, 3],
44
79
  bateria: true, ondaPad: "triangle", ondaArp: "square",
45
- corte: 2600, ganhoPad: 0.05, ganhoArp: 0.045, arpDiv: 4,
80
+ corte: 2600, ganhoPad: 0.045, ganhoArp: 0.04, arpDiv: 4,
81
+ largura: 0.45, reverb: 0.2, bombeia: true,
46
82
  },
47
83
  tenso: {
48
84
  rotulo: "Tenso", bpm: 100, raiz: 36,
49
85
  escala: [0, 1, 3, 5, 6, 8, 10],
50
86
  progressao: [0, 0, 1, 0],
51
87
  bateria: true, ondaPad: "sawtooth", ondaArp: "sawtooth",
52
- corte: 700, ganhoPad: 0.075, ganhoArp: 0.035, arpDiv: 4,
88
+ corte: 700, ganhoPad: 0.07, ganhoArp: 0.032, arpDiv: 4,
89
+ largura: 0.3, reverb: 0.3, setima: true,
90
+ },
91
+ /* As duas de baixo entraram junto com a reforma, e por um motivo de uso:
92
+ a maior parte dos vídeos deste estúdio é gente falando na frente da
93
+ câmera. `epico` e `tenso` competem com a voz; para uma narração o que
94
+ serve é uma cama que quase não se nota. Sem elas, o agente escolhia entre
95
+ seis estilos e cinco eram grandes demais para o vídeo que ele estava
96
+ montando. */
97
+ /* `suave` mora BAIXO de propósito (raiz 40, corte 1000): a fala vive dos
98
+ 300 aos 3500 Hz, e uma cama nesse registro disputa com a narração por mais
99
+ que o volume esteja baixo. Medido: com a raiz em 46 ela ocupava mais a
100
+ faixa da voz que o `epico`, que é uma faixa feita para ser grande. */
101
+ suave: {
102
+ rotulo: "Suave", bpm: 60, raiz: 40,
103
+ escala: [0, 2, 4, 7, 9],
104
+ progressao: [0, 4, 2, 5],
105
+ bateria: false, ondaPad: "sine", ondaArp: "sine",
106
+ corte: 1000, ganhoPad: 0.042, ganhoArp: 0.016, arpDiv: 2,
107
+ largura: 0.65, reverb: 0.5, setima: true,
108
+ },
109
+ foco: {
110
+ rotulo: "Foco", bpm: 96, raiz: 44,
111
+ escala: [0, 2, 4, 5, 7, 9],
112
+ progressao: [0, 2, 4, 2],
113
+ bateria: true, ondaPad: "triangle", ondaArp: "sine",
114
+ corte: 1500, ganhoPad: 0.036, ganhoArp: 0.02, arpDiv: 2, semCaixa: true,
115
+ largura: 0.6, reverb: 0.28, setima: true, discreta: true,
53
116
  },
54
117
  };
55
118
 
@@ -57,26 +120,68 @@ function nomesEstilosTrilha() { return Object.keys(ESTILOS_TRILHA); }
57
120
 
58
121
  function _hz(midi) { return 440 * Math.pow(2, (midi - 69) / 12); }
59
122
 
123
+ /* Sorteio determinístico (mulberry32). Ver o cabeçalho: com Math.random a
124
+ prévia aprovada e o arquivo exportado seriam duas músicas diferentes. */
125
+ function _sorteio(semente) {
126
+ var a = semente >>> 0;
127
+ return function () {
128
+ a = (a + 0x6D2B79F5) >>> 0;
129
+ var t = Math.imul(a ^ (a >>> 15), 1 | a);
130
+ t = (t + Math.imul(t ^ (t >>> 7), 61 | t)) ^ t;
131
+ return ((t ^ (t >>> 14)) >>> 0) / 4294967296;
132
+ };
133
+ }
134
+
135
+ function _semeadoPor(texto) {
136
+ var h = 2166136261;
137
+ for (var i = 0; i < String(texto).length; i++) {
138
+ h ^= String(texto).charCodeAt(i);
139
+ h = Math.imul(h, 16777619);
140
+ }
141
+ return h >>> 0;
142
+ }
143
+
144
+ /* A sala. Ruído decaindo é a forma mais barata de impulso que soa como espaço
145
+ de verdade; o pré-atraso de 20ms é o que separa "reverberação" de "borrão"
146
+ — sem ele o primeiro reflexo chega junto com a nota e engrossa o ataque. */
147
+ function _impulso(ctx, segundos, decaimento, rnd) {
148
+ var n = Math.max(1, Math.floor(ctx.sampleRate * segundos));
149
+ var buf = ctx.createBuffer(2, n, ctx.sampleRate);
150
+ for (var c = 0; c < 2; c++) {
151
+ var d = buf.getChannelData(c);
152
+ for (var i = 0; i < n; i++) {
153
+ d[i] = (rnd() * 2 - 1) * Math.pow(1 - i / n, decaimento);
154
+ }
155
+ }
156
+ return buf;
157
+ }
60
158
 
61
- function criarTrilha(cfg, aoErro) {
62
- cfg = cfg || {};
63
- var Ctx = window.AudioContext || window.webkitAudioContext;
64
- if (!Ctx) return null;
65
159
 
66
- var ctx = new Ctx();
160
+ function criarTrilha(cfg, aoErro, ctxDado) {
161
+ cfg = cfg || {};
162
+ var ctx = ctxDado;
163
+ if (!ctx) {
164
+ var Ctx = window.AudioContext || window.webkitAudioContext;
165
+ if (!Ctx) return null;
166
+ ctx = new Ctx();
167
+ }
67
168
  var mestre = ctx.createGain();
68
169
  var volumeBase = cfg.volume != null ? cfg.volume : 0.7;
69
170
  mestre.gain.value = volumeBase;
70
171
 
71
172
  var compressor = ctx.createDynamicsCompressor();
72
- compressor.threshold.value = -14;
73
- compressor.ratio.value = 4;
173
+ compressor.threshold.value = -16;
174
+ compressor.ratio.value = 3;
175
+ compressor.attack.value = 0.006;
176
+ compressor.release.value = 0.22;
74
177
  mestre.connect(compressor);
75
178
  compressor.connect(ctx.destination);
76
179
 
77
- // saída paralela: é daqui que a gravação puxa o som
78
- var paraGravar = ctx.createMediaStreamDestination();
79
- compressor.connect(paraGravar);
180
+ /* Saída paralela: é daqui que a gravação puxa o som. O contexto OFFLINE (o
181
+ do render de teste) não tem `createMediaStreamDestination`, e chamá-lo ali
182
+ derrubaria a trilha inteira num TypeError — antes de tocar uma nota. */
183
+ var paraGravar = ctx.createMediaStreamDestination ? ctx.createMediaStreamDestination() : null;
184
+ if (paraGravar) compressor.connect(paraGravar);
80
185
 
81
186
  /* Entrada da NARRAÇÃO: depois do `mestre`, antes do compressor. A voz não
82
187
  passa pelo volume da trilha nem pelo duck — quem abaixa é a música, nunca
@@ -90,7 +195,7 @@ function criarTrilha(cfg, aoErro) {
90
195
  ctx: ctx,
91
196
  no: compressor, // onde SFX e outras fontes se conectam
92
197
  entradaVoz: entradaVoz,
93
- fluxo: paraGravar.stream,
198
+ fluxo: paraGravar ? paraGravar.stream : null,
94
199
  tocando: false,
95
200
  tocar: function () {},
96
201
  parar: function () {},
@@ -109,15 +214,17 @@ function criarTrilha(cfg, aoErro) {
109
214
  },
110
215
  };
111
216
 
217
+ function retomar() {
218
+ if (ctx.state === "suspended" && ctx.resume) ctx.resume();
219
+ }
220
+
112
221
  /* -------------------------------------------------------- sem trilha ---- */
113
222
  /* tipo "nenhuma": o documento não tem música, mas o grafo ainda serve —
114
223
  é por ele que narração e SFX entram na gravação. */
115
224
  if (cfg.tipo === "nenhuma") {
116
- api.tocar = function () {
117
- if (ctx.state === "suspended") ctx.resume();
118
- api.tocando = true;
119
- };
225
+ api.tocar = function () { retomar(); api.tocando = true; };
120
226
  api.parar = function () { api.tocando = false; };
227
+ api.agendarAte = function () {};
121
228
  return api;
122
229
  }
123
230
 
@@ -141,7 +248,7 @@ function criarTrilha(cfg, aoErro) {
141
248
 
142
249
  api.elemento = el;
143
250
  api.tocar = function () {
144
- if (ctx.state === "suspended") ctx.resume();
251
+ retomar();
145
252
  el.currentTime = cfg.inicio || 0;
146
253
  var p = el.play();
147
254
  if (p && p.catch) p.catch(function () { if (aoErro) aoErro("o navegador bloqueou o áudio; clique na tela"); });
@@ -163,13 +270,56 @@ function criarTrilha(cfg, aoErro) {
163
270
  var E = ESTILOS_TRILHA[cfg.estilo] || ESTILOS_TRILHA.pulso;
164
271
  var tempoBatida = 60 / E.bpm;
165
272
  var passo = 0, proximo = 0, relogio = null;
273
+ var rnd = _sorteio(_semeadoPor(cfg.estilo || "pulso"));
274
+
275
+ /* O barramento da música existe por causa do BOMBEIO: é ele que abaixa no
276
+ bumbo. Sem um nó só para os instrumentos, o duck da narração e o bombeio
277
+ escreveriam no MESMO ganho — o segundo a agendar cancelaria o primeiro, e
278
+ a voz ficaria por baixo da música na primeira batida depois de falar. */
279
+ var corpo = ctx.createGain();
280
+ corpo.gain.value = 1;
281
+ corpo.connect(mestre);
282
+
283
+ /* A sala. O retorno entra no `mestre` (e não no compressor) de propósito:
284
+ assim a reverberação abaixa junto no duck. Cauda vazando por cima da
285
+ narração é o defeito que faz a pessoa baixar o volume da trilha inteira. */
286
+ var sala = null, envio = null;
287
+ if (E.reverb && ctx.createConvolver) {
288
+ var conv = ctx.createConvolver();
289
+ conv.buffer = _impulso(ctx, E.reverb > 0.4 ? 2.6 : 1.6, 2.4, rnd);
290
+ var atraso = ctx.createDelay ? ctx.createDelay(0.2) : null;
291
+ envio = ctx.createGain();
292
+ envio.gain.value = E.reverb;
293
+ var retorno = ctx.createGain();
294
+ retorno.gain.value = 0.9;
295
+ if (atraso) { atraso.delayTime.value = 0.02; envio.connect(atraso); atraso.connect(conv); }
296
+ else envio.connect(conv);
297
+ conv.connect(retorno);
298
+ retorno.connect(mestre);
299
+ sala = envio;
300
+ }
301
+
302
+ function panorama(quanto) {
303
+ if (!ctx.createStereoPanner) return null;
304
+ var p = ctx.createStereoPanner();
305
+ p.pan.value = Math.max(-1, Math.min(1, quanto));
306
+ return p;
307
+ }
308
+
309
+ /** Liga uma voz à saída, com posição no estéreo e mandada para a sala. */
310
+ function ligar(no, pan, mandaSala) {
311
+ var p = panorama(pan);
312
+ if (p) { no.connect(p); no = p; }
313
+ no.connect(corpo);
314
+ if (sala && mandaSala) no.connect(sala);
315
+ }
166
316
 
167
317
  function grau(n) {
168
318
  var oitava = Math.floor(n / E.escala.length);
169
319
  return E.raiz + E.escala[((n % E.escala.length) + E.escala.length) % E.escala.length] + 12 * oitava;
170
320
  }
171
321
 
172
- function nota(freq, quando, dur, onda, ganho, corte) {
322
+ function nota(freq, quando, dur, onda, ganho, corte, pan, sala2, ataque) {
173
323
  var o = ctx.createOscillator();
174
324
  var g = ctx.createGain();
175
325
  var f = ctx.createBiquadFilter();
@@ -178,84 +328,218 @@ function criarTrilha(cfg, aoErro) {
178
328
  o.type = onda;
179
329
  o.frequency.setValueAtTime(freq, quando);
180
330
  g.gain.setValueAtTime(0, quando);
181
- g.gain.linearRampToValueAtTime(ganho, quando + Math.min(0.06, dur * 0.3));
331
+ g.gain.linearRampToValueAtTime(ganho,
332
+ quando + Math.min(ataque || 0.06, dur * 0.45));
182
333
  g.gain.exponentialRampToValueAtTime(0.0001, quando + dur);
183
- o.connect(f); f.connect(g); g.connect(mestre);
334
+ o.connect(f); f.connect(g);
335
+ ligar(g, pan || 0, sala2 !== false);
184
336
  o.start(quando);
185
337
  o.stop(quando + dur + 0.05);
186
338
  }
187
339
 
188
- function pad(acorde, quando, dur) {
189
- [0, 2, 4].forEach(function (t, i) {
190
- nota(_hz(grau(acorde + t) + 12), quando, dur, E.ondaPad,
191
- E.ganhoPad * (i === 0 ? 1 : 0.7), E.corte);
340
+ /* O acorde ganhou a SÉTIMA e uma INVERSÃO por compasso.
341
+ Tríade em posição fundamental, sempre a mesma, quatro compassos seguidos,
342
+ é o som de exercício de teclado. A sétima dá cor e a inversão faz a nota
343
+ de cima andar pouco de um acorde para o outro — que é o que o ouvido lê
344
+ como "alguém tocou isso". */
345
+ function pad(acorde, quando, dur, compasso, b) {
346
+ var vozes = E.setima ? [0, 2, 4, 6] : [0, 2, 4];
347
+ var giro = compasso % vozes.length;
348
+ /* AS VOZES ENTRAM ESPALHADAS nos estilos sem bateria, e isso é o conserto
349
+ de um defeito medido: sem bateria e com o acorde inteiro entrando junto
350
+ no primeiro tempo, todo compasso desenha a MESMA rampa e a faixa volta a
351
+ ser um loop — `ambiente` e `suave` davam dois compassos distintos em
352
+ cinco. Com a entrada escalonada e um ataque diferente por compasso, o
353
+ acorde "abre" de um jeito por vez, que é como um pad de verdade soa. */
354
+ var espalha = E.bateria ? 0 : b.espalha;
355
+ vozes.forEach(function (t, i) {
356
+ var deslocada = (i + giro) % vozes.length;
357
+ var oitava = deslocada < giro ? 12 : 0; // inversão: sobe quem passou
358
+ var largura = E.largura || 0.35;
359
+ // Voz aguda mais para fora, fundamental no centro: é assim que um pad
360
+ // soa largo sem o baixo puxar para um lado.
361
+ var pan = ((i / (vozes.length - 1)) - 0.5) * 2 * largura;
362
+ var atraso = i * espalha;
363
+ nota(_hz(grau(acorde + vozes[i]) + 12 + oitava), quando + atraso,
364
+ Math.max(0.2, dur - atraso), E.ondaPad,
365
+ E.ganhoPad * (i === 0 ? 1 : 0.62) * b.brilho, E.corte, pan, true, b.ataque);
192
366
  });
193
- nota(_hz(grau(acorde) - (E.oitavaBaixo ? 12 : 0)), quando, dur, "sine", 0.11, 420);
367
+ // O baixo fica no centro e quase seco: grave em estéreo some em caixa de
368
+ // celular, e grave com reverberação vira ronco.
369
+ nota(_hz(grau(acorde) - (E.oitavaBaixo ? 12 : 0)), quando, dur, "sine",
370
+ 0.1 * b.brilho * b.forca, 420, 0, false);
371
+
372
+ /* SEM BATERIA, O BAIXO É QUEM MARCA O COMPASSO. Nos estilos de cama o pad
373
+ decai suave por quatro segundos e a silhueta de energia de um compasso é
374
+ igual à do seguinte por mais que as notas mudem — medido: `suave` dava
375
+ dois compassos distintos em cinco. Uma segunda nota grave, em lugar
376
+ diferente a cada compasso, é o que dá pé à cama sem pôr bateria nela. */
377
+ if (!E.bateria) {
378
+ var onde = quando + dur * b.apoio;
379
+ nota(_hz(grau(acorde + 2) - (E.oitavaBaixo ? 12 : 0)), onde,
380
+ dur * (1 - b.apoio), "sine", 0.062 * b.brilho * b.forca, 420, 0, false);
381
+ }
194
382
  }
195
383
 
196
- function bumbo(quando) {
384
+ /* Quanto a bateria pesa. `discreta` é o que faz `foco` ser cama e não faixa:
385
+ a batida existe para dar andamento, e não para disputar com a voz. */
386
+ var PESO = E.discreta ? 0.45 : 1;
387
+
388
+ function bumbo(quando, forca) {
389
+ forca *= PESO;
197
390
  var o = ctx.createOscillator(), g = ctx.createGain();
198
391
  o.type = "sine";
199
392
  o.frequency.setValueAtTime(150, quando);
200
393
  o.frequency.exponentialRampToValueAtTime(45, quando + 0.12);
201
- g.gain.setValueAtTime(0.32, quando);
394
+ g.gain.setValueAtTime(0.3 * forca, quando);
202
395
  g.gain.exponentialRampToValueAtTime(0.0001, quando + 0.28);
203
- o.connect(g); g.connect(mestre);
396
+ o.connect(g);
397
+ ligar(g, 0, false);
204
398
  o.start(quando); o.stop(quando + 0.3);
399
+
400
+ /* BOMBEIO. A música cede no golpe e volta em 250ms. É o que dá a
401
+ respiração de faixa produzida — e, de quebra, abre espaço para o grave
402
+ do bumbo em vez de somar tudo no mesmo instante. */
403
+ if (E.bombeia) {
404
+ corpo.gain.cancelScheduledValues(quando);
405
+ corpo.gain.setValueAtTime(1, quando);
406
+ corpo.gain.linearRampToValueAtTime(0.55, quando + 0.02);
407
+ corpo.gain.linearRampToValueAtTime(1, quando + 0.26);
408
+ }
205
409
  }
206
410
 
207
411
  var ruido = null;
208
412
  function bufferRuido() {
209
413
  if (ruido) return ruido;
210
- ruido = ctx.createBuffer(1, ctx.sampleRate * 0.4, ctx.sampleRate);
414
+ ruido = ctx.createBuffer(1, Math.floor(ctx.sampleRate * 0.4), ctx.sampleRate);
211
415
  var d = ruido.getChannelData(0);
212
- for (var i = 0; i < d.length; i++) d[i] = Math.random() * 2 - 1;
416
+ for (var i = 0; i < d.length; i++) d[i] = rnd() * 2 - 1;
213
417
  return ruido;
214
418
  }
215
419
 
216
- function chimbal(quando, forte) {
420
+ function chimbal(quando, forte, pan) {
217
421
  var s = ctx.createBufferSource(), g = ctx.createGain(), f = ctx.createBiquadFilter();
218
422
  s.buffer = bufferRuido();
219
423
  f.type = "highpass";
220
424
  f.frequency.value = forte ? 4000 : 7000;
221
- g.gain.setValueAtTime(forte ? 0.09 : 0.045, quando);
425
+ g.gain.setValueAtTime((forte ? 0.085 : 0.04) * PESO * (0.75 + rnd() * 0.5), quando);
222
426
  g.gain.exponentialRampToValueAtTime(0.0001, quando + (forte ? 0.16 : 0.055));
223
- s.connect(f); f.connect(g); g.connect(mestre);
427
+ s.connect(f); f.connect(g);
428
+ ligar(g, pan, true);
224
429
  s.start(quando); s.stop(quando + 0.25);
225
430
  }
226
431
 
227
- function caixa(quando) {
432
+ function caixa(quando, forca) {
228
433
  var s = ctx.createBufferSource(), g = ctx.createGain(), f = ctx.createBiquadFilter();
229
434
  s.buffer = bufferRuido();
230
435
  f.type = "bandpass";
231
436
  f.frequency.value = 1900;
232
- g.gain.setValueAtTime(0.13, quando);
437
+ g.gain.setValueAtTime(0.12 * forca * PESO, quando);
233
438
  g.gain.exponentialRampToValueAtTime(0.0001, quando + 0.19);
234
- s.connect(f); f.connect(g); g.connect(mestre);
439
+ s.connect(f); f.connect(g);
440
+ ligar(g, 0, true);
235
441
  s.start(quando); s.stop(quando + 0.25);
236
442
  }
237
443
 
444
+ /* Os desenhos do arpejo. Quatro, e não um: é a troca deles que faz o quarto
445
+ compasso não ser o primeiro de novo. */
446
+ var DESENHOS = [
447
+ [0, 2, 4, 2, 4, 6, 4, 2],
448
+ [4, 2, 0, 2, 4, 6, 7, 6],
449
+ [0, 4, 2, 6, 4, 2, 4, 0],
450
+ [6, 4, 6, 2, 4, 0, 2, 4],
451
+ ];
452
+
453
+ /* Uma decisão por compasso, sempre a mesma para o mesmo compasso: o pump do
454
+ áudio agenda com folga e um compasso pode ser calculado duas vezes se a
455
+ aba engasgar. Sorteando na hora, a segunda volta daria outro desenho e a
456
+ música tropeçaria em si mesma. */
457
+ /* O DESENHO RÍTMICO DO ARPEJO, compasso a compasso.
458
+ A grade fixa (uma nota a cada N divisões, sempre nas mesmas) é o que fazia
459
+ todo compasso ter a mesma silhueta de energia. Aqui uma nota em cada
460
+ quatro sai do tempo (síncope) e uma em cada sete cala — e silêncio é
461
+ desenho tanto quanto nota. */
462
+ function arpDaBarra(r, div, desenho) {
463
+ var base = 4 / (div || 2);
464
+ var mapa = new Array(16).fill(null);
465
+ var i = 0;
466
+ for (var k = 0; k < 16; k += base) {
467
+ var pos = k;
468
+ if (base > 1 && r() < 0.25) pos = Math.min(15, k + base / 2);
469
+ if (r() < 0.14) { i++; continue; }
470
+ mapa[pos] = desenho[i % desenho.length];
471
+ i++;
472
+ }
473
+ return mapa;
474
+ }
475
+
476
+ function daBarra(compasso) {
477
+ var r = _sorteio(_semeadoPor((cfg.estilo || "pulso") + ":" + compasso));
478
+ var frase = Math.floor(compasso / 4);
479
+ var desenho = DESENHOS[Math.floor(r() * DESENHOS.length) % DESENHOS.length];
480
+ return {
481
+ desenho: desenho,
482
+ arp: arpDaBarra(r, E.arpDiv, desenho),
483
+ // Quanto o acorde demora a abrir, e em quanto tempo cada voz entra.
484
+ // Só vale nos estilos sem bateria — com bateria o acorde tem de chegar
485
+ // no tempo, senão soa como erro e não como respiração.
486
+ ataque: 0.08 + r() * 0.9,
487
+ espalha: 0.05 + r() * 0.28,
488
+ // Onde cai a segunda nota do baixo nos estilos sem bateria.
489
+ apoio: 0.34 + r() * 0.4,
490
+ // O filtro anda numa onda lenta de oito compassos: é o que dá a sensação
491
+ // de que a música vai a algum lugar, sem mudar uma nota.
492
+ brilho: 0.72 + 0.42 * (0.5 + 0.5 * Math.sin((compasso / 8) * Math.PI * 2)),
493
+ // Chimbal denso alternando por frase de quatro compassos.
494
+ denso: frase % 2 === 1,
495
+ // Última barra da frase leva virada.
496
+ virada: compasso % 4 === 3,
497
+ // Um compasso em cada quatro respira sem pad novo.
498
+ respira: r() < 0.18,
499
+ // Volume geral da barra: gente não toca dois compassos na mesma força.
500
+ forca: 0.86 + r() * 0.28,
501
+ };
502
+ }
503
+
238
504
  function agendarPasso(p, quando) {
239
505
  var naBarra = p % 16;
240
506
  var compasso = Math.floor(p / 16);
241
507
  var acorde = E.progressao[compasso % E.progressao.length];
508
+ var b = daBarra(compasso);
509
+ // Humanização: alguns milissegundos fora do quadrado. Zero para o compasso
510
+ // que começa, senão o pad entra atrasado e soa como erro, não como gente.
511
+ var tremor = naBarra === 0 ? 0 : (rnd() - 0.5) * 0.016;
512
+ var t = quando + tremor;
513
+
514
+ if (naBarra === 0 && !b.respira) {
515
+ pad(acorde, quando, tempoBatida * 3.9, compasso, b);
516
+ }
242
517
 
243
- if (naBarra === 0) pad(acorde, quando, tempoBatida * 3.9);
244
-
245
- // arpejo
246
- var div = E.arpDiv || 2;
247
- if (naBarra % (4 / div) === 0) {
248
- var passoArp = Math.floor(naBarra / (4 / div));
249
- var seq = [0, 2, 4, 2, 4, 6, 4, 2];
250
- nota(_hz(grau(acorde + seq[passoArp % seq.length]) + 12),
251
- quando, tempoBatida * 0.55, E.ondaArp, E.ganhoArp, E.corte * 1.4);
518
+ // arpejo, na posição que ESTE compasso pediu
519
+ if (b.arp[naBarra] != null) {
520
+ // Ping-pong: as notas alternam de lado. Um arpejo no centro some atrás
521
+ // do pad; alternando, ele aparece sem precisar de volume.
522
+ var pan = (naBarra % 4 < 2 ? -1 : 1) * (E.largura || 0.35) * 0.8;
523
+ nota(_hz(grau(acorde + b.arp[naBarra]) + 12), t,
524
+ tempoBatida * 0.55, E.ondaArp,
525
+ E.ganhoArp * b.forca * (0.8 + rnd() * 0.4), E.corte * 1.4 * b.brilho, pan);
252
526
  }
253
527
 
254
528
  if (E.bateria) {
255
529
  var atraso = E.solto ? 0.018 : 0; // lo-fi: levada atrasada
256
- if (naBarra % 8 === 0) bumbo(quando);
257
- if (naBarra === 4 || naBarra === 12) caixa(quando + atraso);
258
- if (naBarra % 2 === 0) chimbal(quando + atraso, naBarra % 8 === 4);
530
+ if (naBarra % 8 === 0) bumbo(quando, b.forca);
531
+ // Bumbo de apoio na virada: é o empurrão para o compasso seguinte.
532
+ if (b.virada && naBarra === 14) bumbo(quando + atraso, b.forca * 0.8);
533
+ if (!E.semCaixa) {
534
+ if (naBarra === 4 || naBarra === 12) caixa(quando + atraso, b.forca);
535
+ if (b.virada && (naBarra === 13 || naBarra === 15)) {
536
+ caixa(quando + atraso, b.forca * 0.55);
537
+ }
538
+ }
539
+ var cada = b.denso ? 1 : 2;
540
+ if (naBarra % cada === 0) {
541
+ chimbal(t + atraso, naBarra % 8 === 4, ((naBarra % 4) / 3 - 0.5) * 0.5);
542
+ }
259
543
  }
260
544
  }
261
545
 
@@ -269,7 +553,7 @@ function criarTrilha(cfg, aoErro) {
269
553
 
270
554
  api.tocar = function () {
271
555
  if (api.tocando) return;
272
- if (ctx.state === "suspended") ctx.resume();
556
+ retomar();
273
557
  passo = 0;
274
558
  proximo = ctx.currentTime + 0.08;
275
559
  api.tocando = true;
@@ -287,9 +571,37 @@ function criarTrilha(cfg, aoErro) {
287
571
  mestre.gain.linearRampToValueAtTime(0.0001, agora + (segundos || 1.2));
288
572
  };
289
573
 
574
+ /* Agenda a faixa inteira de uma vez, para render offline. É o que permite
575
+ MEDIR a música num teste em vez de olhar o código e achar que variou. */
576
+ api.agendarAte = function (segundos) {
577
+ passo = 0; proximo = 0;
578
+ while (proximo < segundos) {
579
+ agendarPasso(passo, proximo);
580
+ proximo += tempoBatida / 4;
581
+ passo++;
582
+ }
583
+ };
584
+
290
585
  return api;
291
586
  }
292
587
 
588
+ /* Renderiza a trilha sem tocar, num contexto offline. Só o teste chama isto —
589
+ e é por ele que "a música varia" deixa de ser opinião. */
590
+ function renderizarTrilha(estilo, segundos, taxa) {
591
+ var Off = window.OfflineAudioContext || window.webkitOfflineAudioContext;
592
+ if (!Off) return Promise.reject(new Error("sem OfflineAudioContext"));
593
+ taxa = taxa || 44100;
594
+ var ctx = new Off(2, Math.round(taxa * segundos), taxa);
595
+ var t = criarTrilha({ tipo: "gerada", estilo: estilo, volume: 0.9 }, null, ctx);
596
+ if (!t) return Promise.reject(new Error("não montou a trilha"));
597
+ t.agendarAte(segundos);
598
+ return ctx.startRendering();
599
+ }
600
+
293
601
  if (typeof module !== "undefined" && module.exports) {
294
- module.exports = { criarTrilha: criarTrilha, ESTILOS_TRILHA: ESTILOS_TRILHA };
602
+ module.exports = {
603
+ criarTrilha: criarTrilha,
604
+ ESTILOS_TRILHA: ESTILOS_TRILHA,
605
+ nomesEstilosTrilha: nomesEstilosTrilha,
606
+ };
295
607
  }