【失敗談】ポエトリーリーディングのリアタイ自動生成ができなかった話【Computer Society Advent Calendar 2024】

# 本記事でわかること
- レイテンシはいつだって付きまとう
- 変動する誤差は難しい
- 諦めることも肝心

# 0. はじめに
はじめまして。1010mark（てんてんまーく）と申します。
本記事は[Computer Society Advent Calendar 2024](https://qiita.com/advent-calendar/2024/kcs) 9日目の記事となっています。KCSは、Computer Societyという慶應義塾大学の公認サークルです。このアドベントカレンダーでは、サークル員が記事を書いています。分野の垣根を超えた様々な記事がありますので、ぜひご覧ください。

8日目「矢上キャンパスにも一際の花（クリスマスツリー）」
↑
この記事
↓
10日目「何か書くcar」

ちなみに俺が一番気になっている記事は最終日の「入水しました！！」（記事執筆時点仮タイトル）です。

さて、**Tidal Cycles**をご存知ですか？ご存知でなければ、以下の動画を見てみるのが一番早いです。1秒だけでもいいのでクリックしてご視聴ください。
<iframe width="560" height="315" src="https://www.youtube.com/embed/wgPf24_OQZY?si=q464hPyTdJHn9vxx&amp;start=133" title="YouTube video player" frameborder="0" allow="accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture; web-share" referrerpolicy="strict-origin-when-cross-origin" allowfullscreen></iframe>
Tidal Cyclesはリアルタイムに音楽を生成することができるツールの一つです。もし興味があれば、以下の記事を参考に触ってみるのが早いです。

- [公式ドキュメント](https://tidalcycles.org/docs/) - 環境構築はこれが一番参考になります。各種OSに対応しており、導入も簡単です。Windowsに関してはコマンドライン2行で構築できます。
- [はじめてのTidal Cycles](https://barbegenerativediary.com/tag/biginner-tidalcycles/) - 実際に手を動かしながらTidal Cyclesを学べます。

さて、このTidal Cyclesは様々な面白い機能を持っています。その一つが**OSC**です。

> OpenSound Control（OSC）とは、電子楽器（特にシンセサイザー）やコンピュータなどの機器において音楽演奏データをネットワーク経由でリアルタイムに共有するための通信プロトコルである。
（引用: [Wikipedia「OpenSound Control」](https://ja.wikipedia.org/wiki/OpenSound_Control)）

これで何ができるかって言うと、例えば**バスドラムに合わせて、外部に信号を出してLEDを点灯させる**みたいなことができます。つまりリアルタイムに変動する音楽に合わせて即応性の高い反応ができるわけですね！

そんなある日、私はあることを思いつきました。
<img width="25%"><img src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1542948/88c2960d-b953-7254-bc7e-f602628245fa.png" alt="虹色に発光する人" width="50%"/>
「OSCを使って**リアルタイムにポエトリーリーディング**ができるんじゃないか？」

### これは失敗したコードの記事。
この失敗を見返したくない私が、

記事を書き上げるまで幾度となく絶望した。
<img width="25%"><img src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1542948/3e3e6f15-5ea0-9310-c5a1-3491243b93cb.png" width="50%" alt="暗虹色に発光する人" />

そんな、記事です。

# 1. とりあえずOSCを触る
まず[公式ドキュメント](https://tidalcycles.org/docs/configuration/MIDIOSC/osc/)を読みましょう。
基本的には公式ドキュメントの通りにしておけば、すべて動きます。
ちなみにTydal CyclesのOSCなので、間違ってもSuperColiderに打ち込んで実行しようとしないこと。~~私は1時間溶かしました。~~
```haskell
let visualTarget = Target
      { oName = "processing"   -- ターゲットの名前
      , oAddress = "localhost" -- localhostに送信
      , oPort = 2020           -- Processingがリッスンしているポート番号
      , oLatency = 10000         -- レイテンシ調整
      , oSchedule = Pre MessageStamp      -- スケジューリング設定 おい！スペース空いてて良いんですね！？
      , oWindow = Nothing
      , oHandshake = False
      , oBusPort = Nothing }

let oscVisual = OSC "/visual" $ ArgList
      [ ("s", Nothing)
      , ("cycle", Just $ VF 0)
      , ("sec", Just $ VF 0)
      , ("usec", Just $ VF 0)
      ]

let oscmap = [(visualTarget, [oscVisual])]

stream <- startStream defaultConfig oscmap

streamReplace stream 1 $ s "testbb"
```
せっかくなので、それを受信するコードも書いてみましょう。とりあえずPythonで。
```Python
import argparse
from pythonosc import dispatcher
from pythonosc import osc_server

def parse_osc_message(message):
    parsed = {}
    for i in range(0, len(message), 2):  # 2個ずつ取り出す
        key = message[i]
        value = message[i + 1]
        parsed[key] = value
    return parsed

def handle_cycles(address, *args):    # Cycle表示用の関数を作るつもりだったけど、やっぱやめたのでこういう関数名です
    print(args)


if __name__ == "__main__":
    parser = argparse.ArgumentParser()
    parser.add_argument("--ip",
                        default="127.0.0.1", help="The ip to listen on")
    parser.add_argument("--port",
                        type=int, default=2020, help="The port to listen on")
    args = parser.parse_args()

    dispatcher = dispatcher.Dispatcher()
    dispatcher.map("/*", handle_cycles)

    server = osc_server.ThreadingOSCUDPServer(
        (args.ip, args.port), dispatcher)
    print("Serving on {}".format(server.server_address))
    server.serve_forever()
```
これを実行すると
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1542948/9c23fd98-c769-5f7a-aede-99943cf499d9.png)
ちゃんと受信できていますね。

# 2. BPMに合わせてVOICEVOXを喋らせる（誤差①）
やってることはこのツールと同じです→[VOICEVOXの読み上げをBPMと同期するツールについて by melonadeさん](https://note.com/melonade/n/n06351a57a9e8?sub_rt=share_sb)
てっきりOSSかと思ったらそうでもなかったので、いい感じに自作。
とりあえずコードを書いてみます

## 1時間後

コードができました。

「よっしゃ！さっそく実行や！」ということでテキトーな文章で実行…！

![実行ログ。正常に終了してそう](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1542948/9c87cf54-641d-e9a8-b6d8-b8a412144d45.png)


…ワクワク
　　　　ドキドキ…

**結果：**
![音声波形](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1542948/1bbcb32f-488e-1702-2879-3b479c7ac635.png)
![波形上で、ポエトリーとBPMに線を引いている。その線は微妙にズレている](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1542948/8d7b4f58-e91a-1f8c-6f41-75941675291c.png)

**バカズレてる！！！！！！！！！！！！！**


…これが**今回の記事のメインテーマである誤差**の１つ目です。そしてそれぞれ誤差にどのように対処するかが重要になってきます。

今回の誤差はたまたま**変動しない誤差**なので、実際に誤差を計測し、その分音声を遅らせることで事なきを得ました。

[実際のコードはこちら](https://github.com/1010mark/poetry_reading_failure/blob/main/voicevox_bpm.py)

:::note info
これの原因はマジでよくわからん　先行無音・後方無音は0秒に設定してるのになんかズレるのよ　なんで？
:::

# 3. この2つを同時に再生してみる（誤差②）

BPMに合わせた音声が生成できました。あとはこれを同時に再生するだけですね！
ではさっそく試してみましょう。

![Enter text: ダン、ダン、ダン、…](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1542948/6248ffae-59e9-6935-22ac-1ce214b77a70.png)


とりあえずCLIで入力を受け付け、その文章を歌詞とします。
これで完璧にイカしたポエトリーリーディングがリアルタイムに…！

…ワクワク
　　　　ドキドキ…

**結果：**
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1542948/9a481296-b508-ca24-19ce-cf5bb7fd49d7.png)

**バカズレてる！！！！！！！！！！！！！！！！！！！！！！！！**

これが今回の記事のメインテーマである誤差の２つ目です。
さらに今回の誤差はただの誤差ではありません。それは音声波形を見るとわかります。

![音声波形にBPMとポエトリーに](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1542948/4828d778-790d-fcb7-d90b-89abf4c572a1.png)


なんとこの誤差は**変動します**。つまり、先程のように計測して固定値でズラすという方法は通用しません。
困った…。

<img width="25%"><img width="50%" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1542948/a002cbb3-046d-1280-e695-990e024f6431.jpeg" alt="黒色に染まる人">

「もう打つ手はない…」
絶望に打ちひしがれながら、ズレたポエトリーを聞くしかありません。

:::note info
人生には諦めなければならない瞬間があります。
:::

そんな時、

<img width="25%"><img width="50%" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1542948/07787a4f-65eb-6868-f4b6-b3b564e66f0b.jpeg" alt="夕焼けのように紅潮する人">

ある**天啓**が舞い降ります。

# 4. 天啓
![TydalCyclesから2つの道筋が示されている。1つ目がSuperDirtを通って音を鳴らすルート。2つ目が先述のPythonコードとPyAudioを通って音を鳴らすルート。](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1542948/f5bd1f0f-797d-7425-0ca0-e9597849af04.jpeg)

現在の構成は上図のようです。
TydalCyclesは単体で音がなるわけではありません。SuperDirtと合わせて使うものであり、SuperDirtにOSCを送ることでSuperDirtが音を鳴らします。
そして私は**TydalCyclesからPythonサーバーまでに誤差の変動が生じている**と考えました。

![先ほどの画像のうち、TydalCyclesからPythonコードへの矢印が太くなっている。](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1542948/a955134a-e98d-a6f9-b148-0d91a513b67f.jpeg)

そしてこの誤差の変動を無くす方法を思いつきました。

![](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1542948/1b904ce7-6545-38b7-d5c5-47a06f2aea7e.jpeg)

**間に別のサーバーを噛ましてあげれば**、誤差が一定になるのでは…？

試してみる価値はあります。やってみましょう。

# 5. そして、敗北。（誤差③）
ということでやってみましょう。まずはSuperDirtのポート番号をテキトーにズラします。
SuperColider上でFileから「Open user support directory」を選択し、downloaded-quarks＞SuperDirtとフォルダを開いていきます。そこにある57120がポート番号なので、57121に書き換えます。

次に、57120にlistenして、TydalCyclesからのOSCを転送するサーバーを立てます。なぜかPythonだと上手くいかなかったので、Node.jsで。


これで完璧～<font color="red">♥</font>（早瀬ユウカ）です。

…ワクワク
　　　　ドキドキ…


ついに夢にまで見たポエトリーリーディングのリアルタイム自動生成が日の目を見ます。

…ワクワク
　　　　ドキドキ…

思えばここまで長かった…。

…ワクワク
　　　　ドキドキ…

途中で[4年前のフリーゲーム](https://ribon-games.itch.io/here)にドハマリして狂ってたけど、ついに…

…ワクワク
　　　　ドキドキ…

ついに、完成するんだ！！！！！！

**結果：**
![音声波形。明確に2つの山がズレている。](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1542948/d2a8d399-1ecc-ce03-77d3-5f48dd036b7e.png)

**ズレてるし、なんか不整脈みたいなビート！！！！！！！！！！！！！！！！！！**

# 6. 敗因

そもそも誤差の変動の発生箇所を見誤っていました。

![3章2枚目の画像と同じ。TydalCyclesからPythonコードへの矢印が太くなっている。](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1542948/a955134a-e98d-a6f9-b148-0d91a513b67f.jpeg)

てっきり上図の箇所で誤差が変動している（いわゆるジッター）ものだと思っていましたが…

![PyAudioが赤くなっている](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1542948/0b9a5894-8e5f-b527-6201-9f79e21f4a4a.jpeg)

**音声を再生するときに生じる誤差が変動している**みたいなんですよね。
これはPyAudioの責任ではありません。（むしろPyAudioは他のライブラリと比べて低遅延）
データの伝達がある以上、どうしても仕方ないことだと思います。
ということで、私の夢は潰えました。

<img width="25%"><img width="50%" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1542948/733473f7-3523-01c3-d1e8-aad65b4cbb45.jpeg" alt="暗闇に染まる人">

:::note info
人生には諦めなければならない瞬間があります。
:::

以上です。

# 参考文献
- [OSC | Tidal Cycles](https://tidalcycles.org/docs/configuration/MIDIOSC/osc/)
- [失敗した記事。 | オモコロ](https://omocoro.jp/kiji/443493/)
- [voicevox_engine API Document](https://voicevox.github.io/voicevox_engine/api/)
- [[How to] SuperCollider 起動時にSuperDirtを起動させる２つの方法 | barbe_generative_diary](https://barbegenerativediary.com/tutorials/tidal-cycles-tutorials/tidalcycles-setup-supercollider01/)

ちなみに[今回のコードまとめのリポジトリあります](https://github.com/1010mark/poetry_reading_failure)
BPMに合わせて音声を生成するパートは上手く行ってるはずなんで…。