◆ Web Speech API
◆ 読み上げ状態はタブ間で共有
◆ タブを閉じても止まらない
  ◆ 嫌がらせに使えてしまう
◆ 別タブでキャンセルできる

ネットである記事を読んでたときに音読ボタンがありました
押してみたら記事の内容を読んでくれます

そんな機能あったんだー と思いつつも長めの記事を全部読んでもらう気もなかったので止めようとしました
音楽聴きながらだったので邪魔だったんです

しかし 止めるボタンが用意されていません
もう一回押せば止まるのかなと思って押してみても止まりません

一旦タブ閉じれば流石に止まるでしょ と思って閉じたのに

止まりません

さすがにこれはどうなの……
とりあえずどういう機能で音読してるんだろうとそのサイトのソースを見てみました

Web Speech API というのを使ってるようです
MDN で使い方をさっと見てみると speechSynthesis.cancel でキャンセルできるみたいだったのでやってみると止まりました

タブ内の処理なのにタブを閉じても残り続ける挙動ってどうなんでしょうね
バグを疑いたいです
ブラウザ自体を閉じれば流石に止まるでしょうけど 別タブを消したくなくてブラウザを閉じるのは避けたいことは多いはずです
そういう人に対して ちょっとした嫌がらせができてしまいます
"ばーか".repeat(1000) とか "すぐにとじろ".repeat(2000) を音読されるとさすがにイラッとしたり気分が悪くなります
ネットには画像でのブラクラとかはなくはないですが タブを閉じれば終わるのでそこまで問題じゃないと思います
しかしこれはタブを閉じてまで残ってるので良い挙動だとは思えません

使い方

せっかく調べたので簡単に使い方を書いておきます

喋ってもらうときは

speechSynthesis.speak()

を呼び出します

同時に複数の読み上げされても困るのでグローバルにインスタンスひとつのみになってるようです
グローバルに SpeechSynthesis コンストラクタはないですし speechSynthesis.constructor から取り出して new してもエラーでした
また あとから別のタブで cancel メソッド呼び出しで取り消せたように別タブの window とも共有してます
タブごとに同時に色々読み上げられても困りますからね
それが原因なのかタブを閉じても読み上げだけ残ってしまう問題が起きています

読み上げ開始前に現時点のを cancel で止めないとキューに入るだけですぐには読み上げてくれません
別タブで読み上げ中のでもキャンセルできるので必要なら speak メソッドの前に cancel メソッドを呼び出しておきましょう

読み上げるテキストは speak の引数に SpeechSynthesisUtterance 型で渡します

const ut = new SpeechSynthesisUtterance()
ut.text = "1234567890".repeat(10)
ut.voice = speechSynthesis.getVoices().filter(e => e.lang === "ja-JP")[0]

speechSynthesis.speak(ut)

読み方は自動で判断されるので 思ったとおりに読んでくれないならひらがな入力がいいかもです

ボイスを選択することもできて

speechSynthesis.getVoices()

で一覧を取得できます
いろいろな言語があるので 20 種類くらいありますが日本語は 1 つか 2 つです
Windows 7 だと Google 日本語というのがひとつだけでしたが Windows 10 だと Microsoft Haruka Desktop も入ってました
同じ PC の Chrome と Vivaldi でも違いがあったので OS にインストールされてるかでの判断でもないようです

speechSynthesis.getVoices().filter(e => e.lang === "ja-JP")[0]
// SpeechSynthesisVoice {voiceURI: "Google 日本語", name: "Google 日本語", lang: "ja-JP", localService: false, default: false}

DEMO

下の HTML をプレビューしたページで試せるようにしてます

<h1 id="message">speechSynthesis try</h1>
<div class="text">
Text<br/>
<textarea id="text"></textarea>
</div>
<div class="voice">
Voice <button id="voiceload">Load</button><br/>
<select id="voice"></select>
</div>
<div class="btns">
<button id="speak">Speak</button>
<button id="stop">Stop</button>
</div>

<style>
textarea { width: 500px; height: 300px; padding: 10px; }
input, button, select {
padding: 3px 6px;
margin: 5px;
}
.text, .voice, .btns {
margin: 15px 0;
}
</style>

<script>
const $ = document.getElementById.bind(document)

if(!window.speechSynthesis) {
$("message").textContent = "対応してません"
}

const voice_map = {}

$("voiceload").onclick = () => {
for(const v of speechSynthesis.getVoices()) {
voice_map[v.name] = v
$("voice").append(new Option(v.name))
}
}
$("speak").onclick = () => {
const text = $("text").value.trim()
const voice = voice_map[$("voice").value]
const ut = new SpeechSynthesisUtterance()
ut.text = text
if(voice){
ut.voice = voice
}
speechSynthesis.speak(ut)
}
$("stop").onclick = () => {
speechSynthesis.cancel()
}
</script>