Files
julioandClaude Opus 5.5 e0dd03411d fix(voz): tira o timbre de sapo; voz aguda, limpa e com brilho metalico
O coaxar vinha do zumbido de 72 Hz, da ressonancia grave (~230 Hz), da
oitava de baixo e do FM desalinhado. Agora: ressonancia de lata fina
(~850 Hz), FM harmonico leve, realce em 3,2 kHz, oitava de baixo quase
nula e tom 25% acima. Bravo vira bipes zangados e pensar vira 'hmm?' com
bipinhos; nenhum som tem energia relevante abaixo de 250 Hz.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-06 03:02:14 -03:00

189 lines
11 KiB
JavaScript

/* voz-robo.js — voz sintetizada do Carinha, no estilo dos robozinhos de cinema.
*
* Tudo é gerado na hora com Web Audio: nenhuma gravação, nenhum arquivo de terceiros.
* Cada som é uma sequência de "sílabas": um oscilador que desliza de um tom a outro, com vibrato,
* passando por um filtro de formante (a "vogal": o→i faz o "uou"). Motores das mãos viram ruído
* filtrado, e a resposta da conversa vira um balbucio que sobe no fim quando é pergunta.
*/
(function () {
'use strict';
let ruido = null; // buffer de ruído reaproveitado
const TOM = 1.25; // todas as sílabas 25% acima do tom escrito
const saidas = new WeakMap(); // AudioContext → nó de saída (compressor)
/*
* Cadeia de "brilho metálico" (criada uma vez por AudioContext):
* voz → ressonância de lata fina (agudo, ~850 Hz) → saturação leve → compressor → saída
* Nada grave: zumbido e ressonância graves deixavam a voz com cara de sapo.
*/
function saida(c) {
if (saidas.has(c)) return saidas.get(c);
const entrada = c.createGain();
const seca = c.createGain(); seca.gain.value = 0.8;
const atraso = c.createDelay(0.05); atraso.delayTime.value = 0.00118;
const volta = c.createGain(); volta.gain.value = 0.42;
const molhada = c.createGain(); molhada.gain.value = 0.35;
entrada.connect(seca);
entrada.connect(atraso); atraso.connect(volta); volta.connect(atraso); atraso.connect(molhada);
const satura = c.createWaveShaper();
const n = 2048, curva = new Float32Array(n);
for (let i = 0; i < n; i++) { const x = (i / (n - 1)) * 2 - 1; curva[i] = Math.tanh(x * 1.3); }
satura.curve = curva;
seca.connect(satura); molhada.connect(satura);
const brilho = c.createBiquadFilter(); // realce leve nos agudos: "metal" limpo
brilho.type = 'peaking'; brilho.frequency.value = 3200; brilho.Q.value = 1.2; brilho.gain.value = 4;
const comp = c.createDynamicsCompressor();
comp.threshold.value = -16; comp.ratio.value = 4; comp.attack.value = 0.003; comp.release.value = 0.15;
const passaBaixa = c.createBiquadFilter();
passaBaixa.type = 'lowpass'; passaBaixa.frequency.value = 8000;
const volume = c.createGain(); volume.gain.value = 0.8;
satura.connect(brilho); brilho.connect(comp); comp.connect(passaBaixa); passaBaixa.connect(volume); volume.connect(c.destination);
saidas.set(c, entrada);
return entrada;
}
/**
* Uma sílaba. de/para: tom em Hz; vogal: [formante inicial, final] em Hz;
* vib: [Hz, profundidade em Hz]; forma: tipo de onda.
*/
function silaba(c, t0, dur, de, para, vol, op) {
op = op || {};
vol *= 2.4; // compensa a energia que o filtro de formante tira
de *= TOM; para *= TOM; // voz de robozinho: aguda
const osc = c.createOscillator();
osc.type = op.forma || 'sawtooth';
osc.frequency.setValueAtTime(de, t0);
osc.frequency.exponentialRampToValueAtTime(Math.max(20, para), t0 + dur);
if (op.vib) {
const lfo = c.createOscillator(), prof = c.createGain();
lfo.frequency.value = op.vib[0]; prof.gain.value = op.vib[1];
lfo.connect(prof); prof.connect(osc.frequency);
lfo.start(t0); lfo.stop(t0 + dur + 0.05);
}
// FM harmônico e leve: brilho de metal sem aspereza
const mod = c.createOscillator(), modProf = c.createGain();
mod.type = 'sine';
mod.frequency.setValueAtTime(de * 2, t0);
mod.frequency.exponentialRampToValueAtTime(Math.max(20, para * 2), t0 + dur);
modProf.gain.setValueAtTime(de * (op.metal ?? 0.22), t0);
modProf.gain.exponentialRampToValueAtTime(Math.max(1, para * (op.metal ?? 0.22)), t0 + dur);
mod.connect(modProf); modProf.connect(osc.frequency);
mod.start(t0); mod.stop(t0 + dur + 0.05);
// corpo: uma senoide uma oitava abaixo deixa a voz menos "apito"
const sub = c.createOscillator();
sub.type = 'sine';
sub.frequency.setValueAtTime(de / 2, t0);
sub.frequency.exponentialRampToValueAtTime(Math.max(20, para / 2), t0 + dur);
const vogal = op.vogal || [900, 1500];
const filtro = c.createBiquadFilter();
filtro.type = 'bandpass'; filtro.Q.value = op.q || 5;
filtro.frequency.setValueAtTime(vogal[0], t0);
filtro.frequency.exponentialRampToValueAtTime(vogal[1], t0 + dur);
const g = c.createGain(), gs = c.createGain();
const ataque = Math.min(0.02, dur * 0.25);
g.gain.setValueAtTime(0.0001, t0);
g.gain.exponentialRampToValueAtTime(vol, t0 + ataque);
g.gain.setValueAtTime(vol, t0 + dur * 0.7);
g.gain.exponentialRampToValueAtTime(0.0001, t0 + dur);
gs.gain.value = 0.12; // pouca oitava de baixo: grave demais vira coaxar
osc.connect(filtro); sub.connect(gs); gs.connect(filtro); filtro.connect(g); g.connect(saida(c));
osc.start(t0); sub.start(t0);
osc.stop(t0 + dur + 0.05); sub.stop(t0 + dur + 0.05);
}
function bufferRuido(c) {
if (ruido && ruido.sampleRate === c.sampleRate) return ruido;
ruido = c.createBuffer(1, c.sampleRate, c.sampleRate);
const d = ruido.getChannelData(0);
for (let i = 0; i < d.length; i++) d[i] = Math.random() * 2 - 1;
return ruido;
}
/** Motorzinho: ruído filtrado que sobe de tom, com um tremor mecânico. */
function servo(c, t0, dur, vol, sobe) {
const fonte = c.createBufferSource();
fonte.buffer = bufferRuido(c); fonte.loop = true;
const f = c.createBiquadFilter();
f.type = 'bandpass'; f.Q.value = 9;
f.frequency.setValueAtTime(sobe === false ? 1500 : 500, t0);
f.frequency.exponentialRampToValueAtTime(sobe === false ? 500 : 1500, t0 + dur);
const g = c.createGain(), trem = c.createOscillator(), tg = c.createGain();
trem.frequency.value = 38; tg.gain.value = vol * 0.35;
trem.connect(tg); tg.connect(g.gain);
g.gain.setValueAtTime(0.0001, t0);
g.gain.exponentialRampToValueAtTime(vol * 0.6, t0 + 0.05);
g.gain.setValueAtTime(vol * 0.6, t0 + dur - 0.06);
g.gain.exponentialRampToValueAtTime(0.0001, t0 + dur);
fonte.connect(f); f.connect(g); g.connect(saida(c));
fonte.start(t0); trem.start(t0);
fonte.stop(t0 + dur + 0.05); trem.stop(t0 + dur + 0.05);
}
function clique(c, t0, vol) {
const fonte = c.createBufferSource();
fonte.buffer = bufferRuido(c);
const f = c.createBiquadFilter(); f.type = 'highpass'; f.frequency.value = 3000;
const g = c.createGain();
g.gain.setValueAtTime(vol * 0.5, t0); g.gain.exponentialRampToValueAtTime(0.0001, t0 + 0.03);
fonte.connect(f); f.connect(g); g.connect(saida(c));
fonte.start(t0); fonte.stop(t0 + 0.05);
}
const O = [500, 900], I = [1800, 2400], U = [350, 700], A = [900, 1300]; // vogais (formantes)
// Cada som: (c, t, v) → agenda as sílabas. Volumes relativos ao volume do app.
const SONS = {
feliz: (c, t, v) => { silaba(c, t, 0.12, 620, 900, v, { vogal: [1200, 2000] }); silaba(c, t + 0.13, 0.18, 900, 1250, v, { vogal: I, vib: [9, 18] }); },
alegre: (c, t, v) => [560, 700, 860, 1100].forEach((f, i) => silaba(c, t + i * 0.085, 0.09, f, f * 1.25, v * 0.85, { vogal: I, forma: 'square' })),
coracao: (c, t, v) => { silaba(c, t, 0.55, 760, 560, v, { vogal: [800, 1300], vib: [7, 28] }); },
piscadinha: (c, t, v) => { clique(c, t, v); silaba(c, t + 0.04, 0.08, 1000, 1300, v * 0.6, { forma: 'square', vogal: I }); },
surpreso: (c, t, v) => { silaba(c, t, 0.22, 260, 1100, v, { vogal: [450, 2000] }); silaba(c, t + 0.22, 0.12, 1100, 820, v * 0.8, { vogal: I }); },
bravo: (c, t, v) => [0, 0.13, 0.26].forEach((d, i) => silaba(c, t + d, 0.1, 480 - i * 30, 380 - i * 30, v * 0.85, { forma: 'square', vogal: [1300, 900] })), // bipes zangados
triste: (c, t, v) => { silaba(c, t, 0.65, 640, 380, v * 0.85, { vogal: [1200, 750], vib: [5, 14] }); },
dormir: (c, t, v) => { silaba(c, t, 0.35, 460, 760, v * 0.7, { vogal: [900, 1500] }); silaba(c, t + 0.36, 0.75, 760, 340, v * 0.6, { vogal: [1500, 700], vib: [4, 8] }); },
acordar: (c, t, v) => { servo(c, t, 0.25, v * 0.5); [300, 450, 700].forEach((f, i) => silaba(c, t + 0.22 + i * 0.1, 0.1, f, f * 1.2, v * 0.8, { vogal: I, forma: 'square' })); silaba(c, t + 0.56, 0.2, 700, 1050, v, { vogal: [1200, 2200], vib: [8, 15] }); },
acenar: (c, t, v) => { servo(c, t, 0.3, v * 0.45); silaba(c, t + 0.28, 0.13, 520, 820, v, { vogal: [1000, 1900] }); silaba(c, t + 0.44, 0.2, 760, 1080, v, { vogal: I, vib: [9, 20] }); },
esconder: (c, t, v) => { servo(c, t, 0.45, v * 0.5); silaba(c, t + 0.1, 0.08, 950, 1350, v * 0.8, { vogal: I }); for (let i = 0; i < 5; i++) silaba(c, t + 0.6 + i * 0.09, 0.05, 1200 + (i % 2) * 180, 1300, v * 0.35, { forma: 'square', vogal: I }); servo(c, t + 2.05, 0.4, v * 0.45, false); },
espiar: (c, t, v) => { servo(c, t, 0.45, v * 0.5); silaba(c, t + 1.0, 0.3, 480, 760, v * 0.9, { vogal: [600, 1700] }); silaba(c, t + 2.0, 0.16, 800, 1150, v, { vogal: I }); },
pensar: (c, t, v) => { servo(c, t, 0.35, v * 0.4); silaba(c, t + 0.35, 0.5, 520, 640, v * 0.7, { vogal: [900, 1600], vib: [5, 10] }); [0.95, 1.12].forEach((d) => silaba(c, t + d, 0.05, 1500, 1600, v * 0.35, { forma: 'square', vogal: I })); }, // "hmm?" e bipinhos
musica: (c, t, v) => [523, 659, 784, 659, 880, 784, 659, 1047].forEach((f, i) => silaba(c, t + i * 0.26, 0.2, f, f * 1.02, v * 0.6, { forma: 'triangle', vogal: A, vib: [6, 5] })),
};
// Gerador previsível: a mesma frase sempre "soa" igual.
function semente(texto) {
let h = 2166136261;
for (let i = 0; i < texto.length; i++) h = Math.imul(h ^ texto.charCodeAt(i), 16777619);
return () => { h = Math.imul(h ^ (h >>> 15), 2246822507); h = Math.imul(h ^ (h >>> 13), 3266489909); return ((h ^= h >>> 16) >>> 0) / 4294967296; };
}
/** Balbucio de robô para uma resposta: ~1 sílaba a cada 6 letras, no máximo 16. */
function balbuciar(c, texto, v) {
texto = String(texto || '').trim();
if (!texto) return 0;
const rnd = semente(texto), n = Math.max(2, Math.min(16, Math.ceil(texto.length / 6)));
const pergunta = /\?\s*$/.test(texto), exclama = /!\s*$/.test(texto);
const vogais = [O, I, A, U];
let t = c.currentTime + 0.03, tom = 520 + rnd() * 180;
for (let i = 0; i < n; i++) {
const ultima = i === n - 1;
const dur = 0.07 + rnd() * 0.1;
let para = tom * (0.8 + rnd() * 0.5);
if (ultima) para = pergunta ? tom * 1.6 : exclama ? tom * 1.35 : tom * 0.7;
silaba(c, t, ultima ? dur + 0.08 : dur, tom, para, v * (0.65 + rnd() * 0.3), { vogal: vogais[Math.floor(rnd() * 4)], vib: rnd() < 0.3 ? [8, 14] : null, forma: rnd() < 0.25 ? 'square' : 'sawtooth' });
t += dur + 0.025 + rnd() * 0.05;
tom = Math.max(330, Math.min(1000, para * (0.9 + rnd() * 0.25)));
}
return t - c.currentTime;
}
function tocar(c, nome, v) {
const f = SONS[nome];
if (!f || !c) return false;
f(c, c.currentTime + 0.02, v);
return true;
}
window.VozRobo = { tocar, balbuciar, servo: (c, dur, v) => servo(c, c.currentTime + 0.01, dur, v), sons: Object.keys(SONS) };
})();