From 89b16177aaaf0b07fbc6f611abbc15c03e16fded Mon Sep 17 00:00:00 2001 From: Stephen DeRosa Date: Wed, 27 May 2026 11:53:52 -0600 Subject: [PATCH 1/3] PlatformAudio --- CMakeLists.txt | 1 + README.md | 29 ++++ include/livekit/livekit.h | 3 +- include/livekit/local_audio_track.h | 12 ++ include/livekit/platform_audio.h | 141 +++++++++++++++++++ src/local_audio_track.cpp | 41 +++++- src/platform_audio.cpp | 186 +++++++++++++++++++++++++ src/tests/unit/test_platform_audio.cpp | 69 +++++++++ 8 files changed, 474 insertions(+), 8 deletions(-) create mode 100644 include/livekit/platform_audio.h create mode 100644 src/platform_audio.cpp create mode 100644 src/tests/unit/test_platform_audio.cpp diff --git a/CMakeLists.txt b/CMakeLists.txt index 895284b1..9ea98bb2 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -373,6 +373,7 @@ add_library(livekit SHARED src/logging.cpp src/local_audio_track.cpp src/local_data_track.cpp + src/platform_audio.cpp src/remote_audio_track.cpp src/remote_data_track.cpp src/room.cpp diff --git a/README.md b/README.md index ec266b2f..15a0895e 100644 --- a/README.md +++ b/README.md @@ -195,6 +195,35 @@ If the E2EE keys do not match between participants: Press Ctrl-C to exit the example. +## Platform Audio + +For normal microphone publishing, use `PlatformAudio`. It uses WebRTC's platform +Audio Device Module for microphone capture and speaker playout, with built-in +echo cancellation, noise suppression, and automatic gain control. Use +`AudioSource` instead when you need to push raw PCM frames yourself, such as TTS, +file playback, synthesis, or custom processing. + +```cpp +livekit::PlatformAudio platform_audio; + +// Optional: choose devices by stable AudioDeviceInfo::id. +for (const auto& device : platform_audio.recordingDevices()) { + // device.name, device.id +} + +livekit::PlatformAudioOptions audio_options; +audio_options.echo_cancellation = true; +audio_options.noise_suppression = true; +audio_options.auto_gain_control = true; + +auto source = platform_audio.createAudioSource(audio_options); +auto track = livekit::LocalAudioTrack::createLocalAudioTrack("microphone", source); + +livekit::TrackPublishOptions publish_options; +publish_options.source = livekit::TrackSource::SOURCE_MICROPHONE; +room->localParticipant()->publishTrack(track, publish_options); +``` + ### SimpleRpc The SimpleRpc example demonstrates how to: - Connect multiple participants to the same LiveKit room diff --git a/include/livekit/livekit.h b/include/livekit/livekit.h index f7906c42..4790d2c4 100644 --- a/include/livekit/livekit.h +++ b/include/livekit/livekit.h @@ -28,6 +28,7 @@ #include "livekit/local_video_track.h" #include "livekit/logging.h" #include "livekit/participant.h" +#include "livekit/platform_audio.h" #include "livekit/remote_participant.h" #include "livekit/remote_track_publication.h" #include "livekit/room.h" @@ -67,4 +68,4 @@ LIVEKIT_API bool initialize(const LogLevel& level = LogLevel::Info, const LogSin /// After shutdown, you may call initialize() again. LIVEKIT_API void shutdown(); -} // namespace livekit \ No newline at end of file +} // namespace livekit diff --git a/include/livekit/local_audio_track.h b/include/livekit/local_audio_track.h index 827ba4e7..116c7856 100644 --- a/include/livekit/local_audio_track.h +++ b/include/livekit/local_audio_track.h @@ -31,6 +31,7 @@ class OwnedTrack; } class AudioSource; +class PlatformAudioSource; /// Represents a user-provided audio track sourced from the local device. /// @@ -64,6 +65,17 @@ class LIVEKIT_API LocalAudioTrack : public Track { static std::shared_ptr createLocalAudioTrack(const std::string& name, const std::shared_ptr& source); + /// Creates a new local audio track backed by the given `PlatformAudioSource`. + /// + /// @param name Human-readable name for the track. This may appear to + /// remote participants and in analytics/debug logs. + /// @param source The platform source that captures microphone audio + /// automatically through WebRTC's Audio Device Module. + /// + /// @return A shared pointer to the newly constructed `LocalAudioTrack`. + static std::shared_ptr createLocalAudioTrack(const std::string& name, + const std::shared_ptr& source); + /// Mutes the audio track. /// /// A muted track stops sending audio to the room, but the track remains diff --git a/include/livekit/platform_audio.h b/include/livekit/platform_audio.h new file mode 100644 index 00000000..74c5f3b9 --- /dev/null +++ b/include/livekit/platform_audio.h @@ -0,0 +1,141 @@ +/* + * Copyright 2026 LiveKit + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#pragma once + +#include +#include +#include +#include +#include + +#include "livekit/ffi_handle.h" +#include "livekit/visibility.h" + +namespace livekit { + +struct PlatformAudioState; + +/** + * Information about a platform audio device. + */ +struct AudioDeviceInfo { + /** Current device index. Indices may change when devices are added or removed. */ + std::uint32_t index = 0; + + /** Device name reported by the operating system. */ + std::string name; + + /** Platform-specific stable device identifier. Prefer this over index for selection. */ + std::string id; +}; + +/** + * Audio processing options for platform microphone capture. + */ +struct PlatformAudioOptions { + /** Enable acoustic echo cancellation. */ + bool echo_cancellation = true; + + /** Enable background noise suppression. */ + bool noise_suppression = true; + + /** Enable automatic gain control. */ + bool auto_gain_control = true; + + /** Prefer hardware audio processing when the platform provides it. */ + bool prefer_hardware = false; +}; + +/** + * Error raised when platform audio setup or device operations fail. + */ +class LIVEKIT_API PlatformAudioError : public std::runtime_error { +public: + explicit PlatformAudioError(const std::string& message) : std::runtime_error(message) {} +}; + +/** + * Audio source backed by WebRTC's platform Audio Device Module. + * + * A PlatformAudioSource captures microphone audio automatically. Unlike + * AudioSource, callers do not push frames with captureFrame(). + */ +class LIVEKIT_API PlatformAudioSource { +public: + ~PlatformAudioSource() = default; + + PlatformAudioSource(const PlatformAudioSource&) = delete; + PlatformAudioSource& operator=(const PlatformAudioSource&) = delete; + PlatformAudioSource(PlatformAudioSource&&) noexcept = default; + PlatformAudioSource& operator=(PlatformAudioSource&&) noexcept = default; + + /// Underlying FFI handle ID used in FFI requests. + std::uint64_t ffiHandleId() const noexcept { return static_cast(handle_.get()); } + +private: + friend class PlatformAudio; + + PlatformAudioSource(FfiHandle handle, std::shared_ptr platform_audio) noexcept; + + FfiHandle handle_; + std::shared_ptr platform_audio_; +}; + +/** + * Platform audio device manager backed by WebRTC's Audio Device Module. + * + * Use PlatformAudio for normal microphone publishing when built-in echo + * cancellation, noise suppression, automatic gain control, and speaker playout + * are desired. Use AudioSource instead when the application needs direct access + * to raw PCM frames or custom audio generation. + */ +class LIVEKIT_API PlatformAudio { +public: + PlatformAudio(); + ~PlatformAudio() = default; + + PlatformAudio(const PlatformAudio&) = default; + PlatformAudio& operator=(const PlatformAudio&) = default; + PlatformAudio(PlatformAudio&&) noexcept = default; + PlatformAudio& operator=(PlatformAudio&&) noexcept = default; + + /// Number of recording devices reported when this instance was created. + std::int32_t recordingDeviceCount() const noexcept; + + /// Number of playout devices reported when this instance was created. + std::int32_t playoutDeviceCount() const noexcept; + + /// Enumerate available microphones. + std::vector recordingDevices() const; + + /// Enumerate available speakers/headphones. + std::vector playoutDevices() const; + + /// Select the microphone by AudioDeviceInfo::id. + void setRecordingDevice(const std::string& device_id) const; + + /// Select the speaker/headphones by AudioDeviceInfo::id. + void setPlayoutDevice(const std::string& device_id) const; + + /// Create an automatically captured microphone source for LocalAudioTrack. + std::shared_ptr createAudioSource(const PlatformAudioOptions& options = {}) const; + +private: + std::shared_ptr state_; +}; + +} // namespace livekit diff --git a/src/local_audio_track.cpp b/src/local_audio_track.cpp index 489cf6a3..7eaf8b10 100644 --- a/src/local_audio_track.cpp +++ b/src/local_audio_track.cpp @@ -16,27 +16,54 @@ #include "livekit/local_audio_track.h" +#include +#include + #include "ffi.pb.h" #include "ffi_client.h" #include "livekit/audio_source.h" +#include "livekit/platform_audio.h" #include "track.pb.h" #include "track_proto_converter.h" namespace livekit { +namespace { + +proto::OwnedTrack createAudioTrackWithSourceHandle(const std::string& name, std::uint64_t source_handle) { + proto::FfiRequest req; + auto* msg = req.mutable_create_audio_track(); + msg->set_name(name); + msg->set_source_handle(source_handle); + + const proto::FfiResponse resp = FfiClient::instance().sendRequest(req); + return resp.create_audio_track().track(); +} + +} // namespace + LocalAudioTrack::LocalAudioTrack(FfiHandle handle, const proto::OwnedTrack& track) : Track(std::move(handle), track.info().sid(), track.info().name(), fromProto(track.info().kind()), fromProto(track.info().stream_state()), track.info().muted(), false) {} std::shared_ptr LocalAudioTrack::createLocalAudioTrack(const std::string& name, const std::shared_ptr& source) { - proto::FfiRequest req; - auto* msg = req.mutable_create_audio_track(); - msg->set_name(name); - msg->set_source_handle(static_cast(source->ffiHandleId())); + if (!source) { + throw std::invalid_argument("LocalAudioTrack::createLocalAudioTrack: source is null"); + } - const proto::FfiResponse resp = FfiClient::instance().sendRequest(req); - const proto::OwnedTrack& owned = resp.create_audio_track().track(); + const proto::OwnedTrack owned = createAudioTrackWithSourceHandle(name, source->ffiHandleId()); + FfiHandle handle(static_cast(owned.handle().id())); + return std::shared_ptr(new LocalAudioTrack(std::move(handle), owned)); +} + +std::shared_ptr LocalAudioTrack::createLocalAudioTrack( + const std::string& name, const std::shared_ptr& source) { + if (!source) { + throw std::invalid_argument("LocalAudioTrack::createLocalAudioTrack: source is null"); + } + + const proto::OwnedTrack owned = createAudioTrackWithSourceHandle(name, source->ffiHandleId()); FfiHandle handle(static_cast(owned.handle().id())); return std::shared_ptr(new LocalAudioTrack(std::move(handle), owned)); } @@ -75,4 +102,4 @@ std::string LocalAudioTrack::toString() const { return "rtc.LocalAudioTrack(sid= std::string LocalAudioTrack::to_string() const { return toString(); } -} // namespace livekit \ No newline at end of file +} // namespace livekit diff --git a/src/platform_audio.cpp b/src/platform_audio.cpp new file mode 100644 index 00000000..10673cb0 --- /dev/null +++ b/src/platform_audio.cpp @@ -0,0 +1,186 @@ +/* + * Copyright 2026 LiveKit + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#include "livekit/platform_audio.h" + +#include + +#include "audio_frame.pb.h" +#include "ffi.pb.h" +#include "ffi_client.h" + +namespace livekit { + +struct PlatformAudioState { + FfiHandle handle; + std::int32_t recording_device_count = 0; + std::int32_t playout_device_count = 0; +}; + +namespace { + +std::uint64_t requireHandle(const std::shared_ptr& state) { + if (!state || !state->handle) { + throw PlatformAudioError("PlatformAudio has no valid FFI handle"); + } + return static_cast(state->handle.get()); +} + +AudioDeviceInfo fromProto(const proto::AudioDeviceInfo& device) { + AudioDeviceInfo out; + out.index = device.index(); + out.name = device.name(); + out.id = device.has_guid() ? device.guid() : std::string(); + return out; +} + +std::vector convertDevices(const google::protobuf::RepeatedPtrField& devices) { + std::vector out; + out.reserve(static_cast(devices.size())); + for (const auto& device : devices) { + out.push_back(fromProto(device)); + } + return out; +} + +proto::AudioSourceOptions toProto(const PlatformAudioOptions& options) { + proto::AudioSourceOptions out; + out.set_echo_cancellation(options.echo_cancellation); + out.set_noise_suppression(options.noise_suppression); + out.set_auto_gain_control(options.auto_gain_control); + out.set_prefer_hardware(options.prefer_hardware); + return out; +} + +} // namespace + +PlatformAudioSource::PlatformAudioSource(FfiHandle handle, std::shared_ptr platform_audio) noexcept + : handle_(std::move(handle)), platform_audio_(std::move(platform_audio)) {} + +PlatformAudio::PlatformAudio() { + proto::FfiRequest req; + req.mutable_new_platform_audio(); + + const proto::FfiResponse resp = FfiClient::instance().sendRequest(req); + if (!resp.has_new_platform_audio()) { + throw PlatformAudioError("FfiResponse missing new_platform_audio"); + } + + const auto& platform_audio = resp.new_platform_audio(); + if (platform_audio.has_error()) { + throw PlatformAudioError(platform_audio.error()); + } + if (!platform_audio.has_platform_audio()) { + throw PlatformAudioError("NewPlatformAudioResponse missing platform_audio"); + } + + const auto& owned = platform_audio.platform_audio(); + state_ = std::make_shared(); + state_->handle = FfiHandle(static_cast(owned.handle().id())); + state_->recording_device_count = owned.info().recording_device_count(); + state_->playout_device_count = owned.info().playout_device_count(); +} + +std::int32_t PlatformAudio::recordingDeviceCount() const noexcept { + return state_ ? state_->recording_device_count : 0; +} + +std::int32_t PlatformAudio::playoutDeviceCount() const noexcept { return state_ ? state_->playout_device_count : 0; } + +std::vector PlatformAudio::recordingDevices() const { + proto::FfiRequest req; + req.mutable_get_audio_devices()->set_platform_audio_handle(requireHandle(state_)); + + const proto::FfiResponse resp = FfiClient::instance().sendRequest(req); + if (!resp.has_get_audio_devices()) { + throw PlatformAudioError("FfiResponse missing get_audio_devices"); + } + + const auto& devices = resp.get_audio_devices(); + if (devices.has_error() && !devices.error().empty()) { + throw PlatformAudioError(devices.error()); + } + return convertDevices(devices.recording_devices()); +} + +std::vector PlatformAudio::playoutDevices() const { + proto::FfiRequest req; + req.mutable_get_audio_devices()->set_platform_audio_handle(requireHandle(state_)); + + const proto::FfiResponse resp = FfiClient::instance().sendRequest(req); + if (!resp.has_get_audio_devices()) { + throw PlatformAudioError("FfiResponse missing get_audio_devices"); + } + + const auto& devices = resp.get_audio_devices(); + if (devices.has_error() && !devices.error().empty()) { + throw PlatformAudioError(devices.error()); + } + return convertDevices(devices.playout_devices()); +} + +void PlatformAudio::setRecordingDevice(const std::string& device_id) const { + proto::FfiRequest req; + auto* msg = req.mutable_set_recording_device(); + msg->set_platform_audio_handle(requireHandle(state_)); + msg->set_device_id(device_id); + + const proto::FfiResponse resp = FfiClient::instance().sendRequest(req); + if (!resp.has_set_recording_device()) { + throw PlatformAudioError("FfiResponse missing set_recording_device"); + } + + const auto& set_device = resp.set_recording_device(); + if (set_device.has_error() && !set_device.error().empty()) { + throw PlatformAudioError(set_device.error()); + } +} + +void PlatformAudio::setPlayoutDevice(const std::string& device_id) const { + proto::FfiRequest req; + auto* msg = req.mutable_set_playout_device(); + msg->set_platform_audio_handle(requireHandle(state_)); + msg->set_device_id(device_id); + + const proto::FfiResponse resp = FfiClient::instance().sendRequest(req); + if (!resp.has_set_playout_device()) { + throw PlatformAudioError("FfiResponse missing set_playout_device"); + } + + const auto& set_device = resp.set_playout_device(); + if (set_device.has_error() && !set_device.error().empty()) { + throw PlatformAudioError(set_device.error()); + } +} + +std::shared_ptr PlatformAudio::createAudioSource(const PlatformAudioOptions& options) const { + proto::FfiRequest req; + auto* msg = req.mutable_new_audio_source(); + msg->set_type(proto::AudioSourceType::AUDIO_SOURCE_PLATFORM); + msg->set_platform_audio_handle(requireHandle(state_)); + *msg->mutable_options() = toProto(options); + + const proto::FfiResponse resp = FfiClient::instance().sendRequest(req); + if (!resp.has_new_audio_source()) { + throw PlatformAudioError("FfiResponse missing new_audio_source"); + } + + const auto& source = resp.new_audio_source().source(); + FfiHandle handle(static_cast(source.handle().id())); + return std::shared_ptr(new PlatformAudioSource(std::move(handle), state_)); +} + +} // namespace livekit diff --git a/src/tests/unit/test_platform_audio.cpp b/src/tests/unit/test_platform_audio.cpp new file mode 100644 index 00000000..743d09a8 --- /dev/null +++ b/src/tests/unit/test_platform_audio.cpp @@ -0,0 +1,69 @@ +/* + * Copyright 2026 LiveKit + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#include +#include +#include +#include + +#include + +namespace livekit::test { + +class PlatformAudioTest : public ::testing::Test { +protected: + void SetUp() override { livekit::initialize(livekit::LogLevel::Info, livekit::LogSink::kConsole); } + void TearDown() override { livekit::shutdown(); } +}; + +TEST_F(PlatformAudioTest, DefaultOptionsEnableVoiceProcessing) { + PlatformAudioOptions options; + EXPECT_TRUE(options.echo_cancellation); + EXPECT_TRUE(options.noise_suppression); + EXPECT_TRUE(options.auto_gain_control); + EXPECT_FALSE(options.prefer_hardware); +} + +TEST_F(PlatformAudioTest, DeviceInfoStoresStableId) { + AudioDeviceInfo device; + device.index = 1; + device.name = "Microphone"; + device.id = "device-guid"; + + EXPECT_EQ(device.index, 1u); + EXPECT_EQ(device.name, "Microphone"); + EXPECT_EQ(device.id, "device-guid"); +} + +TEST_F(PlatformAudioTest, CreateSourceAndTrackWhenAvailable) { + std::unique_ptr platform_audio; + try { + platform_audio = std::make_unique(); + } catch (const PlatformAudioError& error) { + GTEST_SKIP() << "PlatformAudio unavailable: " << error.what(); + } + + const auto source = platform_audio->createAudioSource(); + ASSERT_NE(source, nullptr); + EXPECT_NE(source->ffiHandleId(), 0u); + + const auto track = LocalAudioTrack::createLocalAudioTrack("platform-mic", source); + ASSERT_NE(track, nullptr); + EXPECT_EQ(track->name(), "platform-mic"); + EXPECT_EQ(track->kind(), TrackKind::KIND_AUDIO); +} + +} // namespace livekit::test From 2d3ccdf636c566a48618349c3d1aef5962f6c223 Mon Sep 17 00:00:00 2001 From: Stephen DeRosa Date: Wed, 27 May 2026 12:00:07 -0600 Subject: [PATCH 2/3] update commetns --- AGENTS.md | 1 + include/livekit/local_audio_track.h | 65 ++++---- include/livekit/platform_audio.h | 226 ++++++++++++++++++++++------ 3 files changed, 220 insertions(+), 72 deletions(-) diff --git a/AGENTS.md b/AGENTS.md index 636f7e1e..94a8c9bd 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -60,6 +60,7 @@ All `RoomDelegate` callbacks and stream handler callbacks (e.g., `registerTextSt | `SubscriptionThreadDispatcher` | Yes | Internal `std::mutex` protects registrations and active readers. Thread joins happen outside the lock. | | `AudioStream` / `VideoStream` / `DataTrackStream` | Yes | Internal `std::mutex` + `condition_variable` coordinate the FFI producer thread and the consumer reader thread. | | `AudioSource::captureFrame` | No | Not safe to call concurrently from multiple threads. | +| `PlatformAudio` / `PlatformAudioSource` | Yes | Thin `sendRequest` wrappers over immutable FFI handle state; destruction and move operations must be externally synchronized. | | `VideoSource::captureFrame` | No | Not safe to call concurrently from multiple threads. | | `LocalAudioTrack` / `LocalVideoTrack` | No | Thin `sendRequest` wrappers with no internal synchronization. | | `LocalDataTrack::tryPush` | No | Thin `sendRequest` wrapper with no internal synchronization. | diff --git a/include/livekit/local_audio_track.h b/include/livekit/local_audio_track.h index 116c7856..26640e03 100644 --- a/include/livekit/local_audio_track.h +++ b/include/livekit/local_audio_track.h @@ -35,33 +35,27 @@ class PlatformAudioSource; /// Represents a user-provided audio track sourced from the local device. /// -/// `LocalAudioTrack` is used to publish microphone audio (or any custom -/// audio source) to a LiveKit room. It wraps a platform-specific audio -/// source and exposes simple controls such as `mute()` and `unmute()`. +/// `LocalAudioTrack` is used to publish microphone audio or any custom audio +/// source to a LiveKit room. It wraps a platform-specific audio +/// source and exposes simple controls such as `mute()` and `unmute()`. +/// Muting a local audio track stops transmitting audio to the room, but +/// the underlying source may continue capturing depending on platform behavior. /// -/// Typical usage: -/// -/// auto source = AudioSource::create(...); -/// auto track = LocalAudioTrack::createLocalAudioTrack("mic", source); -/// room->localParticipant()->publishTrack(track); -/// -/// Muting a local audio track stops transmitting audio to the room, but -/// the underlying source may continue capturing depending on platform -/// behavior. -/// -/// The track name provided during creation is visible to remote -/// participants and can be used for debugging or UI display. +/// @note Thread-safety: Not thread-safe. This is a thin FFI wrapper with no +/// internal synchronization. class LIVEKIT_API LocalAudioTrack : public Track { public: /// Creates a new local audio track backed by the given `AudioSource`. /// /// @param name Human-readable name for the track. This may appear to /// remote participants and in analytics/debug logs. - /// @param source The audio source that produces PCM frames for this track. + /// @param source The audio source that produces PCM frames for this track. /// The caller retains ownership and should use this source /// directly for frame capture. /// /// @return A shared pointer to the newly constructed `LocalAudioTrack`. + /// @throws std::invalid_argument If \p source is null. + /// @throws std::runtime_error If the FFI request fails. static std::shared_ptr createLocalAudioTrack(const std::string& name, const std::shared_ptr& source); @@ -69,10 +63,15 @@ class LIVEKIT_API LocalAudioTrack : public Track { /// /// @param name Human-readable name for the track. This may appear to /// remote participants and in analytics/debug logs. - /// @param source The platform source that captures microphone audio + /// @param source The platform source that captures microphone audio /// automatically through WebRTC's Audio Device Module. /// /// @return A shared pointer to the newly constructed `LocalAudioTrack`. + /// @throws std::invalid_argument If \p source is null. + /// @throws std::runtime_error If the FFI request fails. + /// + /// @note Thread-safety: Not thread-safe. This is a thin FFI wrapper with no + /// internal synchronization. static std::shared_ptr createLocalAudioTrack(const std::string& name, const std::shared_ptr& source); @@ -80,29 +79,37 @@ class LIVEKIT_API LocalAudioTrack : public Track { /// /// A muted track stops sending audio to the room, but the track remains /// published and can be unmuted later without renegotiation. + /// + /// @throws std::runtime_error If the FFI request fails. void mute(); - /// Unmutes the audio track and resumes sending audio to the room. + /// Unmute the audio track. + /// + /// Resumes sending audio to the room. + /// + /// @throws std::runtime_error If the FFI request fails. void unmute(); - /// Returns a human-readable string representation of the track, - /// including its SID and name. Useful for debugging and logging. + /// Return a human-readable string representation of the track. + /// + /// @return String containing the track SID and name. std::string toString() const; + /// @return String containing the track SID and name. /// @deprecated Use toString() instead. - // NOLINTBEGIN(readability-identifier-naming) [[deprecated("LocalAudioTrack::to_string is deprecated; use LocalAudioTrack::toString instead")]] - std::string to_string() const; - // NOLINTEND(readability-identifier-naming) + std::string to_string() const; // NOLINT(readability-identifier-naming) - /// Returns the publication that owns this track, or nullptr if the track is - /// not published. + /// Return the publication that owns this track. + /// + /// @return Publication that owns this track, or nullptr if the track is not + /// published. std::shared_ptr publication() const noexcept { return local_publication_; } - /// Sets the publication that owns this track. - /// Note: std::move on a const& silently falls back to a copy, so we assign - /// directly. Changing the virtual signature to take by value would enable - /// a true move but is an API-breaking change hence left for a future revision. + /// Set the publication that owns this track. + /// + /// @param publication Publication that owns this track, or nullptr to clear + /// the association. void setPublication(const std::shared_ptr& publication) noexcept override { local_publication_ = publication; } diff --git a/include/livekit/platform_audio.h b/include/livekit/platform_audio.h index 74c5f3b9..501787ff 100644 --- a/include/livekit/platform_audio.h +++ b/include/livekit/platform_audio.h @@ -27,63 +27,119 @@ namespace livekit { +/// Internal shared state for platform audio handles. +/// +/// This forward declaration is exposed only so public wrapper types can hold a +/// shared implementation pointer. +/// +/// @note Thread-safety: Thread-safe. Instances are managed internally by +/// PlatformAudio. struct PlatformAudioState; -/** - * Information about a platform audio device. - */ +/// Information about a platform audio device. +/// +/// Device indices may change when audio hardware is added or removed. Prefer +/// the stable `id` value when selecting a device. +/// +/// @note Thread-safety: Thread-safe. This is an aggregate value type with no +/// internal shared state. struct AudioDeviceInfo { - /** Current device index. Indices may change when devices are added or removed. */ + /// Current device index. std::uint32_t index = 0; - /** Device name reported by the operating system. */ + /// Device name reported by the operating system. std::string name; - /** Platform-specific stable device identifier. Prefer this over index for selection. */ + /// Platform-specific stable device identifier. std::string id; }; -/** - * Audio processing options for platform microphone capture. - */ +/// Audio processing options for platform microphone capture. +/// +/// The default values enable WebRTC's voice processing path for typical +/// microphone publishing. +/// +/// @note Thread-safety: Thread-safe. This is an aggregate value type with no +/// internal shared state. struct PlatformAudioOptions { - /** Enable acoustic echo cancellation. */ + /// Enable acoustic echo cancellation. bool echo_cancellation = true; - /** Enable background noise suppression. */ + /// Enable background noise suppression. bool noise_suppression = true; - /** Enable automatic gain control. */ + /// Enable automatic gain control. bool auto_gain_control = true; - /** Prefer hardware audio processing when the platform provides it. */ + /// Prefer hardware audio processing when the platform provides it. bool prefer_hardware = false; }; -/** - * Error raised when platform audio setup or device operations fail. - */ +/// Error raised when platform audio setup or device operations fail. +/// +/// @note Thread-safety: Thread-safe. Instances are immutable after +/// construction. class LIVEKIT_API PlatformAudioError : public std::runtime_error { public: + /// Create a platform audio error. + /// + /// @param message Human-readable error message. + /// + /// @note Thread-safety: Thread-safe. Instances are immutable after + /// construction. explicit PlatformAudioError(const std::string& message) : std::runtime_error(message) {} }; -/** - * Audio source backed by WebRTC's platform Audio Device Module. - * - * A PlatformAudioSource captures microphone audio automatically. Unlike - * AudioSource, callers do not push frames with captureFrame(). - */ +/// Audio source backed by WebRTC's platform Audio Device Module. +/// +/// A PlatformAudioSource captures microphone audio automatically. Unlike +/// AudioSource, callers do not push frames with captureFrame(). +/// +/// @note Thread-safety: Thread-safe. The source owns an immutable FFI handle +/// and keeps the shared PlatformAudio state alive. class LIVEKIT_API PlatformAudioSource { public: + /// Destroy the platform audio source and release its FFI handle. + /// + /// @note Thread-safety: Not thread-safe. Do not destroy the source while + /// another thread is using it. ~PlatformAudioSource() = default; - PlatformAudioSource(const PlatformAudioSource&) = delete; - PlatformAudioSource& operator=(const PlatformAudioSource&) = delete; - PlatformAudioSource(PlatformAudioSource&&) noexcept = default; - PlatformAudioSource& operator=(PlatformAudioSource&&) noexcept = default; + /// Copy construction is disabled. + /// + /// @param other Source to copy from. + /// + /// @note Thread-safety: Not thread-safe. This operation is deleted. + PlatformAudioSource(const PlatformAudioSource& other) = delete; + + /// Copy assignment is disabled. + /// + /// @param other Source to copy from. + /// @return Reference to this source. + /// + /// @note Thread-safety: Not thread-safe. This operation is deleted. + PlatformAudioSource& operator=(const PlatformAudioSource& other) = delete; + + /// Move the platform audio source. + /// + /// @param other Source to move from. + /// + /// @note Thread-safety: Not thread-safe. The moved-from and moved-to objects + /// must not be accessed concurrently during the move. + PlatformAudioSource(PlatformAudioSource&& other) noexcept = default; - /// Underlying FFI handle ID used in FFI requests. + /// Move-assign the platform audio source. + /// + /// @param other Source to move from. + /// @return Reference to this source. + /// + /// @note Thread-safety: Not thread-safe. The moved-from and moved-to objects + /// must not be accessed concurrently during the move. + PlatformAudioSource& operator=(PlatformAudioSource&& other) noexcept = default; + + /// Return the underlying FFI handle ID used in FFI requests. + /// + /// @note Thread-safety: Thread-safe. Reads immutable handle state. std::uint64_t ffiHandleId() const noexcept { return static_cast(handle_.get()); } private: @@ -95,43 +151,127 @@ class LIVEKIT_API PlatformAudioSource { std::shared_ptr platform_audio_; }; -/** - * Platform audio device manager backed by WebRTC's Audio Device Module. - * - * Use PlatformAudio for normal microphone publishing when built-in echo - * cancellation, noise suppression, automatic gain control, and speaker playout - * are desired. Use AudioSource instead when the application needs direct access - * to raw PCM frames or custom audio generation. - */ +/// Platform audio device manager backed by WebRTC's Audio Device Module. +/// +/// Use PlatformAudio for normal microphone publishing when built-in echo +/// cancellation, noise suppression, automatic gain control, and speaker playout +/// are desired. Use AudioSource instead when the application needs direct access +/// to raw PCM frames or custom audio generation. +/// +/// @note Thread-safety: Thread-safe. Methods send independent FFI requests and +/// share immutable handle state. class LIVEKIT_API PlatformAudio { public: + /// Create a platform audio manager. + /// + /// Enables WebRTC's platform Audio Device Module for microphone capture and + /// speaker playout. + /// + /// @throws PlatformAudioError If the FFI response is malformed or the + /// platform Audio Device Module cannot be created. + /// + /// @note Thread-safety: Thread-safe. Sends an independent FFI request. PlatformAudio(); + + /// Destroy the platform audio manager and release its FFI handle. + /// + /// @note Thread-safety: Not thread-safe. Do not destroy the manager while + /// another thread is using it. ~PlatformAudio() = default; - PlatformAudio(const PlatformAudio&) = default; - PlatformAudio& operator=(const PlatformAudio&) = default; - PlatformAudio(PlatformAudio&&) noexcept = default; - PlatformAudio& operator=(PlatformAudio&&) noexcept = default; + /// Copy the platform audio manager. + /// + /// The copy shares the same underlying platform audio handle. + /// + /// @param other Manager to copy from. + /// + /// @note Thread-safety: Not thread-safe. The source object must not be + /// concurrently moved or assigned while copying. + PlatformAudio(const PlatformAudio& other) = default; + + /// Copy-assign the platform audio manager. + /// + /// The assigned instance shares the same underlying platform audio handle. + /// + /// @param other Manager to copy from. + /// @return Reference to this manager. + /// + /// @note Thread-safety: Not thread-safe. The assigned object and source + /// object must not be accessed concurrently during assignment. + PlatformAudio& operator=(const PlatformAudio& other) = default; + + /// Move the platform audio manager. + /// + /// @param other Manager to move from. + /// + /// @note Thread-safety: Not thread-safe. The moved-from and moved-to objects + /// must not be accessed concurrently during the move. + PlatformAudio(PlatformAudio&& other) noexcept = default; + + /// Move-assign the platform audio manager. + /// + /// @param other Manager to move from. + /// @return Reference to this manager. + /// + /// @note Thread-safety: Not thread-safe. The moved-from and moved-to objects + /// must not be accessed concurrently during the move. + PlatformAudio& operator=(PlatformAudio&& other) noexcept = default; - /// Number of recording devices reported when this instance was created. + /// Return the number of recording devices reported when this instance was created. + /// + /// @note Thread-safety: Thread-safe. Reads immutable handle state. std::int32_t recordingDeviceCount() const noexcept; - /// Number of playout devices reported when this instance was created. + /// Return the number of playout devices reported when this instance was created. + /// + /// @note Thread-safety: Thread-safe. Reads immutable handle state. std::int32_t playoutDeviceCount() const noexcept; /// Enumerate available microphones. + /// + /// @return List of available recording devices. + /// @throws PlatformAudioError If the FFI response is malformed or device + /// enumeration fails. + /// + /// @note Thread-safety: Thread-safe. Sends an independent FFI request. std::vector recordingDevices() const; /// Enumerate available speakers/headphones. + /// + /// @return List of available playout devices. + /// @throws PlatformAudioError If the FFI response is malformed or device + /// enumeration fails. + /// + /// @note Thread-safety: Thread-safe. Sends an independent FFI request. std::vector playoutDevices() const; - /// Select the microphone by AudioDeviceInfo::id. + /// Select the microphone by device ID. + /// + /// @param device_id Stable device identifier from AudioDeviceInfo::id. + /// @throws PlatformAudioError If the FFI response is malformed or device + /// selection fails. + /// + /// @note Thread-safety: Thread-safe. Sends an independent FFI request. void setRecordingDevice(const std::string& device_id) const; - /// Select the speaker/headphones by AudioDeviceInfo::id. + /// Select the speaker/headphones by device ID. + /// + /// @param device_id Stable device identifier from AudioDeviceInfo::id. + /// @throws PlatformAudioError If the FFI response is malformed or device + /// selection fails. + /// + /// @note Thread-safety: Thread-safe. Sends an independent FFI request. void setPlayoutDevice(const std::string& device_id) const; /// Create an automatically captured microphone source for LocalAudioTrack. + /// + /// @param options Audio processing options for the platform microphone path. + /// @return Platform-backed audio source suitable for LocalAudioTrack. + /// @throws PlatformAudioError If the FFI response is malformed or source + /// creation fails. + /// + /// @note Thread-safety: Thread-safe. Sends an independent FFI request and + /// returns a source that keeps the shared PlatformAudio state alive. std::shared_ptr createAudioSource(const PlatformAudioOptions& options = {}) const; private: From b81c405941a94a82e2f515d0e708b55856826d85 Mon Sep 17 00:00:00 2001 From: Stephen DeRosa Date: Wed, 27 May 2026 13:19:37 -0600 Subject: [PATCH 3/3] platform_audio --- cpp-example-collection | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/cpp-example-collection b/cpp-example-collection index 839e38fa..cce87b3c 160000 --- a/cpp-example-collection +++ b/cpp-example-collection @@ -1 +1 @@ -Subproject commit 839e38fa3079c1c63efeeb73ca476bd1c42afc51 +Subproject commit cce87b3ca1cace650902e85d3b3c02b8e30e0873