-
📢 Speech Synthesis 🔊Web/JS30 2020. 12. 23. 06:57반응형
이번에도 목소리와 텍스트를 다루는 브라우저 내장 객체에 대한 내용이다.
생각했던 것보다 훨씬 다양한 기능이 들어있는 신비한 브라우저였구만.
이전에 Speech Recognition 이 목소리를 텍스트로 바꾸는 브라우저의 기본 내장 객체였다면
Speech Synthesis는 텍스트를 목소리로 바꾸는 브라우저의 내장 객체라고 볼 수 있다.
위의 인터페이스를 구성하는 HTML 코드는 다음과 같다.
<div class="voiceinator"> <h1>The Voiceinator 5000</h1> <select name="voice" id="voices"> <option value="">Select A Voice</option> </select> <label for="rate">Rate:</label> <input name="rate" type="range" min="0" max="3" value="1" step="0.1"> <label for="pitch">Pitch:</label> <input name="pitch" type="range" min="0" max="2" step="0.1"> <textarea name="text">Hello! I love JavaScript 👍</textarea> <button id="stop">Stop!</button> <button id="speak">Speak</button> </div>
const msg = new SpeechSynthesisUtterance(); let voices = []; const voicesDropdown = document.querySelector('[name="voice"]'); const options = document.querySelectorAll('[type="range"], [name="text"]'); const speakButton = document.querySelector('#speak'); const stopButton = document.querySelector('#stop');
SpeechSynthesisUtterance 객체는 텍스트를 말하는 빠르기, 말하는 목소리의 높이,
어떤 목소리로 말하는지, 어떤 내용을 말하는지 등에 대한 정보를 담고 있는 객체이다.
그래서 위의 인터페이스에서 조절이 가능한 rate, pitch, voice, text 등이 전부 utterance 객체에 담기게 되는 것.
voices 배열은 우리가 선택 가능한 '목소리'들을 전부 담을 배열이다.
우선은, 페이지가 로드될 때 저 text 영역에 들어있는 내용을 msg의 내용으로 담아준다.
그냥 default 내용을 설정해주는 느낌이라고 간단하게 생각하면 된다.
msg.text = document.querySelector('[name="text"]').value;
그리고 콘솔창에 msg를 입력해서 확인해보면, 객체의 text에 해당 내용이 담긴 것을 확인할 수 있다.
하지만 voice를 설정해주지 않았기 때문에, 아직 해당 텍스트를 목소리로 들을 수는 없다.
참고로 'utterance'는 '말'이라는 뜻인데, 이 '말 객체'를 목소리로 듣기 위해서는 '말하기 객체'가 필요하다.
이는 'speechSynthesis'라는 객체로 존재하고, 해당 객체의 speak 메소드 안에
utterance 객체를 넣어주면 브라우저가 해당 '말 객체'에 들어있는 각종 정보를 이용해서
'말하기 객체'로 우리에게 목소리를 들려주게 되는 것이다.
그러니까, text, voice, pitch, rate 등의 말과 관련된 모든 정보를 담고 있는 객체가 utterance 객체라고 생각하면 되고,
이를 목소리로 직접 출력하기 위해서는 speechSynthesis 객체가 필요한 것이다.
일단, 우리는 저 인터페이스에서 목소리를 선택할 수 있어야 하므로, 그것과 관련된 함수를 만들어보자.
위의 HTML 코드를 보면 알겠지만, select 태그에서 목소리를 선택해야하는데 아직은 option들이 존재하지 않는다.
그래서, 목소리에 관한 정보를 전부 가져와서 option들을 만들어보도록 하겠다.
function populateVoices() { voices = this.getVoices(); const voiceOptions = voices .map(voice => `<option value="${voice.name}">${voice.name} (${voice.lang})</option>`) .join(''); voicesDropdown.innerHTML = voiceOptions; } speechSynthesis.addEventListener('voiceschanged', populateVoices);
우선은 아까 말했던 글로벌 객체인 speechSynthesis 객체에 voiceschanged 이벤트 리스너를 붙여준다.
검색해보니, voiceschanged 이벤트가 발생하는 조건 중의 하나가 보이스들이 처음 설치될 때라고 하는데,
아마 페이지가 로드되면서 저 이벤트가 한 번 호출이 되는 듯하다.
그리고 this.getVoices()로 가능한 목소리 목록들을 불러와서 voices 배열에다가 담아주고
map 메소드를 이용하여 option 태그로 만들어 select 태그에다가 붙여주면 된다.
참고로 불러올 수 있는 목소리의 종류는 윈도우보다 맥이 훨씬 다양하다.
윈도우에서, 그것도 크롬에서는 구글에서 지원하는 몇 가지의 목소리만 가져올 수가 있다.
이 때, msg 객체의 voice는 여전히 null인 상태이지만,
목소리들이 페이지에 로드된 상태이기 때문에 speechSynthesis.speak(msg)를 하면
default 목소리로 텍스트를 읽어준다.
현재 나는 default 목소리가 'Google 한국어' 목소리로 설정되어있기 때문에
'헬로. 아이러브자바스크립트 엄지손가락을 위로' 라고 또박또박 여성의 목소리로 읽어주더라.
여튼.
이제 select 태그의 드롭다운을 모두 채워넣었으니,
드롭다운에서 목소리 하나를 선택했을 때 msg 객체의 voice를
그 목소리로 설정해주는 함수를 다음과 같이 작성해주면 된다.
function setVoice() { msg.voice = voices.find(voice => voice.name === this.value); } voicesDropdown.addEventListener('change', setVoice);
드롭다운에서 목소리를 하나 선택하면, 해당 option의 value 값과
동일한 이름을 가진 voice 객체를 loop를 돌면서 찾는다.
그런 다음에 msg 객체의 voice를 해당 voice 객체로 설정해주는 것.
그 다음 추가할 함수는, 우리가 인터페이스에서 목소리에 관해 어떤 설정을 바꿀 때마다
현재 말하고 있던 내용을 처음부터 다시 말하도록 하는 함수(혹은 중지시키는 함수)이다.
function toggle(startOver = true) { speechSynthesis.cancel(); if (startOver) { speechSynthesis.speak(msg); } }
speechSynthesis.cancel()은 말 그대로 현재 말하고 있던 내용을 중단하도록 하는 메소드.
매개변수로는 startOver라는 변수가 들어가는데, 기본값은 true이다.
만약 이걸 false로 넣어주면, if문에서 걸러지면서 다시 말하지 않고 말하는 것을 중단하기만 한다.
이제 인터페이스로 text, pitch, rate를 바꿀 수 있도록 함수를 만들어 줄 차례.
이와 관련된 input들은 options에 들어가있다.
function setOption() { console.log(this.name, this.value); } options.forEach(option => option.addEventListener('change', setOption));
위의 코드를 실행해보면 알겠지만, 우리가 input에 변화를 주면
'무엇의 값을 바꿀지'는 this.name에 해당하고, '바꿀 값이 얼마인지'는 this.value에 해당한다.
그래서, 다음과 같이 코드를 작성하면 된다.
function setOption() { msg[this.name] = this.value; toggle(); } options.forEach(option => option.addEventListener('change', setOption));
msg 객체의 this.name 속성의 값을 this.value로 바꾸겠다는 뜻.
참고로, textarea에서 change 이벤트가 발생하려면
값을 입력하고나서 다른 아무 곳이나 한 번 클릭만 해주면 된다.
마지막으로, 목소리를 시작하고 멈추는 버튼에 관한 함수만 작성해주자.
여기에서 우리가 아까 toggle 함수에 매개변수를 만들어뒀던 이유가 드러난다.
start 버튼을 누르면 toggle()을 호출하고, stop 버튼을 누르면 toggle(false)를 호출하면 되는 것인데
다음과 같이 코드를 작성했다고 쳐보자.
speakButton.addEventListener('click', toggle); stopButton.addEventListener('click', toggle(false));
그러면, stopButton은 처음 페이지가 로드될 때 toggle(false)가 호출되면서 딱 한 번 동작하고
그 이후로는 아무런 동작을 하지 않는다.
나도 신경을 안쓰면 종종 하는 실수인데, 함수 뒤에 괄호를 붙이면 '즉시 호출'의 의미이기 때문.
이를 해결하는 방법은 세 가지 정도가 있다.
사실, 방법은 한 가지로 동일한데 작성하는 코드의 형태가 조금 다른 것 뿐이다.
// 1. stopButton.addEventListener('click', function() { toggle(false); }); // 2. stopButton.addEventListener('click', toggle.bind(null, false)); // 3. stopButton.addEventListener('click', () => toggle(false));
첫 번째는 toggle(false)를 호출하는 또 다른 함수를 하나 만들어서 이벤트 리스너 함수로 달아주는 방법이다.
세 번째는 이를 화살표 함수로 만들어서 좀 더 보기 편하게 만든 것.
두 번째는 bind 메소드를 이용한 방법인데, 첫 번째 인자로 'this'를 무엇으로 설정할 것인지,
그리고 두 번째 인자부터는 실제로 그 함수에 들어갈 인자를 넣어주면 된다.
간단히 말해, 첫 번째 인자를 this로 하는 새로운 toggle 함수를 만든다고 보면 된다.
여튼 세 가지 방법 중 하나를 선택해서 작성하면
이벤트 리스너의 콜백 함수에다가 매개변수를 집어넣는 것이 가능해진다.
최종 자바스크립트 코드
<script> const msg = new SpeechSynthesisUtterance(); let voices = []; const voicesDropdown = document.querySelector('[name="voice"]'); const options = document.querySelectorAll('[type="range"], [name="text"]'); const speakButton = document.querySelector('#speak'); const stopButton = document.querySelector('#stop'); msg.text = document.querySelector('[name="text"]').value; function populateVoices() { voices = this.getVoices(); const voiceOptions = voices .filter(voice => voice.lang.includes('ko')) .map(voice => `<option value="${voice.name}">${voice.name} (${voice.lang})</option>`) .join(''); voicesDropdown.innerHTML = voiceOptions; } function setVoice() { msg.voice = voices.find(voice => voice.name === this.value); toggle(); } function toggle(startOver = true) { speechSynthesis.cancel(); if (startOver) { speechSynthesis.speak(msg); } } function setOption() { console.log(this.name, this.value); msg[this.name] = this.value; toggle(); } speechSynthesis.addEventListener('voiceschanged', populateVoices); voicesDropdown.addEventListener('change', setVoice); options.forEach(option => option.addEventListener('change', setOption)); speakButton.addEventListener('click', toggle); stopButton.addEventListener('click', () => toggle(false)); </script>
populateVoices 함수 안에 보면 filter가 추가된 것을 볼 수 있는데
저건 그냥 한국어로 말하는 목소리만 추리겠다는 의미.
반응형'Web > JS30' 카테고리의 다른 글
Event Capture, Propagation, Bubbling and Once (0) 2020.12.24 Sticky Nav 🩹 (0) 2020.12.23 Following Along Links (0) 2020.11.29 🎤Native Speech Recognition📢 (0) 2020.11.23 ⌚Tally String Times with Reduce⏱ (0) 2020.11.20