249 lines
7.5 KiB
JavaScript
249 lines
7.5 KiB
JavaScript
/**
|
|
* audio-robot.js — Sintetizador procedural de efeitos sonoros robóticos (Web Audio API)
|
|
* Gera bips, assobios, trinados alegres e sons estilo WALL-E / Eilik diretamente no navegador.
|
|
* Sem arquivos de áudio externos, sem dependências, 100% autônomo e instantâneo.
|
|
*/
|
|
(function(window) {
|
|
'use strict';
|
|
|
|
let audioCtx = null;
|
|
let enabled = localStorage.getItem('carinha_sfx') !== 'false';
|
|
let volume = parseFloat(localStorage.getItem('carinha_volume') || '0.7');
|
|
|
|
function getContext() {
|
|
if (!audioCtx) {
|
|
const AudioContext = window.AudioContext || window.webkitAudioContext;
|
|
if (AudioContext) {
|
|
audioCtx = new AudioContext();
|
|
}
|
|
}
|
|
if (audioCtx && audioCtx.state === 'suspended') {
|
|
audioCtx.resume();
|
|
}
|
|
return audioCtx;
|
|
}
|
|
|
|
const RobotAudio = {
|
|
get enabled() { return enabled; },
|
|
set enabled(val) {
|
|
enabled = Boolean(val);
|
|
localStorage.setItem('carinha_sfx', enabled ? 'true' : 'false');
|
|
window.dispatchEvent(new CustomEvent('carinha:audio-toggle', { detail: { enabled } }));
|
|
},
|
|
|
|
get volume() { return volume; },
|
|
set volume(val) {
|
|
volume = Math.max(0, Math.min(1, parseFloat(val) || 0.7));
|
|
localStorage.setItem('carinha_volume', volume.toString());
|
|
},
|
|
|
|
toggle() {
|
|
this.enabled = !this.enabled;
|
|
if (this.enabled) this.bleep();
|
|
return this.enabled;
|
|
},
|
|
|
|
// Bip simples de confirmação / clique
|
|
click() {
|
|
if (!enabled) return;
|
|
const ctx = getContext();
|
|
if (!ctx) return;
|
|
const osc = ctx.createOscillator();
|
|
const gain = ctx.createGain();
|
|
const now = ctx.currentTime;
|
|
osc.type = 'sine';
|
|
osc.frequency.setValueAtTime(800, now);
|
|
osc.frequency.exponentialRampToValueAtTime(1400, now + 0.04);
|
|
gain.gain.setValueAtTime(volume * 0.15, now);
|
|
gain.gain.exponentialRampToValueAtTime(0.001, now + 0.05);
|
|
osc.connect(gain);
|
|
gain.connect(ctx.destination);
|
|
osc.start(now);
|
|
osc.stop(now + 0.05);
|
|
},
|
|
|
|
// Bip robótico positivo
|
|
bleep() {
|
|
if (!enabled) return;
|
|
const ctx = getContext();
|
|
if (!ctx) return;
|
|
const osc = ctx.createOscillator();
|
|
const gain = ctx.createGain();
|
|
const now = ctx.currentTime;
|
|
osc.type = 'sine';
|
|
osc.frequency.setValueAtTime(659.25, now); // E5
|
|
osc.frequency.setValueAtTime(880, now + 0.06); // A5
|
|
gain.gain.setValueAtTime(volume * 0.25, now);
|
|
gain.gain.exponentialRampToValueAtTime(0.001, now + 0.16);
|
|
osc.connect(gain);
|
|
gain.connect(ctx.destination);
|
|
osc.start(now);
|
|
osc.stop(now + 0.16);
|
|
},
|
|
|
|
// Trinado alegre (estilo WALL-E / Eilik feliz)
|
|
happy() {
|
|
if (!enabled) return;
|
|
const ctx = getContext();
|
|
if (!ctx) return;
|
|
const notes = [523.25, 659.25, 783.99, 1046.50]; // C5, E5, G5, C6
|
|
const now = ctx.currentTime;
|
|
notes.forEach((freq, idx) => {
|
|
const osc = ctx.createOscillator();
|
|
const gain = ctx.createGain();
|
|
const start = now + idx * 0.07;
|
|
osc.type = 'triangle';
|
|
osc.frequency.setValueAtTime(freq, start);
|
|
osc.frequency.exponentialRampToValueAtTime(freq * 1.08, start + 0.06);
|
|
gain.gain.setValueAtTime(0.001, start);
|
|
gain.gain.linearRampToValueAtTime(volume * 0.3, start + 0.02);
|
|
gain.gain.exponentialRampToValueAtTime(0.001, start + 0.1);
|
|
osc.connect(gain);
|
|
gain.connect(ctx.destination);
|
|
osc.start(start);
|
|
osc.stop(start + 0.1);
|
|
});
|
|
},
|
|
|
|
// Curiosidade / interrogação
|
|
curious() {
|
|
if (!enabled) return;
|
|
const ctx = getContext();
|
|
if (!ctx) return;
|
|
const osc = ctx.createOscillator();
|
|
const gain = ctx.createGain();
|
|
const now = ctx.currentTime;
|
|
osc.type = 'sine';
|
|
osc.frequency.setValueAtTime(440, now);
|
|
osc.frequency.exponentialRampToValueAtTime(950, now + 0.22);
|
|
gain.gain.setValueAtTime(volume * 0.22, now);
|
|
gain.gain.exponentialRampToValueAtTime(0.001, now + 0.25);
|
|
osc.connect(gain);
|
|
gain.connect(ctx.destination);
|
|
osc.start(now);
|
|
osc.stop(now + 0.25);
|
|
},
|
|
|
|
// Surpreso / Alerta
|
|
surprise() {
|
|
if (!enabled) return;
|
|
const ctx = getContext();
|
|
if (!ctx) return;
|
|
const osc = ctx.createOscillator();
|
|
const gain = ctx.createGain();
|
|
const now = ctx.currentTime;
|
|
osc.type = 'square';
|
|
osc.frequency.setValueAtTime(800, now);
|
|
osc.frequency.linearRampToValueAtTime(1400, now + 0.08);
|
|
osc.frequency.linearRampToValueAtTime(1800, now + 0.15);
|
|
gain.gain.setValueAtTime(volume * 0.2, now);
|
|
gain.gain.exponentialRampToValueAtTime(0.001, now + 0.18);
|
|
osc.connect(gain);
|
|
gain.connect(ctx.destination);
|
|
osc.start(now);
|
|
osc.stop(now + 0.18);
|
|
},
|
|
|
|
// Triste / Melancólico
|
|
sad() {
|
|
if (!enabled) return;
|
|
const ctx = getContext();
|
|
if (!ctx) return;
|
|
const osc = ctx.createOscillator();
|
|
const gain = ctx.createGain();
|
|
const now = ctx.currentTime;
|
|
osc.type = 'sine';
|
|
osc.frequency.setValueAtTime(600, now);
|
|
osc.frequency.linearRampToValueAtTime(320, now + 0.35);
|
|
gain.gain.setValueAtTime(volume * 0.2, now);
|
|
gain.gain.exponentialRampToValueAtTime(0.001, now + 0.4);
|
|
osc.connect(gain);
|
|
gain.connect(ctx.destination);
|
|
osc.start(now);
|
|
osc.stop(now + 0.4);
|
|
},
|
|
|
|
// Acordando (Power on)
|
|
wake() {
|
|
if (!enabled) return;
|
|
const ctx = getContext();
|
|
if (!ctx) return;
|
|
const freqs = [350, 440, 587.33, 880];
|
|
const now = ctx.currentTime;
|
|
freqs.forEach((f, i) => {
|
|
const osc = ctx.createOscillator();
|
|
const gain = ctx.createGain();
|
|
const t = now + i * 0.06;
|
|
osc.type = 'triangle';
|
|
osc.frequency.setValueAtTime(f, t);
|
|
gain.gain.setValueAtTime(volume * 0.25, t);
|
|
gain.gain.exponentialRampToValueAtTime(0.001, t + 0.12);
|
|
osc.connect(gain);
|
|
gain.connect(ctx.destination);
|
|
osc.start(t);
|
|
osc.stop(t + 0.12);
|
|
});
|
|
},
|
|
|
|
// Dormindo (Power down)
|
|
sleep() {
|
|
if (!enabled) return;
|
|
const ctx = getContext();
|
|
if (!ctx) return;
|
|
const freqs = [700, 520, 380, 220];
|
|
const now = ctx.currentTime;
|
|
freqs.forEach((f, i) => {
|
|
const osc = ctx.createOscillator();
|
|
const gain = ctx.createGain();
|
|
const t = now + i * 0.08;
|
|
osc.type = 'sine';
|
|
osc.frequency.setValueAtTime(f, t);
|
|
gain.gain.setValueAtTime(volume * 0.2, t);
|
|
gain.gain.exponentialRampToValueAtTime(0.001, t + 0.14);
|
|
osc.connect(gain);
|
|
gain.connect(ctx.destination);
|
|
osc.start(t);
|
|
osc.stop(t + 0.14);
|
|
});
|
|
},
|
|
|
|
// Tocar som de acordo com o gesto/emoção
|
|
tocarGesto(gesto) {
|
|
if (!enabled) return;
|
|
switch ((gesto || '').toLowerCase()) {
|
|
case 'feliz':
|
|
case 'alegre':
|
|
case 'coracao':
|
|
this.happy();
|
|
break;
|
|
case 'surpreso':
|
|
this.surprise();
|
|
break;
|
|
case 'triste':
|
|
this.sad();
|
|
break;
|
|
case 'dormir':
|
|
this.sleep();
|
|
break;
|
|
case 'acordar':
|
|
this.wake();
|
|
break;
|
|
case 'olhar_esq':
|
|
case 'olhar_dir':
|
|
case 'olhar_baixo':
|
|
this.curious();
|
|
break;
|
|
case 'piscar':
|
|
case 'piscar2':
|
|
case 'piscadinha':
|
|
this.click();
|
|
break;
|
|
default:
|
|
this.bleep();
|
|
}
|
|
}
|
|
};
|
|
|
|
window.RobotAudio = RobotAudio;
|
|
})(window);
|