Skip to content

Commit

Permalink
Add voice rate and pitch selection.
Browse files Browse the repository at this point in the history
  • Loading branch information
tkem committed Mar 26, 2024
1 parent ff78fb8 commit ecbe4a1
Show file tree
Hide file tree
Showing 15 changed files with 149 additions and 40 deletions.
2 changes: 2 additions & 0 deletions src/app/app-settings.ts
Original file line number Diff line number Diff line change
Expand Up @@ -69,6 +69,8 @@ export class Options {
speech = true;
sectors = false;
voice = '';
rate = 1000;
pitch = 1000;
}

export interface Notification {
Expand Down
2 changes: 2 additions & 0 deletions src/app/app.component.ts
Original file line number Diff line number Diff line change
Expand Up @@ -66,6 +66,8 @@ export class AppComponent implements AfterViewInit, OnInit, OnDestroy {
this.logger.setDebugEnabled(options.debug);
this.setLanguage(options.language);
this.speech.setVoice(options.voice);
this.speech.setRate(options.rate / 1000.0);
this.speech.setPitch(options.pitch / 1000.0);
});
}

Expand Down
8 changes: 8 additions & 0 deletions src/app/services/speech.service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,7 @@ class WebSpeech {
utterance.text = textOrOptions.text;
utterance.lang = textOrOptions.locale;
utterance.rate = textOrOptions.rate;
utterance.pitch = textOrOptions.pitch;
utterance.voice = this.getVoiceMap().get(textOrOptions.identifier);
}
utterance.onend = () => {
Expand Down Expand Up @@ -100,6 +101,8 @@ export class SpeechService {

private rate = 1.0;

private pitch = 1.0;

private voice: string;

private lastMessage: string;
Expand All @@ -123,6 +126,10 @@ export class SpeechService {
this.rate = rate;
}

setPitch(pitch: number) {
this.pitch = pitch;
}

setVoice(voice: string) {
this.voice = voice;
}
Expand All @@ -138,6 +145,7 @@ export class SpeechService {
text: message,
locale: this.locale || 'en-us',
rate: this.rate,
pitch: this.pitch,
identifier: this.voice || null
}).then(() => {
if (this.pending === 0) {
Expand Down
3 changes: 2 additions & 1 deletion src/app/settings/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,4 +4,5 @@ export * from './about.page';
export * from './logging.page';
export * from './licenses.page';
export * from './connection.page';
export * from './notifications.page';
export * from './notifications.page';
export * from './voice.page';
6 changes: 6 additions & 0 deletions src/app/settings/settings.module.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ import { LoggingMenu } from './logging.menu';
import { LoggingPage } from './logging.page';
import { NotificationsPage } from './notifications.page';
import { SettingsPage } from './settings.page';
import { VoicePage } from './voice.page';

const routes: Routes = [
{
Expand All @@ -39,6 +40,10 @@ const routes: Routes = [
{
path: 'notifications',
component: NotificationsPage
},
{
path: 'voice',
component: VoicePage
}
];

Expand All @@ -50,6 +55,7 @@ const routes: Routes = [
LoggingMenu,
LoggingPage,
NotificationsPage,
VoicePage,
SettingsPage
],
exports: [
Expand Down
9 changes: 3 additions & 6 deletions src/app/settings/settings.page.html
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@
<ion-content>
<ion-list lines="full">
<ion-item>
<ion-select label="{{'Language' | translate}}" [(ngModel)]="options.language" (ionChange)="update()" cancelText="{{'Cancel' | translate}}" okText="{{'OK' | translate}}">
<ion-select label="{{'Language' | translate}}" [(ngModel)]="options.language" (ionChange)="updateLanguage()" cancelText="{{'Cancel' | translate}}" okText="{{'OK' | translate}}">
<!-- ion-option seems to require translate attribute value; see
https://github.com/ionic-team/ionic/issues/8561#issuecomment-391079689
-->
Expand All @@ -25,11 +25,8 @@
<ion-select-option value="sk">Slovak</ion-select-option>
</ion-select>
</ion-item>
<ion-item>
<ion-select label="{{'Voice' | translate}}" [disabled]="!options.language || voices.length < 2" [(ngModel)]="options.voice" (ionChange)="updateAndGreet()" cancelText="{{'Cancel' | translate}}" okText="{{'OK' | translate}}">
<ion-select-option translate value="">Default</ion-select-option>
<ion-select-option *ngFor="let v of voices" value="{{v.identifier}}">{{v.name}}</ion-select-option>
</ion-select>
<ion-item routerLink="/settings/voice">
<ion-label translate>Voice</ion-label>
</ion-item>
<ion-item routerLink="/settings/connection">
<ion-label translate>Connection</ion-label>
Expand Down
32 changes: 5 additions & 27 deletions src/app/settings/settings.page.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,5 @@
import { Component, OnDestroy, OnInit } from '@angular/core';

import { TranslateService } from '@ngx-translate/core';

import { AboutPage } from './about.page';
import { ConnectionPage } from './connection.page';
import { LicensesPage } from './licenses.page';
Expand All @@ -15,24 +13,16 @@ import { I18nAlertService, SpeechService } from '../services';
templateUrl: 'settings.page.html'
})
export class SettingsPage implements OnDestroy, OnInit {
aboutPage = AboutPage;
connectionPage = ConnectionPage;
licensesPage = LicensesPage;
loggingPage = LoggingPage;
notificationsPage = NotificationsPage;

options = new Options();

voices = [];

private subscription: any;

constructor(private alert: I18nAlertService, private settings: AppSettings, private speech: SpeechService, private translate: TranslateService) {}
constructor(private alert: I18nAlertService, private settings: AppSettings, private speech: SpeechService) {}

ngOnInit() {
this.subscription = this.settings.getOptions().subscribe(options => {
this.options = options;
this.updateVoices();
});
}

Expand All @@ -53,27 +43,15 @@ export class SettingsPage implements OnDestroy, OnInit {
});
}

async update() {
await this.updateVoices();
return this.settings.setOptions(this.options);
}

async updateVoices() {
async updateLanguage() {
if (this.options.language) {
this.voices = await this.speech.getVoices(this.options.language);
if (!this.voices.find(v => v.identifier == this.options.voice)) {
let voices = await this.speech.getVoices(this.options.language);
if (!voices.find(v => v.identifier == this.options.voice)) {
this.options.voice = "";
}
} else {
this.voices = [];
this.options.voice = "";
}
}

async updateAndGreet() {
await this.update();
// TODO: trigger when voice is selected
const greeting = this.translate.instant("notifications.greeting");
this.speech.speak(greeting);
return this.settings.setOptions(this.options);
}
}
48 changes: 48 additions & 0 deletions src/app/settings/voice.page.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
<ion-header>
<ion-toolbar>
<ion-buttons slot="start">
<ion-back-button defaultHref="/"></ion-back-button>
</ion-buttons>
<ion-title>
<span translate>Voice</span>
</ion-title>
</ion-toolbar>
</ion-header>

<ion-content>
<ion-list>
<ion-item>
<ion-select label="{{'Voice' | translate}}" [disabled]="!options.language || voices.length < 2" [(ngModel)]="options.voice" (ionChange)="update()" cancelText="{{'Cancel' | translate}}" okText="{{'OK' | translate}}">
<ion-select-option translate value="">Default</ion-select-option>
<ion-select-option *ngFor="let v of voices" value="{{v.identifier}}">{{v.name}}</ion-select-option>
</ion-select>
</ion-item>
<ion-item>
<ion-label class="ion-text-wrap" translate>
Rate
</ion-label>
<ion-range legacy="true" slot="end" [(ngModel)]="options.rate" (ionChange)="update()" min="700" max="1300">
</ion-range>
<ion-buttons slot="end">
<ion-button fill="clear" (click)="resetRate()">
<ion-icon name="refresh-outline"></ion-icon>
</ion-button>
</ion-buttons>
</ion-item>
<ion-item>
<ion-label class="ion-text-wrap" translate>
Pitch
</ion-label>
<ion-range legacy="true" slot="end" [(ngModel)]="options.pitch" (ionChange)="update()" min="500" max="1500">
</ion-range>
<ion-buttons slot="end">
<ion-button fill="clear" (click)="resetPitch()">
<ion-icon name="refresh-outline"></ion-icon>
</ion-button>
</ion-buttons>
</ion-item>
<ion-item>
<ion-button (click)="test()">Test</ion-button>
</ion-item>
</ion-list>
</ion-content>
55 changes: 55 additions & 0 deletions src/app/settings/voice.page.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,55 @@
import { Component, OnDestroy } from '@angular/core';

import { TranslateService } from '@ngx-translate/core';

import { AppSettings, Options } from '../app-settings';
import { SpeechService } from '../services';


@Component({
templateUrl: 'voice.page.html'
})
export class VoicePage implements OnDestroy {

options = new Options();

voices = [];

private subscription: any;

constructor(private settings: AppSettings, private speech: SpeechService, private translate: TranslateService) {}

ngOnInit() {
this.subscription = this.settings.getOptions().subscribe(options => {
this.options = options;
this.updateVoices();
});
}

ngOnDestroy() {
this.subscription.unsubscribe();
}

resetRate() {
this.options.rate = 1000;
this.update();
}

resetPitch() {
this.options.pitch = 1000;
this.update();
}

async update() {
return this.settings.setOptions(this.options);
}

async test() {
const example = this.translate.instant("notifications.example");
this.speech.speak(example);
}

async updateVoices() {
this.voices = await this.speech.getVoices(this.options.language);
}
}
4 changes: 3 additions & 1 deletion src/assets/i18n/de.json
Original file line number Diff line number Diff line change
Expand Up @@ -71,10 +71,12 @@
"Open source licenses": "Open-Source-Lizenzen",
"Order by number": "Nach Nummer ordnen",
"Pace Car": "Pace Car",
"Pitch": "Tonhöhe",
"Privacy policy": "Datenschutz-Bestimmungen",
"Qualifying": "Qualifying",
"Race finished": "Rennen beendet",
"Race": "Rennen",
"Rate": "Geschwindigkeit",
"Reconnect": "Erneut verbinden",
"Reconnect delay": "Verzögerung beim erneuten Verbinden",
"Request timeout": "Zeitlimit für Anfragen",
Expand All @@ -99,7 +101,7 @@

"notifications": {
"locale": "de-DE",
"greeting": "Hallo!",
"example": "Dies ist ein Beispiel für Sprachsynthese in Deutsch",
"bestlap": "Schnellste Runde!",
"bests1": "Schnellster Sektor 1!",
"bests2": "Schnellster Sektor 2!",
Expand Down
4 changes: 3 additions & 1 deletion src/assets/i18n/en.json
Original file line number Diff line number Diff line change
Expand Up @@ -71,10 +71,12 @@
"Open source licenses": "Open source licenses",
"Order by number": "Order by number",
"Pace Car": "Pace Car",
"Pitch": "Pitch",
"Privacy policy": "Privacy policy",
"Qualifying": "Qualifying",
"Race finished": "Race finished",
"Race": "Race",
"Rate": "Rate",
"Reconnect": "Reconnect",
"Reconnect delay": "Reconnect delay",
"Request timeout": "Request timeout",
Expand All @@ -99,7 +101,7 @@

"notifications": {
"locale": "en-US",
"greeting": "Hello!",
"example": "This is an example for speech synthesis in English",
"bestlap": "Fastest lap!",
"bests1": "Fastest sector 1!",
"bests2": "Fastest sector 2!",
Expand Down
4 changes: 3 additions & 1 deletion src/assets/i18n/es.json
Original file line number Diff line number Diff line change
Expand Up @@ -71,10 +71,12 @@
"Open source licenses": "Licencia open source",
"Order by number": "Ordenar por número",
"Pace Car": "Coche de seguridad",
"Pitch": "Paso",
"Privacy policy": "Política de privacidad",
"Qualifying": "Clasificación",
"Race finished": "Carrera terminada",
"Race": "Carrera",
"Rate": "Velocidad",
"Reconnect": "Reconectar",
"Reconnect delay": "Retardo para reconexión",
"Request timeout": "Solicitud de tiempo de espera",
Expand All @@ -99,7 +101,7 @@

"notifications": {
"locale": "es-ES",
"greeting": "¡Hola!",
"example": "Este es un ejemplo de síntesis de voz en español",
"bestlap": "¡Vuelta rápida!",
"bests1": "¡Mejor sector 1!",
"bests2": "¡Mejor sector 2!",
Expand Down
4 changes: 3 additions & 1 deletion src/assets/i18n/fr.json
Original file line number Diff line number Diff line change
Expand Up @@ -71,10 +71,12 @@
"Open source licenses": "Licence Open source",
"Order by number": "Ordre par numéro",
"Pace Car": "Pace Car",
"Pitch": "Pas",
"Privacy policy": "Politique de confidentialité",
"Qualifying": "Qualifications",
"Race finished": "Course terminée",
"Race": "Course",
"Rate": "Vitesse",
"Reconnect": "Reconnexion",
"Reconnect delay": "Délai de reconnexion",
"Request timeout": "Demande de délai d'attente",
Expand All @@ -99,7 +101,7 @@

"notifications": {
"locale": "fr-FR",
"greeting": "Bonjour!",
"example": "Ceci est un exemple de synthèse vocale en français",
"bestlap": "Meilleur tour!",
"bests1": "Meilleur intermédiaire 1!",
"bests2": "Meilleur intermédiaire 2!",
Expand Down
4 changes: 3 additions & 1 deletion src/assets/i18n/it.json
Original file line number Diff line number Diff line change
Expand Up @@ -71,10 +71,12 @@
"Open source licenses": "Licenze open source",
"Order by number": "In ordine di numero",
"Pace Car": "Pace Car",
"Pitch": "Pece",
"Privacy policy": "Politica sulla riservatezza",
"Qualifying": "Qualificazioni",
"Race finished": "Corsa finita",
"Race": "Corsa",
"Rate": "Velocità",
"Reconnect": "Ricollegamento",
"Reconnect delay": "Attesa per il ricollegamento",
"Request timeout": "Tempo massimo per la richiesta",
Expand All @@ -99,7 +101,7 @@

"notifications": {
"locale": "it-IT",
"greeting": "Ciao!",
"example": "Questo è un esempio di sintesi vocale in italiano",
"bestlap": "Giro più veloce!",
"bests1": "Miglior settore 1!",
"bests2": "Miglior settore 2!",
Expand Down
Loading

0 comments on commit ecbe4a1

Please sign in to comment.