Skip to content

Commit b255b9d

Browse files
committed
publish sine wave example
1 parent 42b78af commit b255b9d

File tree

5 files changed

+155
-17
lines changed

5 files changed

+155
-17
lines changed

examples/publish_wave/room.py

Lines changed: 84 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,84 @@
1+
import livekit
2+
import logging
3+
import numpy as np
4+
import math
5+
import asyncio
6+
from signal import SIGINT, SIGTERM
7+
8+
URL = 'ws://localhost:7880'
9+
TOKEN = 'eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJleHAiOjE5MDY2MTMyODgsImlzcyI6IkFQSVRzRWZpZFpqclFvWSIsIm5hbWUiOiJuYXRpdmUiLCJuYmYiOjE2NzI2MTMyODgsInN1YiI6Im5hdGl2ZSIsInZpZGVvIjp7InJvb20iOiJ0ZXN0Iiwicm9vbUFkbWluIjp0cnVlLCJyb29tQ3JlYXRlIjp0cnVlLCJyb29tSm9pbiI6dHJ1ZSwicm9vbUxpc3QiOnRydWV9fQ.uSNIangMRu8jZD5mnRYoCHjcsQWCrJXgHCs0aNIgBFY'
10+
11+
12+
async def publish_frames(source: livekit.AudioSource):
13+
sample_rate = 48000
14+
frequency = 440
15+
amplitude = 32767 # for 16-bit audio
16+
num_channels = 1
17+
samples_per_channel = 480 # 10ms at 48kHz
18+
time = np.arange(samples_per_channel) / sample_rate
19+
total_samples = 0
20+
21+
audio_frame = livekit.AudioFrame.create(
22+
sample_rate, num_channels, samples_per_channel)
23+
24+
audio_data = np.ctypeslib.as_array(audio_frame.data)
25+
26+
while True:
27+
time = (total_samples + np.arange(samples_per_channel)) / sample_rate
28+
29+
sine_wave = (amplitude * np.sin(2 * np.pi *
30+
frequency * time)).astype(np.int16)
31+
np.copyto(audio_data, sine_wave)
32+
33+
source.capture_frame(audio_frame)
34+
35+
total_samples += samples_per_channel
36+
37+
try:
38+
await asyncio.sleep(1 / 100) # 10m
39+
except asyncio.CancelledError:
40+
break
41+
42+
43+
async def main():
44+
room = livekit.Room()
45+
46+
logging.info("connecting to %s", URL)
47+
try:
48+
await room.connect(URL, TOKEN)
49+
logging.info("connected to room %s", room.name)
50+
except livekit.ConnectError as e:
51+
logging.error("failed to connect to the room: %s", e)
52+
return False
53+
54+
# publish a track
55+
source = livekit.AudioSource()
56+
source_task = asyncio.create_task(publish_frames(source))
57+
58+
track = livekit.LocalAudioTrack.create_audio_track("sinewave", source)
59+
options = livekit.TrackPublishOptions()
60+
options.source = livekit.TrackSource.SOURCE_MICROPHONE
61+
publication = await room.local_participant.publish_track(track, options)
62+
logging.info("published track %s", publication.sid)
63+
64+
try:
65+
await room.run()
66+
except asyncio.CancelledError:
67+
logging.info("closing the room")
68+
source_task.cancel()
69+
await source_task
70+
await room.close()
71+
72+
73+
if __name__ == "__main__":
74+
logging.basicConfig(level=logging.INFO, handlers=[
75+
logging.FileHandler("publish_wave.log"), logging.StreamHandler()])
76+
77+
loop = asyncio.get_event_loop()
78+
main_task = asyncio.ensure_future(main())
79+
for signal in [SIGINT, SIGTERM]:
80+
loop.add_signal_handler(signal, main_task.cancel)
81+
try:
82+
loop.run_until_complete(main_task)
83+
finally:
84+
loop.close()

livekit/audio_frame.py

Lines changed: 37 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -1,14 +1,42 @@
11
import ctypes
2+
from ._ffi_client import (FfiClient, FfiHandle)
3+
from ._proto import ffi_pb2 as proto_ffi
4+
from ._proto import audio_frame_pb2 as proto_audio_frame
25

36

47
class AudioFrame():
5-
def __init__(self, sample_rate: int, num_channels: int, samples_per_channel: int, data=None, ffi_handle=None):
8+
def __init__(self, info: proto_audio_frame.AudioFrameBufferInfo, ffi_handle: FfiHandle):
9+
self._info = info
610
self._ffi_handle = ffi_handle
7-
self.sample_rate = sample_rate
8-
self.num_channels = num_channels
9-
self.samples_per_channel = samples_per_channel
10-
if data is None:
11-
self.data = (ctypes.c_int16 *
12-
(num_channels * samples_per_channel))()
13-
else:
14-
self.data = data
11+
12+
data_len = self.num_channels * self.samples_per_channel
13+
self.data = ctypes.cast(info.data_ptr,
14+
ctypes.POINTER(ctypes.c_int16 * data_len)).contents
15+
16+
@staticmethod
17+
def create(sample_rate: int, num_channels: int, samples_per_channel: int):
18+
# TODO(theomonnom): There should be no problem to directly send audio date from a Python created ctypes buffer
19+
req = proto_ffi.FfiRequest()
20+
req.alloc_audio_buffer.sample_rate = sample_rate
21+
req.alloc_audio_buffer.num_channels = num_channels
22+
req.alloc_audio_buffer.samples_per_channel = samples_per_channel
23+
24+
ffi_client = FfiClient()
25+
resp = ffi_client.request(req)
26+
27+
info = resp.alloc_audio_buffer.buffer
28+
ffi_handle = FfiHandle(info.handle.id)
29+
30+
return AudioFrame(info, ffi_handle)
31+
32+
@property
33+
def sample_rate(self) -> int:
34+
return self._info.sample_rate
35+
36+
@property
37+
def num_channels(self) -> int:
38+
return self._info.num_channels
39+
40+
@property
41+
def samples_per_channel(self) -> int:
42+
return self._info.samples_per_channel

livekit/audio_source.py

Lines changed: 21 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,24 @@
1+
from ._ffi_client import (FfiClient, FfiHandle)
2+
from ._proto import ffi_pb2 as proto_ffi
3+
from ._proto import audio_frame_pb2 as proto_audio_frame
4+
from livekit import (AudioFrame)
15

26

3-
class AudioSource():
7+
class AudioSource:
48
def __init__(self):
5-
pass
9+
req = proto_ffi.FfiRequest()
10+
req.new_audio_source.type = proto_audio_frame.AudioSourceType.AUDIO_SOURCE_NATIVE
11+
12+
ffi_client = FfiClient()
13+
resp = ffi_client.request(req)
14+
self._info = resp.new_audio_source.source
15+
self._ffi_handle = FfiHandle(self._info.handle.id)
16+
17+
def capture_frame(self, frame: AudioFrame):
18+
req = proto_ffi.FfiRequest()
19+
20+
req.capture_audio_frame.source_handle.id = self._ffi_handle.handle
21+
req.capture_audio_frame.buffer_handle.id = frame._ffi_handle.handle
22+
23+
ffi_client = FfiClient()
24+
ffi_client.request(req)

livekit/audio_stream.py

Lines changed: 1 addition & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -37,11 +37,7 @@ def _on_audio_stream_event(cls, event: proto_audio_frame.AudioStreamEvent):
3737
if which == 'frame_received':
3838
frame_info = event.frame_received.frame
3939
ffi_handle = FfiHandle(frame_info.handle.id)
40-
data_len = frame_info.num_channels * frame_info.samples_per_channel
41-
data = ctypes.cast(frame_info.data_ptr,
42-
ctypes.POINTER(ctypes.c_uint16 * data_len)).contents
43-
frame = AudioFrame(
44-
frame_info.sample_rate, frame_info.num_channels, frame_info.samples_per_channel, data, ffi_handle)
40+
frame = AudioFrame(frame_info, ffi_handle)
4541
stream._on_frame_received(frame)
4642

4743
def __init__(self, track: Track):

livekit/track.py

Lines changed: 12 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@
44
from typing import (Optional, TYPE_CHECKING)
55

66
if TYPE_CHECKING:
7-
from livekit import (VideoSource)
7+
from livekit import (VideoSource, AudioSource)
88

99

1010
class Track():
@@ -40,6 +40,17 @@ class LocalAudioTrack(Track):
4040
def __init__(self, ffi_handle: FfiHandle, info: proto_track.TrackInfo):
4141
super().__init__(ffi_handle, info)
4242

43+
def create_audio_track(name: str, source: 'AudioSource') -> 'LocalAudioTrack':
44+
req = proto_ffi.FfiRequest()
45+
req.create_audio_track.name = name
46+
req.create_audio_track.source_handle.id = source._ffi_handle.handle
47+
48+
ffi_client = FfiClient()
49+
resp = ffi_client.request(req)
50+
track_info = resp.create_audio_track.track
51+
ffi_handle = FfiHandle(track_info.handle.id)
52+
return LocalAudioTrack(ffi_handle, track_info)
53+
4354

4455
class LocalVideoTrack(Track):
4556
def __init__(self, ffi_handle: FfiHandle, info: proto_track.TrackInfo):

0 commit comments

Comments
 (0)