ENESFRPT

Profundidade em tudo. Superficialidade em nada.

speechSynthesis não se consegue capturar

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.

Tens uma coisa em mãos que tem de aguentar?

Se estás algures entre uma demo e um sistema de que depende gente a sério, essa é precisamente a parte que eu faço.