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");
}
+
+ });
}
}