From 3c335a50a96b07beeec4baeeae624c54d4d47ea0 Mon Sep 17 00:00:00 2001 From: Miika Alonen Date: Thu, 31 Aug 2023 00:22:58 +0300 Subject: [PATCH] Added speaker as promise --- src/API.ts | 16 ++++----- src/Documentation.ts | 31 +++++++++++++--- src/StringExtensions.ts | 78 ++++++++++++++++++++++++++++++----------- 3 files changed, 91 insertions(+), 34 deletions(-) diff --git a/src/API.ts b/src/API.ts index 6e69112..4d4b373 100644 --- a/src/API.ts +++ b/src/API.ts @@ -15,6 +15,7 @@ import { soundMap, // @ts-ignore } from "superdough"; +import { Speaker } from "./StringExtensions"; interface ControlChange { channel: number; @@ -1292,7 +1293,7 @@ export class UserAPI { // Speech synthesis // ============================================================= - speak = (text: string, voice: number, rate: number = 1, pitch: number = 1): void => { + speak = (text: string, lang: string = "en-US", voice: number = 0, rate: number = 1, pitch: number = 1): void => { /* * Speaks the given text using the browser's speech synthesis API. * @param text - The text to speak @@ -1301,13 +1302,12 @@ export class UserAPI { * @param pitch - The pitch at which to speak the text * */ - const synth = window.speechSynthesis; - synth.cancel(); - const utterance = new SpeechSynthesisUtterance(text); - utterance.voice = speechSynthesis.getVoices()[voice]; - utterance.rate = rate; - utterance.pitch = pitch; - synth.speak(utterance); + const speaker = new Speaker({text: text, lang: lang, voice: voice, rate: rate, pitch: pitch}); + speaker.speak().then(() => { + // Done speaking + }).catch((err) => { + console.log(err); + }); }; // ============================================================= diff --git a/src/Documentation.ts b/src/Documentation.ts index 87d7dc7..933186d 100644 --- a/src/Documentation.ts +++ b/src/Documentation.ts @@ -1579,16 +1579,21 @@ mod(0.25) :: sound('sine') # Speech synthesis -Topos can also speak using [Web Speec API](https://developer.mozilla.org/en-US/docs/Web/API/Web_Speech_API)! +Topos can also speak using the Web Speech API. Speech synthesis can be used in two ways: -Speech synthesis can be used in two ways: - -- speak(text: string, voice: number, rate: number, pitch: number): speak the given text. +- speak(text: string, lang: string, voice: number, rate: number, pitch: number, volume: number): speak the given text. Or by using string and chaining: - "Hello".rate(1.5).pitch(0.5).speak(). +Value ranges for the different parameters are: +- lang(string): language code, for example en for English, fr for French or with the country code for example British English en-GB. See supported values from the [list](https://cloud.google.com/speech-to-text/docs/speech-to-text-supported-languages). +- voice(number): voice index, for example 0 for the first voice, 1 for the second voice, etc. +- rate(number): speaking rate, from 0.0 to 10. +- pitch(number): speaking pitch, from 0.0 to 2. +- volume(number): speaking volume, from 0.0 to 1.0. + Examples: ${makeExample( @@ -1602,7 +1607,7 @@ mod(4) :: speak("Hello world!") ${makeExample( "Different voices", ` -mod(2) :: speak("Topos!",irand(0,25)) +mod(2) :: speak("Topos!","fr",irand(0,5)) `, false )} @@ -1628,6 +1633,22 @@ ${makeExample( false )} +${makeExample( + "String chaining with array chaining", + ` + const croissant = ["Croissant!", "Volant", "Arc-en-ciel", "Chocolat", "Dansant", "Nuage", "Tournant", "Galaxie", "Chatoyant", "Flamboyant", "Cosmique"]; + + onbeat(1) :: croissant.bar() + .lang("fr") + .volume(rand(0.2,2.0)) + .rate(rand(.4,.6)) + .speak(); + + `, + false +)} + + diff --git a/src/StringExtensions.ts b/src/StringExtensions.ts index d34839d..ecde428 100644 --- a/src/StringExtensions.ts +++ b/src/StringExtensions.ts @@ -11,44 +11,53 @@ declare global { pitch(pitch: number): string; volume(volume: number): string; voice(voice: number): string; + lang(language: string): string; options(): SpeechOptions; } } -const speechOptionsMap = new Map(); +const isJsonString = (str: string):boolean => { + return str[0] === '{' && str[str.length - 1] === '}' +} + +const stringObject = (str: string, params: object) => { + if(isJsonString(str)) { + const obj = JSON.parse(str); + return JSON.stringify({...obj, ...params}); + } else { + return JSON.stringify({...params, text: str}); + } +} export const makeStringExtensions = (api: UserAPI) => { String.prototype.speak = function () { - const options = speechOptionsMap.get(this.valueOf()) || {}; - new Speech({ ...options, text: this.valueOf() }).say(); + const options = JSON.parse(this.valueOf()); + console.log("SPEAKING:", options); + new Speaker({ ...options, text: options.text }).speak().then(() => { + // Done + }).catch((e) => { + console.log("Error speaking:", e); + }); }; String.prototype.rate = function (speed: number) { - const options = speechOptionsMap.get(this.valueOf()) || {}; - speechOptionsMap.set(this.valueOf(), { ...options, rate: speed }); - return this.valueOf(); + return stringObject(this.valueOf(), {rate: speed}); }; String.prototype.pitch = function (pitch: number) { - const options = speechOptionsMap.get(this.valueOf()) || {}; - speechOptionsMap.set(this.valueOf(), { ...options, pitch: pitch }); - return this.valueOf(); + return stringObject(this.valueOf(), {pitch: pitch}); + }; + + String.prototype.lang = function (language: string) { + return stringObject(this.valueOf(),{lang: language}); }; String.prototype.volume = function (volume: number) { - const options = speechOptionsMap.get(this.valueOf()) || {}; - speechOptionsMap.set(this.valueOf(), { ...options, volume: volume }); - return this.valueOf(); + return stringObject(this.valueOf(), {volume: volume}); }; String.prototype.voice = function (voice: number) { - const options = speechOptionsMap.get(this.valueOf()) || {}; - speechOptionsMap.set(this.valueOf(), { ...options, voice: voice }); - return this.valueOf(); - }; - - String.prototype.options = function (): SpeechOptions { - return speechOptionsMap.get(this.valueOf()) || {}; + return stringObject(this.valueOf(), {voice: voice}); }; String.prototype.z = function () { @@ -62,14 +71,16 @@ type SpeechOptions = { pitch?: number; volume?: number; voice?: number; + lang?: string; } -class Speech { +export class Speaker { constructor( public options: SpeechOptions ) {} - say = () => { + speak = () => { + return new Promise((resolve, reject) => { if (this.options.text) { const synth = window.speechSynthesis; synth.cancel(); @@ -80,7 +91,32 @@ class Speech { if (this.options.voice) { utterance.voice = synth.getVoices()[this.options.voice]; } + if(this.options.lang) { + // Check if language has country code + if (this.options.lang.length === 2) { + utterance.lang = `${this.options.lang}-${this.options.lang.toUpperCase()}` + } else if (this.options.lang.length === 5) { + utterance.lang = this.options.lang; + } else { + // Fallback to en us + utterance.lang = 'en-US'; + } + } + + utterance.onend = () => { + resolve(); + }; + + utterance.onerror = (error) => { + reject(error); + }; + synth.speak(utterance); + + } else { + reject("No text provided"); } + + }); } }