diff --git a/src/API.ts b/src/API.ts
index c1fb24e..6e69112 100644
--- a/src/API.ts
+++ b/src/API.ts
@@ -358,7 +358,7 @@ export class UserAPI {
input: string,
options: InputOptions = {},
id: number | string = ""
- ) => {
+ ): Player => {
const zid = "z" + id.toString();
const key = id === "" ? this.generateCacheKey(input, options) : zid;
@@ -1292,24 +1292,23 @@ export class UserAPI {
// Speech synthesis
// =============================================================
- speak = (text: string, index: number, rate: number = 1, pitch: number = 1): void => {
+ speak = (text: string, voice: number, rate: number = 1, pitch: number = 1): void => {
/*
* Speaks the given text using the browser's speech synthesis API.
* @param text - The text to speak
- * @param index - The index of the voice to use
+ * @param voice - The index of the voice to use
+ * @param rate - The rate at which to speak the text
+ * @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()[index];
+ utterance.voice = speechSynthesis.getVoices()[voice];
utterance.rate = rate;
utterance.pitch = pitch;
synth.speak(utterance);
- }
-
- say = this.speak;
-
-
+ };
// =============================================================
// Trivial functions
diff --git a/src/Documentation.ts b/src/Documentation.ts
index 2f94e59..121b2c7 100644
--- a/src/Documentation.ts
+++ b/src/Documentation.ts
@@ -1581,6 +1581,16 @@ mod(0.25) :: sound('sine')
Topos can also speak using [Web Speec API](https://developer.mozilla.org/en-US/docs/Web/API/Web_Speech_API)!
+Speech synthesis can be used in two ways:
+
+- speak(text: string, voice: number, rate: number, pitch: number): speak the given text.
+
+Or by using string and chaining:
+
+- "Hello".rate(1.5).pitch(0.5).speak().
+
+Examples:
+
${makeExample(
"Hello world!",
`
@@ -1590,13 +1600,36 @@ mod(4) :: speak("Hello world!")
)}
${makeExample(
- "Speak with a different voice",
+ "Different voices",
`
mod(2) :: speak("Topos!",irand(0,25))
`,
false
)}
+${makeExample(
+ "Chaining string",
+ `
+ onbeat(1,3) :: "Foobaba".voice(irand(0,10)).speak()
+ `,
+ false
+)}
+
+${makeExample(
+ "Building string and chaining",
+ `
+ const subject = ["coder","user","loser"].pick()
+ const verb = ["is", "was", "isnt"].pick()
+ const object = ["happy","sad","tired"].pick()
+ const sentence = subject+" "+verb+" "+" "+object
+
+ mod(6) :: sentence.pitch(0).rate(0).voice([0,2].pick()).say()
+ `,
+ false
+)}
+
+
+
`;
diff --git a/src/StringExtensions.ts b/src/StringExtensions.ts
new file mode 100644
index 0000000..d34839d
--- /dev/null
+++ b/src/StringExtensions.ts
@@ -0,0 +1,86 @@
+import { type UserAPI } from "./API";
+import { Player } from "./classes/ZPlayer";
+export {};
+
+// Extend String prototype
+declare global {
+ interface String {
+ z(): Player;
+ speak(): void;
+ rate(speed: number): string;
+ pitch(pitch: number): string;
+ volume(volume: number): string;
+ voice(voice: number): string;
+ options(): SpeechOptions;
+ }
+}
+
+const speechOptionsMap = new Map();
+
+export const makeStringExtensions = (api: UserAPI) => {
+ String.prototype.speak = function () {
+ const options = speechOptionsMap.get(this.valueOf()) || {};
+ new Speech({ ...options, text: this.valueOf() }).say();
+ };
+
+ String.prototype.rate = function (speed: number) {
+ const options = speechOptionsMap.get(this.valueOf()) || {};
+ speechOptionsMap.set(this.valueOf(), { ...options, rate: speed });
+ return this.valueOf();
+ };
+
+ String.prototype.pitch = function (pitch: number) {
+ const options = speechOptionsMap.get(this.valueOf()) || {};
+ speechOptionsMap.set(this.valueOf(), { ...options, pitch: pitch });
+ return this.valueOf();
+ };
+
+ String.prototype.volume = function (volume: number) {
+ const options = speechOptionsMap.get(this.valueOf()) || {};
+ speechOptionsMap.set(this.valueOf(), { ...options, volume: volume });
+ return this.valueOf();
+ };
+
+ 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()) || {};
+ };
+
+ String.prototype.z = function () {
+ return api.z(this.valueOf());
+ };
+}
+
+type SpeechOptions = {
+ text?: string;
+ rate?: number;
+ pitch?: number;
+ volume?: number;
+ voice?: number;
+}
+
+class Speech {
+ constructor(
+ public options: SpeechOptions
+ ) {}
+
+ say = () => {
+ if (this.options.text) {
+ const synth = window.speechSynthesis;
+ synth.cancel();
+ const utterance = new SpeechSynthesisUtterance(this.options.text);
+ utterance.rate = this.options.rate || 1;
+ utterance.pitch = this.options.pitch || 1;
+ utterance.volume = this.options.volume || 1;
+ if (this.options.voice) {
+ utterance.voice = synth.getVoices()[this.options.voice];
+ }
+ synth.speak(utterance);
+ }
+ }
+}
diff --git a/src/main.ts b/src/main.ts
index c0d454a..d1274d5 100644
--- a/src/main.ts
+++ b/src/main.ts
@@ -30,6 +30,7 @@ import { gzipSync, decompressSync, strFromU8 } from "fflate";
import showdown from "showdown";
showdown.setFlavor("github");
import showdownHighlight from "showdown-highlight";
+import { makeStringExtensions } from "./StringExtensions";
const classMap = {
h1: "text-white lg:text-4xl text-xl lg:ml-4 lg:mx-4 mx-2 lg:my-4 my-2 lg:mb-4 mb-4 bg-neutral-900 rounded-lg py-2 px-2",
h2: "text-white lg:text-3xl text-xl lg:ml-4 lg:mx-4 mx-2 lg:my-4 my-2 lg:mb-4 mb-4 bg-neutral-900 rounded-lg py-2 px-2",
@@ -219,6 +220,7 @@ export class Editor {
this.api = new UserAPI(this);
makeArrayExtensions(this.api);
+ makeStringExtensions(this.api);
// ================================================================================
// CodeMirror Management