ブラウザのテキスト読み上げに合わせてバーが伸び縮みするアニメーション
TEXTAREA に入力したテキストを読み上げます。
ページ下部のソースをローカルに保存してご利用ください。
Windows 10 上の Google Chrome、Microsoft Edge、Firefox で動作を確認しています。
Microsoft Edge は、音声で日本語を選択しないと読み上げてくれませんでした。
パソコンのスピーカーから出た音声をマイクで拾って可視化しているので、ヘッドホンなどで音声出力がふさがっていると、バーの表示は何も起きません。
ブラウザが読み上げた台詞だけでなく、ユーザが読み上げた台詞にも反応しますが、気にしてはいけません。
「それは失敗じゃない。”大失敗”だ。」
Canvasで赤いLEDが残光を曳くアニメーション が必要な方は過去の投稿をどうぞ。
使い方
TEXTAREA にお好みの名台詞/迷台詞を入力して「話す」ボタンを押すと、マイクの使用許可を求めるダイアログが出ます。
マイクの使用を許可すると、数秒後に読み上げが始まります。
実行するとこんな感じです。
ソース
speak.html
<!DOCTYPE html>
<html lang='ja'>
<head>
<meta http-equiv='Content-Type' content='text/html; charset=utf-8' />
<meta http-equiv="Pragma" content="no-cache">
<meta http-equiv="Cache-Control" content="no-cache">
<title>What would you like to hear?</title>
<script>
let $ = e => document.getElementById(e);
const LEDColor = {
red: 0,
yellow: 1
};
Object.freeze(LEDColor);
class Lamp {
constructor() {
this.width = 20;
this.height = 11;
this.x = 0;
this.y = 0;
this.brightness = 100; //0 <= brightness <= 100
this.colortype = LEDColor.red;
}
color() {
const alpha = ((this.brightness == 0) ? 0 : 1);
return (this.colortype == LEDColor.red) ?
{r:Math.round(255 * this.brightness / 100), g:0, b:0, a:alpha} :
{r:Math.round(255 * this.brightness / 95), g:Math.round(255 * this.brightness / 90), b:0, a:alpha};
}
drawon(context) {
const mycolor = this.color();
context.fillStyle = "rgba(" + mycolor.r + "," + mycolor.g + "," + mycolor.b + "," + mycolor.a + ")";
context.fillRect(this.x, this.y, this.width, this.height);
}
}
const LEDArray = {
vLamp: [[], [], []],
col: 3,
row: 16,
brightness_min: 10,
colortype: LEDColor.red,
init() {
let r = this.row;
this.vLamp = new Array(3);
this.vLamp[0] = new Array(r).fill(0);
this.vLamp[1] = new Array(r).fill(0);
this.vLamp[2] = new Array(r).fill(0);
const gap_x = 3;
const gap_y = 1;
let x0 = 40, y0 = 28, x = x0, y = y0;
for (let i = 0; i < this.col; i++) {
y = y0;
for (let j = 0; j < r; j++) {
this.vLamp[i][j] = new Lamp();
Object.assign(this.vLamp[i][j], {x:x, y:y, brightness:10});
y += (this.vLamp[i][j].height + gap_y);
}
x += (this.vLamp[i][0].width + gap_x);
}
},
setBrightness_off() {
for (let i = 0; i < this.col; i++) {
for (let j = 0; j < this.row; j++) {
Object.assign(this.vLamp[i][j],
{brightness:this.brightness_min, colortype:this.colortype});
}
}
},
setBrightness(brightness) {
//center
for (let j = 0; j < this.row; j++) {
Object.assign(this.vLamp[1][j],
{brightness:brightness * Math.cos((j - 7.5) / 23.5 * Math.PI), colortype:this.colortype});
}
//side
if (this.colortype == LEDColor.red) {
for (let i = 0; i < this.col; i += 2) {
for (let j = 0; j < this.row; j++) {
Object.assign(this.vLamp[i][j],
{brightness:brightness * Math.cos((j - 7.5) / 13.5 * Math.PI), colortype:this.colortype});
}
}
} else {
for (let i = 0; i < this.col; i += 2) {
for (let j = 0; j < this.row; j++) {
Object.assign(this.vLamp[i][j],
{brightness:brightness * (1 - Math.sin((j + 0.5) / 15.5 * Math.PI)), colortype:this.colortype});
}
}
}
}
}
const Animation = {
context: 0,
init() {
LEDArray.init();
const canvas = $("canvas");
if (!canvas.getContext) { return; }
this.context = canvas.getContext("2d");
this.redraw_off();
},
redraw_off() {
LEDArray.setBrightness_off();
for (let i = 0; i < LEDArray.col; i++) {
for (let j = 0; j < LEDArray.row; j++) {
LEDArray.vLamp[i][j].drawon(this.context);
}
}
},
redraw(brightness) {
LEDArray.setBrightness(brightness);
for (let i = 0; i < LEDArray.col; i++) {
for (let j = 0; j < LEDArray.row; j++) {
LEDArray.vLamp[i][j].drawon(this.context);
}
}
}
}
const allVoicesObtained = new Promise(function(resolve, reject) {
let voices = window.speechSynthesis.getVoices();
if (voices.length !== 0) {
resolve(voices);
} else {
window.speechSynthesis.addEventListener("voiceschanged", function() {
voices = window.speechSynthesis.getVoices();
voices.sort(function(a, b) {
return (a.lang > b.lang) ? 1 : -1;
});
resolve(voices);
});
}
});
function InitVoice() {
let ssu = new SpeechSynthesisUtterance();
ssu.text = " ";
window.speechSynthesis.speak(ssu);
allVoicesObtained.then(voices => {
const select = $("selVoice");
let nSelected = -1;
for (let i = 0; i < voices.length; i++) {
const option = document.createElement("option");
option.text = voices[i].lang + " " + voices[i].name;
option.value = i;
select.appendChild(option);
if ((nSelected == -1) && (voices[i].lang.substr(0, 2) == "en")) {
nSelected = i;
}
}
if (nSelected == -1) { nSelected = 0; }
select.options[nSelected].selected = true;
});
}
function doSpeak() {
const select = $("selVoice");
const option = select.options[select.selectedIndex];
let ssu = new SpeechSynthesisUtterance();
ssu.lang = option.text.substr(0, 5);
ssu.volume = 1.0;
ssu.rate = 1.0;
ssu.pitch = 1.0;
ssu.text = " ";
window.speechSynthesis.speak(ssu);
allVoicesObtained.then(voices => {
for (let i = 0; i < voices.length; i++) {
if (voices[i].name == option.text.substring(6)) {
ssu.voice = voices[i];
break;
}
}
});
ssu.text = $("script").value;
ssu.onend = function(ev) {
//$("btnStop").onclick();
}
window.speechSynthesis.speak(ssu);
}
function doShutUp() {
window.speechSynthesis.cancel();
}
function split_text(text) {
const eol = text.match(/\r\n|\n|\r/);
let vLines = text.split(eol).map(s => s.trim());
return vLines.filter(s => s != "");
}
const MediaStream = {
stream: {},
start() {
//window.speechSynthesis.onvoiceschanged = function() {
// window.speechSynthesis.getVoices();
//};
let objAudioContext = new (window.AudioContext || window.webkitAudioContext)();
navigator.mediaDevices.getUserMedia({ audio: true, video: false }).then((stream) => {
doSpeak();
this.stream = stream;
let src = objAudioContext.createMediaStreamSource(this.stream);
let analyser = objAudioContext.createAnalyser();
analyser.fftSize = 2048;
let processor = objAudioContext.createScriptProcessor(0, 1, 1);
processor.onaudioprocess = (ev) => {
let dataArray = new Uint8Array(analyser.fftSize);
analyser.getByteTimeDomainData(dataArray);
let ymax = 0;
for (let i = 0; i < dataArray.length; i++) {
let y = dataArray[i] / 128.0 * 100 - 100;
ymax = Math.max(ymax, y);
}
Animation.redraw(ymax);
}
src.connect(processor);
processor.connect(objAudioContext.destination);
src.connect(analyser);
}).catch((err) => {
console.log(err);
})
},
stop() {
doShutUp();
this.stream.getTracks().forEach((track) => {
track.stop();
})
}
}
window.onload = function() {
if (!navigator.getUserMedia) {
navigator.getUserMedia = navigator.webkitGetUserMedia || navigator.mozGetUserMedia;
}
$("btnStart").onclick = () => {
$("btnStart").disabled = true;
$("btnStop").disabled = false;
MediaStream.start();
}
$("btnStop").onclick = () => {
if (!$("btnStart").disabled) { return; }
$("btnStart").disabled = false;
$("btnStop").disabled = true;
MediaStream.stop();
}
document.getElementsByName("rdoColor").forEach(
e => e.addEventListener("change",
ev => {
LEDArray.colortype = (ev.target.value == 1) ? LEDColor.yellow : LEDColor.red;
Animation.redraw_off();
}
)
);
InitVoice();
Animation.init();
}
</script>
</head>
<body>
<h2>テキスト読み上げに合わせてバーが伸び縮み</h2>
<canvas id="canvas" width="150" height="250" style="background-color:#1e1e1e;"></canvas>
<br />
<div>
色:
<input type="radio" name="rdoColor" value="0" checked />赤
<input type="radio" name="rdoColor" value="1" />黄
<br />
音声:
<select id="selVoice" style="width:350;">
</select>
<button id="btnStart">話す</button>
<button id="btnStop" disabled>中止</button>
</div>
<textarea id="script" rows="20" col="100" style="width:700px;height:150px;">
The quick brown fox jumps over the lazy dog.
</textarea>
</body>
</html>