apphouse
Version:
Component library for React that uses observable state management and theme-able components.
282 lines (254 loc) • 7.46 kB
text/typescript
import { makeAutoObservable, runInAction } from 'mobx';
import { getErrorMessage, keys } from '../..';
const defaultVoice: SpeechSynthesisVoice = {
name: 'Alex',
lang: 'en-US',
localService: false,
voiceURI: 'Alex',
default: true
};
export interface IUtterance extends SpeechSynthesisUtterance {
id: string;
}
export type IUtteranceStatus = 'idle' | 'speaking' | 'ended' | 'error';
export interface IUtteranceOptions {
volume?: number;
rate?: number;
pitch?: number;
lang?: string;
voice?: SpeechSynthesisVoice;
onstart?: () => void;
onend?: () => void;
onerror?: () => void;
onpause?: () => void;
onresume?: () => void;
onmark?: () => void;
onboundary?: () => void;
}
/**
* A class that represents a speech synthesis utterance
* It is a wrapper around the SpeechSynthesisUtterance class
*/
export class Utterance {
id: string;
utterance?: SpeechSynthesisUtterance;
retryCount: number;
error: string | null;
status: IUtteranceStatus;
userOptions: IUtteranceOptions;
text: string;
rate: number;
volume: number;
pitch: number;
lang: string;
voice: SpeechSynthesisVoice;
mark: SpeechSynthesisEvent | null;
/**
* It marks the character position that has been read by the speech
* synthesis engine.
*/
readerIndex: number;
read: string;
unread: string;
constructor(id: string, text: string, options: IUtteranceOptions = {}) {
this.utterance = new SpeechSynthesisUtterance();
this.id = id;
this.retryCount = 0;
this.error = null;
this.status = 'idle';
this.userOptions = options;
this.text = '';
this.rate = options?.rate || 1;
this.volume = options?.volume || 1;
this.pitch = options?.pitch || 1;
this.lang = options?.lang || 'en-US';
this.voice = options?.voice || defaultVoice;
this.mark = null;
this.readerIndex = 0;
this.read = '';
this.unread = text;
this.setUtteranceOptions(options);
this.setUtterance(options);
this.setText(text);
makeAutoObservable(this);
}
restart = () => {
this.utterance = new SpeechSynthesisUtterance();
const options = this.userOptions;
this.setUtteranceOptions(options);
this.setUtterance(options);
this.setReaderIndex(0);
};
setVolume = (volume: number) => {
if (this.utterance) {
this.utterance.volume = volume;
this.volume = volume;
}
};
setRate = (rate: number) => {
runInAction(() => {
if (this.utterance) {
this.utterance.rate = rate;
this.rate = rate;
}
});
};
setMark = (mark: string) => {
if (this.utterance) {
this.utterance.text = mark;
this.text = mark;
}
};
setVoice = (voice: SpeechSynthesisVoice) => {
if (this.utterance) {
this.utterance.voice = voice;
this.voice = voice;
}
};
setPitch = (pitch: number) => {
if (this.utterance) {
this.utterance.pitch = pitch;
this.pitch = pitch;
}
};
setReaderIndex = (index: number) => {
runInAction(() => {
this.readerIndex = index;
});
};
/**
* Increments the retry count
*/
setRetry = () => {
this.retryCount = this.retryCount + 1;
};
private setUtteranceOptions = (options: IUtteranceOptions) => {
try {
keys(options).forEach((key) => {
if (this.utterance) {
// do not set the option if it is one of the options we set internally
if (this.isNotOfOverwrittenValues(key)) {
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
// @ts-ignore
const value = options[key];
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
// @ts-ignore
this.utterance[key] = value;
}
}
});
} catch (error) {
console.log({ error });
}
};
private isNotOfOverwrittenValues = (option: string) => {
return (
!option.includes('onstart') &&
!option.includes('onend') &&
!option.includes('onerror') &&
!option.includes('voice')
);
};
private setUtterance = (options: IUtteranceOptions) => {
const setError = this.setError;
const setStatus = this.setStatus;
const setReaderIndex = this.setReaderIndex;
const setRead = this.setRead;
const setUnread = this.setUnread;
const read = this.read;
if ('speechSynthesis' in window) {
// Create new SpeechSynthesisUtterance object
const utterance = this.utterance;
if (utterance) {
utterance.volume = options?.volume || 1;
utterance.rate = options?.rate || 1;
utterance.pitch = options?.pitch || 1;
utterance.lang = options?.lang || 'en-US';
// utterance.voice = options?.voice || defaultVoice;
if (utterance) {
utterance.onstart = () => {
// console.log('onstart', { id: this.id });
options.onstart && options.onstart();
setStatus('speaking');
};
utterance.onerror = (error) => {
// console.log('error', { id: this.id, error });
options.onerror && options.onerror();
setError(getErrorMessage(error));
setStatus('error');
};
utterance.onend = () => {
// console.log('ended', { id: this.id });
options.onend && options.onend();
setStatus('ended');
};
utterance.onmark = (event) => {
// console.log('onmark', { id: this.id });
options.onmark && options.onmark();
console.log('onmark', event);
};
utterance.onboundary = (event) => {
// console.log('onboundary', { id: this.id });
options.onboundary && options.onboundary();
const text = this.text;
if (event.name === 'word') {
setReaderIndex(event.charIndex + event.charLength);
// mark the word as read
const _read = text.slice(0, event.charIndex + event.charLength);
const unreadText = text.slice(event.charIndex + event.charLength);
setRead(read + _read);
setUnread(unreadText);
}
};
}
}
return utterance;
}
};
setRead = (read: string) => {
runInAction(() => {
this.read = read;
});
};
setUnread = (unread: string) => {
runInAction(() => {
this.unread = unread;
});
};
/**
* Sets the status of the utterance.
* Possible values: idle, active, ended, error.
* Use idle when the utterance has been created but not started.
* Use active when the utterance is being spoken.
* Use ended when the utterance has finished being spoken.
* Use error when the utterance has encountered an error.
* @param status IUtteranceStatus
*/
private setStatus = (status: IUtteranceStatus) => {
runInAction(() => {
this.status = status;
});
};
/**
* Sets the error property in this utterance
* @param error the error
*/
private setError = (error: string) => {
runInAction(() => {
this.error = error;
});
};
/**
* Sets the text property in this utterance.
* This is the text that will be spoken.
* @param text the text to speak
*/
setText = (text: string) => {
runInAction(() => {
this.text = text;
if (this.utterance) {
this.utterance.text = text;
}
});
};
}