音声コマンドを使用して標準のSailfish OSプレーヤーを制御します

多くの人が、Google NowやGoogle AssistantなどのAndroidオペレーティングシステムの機能を知って使用しています。これらの機能を使用すると、インターネット上で有用な情報を取得して検索できるだけでなく、音声コマンドを使用してデバイスを制御することもできます。 残念ながら、Sailfish OS(フィンランドのJolla社とロシアのOpen Mobile Platform社が開発したオペレーティングシステム)は、そのような機会を「そのまま」提供していません。 その結果、これらの設備の不足を独自に補うことが決定されました。 開発したソリューションの機能の1つは、音声コマンドを使用して音楽プレーヤーを制御する機能です。その技術的な側面については、この記事で説明します。



音声認識と実行を実装するには、4つの簡単な手順を実行する必要があります。



  1. チームシステムを開発する
  2. 音声認識を実装する
  3. コマンドの識別と実行を実装し、
  4. 音声フィードバックを追加します。


資料をよりよく理解するために、読者はすでにC ++、JavaScript、Qt、QML、Linuxの基本的な知識を持ち、 Sailfish OSのフレームワーク内での相互作用の例に精通していると想定されます。 2016年の夏にイノポリスのセイルフィッシュOSサマースクールの一部として開催された関連トピックに関する講演や、Habréで既に公開されているこのプラットフォームの開発に関するその他の記事を知ることも役立ちます。



チームシステム開発



5つの関数に限定された簡単な例を調べてみましょう。





新しい再生を開始するには、プレーヤーの開いているインスタンスの可用性を確認し(必要に応じて作成し)、ランダムな順序で音楽の再生を開始する必要があります。 有効にするには、「音楽をオンにする」コマンドを使用します。



再生を再開して一時停止する場合は、プレーヤーのステータスを確認し、可能であれば、再生を開始するか一時停止する必要があります。 再生を再開するには、「Play」コマンドを使用します。 一時停止するには、「一時停止」および「停止」コマンドを使用します。



楽曲をナビゲートする場合、オーディオプレーヤーのステータスをチェックする上記の原則が適用されます。 フォワードナビゲーションをアクティブにするには、「Forward」、「Next」、および「Next」コマンドを使用します。 後方ナビゲーションを有効にするには、「戻る」および「前へ」コマンドを使用します。



音声認識



音声コマンドの認識プロセスは、3つの段階に分かれています。



  1. 音声コマンドをファイルに録音する、
  2. サーバー上のコマンド認識、
  3. デバイス上のチームID。


音声コマンドをファイルに録音する


まず、音声コマンドをキャプチャするためのユーザーインターフェイスを作成する必要があります。 例を単純化するために、ボタンを押して録音を開始および終了します。音声コマンドの開始と終了を検出するプロセスの実装には、別の資料が必要だからです。



IconButton { property bool isRecording: false width: Theme.iconSizeLarge height: Theme.iconSizeLarge icon.source: isRecording ? "image://theme/icon-m-search" : "image://theme/icon-m-mic" onClicked: { if (isRecording) { isRecording = false recorder.stopRecord() yandexSpeechKitHelper.recognizeQuery(recorder.getActualLocation()) } else { isRecording = true recorder.startRecord() } } }
      
      





上記のコードから、ボタンは標準サイズと標準アイコン(アプリケーションインターフェイスを統合するSailfish OSの興味深い機能)を使用し、2つの状態になっていることがわかります。 最初の状態では、録音が実行されていない場合、ボタンを押した後、音声コマンドの録音が開始されます。 2番目の状態では、コマンドの記録がアクティブな場合、ボタンを押すと記録が停止し、音声認識が開始されます。



音声を録音するには、入力オーディオストリームを制御するための高レベルインターフェイスを提供するQAudioRecorderクラスと、録音プロセスを調整するためのQAudioEncoderSettingsを使用します。



 class Recorder : public QObject { Q_OBJECT public: explicit Recorder(QObject *parent = 0); Q_INVOKABLE void startRecord(); Q_INVOKABLE void stopRecord(); Q_INVOKABLE QUrl getActualLocation(); Q_INVOKABLE bool isRecording(); private: QAudioRecorder _audioRecorder; QAudioEncoderSettings _settings; bool _recording = false; }; Recorder::Recorder(QObject *parent) : QObject(parent) { _settings.setCodec("audio/PCM"); _settings.setQuality(QMultimedia::NormalQuality); _audioRecorder.setEncodingSettings(_settings); _audioRecorder.setContainerFormat("wav"); } void Recorder::startRecord() { _recording = true; _audioRecorder.record(); } void Recorder::stopRecord() { _recording = false; _audioRecorder.stop(); } QUrl Recorder::getActualLocation() { return _audioRecorder.actualLocation(); } bool Recorder::isRecording() { return _recording; }
      
      





これは、コマンドの録音が通常の品質のwav形式であり、録音の開始と終了、オーディオファイルの保存場所と録音プロセスのステータスを取得する方法も決定されることを示します。



サーバーコマンド認識


Yandex SpeechKit Cloudサービスは、音声ファイルをテキストにブロードキャストするために使用されます。 作業を開始するために必要なのは、開発者のオフィスでトークンを取得することだけです。 サービスのドキュメントは非常に詳細であるため、私たちはプライベートな瞬間にのみ焦点を当てます。



最初のステップは、記録されたコマンドをサーバーに転送することです。



 void YandexSpeechKitHelper::recognizeQuery(QString path_to_file) { QFile *file = new QFile(path_to_file); if (file->open(QIODevice::ReadOnly)) { QUrlQuery query; query.addQueryItem("key", "API_KEY"); query.addQueryItem("uuid", _buildUniqID()); query.addQueryItem("topic", "queries"); QUrl url("https://asr.yandex.net/asr_xml"); url.setQuery(query); QNetworkRequest request(url); request.setHeader(QNetworkRequest::ContentTypeHeader, "audio/x-wav"); request.setHeader(QNetworkRequest::ContentLengthHeader, file->size()); _manager->post(request, file->readAll()); file->close(); } file->remove(); }
      
      





ここでは、受信したトークンが送信されるYandexサーバーに対してPOST要求が生成され、一意のデバイスID(この場合はWiFiモジュールのMACアドレスが使用されます)および要求の種類(「クエリ」がここで使用されます。短く正確なコマンド)。 リクエストヘッダーは、オーディオファイルの形式とそのサイズを示します-直接コンテンツ。 サーバーに要求を送信した後、ファイルは不要として削除されます。



応答として、SpeechKit Cloudサーバーは認識オプションとそれらに対するある程度の自信を持ってXMLを返します。 標準のQtツールを使用して、必要な情報を強調します。



 void YandexSpeechKitHelper::_parseResponce(QXmlStreamReader *element) { double idealConfidence = 0; QString idealQuery; while (!element->atEnd()) { element->readNext(); if (element->tokenType() != QXmlStreamReader::StartElement) continue; if (element->name() != "variant") continue; QXmlStreamAttribute attr = element->attributes().at(0); if (attr.value().toDouble() > idealConfidence) { idealConfidence = attr.value().toDouble(); element->readNext(); idealQuery = element->text().toString(); } } if (element->hasError()) qDebug() << element->errorString(); emit gotResponce(idealQuery); }
      
      





ここでは、受信した回答が連続的にレビューされ、バリアントタグについては、認識精度インジケータがチェックされます。 新しいオプションが正しい場合は保存され、スキャンが続行されます。 応答の表示が終了すると、選択したコマンドテキストとともに信号が送信されます。



デバイス識別


最後に、チームを特定する必要があります。 YandexSpeechKitHelper :: _ parseResponceメソッドの最後に、前述のように、コマンドテキストを含むgotResponceシグナルが送信されます。 次に、プログラムのQMLコードで処理する必要があります。



 Connections { target: yandexSpeechKitHelper onGotResponce: { switch (query.toLowerCase()) { case " ": dbusHelper.startMediaplayerIfNeed() mediaPlayer.shuffleAndPlay() break; case "": mediaPlayerControl.play() break; case "": case "": mediaPlayerControl.pause() break; case "": case "": case "": mediaPlayerControl.next() break; case "": case "": mediaPlayerControl.previous() break; default: generateErrorMessage(query) break; } } }
      
      





これは、Connections要素を使用して着信信号を処理し、認識されたコマンドを以前に定義された音声コマンドパターンと比較します。



働くプレーヤーの管理



オーディオプレーヤーが開いている場合、Linuxの大兄弟から継承された標準のDBusインターフェイスを介してオーディオプレーヤーと対話することができます 。 これにより、プレイリストをナビゲートしたり、再生を開始または一時停止したりできます。 これは、 DBMLInterface QML要素を使用して行われます。



 DBusInterface { id: mediaPlayerControl service: "org.mpris.MediaPlayer2.jolla-mediaplayer" iface: "org.mpris.MediaPlayer2.Player" path: "/org/mpris/MediaPlayer2" function play() { call("Play", undefined) } function pause() { call("Pause", undefined) } function next() { call("Next", undefined) } function previous() { call("Previous", undefined) call("Previous", undefined) } }
      
      





この要素を使用して、標準のオーディオプレーヤーのDBusインターフェイスは、4つの基本機能を定義することにより使用されます。 DBusメソッドが引数を受け入れない場合、 呼び出し関数の未定義のパラメーターが渡されます。



前の曲に移動するために、Previousメソッドが2回呼び出されることに注意してください。1回の呼び出しで現在の曲が最初から再生されるためです。



最初から再生を開始する



すでに実行中のプレーヤーを管理するのに複雑なことはありません。 ただし、音楽が閉じられたときに音楽の再生を開始したい場合は、デフォルトで、コレクション全体の同時再生を行う標準プレーヤーの起動機能が提供されないため、問題が発生します。



ただし、Sailfish OSは無料の変更が可能なオープンソースのオペレーティングシステムであることを忘れないでください。 この結果、問題は2段階で解決できます。





標準のオーディオプレーヤーの機能を拡張する


標準のオーディオプレーヤーは、 org.mpris.MediaPlayer2.Playerインターフェースに加えて、 / usr / share / jolla-mediaplayer / mediaplayer.qmlファイルで定義されているcom.jolla.mediaplayer.uiインターフェースを提供します。 このことから、必要な機能を追加することにより、このファイルを変更することができます。



 DBusAdaptor { service: "com.jolla.mediaplayer" path: "/com/jolla/mediaplayer/ui" iface: "com.jolla.mediaplayer.ui" function openUrl(arg) { if (arg[0] == undefined) { return false } AudioPlayer.playUrl(Qt.resolvedUrl(arg[0])) if (!pageStack.currentPage || pageStack.currentPage.objectName !== "PlayQueuePage") { root.pageStack.push(playQueuePage, {}, PageStackAction.Immediate) } activate() return true } function shuffleAndPlay() { AudioPlayer.shuffleAndPlay(allSongModel, allSongModel.count) if (!pageStack.currentPage || pageStack.currentPage.objectName !== "PlayQueuePage") { root.pageStack.push(playQueuePage, {}, PageStackAction.Immediate) } activate() return true } }
      
      





ここで、 DBusインターフェイスを提供するために使用されるDBusAdaptor要素は、 shuffleAndPlayメソッドを追加することにより変更されました。 標準プレーヤー機能を使用して、c om.jolla.mediaplayerモジュールによって提供されるランダムな順序ですべての曲の再生を開始し、現在の再生キューが前面に表示されます。



例の一部として、簡単にするために、システムファイルの簡単な変更を実行しました。 ただし、プログラムを配布するときは、 適切な指示を使用して、そのような変更をパッチの形で行う必要があります。



現在、開発中のプログラムから、新しい方法に目を向ける必要があります。 これは、上記で定義したサービスに接続し、プレーヤーに追加された関数を呼び出す、使い慣れたDBusInterface要素を使用して行われます。



 DBusInterface { id: mediaPlayer service: "com.jolla.mediaplayer" iface: "com.jolla.mediaplayer.ui" path: "/com/jolla/mediaplayer/ui" function shuffleAndPlay() { call("shuffleAndPlay", undefined) } }
      
      





閉じている場合はプレーヤーを起動します


最後に、最後に残っているのは、オーディオプレーヤーが閉じている場合に起動することです。 従来、タスクは2つの段階に分けられます。





 void DBusHelper::startMediaplayerIfNeed() { QDBusReply<bool> reply = QDBusConnection::sessionBus().interface()->isServiceRegistered("com.jolla.mediaplayer"); if (!reply.value()) { QProcess process; process.start("/bin/bash -c \"jolla-mediaplayer &\""); process.waitForFinished(); QDBusInterface interface("com.jolla.mediaplayer", "/com/jolla/mediaplayer/ui", "com.jolla.mediaplayer.ui"); while (true) { QDBusReply<bool> reply = interface.call("isSongsModelFinished"); if (reply.isValid() && reply.value()) break; QThread::sleep(1); } } }
      
      





提示された関数のコードから、最初の段階で、必要なDBusサービスの可用性が確認されていることがわかります。 システムに登録されている場合、機能は終了し、再生開始への移行が開始されます。 サービスが見つからない場合、 QProcessを使用してオーディオプレーヤーの新しいインスタンスが作成され 、その完全な起動が期待されます。 関数の2番目の部分では、 QDBusInterfaceを使用して、デバイス上の音楽コレクションのスキャン終了のフラグがチェックされます。



コレクションスキャンフラグをチェックするために、/ usr / share / jolla-mediaplayer / mediaplayer.qmlファイルに2つの追加変更が加えられたことに注意してください。



まず、 com.jolla.mediaplayerモジュールによって提供されるGriloTrackerModel要素は、スキャン終了フラグを追加することにより変更されました。



 GriloTrackerModel { id: allSongModel property bool isFinished: false query: { //: placeholder string for albums without a known name //% "Unknown album" var unknownAlbum = qsTrId("mediaplayer-la-unknown-album") //: placeholder string to be shown for media without a known artist //% "Unknown artist" var unknownArtist = qsTrId("mediaplayer-la-unknown-artist") return AudioTrackerHelpers.getSongsQuery("", {"unknownArtist": unknownArtist, "unknownAlbum": unknownAlbum}) } onFinished: { isFinished = true var artList = fetchAlbumArts(3) if (artList[0]) { if (!artList[0].url || artList[0].url == "") { mediaPlayerCover.idleArtist = artList[0].author ? artList[0].author : "" mediaPlayerCover.idleSong = artList[0].title ? artList[0].title : "" } else { mediaPlayerCover.idle.largeAlbumArt = artList[0].url mediaPlayerCover.idle.leftSmallAlbumArt = artList[1] && artList[1].url ? artList[1].url : "" mediaPlayerCover.idle.rightSmallAlbumArt = artList[2] && artList[2].url ? artList[2].url : "" mediaPlayerCover.idle.sourcesReady = true } } } }
      
      





次に、com.jolla.mediaplayer.ui DBusインターフェイスからアクセスできる別の関数が追加されました。この関数は、オーディオファイルのコレクションのスキャンステータスフラグの値を返します。



 function isSongsModelFinished() { return allSongModel.isFinished }
      
      





エラーコマンドメッセージ



この例の最後の要素は、間違ったコマンドに関する音声メッセージです。 これを行うには、Yandex SpeechKit Cloud音声合成サービスを使用します。



 Audio { id: audio } function generateErrorMessage(query) { var message = ".  " + query + "  ." audio.source = "https://tts.voicetech.yandex.net/generate?" + "text=\"" + message + "\"&" + "format=mp3&" + "lang=ru-RU&" + "speaker=jane&" + "emotion=good&" + "key=API_KEY" audio.play() }
      
      





ここでは、生成された音声を再現するAudioオブジェクトが作成され、Yandexサーバーへのリクエストを生成して再生を開始するgenerateErrorMessage関数が宣言されました。 リクエストには次のパラメータが含まれます。





おわりに



この記事では、音声コマンドを使用して標準のSailfish OSオーディオプレーヤーで音楽の再生を制御する簡単な例を説明します。 Qtツールを使用したYandex SpeechKit Cloudを使用した音声認識と合成に関する基本的な知識が得られ、繰り返されました。また、Sailfish OSでのプログラムの相互作用の原理も得られました。 この資料は、このオペレーティングシステムのより深い研究と実験の出発点として役立ちます。



上記のコードの例はビデオで見ることができます:





著者:ピョートル・ヴィトフトフ



All Articles