序章

何らかの音声体験を提供するアプリを操作した可能性が非常に高くなります。 テキストメッセージや通知を声に出して読むなど、テキスト読み上げ機能を備えたアプリである可能性があります。 また、SiriやGoogleアシスタントなどの音声認識機能を備えたアプリの場合もあります。

HTML5の登場により、Webプラットフォームで利用可能なAPIの数が急速に増加しました。 Web Speech API と呼ばれるAPIがいくつかあり、さまざまな種類の音声アプリケーションとWeb用のエクスペリエンスをシームレスに構築できるように開発されています。 これらのAPIはまだかなり実験的なものですが、最近のすべてのブラウザーでそれらのほとんどのサポートが増えています。

Animated gif of text-to-speech app loading, refreshing, and playing the audio for a quotation.

この記事では、ランダムな引用を取得し、引用を表示し、ブラウザがテキスト読み上げを使用して引用を読み上げる機能をユーザーに提供するアプリケーションを構築します。

前提条件

このチュートリアルを完了するには、次のものが必要です。

このチュートリアルは、ノードv14.4.0、npm v6.14.5、axios v0.19.2、cors v2.8.5、express v4.17.1、およびjQueryv3.5.1。

WebSpeechAPIの使用

Web Speech APIには、次の2つの主要なインターフェイスがあります。

  • SpeechSynthesis-テキスト読み上げアプリケーション用。 これにより、アプリはデバイスの音声合成装置を使用してテキストコンテンツを読み上げることができます。 使用可能な音声タイプはSpeechSynthesisVoiceオブジェクトで表され、発声されるテキストはSpeechSynthesisUtteranceオブジェクトで表されます。 ブラウザのサポートの詳細については、SpeechSynthesisインターフェイスのサポートテーブルを参照してください。

  • SpeechRecognition-非同期音声認識を必要とするアプリケーション向け。 これにより、アプリは音声入力から音声コンテキストを認識できます。 SpeechRecognitionオブジェクトは、コンストラクターを使用して作成できます。 SpeechGrammarインターフェースは、アプリが認識すべき文法のセットを表すために存在します。 ブラウザのサポートの詳細については、SpeechRecognitionインターフェイスのサポートテーブルを参照してください。

このチュートリアルでは、SpeechSynthesisに焦点を当てます。

参照の取得

SpeechSynthesisオブジェクトへの参照を取得するには、次の1行のコードで実行できます。

var synthesis = window.speechSynthesis;

次のコードスニペットは、ブラウザのサポートを確認する方法を示しています。

if ('speechSynthesis' in window) {
  var synthesis = window.speechSynthesis;
} else {
  console.log('Text-to-speech not supported.');
}

SpeechSynthesisが提供する機能を使用する前に、ブラウザでサポートされているかどうかを確認すると非常に便利です。

利用可能な音声の取得

このステップでは、既存のコードに基づいて、利用可能な音声を取得します。 getVoices()メソッドは、デバイスで使用可能なすべての音声を表すSpeechSynthesisVoiceオブジェクトのリストを返します。

次のコードスニペットを見てください。

if ('speechSynthesis' in window) {
  var synthesis = window.speechSynthesis;

  // Regex to match all English language tags e.g en, en-US, en-GB
  var langRegex = /^en(-[a-z]{2})?$/i;

  // Get the available voices and filter the list to only have English speakers
  var voices = synthesis
    .getVoices()
    .filter((voice) => langRegex.test(voice.lang));

  // Log the properties of the voices in the list
  voices.forEach(function (voice) {
    console.log({
      name: voice.name,
      lang: voice.lang,
      uri: voice.voiceURI,
      local: voice.localService,
      default: voice.default,
    });
  });
} else {
  console.log('Text-to-speech not supported.');
}

コードのこのセクションでは、デバイスで使用可能な音声のリストを取得し、langRegex正規表現を使用してリストをフィルタリングして、英語を話す人だけの音声を取得できるようにします。 最後に、リスト内の音声をループして、それぞれのプロパティをコンソールに記録します。

発話の構築

このステップでは、SpeechSynthesisUtteranceコンストラクターを使用し、使用可能なプロパティの値を設定して、音声発話を作成します。

次のコードスニペットは、テキスト"Hello World"を読むための音声発話を作成します。

if ('speechSynthesis' in window) {
  var synthesis = window.speechSynthesis;

  // Get the first `en` language voice in the list
  var voice = synthesis.getVoices().filter(function (voice) {
    return voice.lang === 'en';
  })[0];

  // Create an utterance object
  var utterance = new SpeechSynthesisUtterance('Hello World');

  // Set utterance properties
  utterance.voice = voice;
  utterance.pitch = 1.5;
  utterance.rate = 1.25;
  utterance.volume = 0.8;

  // Speak the utterance
  synthesis.speak(utterance);
} else {
  console.log('Text-to-speech not supported.');
}

ここでは、使用可能な音声のリストから最初のen言語の音声を取得します。 次に、SpeechSynthesisUtteranceコンストラクターを使用して新しい発話を作成します。 次に、voicepitchratevolumeなどのいくつかのプロパティを発話オブジェクトに設定します。 最後に、SpeechSynthesisspeak()メソッドを使用して発話を話します。

注:発話で話すことができるテキストのサイズには制限があります。 各発話で話せるテキストの最大長は32,767文字です。

コンストラクターで発声するテキストを渡したことに注意してください。

発話オブジェクトのtextプロパティを設定して、発話するテキストを設定することもできます。

簡単な例を次に示します。

var synthesis = window.speechSynthesis;
var utterance = new SpeechSynthesisUtterance("Hello World");

// This overrides the text "Hello World" and is uttered instead
utterance.text = "My name is Glad.";

synthesis.speak(utterance);

これは、コンストラクターで渡されたテキストをオーバーライドします。

発話を話す

前のコードスニペットでは、SpeechSynthesisインスタンスでspeak()メソッドを呼び出して発話を話しました。 これで、SpeechSynthesisUtteranceインスタンスをspeak()メソッドの引数として渡して発話を話すことができます。

var synthesis = window.speechSynthesis;

var utterance1 = new SpeechSynthesisUtterance("Hello World");
var utterance2 = new SpeechSynthesisUtterance("My name is Glad.");
var utterance3 = new SpeechSynthesisUtterance("I'm a web developer from Nigeria.");

synthesis.speak(utterance1);
synthesis.speak(utterance2);
synthesis.speak(utterance3);

SpeechSynthesisインスタンスでは、発話の一時停止、再開、キャンセルなど、他にもいくつかのことができます。 したがって、pause()resume()、およびcancel()メソッドは、SpeechSynthesisインスタンスでも使用できます。

ステップ1—テキスト読み上げアプリを作成する

SpeechSynthesisインターフェースの基本的な側面を見てきました。 次に、テキスト読み上げアプリケーションの作成を開始します。 始める前に、マシンにNodeとnpmがインストールされていることを確認してください。

ターミナルで次のコマンドを実行して、アプリのプロジェクトをセットアップし、依存関係をインストールします。

新しいプロジェクトディレクトリを作成します。

  1. mkdir web-speech-app

新しく作成されたプロジェクトディレクトリに移動します。

  1. cd web-speech-app

プロジェクトを初期化します。

  1. npm init -y

プロジェクトに必要な依存関係をインストールします-expresscors、およびaxios

  1. npm install express cors axios

package.jsonファイルの"scripts"セクションを次のスニペットのように変更します。

package.json
"scripts": {
  "start": "node server.js"
}

アプリケーションのプロジェクトを初期化したので、Expressを使用してアプリのサーバーをセットアップします。

新しいserver.jsファイルを作成し、それに次のコンテンツを追加します。

server.js
const cors = require('cors');
const path = require('path');
const axios = require('axios');
const express = require('express');

const app = express();
const PORT = process.env.PORT || 5000;

app.set('port', PORT);

// Enable CORS (Cross-Origin Resource Sharing)
app.use(cors());

// Serve static files from the /public directory
app.use('/', express.static(path.join(__dirname, 'public')));

// A simple endpoint for fetching a random quote from QuotesOnDesign
app.get('/api/quote', (req, res) => {
  axios
    .get(
      'https://quotesondesign.com/wp-json/wp/v2/posts/?orderby=rand'
    )
    .then((response) => {
      const [post] = response.data;
      const { title, content } = post || {};

      return title && content
        ? res.json({ status: 'success', data: { title, content } })
        : res
            .status(500)
            .json({ status: 'failed', message: 'Could not fetch quote.' });
    })
    .catch((err) =>
      res
        .status(500)
        .json({ status: 'failed', message: 'Could not fetch quote.' })
    );
});

app.listen(PORT, () => console.log(`> App server is running on port ${PORT}.`));

ここでは、Expressを使用してノードサーバーをセットアップします。 cors()ミドルウェアを使用してCORS(クロスオリジンリクエストシェアリング)を有効にしました。 また、express.static()ミドルウェアを使用して、プロジェクトルートの/publicディレクトリから静的ファイルを提供します。 これにより、すぐに作成するインデックスページを提供できるようになります。

最後に、 QuotesOnDesignAPIサービスからランダムな見積もりを取得するためのGET/api/quoteルートを設定します。 axios (PromiseベースのHTTPクライアントライブラリ)を使用してHTTPリクエストを作成しています。

QuotesOnDesignAPIからのサンプル応答は次のようになります。

Output
[ { "title": { "rendered": "Victor Papanek" }, "content": { "rendered": "<p>Any attempt to separate design, to make it a thing-by-itself, works counter to the inherent value of design as the primary, underlying matrix of life.</p>\n", "protected": false } } ]

注: QuotesOnDesignのAPIの変更の詳細については、4.0と5.0の間の変更を文書化したページを参照してください。

見積もりを正常にフェッチすると、見積もりのtitlecontentがJSON応答のdataフィールドに返されます。 そうしないと、500HTTPステータスコードを含む失敗したJSON応答が返されます。

次に、アプリビューのインデックスページを作成します。

まず、プロジェクトのルートに新しいpublicフォルダーを作成します。

  1. mkdir public

次に、新しく作成したpublicフォルダーに新しいindex.htmlファイルを作成し、次のコンテンツを追加します。

public / index.html
<html>

<head>
    <title>Daily Quotes</title>
    <meta charset="UTF-8" />
    <meta name="viewport" content="width=device-width, initial-scale=1, shrink-to-fit=no">
    <link rel="stylesheet" href="https://stackpath.bootstrapcdn.com/bootstrap/4.1.1/css/bootstrap.min.css" integrity="sha384-WskhaSGFgHYWDcbwN70/dfYBj47jz9qbsMId/iRN3ewGhXQFZCSftd1LZCfmhktB" crossorigin="anonymous">
</head>

<body class="position-absolute h-100 w-100">
    <div id="app" class="d-flex flex-wrap align-items-center align-content-center p-5 mx-auto w-50 position-relative"></div>

    <script src="https://unpkg.com/jquery/dist/jquery.min.js"></script>
    <script src="https://cdn.jsdelivr.net/npm/feather-icons/dist/feather.min.js"></script>
    <script src="main.js"></script>
</body>

</html>

これにより、アプリのすべての動的コンテンツのマウントポイントとして機能する<div id="app">が1つだけのアプリの基本的なインデックスページが作成されます。

また、Bootstrap CDNへのリンクを追加して、アプリのデフォルトの Bootstrap4スタイルを取得しました。 また、DOM操作とAJAXリクエスト用の jQuery と、エレガントなSVGアイコン用のフェザーアイコンも含まれています。

ステップ2—メインスクリプトを作成する

これで、アプリを強化する最後の部分であるメインスクリプトにたどり着きました。 アプリのpublicディレクトリに新しいmain.jsファイルを作成し、それに次のコンテンツを追加します。

public / main.js
jQuery(function ($) {
  let app = $('#app');

  let SYNTHESIS = null;
  let VOICES = null;

  let QUOTE_TEXT = null;
  let QUOTE_PERSON = null;

  let VOICE_SPEAKING = false;
  let VOICE_PAUSED = false;
  let VOICE_COMPLETE = false;

  let iconProps = {
    'stroke-width': 1,
    'width': 48,
    'height': 48,
    'class': 'text-secondary d-none',
    'style': 'cursor: pointer'
  };

  function iconSVG(icon) {}

  function showControl(control) {}

  function hideControl(control) {}

  function getVoices() {}

  function resetVoice() {}

  function fetchNewQuote() {}

  function renderQuote(quote) {}

  function renderVoiceControls(synthesis, voice) {}

  function updateVoiceControls() {}

  function initialize() {}

  initialize();
});

このコードは、jQueryを使用して、DOMのロード時に関数を実行します。 #app要素への参照を取得し、いくつかの変数を初期化します。 また、次のセクションで実装するいくつかの空の関数を宣言します。 最後に、initialize()関数を呼び出してアプリケーションを初期化します。

iconProps変数には、フェザーアイコンをSVGとしてDOMにレンダリングするために使用されるいくつかのプロパティが含まれています。

そのコードを配置すると、関数の実装を開始する準備が整います。 public/main.jsファイルを変更して、次の機能を実装します。

public / main.js
// Gets the SVG markup for a Feather icon
function iconSVG(icon) {
  let props = $.extend(iconProps, { id: icon });
  return feather.icons[icon].toSvg(props);
}

// Shows an element
function showControl(control) {
  control.addClass('d-inline-block').removeClass('d-none');
}

// Hides an element
function hideControl(control) {
  control.addClass('d-none').removeClass('d-inline-block');
}

// Get the available voices, filter the list to have only English filters
function getVoices() {
  // Regex to match all English language tags e.g en, en-US, en-GB
  let langRegex = /^en(-[a-z]{2})?$/i;

  // Get the available voices and filter the list to only have English speakers
  VOICES = SYNTHESIS.getVoices()
    .filter(function (voice) {
      return langRegex.test(voice.lang);
    })
    .map(function (voice) {
      return {
        voice: voice,
        name: voice.name,
        lang: voice.lang.toUpperCase(),
      };
    });
}

// Reset the voice variables to the defaults
function resetVoice() {
  VOICE_SPEAKING = false;
  VOICE_PAUSED = false;
  VOICE_COMPLETE = false;
}

iconSVG(icon)関数は、引数としてフェザーアイコン名の文字列('play-circle'など)を取り、アイコンのSVGマークアップを返します。 フェザーのWebサイトをチェックして、使用可能なフェザーアイコンの完全なリストを確認してください。 APIの詳細については、フェザーのドキュメントも確認してください。

getVoices()関数は、SYNTHESISオブジェクトを使用して、デバイスで使用可能なすべての音声のリストをフェッチします。 次に、正規表現を使用してリストをフィルタリングし、英語を話す人だけの声を取得します。

次に、DOMで引用符をフェッチおよびレンダリングするための関数を実装します。 public/main.jsファイルを変更して、次の機能を実装します。

public / main.js
function fetchNewQuote() {
  // Clean up the #app element
  app.html('');

  // Reset the quote variables
  QUOTE_TEXT = null;
  QUOTE_PERSON = null;

  // Reset the voice variables
  resetVoice();

  // Pick a voice at random from the VOICES list
  let voice =
    VOICES && VOICES.length > 0
      ? VOICES[Math.floor(Math.random() * VOICES.length)]
      : null;

  // Fetch a quote from the API and render the quote and voice controls
  $.get('/api/quote', function (quote) {
    renderQuote(quote.data);
    SYNTHESIS && renderVoiceControls(SYNTHESIS, voice || null);
  });
}

function renderQuote(quote) {
  // Create some markup for the quote elements
  let quotePerson = $('<h1 id="quote-person" class="mb-2 w-100"></h1>');
  let quoteText = $('<div id="quote-text" class="h3 py-5 mb-4 w-100 font-weight-light text-secondary border-bottom border-gray"></div>');

  // Add the quote data to the markup
  quotePerson.html(quote.title.rendered);
  quoteText.html(quote.content.rendered);

  // Attach the quote elements to the DOM
  app.append(quotePerson);
  app.append(quoteText);

  // Update the quote variables with the new data
  QUOTE_TEXT = quoteText.text();
  QUOTE_PERSON = quotePerson.text();
}

ここでfetchNewQuote()メソッドでは、最初にアプリの要素と変数をリセットします。 次に、VOICES変数に格納されている音声のリストから、Math.random()を使用して音声をランダムに選択します。 $.get()を使用して、/api/quoteエンドポイントにAJAXリクエストを行い、ランダムな見積もりを取得し、見積もりデータを音声コントロールと一緒にビューにレンダリングします。

renderQuote(quote)メソッドは、引数としてquoteオブジェクトを受け取り、その内容をDOMに追加します。 最後に、引用変数QUOTE_TEXTおよびQUOTE_PERSONを更新します。

fetchNewQuote()関数を見ると、renderVoiceControls()関数を呼び出していることがわかります。 この機能は、音声出力の再生、一時停止、および停止のコントロールをレンダリングする役割を果たします。 また、使用中の現在の音声と言語をレンダリングします。

public/main.jsファイルに次の変更を加えて、renderVoiceControls()機能を実装します。

public / main.js
function renderVoiceControls(synthesis, voice) {
  let controlsPane = $('<div id="voice-controls-pane" class="d-flex flex-wrap w-100 align-items-center align-content-center justify-content-between"></div>');

  let voiceControls = $('<div id="voice-controls"></div>');

  // Create the SVG elements for the voice control buttons
  let playButton = $(iconSVG('play-circle'));
  let pauseButton = $(iconSVG('pause-circle'));
  let stopButton = $(iconSVG('stop-circle'));

  // Helper function to enable pause state for the voice output
  let paused = function () {
    VOICE_PAUSED = true;
    updateVoiceControls();
  };

  // Helper function to disable pause state for the voice output
  let resumed = function () {
    VOICE_PAUSED = false;
    updateVoiceControls();
  };

  // Click event handler for the play button
  playButton.on('click', function (evt) {});

  // Click event handler for the pause button
  pauseButton.on('click', function (evt) {});

  // Click event handler for the stop button
  stopButton.on('click', function (evt) {});

  // Add the voice controls to their parent element
  voiceControls.append(playButton);
  voiceControls.append(pauseButton);
  voiceControls.append(stopButton);

  // Add the voice controls parent to the controlsPane element
  controlsPane.append(voiceControls);

  // If voice is available, add the voice info element to the controlsPane
  if (voice) {
    let currentVoice = $('<div class="text-secondary font-weight-normal"><span class="text-dark font-weight-bold">' + voice.name + '</span> (' + voice.lang + ')</div>');

    controlsPane.append(currentVoice);
  }

  // Add the controlsPane to the DOM
  app.append(controlsPane);

  // Show the play button
  showControl(playButton);
}

ここでは、音声コントロールとコントロールペインのコンテナ要素を作成します。 前に作成したiconSVG()関数を使用して、コントロールボタンのSVGマークアップを取得し、ボタン要素も作成します。 paused()およびresumed()ヘルパー関数を定義します。これらの関数は、ボタンのイベントハンドラーを設定するときに使用されます。

最後に、音声制御ボタンと音声情報をDOMにレンダリングします。 また、最初は再生ボタンのみが表示されるように構成されています。

次に、前のセクションで定義した音声制御ボタンのクリックイベントハンドラーを実装します。

次のコードスニペットに示すように、イベントハンドラーを設定します。

public / main.js
// Click event handler for the play button
playButton.on('click', function (evt) {
  evt.preventDefault();

  if (VOICE_SPEAKING) {
    // If voice is paused, it is resumed when the playButton is clicked
    if (VOICE_PAUSED) synthesis.resume();
    return resumed();
  } else {
    // Create utterances for the quote and the person
    let quoteUtterance = new SpeechSynthesisUtterance(QUOTE_TEXT);
    let personUtterance = new SpeechSynthesisUtterance(QUOTE_PERSON);

    // Set the voice for the utterances if available
    if (voice) {
      quoteUtterance.voice = voice.voice;
      personUtterance.voice = voice.voice;
    }

    // Set event listeners for the quote utterance
    quoteUtterance.onpause = paused;
    quoteUtterance.onresume = resumed;
    quoteUtterance.onboundary = updateVoiceControls;

    // Set the listener to activate speaking state when the quote utterance starts
    quoteUtterance.onstart = function (evt) {
      VOICE_COMPLETE = false;
      VOICE_SPEAKING = true;
      updateVoiceControls();
    };

    // Set event listeners for the person utterance
    personUtterance.onpause = paused;
    personUtterance.onresume = resumed;
    personUtterance.onboundary = updateVoiceControls;

    // Refresh the app and fetch a new quote when the person utterance ends
    personUtterance.onend = fetchNewQuote;

    // Speak the utterances
    synthesis.speak(quoteUtterance);
    synthesis.speak(personUtterance);
  }
});

// Click event handler for the pause button
pauseButton.on('click', function (evt) {
  evt.preventDefault();

  // Pause the utterance if it is not in paused state
  if (VOICE_SPEAKING) synthesis.pause();
  return paused();
});

// Click event handler for the stop button
stopButton.on('click', function (evt) {
  evt.preventDefault();

  // Clear the utterances queue
  if (VOICE_SPEAKING) synthesis.cancel();
  resetVoice();

  // Set the complete status of the voice output
  VOICE_COMPLETE = true;
  updateVoiceControls();
});

ここでは、音声コントロールボタンのクリックイベントリスナーを設定します。 再生ボタンをクリックすると、quoteUtterancepersonUtteranceの順に発話が始まります。 ただし、音声出力が一時停止状態の場合は再開します。

quoteUtteranceonstartイベントハンドラーでVOICE_SPEAKINGtrueに設定します。 personUtteranceが終了すると、アプリは新しい見積もりを更新して取得します。

Pause ボタンは音声出力を一時停止し、 Stop ボタンは音声出力を終了し、SpeechSynthesisインターフェース。 コードは毎回updateVoiceControls()関数を呼び出して、適切なボタンを表示します。

前のコードスニペットで、updateVoiceControls()関数をいくつか呼び出して参照しました。 この関数は、音声コントロールを更新して、音声状態変数に基づいて適切なコントロールを表示する役割を果たします。

public/main.jsファイルに次の変更を加えて、updateVoiceControls()機能を実装します。

public / main.js
function updateVoiceControls() {
  // Get a reference to each control button
  let playButton = $('#play-circle');
  let pauseButton = $('#pause-circle');
  let stopButton = $('#stop-circle');

  if (VOICE_SPEAKING) {
    // Show the stop button if speaking is in progress
    showControl(stopButton);

    // Toggle the play and pause buttons based on paused state
    if (VOICE_PAUSED) {
      showControl(playButton);
      hideControl(pauseButton);
    } else {
      hideControl(playButton);
      showControl(pauseButton);
    }
  } else {
    // Show only the play button if no speaking is in progress
    showControl(playButton);
    hideControl(pauseButton);
    hideControl(stopButton);
  }
}

コードのこのセクションでは、最初に各音声制御ボタン要素への参照を取得します。 次に、音声出力のさまざまな状態で表示する音声制御ボタンを指定します。

これで、initialize()関数を実装する準備が整いました。 この関数は、アプリケーションの初期化を担当します。 次のコードスニペットをpublic/main.jsファイルに追加して、initialize()関数を実装します。

public / main.js
function initialize() {
  if ('speechSynthesis' in window) {
    SYNTHESIS = window.speechSynthesis;

    let timer = setInterval(function () {
      let voices = SYNTHESIS.getVoices();

      if (voices.length > 0) {
        getVoices();
        fetchNewQuote();
        clearInterval(timer);
      }
    }, 200);
  } else {
    let message = 'Text-to-speech not supported by your browser.';

    // Create the browser notice element
    let notice = $('<div class="w-100 py-4 bg-danger font-weight-bold text-white position-absolute text-center" style="bottom:0; z-index:10">' + message + '</div>');

    fetchNewQuote();
    console.log(message);

    // Display non-support info on DOM
    $(document.body).append(notice);
  }
}

このコードは、最初にspeechSynthesiswindowグローバルオブジェクトで使用可能かどうかを確認し、使用可能な場合はSYNTHESIS変数に割り当てられます。 次に、使用可能な音声のリストを取得するための間隔を設定します。

ここでは、音声がまだロードされていないために最初の呼び出しで空の配列を返すSpeechSynthesis.getVoices()の既知の非同期動作があるため、間隔を使用しています。 間隔は、ランダムな引用をフェッチして間隔をクリアする前に、音声のリストを確実に取得します。

これで、テキスト読み上げアプリが正常に完了しました。 ターミナルで次のコマンドを実行すると、アプリを起動できます。

  1. npm start

利用可能な場合、アプリはポート5000で実行されます。

ブラウザでlocalhost:5000にアクセスして、アプリを確認してください。

Final screenshot of voice-to-text app

次に、再生ボタンを操作して、引用が話されているのを聞きます。

結論

このチュートリアルでは、Web Speech APIを使用して、Web用のテキスト読み上げアプリを作成しました。 Web Speech APIの詳細については、MDN WebDocsいくつかの役立つリソースを見つけることができます。

アプリの改良を続けたい場合は、音量コントロール、音声ピッチコントロール、速度/速度コントロール、発声されたテキストの割合など、まだ実装して実験できる興味深い機能がいくつかあります。

このチュートリアルの完全なソースコードは、GitHubで入手できます。