クリスマスの夜にボイパやラップで踊り狂うアレクサ

こんにちは。CYBIRDエンジニアによる [CYBIRD Advent Calendar 2021](https://qiita.com/advent-calendar/2021/cybird) 、16日目担当の @kyukkyu81 です。
15日目は @utamakura さんによる「Amplify+React+Node.jsを使ってWebSocket通信の簡単なチャットアプリを作ってみる」でした。
1日じゃ読み切れないボリュームで先輩びっくりだよ…まぁそれはそれとして、サーバーレス環境をいじってきた私もAmplifyやReactはほぼ同じくらいに始めた感じですが、あれは慣れればちょっとした管理ツールは簡単に作れていいですね。ただAmplify CLIのあの最初の怒涛のYes/No攻撃に慣れるまではちょっとしたハードルでした。デフォルト選択を覆せない日本人…。

#はじめに

趣味も仕事も似たようなことをしているのですが、amazon EchoShow等のAlexa対応端末で、カスタムスキル（スマホで言うアプリのようなもの）を開発・公開しており、特に趣味で開発しているものについては画面付き端末で画面表示言語APL(Alexa Presentation Language)を駆使してアクションゲームの実現に情熱を注いでいます。スマートスピーカーなのに画面制御メイン。こんなニッチなジャンルのせいか、同志がめちゃめちゃ少ないです。「海外の大会に参加する旅費があれば日本代表になれるモルック」のような状態です。

https://molkky.jp/faq/

すいません、脱線しました。代わりにこちらのサイト[apl.ninja]を紹介します。先ほどのAPL言語の虜となった人たちが、自作した「Alexaの画面表示部分」を発表しあうコミュニティです。

https://apl.ninja/

公式のAlexaカスタムスキルは、スキルとして完成されている必要があり、審査を通過しなければ公開することができませんが、こちらは審査無くどんなに簡単なサンプルでも即公開することができます。

興味ある方は覗いてみてください。私も参戦しています。

#はじめに(Take2)

ここまで書きましたが、やっぱりCYBIRDで技術1部兼VoiceUI部の身としては声にまつわる話がしたいので、SSML(音声合成マークアップ言語)について何かやってみたいと思います。

SSMLはW3Cによって定義されたれっきとしたマークアップ言語ですが、スマートスピーカーのメーカーによって独自の進化や方言が発達しており、今回はamazon Alexa対応端末のAPLA(APL for Audio)という仕様で鳴らせるSSMLを使って、そうだなぁ…ボイパとかラップみたいなのを作ってみようと思います。

#ざっくりルールを決める

そもそもボカロではないので、できる範囲でやっていきます。

+ あたかもリズムが取れているような演出を目指す
+ 音程を表現できないことをあきらめる
+ mp3を鳴らせるが、敢えて今回は音源は使わず純粋にアレクサ姉さんの声だけで挑戦
+ SSMLタグでいろいろ声をいじってみる

#実践方法

誰でも無料でAlexaのカスタムスキルを作ることが出来る「Alexa開発コンソール」というものがあります。

https://developer.amazon.com/ja-JP/alexa/alexa-skills-kit

そこから適当にダミーのスキルを作り、「マルチモーダル」⇒「Audio」⇒「Create Audio Response」のボタンを押すと、なにやらJSON形式のひな型が出てきます。この画面でそのまま発話を試すことができるので、早速これを改造していきます。

#さぁやっぞ

まず基本となる要素だけ作ります。これをベースに肉付けしていきます。

```json
{
    "type": "APLA",
    "version": "0.9",
    "mainTemplate": {
        "item": {
            "type": "Mixer",
            "items": [
                {
                    "type": "Sequencer",
                    "data": [ 1,2,3,4 ],
                    "items": [
                        {
                            "type": "Mixer",
                            "items": [
                                {
                                    "type": "Speech",
                                    "contentType": "SSML",
                                    "content": "<speak><break time=\"1s\"/></speak>"
                                },
                                {
                                    "type": "Speech",
                                    "contentType": "SSML",
                                    "content": "<speak>ずん</speak>"
                                }
                            ]
                        }
                    ]
                }
            ]
        }
    }
}
```

JSONなので、コードがこんもり山になってしまうのは仕方のないことです。諦めてください。
上記何やってるかと言うと、だいたいこんな感じです。

+ mainTemplate ⇒ この中が本体
+ Mixer ⇒ 記載した要素を同時に実行する
+ Sequencer ⇒ 記載した要素を順次実行する
+ Speech ⇒ しゃべる

Speech内のcontentに記載しているのが、実際のSSMLです。HTMLのようなタグで表記されています。

https://developer.amazon.com/ja-JP/docs/alexa/custom-skills/speech-synthesis-markup-language-ssml-reference.html

実際に鳴らすと、こんな感じになります。

https://drive.google.com/file/d/1Kcptzi03t-6FELBQ9uzYAaQwwlCeyyyv/view?usp=sharing

アレクサ姉さんが、「ずん、ずん、ずん、ずん」とリズムを刻んだと思います。
ここで大事なのは、1つ目のSpeechで「1秒の無音」を再生していることです。

```
<speak><break time=\"1s\"/></speak>
```

これにより、他の発話が1秒に満たなくても1秒待つことで1小節？1秒を確保しています。発話が1秒を超えてしまうと容易にリズムを崩してしまうので、必ず1秒に収まるように発話させる必要があります。

また、繰り返しの回数を決めているのは

```
"data": [ 1,2,3,4 ],
```

この行です。APLAの仕様で、コンポーネントがdataプロパティを持つと、その配列数分だけ中のコンポーネントが複製される、というものです。
子コンポーネントの中では`${data}`と記載することにより、その値を拾うことが出来ます。

さて、次に半拍遅れて「ちゃ」と言わせてみたいと思います。
2つのSpeechのあとにもうひとつSpeechを追加します。

```json
{
    "type": "Speech",
    "contentType": "SSML",
    "content": "<speak><break time=\"0.5s\"/>ちゃ<sub alias=\"\">${data*1}</sub></speak>"
}
```

mp3にしてみるとこんな感じです。

https://drive.google.com/file/d/1nBnMzYaJ1qxPYWWZQ20Fa8ddj3TQtlJ1/view?usp=sharing

breakタグで0.5秒の無音を生成することで半拍を表現しています。
最後のsubタグは何かというと…このタグは本来ルビのようなもので、その文字とは異なる言い回しを表現するためのものです。しかし文字の部分には`${data*1}`すなわち1～4の数値が入り、その言い方は空文字である…つまり「何も発話しない」という意味になります。
なぜこのような表記が必要かというと、このAlexa音声シミュレータの~~バグ~~仕様で「発話する中身が毎回変わらないと3回以上繰り返してくれない」という現象が発生するため、毎回何かしら出力内容を変えることで回避しています。これ作っているときに気付きました。あとで報告してあげようっと。

次に、ただ4回繰り返すだけではつまらないので、毎回違うことをしゃべらせてみます。
ちょっといろいろ改変したので、Sequencerコンポーネントの中身をまるっと再掲します。

```json
{
    "type": "Sequencer",
    "data": [
        [ 1,"ぶんぶんぶーん","すたすた" ],
        [ 2,"ぶんぶんぶーん","ちゃかちゃか" ],
        [ 3,"ぶんぶんぶーん","ほーっほーっ" ],
        [ 4,"",""]
    ],
    "items": [
        {
            "type": "Mixer",
            "items": [
                {
                    "type": "Speech",
                    "contentType": "SSML",
                    "content": "<speak><break time=\"2s\"/><sub alias=\"\">${data[0]}</sub></speak>"
                },
                {
                    "type": "Speech",
                    "contentType": "SSML",
                    "content": "<speak><prosody pitch=\"-30%\" volume=\"-10dB\">${data[1]}</prosody><sub alias=\"\">${data[0]}</sub></speak>"
                },
                {
                    "type": "Speech",
                    "contentType": "SSML",
                    "content": "<speak><break time=\"1s\"/><prosody pitch=\"-30%\" volume=\"-10dB\">${data[2]}</prosody><sub alias=\"\">${data[0]}</sub></speak>"
                }
            ]
        }
    ]
}
```

はい、こんな感じになりました。
発話が間に合わないので、そっと１小節２秒にしています…。

https://drive.google.com/file/d/1UCXRc-J0rUulLLBVAA4TP2mzYpo46T-V/view?usp=sharing

ポイントとしては、dataを2次元配列とし、各小節ごとに発話したい内容を文字列で持ったことです。Speechコンポーネントの中で`${data[1]}`などとして異なる言葉を取得しています。
また、SSMLで発話を装飾しています。

```
<prosody pitch=\"-30%\" volume=\"-10dB\">ほげほげ</prosody>
```

pitchで声の高低を、volumeで声の大きさを指定できます。ここでは指定していませんが、rateで声の速さを指定することもできます。

#そして完成

いきなり端折りますが、完成形はこちらです。

https://drive.google.com/file/d/1DeAHWAezOStwz82QdUEhQpt7YarvDyj5/view?usp=sharing

ソース、ちょっと長いですが直接貼っちゃいます。お許しください。

```json
{
    "type": "APLA",
    "version": "0.9",
    "description": "Christmas when my boss was hustle",
    "compositions": {},
    "resources": [],
    "mainTemplate": {
        "parameters": [
            "payload"
        ],
        "item": {
            "type": "Mixer",
            "items": [
                {
                    "type": "Sequencer",
                    "data": [
                        [ 1,"そしていつもの朝が来て" ],
                        [ 2,"普段通りに仕事して" ],
                        [ 3,"終わって気づいた今日の夜" ],
                        [ 4,"<say-as interpret-as=\"interjection\">メリークリスマス</say-as><say-as interpret-as=\"interjection\">ウオーッ</say-as>"]
                    ],
                    "items": [
                        {
                            "type": "Mixer",
                            "items": [
                                {
                                    "type": "Speech",
                                    "contentType": "SSML",
                                    "content": "<speak><break time=\"2s\"/><sub alias=\"\">${data[0]}</sub></speak>"
                                },
                                {
                                    "type": "Speech",
                                    "contentType": "SSML",
                                    "content": "<speak><amazon:domain name=\"fun\">${data[1]}</amazon:domain></speak>"
                                }
                            ]
                        }
                    ]
                },
                {
                    "type": "Sequencer",
                    "data": [
                        [ 1,"ぶんぶんぶーん","すたすた" ],
                        [ 2,"ぶんぶんぶーん","ちゃかちゃか" ],
                        [ 3,"ぶんぶんぶーん","ほーっほーっ" ],
                        [ 4,"","" ]
                    ],
                    "items": [
                        {
                            "type": "Mixer",
                            "items": [
                                {
                                    "type": "Speech",
                                    "contentType": "SSML",
                                    "content": "<speak><break time=\"2s\"/><sub alias=\"\">${data[0]}</sub></speak>"
                                },
                                {
                                    "type": "Speech",
                                    "contentType": "SSML",
                                    "content": "<speak><prosody pitch=\"-30%\" volume=\"-10dB\">${data[1]}</prosody><sub alias=\"\">${data[0]}</sub></speak>"
                                },
                                {
                                    "type": "Speech",
                                    "contentType": "SSML",
                                    "content": "<speak><break time=\"1s\"/><prosody pitch=\"-30%\" volume=\"-10dB\">${data[2]}</prosody><sub alias=\"\">${data[0]}</sub></speak>"
                                }
                            ]
                        }
                    ]
                },
                {
                    "type": "Sequencer",
                    "data": [
                        [ 1,"ち","ち","ち","あさー" ],
                        [ 2,"ち","ち","ち","ひるー" ],
                        [ 3,"ち","ち","ち","よるー" ],
                        [ 4,"","","",""]
                    ],
                    "items": [
                        {
                            "type": "Mixer",
                            "items": [
                                {
                                    "type": "Speech",
                                    "contentType": "SSML",
                                    "content": "<speak><break time=\"2s\"/><sub alias=\"\">${data[0]}</sub></speak>"
                                },
                                {
                                    "type": "Speech",
                                    "contentType": "SSML",
                                    "content": "<speak><prosody pitch=\"+50%\" rate=\"200%\" volume=\"-10dB\">${data[1]}</prosody><sub alias=\"\">${data[0]}</sub></speak>"
                                },
                                {
                                    "type": "Speech",
                                    "contentType": "SSML",
                                    "content": "<speak><break time=\"0.5s\"/><prosody pitch=\"+50%\" rate=\"200%\" volume=\"-10dB\">${data[2]}</prosody><sub alias=\"\">${data[0]}</sub></speak>"
                                },
                                {
                                    "type": "Speech",
                                    "contentType": "SSML",
                                    "content": "<speak><break time=\"1s\"/><prosody pitch=\"+50%\" rate=\"200%\" volume=\"-10dB\">${data[3]}</prosody><sub alias=\"\">${data[0]}</sub></speak>"
                                },
                                {
                                    "type": "Speech",
                                    "contentType": "SSML",
                                    "content": "<speak><break time=\"1.5s\"/><prosody pitch=\"+50%\" rate=\"200%\" volume=\"-10dB\">${data[4]}</prosody><sub alias=\"\">${data[0]}</sub></speak>"
                                }
                            ]
                        }
                    ]
                }
            ]
        }
    }
}
```

ついカッとなって小一時間悪乗りしてしまった。後悔はしていない。

ここでのポイントは、今までのSequencerを３つ並べて、ボーカル・ベース・パーカス？みたいな３重唱にしているところです。また、SSMLについて

```
<amazon:domain name=\"fun\">たーのしーい</amazon:domain>
```

というタグ表記でアレクサ姉さんが楽しいモードでしゃべってくれています。
あ、あとこれですね。

```
<say-as interpret-as=\"interjection\">ウオーッ</say-as>
```

これはSpeechConといって、より表情豊かに発話する特別な語句やフレーズをしゃべらせる一種の定型文です。一覧は下記にあります。

https://developer.amazon.com/ja-JP/docs/alexa/custom-skills/speechcon-reference-interjections-japanese.html

#あとがき

いかがだったでしょうか。本当はAmazon PollyというサービスのMizukiさんやTakumiさんも発話させることができるのですが、発話の立ち上がりが遅くてリズム感良くしゃべらせられませんでした。もっと凝った書き方をして全体を遅らせることで出だしを合わせることは可能かと思いますが、まぁこれ位にしておきます。さらに楽曲を再生したり、映像表示と組み合わせたりしたらもう、立派なラッパースキルのできあがりじゃないですか！興味ある方は試してみましょう！そして増えて欲しい同志。（切実）

明日の[CYBIRDエンジニア Advent Calendar 2021](https://qiita.com/advent-calendar/2021/cybird) 17日目は、@cy-tatsuya-sakai さんの「Unity的な何か」です。Unity、世に出たころから非常に気になっているのに手を出さぬまま月日が経ってしまいました。。俺、年が明けたらUnityがんばるよ！ペキッ（何かのフラグを踏んだ音）