diff --git a/android/app/assets/voz-robo.js b/android/app/assets/voz-robo.js index cb2b353..67a1c17 100644 --- a/android/app/assets/voz-robo.js +++ b/android/app/assets/voz-robo.js @@ -11,15 +11,45 @@ let ruido = null; // buffer de ruído reaproveitado const saidas = new WeakMap(); // AudioContext → nó de saída (compressor) + /* + * Cadeia de "garganta metálica" (criada uma vez por AudioContext): + * voz → [seca + modulação em anel] → ressonância de lata → degraus digitais → compressor → saída + */ function saida(c) { if (saidas.has(c)) return saidas.get(c); + const entrada = c.createGain(); + // modulação em anel: multiplica a voz por um zumbido grave → timbre mecânico + const seca = c.createGain(); seca.gain.value = 0.55; + const anel = c.createGain(); anel.gain.value = 0; + const portadora = c.createOscillator(); portadora.frequency.value = 72; + const profundidade = c.createGain(); profundidade.gain.value = 0.9; + portadora.connect(profundidade); profundidade.connect(anel.gain); portadora.start(); + const mixAnel = c.createGain(); mixAnel.gain.value = 0.75; + entrada.connect(seca); entrada.connect(anel); anel.connect(mixAnel); + // ressonância de lata: atraso curtíssimo realimentado (pente de harmônicos) + const lata = c.createGain(); + const atraso = c.createDelay(0.05); atraso.delayTime.value = 0.0043; + const volta = c.createGain(); volta.gain.value = 0.58; + const molhada = c.createGain(); molhada.gain.value = 0.55; + seca.connect(lata); mixAnel.connect(lata); + lata.connect(atraso); atraso.connect(volta); volta.connect(atraso); atraso.connect(molhada); + // degraus digitais leves + saturação suave + const degraus = c.createWaveShaper(); + const n = 2048, curva = new Float32Array(n), niveis = 28; + for (let i = 0; i < n; i++) { + const x = (i / (n - 1)) * 2 - 1; + curva[i] = Math.tanh(Math.round(x * niveis) / niveis * 1.6); + } + degraus.curve = curva; + lata.connect(degraus); molhada.connect(degraus); 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 = 7000; // tira o "chiado" de osciladores crus - comp.connect(passaBaixa); passaBaixa.connect(c.destination); - saidas.set(c, comp); - return comp; + passaBaixa.type = 'lowpass'; passaBaixa.frequency.value = 6500; + const volume = c.createGain(); volume.gain.value = 0.7; // a cadeia metálica soma energia + degraus.connect(comp); comp.connect(passaBaixa); passaBaixa.connect(volume); volume.connect(c.destination); + saidas.set(c, entrada); + return entrada; } /** @@ -39,6 +69,15 @@ lfo.connect(prof); prof.connect(osc.frequency); lfo.start(t0); lfo.stop(t0 + dur + 0.05); } + // FM com razão não inteira: harmônicos desalinhados soam como metal + const mod = c.createOscillator(), modProf = c.createGain(); + mod.type = 'sine'; + mod.frequency.setValueAtTime(de * 1.41, t0); + mod.frequency.exponentialRampToValueAtTime(Math.max(20, para * 1.41), t0 + dur); + modProf.gain.setValueAtTime(de * (op.metal ?? 0.55), t0); + modProf.gain.exponentialRampToValueAtTime(Math.max(1, para * (op.metal ?? 0.55)), 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'; diff --git a/voz-robo.js b/voz-robo.js index cb2b353..67a1c17 100644 --- a/voz-robo.js +++ b/voz-robo.js @@ -11,15 +11,45 @@ let ruido = null; // buffer de ruído reaproveitado const saidas = new WeakMap(); // AudioContext → nó de saída (compressor) + /* + * Cadeia de "garganta metálica" (criada uma vez por AudioContext): + * voz → [seca + modulação em anel] → ressonância de lata → degraus digitais → compressor → saída + */ function saida(c) { if (saidas.has(c)) return saidas.get(c); + const entrada = c.createGain(); + // modulação em anel: multiplica a voz por um zumbido grave → timbre mecânico + const seca = c.createGain(); seca.gain.value = 0.55; + const anel = c.createGain(); anel.gain.value = 0; + const portadora = c.createOscillator(); portadora.frequency.value = 72; + const profundidade = c.createGain(); profundidade.gain.value = 0.9; + portadora.connect(profundidade); profundidade.connect(anel.gain); portadora.start(); + const mixAnel = c.createGain(); mixAnel.gain.value = 0.75; + entrada.connect(seca); entrada.connect(anel); anel.connect(mixAnel); + // ressonância de lata: atraso curtíssimo realimentado (pente de harmônicos) + const lata = c.createGain(); + const atraso = c.createDelay(0.05); atraso.delayTime.value = 0.0043; + const volta = c.createGain(); volta.gain.value = 0.58; + const molhada = c.createGain(); molhada.gain.value = 0.55; + seca.connect(lata); mixAnel.connect(lata); + lata.connect(atraso); atraso.connect(volta); volta.connect(atraso); atraso.connect(molhada); + // degraus digitais leves + saturação suave + const degraus = c.createWaveShaper(); + const n = 2048, curva = new Float32Array(n), niveis = 28; + for (let i = 0; i < n; i++) { + const x = (i / (n - 1)) * 2 - 1; + curva[i] = Math.tanh(Math.round(x * niveis) / niveis * 1.6); + } + degraus.curve = curva; + lata.connect(degraus); molhada.connect(degraus); 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 = 7000; // tira o "chiado" de osciladores crus - comp.connect(passaBaixa); passaBaixa.connect(c.destination); - saidas.set(c, comp); - return comp; + passaBaixa.type = 'lowpass'; passaBaixa.frequency.value = 6500; + const volume = c.createGain(); volume.gain.value = 0.7; // a cadeia metálica soma energia + degraus.connect(comp); comp.connect(passaBaixa); passaBaixa.connect(volume); volume.connect(c.destination); + saidas.set(c, entrada); + return entrada; } /** @@ -39,6 +69,15 @@ lfo.connect(prof); prof.connect(osc.frequency); lfo.start(t0); lfo.stop(t0 + dur + 0.05); } + // FM com razão não inteira: harmônicos desalinhados soam como metal + const mod = c.createOscillator(), modProf = c.createGain(); + mod.type = 'sine'; + mod.frequency.setValueAtTime(de * 1.41, t0); + mod.frequency.exponentialRampToValueAtTime(Math.max(20, para * 1.41), t0 + dur); + modProf.gain.setValueAtTime(de * (op.metal ?? 0.55), t0); + modProf.gain.exponentialRampToValueAtTime(Math.max(1, para * (op.metal ?? 0.55)), 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';