前回に続きsndcpy関連の話です。

sndcpyのサーバーは、ソースコードを読むと分かるように複数クライアントの接続を想定していません。 そのため、録音しながらモニタリングできないのです。ならば複数接続できるようにしてやろう、ということでPythonでTCPサーバーを作ります。

仕組み

まず、すべてのクライアントに同じデータを送信する必要があります。

1つの変数にデータを入れていって、各クライアントを処理するプロセスがそれを読み取ればいいのではないかと考える方もいるかと思いますが、それだと矛盾が起きたり後にバグに繋がる可能性があるので、クライアントごとにキューを持たせ、sndcpyのサーバーからデータを受け取ってそれぞれのキューに入れていくようにします。クライアントに配信するプロセスは自身のキューを読むだけで済むのでバグの原因になりにくいです。

実装

まずsndcpyのサーバーに接続します。

sndcpy = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
sndcpy.connect(('localhost', 28200))

threadingを使ってバッググラウンドでキューに貯めていってもらいます。 queuesという配列に入っているクライアントに全部入れています。

queues = []
stopFlag = False
def data_queue():
    while not stopFlag:
        data = sndcpy.recv(1024)
        for q in queues:
            q: queue.Queue = q['queue']
            q.put(data)
data_queue_thread = threading.Thread(target=data_queue)
data_queue_thread.start()

stopFlagを用意しているのはプログラムが完全に止まるようにするためです。 スレッドが動いているとメインプログラムが終了してもいつまでも全体が止まりません。Ctrl+Cでも止められなくなります。

次にキュー管理用の関数を作ります。

def create_queue():
    qid = str(uuid.uuid4())
    d = {'queue': queue.Queue(), 'id': qid}
    queues.append(d)
    return qid

def get_queue(qid):
    for q in queues:
        if q['id'] == qid:
            return q

def delete_queue(qid):
    for q in queues:
        if q['id'] == qid:
            queues.remove(q)

次に各クライアントが接続してきた時の処理を実装します。

async def handle_client(reader: asyncio.StreamReader, writer: asyncio.StreamWriter):
    qid = create_queue()
    while True:
        if writer.is_closing():
            delete_queue(qid)
            break

        q: queue.Queue = get_queue(qid)['queue']
        data = q.get()
        try:
            writer.write(data)
            await writer.drain()
        except:
            delete_queue(qid)
            break

キューを作成して、ただ受け取って相手に送信するだけの非常に簡単な仕組みになっています。相手から切断されるまでずっと送信します。

最後にサーバーを起動するコードを書いて終わりです。ここではポート28201でリッスンしています。

async def run_server():
    server = await asyncio.start_server(handle_client, 'localhost', 28201)
    async with server:
        await server.serve_forever()

try:
    asyncio.get_event_loop().run_until_complete(run_server())
except:
    stopFlag = True

localhost の部分を 0.0.0.0 にすればローカルネットワーク上の他のデバイスからアクセスすることもできます。

あとは適当な名前で保存して、前回の記事のようにsndcpyを立ち上げてフォワーディングの準備ができたらこのサーバーを起動するだけでOKです。

プログラムはGitHubのリポジトリに上げているのでぜひ活用してください。

https://github.com/CyberRex0/sndcpy/blob/master/sndcpy_relay.py

自分が必要としているものを自分で作って解決する・・・これ結構楽しいですよ。