本記事でわかること
- レイテンシはいつだって付きまとう
- 変動する誤差は難しい
- 諦めることも肝心
0. はじめに
はじめまして。1010mark(てんてんまーく)と申します。
本記事はComputer Society Advent Calendar 2024 9日目の記事となっています。KCSは、Computer Societyという慶應義塾大学の公認サークルです。このアドベントカレンダーでは、サークル員が記事を書いています。分野の垣根を超えた様々な記事がありますので、ぜひご覧ください。
8日目「矢上キャンパスにも一際の花(クリスマスツリー)」
↑
この記事
↓
10日目「何か書くcar」
ちなみに俺が一番気になっている記事は最終日の「入水しました!!」(記事執筆時点仮タイトル)です。
さて、Tidal Cyclesをご存知ですか?ご存知でなければ、以下の動画を見てみるのが一番早いです。1秒だけでもいいのでクリックしてご視聴ください。
Tidal Cyclesはリアルタイムに音楽を生成することができるツールの一つです。もし興味があれば、以下の記事を参考に触ってみるのが早いです。- 公式ドキュメント - 環境構築はこれが一番参考になります。各種OSに対応しており、導入も簡単です。Windowsに関してはコマンドライン2行で構築できます。
- はじめてのTidal Cycles - 実際に手を動かしながらTidal Cyclesを学べます。
さて、このTidal Cyclesは様々な面白い機能を持っています。その一つがOSCです。
OpenSound Control(OSC)とは、電子楽器(特にシンセサイザー)やコンピュータなどの機器において音楽演奏データをネットワーク経由でリアルタイムに共有するための通信プロトコルである。
(引用: Wikipedia「OpenSound Control」)
これで何ができるかって言うと、例えばバスドラムに合わせて、外部に信号を出してLEDを点灯させるみたいなことができます。つまりリアルタイムに変動する音楽に合わせて即応性の高い反応ができるわけですね!
そんなある日、私はあることを思いつきました。
「OSCを使ってリアルタイムにポエトリーリーディングができるんじゃないか?」
これは失敗したコードの記事。
この失敗を見返したくない私が、
そんな、記事です。
1. とりあえずOSCを触る
まず公式ドキュメントを読みましょう。
基本的には公式ドキュメントの通りにしておけば、すべて動きます。
ちなみにTydal CyclesのOSCなので、間違ってもSuperColiderに打ち込んで実行しようとしないこと。私は1時間溶かしました。
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で。
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()
2. BPMに合わせてVOICEVOXを喋らせる(誤差①)
やってることはこのツールと同じです→VOICEVOXの読み上げをBPMと同期するツールについて by melonadeさん
てっきりOSSかと思ったらそうでもなかったので、いい感じに自作。
とりあえずコードを書いてみます
1時間後
コードができました。
「よっしゃ!さっそく実行や!」ということでテキトーな文章で実行…!
…ワクワク
ドキドキ…
バカズレてる!!!!!!!!!!!!!
…これが今回の記事のメインテーマである誤差の1つ目です。そしてそれぞれ誤差にどのように対処するかが重要になってきます。
今回の誤差はたまたま変動しない誤差なので、実際に誤差を計測し、その分音声を遅らせることで事なきを得ました。
これの原因はマジでよくわからん 先行無音・後方無音は0秒に設定してるのになんかズレるのよ なんで?
3. この2つを同時に再生してみる(誤差②)
BPMに合わせた音声が生成できました。あとはこれを同時に再生するだけですね!
ではさっそく試してみましょう。
とりあえずCLIで入力を受け付け、その文章を歌詞とします。
これで完璧にイカしたポエトリーリーディングがリアルタイムに…!
…ワクワク
ドキドキ…
バカズレてる!!!!!!!!!!!!!!!!!!!!!!!!
これが今回の記事のメインテーマである誤差の2つ目です。
さらに今回の誤差はただの誤差ではありません。それは音声波形を見るとわかります。
なんとこの誤差は変動します。つまり、先程のように計測して固定値でズラすという方法は通用しません。
困った…。
「もう打つ手はない…」
絶望に打ちひしがれながら、ズレたポエトリーを聞くしかありません。
人生には諦めなければならない瞬間があります。
そんな時、
ある天啓が舞い降ります。
4. 天啓
現在の構成は上図のようです。
TydalCyclesは単体で音がなるわけではありません。SuperDirtと合わせて使うものであり、SuperDirtにOSCを送ることでSuperDirtが音を鳴らします。
そして私はTydalCyclesからPythonサーバーまでに誤差の変動が生じていると考えました。
そしてこの誤差の変動を無くす方法を思いつきました。
間に別のサーバーを噛ましてあげれば、誤差が一定になるのでは…?
試してみる価値はあります。やってみましょう。
5. そして、敗北。(誤差③)
ということでやってみましょう。まずはSuperDirtのポート番号をテキトーにズラします。
SuperColider上でFileから「Open user support directory」を選択し、downloaded-quarks>SuperDirtとフォルダを開いていきます。そこにある57120がポート番号なので、57121に書き換えます。
%%% コード or 画像
次に、57120にlistenして、TydalCyclesからのOSCを転送するサーバーを立てます。なぜかPythonだと上手くいかなかったので、Node.jsで。
これで完璧~♥(早瀬ユウカ)です。
…ワクワク
ドキドキ…
ついに夢にまで見たポエトリーリーディングのリアルタイム自動生成が日の目を見ます。
…ワクワク
ドキドキ…
思えばここまで長かった…。
…ワクワク
ドキドキ…
途中で4年前のフリーゲームにドハマリして狂ってたけど、ついに…
…ワクワク
ドキドキ…
ついに、完成するんだ!!!!!!
ズレてるし、なんか不整脈みたいなビート!!!!!!!!!!!!!!!!!!
6. 敗因
そもそも誤差の変動の発生箇所を見誤っていました。
てっきり上図の箇所で誤差が変動している(いわゆるジッター)ものだと思っていましたが…
音声を再生するときに生じる誤差が変動しているみたいなんですよね。
これはPyAudioの責任ではありません。(むしろPyAudioは他のライブラリと比べて低遅延)
データの伝達がある以上、どうしても仕方ないことだと思います。
ということで、私の夢は潰えました。
人生には諦めなければならない瞬間があります。
以上です。
参考文献
- OSC | Tidal Cycles
- 失敗した記事。 | オモコロ
- voicevox_engine API Document
- [How to] SuperCollider 起動時にSuperDirtを起動させる2つの方法 | barbe_generative_diary
ちなみに今回のコードまとめのリポジトリあります
BPMに合わせて音声を生成するパートは上手く行ってるはずなんで…。