Liquidsoap と VOICEVOX でつくる音声合成ラジオ
はじめに
Liquidsoap というのは、音声や動画のストリーミングをプログラマブルに扱うことのできるツールです。Liquidsoap 独自のスクリプト言語で番組を定義すると、それに従って音声や動画をストリーミングできます。また、Liquidsoap 自体をストリーミングを行うためのサーバーとすることもできます。
このエントリでは、その Liquidsoap を使って、日本語音声合成エンジンである VOICEVOX による音声合成ラジオ配信システムを作る方法を紹介します。
最終的な成果物は GitHub darashi/liquidsoap-voicevox-radio にあります。
やや複雑なデプロイメントが必要になりますので、compose.yml
を置いておきました。併せてご覧ください。
VOICEVOX および音声合成エンジンの利用規約を遵守してお使いください。
Liquidsoap について
キューを定義して音楽を順に流すくらいはお手の物で、スクリプトを書きさえすれば、とにかく何でもできそうです。詳しくはドキュメントを参照してください。
さて、実は Liquidsoap はデフォルトでいくつかの音声合成エンジンに対応しています。Protocols として、HTTP 等の他に SAY などが定義されており、これを使うと音声合成エンジンに音声を送って、その音声をストリーミングできます。
しかし、 VOICEVOX には対応していません。そこで、対応させるためのライブラリを作りました。
本稿執筆時点での Liquidsoap のバージョンは 2.1.4 です。
なお、Ubuntu 22.10 で標準でインストールできる Liquidsoap は ocurl が入っていない状態でビルドされているのか(詳細は理解できていません)、Liquidsoap から外部への HTTP がうまく動作しませんでした。このため、 Docker image savonet/liquidsoap:v2.1.4
を使いました。
HLS 配信サーバーの構築
まず、簡単なところから始めましょう。 キューに投入された音声を順に再生する HLS サーバをつくる例です。
HLS とは、HTTP Live Streaming の略で、HTTP 経由で細切れのファイルを次々に配信することでライブストリーミングを実現します。なお、Liquidsoap は他にも icecast をはじめとして様々なストリーミングプロトコルの入出力に対応しています。詳細はドキュメントをご覧ください。
# hls.liq
settings.server.telnet.set(true)
settings.server.telnet.port.set(1234)
settings.server.telnet.bind_addr.set("0.0.0.0")
settings.server.log.level.set(4)
log.stdout.set(true)
settings.protocol.voicevox.url.set("http://example.com:50021") # localhost 以外に VOICEVOX を置く場合に URL を指定する
settings.server.telnet.set(true)
radio = request.queue(id="request")
formats = [("aac",
%ffmpeg(
format="mpegts",
%audio(codec="aac", b="96k")
))]
output.harbor.hls(
segment_duration=5.0,
segments=5,
segments_overhead=5,
path="/live",
tmpdir="/tmp/hls",
port=port,
formats,
radio)
この hls.sql
を liquidsoap hls.liq
として実行します。
すると、 http://localhost:8080/live/stream.m3u8
で HLS で配信が開始されます。
caniuse で見る限り、執筆時点で HLS に対応しているデスクトップブラウザは Safari だけです。それでも hls.js という Polyfill を使うことで HLS を再生できます。iPhone や Android のブラウザは Polyfill 不要です。
また、mpv や vlc でも再生できます。動作確認にはこれらのアプリケーションが便利です。たとえば mpv を使う場合、
mpv http://localhost:8080/live/stream.m3u8
とします。
さて、http://localhost:8080/live/stream.m3u8 を再生すると、無音状態になっているはずです。 これはキューが空のためです。
http、あるいは https で取得できる場所に音楽等を置いて、キューに追加してみましょう。 https://example.com/music.mp3 に配置したとすると、以下のようにします。
❯ telnet localhost 1234
Trying 127.0.0.1...
Connected to localhost.
Escape character is '^]'.
request.push https://example.com/music.mp3
0
END
しばらくするとファイルが再生されるはずです。
Docker の中で実行している場合は 1234/tcp と 8080/tcp をポートフォワードする必要があることに注意してください。
さて、この push
の引数に VOICEVOX への合成の指示を入れられるようにします。
VOICEVOX のセットアップ
まずは VOICEVOX https://voicevox.hiroshiba.jp/ をインストールしておきます。
Engine 部分 https://github.com/VOICEVOX/voicevox_engine だけのインストールでも、以降のスクリプトは動作します。
voicevox.liq
以下のファイルを voicevox.liq
として保存します。
# voicevox.liq
let settings.protocol.voicevox = settings.make.protocol("voicevoox")
let settings.protocol.voicevox.url = settings.make(
description="URL of VOICEVOX engine",
"http://localhost:50021"
)
def voicevox_protocol(~rlog, ~maxtime, arg) =
let [args, ...text] = r/:/.split(arg)
let text = string.concat(separator=":", text)
let speaker = args
output = file.temp("voicevox", ".wav")
let base = settings.protocol.voicevox.url()
let query_url = "#{base}/audio_query?speaker=#{speaker}&text=#{text}"
query = http.post(query_url)
let synthesis_url = "#{base}/synthesis?speaker=#{speaker}"
let file_writer = file.write.stream(output)
let response = http.post.stream(synthesis_url, headers=[("Content-Type", "application/json")], data=query, on_body_data=file_writer)
[output]
end
add_protocol("voicevox", voicevox_protocol)
このファイルを %include "voicevox.liq"
で読み込むと、 voicevox
というプロトコルが定義されます。使い方は以下の通りです。
# hls-voicevox.liq
%include "voicevox.liq" # ここを追加
settings.server.telnet.set(true)
settings.server.telnet.port.set(1234)
settings.server.telnet.bind_addr.set("0.0.0.0")
settings.server.log.level.set(4)
log.stdout.set(true)
settings.protocol.voicevox.url.set("http://example.com:50021") # localhost 以外に VOICEVOX を置く場合に URL を指定する
settings.server.telnet.set(true)
radio = request.queue(id="request")
formats = [("aac",
%ffmpeg(
format="mpegts",
%audio(codec="aac", b="96k")
))]
output.harbor.hls(
segment_duration=5.0,
segments=5,
segments_overhead=5,
path="/live",
tmpdir="/tmp/hls",
port=port,
formats,
radio)
このスクリプトを hls-voicevox.liq
として保存して、 liquidsoap hls-voicevox.liq
で実行します。
次に、 telnet で 1234/tcp に接続して、音声合成リクエストをキューに投入します。
❯ telnet localhost 1234
Trying 127.0.0.1...
Connected to localhost.
Escape character is '^]'.
request.push voicevox:1:test
0
END
ここで、 voicevox:
のあとの 1
が speaker の ID 、2個目の :
以降が読み上げるテキストです。
VOICEVOX Engine が起動している状態で http://localhost:50021/speakers を表示すると話者一覧が取得できます。 speaker ID はここから選択できます。
また、Liquidsoap の仕様なのか、URL部分に日本語を含むと途中で化けてしまうようなので、テキスト部分は URL Encode する仕様にしました。
例として「本日は晴天なり」と発話させてみましょう。Node.js でエンコードするには以下のようにします。
❯ node
Welcome to Node.js v18.12.0.
Type ".help" for more information.
> encodeURI("本日は晴天なり")
'%E6%9C%AC%E6%97%A5%E3%81%AF%E6%99%B4%E5%A4%A9%E3%81%AA%E3%82%8A'
では、これを Liquidsoap に送ってみます。
❯ telnet localhost 1234
Trying 127.0.0.1...
Connected to localhost.
Escape character is '^]'.
request.push voicevox:3:%E6%9C%AC%E6%97%A5%E3%81%AF%E6%99%B4%E5%A4%A9%E3%81%AA%E3%82%8A
0
END
どうでしょう。合成されたでしょうか。これで VOICEVOX による読み上げラジオができました。
HLS で配信するラジオ局スクリプト
とはいえ、telnet
でコマンドを送信するのはやや面倒です。
配信サーバを外部からアクセス可能な場所に置くのであれば、何らかの方法で制御用のポートを保護する必要があります。
ところで、 Liquidsoap は HTTP サーバを作ることもできます。 これを使って、HTTP 経由でキューに投入できるようにしてみましょう。
Basic認証でパスワードを設定できるようにしておきます。
制御用のパスワードは CONTROL_PASSWORD
環境変数で設定します。
%include "voicevox.liq"
settings.server.log.level.set(4)
log.stdout.set(true)
settings.protocol.voicevox.url.set("http://example.com:50021") # localhost 以外に VOICEVOX を置く場合に URL を指定する
settings.harbor.verbose.set(true)
settings.frame.audio.samplerate.set(24000)
settings.frame.audio.channels.set(1)
port = 8080
formats = [("aac",
%ffmpeg(
format="mpegts",
%audio(codec="aac", b="96k")
))]
queue = request.queue(id="request")
radio = mksafe(queue)
def ping_handler(~protocol, ~data, ~headers, uri) =
http.response(
protocol=protocol,
code=200,
headers=[("Content-Type","application/json; charset=utf-8")],
data=json.stringify({message = "OK"})
)
end
harbor.http.register(port=port, method="GET", "/ping", ping_handler)
def get_password(headers) =
try
let entry = list.find(fun (v) -> begin
let (header_name, _) = v
string.case(lower=true, header_name) == "authorization"
end, headers)
let (_, authorization) = entry
let [scheme, ...credential] = r/ /.split(authorization)
let credential = string.base64.decode(string.concat(separator=" ", credential))
let [user, ...password] = r/:/.split(credential)
string.concat(separator=":", password)
catch _ : [error.not_found] do
null()
end
end
def say_handler(~protocol, ~data, ~headers, uri) =
pw = get_password(headers)
if pw == null() then
http.response(
protocol=protocol,
code=401,
headers=[("Content-Type","application/json; charset=utf-8")],
data=json.stringify({message = "Unauthorized"})
)
else
if pw != getenv("CONTROL_PASSWORD") then
http.response(
protocol=protocol,
code=401,
headers=[("Content-Type","application/json; charset=utf-8")],
data=json.stringify({message = "Unauthorized"})
)
else
let json.parse ({speaker, text} : {speaker: int, text: string} ) = data
print("speaker: #{speaker}, text: #{text}")
let req = request.create("voicevox:#{speaker}:#{url.encode(text)}")
queue.push(req)
http.response(
protocol=protocol,
code=200,
headers=[("Content-Type","application/json; charset=utf-8")],
data=json.stringify({message = "OK"})
)
end
end
end
harbor.http.register(port=port, method="POST", "/say", say_handler)
output.harbor.hls(
segment_duration=5.0,
segments=5,
segments_overhead=5,
path="/live",
tmpdir="/tmp/hls",
port=port,
formats,
radio)
このスクリプトを実行して
❯ curl -v --data-binary '{"speaker": 14, "text": "テストなのだ"}' -H 'Content-type: application/json' http://vv:super-secret-password@localhost:8080/say
のようにすると、キューにリクエストが投入されます。
まとめ
Liquidsoap と VOICEVOX を使って、音声合成を活用したラジオストリーミングサーバをつくる方法を紹介しました。 面白いアプリケーションづくりに活用してみてください。
今後の課題
以下のようなAPIがあると楽しそうですね。
- キューをクリアして即時に音声再生を開始するAPI
- バックグラウンドで曲をかけるAPI
- 音声URLを利用して曲をかけるAPI