@bravophone/webphone 0.1.2 → 0.2.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.
Files changed (84) hide show
  1. package/README.md +49 -3
  2. package/dist/bravophone.mjs +358 -287
  3. package/dist/bravophone.umd.js +43 -7
  4. package/host/_locales/pt_BR/messages.json +1430 -0
  5. package/host/allowed-origins.json +8 -0
  6. package/host/css/bravophoneContactDialer.css +385 -0
  7. package/host/css/dark-theme.css +116 -0
  8. package/host/favicon.ico +0 -0
  9. package/host/fonts/Audiowide-Regular.ttf +0 -0
  10. package/host/fonts/Seguiemj.ttf +0 -0
  11. package/host/images/answer-gray.png +0 -0
  12. package/host/images/answer.png +0 -0
  13. package/host/images/auto-answer-off.png +0 -0
  14. package/host/images/auto-answer-on.png +0 -0
  15. package/host/images/avatar.png +0 -0
  16. package/host/images/bravophone-logo.svg +39 -0
  17. package/host/images/bravotech_Bravotech_logo.png +0 -0
  18. package/host/images/circle-green.png +0 -0
  19. package/host/images/circle-orange.png +0 -0
  20. package/host/images/circle-red.png +0 -0
  21. package/host/images/dialer-button-down.png +0 -0
  22. package/host/images/dialer-button.png +0 -0
  23. package/host/images/gateways/activecampaign.png +0 -0
  24. package/host/images/gateways/agendor.png +0 -0
  25. package/host/images/gateways/appfacilita.png +0 -0
  26. package/host/images/gateways/call-in.png +0 -0
  27. package/host/images/gateways/click-to-call.png +0 -0
  28. package/host/images/gateways/clickup.png +0 -0
  29. package/host/images/gateways/exactsales.png +0 -0
  30. package/host/images/gateways/go-high-lvl.png +0 -0
  31. package/host/images/gateways/groner.png +0 -0
  32. package/host/images/gateways/hubspot.png +0 -0
  33. package/host/images/gateways/kommo.png +0 -0
  34. package/host/images/gateways/moskit.png +0 -0
  35. package/host/images/gateways/nectar.png +0 -0
  36. package/host/images/gateways/pipedrive.png +0 -0
  37. package/host/images/gateways/pipefy.png +0 -0
  38. package/host/images/gateways/piperun.png +0 -0
  39. package/host/images/gateways/ploomes.png +0 -0
  40. package/host/images/gateways/rdstation-crm.png +0 -0
  41. package/host/images/gateways/salesforce.png +0 -0
  42. package/host/images/gateways/sharpspring.png +0 -0
  43. package/host/images/gateways/sunhub.png +0 -0
  44. package/host/images/gateways/vendfly.png +0 -0
  45. package/host/images/gateways/zendesk.png +0 -0
  46. package/host/images/gateways/zoho.png +0 -0
  47. package/host/images/hangup.png +0 -0
  48. package/host/images/hold.png +0 -0
  49. package/host/images/icons/logo-128-christmas-celebration.png +0 -0
  50. package/host/images/icons/logo-128.png +0 -0
  51. package/host/images/icons/logo-16-christmas-celebration.png +0 -0
  52. package/host/images/icons/logo-16.png +0 -0
  53. package/host/images/icons/logo-24-christmas-celebration.png +0 -0
  54. package/host/images/icons/logo-24.png +0 -0
  55. package/host/images/icons/logo-32-christmas-celebration.png +0 -0
  56. package/host/images/icons/logo-32.png +0 -0
  57. package/host/images/icons/logo-48-christmas-celebration.png +0 -0
  58. package/host/images/icons/logo-48.png +0 -0
  59. package/host/images/loading.gif +0 -0
  60. package/host/images/logo-bravotech-white.png +0 -0
  61. package/host/images/logo-bravotech.png +0 -0
  62. package/host/images/logo-inline-christmas-celebration.png +0 -0
  63. package/host/images/logo-inline.png +0 -0
  64. package/host/images/mic-off.png +0 -0
  65. package/host/images/mic-on.png +0 -0
  66. package/host/images/play.png +0 -0
  67. package/host/images/transfer.png +0 -0
  68. package/host/index.html +34 -0
  69. package/host/js/bravophone-noise-suppressor.js +418 -0
  70. package/host/js/bravophone-route-selector.js +1099 -0
  71. package/host/js/libwebphone.js +20 -0
  72. package/host/js/noise/LICENSE-web-noise-suppressor.txt +21 -0
  73. package/host/js/noise/rnnoise-worklet.js +13 -0
  74. package/host/js/noise/rnnoise.wasm +0 -0
  75. package/host/js/noise/rnnoise_simd.wasm +0 -0
  76. package/host/mock-webphone.js +195 -0
  77. package/host/mock.html +54 -0
  78. package/host/popup.js +23 -0
  79. package/host/shim/chrome-shim.js +287 -0
  80. package/host/shim/guest-bridge.js +353 -0
  81. package/host/shim/messages.js +13 -0
  82. package/host/styles/theme-fixes.css +92 -0
  83. package/package.json +4 -2
  84. package/types/index.d.ts +21 -1
Binary file
Binary file
Binary file
Binary file
Binary file
Binary file
Binary file
Binary file
Binary file
Binary file
Binary file
Binary file
@@ -0,0 +1,34 @@
1
+ <!doctype html>
2
+ <html lang="pt-BR">
3
+ <head>
4
+ <meta charset="UTF-8">
5
+ <meta name="viewport" content="width=device-width,initial-scale=1">
6
+ <meta name="referrer" content="strict-origin">
7
+ <title>Webphone BRAVOPHONE</title>
8
+ <link rel="icon" href="./favicon.ico">
9
+ <link rel="stylesheet" href="./css/dark-theme.css">
10
+ <!-- Correções de tema que o dark-theme.css da extensão não cobre.
11
+ Fica em styles/ (e não em css/) porque o sync apaga css/ inteiro. -->
12
+ <link rel="stylesheet" href="./styles/theme-fixes.css">
13
+ <style>
14
+ html, body { margin: 0; height: 100%; background: #1e1e2d; overflow: hidden; }
15
+ #app { height: 100%; }
16
+ </style>
17
+
18
+ <!-- ORDEM IMPORTA: o shim precisa existir antes de qualquer código que
19
+ toque em chrome.* durante a avaliação do bundle. -->
20
+ <!-- Mensagens de _locales/: alimentam chrome.i18n.getMessage, de onde vem
21
+ praticamente todo label da interface. Precisa vir antes do popup.js. -->
22
+ <script src="./shim/messages.js"></script>
23
+ <script src="./shim/chrome-shim.js"></script>
24
+ <script src="./js/libwebphone.js"></script>
25
+ <script src="./js/bravophone-route-selector.js"></script>
26
+ <script defer src="./popup.js"></script>
27
+ <!-- guest-bridge roda depois do app montar (defer preserva a ordem). -->
28
+ <script defer src="./shim/guest-bridge.js"></script>
29
+ </head>
30
+ <body>
31
+ <noscript><strong>O webphone precisa de JavaScript habilitado.</strong></noscript>
32
+ <div id="app"></div>
33
+ </body>
34
+ </html>
@@ -0,0 +1,418 @@
1
+ /*
2
+ * BravoPhone > Supressor de ruido de fundo (AudioWorklet + RNNoise/WASM)
3
+ *
4
+ * Onde entra no caminho do audio
5
+ * ------------------------------
6
+ * O microfone nunca passava por Web Audio: o libwebphone pega o stream do
7
+ * getUserMedia e entrega direto pro JsSIP. O funil e o _createCallStream(),
8
+ * em js/libwebphone.js, que clona as tracks e devolve o MediaStream usado como
9
+ * `mediaStream` no call()/answer(). E la que este modulo se enfia:
10
+ *
11
+ * getUserMedia -> [source -> rnnoise worklet -> destination] -> JsSIP
12
+ *
13
+ * Contrato com quem chama
14
+ * -----------------------
15
+ * process() e SINCRONO (o _createCallStream tambem e) e NUNCA lanca. Se o
16
+ * supressor nao estiver pronto, estiver desligado, ou qualquer coisa falhar,
17
+ * ele devolve o stream original intacto. Ruido no audio e um incomodo; chamada
18
+ * que nao completa e um defeito. Na duvida, passa reto.
19
+ *
20
+ * Por isso o carregamento e feito uma vez, no boot do popup (init), e nao na
21
+ * hora da ligacao: compilar o WASM e registrar o worklet sao operacoes async
22
+ * que nao cabem dentro do _createCallStream.
23
+ *
24
+ * 48 kHz nao e negociavel
25
+ * -----------------------
26
+ * O RNNoise processa quadros de 480 amostras = 10 ms a 48 kHz, e o worklet nao
27
+ * reamostra. O AudioContext daqui e criado com sampleRate: 48000, separado do
28
+ * AudioContext de saida do libwebphone (que e forcado a 44100 no Chrome). Se o
29
+ * navegador nao entregar 48 kHz, o supressor se desliga em vez de processar em
30
+ * taxa errada — o resultado seria voz acelerada/grave, muito pior que o ruido.
31
+ *
32
+ * Terceiros: js/noise/rnnoise-worklet.js e rnnoise*.wasm vem de
33
+ * @sapphi-red/web-noise-suppressor 0.3.5 (MIT), que empacota xiph/rnnoise.
34
+ * Licenca em js/noise/LICENSE-web-noise-suppressor.txt.
35
+ */
36
+ (function () {
37
+ "use strict";
38
+
39
+ var LOG = "BravoPhone > NoiseSuppressor >";
40
+
41
+ /* Id sob o qual o processador se registra dentro do worklet. Vem do pacote
42
+ original; mudar aqui sem mudar o arquivo do worklet quebra o addModule. */
43
+ var WORKLET_ID = "@sapphi-red/web-noise-suppressor/rnnoise";
44
+ var WORKLET_PATH = "js/noise/rnnoise-worklet.js";
45
+ var WASM_PATH = "js/noise/rnnoise.wasm";
46
+ var WASM_SIMD_PATH = "js/noise/rnnoise_simd.wasm";
47
+
48
+ var STORAGE_KEY = "bravophoneNoiseSuppression";
49
+ var REQUIRED_SAMPLE_RATE = 48000;
50
+
51
+ var enabled = true; // ligado por padrao; storage sobrescreve no init
52
+ var ready = false;
53
+ var initPromise = null;
54
+ var ctx = null;
55
+ var wasmBinary = null;
56
+
57
+ /*
58
+ * O no do worklet e criado UMA vez, no boot, e reaproveitado em todas as
59
+ * chamadas. Motivo medido: o RNNoise instancia o wasm de forma assincrona
60
+ * dentro do worklet, entao criar o no na hora da ligacao custava ~29 ms de
61
+ * silencio no comeco da chamada. Criando no boot, esse custo acontece
62
+ * enquanto ninguem esta falando.
63
+ *
64
+ * O destino, ao contrario, precisa ser novo a cada chamada: o libwebphone
65
+ * chama track.stop() na track do stream que devolvemos, e uma track de
66
+ * MediaStreamAudioDestinationNode parada nao volta.
67
+ */
68
+ var node = null;
69
+
70
+ /* Um grafo por vez. Cada process() derruba o anterior, entao uma ligacao que
71
+ troca de microfone no meio nao deixa worklet orfao girando. */
72
+ var active = null;
73
+
74
+ var stats = { processed: 0, bypassed: 0, autoBypass: 0, lastError: null };
75
+
76
+ /* ─── SIMD ────────────────────────────────────────────────────────────────
77
+ O build SIMD e bem mais rapido, mas so roda onde o navegador suporta. O
78
+ teste e o modulo minimo do wasm-feature-detect: se validar, tem SIMD. */
79
+ function hasSimd() {
80
+ try {
81
+ return WebAssembly.validate(new Uint8Array([
82
+ 0, 97, 115, 109, 1, 0, 0, 0, 1, 5, 1, 96, 0, 1, 123,
83
+ 3, 2, 1, 0, 10, 10, 1, 8, 0, 65, 0, 253, 15, 253, 98, 11
84
+ ]));
85
+ } catch (e) {
86
+ return false;
87
+ }
88
+ }
89
+
90
+ /*
91
+ * A CSP da pagina precisa permitir WebAssembly.
92
+ *
93
+ * Este modulo so faz FETCH do .wasm; quem COMPILA e o worklet. E worklet
94
+ * herda a CSP do documento — mas falha calado: sob CSP restritiva ele nao
95
+ * lanca, nao dispara onprocessorerror, nao loga nada, so devolve zeros. O
96
+ * resultado e uma chamada com RTP saindo normalmente e ninguem ouvindo nada
97
+ * do outro lado. Foi exatamente isso que aconteceu em producao.
98
+ *
99
+ * A compilacao na thread principal e barrada pela mesma regra, entao esta
100
+ * sonda de tres bytes responde pelo worklet antes de prometer supressao.
101
+ */
102
+ function wasmPermitido() {
103
+ try {
104
+ new WebAssembly.Module(new Uint8Array([0, 97, 115, 109, 1, 0, 0, 0]));
105
+ return true;
106
+ } catch (e) {
107
+ return false;
108
+ }
109
+ }
110
+
111
+ function url(path) {
112
+ return (typeof chrome !== "undefined" && chrome.runtime && chrome.runtime.getURL)
113
+ ? chrome.runtime.getURL(path)
114
+ : path;
115
+ }
116
+
117
+ function readEnabledFromStorage() {
118
+ return new Promise(function (resolve) {
119
+ try {
120
+ if (typeof chrome === "undefined" || !chrome.storage || !chrome.storage.local) return resolve(true);
121
+ chrome.storage.local.get(STORAGE_KEY, function (got) {
122
+ void (chrome.runtime && chrome.runtime.lastError);
123
+ var v = got && got[STORAGE_KEY];
124
+ resolve(v === undefined || v === null ? true : !!v);
125
+ });
126
+ } catch (e) {
127
+ resolve(true);
128
+ }
129
+ });
130
+ }
131
+
132
+ /* ─── Carga unica ─────────────────────────────────────────────────────── */
133
+ function init() {
134
+ if (initPromise) return initPromise;
135
+
136
+ initPromise = readEnabledFromStorage()
137
+ .then(function (fromStorage) {
138
+ enabled = fromStorage;
139
+
140
+ if (!wasmPermitido()) {
141
+ throw new Error(
142
+ "WebAssembly bloqueado pela CSP desta pagina. Falta " +
143
+ "\"content_security_policy\": {\"extension_pages\": \"script-src 'self' 'wasm-unsafe-eval'; " +
144
+ "object-src 'self'\"} no manifest.json (e recarregar a extensao)."
145
+ );
146
+ }
147
+
148
+ ctx = new (window.AudioContext || window.webkitAudioContext)({
149
+ sampleRate: REQUIRED_SAMPLE_RATE,
150
+ latencyHint: "interactive"
151
+ });
152
+
153
+ if (ctx.sampleRate !== REQUIRED_SAMPLE_RATE) {
154
+ throw new Error(
155
+ "navegador entregou AudioContext a " + ctx.sampleRate +
156
+ " Hz; o RNNoise exige " + REQUIRED_SAMPLE_RATE
157
+ );
158
+ }
159
+
160
+ var simd = hasSimd();
161
+ return Promise.all([
162
+ ctx.audioWorklet.addModule(url(WORKLET_PATH)),
163
+ fetch(url(simd ? WASM_SIMD_PATH : WASM_PATH)).then(function (r) {
164
+ if (!r.ok) throw new Error("wasm HTTP " + r.status);
165
+ return r.arrayBuffer();
166
+ })
167
+ ]).then(function (out) {
168
+ wasmBinary = out[1];
169
+
170
+ node = new AudioWorkletNode(ctx, WORKLET_ID, {
171
+ processorOptions: { maxChannels: 1, wasmBinary: wasmBinary.slice(0) }
172
+ });
173
+
174
+ /* O wasm so termina de instanciar quando a thread de audio roda, e
175
+ ela nao roda com o contexto suspenso. Tenta agora (costuma falhar
176
+ sem gesto) e, se falhar, no primeiro clique/tecla — bem antes de
177
+ existir uma ligacao, que exige clique de qualquer forma. */
178
+ resumeOnGesture();
179
+
180
+ ready = true;
181
+ console.info(LOG, "pronto", {
182
+ sampleRate: ctx.sampleRate,
183
+ simd: simd,
184
+ wasmBytes: wasmBinary.byteLength,
185
+ habilitado: enabled,
186
+ estadoDoContexto: ctx.state
187
+ });
188
+ });
189
+ })
190
+ .catch(function (err) {
191
+ ready = false;
192
+ stats.lastError = err && err.message ? err.message : String(err);
193
+ /* Falhar aqui e aceitavel: process() passa o stream reto e a ligacao
194
+ acontece sem supressao. Erro no console pra nao virar silencio. */
195
+ console.error(LOG, "indisponivel — chamadas seguem sem supressao:", stats.lastError);
196
+ if (ctx) { try { ctx.close(); } catch (e) {} ctx = null; }
197
+ });
198
+
199
+ return initPromise;
200
+ }
201
+
202
+ /* ─── Contexto ────────────────────────────────────────────────────────── */
203
+ function resumeOnGesture() {
204
+ if (!ctx) return;
205
+ if (ctx.state !== "suspended") return;
206
+ ctx.resume().catch(function () {});
207
+ var once = function () {
208
+ if (ctx) ctx.resume().catch(function () {});
209
+ document.removeEventListener("pointerdown", once, true);
210
+ document.removeEventListener("keydown", once, true);
211
+ };
212
+ document.addEventListener("pointerdown", once, true);
213
+ document.addEventListener("keydown", once, true);
214
+ }
215
+
216
+ /* ─── Vigia de silencio ───────────────────────────────────────────────────
217
+ * Cinto de seguranca para o modo de falha que ninguem ve: worklet vivo,
218
+ * track viva, RTP saindo — e zero audio dentro. Se o microfone tem energia
219
+ * e a saida do worklet nao tem, religa o microfone cru direto no destino.
220
+ * Perder a supressao e um incomodo; perder a voz e uma chamada perdida.
221
+ */
222
+ var LIMIAR_ENTRADA = 0.01; // acima disso alguem esta claramente falando
223
+ var AMOSTRAS_COM_VOZ = 5; // quantos momentos de fala observar antes de julgar
224
+
225
+ function bypassEmChamada(motivo, medidas) {
226
+ if (!active || active.bypassed) return;
227
+ console.error(LOG, "sem audio na saida (" + motivo + ") — voltando pro microfone cru", medidas || {});
228
+ try { active.source.disconnect(node); } catch (e) {}
229
+ try { node.disconnect(active.destination); } catch (e) {}
230
+ try { active.source.connect(active.destination); } catch (e) {}
231
+ active.bypassed = true;
232
+ stats.autoBypass++;
233
+ if (active.vigia) { clearInterval(active.vigia); active.vigia = null; }
234
+ }
235
+
236
+ function armarVigia(source, destination) {
237
+ var entrada = ctx.createAnalyser(); entrada.fftSize = 1024;
238
+ var saida = ctx.createAnalyser(); saida.fftSize = 1024;
239
+ source.connect(entrada);
240
+ node.connect(saida);
241
+
242
+ /* Um AnalyserNode que nao chega a nenhum destino nao e renderizado: ele
243
+ simplesmente devolve zeros, e o vigia ficaria cego achando que esta
244
+ tudo em silencio. O dreno de ganho zero coloca os dois no caminho de
245
+ renderizacao sem produzir som nenhum. */
246
+ var dreno = ctx.createGain();
247
+ dreno.gain.value = 0;
248
+ entrada.connect(dreno);
249
+ saida.connect(dreno);
250
+ dreno.connect(ctx.destination);
251
+
252
+ var buf = new Float32Array(1024);
253
+ var pico = function (a) {
254
+ a.getFloatTimeDomainData(buf);
255
+ var p = 0;
256
+ for (var i = 0; i < buf.length; i++) { var v = Math.abs(buf[i]); if (v > p) p = v; }
257
+ return p;
258
+ };
259
+
260
+ if (active) active.dreno = dreno;
261
+
262
+ /*
263
+ * O criterio NAO pode ser "saida baixa": o RNNoise derrubando um ventilador
264
+ * alto produz exatamente isso — entrada alta, saida quase zero — e eu
265
+ * desligaria a supressao justo quando ela esta funcionando.
266
+ *
267
+ * O que separa os dois casos e que um worklet que nunca rodou devolve zero
268
+ * ABSOLUTO, para sempre. Um worklet vivo, mesmo suprimindo com forca, uma
269
+ * hora entrega alguma amostra diferente de zero. Entao a pergunta e "esse
270
+ * worklet ja produziu audio alguma vez?", nao "esta baixo agora?".
271
+ *
272
+ * Assim que ele produz qualquer coisa, o vigia se aposenta: alem de nao
273
+ * haver mais o que vigiar, para de custar CPU pelo resto da chamada.
274
+ */
275
+ var comVoz = 0;
276
+ var id = setInterval(function () {
277
+ if (!active || active.bypassed) return;
278
+ var pin = pico(entrada), pout = pico(saida);
279
+ stats.ultimoPicoEntrada = pin;
280
+ stats.ultimoPicoSaida = pout;
281
+
282
+ if (pout > 0) { // worklet vivo: nada a fazer
283
+ stats.workletProduziuAudio = true;
284
+ clearInterval(id);
285
+ if (active) active.vigia = null;
286
+ return;
287
+ }
288
+
289
+ if (pin <= LIMIAR_ENTRADA) return; // ninguem falando: nao julga
290
+ comVoz++;
291
+ stats.amostrasComVoz = comVoz;
292
+ if (comVoz >= AMOSTRAS_COM_VOZ) {
293
+ bypassEmChamada("worklet nunca produziu audio", {
294
+ picoEntrada: pin, amostrasComVoz: comVoz, estadoDoContexto: ctx.state
295
+ });
296
+ }
297
+ }, 400);
298
+ return id;
299
+ }
300
+
301
+ /* ─── Grafo ───────────────────────────────────────────────────────────── */
302
+ function teardown() {
303
+ if (!active) return;
304
+ var a = active;
305
+ active = null;
306
+ if (a.vigia) { try { clearInterval(a.vigia); } catch (e) {} }
307
+ if (a.dreno) { try { a.dreno.disconnect(); } catch (e) {} }
308
+ try { a.source.disconnect(); } catch (e) {}
309
+ /* O no e reaproveitado entre chamadas: so desconecta, nunca destroy() —
310
+ destroy() libera o wasm e o proximo process() sairia mudo. Desconectado
311
+ da saida, ele sai do grafo de renderizacao e nao consome CPU. */
312
+ try { if (node) node.disconnect(); } catch (e) {}
313
+ /* As tracks de entrada eram clones feitos pelo _createCallStream. Quem
314
+ para o stream la fora para o stream de SAIDA, entao estas ficariam
315
+ vivas segurando o microfone se nao parassem aqui. */
316
+ try {
317
+ a.inputStream.getTracks().forEach(function (t) { try { t.stop(); } catch (e) {} });
318
+ } catch (e) {}
319
+ }
320
+
321
+ /*
322
+ * Recebe o MediaStream que iria pro JsSIP e devolve o processado.
323
+ * Sincrono e a prova de falha: qualquer problema devolve a entrada.
324
+ */
325
+ function process(stream) {
326
+ try {
327
+ if (!enabled || !ready || !ctx || !node || !stream) { stats.bypassed++; return stream; }
328
+ if (stream.getAudioTracks().length === 0) { stats.bypassed++; return stream; }
329
+
330
+ teardown();
331
+
332
+ /* O contexto pode ter suspendido junto com o popup; a ligacao parte de um
333
+ clique, entao o resume aqui costuma ser aceito. Se nao for, o worklet
334
+ so volta a rodar quando o contexto acordar — sem quebrar nada. */
335
+ if (ctx.state === "suspended") ctx.resume().catch(function () {});
336
+
337
+ var source = ctx.createMediaStreamSource(stream);
338
+ var destination = ctx.createMediaStreamDestination();
339
+
340
+ source.connect(node);
341
+ node.connect(destination);
342
+
343
+ var out = new MediaStream();
344
+ destination.stream.getAudioTracks().forEach(function (t) { out.addTrack(t); });
345
+ /* Video esta desligado neste produto, mas se um dia entrar ele passa
346
+ reto em vez de sumir. */
347
+ stream.getVideoTracks().forEach(function (t) { out.addTrack(t); });
348
+
349
+ active = { source: source, destination: destination, inputStream: stream, bypassed: false, vigia: null };
350
+ /* Um erro dentro do processador tambem derruba o audio; ai o bypass e
351
+ imediato, sem esperar o vigia. */
352
+ node.onprocessorerror = function () { bypassEmChamada("onprocessorerror"); };
353
+ active.vigia = armarVigia(source, destination);
354
+
355
+ stats.processed++;
356
+ console.info(LOG, "supressao ativa na chamada", {
357
+ sampleRate: ctx.sampleRate, estadoDoContexto: ctx.state
358
+ });
359
+ return out;
360
+ } catch (err) {
361
+ stats.lastError = err && err.message ? err.message : String(err);
362
+ console.error(LOG, "falhou — seguindo sem supressao:", stats.lastError);
363
+ try { teardown(); } catch (e) {}
364
+ stats.bypassed++;
365
+ return stream;
366
+ }
367
+ }
368
+
369
+ var api = {
370
+ /** Pre-aquece (AudioContext 48k + worklet + wasm). Idempotente. */
371
+ init: init,
372
+ /** true quando worklet e wasm ja estao carregados. */
373
+ isReady: function () { return ready; },
374
+ /** true quando o usuario nao desligou a supressao. */
375
+ isEnabled: function () { return enabled; },
376
+ /** Liga/desliga e persiste. Passa a valer na proxima chamada. */
377
+ setEnabled: function (v) {
378
+ enabled = !!v;
379
+ if (!enabled) teardown();
380
+ try {
381
+ var patch = {};
382
+ patch[STORAGE_KEY] = enabled;
383
+ chrome.storage.local.set(patch, function () { void (chrome.runtime && chrome.runtime.lastError); });
384
+ } catch (e) {}
385
+ console.info(LOG, enabled ? "ligado" : "desligado");
386
+ return enabled;
387
+ },
388
+ /** Chamado pelo _createCallStream. Sincrono, nunca lanca. */
389
+ process: process,
390
+ /** Derruba o grafo ativo (fim de chamada). */
391
+ stop: teardown,
392
+ /** Diagnostico rapido pelo console do popup. */
393
+ getStats: function () {
394
+ return {
395
+ pronto: ready,
396
+ habilitado: enabled,
397
+ sampleRate: ctx ? ctx.sampleRate : null,
398
+ estadoDoContexto: ctx ? ctx.state : null,
399
+ wasmPermitido: wasmPermitido(),
400
+ noPreAquecido: !!node,
401
+ grafoAtivo: !!active,
402
+ emBypassNestaChamada: !!(active && active.bypassed),
403
+ simd: hasSimd(),
404
+ processadas: stats.processed,
405
+ semSupressao: stats.bypassed,
406
+ bypassAutomatico: stats.autoBypass,
407
+ ultimoPicoEntrada: stats.ultimoPicoEntrada,
408
+ ultimoPicoSaida: stats.ultimoPicoSaida,
409
+ amostrasComVoz: stats.amostrasComVoz,
410
+ workletProduziuAudio: !!stats.workletProduziuAudio,
411
+ ultimoErro: stats.lastError
412
+ };
413
+ }
414
+ };
415
+
416
+ window.BravoPhoneNoiseSuppressor = api;
417
+ init();
418
+ })();