From 628aceb6775ae7f8e8d7287731b771cf653ed87b Mon Sep 17 00:00:00 2001 From: Stephen DeRosa Date: Wed, 27 May 2026 13:16:56 -0600 Subject: [PATCH] platform_audio --- CMakeLists.txt | 1 + README.md | 12 ++- platform_audio/CMakeLists.txt | 16 +++ platform_audio/README.md | 44 ++++++++ platform_audio/player/CMakeLists.txt | 22 ++++ platform_audio/player/main.cpp | 152 +++++++++++++++++++++++++++ platform_audio/sender/CMakeLists.txt | 22 ++++ platform_audio/sender/main.cpp | 145 +++++++++++++++++++++++++ 8 files changed, 413 insertions(+), 1 deletion(-) create mode 100644 platform_audio/CMakeLists.txt create mode 100644 platform_audio/README.md create mode 100644 platform_audio/player/CMakeLists.txt create mode 100644 platform_audio/player/main.cpp create mode 100644 platform_audio/sender/CMakeLists.txt create mode 100644 platform_audio/sender/main.cpp diff --git a/CMakeLists.txt b/CMakeLists.txt index a102cc5..83b916a 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -89,6 +89,7 @@ add_subdirectory(logging_levels/basic_usage logging_levels_basic_usage) add_subdirectory(logging_levels/custom_sinks logging_levels_custom_sinks) add_subdirectory(hello_livekit/sender hello_livekit_sender) add_subdirectory(hello_livekit/receiver hello_livekit_receiver) +add_subdirectory(platform_audio) add_subdirectory(ping_pong/ping ping_pong_ping) add_subdirectory(ping_pong/pong ping_pong_pong) add_subdirectory(user_timestamped_video) diff --git a/README.md b/README.md index 0d5064f..88107bd 100644 --- a/README.md +++ b/README.md @@ -73,6 +73,16 @@ For example: ./build/basic_room/basic_room --url --token ``` +### PlatformAudio + +The `platform_audio` examples show microphone capture and speaker playout using +WebRTC's platform Audio Device Module: + +```bash +./build/platform_audio/player/PlatformAudioPlayer +./build/platform_audio/sender/PlatformAudioSender +``` + ### Supported platforms Prebuilt SDKs are downloaded automatically for: @@ -80,4 +90,4 @@ Prebuilt SDKs are downloaded automatically for: * macOS: x64, arm64 (Apple Silicon) * Linux: x64 -If no matching SDK is available for your platform, CMake configuration will fail with a clear error. \ No newline at end of file +If no matching SDK is available for your platform, CMake configuration will fail with a clear error. diff --git a/platform_audio/CMakeLists.txt b/platform_audio/CMakeLists.txt new file mode 100644 index 0000000..190e0c5 --- /dev/null +++ b/platform_audio/CMakeLists.txt @@ -0,0 +1,16 @@ +# Copyright 2026 LiveKit, Inc. +# +# 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. + +add_subdirectory(sender) +add_subdirectory(player) diff --git a/platform_audio/README.md b/platform_audio/README.md new file mode 100644 index 0000000..bb61d54 --- /dev/null +++ b/platform_audio/README.md @@ -0,0 +1,44 @@ +# PlatformAudio + +These examples demonstrate the platform Audio Device Module path: + +- `PlatformAudioSender` publishes microphone audio with echo cancellation, noise suppression, and auto gain control. +- `PlatformAudioPlayer` joins the same room and plays subscribed remote audio through the platform output device. + +Build the collection, then run the player and sender with different participant tokens for the same room: + +```bash +./build/platform_audio/player/PlatformAudioPlayer +./build/platform_audio/sender/PlatformAudioSender +``` + +Environment fallbacks: + +```bash +export LIVEKIT_URL=wss://your-livekit-host +export LIVEKIT_PLAYER_TOKEN= +export LIVEKIT_SENDER_TOKEN= +``` + +## Test environments + +The sender captures the mic and runs the AEC/NS/AGC front-end; the player drives +hardware playout. The acoustic echo cancellation (AEC) reference is per Audio +Device Module (ADM), so the sender can only cancel playout from *its own* ADM. +Pick an environment based on what you want to prove out. + +| Environment | What it proves | Notes | +|-------------|----------------|-------| +| **Two machines** (sender on A, player on B) | End-to-end capture → publish → subscribe → hardware playout over the network, like a real call. | Closest to a real LiveKit call. For full duplex (both apps on both boxes), use headphones or separate rooms to avoid feedback. | +| **One machine + headphones** | Mic capture and ADM playout both work on one box, with no acoustic feedback path. | Best for confirming device selection and round-trip latency in isolation. | +| **Noise suppression (NS)** | Steady background noise is attenuated while speech passes. | Add a fan / AC hum / typing near the mic. A/B by setting `noise_suppression = false` in `sender/main.cpp`. | +| **Auto gain control (AGC)** | Quiet vs. loud / near vs. far speech is normalized on the player side. | A/B by setting `auto_gain_control = false` in `sender/main.cpp`. | +| **`prefer_hardware = true`** | Platform hardware voice processing engages (e.g. macOS voice-processing I/O). | Set in the sender options; compare CPU and audio character vs. the software path. | +| **Device / hot-plug sanity** | `recordingDevices()` / `playoutDevices()` reflect attached hardware and route correctly. | Plug/unplug a USB or Bluetooth mic/headset before launch and check the startup device logs. | + +> **AEC caveat:** these split sender/player apps cannot demonstrate AEC against +> each other on one machine over open speakers — the sender's AEC has no +> reference to the player's separate ADM, so the speaker output is treated as +> external sound and you get an echo/feedback loop. Genuine AEC requires a +> single application that both plays remote audio and captures the mic through +> the *same* ADM. Use headphones to avoid feedback with these examples. diff --git a/platform_audio/player/CMakeLists.txt b/platform_audio/player/CMakeLists.txt new file mode 100644 index 0000000..3186653 --- /dev/null +++ b/platform_audio/player/CMakeLists.txt @@ -0,0 +1,22 @@ +# Copyright 2026 LiveKit, Inc. +# +# 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. + +add_executable(PlatformAudioPlayer + main.cpp +) + +target_include_directories(PlatformAudioPlayer PRIVATE ${CMAKE_CURRENT_SOURCE_DIR}) +target_link_libraries(PlatformAudioPlayer PRIVATE ${LIVEKIT_CORE_TARGET}) + +livekit_copy_windows_runtime_dlls(PlatformAudioPlayer) diff --git a/platform_audio/player/main.cpp b/platform_audio/player/main.cpp new file mode 100644 index 0000000..738a0f6 --- /dev/null +++ b/platform_audio/player/main.cpp @@ -0,0 +1,152 @@ +/* + * Copyright 2026 LiveKit, Inc. + * + * 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. + */ + +/// Plays subscribed room audio using PlatformAudio. +/// +/// Usage: +/// PlatformAudioPlayer +/// +/// Or via environment variables: +/// LIVEKIT_URL, LIVEKIT_PLAYER_TOKEN + +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include "livekit/livekit.h" + +using namespace livekit; + +namespace { + +std::atomic g_running{true}; + +void handleSignal(int) { g_running.store(false); } + +std::string getenvOrEmpty(const char* name) { + const char* value = std::getenv(name); + return value ? std::string(value) : std::string{}; +} + +void printUsage() { + std::cerr << "[error] Usage: PlatformAudioPlayer \n" + << " or set LIVEKIT_URL and LIVEKIT_PLAYER_TOKEN\n"; +} + +class PlayerDelegate final : public RoomDelegate { +public: + void onParticipantConnected(Room&, const ParticipantConnectedEvent& event) override { + if (event.participant) { + std::cout << "[info] [platform-audio-player] Participant connected identity='" << event.participant->identity() + << "'\n"; + } + } + + void onTrackSubscribed(Room&, const TrackSubscribedEvent& event) override { + if (!event.track || event.track->kind() != TrackKind::KIND_AUDIO) { + return; + } + + const std::string participant_identity = event.participant ? event.participant->identity() : std::string("unknown"); + const std::string publication_name = event.publication ? event.publication->name() : event.track->name(); + std::cout << "[info] [platform-audio-player] Playing audio track '" << publication_name + << "' from participant identity='" << participant_identity << "'\n"; + } + + void onTrackSubscriptionFailed(Room&, const TrackSubscriptionFailedEvent& event) override { + const std::string participant_identity = event.participant ? event.participant->identity() : std::string("unknown"); + std::cerr << "[warn] [platform-audio-player] Audio subscription failed for participant identity='" + << participant_identity << "' track_sid='" << event.track_sid << "'\n"; + } +}; + +} // namespace + +int main(int argc, char* argv[]) { + std::string url = getenvOrEmpty("LIVEKIT_URL"); + std::string player_token = getenvOrEmpty("LIVEKIT_PLAYER_TOKEN"); + + if (argc >= 3) { + url = argv[1]; + player_token = argv[2]; + } + + if (url.empty() || player_token.empty()) { + printUsage(); + return 1; + } + + std::signal(SIGINT, handleSignal); +#ifdef SIGTERM + std::signal(SIGTERM, handleSignal); +#endif + + livekit::initialize(livekit::LogLevel::Info); + + try { + PlatformAudio platform_audio; + + auto playout_devices = platform_audio.playoutDevices(); + std::cout << "[info] [platform-audio-player] Playout devices: " << playout_devices.size() << "\n"; + for (const auto& device : playout_devices) { + std::cout << " [" << device.index << "] " << device.name << " id=" << device.id << "\n"; + } + + auto room = std::make_unique(); + PlayerDelegate delegate; + room->setDelegate(&delegate); + + RoomOptions options; + options.auto_subscribe = true; + options.dynacast = false; + + if (!room->connect(url, player_token, options)) { + std::cerr << "[error] [platform-audio-player] Failed to connect\n"; + room.reset(); + livekit::shutdown(); + return 1; + } + + if (auto local_participant = room->localParticipant().lock()) { + std::cout << "[info] [platform-audio-player] Connected as identity='" << local_participant->identity() + << "' room='" << room->roomInfo().name << "'\n"; + } else { + throw std::runtime_error("unable to lock local participant"); + } + + std::cout << "[info] [platform-audio-player] Waiting for remote audio; Ctrl-C to exit\n"; + while (g_running.load()) { + std::this_thread::sleep_for(std::chrono::milliseconds(50)); + } + + std::cout << "[info] [platform-audio-player] Disconnecting\n"; + room->setDelegate(nullptr); + room.reset(); + } catch (const std::exception& error) { + std::cerr << "[error] [platform-audio-player] " << error.what() << "\n"; + livekit::shutdown(); + return 1; + } + + livekit::shutdown(); + return 0; +} diff --git a/platform_audio/sender/CMakeLists.txt b/platform_audio/sender/CMakeLists.txt new file mode 100644 index 0000000..5593779 --- /dev/null +++ b/platform_audio/sender/CMakeLists.txt @@ -0,0 +1,22 @@ +# Copyright 2026 LiveKit, Inc. +# +# 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. + +add_executable(PlatformAudioSender + main.cpp +) + +target_include_directories(PlatformAudioSender PRIVATE ${CMAKE_CURRENT_SOURCE_DIR}) +target_link_libraries(PlatformAudioSender PRIVATE ${LIVEKIT_CORE_TARGET}) + +livekit_copy_windows_runtime_dlls(PlatformAudioSender) diff --git a/platform_audio/sender/main.cpp b/platform_audio/sender/main.cpp new file mode 100644 index 0000000..1b13626 --- /dev/null +++ b/platform_audio/sender/main.cpp @@ -0,0 +1,145 @@ +/* + * Copyright 2026 LiveKit, Inc. + * + * 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. + */ + +/// Publishes microphone audio using PlatformAudio. +/// +/// Usage: +/// PlatformAudioSender +/// +/// Or via environment variables: +/// LIVEKIT_URL, LIVEKIT_SENDER_TOKEN + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include "livekit/livekit.h" + +using namespace livekit; + +namespace { + +constexpr const char* kAudioTrackName = "platform-microphone"; + +std::atomic g_running{true}; + +void handleSignal(int) { g_running.store(false); } + +std::string getenvOrEmpty(const char* name) { + const char* value = std::getenv(name); + return value ? std::string(value) : std::string{}; +} + +void printUsage() { + std::cerr << "[error] Usage: PlatformAudioSender \n" + << " or set LIVEKIT_URL and LIVEKIT_SENDER_TOKEN\n"; +} + +} // namespace + +int main(int argc, char* argv[]) { + std::string url = getenvOrEmpty("LIVEKIT_URL"); + std::string sender_token = getenvOrEmpty("LIVEKIT_SENDER_TOKEN"); + + if (argc >= 3) { + url = argv[1]; + sender_token = argv[2]; + } + + if (url.empty() || sender_token.empty()) { + printUsage(); + return 1; + } + + std::signal(SIGINT, handleSignal); +#ifdef SIGTERM + std::signal(SIGTERM, handleSignal); +#endif + + livekit::initialize(livekit::LogLevel::Info); + + try { + PlatformAudio platform_audio; + + auto recording_devices = platform_audio.recordingDevices(); + std::cout << "[info] [platform-audio-sender] Recording devices: " << recording_devices.size() << "\n"; + for (const auto& device : recording_devices) { + std::cout << " [" << device.index << "] " << device.name << " id=" << device.id << "\n"; + } + + auto room = std::make_unique(); + RoomOptions options; + options.auto_subscribe = true; + options.dynacast = false; + + if (!room->connect(url, sender_token, options)) { + std::cerr << "[error] [platform-audio-sender] Failed to connect\n"; + room.reset(); + livekit::shutdown(); + return 1; + } + + auto local_participant = room->localParticipant().lock(); + if (!local_participant) { + throw std::runtime_error("unable to lock local participant"); + } + + std::cout << "[info] [platform-audio-sender] Connected as identity='" << local_participant->identity() << "' room='" + << room->roomInfo().name << "'\n"; + + livekit::PlatformAudioOptions audio_options; + audio_options.echo_cancellation = true; + audio_options.noise_suppression = true; + audio_options.auto_gain_control = true; + + auto audio_source = platform_audio.createAudioSource(audio_options); + auto audio_track = LocalAudioTrack::createLocalAudioTrack(kAudioTrackName, audio_source); + + TrackPublishOptions publish_options; + publish_options.source = TrackSource::SOURCE_MICROPHONE; + + local_participant->publishTrack(audio_track, publish_options); + local_participant.reset(); + auto publication = audio_track->publication(); + std::cout << "[info] [platform-audio-sender] Published microphone track"; + if (publication) { + std::cout << " sid=" << publication->sid(); + } + std::cout << "\n"; + + std::cout << "[info] [platform-audio-sender] Sending microphone audio; Ctrl-C to exit\n"; + while (g_running.load()) { + std::this_thread::sleep_for(std::chrono::milliseconds(50)); + } + + std::cout << "[info] [platform-audio-sender] Disconnecting\n"; + room.reset(); + } catch (const std::exception& error) { + std::cerr << "[error] [platform-audio-sender] " << error.what() << "\n"; + livekit::shutdown(); + return 1; + } + + livekit::shutdown(); + return 0; +}