O speechSynthesis dá-te áudio que consegues ouvir e não consegues enviar. Do outro lado não existe nenhum MediaStreamTrack, por isso uma resposta traduzida dita pelo navegador nunca pode entrar num sender de WebRTC. Encontrei isto a construir uma sala de chamadas 1:1 com tradução em direto, e a saída é deixar de o usar para o que tem de sair da máquina.
O que a API devolve na realidade
A Web Speech API fala pelo caminho de áudio da plataforma, não por nada que a página possua. Recebes uma utterance, recebes eventos e recebes som pelas colunas. O que nunca recebes é um nó, um stream ou uma faixa. Não é um descuido de um navegador em concreto: não existe forma especificada de a capturar, por isso não há nada para detetar nem nada que venha a chegar numa versão posterior.
Isto conta a partir do momento em que o áudio tem um destino que não sejam as colunas locais. Uma legenda serve. Uma voz que a pessoa do outro lado tem de ouvir não serve, porque o WebRTC só envia o que está numa faixa.
A via que resulta
Usa um sintetizador que devolva um ficheiro. Eu uso o melotts do Workers AI, que responde com bytes de áudio em vez da promessa de fazer barulho. Daí em diante o caminho é Web Audio banal: descodifica os bytes, liga o buffer source a um MediaStreamDestination e tira a faixa desse destino.
// Synthesise to a file, decode through Web Audio, and replace the
// microphone track on the sender. speechSynthesis never enters this path.
const buf = await audioCtx.decodeAudioData(await res.arrayBuffer());
const dest = audioCtx.createMediaStreamDestination();
const src = audioCtx.createBufferSource();
src.buffer = buf;
src.connect(dest);
src.start();
const sender = pc.getSenders().find((s) => s.track?.kind === 'audio');
await sender.replaceTrack(dest.stream.getAudioTracks()[0]);O replaceTrack é a chamada que interessa. Troca o áudio de saída sem renegociar, por isso o outro lado ouve a voz traduzida em vez do microfone, sem glare, sem oferta nova e sem falha na ligação.
A consequência de que ninguém avisa
Como a voz traduzida substitui o microfone no sender, tudo o que reconstrua a RTCPeerConnection tem de reconstruir também o canal de voz. Uma reconexão que só repõe o peer devolve o microfone em bruto ao fio, e o outro lado passa a ouvir de repente a voz por traduzir a meio da frase.
Essa falha é silenciosa do lado que a provoca. Ouves-te normalmente, a chamada continua ligada e nada regista um erro. Hoje só a apanho porque existe um teste de navegador que recarrega o outro lado a meio da chamada e falha se o microfone voltar.
O que eu via primeiro
- Se o áudio tem de sair da máquina, põe o speechSynthesis de parte no desenho e não depois do protótipo.
- Mantém uma única função que constrói a faixa de áudio de saída, e chama-a a partir de todos os caminhos que criam uma ligação.
- Escreve o teste que recarrega um dos lados a meio da chamada. É a única coisa que apanha a regressão, porque o sintoma é inaudível para quem a introduz.