Skip to content

Commit

Permalink
feat(useSpeechSynthesis): new function (#837)
Browse files Browse the repository at this point in the history
* feat(useSpeechSynthesis): new function

* chore: use longer text for test
  • Loading branch information
YunYouJun committed Oct 16, 2021
1 parent 3e76f6a commit 4dd83e3
Show file tree
Hide file tree
Showing 6 changed files with 295 additions and 0 deletions.
6 changes: 6 additions & 0 deletions packages/.vitepress/theme/styles/demo.css
Expand Up @@ -71,6 +71,12 @@
--c-brand-active: #d67e36;
}

button.red {
--c-brand: #f56c6c;
--c-brand-dark: #e41c1c;
--c-brand-active: #e94c4c;
}

button:hover {
background-color: var(--c-brand-active);
}
Expand Down
1 change: 1 addition & 0 deletions packages/core/index.ts
Expand Up @@ -63,6 +63,7 @@ export * from './useScroll'
export * from './useSessionStorage'
export * from './useShare'
export * from './useSpeechRecognition'
export * from './useSpeechSynthesis'
export * from './useStorage'
export * from './useSwipe'
export * from './useTemplateRefsList'
Expand Down
89 changes: 89 additions & 0 deletions packages/core/useSpeechSynthesis/demo.vue
@@ -0,0 +1,89 @@
<script setup lang="ts">
import { ref } from 'vue-demi'
import { useSpeechSynthesis } from '.'
const lang = ref('en-US')
const text = ref('Hello, everyone! Good morning!')
const speech = useSpeechSynthesis(text, {
lang,
})
console.log(speech.isPlaying.value)
let synth: SpeechSynthesis
const voices = ref<SpeechSynthesisVoice[]>([])
if (speech.isSupported) {
// load at last
setTimeout(() => {
synth = window.speechSynthesis
voices.value = synth.getVoices()
})
}
const play = () => {
if (speech.status.value === 'pause') {
console.log('resume')
window.speechSynthesis.resume()
}
else {
speech.speak()
}
}
const pause = () => {
window.speechSynthesis.pause()
}
const stop = () => {
window.speechSynthesis.cancel()
}
</script>

<template>
<div>
<div v-if="!speech.isSupported">
Your browser does not support SpeechSynthesis API,
<a
href="https://caniuse.com/mdn-api_speechsynthesis"
target="_blank"
>more details</a>
</div>
<div v-else>
<label class="font-bold mr-2">Spoken Text</label>
<input v-model="text" class="!inline-block" type="text" />

<br />
<label class="font-bold mr-2">Language</label>
<select v-model="lang" class="ml-5 border h-9 w-50 outline-none">
<option disabled>
Select Language
</option>
<option
v-for="(voice, i) in voices"
:key="i"
:value="voice.lang"
>
{{ `${voice.name} (${voice.lang})` }}
</option>
</select>

<div class="mt-2">
<button
:disabled="speech.isPlaying.value"
@click="play"
>
{{ speech.status.value === 'pause' ? 'Resume' : 'Speak' }}
</button>
<button :disabled="!speech.isPlaying.value" class="orange" @click="pause">
Pause
</button>
<button :disabled="!speech.isPlaying.value" class="red" @click="stop">
Stop
</button>
</div>
</div>
</div>
</template>
40 changes: 40 additions & 0 deletions packages/core/useSpeechSynthesis/index.md
@@ -0,0 +1,40 @@
---
category: Sensors
---

# useSpeechSynthesis

Reactive [SpeechSynthesis](https://developer.mozilla.org/en-US/docs/Web/API/SpeechSynthesis).

> [Can I use?](https://caniuse.com/mdn-api_speechsynthesis)
## Usage

```ts
import { useSpeechSynthesis } from '@vueuse/core'

const {
isSupported,
isPlaying,
status,
voiceInfo,
utterance,
error,

toggle,
speak
} = useSpeechSynthesis()
```

### Options

The following shows the default values of the options, they will be directly passed to [SpeechSynthesis API](https://developer.mozilla.org/en-US/docs/Web/API/SpeechSynthesis).

```ts
{
lang: 'en-US',
pitch: 1,
rate: 1,
volume: 1,
}
```
158 changes: 158 additions & 0 deletions packages/core/useSpeechSynthesis/index.ts
@@ -0,0 +1,158 @@
import { tryOnScopeDispose, MaybeRef } from '@vueuse/shared'
import { Ref, ref, watch, shallowRef, unref, computed } from 'vue-demi'
import { ConfigurableWindow, defaultWindow } from '../_configurable'

export type Status = 'init' | 'play' | 'pause' | 'end'

export type VoiceInfo = Pick<SpeechSynthesisVoice, 'lang' | 'name'>

export interface SpeechSynthesisOptions extends ConfigurableWindow {
/**
* Language for SpeechSynthesis
*
* @default 'en-US'
*/
lang?: MaybeRef<string>
/**
* Gets and sets the pitch at which the utterance will be spoken at.
*
* @default 1
*/
pitch?: SpeechSynthesisUtterance['pitch']
/**
* Gets and sets the speed at which the utterance will be spoken at.
*
* @default 1
*/
rate?: SpeechSynthesisUtterance['rate']
/**
* Gets and sets the voice that will be used to speak the utterance.
*/
voice?: SpeechSynthesisVoice
/**
* Gets and sets the volume that the utterance will be spoken at.
*
* @default 1
*/
volume?: SpeechSynthesisUtterance['volume']
}

/**
* Reactive SpeechSynthesis.
*
* @see https://vueuse.org/useSpeechSynthesis
* @see https://developer.mozilla.org/en-US/docs/Web/API/SpeechSynthesis SpeechSynthesis
* @param options
*/
export function useSpeechSynthesis(text: MaybeRef<string>, options: SpeechSynthesisOptions = {}) {
const {
pitch = 1,
rate = 1,
volume = 1,
window = defaultWindow,
} = options

const synth = window && (window as any).speechSynthesis as SpeechSynthesis
const isSupported = Boolean(synth)

const isPlaying = ref(false)
const status = ref<Status>('init')

const voiceInfo = {
lang: options.voice?.lang || 'default',
name: options.voice?.name || '',
}

const spokenText = ref(text || '')
const lang = ref(options.lang || 'en-US')
const error = shallowRef(undefined) as Ref<SpeechSynthesisErrorEvent | undefined>

const toggle = (value = !isPlaying.value) => {
isPlaying.value = value
}

const bindEventsForUtterance = (utterance: SpeechSynthesisUtterance) => {
utterance.lang = unref(lang)

options.voice && (utterance.voice = options.voice)
utterance.pitch = pitch
utterance.rate = rate
utterance.volume = volume

utterance.onstart = () => {
isPlaying.value = true
status.value = 'play'
}

utterance.onpause = () => {
isPlaying.value = false
status.value = 'pause'
}

utterance.onresume = () => {
isPlaying.value = true
status.value = 'play'
}

utterance.onend = () => {
isPlaying.value = false
status.value = 'end'
}

utterance.onerror = (event) => {
error.value = event
}

utterance.onend = () => {
isPlaying.value = false
utterance.lang = unref(lang)
}
}

const utterance = computed(() => {
isPlaying.value = false
status.value = 'init'
const newUtterance = new SpeechSynthesisUtterance(spokenText.value)
bindEventsForUtterance(newUtterance)
return newUtterance
})

const speak = () => {
synth!.cancel()
utterance && synth!.speak(utterance.value)
}

if (isSupported) {
bindEventsForUtterance(utterance.value)

watch(lang, (lang) => {
if (utterance.value && !isPlaying.value)
utterance.value.lang = lang
})

watch(isPlaying, () => {
if (isPlaying.value)
synth!.resume()
else
synth!.pause()
})
}

tryOnScopeDispose(() => {
isPlaying.value = false
})

return {
isSupported,
isPlaying,
status,
voiceInfo,
utterance,
error,

toggle,
speak,
}
}

export type UseSpeechSynthesisReturn = ReturnType<typeof useSpeechSynthesis>
1 change: 1 addition & 0 deletions packages/functions.md
Expand Up @@ -93,6 +93,7 @@
- [`useResizeObserver`](https://vueuse.org/core/useResizeObserver/) — reports changes to the dimensions of an Element's content or the border-box
- [`useScroll`](https://vueuse.org/core/useScroll/) — reactive scroll position and state
- [`useSpeechRecognition`](https://vueuse.org/core/useSpeechRecognition/) — reactive [SpeechRecognition](https://developer.mozilla.org/en-US/docs/Web/API/SpeechRecognition)
- [`useSpeechSynthesis`](https://vueuse.org/core/useSpeechSynthesis/) — reactive [SpeechSynthesis](https://developer.mozilla.org/en-US/docs/Web/API/SpeechSynthesis)
- [`useSwipe`](https://vueuse.org/core/useSwipe/) — reactive swipe detection based on [`TouchEvents`](https://developer.mozilla.org/en-US/docs/Web/API/TouchEvent)
- [`useUserMedia`](https://vueuse.org/core/useUserMedia/) — reactive [`mediaDevices.getUserMedia`](https://developer.mozilla.org/en-US/docs/Web/API/MediaDevices/getUserMedia) streaming
- [`useWindowFocus`](https://vueuse.org/core/useWindowFocus/) — reactively track window focus with `window.onfocus` and `window.onblur` events
Expand Down

0 comments on commit 4dd83e3

Please sign in to comment.