diff --git a/index.html b/index.html index 8fbde79..e690910 100644 --- a/index.html +++ b/index.html @@ -2,7 +2,7 @@ - + quackspeak: Text-to-quack @@ -14,10 +14,10 @@
diff --git a/script.js b/script.js index 14453c6..c11c21f 100644 --- a/script.js +++ b/script.js @@ -1,3 +1,5 @@ +//@ts-check + /** * An object containing the decoded audio data of the voices that are * stored locally in the server. @@ -7,22 +9,41 @@ * * This variable starts `undefined`, and is only defined when the user * interacts with the page by using the function - * `loadLocalVoicesDecodedAudioData`. This is needed to fix the error `The - * AudioContext was not allowed to start`, loading the local voices only when - * the user interacts with the page. + * `loadLocalVoices`. This is needed to fix the warning `The AudioContext was + * not allowed to start`, by loading the local voices only when the user + * interacts with the page. */ let localVoices; /** - * An array that contains all the ids of the timeouts set by the `speak` - * function. Each timeout is corresponded to the speak of one of the - * characters inserted in the $inputText element. + * An array that contains all the ids of the timeouts set by the + * `speakAndWriteToDialogueText` function. Each timeout is corresponded to the + * speak of one of the characters inserted in the $inputText element. * - * This array needs to exist to the code be able to cancel previous timeouts - * avoiding the audio to overlap each other. + * This array needs to exist to the code be able to cancel previous timeouts, + * avoiding the audio to overlap. * * @type {Array} */ let speakTimeoutsIds = []; +/** + * A number that will keep the sum of all the random intervals added to the + * timeouts of the `speakAndWriteToDialogueText` function. This is needed to + * make the timeout time work whenever a random interval has been added when a + * white space is found. + */ +let randomIntervalsInMilliseconds = 0; +/** + * A variable that hosts an AudioContext type to be used throughout the code. + * This variable needs to exists because, previously, creating an audio context + * to each function was causing the audios to overlap or mute in Chromium, + * Google Chrome and Brave. By using only one audio context solved that issue. + * + * This variable starts undefined by the same reason the `localVoices` variable + * does: to fix the warning `The AudioContext was not allowed to start`, by + * loading this audio context only when the user interacts with the page. + * @type {AudioContext} + */ +let audioContext; /** @type {HTMLTextAreaElement} */ const $inputText = document.querySelector('.js-input-text'); @@ -38,20 +59,46 @@ const $voiceSelector = document.querySelector('.js-voice-selector'); const $sayItButton = document.querySelector('.js-say-it-button'); $sayItButton.addEventListener('pointerdown', async () => { - await speak(); - writeDialogueText(); + await speakAndWriteDialogue(); }); +/** + * Trims and remove extra white spaces that are between the words of a string. + * + * @param {string} text The text to be treated. + * + * @returns {string} The treated string. + */ +function removeWhiteSpaces(text) { + const treatedCharacters = []; + let lastCharacter = ''; + text + .trim() + .split('') + .forEach((character) => { + if ( + (lastCharacter === ' ' && character !== ' ') || + lastCharacter !== ' ' + ) { + treatedCharacters.push(character); + } + lastCharacter = character; + }); + return treatedCharacters.join(''); + +} + /** * Returns the input text that was inserted in the $inputText element. - * It makes a treatment to remove whitespaces that are on the start and end - * of the text. + * + * It removes white spaces from the start and end of the text as well as extra + * white spaces that have been added between the words. * * @returns {string} The input text that was inserted in the $inputText * element with some treatments. */ function getInputText() { - return $inputText.value.trim(); + return removeWhiteSpaces($inputText.value); } /** @@ -63,20 +110,20 @@ function getInputText() { * @returns {number} The pitch rate. */ function calculatePitchRate() { - return 0.4 * 4 ** $pitchSlider.value + 0.2; + return 0.4 * 4 ** + $pitchSlider.value + 0.2; } /** - * Returns the speak interval in miliseconds based on the value of the + * Returns the speak interval in milliseconds based on the value of the * $intervalSlider element in the page. * * If the $intervalSlider has not been changed, it returns 175 by default, * that is the value set in the HTML page. * - * @returns {number} The speak interval in miliseconds. + * @returns {number} The speak interval in milliseconds. */ -function calculateIntervalInMiliseconds() { - return $intervalSlider.value; +function calculateIntervalInMilliseconds() { + return + $intervalSlider.value; } /** @@ -94,7 +141,6 @@ async function createDecodedAudioDataFromVoiceFile(fileName) { const voiceFileDirectoryPath = 'assets/voices/' + fileName; const fileResponse = await fetch(voiceFileDirectoryPath); const arrayBuffer = await fileResponse.arrayBuffer(); - const audioContext = new AudioContext(); return audioContext.decodeAudioData(arrayBuffer); } @@ -107,7 +153,6 @@ async function createDecodedAudioDataFromVoiceFile(fileName) { * `createDecodedAudioDataFromVoiceFile`. */ function playDecodedAudioData(decodedAudioData) { - const audioContext = new AudioContext(); const source = audioContext.createBufferSource(); const pitchRate = calculatePitchRate(); @@ -122,8 +167,7 @@ function playDecodedAudioData(decodedAudioData) { * and make too much noise. */ function cancelPreviousSpeakTimeouts() { - const hasSpeakTimeoutIds = speakTimeoutsIds.length > 0; - if (hasSpeakTimeoutIds) { + if (speakTimeoutsIds.length > 0) { speakTimeoutsIds.forEach((speakTimeoutId) => { clearTimeout(speakTimeoutId); }); @@ -132,28 +176,7 @@ function cancelPreviousSpeakTimeouts() { } /** - * Writes the input text inserted at the $inputText element to the - * $dialogueText element. - */ -function writeDialogueText() { - const inputText = getInputText(); - const inputTextCharacters = inputText.split(''); - const intervalInMiliseconds = calculateIntervalInMiliseconds(); - - $dialogueText.innerHTML = ''; - - inputTextCharacters.forEach(( - inputTextCharacter, - inputTextCharacterIndex - ) => { - speakTimeoutsIds.push(setTimeout(() => { - $dialogueText.innerHTML += inputTextCharacter; - }, intervalInMiliseconds * inputTextCharacterIndex)); - }); -} - -/** - * Loads the local voices. This is needed to fix the error + * Loads the local voices. This is needed to fix the warning * `The AudioContext was not allowed to start`, loading the local voices only * when the user interact with the page. * @@ -161,6 +184,7 @@ function writeDialogueText() { */ async function loadLocalVoices() { if (!localVoices) { + audioContext = new AudioContext(); localVoices = { quack: await createDecodedAudioDataFromVoiceFile('quack.mp3'), bark: await createDecodedAudioDataFromVoiceFile('bark.wav'), @@ -169,29 +193,64 @@ async function loadLocalVoices() { } } +/** + * Adds a random interval, in milliseconds, into the + * `randomIntervalsInMilliseconds` number that may be used by the + * `speakAndWriteToDialogueText` function to create a more natural feeling, + * being applied whenever there is a white space in the input text. + */ +function addRandomIntervalInMilliseconds() { + const intervalInMilliseconds = calculateIntervalInMilliseconds(); + const minimumIntervalValueInMilliseconds = 100; + const maximumIntervalValueInMilliseconds = 3e4 / intervalInMilliseconds; + const randomIntervalInMilliseconds = + Math.floor(Math.random() * maximumIntervalValueInMilliseconds) + + minimumIntervalValueInMilliseconds; + + randomIntervalsInMilliseconds += randomIntervalInMilliseconds; +} + +/** + * Cancels all the previous random intervals used, by reseting the + * `randomIntervalsInMilliseconds` to zero, as it was at the start. + */ +function cancelRandomIntervals() { + randomIntervalsInMilliseconds = 0; +} + /** * Creates and plays a speak with the input text inserted in the $inputText - * element. + * element. It also, writes the text to the $dialogueText element at the same + * time it is speaking. * * @async */ -async function speak() { +async function speakAndWriteDialogue() { await loadLocalVoices(); cancelPreviousSpeakTimeouts(); + cancelRandomIntervals(); const inputText = getInputText(); const inputTextCharacters = inputText.split(''); - const selectedVoiceDecodedAudioData = - localVoices[$voiceSelector.value]; - const intervalInMiliseconds = calculateIntervalInMiliseconds(); + const selectedLocalVoice = localVoices[$voiceSelector.value]; + const intervalInMilliseconds = calculateIntervalInMilliseconds(); + + $dialogueText.innerHTML = ''; inputTextCharacters.forEach(( - _inputTextCharacter, + inputTextCharacter, inputTextCharacterIndex ) => { + if (inputTextCharacter === ' ') { + addRandomIntervalInMilliseconds(); + } + speakTimeoutsIds.push(setTimeout(() => { - playDecodedAudioData(selectedVoiceDecodedAudioData); - }, intervalInMiliseconds * inputTextCharacterIndex)); + playDecodedAudioData(selectedLocalVoice); + $dialogueText.innerHTML += inputTextCharacter; + }, intervalInMilliseconds * inputTextCharacterIndex + + randomIntervalsInMilliseconds + )); }); }