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.
- package/lib/act.js +68 -0
- package/lib/browser.js +292 -1
- package/lib/catalogo.js +11 -2
- package/lib/claude-engine.js +25 -2
- package/lib/codex-engine.js +9 -2
- package/lib/pipeline.js +447 -0
- package/lib/studio.js +16 -3
- package/lib/tools.js +2 -1
- package/package.json +2 -2
- package/studio/ferramentas/corte.md +73 -10
- package/studio/server.py +78 -4
- package/studio/web/js/analise.js +319 -11
- package/studio/web/js/audio.js +367 -55
- package/studio/web/js/render.js +109 -2
- package/studio/web/js/video.js +4 -0
package/studio/web/js/audio.js
CHANGED
|
@@ -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.
|
|
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.
|
|
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.
|
|
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.
|
|
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.
|
|
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.
|
|
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
|
-
|
|
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 = -
|
|
73
|
-
compressor.ratio.value =
|
|
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
|
-
|
|
78
|
-
|
|
79
|
-
|
|
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
|
-
|
|
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,
|
|
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);
|
|
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
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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.
|
|
394
|
+
g.gain.setValueAtTime(0.3 * forca, quando);
|
|
202
395
|
g.gain.exponentialRampToValueAtTime(0.0001, quando + 0.28);
|
|
203
|
-
o.connect(g);
|
|
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] =
|
|
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.
|
|
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);
|
|
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.
|
|
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);
|
|
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
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
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
|
-
|
|
258
|
-
if (
|
|
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
|
-
|
|
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 = {
|
|
602
|
+
module.exports = {
|
|
603
|
+
criarTrilha: criarTrilha,
|
|
604
|
+
ESTILOS_TRILHA: ESTILOS_TRILHA,
|
|
605
|
+
nomesEstilosTrilha: nomesEstilosTrilha,
|
|
606
|
+
};
|
|
295
607
|
}
|