-
Notifications
You must be signed in to change notification settings - Fork 128
Expand file tree
/
Copy pathplay_audio_stream.py
More file actions
158 lines (131 loc) · 4.28 KB
/
Copy pathplay_audio_stream.py
File metadata and controls
158 lines (131 loc) · 4.28 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
import asyncio
import os
import numpy as np
import sounddevice as sd
from livekit import rtc, api
from livekit.plugins import noise_cancellation
SAMPLERATE = 48000
BLOCKSIZE = 480 # 10ms chunks at 48kHz
CHANNELS = 1
class AudioBuffer:
def __init__(self, blocksize=BLOCKSIZE):
self.blocksize = blocksize
self.buffer = np.array([], dtype=np.int16)
def add_frame(self, frame_data):
self.buffer = np.concatenate([self.buffer, frame_data])
def get_chunk(self):
if len(self.buffer) >= self.blocksize:
chunk = self.buffer[: self.blocksize]
self.buffer = self.buffer[self.blocksize :]
return chunk
return None
def get_padded_chunk(self):
if len(self.buffer) > 0:
chunk = np.zeros(self.blocksize, dtype=np.int16)
available = min(len(self.buffer), self.blocksize)
chunk[:available] = self.buffer[:available]
self.buffer = self.buffer[available:]
return chunk
return np.zeros(self.blocksize, dtype=np.int16)
async def audio_player(queue: asyncio.Queue):
"""Pull from the queue and stream audio using sounddevice."""
buffer = AudioBuffer(BLOCKSIZE)
def callback(outdata, frames, time, status):
if status:
print(f"Audio callback status: {status}")
# Try to fill buffer from queue
while not queue.empty():
try:
data = queue.get_nowait()
buffer.add_frame(data)
except asyncio.QueueEmpty:
break
# Get exactly the right amount of data
chunk = buffer.get_chunk()
if chunk is not None:
outdata[:] = chunk.reshape(-1, 1)
else:
# Not enough data, use what we have padded with zeros
outdata[:] = buffer.get_padded_chunk().reshape(-1, 1)
stream = sd.OutputStream(
samplerate=SAMPLERATE,
channels=CHANNELS,
blocksize=BLOCKSIZE,
dtype="int16",
callback=callback,
latency="low",
)
with stream:
while True:
await asyncio.sleep(0.1) # keep the loop alive
async def rtc_session(room, queue: asyncio.Queue):
track: rtc.RemoteAudioTrack | None = None
while not track:
for participant in room.remote_participants.values():
for t in participant.track_publications.values():
if t.kind == rtc.TrackKind.KIND_AUDIO and t.subscribed:
track = t.track
break
if track:
break
if not track:
print("waiting for audio track")
await asyncio.sleep(2)
stream = rtc.AudioStream.from_track(
track=track,
sample_rate=SAMPLERATE,
num_channels=1,
noise_cancellation=noise_cancellation.BVC(), # or NC()
)
print("playing stream")
try:
# Process audio frames from the stream
async for audio_frame_event in stream:
frame = audio_frame_event.frame
audio_data = np.frombuffer(frame.data, dtype=np.int16)
try:
await queue.put(audio_data)
except asyncio.QueueFull:
# Skip this frame if queue is full
print("Warning: Audio queue full, dropping frame")
continue
finally:
# Clean up the stream when done
await stream.aclose()
async def main():
queue = asyncio.Queue(maxsize=50)
player_task = asyncio.create_task(audio_player(queue))
token = (
api.AccessToken()
.with_identity("python-bot")
.with_name("Python Bot")
.with_grants(
api.VideoGrants(
room_join=True,
room="my-room",
agent=True,
)
)
.to_jwt()
)
url = os.getenv("LIVEKIT_URL")
room = rtc.Room()
await room.connect(
url,
token,
options=rtc.RoomOptions(
auto_subscribe=True,
),
)
print(f"Connected to room: {room.name}")
try:
await rtc_session(room, queue)
finally:
# Clean up
await room.disconnect()
player_task.cancel()
try:
await player_task
except asyncio.CancelledError:
pass
asyncio.run(main())