Управляємо стандартним плеєром Sailfish OS за допомогою голосових команд

Багато знають і користуються такими можливостями операційної системи Android, як Google і Google Now Assistant, які дозволяють не тільки вчасно отримувати корисну інформацію і що-небудь шукати в інтернеті, але і управляти пристроєм за допомогою голосових команд. На жаль, Sailfish OS (операційна система, що розробляється фінською компанією Jolla і російською компанією Відкрита мобільна платформа) не надає такої можливості «з коробки». Як результат, було вирішено заповнити відсутність цих зручностей своїми силами. Однією з функцій розроблюваного рішення є можливість керування музичним плеєром за допомогою голосових команд, технічна сторона якої і буде розглянуто в даній статті.

Для впровадження розпізнавання і виконання голосових команд потрібно пройти чотири простих кроки:
  1. розробити систему команд,
  2. реалізувати розпізнавання мови,
  3. реалізувати ідентифікацію та виконання команд,
  4. додати зворотний голосовий зв'язок.
Передбачається, що для кращого розуміння матеріалу, читач вже має базові знання C++, JavaScript, Qt, QML і Linux і ознайомився з прикладом їх взаємодії в рамках Sailfish OS. Також може бути корисним попереднє знайомство з лекцією з суміжної тематики, проведеної в рамках Літньої школи Sailfish OS в Иннополисе влітку 2016 року, і іншими статтями присвяченими розробці під цю платформу, які вже були опубліковані на Хабре.

Розробка системи команд
Розглянемо простий приклад, обмежений п'ятьма функціями:
  • запуск нового відтворення музики,
  • відновлення відтворення музики,
  • призупинення відтворення музики,
  • перехід до наступної пісні,
  • перехід до попередньої композиції.
Для запуску нового відтворення потрібно перевірити наявність відкритого примірника плеєра (при необхідності створити) і почати відтворення музики у випадковому порядку. Для активації будемо використовувати команду «Увімкни музику».

У випадку з відновленням та призупиненням відтворення потрібно перевірити стан плеєра і, при наявності можливості, запустити відтворення або поставити його на паузу. Для відновлення відтворення будемо використовувати команду «Грай»; для постановки на паузу — команди «Пауза» і «Стоп».

У випадку з навігацією по композиціям діє зазначений вище принцип перевірки стану аудіоплеєра. Для активації навігації вперед використовуємо команди «Вперед», «Далі» і «Наступний»; для активації навігації назад — команди «Назад» і «Попередній».

Розпізнавання мови
Процес розпізнавання мовних команд поділяється на три етапи:
  1. запис голосової команди у файл
  2. розпізнавання команди на сервері,
  3. ідентифікація команди на пристрої.
Запис голосової команди у файл
На початку необхідно сформувати інтерфейс користувача для захоплення голосової команди. З метою спрощення прикладу, будемо починати і закінчувати запис по натисненню на кнопку, так як реалізація процесу виявлення початку і кінця голосової команди заслуговує окремого матеріалу.
IconButton {
property bool isRecording: false

width: Theme.iconSizeLarge
height: Theme.iconSizeLarge
icon.source: isRecording ? "image://тема/icon-m-search" :
"image://тема/icon-m-mic"

onClicked: {
if (isRecording) {
isRecording = false
recorder.stopRecord()
yandexSpeechKitHelper.recognizeQuery(recorder.getActualLocation())
} else {
isRecording = true
recorder.startRecord()
}
}
}

З коду, представленого вище, видно, що кнопка використовує стандартні значення розмірів і стандартні іконки (цікава особливість Sailfish OS для уніфікації інтерфейсів додатків) і має два стани. У першому стані, коли запис не проводиться, після натискання на кнопку починається запис голосової команди. У другому стані, коли запис команди активна, після натискання на кнопку запис зупиняється і починається розпізнавання голосу.

Для запису мови будемо використовувати клас 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 в нормальній якості, а також визначаються методи для початку і закінчення запису, для отримання місця зберігання аудіофайлу і стану процесу запису.

Розпізнавання команди на сервері
Для трансляції аудіо в текст буде використовуватися сервіс Яндекс 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();
}

Тут формується POST-запит до сервера Яндекс, в якому передаються отриманий токен, унікальний ID пристрою (у цьому випадку використовується MAC-адресу WiFi-модуля) і тип запиту (тут використано «запити», так як при голосовому взаємодії з пристроєм найчастіше використовуються короткі і точні команди). У заголовках запиту зазначаються формат звукового файлу і його розмір, теле — безпосередньо вміст. Після передачі запиту на сервер файл видаляється за непотрібністю.

В якості відповіді сервер 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);
}

Тут проглядається послідовно отриманий відповідь, і для тегів variant, перевіряються показники точності розпізнавання. Якщо новий варіант коректніше, то він зберігається, а сканування триває далі. По закінченню перегляду відповіді надсилається сигнал з виділеним текстом команди.

Ідентифікація команди на пристрої
Нарешті, залишається ідентифікувати команду. По закінченню роботи методу 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 для обробки сигналу, що поступає і порівняння розпізнаної команди з шаблонами голосових команд, визначеними раніше.

Управління працюючим плеєром
Якщо аудіоплеєр відкритий, то з ним воможно взаємодіяти через стандартний DBus-інтерфейс, що дістався від великого linux-брата. З його допомогою можна переміщатися за списком відтворення, починати або припиняти відтворення. Робиться це з використанням QML-елемента DBusInterface.
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-інтерфейс стандартного аудіоплеєра шляхом визначення чотирьох базових функцій. Параметр undefined call передається в тому випадку, якщо DBus-метод не приймає аргументів.

Варто зазначити, що для переходу до попередньої композиції метод Previous викликається два рази, так як його одиночний дзвінок призводить до відтворення поточної композиції з початку.

Запуск відтворення з нуля
В управлінні вже працюючим плеєром нічого складного немає. Однак, якщо є бажання почати відтворення музики, коли він закритий — виникає проблема, так як, за замовчуванням, функціонал запуску стандартного плеєра з одночасним відтворенням всієї колекції не надається.

Але не варто забувати про те, що Sailfish OS — операційна система з відкритим вихідним кодом, доступна для вільної модифікації. В наслідок цього виниклу проблему можна вирішити в два етапи:
  • Розширити функції, надані плеєром через DBus-інтерфейс;
  • Реалізувати запуск плеєра (при необхідності) і почати відтворення відразу після запуску.
Розширення функцій стандартного аудіоплеєра
Стандартний аудіоплеєр, крім інтерфейсу org.mpris.MediaPlayer2.Player, надає інтерфейс com.jolla.mediaplayer.ui, визначений у файлі /usr/share/jolla-mediaplayer/mediaplayer.qml. З цього випливає, що можливо модифікувати цей файл, додавши потрібну нам функцію.
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
}
}

Тут був модифікований елемент DBusAdaptorвикористовується для надання DBus-інтерфейсу, шляхом додавання методу shuffleAndPlay. У ньому використовується стандартний функціонал плеєра для запуску відтворення всіх композицій у випадковому порядку, надається модулем com.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)
}
}

Запуск плеєра, якщо закритий
Нарешті, останнє, що залишилося — запуск аудіоплеєра якщо він закритий. Умовно, завдання можна розділити на два етапи:
  • безпосередньо запуск плеєра,
  • очікування сканування музичної колекції.
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, з очікуванням повного його запуску. У другій частині функції, за допомогою QDBusInterface, перевіряється прапор закінчення сканування колекції музики на пристрої.

Слід зазначити, що для перевірки прапора сканування колекції були зроблені два додаткових зміни у файлі /usr/share/jolla-mediaplayer/mediaplayer.qml.

По-перше, був модифікований елемент GriloTrackerModel, що надається модулем com.jolla.mediaplayer, шляхом додавання прапора закінчення сканування.
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
}
}
}
}

По-друге, була додана ще одна функція, доступна через DBus-інтерфейс com.jolla.mediaplayer.ui, що повертає значення прапора стану сканування колекції аудіофайлів.
function isSongsModelFinished() {
return allSongModel.isFinished
}

Повідомлення про помилкову команді
Останнім елементом прикладу є голосове повідомлення про неправильну команді. Для цього скористаємося сервісом синтезу мови Яндекс 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&" +
"speaker=jane&" +
"emotion=good&" +
"key=API_KEY"
audio.play()
}

Тут був створений об'єкт Audio для відтворення згенерованої мови і оголошена функція generateErrorMessage для формування запиту до сервера Яндекс і запуску відтворення. У запиті передаються наступні параметри:
  • text — текст для синтезу (повідомлення про неправильну голосовій команді),
  • format — формат повертається файлу (mp3),
  • мова — мова фрази (російська),
  • speaker — голос озвучення (жіночий),
  • emotion — емоційне забарвлення голосу (доброзичлива),
  • key — отриманий на початку статті ключ.
Висновок
В рамках даної статті розглянуто простий приклад управління відтворенням музики в стандартному аудіоплеєрі Sailfish OS за допомогою голосових команд; отримані і повторені базові знання про розпізнавання та синтез мовлення з допомогою Яндекс SpeechKit Cloud з використанням засобів Qt, а також принципи взаємодії програм один з одним в Sailfish OS. Даний матеріал може послужити відправною точкою для більш глибоких досліджень і експериментів у цій операційній системі.

Приклад роботи наведеного коду можна подивитися на відео:


Автор: Петро Вытовтов
Джерело: Хабрахабр

0 коментарів

Тільки зареєстровані та авторизовані користувачі можуть залишати коментарі.