0
0

More than 1 year has passed since last update.

ブラウザのテキスト読み上げに合わせてバーが伸び縮みするアニメーション

Posted at

ブラウザのテキスト読み上げに合わせてバーが伸び縮みするアニメーション

TEXTAREA に入力したテキストを読み上げます。
ページ下部のソースをローカルに保存してご利用ください。

Windows 10 上の Google Chrome、Microsoft Edge、Firefox で動作を確認しています。
Microsoft Edge は、音声で日本語を選択しないと読み上げてくれませんでした。

パソコンのスピーカーから出た音声をマイクで拾って可視化しているので、ヘッドホンなどで音声出力がふさがっていると、バーの表示は何も起きません。
ブラウザが読み上げた台詞だけでなく、ユーザが読み上げた台詞にも反応しますが、気にしてはいけません。

「それは失敗じゃない。”大失敗”だ。」

Canvasで赤いLEDが残光を曳くアニメーション が必要な方は過去の投稿をどうぞ。

使い方

TEXTAREA にお好みの名台詞/迷台詞を入力して「話す」ボタンを押すと、マイクの使用許可を求めるダイアログが出ます。
マイクの使用を許可すると、数秒後に読み上げが始まります。

実行するとこんな感じです。

red_bar.png
yellow_bar.png

ソース

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>
0
0
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
0
0