From db2434d4dc0eab4586cd086c44612510b820f85a Mon Sep 17 00:00:00 2001 From: Miika Alonen Date: Wed, 30 Aug 2023 22:08:23 +0300 Subject: [PATCH] Add speech as string prototype --- src/API.ts | 17 ++++---- src/Documentation.ts | 35 ++++++++++++++++- src/StringExtensions.ts | 86 +++++++++++++++++++++++++++++++++++++++++ src/main.ts | 2 + 4 files changed, 130 insertions(+), 10 deletions(-) create mode 100644 src/StringExtensions.ts 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