livekit 1.2.1__py3-none-win_arm64.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (71) hide show
  1. livekit/rtc/__init__.py +264 -0
  2. livekit/rtc/_ffi_client.py +321 -0
  3. livekit/rtc/_proto/__init__.py +0 -0
  4. livekit/rtc/_proto/audio_frame_pb2.py +159 -0
  5. livekit/rtc/_proto/audio_frame_pb2.pyi +1560 -0
  6. livekit/rtc/_proto/capture_pb2.py +70 -0
  7. livekit/rtc/_proto/capture_pb2.pyi +559 -0
  8. livekit/rtc/_proto/data_stream_pb2.py +171 -0
  9. livekit/rtc/_proto/data_stream_pb2.pyi +1498 -0
  10. livekit/rtc/_proto/data_track_pb2.py +120 -0
  11. livekit/rtc/_proto/data_track_pb2.pyi +1073 -0
  12. livekit/rtc/_proto/e2ee_pb2.py +81 -0
  13. livekit/rtc/_proto/e2ee_pb2.pyi +591 -0
  14. livekit/rtc/_proto/ffi_pb2.py +55 -0
  15. livekit/rtc/_proto/ffi_pb2.pyi +1246 -0
  16. livekit/rtc/_proto/handle_pb2.py +27 -0
  17. livekit/rtc/_proto/handle_pb2.pyi +51 -0
  18. livekit/rtc/_proto/participant_pb2.py +45 -0
  19. livekit/rtc/_proto/participant_pb2.pyi +313 -0
  20. livekit/rtc/_proto/room_pb2.py +311 -0
  21. livekit/rtc/_proto/room_pb2.pyi +3136 -0
  22. livekit/rtc/_proto/rpc_pb2.py +47 -0
  23. livekit/rtc/_proto/rpc_pb2.pyi +261 -0
  24. livekit/rtc/_proto/stats_pb2.py +123 -0
  25. livekit/rtc/_proto/stats_pb2.pyi +1514 -0
  26. livekit/rtc/_proto/track_pb2.py +76 -0
  27. livekit/rtc/_proto/track_pb2.pyi +544 -0
  28. livekit/rtc/_proto/track_publication_pb2.py +39 -0
  29. livekit/rtc/_proto/track_publication_pb2.pyi +145 -0
  30. livekit/rtc/_proto/video_frame_pb2.py +97 -0
  31. livekit/rtc/_proto/video_frame_pb2.pyi +838 -0
  32. livekit/rtc/_utils.py +187 -0
  33. livekit/rtc/apm.py +119 -0
  34. livekit/rtc/audio_filter.py +23 -0
  35. livekit/rtc/audio_frame.py +242 -0
  36. livekit/rtc/audio_mixer.py +247 -0
  37. livekit/rtc/audio_resampler.py +174 -0
  38. livekit/rtc/audio_source.py +191 -0
  39. livekit/rtc/audio_stream.py +354 -0
  40. livekit/rtc/data_stream.py +640 -0
  41. livekit/rtc/data_track.py +296 -0
  42. livekit/rtc/e2ee.py +320 -0
  43. livekit/rtc/encoded_video.py +127 -0
  44. livekit/rtc/event_emitter.py +215 -0
  45. livekit/rtc/frame_processor.py +37 -0
  46. livekit/rtc/jupyter.py +68 -0
  47. livekit/rtc/log.py +3 -0
  48. livekit/rtc/media_devices.py +596 -0
  49. livekit/rtc/participant.py +1171 -0
  50. livekit/rtc/platform_audio.py +426 -0
  51. livekit/rtc/py.typed +0 -0
  52. livekit/rtc/resources/LICENSE.md +4055 -0
  53. livekit/rtc/resources/__init__.py +1 -0
  54. livekit/rtc/resources/jupyter-html/index.html +101 -0
  55. livekit/rtc/resources/livekit_ffi.dll +0 -0
  56. livekit/rtc/resources/livekit_ffi.h +42 -0
  57. livekit/rtc/room.py +1304 -0
  58. livekit/rtc/rpc.py +244 -0
  59. livekit/rtc/synchronizer.py +202 -0
  60. livekit/rtc/track.py +264 -0
  61. livekit/rtc/track_publication.py +145 -0
  62. livekit/rtc/transcription.py +19 -0
  63. livekit/rtc/utils.py +138 -0
  64. livekit/rtc/version.py +1 -0
  65. livekit/rtc/video_frame.py +347 -0
  66. livekit/rtc/video_source.py +74 -0
  67. livekit/rtc/video_stream.py +191 -0
  68. livekit-1.2.1.dist-info/METADATA +80 -0
  69. livekit-1.2.1.dist-info/RECORD +71 -0
  70. livekit-1.2.1.dist-info/WHEEL +5 -0
  71. livekit-1.2.1.dist-info/top_level.txt +1 -0
livekit/rtc/track.py ADDED
@@ -0,0 +1,264 @@
1
+ # Copyright 2023 LiveKit, Inc.
2
+ #
3
+ # Licensed under the Apache License, Version 2.0 (the "License");
4
+ # you may not use this file except in compliance with the License.
5
+ # You may obtain a copy of the License at
6
+ #
7
+ # http://www.apache.org/licenses/LICENSE-2.0
8
+ #
9
+ # Unless required by applicable law or agreed to in writing, software
10
+ # distributed under the License is distributed on an "AS IS" BASIS,
11
+ # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12
+ # See the License for the specific language governing permissions and
13
+ # limitations under the License.
14
+
15
+ from __future__ import annotations
16
+
17
+ import weakref
18
+ from typing import TYPE_CHECKING, List, Optional, Union
19
+ from ._ffi_client import FfiHandle, FfiClient
20
+ from ._proto import ffi_pb2 as proto_ffi
21
+ from ._proto import track_pb2 as proto_track
22
+ from ._proto import stats_pb2 as proto_stats
23
+
24
+ if TYPE_CHECKING:
25
+ from .audio_source import AudioSource
26
+ from .audio_stream import AudioStream
27
+ from .room import Room
28
+ from .video_source import VideoSource
29
+ from .encoded_video import EncodedVideoSource
30
+ from .platform_audio import PlatformAudioSource
31
+
32
+
33
+ class Track:
34
+ def __init__(self, owned_info: proto_track.OwnedTrack):
35
+ self._info = owned_info.info
36
+ self._ffi_handle = FfiHandle(owned_info.handle.id)
37
+ self._room_ref: Optional[weakref.ref[Room]] = None
38
+ self._audio_streams: weakref.WeakSet[AudioStream] = weakref.WeakSet()
39
+
40
+ def _resolve_room(self) -> Optional[Room]:
41
+ return self._room_ref() if self._room_ref is not None else None
42
+
43
+ def _set_room(self, room: Optional[Room]) -> None:
44
+ old_room = self._resolve_room()
45
+ if old_room is None and room is None:
46
+ # Already roomless — nothing to detach and nothing to re-clear.
47
+ # Without this guard a second _set_room(None) (e.g. the unpublish /
48
+ # local_track_unpublished race calling it from both paths) would
49
+ # re-fire _on_*_cleared on every registered processor.
50
+ return
51
+ if old_room is not room:
52
+ if old_room is not None:
53
+ old_room.off("token_refreshed", self._on_room_token_refreshed)
54
+ if room is not None:
55
+ room.on("token_refreshed", self._on_room_token_refreshed)
56
+
57
+ self._room_ref = weakref.ref(room) if room is not None else None
58
+
59
+ for stream in self._audio_streams:
60
+ self._push_processor_metadata_to_stream(stream, room)
61
+
62
+ def _on_room_token_refreshed(self) -> None:
63
+ room = self._resolve_room()
64
+ if room is None or room._token is None or room._server_url is None:
65
+ return
66
+ for stream in self._audio_streams:
67
+ if not stream._processor:
68
+ continue
69
+ stream._processor._on_credentials_updated(token=room._token, url=room._server_url)
70
+
71
+ def _push_processor_metadata_to_stream(self, stream: AudioStream, room: Optional[Room]) -> None:
72
+ if not stream._processor:
73
+ return
74
+
75
+ if room is None:
76
+ # track left a room - clear processor's room context
77
+ stream._processor._on_stream_info_cleared()
78
+ stream._processor._on_credentials_cleared()
79
+ return
80
+
81
+ identity = ""
82
+ pub_sid = ""
83
+ track_sid = self.sid
84
+ if track_sid:
85
+ for participant in room.remote_participants.values():
86
+ publication = participant.track_publications.get(track_sid)
87
+ if publication is not None:
88
+ identity, pub_sid = participant.identity, publication.sid
89
+ break
90
+ else:
91
+ local = room._local_participant
92
+ if local is not None:
93
+ for local_publication in local.track_publications.values():
94
+ if local_publication.sid == track_sid:
95
+ identity, pub_sid = local.identity, local_publication.sid
96
+ break
97
+
98
+ stream._processor._on_stream_info_updated(
99
+ room_name=room.name,
100
+ participant_identity=identity,
101
+ publication_sid=pub_sid,
102
+ )
103
+ if room._token is not None and room._server_url is not None:
104
+ stream._processor._on_credentials_updated(token=room._token, url=room._server_url)
105
+
106
+ def _register_audio_stream(self, stream: AudioStream) -> None:
107
+ self._audio_streams.add(stream)
108
+ room = self._resolve_room()
109
+ if room is not None:
110
+ self._push_processor_metadata_to_stream(stream, room)
111
+
112
+ def _unregister_audio_stream(self, stream: AudioStream) -> None:
113
+ self._audio_streams.discard(stream)
114
+
115
+ @property
116
+ def sid(self) -> str:
117
+ return self._info.sid
118
+
119
+ @property
120
+ def name(self) -> str:
121
+ return self._info.name
122
+
123
+ @property
124
+ def kind(self) -> proto_track.TrackKind.ValueType:
125
+ return self._info.kind
126
+
127
+ @property
128
+ def stream_state(self) -> proto_track.StreamState.ValueType:
129
+ return self._info.stream_state
130
+
131
+ @property
132
+ def muted(self) -> bool:
133
+ return self._info.muted
134
+
135
+ async def get_stats(self) -> List[proto_stats.RtcStats]:
136
+ req = proto_ffi.FfiRequest()
137
+ req.get_stats.track_handle = self._ffi_handle.handle
138
+
139
+ queue = FfiClient.instance.queue.subscribe()
140
+ try:
141
+ resp = FfiClient.instance.request(req)
142
+ cb: proto_ffi.FfiEvent = await queue.wait_for(
143
+ lambda e: e.get_stats.async_id == resp.get_stats.async_id
144
+ )
145
+ finally:
146
+ FfiClient.instance.queue.unsubscribe(queue)
147
+
148
+ if cb.get_stats.error:
149
+ raise Exception(cb.get_stats.error)
150
+
151
+ return list(cb.get_stats.stats)
152
+
153
+
154
+ class LocalAudioTrack(Track):
155
+ def __init__(self, info: proto_track.OwnedTrack):
156
+ super().__init__(info)
157
+
158
+ @staticmethod
159
+ def create_audio_track(
160
+ name: str, source: Union["AudioSource", "PlatformAudioSource"]
161
+ ) -> "LocalAudioTrack":
162
+ """Create a local audio track from an audio source.
163
+
164
+ Args:
165
+ name: The name of the track (e.g., "microphone", "audio-file").
166
+ source: Either an AudioSource (synthetic mode for manual frame capture)
167
+ or a PlatformAudioSource (from PlatformAudio, uses WebRTC ADM).
168
+
169
+ Returns:
170
+ A LocalAudioTrack that can be published to a room.
171
+
172
+ Example with PlatformAudio (recommended for most use cases):
173
+ ```python
174
+ platform_audio = rtc.PlatformAudio()
175
+ source = platform_audio.create_audio_source()
176
+ track = rtc.LocalAudioTrack.create_audio_track("microphone", source)
177
+ ```
178
+
179
+ Example with AudioSource (synthetic mode for custom processing):
180
+ ```python
181
+ # Synthetic mode: You must manually capture frames
182
+ source = rtc.AudioSource(sample_rate=48000, num_channels=1)
183
+ track = rtc.LocalAudioTrack.create_audio_track("audio", source)
184
+ # Then in a loop: await source.capture_frame(frame)
185
+ ```
186
+ """
187
+ req = proto_ffi.FfiRequest()
188
+ req.create_audio_track.name = name
189
+ req.create_audio_track.source_handle = source._ffi_handle.handle
190
+
191
+ resp = FfiClient.instance.request(req)
192
+ return LocalAudioTrack(resp.create_audio_track.track)
193
+
194
+ def mute(self) -> None:
195
+ req = proto_ffi.FfiRequest()
196
+ req.local_track_mute.track_handle = self._ffi_handle.handle
197
+ req.local_track_mute.mute = True
198
+ FfiClient.instance.request(req)
199
+ self._info.muted = True
200
+
201
+ def unmute(self) -> None:
202
+ req = proto_ffi.FfiRequest()
203
+ req.local_track_mute.track_handle = self._ffi_handle.handle
204
+ req.local_track_mute.mute = False
205
+ FfiClient.instance.request(req)
206
+ self._info.muted = False
207
+
208
+ def __repr__(self) -> str:
209
+ return f"rtc.LocalAudioTrack(sid={self.sid}, name={self.name})"
210
+
211
+
212
+ class LocalVideoTrack(Track):
213
+ def __init__(self, info: proto_track.OwnedTrack):
214
+ super().__init__(info)
215
+
216
+ @staticmethod
217
+ def create_video_track(
218
+ name: str, source: "Union[VideoSource, EncodedVideoSource]"
219
+ ) -> "LocalVideoTrack":
220
+ req = proto_ffi.FfiRequest()
221
+ req.create_video_track.name = name
222
+ req.create_video_track.source_handle = source._ffi_handle.handle
223
+
224
+ resp = FfiClient.instance.request(req)
225
+ return LocalVideoTrack(resp.create_video_track.track)
226
+
227
+ def mute(self) -> None:
228
+ req = proto_ffi.FfiRequest()
229
+ req.local_track_mute.track_handle = self._ffi_handle.handle
230
+ req.local_track_mute.mute = True
231
+ FfiClient.instance.request(req)
232
+ self._info.muted = True
233
+
234
+ def unmute(self) -> None:
235
+ req = proto_ffi.FfiRequest()
236
+ req.local_track_mute.track_handle = self._ffi_handle.handle
237
+ req.local_track_mute.mute = False
238
+ FfiClient.instance.request(req)
239
+ self._info.muted = False
240
+
241
+ def __repr__(self) -> str:
242
+ return f"rtc.LocalVideoTrack(sid={self.sid}, name={self.name})"
243
+
244
+
245
+ class RemoteAudioTrack(Track):
246
+ def __init__(self, info: proto_track.OwnedTrack):
247
+ super().__init__(info)
248
+
249
+ def __repr__(self) -> str:
250
+ return f"rtc.RemoteAudioTrack(sid={self.sid}, name={self.name})"
251
+
252
+
253
+ class RemoteVideoTrack(Track):
254
+ def __init__(self, info: proto_track.OwnedTrack):
255
+ super().__init__(info)
256
+
257
+ def __repr__(self) -> str:
258
+ return f"rtc.RemoteVideoTrack(sid={self.sid}, name={self.name})"
259
+
260
+
261
+ LocalTrack = Union[LocalVideoTrack, LocalAudioTrack]
262
+ RemoteTrack = Union[RemoteVideoTrack, RemoteAudioTrack]
263
+ AudioTrack = Union[LocalAudioTrack, RemoteAudioTrack]
264
+ VideoTrack = Union[LocalVideoTrack, RemoteVideoTrack]
@@ -0,0 +1,145 @@
1
+ # Copyright 2023 LiveKit, Inc.
2
+ #
3
+ # Licensed under the Apache License, Version 2.0 (the "License");
4
+ # you may not use this file except in compliance with the License.
5
+ # You may obtain a copy of the License at
6
+ #
7
+ # http://www.apache.org/licenses/LICENSE-2.0
8
+ #
9
+ # Unless required by applicable law or agreed to in writing, software
10
+ # distributed under the License is distributed on an "AS IS" BASIS,
11
+ # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12
+ # See the License for the specific language governing permissions and
13
+ # limitations under the License.
14
+
15
+ from typing import List, Optional, cast
16
+ import asyncio
17
+
18
+ from ._ffi_client import FfiHandle, FfiClient
19
+ from ._proto import e2ee_pb2 as proto_e2ee
20
+ from ._proto import ffi_pb2 as proto_ffi
21
+ from ._proto import track_pb2 as proto_track
22
+ from ._proto import track_publication_pb2 as proto_track_pub
23
+ from .track import Track, LocalTrack, RemoteTrack
24
+
25
+ VideoQuality = proto_track_pub.VideoQuality
26
+
27
+
28
+ class TrackPublication:
29
+ def __init__(self, owned_info: proto_track.OwnedTrackPublication):
30
+ self._info = owned_info.info
31
+ self._track: Optional[Track] = None
32
+ self._ffi_handle = FfiHandle(owned_info.handle.id)
33
+
34
+ @property
35
+ def track(self) -> Optional[Track]:
36
+ return self._track
37
+
38
+ @property
39
+ def sid(self) -> str:
40
+ return self._info.sid
41
+
42
+ @property
43
+ def name(self) -> str:
44
+ return self._info.name
45
+
46
+ @property
47
+ def kind(self) -> proto_track.TrackKind.ValueType:
48
+ return self._info.kind
49
+
50
+ @property
51
+ def source(self) -> proto_track.TrackSource.ValueType:
52
+ return self._info.source
53
+
54
+ @property
55
+ def simulcasted(self) -> bool:
56
+ return self._info.simulcasted
57
+
58
+ @property
59
+ def width(self) -> int:
60
+ return self._info.width
61
+
62
+ @property
63
+ def height(self) -> int:
64
+ return self._info.height
65
+
66
+ @property
67
+ def mime_type(self) -> str:
68
+ return self._info.mime_type
69
+
70
+ @property
71
+ def muted(self) -> bool:
72
+ return self._info.muted
73
+
74
+ @property
75
+ def encryption_type(self) -> proto_e2ee.EncryptionType.ValueType:
76
+ return self._info.encryption_type
77
+
78
+ @property
79
+ def audio_features(self) -> List[proto_track.AudioTrackFeature.ValueType]:
80
+ return list(self._info.audio_features)
81
+
82
+ @property
83
+ def frame_metadata_features(self) -> List[proto_track.FrameMetadataFeature.ValueType]:
84
+ return list(self._info.frame_metadata_features)
85
+
86
+
87
+ class LocalTrackPublication(TrackPublication):
88
+ def __init__(self, owned_info: proto_track.OwnedTrackPublication):
89
+ super().__init__(owned_info)
90
+ self._first_subscription: asyncio.Future[None] = asyncio.Future()
91
+
92
+ @property
93
+ def track(self) -> Optional[LocalTrack]:
94
+ return cast(Optional[LocalTrack], self._track)
95
+
96
+ async def wait_for_subscription(self) -> None:
97
+ await asyncio.shield(self._first_subscription)
98
+
99
+ def __repr__(self) -> str:
100
+ return f"rtc.LocalTrackPublication(sid={self.sid}, name={self.name}, kind={self.kind}, source={self.source})"
101
+
102
+
103
+ class RemoteTrackPublication(TrackPublication):
104
+ def __init__(self, owned_info: proto_track.OwnedTrackPublication):
105
+ super().__init__(owned_info)
106
+ self._subscribed = False
107
+
108
+ @property
109
+ def track(self) -> Optional[RemoteTrack]:
110
+ return cast(Optional[RemoteTrack], self._track)
111
+
112
+ @property
113
+ def subscribed(self) -> bool:
114
+ return self._subscribed
115
+
116
+ def set_subscribed(self, subscribed: bool) -> None:
117
+ req = proto_ffi.FfiRequest()
118
+ req.set_subscribed.subscribe = subscribed
119
+ req.set_subscribed.publication_handle = self._ffi_handle.handle
120
+ FfiClient.instance.request(req)
121
+
122
+ def set_video_quality(self, quality: "VideoQuality.ValueType") -> None:
123
+ """For simulcasted video tracks, request a specific simulcast layer
124
+ from the server. Use one of `rtc.VideoQuality.VIDEO_QUALITY_LOW` (q),
125
+ `VIDEO_QUALITY_MEDIUM` (h), or `VIDEO_QUALITY_HIGH` (f).
126
+
127
+ Raises:
128
+ ValueError: if the publication is not a video track, or is not
129
+ simulcasted (selecting a layer requires multiple layers).
130
+ """
131
+ if self.kind != proto_track.TrackKind.KIND_VIDEO:
132
+ raise ValueError(
133
+ f"set_video_quality is only supported on video publications (sid={self.sid})"
134
+ )
135
+ if not self.simulcasted:
136
+ raise ValueError(
137
+ f"set_video_quality requires a simulcasted publication (sid={self.sid})"
138
+ )
139
+ req = proto_ffi.FfiRequest()
140
+ req.set_remote_track_publication_quality.track_publication_handle = self._ffi_handle.handle
141
+ req.set_remote_track_publication_quality.quality = quality
142
+ FfiClient.instance.request(req)
143
+
144
+ def __repr__(self) -> str:
145
+ return f"rtc.RemoteTrackPublication(sid={self.sid}, name={self.name}, kind={self.kind}, source={self.source})"
@@ -0,0 +1,19 @@
1
+ from typing import List
2
+ from dataclasses import dataclass
3
+
4
+
5
+ @dataclass
6
+ class Transcription:
7
+ participant_identity: str
8
+ track_sid: str
9
+ segments: List["TranscriptionSegment"]
10
+
11
+
12
+ @dataclass
13
+ class TranscriptionSegment:
14
+ id: str
15
+ text: str
16
+ start_time: int
17
+ end_time: int
18
+ language: str
19
+ final: bool
livekit/rtc/utils.py ADDED
@@ -0,0 +1,138 @@
1
+ from __future__ import annotations
2
+
3
+ from typing import AsyncIterator
4
+
5
+ from .audio_frame import AudioFrame
6
+
7
+
8
+ __all__ = ["combine_audio_frames", "sine_wave_generator"]
9
+
10
+
11
+ def combine_audio_frames(buffer: AudioFrame | list[AudioFrame]) -> AudioFrame:
12
+ """
13
+ Combines one or more `rtc.AudioFrame` objects into a single `rtc.AudioFrame`.
14
+
15
+ This function concatenates the audio data from multiple frames, ensuring that
16
+ all frames have the same sample rate and number of channels. It efficiently
17
+ merges the data by preallocating the necessary memory and copying the frame
18
+ data without unnecessary reallocations.
19
+
20
+ Args:
21
+ buffer: A single `rtc.AudioFrame` or a list of `rtc.AudioFrame`
22
+ objects to be combined.
23
+
24
+ Returns:
25
+ rtc.AudioFrame: A new `rtc.AudioFrame` containing the combined audio data.
26
+
27
+ Raises:
28
+ ValueError: If the buffer is empty.
29
+ ValueError: If frames have differing sample rates.
30
+ ValueError: If frames have differing numbers of channels.
31
+
32
+ Example:
33
+ >>> frame1 = rtc.AudioFrame(
34
+ ... data=b"\x01\x02", sample_rate=48000, num_channels=2, samples_per_channel=1
35
+ ... )
36
+ >>> frame2 = rtc.AudioFrame(
37
+ ... data=b"\x03\x04", sample_rate=48000, num_channels=2, samples_per_channel=1
38
+ ... )
39
+ >>> combined_frame = combine_audio_frames([frame1, frame2])
40
+ >>> combined_frame.data
41
+ b'\x01\x02\x03\x04'
42
+ >>> combined_frame.sample_rate
43
+ 48000
44
+ >>> combined_frame.num_channels
45
+ 2
46
+ >>> combined_frame.samples_per_channel
47
+ 2
48
+ """
49
+ if not isinstance(buffer, list):
50
+ return buffer
51
+
52
+ if not buffer:
53
+ raise ValueError("buffer is empty")
54
+
55
+ sample_rate = buffer[0].sample_rate
56
+ num_channels = buffer[0].num_channels
57
+
58
+ total_data_length = 0
59
+ total_samples_per_channel = 0
60
+
61
+ for frame in buffer:
62
+ if frame.sample_rate != sample_rate:
63
+ raise ValueError(
64
+ f"Sample rate mismatch: expected {sample_rate}, got {frame.sample_rate}"
65
+ )
66
+
67
+ if frame.num_channels != num_channels:
68
+ raise ValueError(
69
+ f"Channel count mismatch: expected {num_channels}, got {frame.num_channels}"
70
+ )
71
+
72
+ total_data_length += len(frame.data)
73
+ total_samples_per_channel += frame.samples_per_channel
74
+
75
+ data = bytearray(total_data_length)
76
+ offset = 0
77
+ for frame in buffer:
78
+ frame_data = frame.data.cast("b")
79
+ data[offset : offset + len(frame_data)] = frame_data
80
+ offset += len(frame_data)
81
+
82
+ return AudioFrame(
83
+ data=data,
84
+ sample_rate=sample_rate,
85
+ num_channels=num_channels,
86
+ samples_per_channel=total_samples_per_channel,
87
+ )
88
+
89
+
90
+ async def sine_wave_generator(
91
+ freq: float,
92
+ duration: float,
93
+ sample_rate: int = 48000,
94
+ amplitude: float = 0.3,
95
+ ) -> AsyncIterator[AudioFrame]:
96
+ """
97
+ Generate sine wave audio frames.
98
+
99
+ Useful for testing audio pipelines and generating test signals.
100
+
101
+ Args:
102
+ freq: Frequency of the sine wave in Hz.
103
+ duration: Duration of the audio in seconds.
104
+ sample_rate: Sample rate in Hz (default: 48000).
105
+ amplitude: Amplitude of the sine wave, range [0.0, 1.0] (default: 0.3).
106
+
107
+ Yields:
108
+ AudioFrame: Audio frames containing sine wave data.
109
+
110
+ Example:
111
+ >>> import asyncio
112
+ >>> async def generate_audio():
113
+ ... async for frame in sine_wave_generator(440, 1.0):
114
+ ... print(f"Generated frame with {frame.samples_per_channel} samples")
115
+ >>> asyncio.run(generate_audio())
116
+ """
117
+ try:
118
+ import numpy as np
119
+ except ImportError:
120
+ raise ImportError(
121
+ "numpy is required for sine_wave_generator. Install it with: pip install numpy"
122
+ )
123
+
124
+ blocksize = sample_rate // 10
125
+ total_frames = int((duration * sample_rate) // blocksize)
126
+ t_frame = np.arange(blocksize) / sample_rate
127
+
128
+ for i in range(total_frames):
129
+ t = t_frame + i * blocksize / sample_rate
130
+ signal = amplitude * np.sin(2 * np.pi * freq * t)
131
+ signal_int16 = np.int16(signal * 32767)
132
+ frame = AudioFrame(
133
+ signal_int16.tobytes(),
134
+ sample_rate,
135
+ 1,
136
+ blocksize,
137
+ )
138
+ yield frame
livekit/rtc/version.py ADDED
@@ -0,0 +1 @@
1
+ __version__ = "1.2.1"