primocode 8.42.1 → 9.0.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/README.md +41 -37
- package/bin/primocode.js +599 -327
- package/lib/acervo.js +8 -1
- package/lib/act.js +205 -114
- package/lib/api.js +35 -4
- package/lib/boas-vindas.js +31 -192
- package/lib/catalogo.js +60 -60
- package/lib/config.js +1 -1
- package/lib/conta.js +3 -24
- package/lib/diff.js +161 -0
- package/lib/diretor.js +37 -47
- package/lib/entrada.js +545 -0
- package/lib/especialistas.js +2 -2
- package/lib/estudio/assets.js +343 -0
- package/lib/estudio/identidade.js +560 -0
- package/lib/estudio/index.js +565 -0
- package/lib/estudio/plano.js +327 -0
- package/lib/estudio/projeto.js +246 -0
- package/lib/estudio/remotion.js +606 -0
- package/lib/estudio/slides.js +190 -0
- package/lib/estudio/video.js +296 -0
- package/lib/fala.js +13 -1
- package/lib/marca.js +4 -1
- package/lib/marketplace.js +189 -0
- package/lib/permissao.js +137 -0
- package/lib/registro.js +229 -0
- package/lib/skills.js +329 -0
- package/lib/tools.js +51 -181
- package/lib/ui.js +7 -3
- package/lib/voz.js +239 -0
- package/package.json +12 -3
- package/skills/acessibilidade/SKILL.md +28 -0
- package/skills/api-que-nao-quebra/SKILL.md +27 -0
- package/skills/depurar-de-verdade/SKILL.md +29 -0
- package/skills/editar-meu-video/SKILL.md +59 -0
- package/skills/escrever-em-portugues/SKILL.md +28 -0
- package/skills/git-limpo/SKILL.md +28 -0
- package/skills/motion-de-primeira/SKILL.md +62 -0
- package/skills/pagina-que-converte/SKILL.md +28 -0
- package/skills/performance-web/SKILL.md +28 -0
- package/skills/revisao-antes-de-entregar/SKILL.md +27 -0
- package/skills/slides-que-prendem/SKILL.md +40 -0
- package/studio/edge.py +16 -66
- package/studio/narrar.py +274 -0
- package/studio/pessoa.py +167 -0
- package/voz/primo_voz/__init__.py +0 -0
- package/voz/primo_voz/__main__.py +325 -0
- package/voz/primo_voz/escuta.py +235 -0
- package/voz/primo_voz/orbe.py +216 -0
- package/lib/audio-livre.js +0 -177
- package/lib/chaves.js +0 -149
- package/lib/claude-engine.js +0 -631
- package/lib/codex-engine.js +0 -208
- package/lib/remotion.js +0 -663
- package/lib/studio.js +0 -960
- package/studio/PRIMOCODE.md +0 -58
- package/studio/README.md +0 -154
- package/studio/__pycache__/edge.cpython-311.pyc +0 -0
- package/studio/__pycache__/elevenlabs.cpython-311.pyc +0 -0
- package/studio/__pycache__/voz.cpython-311.pyc +0 -0
- package/studio/arquivos/corte-teste-de-camera-fb823.json +0 -57
- package/studio/ferramentas/00-protocolo.md +0 -125
- package/studio/ferramentas/01-modelos.md +0 -270
- package/studio/ferramentas/README.md +0 -32
- package/studio/ferramentas/corte.md +0 -919
- package/studio/ferramentas/grade.md +0 -92
- package/studio/ferramentas/prisma.md +0 -107
- package/studio/ferramentas/prosa.md +0 -86
- package/studio/ferramentas/tela.md +0 -93
- package/studio/ferramentas/traco.md +0 -79
- package/studio/server.py +0 -2254
- package/studio/studio.log +0 -57
- package/studio/voz.py +0 -388
- package/studio/web/analise.html +0 -52
- package/studio/web/core.css +0 -1167
- package/studio/web/fontes/bebas.woff2 +0 -0
- package/studio/web/fontes/grotesk-700.woff2 +0 -0
- package/studio/web/fontes/inter-400.woff2 +0 -0
- package/studio/web/fontes/inter-700.woff2 +0 -0
- package/studio/web/fontes/jetbrains-400.woff2 +0 -0
- package/studio/web/fontes/playfair-700.woff2 +0 -0
- package/studio/web/fontes.css +0 -23
- package/studio/web/home.html +0 -217
- package/studio/web/js/analise.js +0 -510
- package/studio/web/js/audio.js +0 -607
- package/studio/web/js/editor.js +0 -1802
- package/studio/web/js/exportar.js +0 -222
- package/studio/web/js/ferramentas.js +0 -108
- package/studio/web/js/icones.js +0 -120
- package/studio/web/js/midia.js +0 -157
- package/studio/web/js/narracao.js +0 -325
- package/studio/web/js/player.js +0 -903
- package/studio/web/js/render-grade.js +0 -243
- package/studio/web/js/render-prosa.js +0 -104
- package/studio/web/js/render.js +0 -1852
- package/studio/web/js/sfx.js +0 -233
- package/studio/web/js/video.js +0 -464
- package/studio/web/js/voz-neural.js +0 -152
- package/studio/web/render.css +0 -1352
- package/studio/web/tf.html +0 -15
- package/studio/web/tool.html +0 -78
- package/studio/web/view.html +0 -27
package/studio/web/js/audio.js
DELETED
|
@@ -1,607 +0,0 @@
|
|
|
1
|
-
/* ==========================================================================
|
|
2
|
-
audio.js — trilha sonora do Corte.
|
|
3
|
-
|
|
4
|
-
Dois caminhos:
|
|
5
|
-
tipo "arquivo" → toca um áudio seu (upload em /midia ou URL direta)
|
|
6
|
-
tipo "gerada" → o estúdio sintetiza a música na hora, sem arquivo
|
|
7
|
-
|
|
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.
|
|
40
|
-
========================================================================== */
|
|
41
|
-
|
|
42
|
-
var ESTILOS_TRILHA = {
|
|
43
|
-
pulso: {
|
|
44
|
-
rotulo: "Pulso", bpm: 112, raiz: 45,
|
|
45
|
-
escala: [0, 2, 3, 5, 7, 8, 10],
|
|
46
|
-
progressao: [0, 5, 3, 4],
|
|
47
|
-
bateria: true, ondaPad: "sawtooth", ondaArp: "square",
|
|
48
|
-
corte: 1600, ganhoPad: 0.05, ganhoArp: 0.045, arpDiv: 2,
|
|
49
|
-
largura: 0.4, reverb: 0.22, setima: true, bombeia: true,
|
|
50
|
-
},
|
|
51
|
-
ambiente: {
|
|
52
|
-
rotulo: "Ambiente", bpm: 68, raiz: 41,
|
|
53
|
-
escala: [0, 2, 4, 7, 9],
|
|
54
|
-
progressao: [0, 3, 5, 3],
|
|
55
|
-
bateria: false, ondaPad: "sawtooth", ondaArp: "sine",
|
|
56
|
-
corte: 900, ganhoPad: 0.08, ganhoArp: 0.026, arpDiv: 1,
|
|
57
|
-
largura: 0.6, reverb: 0.5, setima: true,
|
|
58
|
-
},
|
|
59
|
-
lofi: {
|
|
60
|
-
rotulo: "Lo-fi", bpm: 76, raiz: 43,
|
|
61
|
-
escala: [0, 2, 3, 5, 7, 8, 10],
|
|
62
|
-
progressao: [0, 3, 4, 2],
|
|
63
|
-
bateria: true, ondaPad: "triangle", ondaArp: "sine",
|
|
64
|
-
corte: 1100, ganhoPad: 0.065, ganhoArp: 0.038, arpDiv: 2, solto: true,
|
|
65
|
-
largura: 0.35, reverb: 0.34, setima: true,
|
|
66
|
-
},
|
|
67
|
-
epico: {
|
|
68
|
-
rotulo: "Épico", bpm: 88, raiz: 38,
|
|
69
|
-
escala: [0, 2, 3, 5, 7, 8, 10],
|
|
70
|
-
progressao: [0, 5, 6, 4],
|
|
71
|
-
bateria: true, ondaPad: "sawtooth", ondaArp: "triangle",
|
|
72
|
-
corte: 2200, ganhoPad: 0.085, ganhoArp: 0.05, arpDiv: 2, oitavaBaixo: true,
|
|
73
|
-
largura: 0.5, reverb: 0.42, setima: true, bombeia: true,
|
|
74
|
-
},
|
|
75
|
-
alegre: {
|
|
76
|
-
rotulo: "Alegre", bpm: 124, raiz: 48,
|
|
77
|
-
escala: [0, 2, 4, 5, 7, 9, 11],
|
|
78
|
-
progressao: [0, 4, 5, 3],
|
|
79
|
-
bateria: true, ondaPad: "triangle", ondaArp: "square",
|
|
80
|
-
corte: 2600, ganhoPad: 0.045, ganhoArp: 0.04, arpDiv: 4,
|
|
81
|
-
largura: 0.45, reverb: 0.2, bombeia: true,
|
|
82
|
-
},
|
|
83
|
-
tenso: {
|
|
84
|
-
rotulo: "Tenso", bpm: 100, raiz: 36,
|
|
85
|
-
escala: [0, 1, 3, 5, 6, 8, 10],
|
|
86
|
-
progressao: [0, 0, 1, 0],
|
|
87
|
-
bateria: true, ondaPad: "sawtooth", ondaArp: "sawtooth",
|
|
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,
|
|
116
|
-
},
|
|
117
|
-
};
|
|
118
|
-
|
|
119
|
-
function nomesEstilosTrilha() { return Object.keys(ESTILOS_TRILHA); }
|
|
120
|
-
|
|
121
|
-
function _hz(midi) { return 440 * Math.pow(2, (midi - 69) / 12); }
|
|
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
|
-
}
|
|
158
|
-
|
|
159
|
-
|
|
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
|
-
}
|
|
168
|
-
var mestre = ctx.createGain();
|
|
169
|
-
var volumeBase = cfg.volume != null ? cfg.volume : 0.7;
|
|
170
|
-
mestre.gain.value = volumeBase;
|
|
171
|
-
|
|
172
|
-
var compressor = ctx.createDynamicsCompressor();
|
|
173
|
-
compressor.threshold.value = -16;
|
|
174
|
-
compressor.ratio.value = 3;
|
|
175
|
-
compressor.attack.value = 0.006;
|
|
176
|
-
compressor.release.value = 0.22;
|
|
177
|
-
mestre.connect(compressor);
|
|
178
|
-
compressor.connect(ctx.destination);
|
|
179
|
-
|
|
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);
|
|
185
|
-
|
|
186
|
-
/* Entrada da NARRAÇÃO: depois do `mestre`, antes do compressor. A voz não
|
|
187
|
-
passa pelo volume da trilha nem pelo duck — quem abaixa é a música, nunca
|
|
188
|
-
a fala. E como desemboca no mesmo compressor, a voz entra na gravação
|
|
189
|
-
MP4 pelo grafo, sem depender de "compartilhar áudio da aba". */
|
|
190
|
-
var entradaVoz = ctx.createGain();
|
|
191
|
-
entradaVoz.gain.value = 1;
|
|
192
|
-
entradaVoz.connect(compressor);
|
|
193
|
-
|
|
194
|
-
var api = {
|
|
195
|
-
ctx: ctx,
|
|
196
|
-
no: compressor, // onde SFX e outras fontes se conectam
|
|
197
|
-
entradaVoz: entradaVoz,
|
|
198
|
-
fluxo: paraGravar ? paraGravar.stream : null,
|
|
199
|
-
tocando: false,
|
|
200
|
-
tocar: function () {},
|
|
201
|
-
parar: function () {},
|
|
202
|
-
volume: function (v) { volumeBase = v; mestre.gain.value = v; },
|
|
203
|
-
esmaecer: function () {},
|
|
204
|
-
duracao: function () { return null; },
|
|
205
|
-
/* Ducking: a trilha cede espaço enquanto a narração fala.
|
|
206
|
-
Rampa curta para descer (a voz não espera) e longa para voltar. */
|
|
207
|
-
duck: function (ligado) {
|
|
208
|
-
var agora = ctx.currentTime;
|
|
209
|
-
mestre.gain.cancelScheduledValues(agora);
|
|
210
|
-
mestre.gain.setValueAtTime(mestre.gain.value, agora);
|
|
211
|
-
mestre.gain.linearRampToValueAtTime(
|
|
212
|
-
ligado ? volumeBase * 0.35 : volumeBase,
|
|
213
|
-
agora + (ligado ? 0.15 : 0.5));
|
|
214
|
-
},
|
|
215
|
-
};
|
|
216
|
-
|
|
217
|
-
function retomar() {
|
|
218
|
-
if (ctx.state === "suspended" && ctx.resume) ctx.resume();
|
|
219
|
-
}
|
|
220
|
-
|
|
221
|
-
/* -------------------------------------------------------- sem trilha ---- */
|
|
222
|
-
/* tipo "nenhuma": o documento não tem música, mas o grafo ainda serve —
|
|
223
|
-
é por ele que narração e SFX entram na gravação. */
|
|
224
|
-
if (cfg.tipo === "nenhuma") {
|
|
225
|
-
api.tocar = function () { retomar(); api.tocando = true; };
|
|
226
|
-
api.parar = function () { api.tocando = false; };
|
|
227
|
-
api.agendarAte = function () {};
|
|
228
|
-
return api;
|
|
229
|
-
}
|
|
230
|
-
|
|
231
|
-
/* --------------------------------------------------------- arquivo ---- */
|
|
232
|
-
|
|
233
|
-
if (cfg.tipo !== "gerada" && cfg.src) {
|
|
234
|
-
var el = new Audio();
|
|
235
|
-
el.crossOrigin = "anonymous";
|
|
236
|
-
el.src = cfg.src;
|
|
237
|
-
el.loop = cfg.repetir !== false;
|
|
238
|
-
el.preload = "auto";
|
|
239
|
-
el.onerror = function () { if (aoErro) aoErro("não consegui carregar o áudio"); };
|
|
240
|
-
|
|
241
|
-
var fonte = null;
|
|
242
|
-
try {
|
|
243
|
-
fonte = ctx.createMediaElementSource(el);
|
|
244
|
-
fonte.connect(mestre);
|
|
245
|
-
} catch (_) {
|
|
246
|
-
el.volume = mestre.gain.value; // sem Web Audio: toca direto mesmo
|
|
247
|
-
}
|
|
248
|
-
|
|
249
|
-
api.elemento = el;
|
|
250
|
-
api.tocar = function () {
|
|
251
|
-
retomar();
|
|
252
|
-
el.currentTime = cfg.inicio || 0;
|
|
253
|
-
var p = el.play();
|
|
254
|
-
if (p && p.catch) p.catch(function () { if (aoErro) aoErro("o navegador bloqueou o áudio; clique na tela"); });
|
|
255
|
-
api.tocando = true;
|
|
256
|
-
};
|
|
257
|
-
api.parar = function () { el.pause(); api.tocando = false; };
|
|
258
|
-
api.duracao = function () { return isFinite(el.duration) ? el.duration : null; };
|
|
259
|
-
api.esmaecer = function (segundos) {
|
|
260
|
-
var agora = ctx.currentTime;
|
|
261
|
-
mestre.gain.cancelScheduledValues(agora);
|
|
262
|
-
mestre.gain.setValueAtTime(mestre.gain.value, agora);
|
|
263
|
-
mestre.gain.linearRampToValueAtTime(0.0001, agora + (segundos || 1.2));
|
|
264
|
-
};
|
|
265
|
-
return api;
|
|
266
|
-
}
|
|
267
|
-
|
|
268
|
-
/* ---------------------------------------------------------- gerada ---- */
|
|
269
|
-
|
|
270
|
-
var E = ESTILOS_TRILHA[cfg.estilo] || ESTILOS_TRILHA.pulso;
|
|
271
|
-
var tempoBatida = 60 / E.bpm;
|
|
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
|
-
}
|
|
316
|
-
|
|
317
|
-
function grau(n) {
|
|
318
|
-
var oitava = Math.floor(n / E.escala.length);
|
|
319
|
-
return E.raiz + E.escala[((n % E.escala.length) + E.escala.length) % E.escala.length] + 12 * oitava;
|
|
320
|
-
}
|
|
321
|
-
|
|
322
|
-
function nota(freq, quando, dur, onda, ganho, corte, pan, sala2, ataque) {
|
|
323
|
-
var o = ctx.createOscillator();
|
|
324
|
-
var g = ctx.createGain();
|
|
325
|
-
var f = ctx.createBiquadFilter();
|
|
326
|
-
f.type = "lowpass";
|
|
327
|
-
f.frequency.setValueAtTime(corte || E.corte, quando);
|
|
328
|
-
o.type = onda;
|
|
329
|
-
o.frequency.setValueAtTime(freq, quando);
|
|
330
|
-
g.gain.setValueAtTime(0, quando);
|
|
331
|
-
g.gain.linearRampToValueAtTime(ganho,
|
|
332
|
-
quando + Math.min(ataque || 0.06, dur * 0.45));
|
|
333
|
-
g.gain.exponentialRampToValueAtTime(0.0001, quando + dur);
|
|
334
|
-
o.connect(f); f.connect(g);
|
|
335
|
-
ligar(g, pan || 0, sala2 !== false);
|
|
336
|
-
o.start(quando);
|
|
337
|
-
o.stop(quando + dur + 0.05);
|
|
338
|
-
}
|
|
339
|
-
|
|
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);
|
|
366
|
-
});
|
|
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
|
-
}
|
|
382
|
-
}
|
|
383
|
-
|
|
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;
|
|
390
|
-
var o = ctx.createOscillator(), g = ctx.createGain();
|
|
391
|
-
o.type = "sine";
|
|
392
|
-
o.frequency.setValueAtTime(150, quando);
|
|
393
|
-
o.frequency.exponentialRampToValueAtTime(45, quando + 0.12);
|
|
394
|
-
g.gain.setValueAtTime(0.3 * forca, quando);
|
|
395
|
-
g.gain.exponentialRampToValueAtTime(0.0001, quando + 0.28);
|
|
396
|
-
o.connect(g);
|
|
397
|
-
ligar(g, 0, false);
|
|
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
|
-
}
|
|
409
|
-
}
|
|
410
|
-
|
|
411
|
-
var ruido = null;
|
|
412
|
-
function bufferRuido() {
|
|
413
|
-
if (ruido) return ruido;
|
|
414
|
-
ruido = ctx.createBuffer(1, Math.floor(ctx.sampleRate * 0.4), ctx.sampleRate);
|
|
415
|
-
var d = ruido.getChannelData(0);
|
|
416
|
-
for (var i = 0; i < d.length; i++) d[i] = rnd() * 2 - 1;
|
|
417
|
-
return ruido;
|
|
418
|
-
}
|
|
419
|
-
|
|
420
|
-
function chimbal(quando, forte, pan) {
|
|
421
|
-
var s = ctx.createBufferSource(), g = ctx.createGain(), f = ctx.createBiquadFilter();
|
|
422
|
-
s.buffer = bufferRuido();
|
|
423
|
-
f.type = "highpass";
|
|
424
|
-
f.frequency.value = forte ? 4000 : 7000;
|
|
425
|
-
g.gain.setValueAtTime((forte ? 0.085 : 0.04) * PESO * (0.75 + rnd() * 0.5), quando);
|
|
426
|
-
g.gain.exponentialRampToValueAtTime(0.0001, quando + (forte ? 0.16 : 0.055));
|
|
427
|
-
s.connect(f); f.connect(g);
|
|
428
|
-
ligar(g, pan, true);
|
|
429
|
-
s.start(quando); s.stop(quando + 0.25);
|
|
430
|
-
}
|
|
431
|
-
|
|
432
|
-
function caixa(quando, forca) {
|
|
433
|
-
var s = ctx.createBufferSource(), g = ctx.createGain(), f = ctx.createBiquadFilter();
|
|
434
|
-
s.buffer = bufferRuido();
|
|
435
|
-
f.type = "bandpass";
|
|
436
|
-
f.frequency.value = 1900;
|
|
437
|
-
g.gain.setValueAtTime(0.12 * forca * PESO, quando);
|
|
438
|
-
g.gain.exponentialRampToValueAtTime(0.0001, quando + 0.19);
|
|
439
|
-
s.connect(f); f.connect(g);
|
|
440
|
-
ligar(g, 0, true);
|
|
441
|
-
s.start(quando); s.stop(quando + 0.25);
|
|
442
|
-
}
|
|
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
|
-
|
|
504
|
-
function agendarPasso(p, quando) {
|
|
505
|
-
var naBarra = p % 16;
|
|
506
|
-
var compasso = Math.floor(p / 16);
|
|
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
|
-
}
|
|
517
|
-
|
|
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);
|
|
526
|
-
}
|
|
527
|
-
|
|
528
|
-
if (E.bateria) {
|
|
529
|
-
var atraso = E.solto ? 0.018 : 0; // lo-fi: levada atrasada
|
|
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
|
-
}
|
|
543
|
-
}
|
|
544
|
-
}
|
|
545
|
-
|
|
546
|
-
function bombear() {
|
|
547
|
-
while (proximo < ctx.currentTime + 0.2) {
|
|
548
|
-
agendarPasso(passo, proximo);
|
|
549
|
-
proximo += tempoBatida / 4;
|
|
550
|
-
passo++;
|
|
551
|
-
}
|
|
552
|
-
}
|
|
553
|
-
|
|
554
|
-
api.tocar = function () {
|
|
555
|
-
if (api.tocando) return;
|
|
556
|
-
retomar();
|
|
557
|
-
passo = 0;
|
|
558
|
-
proximo = ctx.currentTime + 0.08;
|
|
559
|
-
api.tocando = true;
|
|
560
|
-
bombear();
|
|
561
|
-
relogio = setInterval(bombear, 25);
|
|
562
|
-
};
|
|
563
|
-
api.parar = function () {
|
|
564
|
-
api.tocando = false;
|
|
565
|
-
clearInterval(relogio);
|
|
566
|
-
};
|
|
567
|
-
api.esmaecer = function (segundos) {
|
|
568
|
-
var agora = ctx.currentTime;
|
|
569
|
-
mestre.gain.cancelScheduledValues(agora);
|
|
570
|
-
mestre.gain.setValueAtTime(mestre.gain.value, agora);
|
|
571
|
-
mestre.gain.linearRampToValueAtTime(0.0001, agora + (segundos || 1.2));
|
|
572
|
-
};
|
|
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
|
-
|
|
585
|
-
return api;
|
|
586
|
-
}
|
|
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
|
-
|
|
601
|
-
if (typeof module !== "undefined" && module.exports) {
|
|
602
|
-
module.exports = {
|
|
603
|
-
criarTrilha: criarTrilha,
|
|
604
|
-
ESTILOS_TRILHA: ESTILOS_TRILHA,
|
|
605
|
-
nomesEstilosTrilha: nomesEstilosTrilha,
|
|
606
|
-
};
|
|
607
|
-
}
|