diff --git a/.gitattributes b/.gitattributes new file mode 100644 index 0000000..efcfff5 --- /dev/null +++ b/.gitattributes @@ -0,0 +1 @@ +data/welcome.wav filter=lfs diff=lfs merge=lfs -text diff --git a/README.md b/README.md index 0b5f2e0..c294f8f 100644 --- a/README.md +++ b/README.md @@ -9,6 +9,9 @@ This SDK enables native C++ applications to connect to LiveKit servers for real- - **Rust / Cargo** (latest stable toolchain) - **Protobuf** compiler (`protoc`) - **macOS** users: System frameworks (CoreAudio, AudioToolbox, etc.) are automatically linked via CMake. +- **Git LFS** (required for examples) + Some example data files (e.g., audio assets) are stored using Git LFS. + You must install Git LFS before cloning or pulling the repo if you want to run the examples. ## 🧩 Clone the Repository @@ -51,7 +54,6 @@ export LIVEKIT_TOKEN= Press Ctrl-C to exit the example. - ## 🧰 Recommended Setup ### macOS ```bash diff --git a/data/welcome.wav b/data/welcome.wav new file mode 100644 index 0000000..d4e10c3 --- /dev/null +++ b/data/welcome.wav @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:e5a61a07be0392ac689876d33b92b75dc73442217188ff5d7ed23e7597d1ae98 +size 666724 diff --git a/examples/CMakeLists.txt b/examples/CMakeLists.txt index 92e35c7..c8d68a4 100644 --- a/examples/CMakeLists.txt +++ b/examples/CMakeLists.txt @@ -1,5 +1,16 @@ cmake_minimum_required(VERSION 3.31.0) project (livekit-examples) -add_executable(SimpleRoom simple_room/main.cpp) +add_executable(SimpleRoom + simple_room/main.cpp + simple_room/wav_audio_source.cpp + simple_room/wav_audio_source.h +) + target_link_libraries(SimpleRoom livekit) + +add_custom_command(TARGET SimpleRoom POST_BUILD + COMMAND ${CMAKE_COMMAND} -E copy_directory + ${CMAKE_SOURCE_DIR}/data + ${CMAKE_CURRENT_BINARY_DIR}/data +) \ No newline at end of file diff --git a/examples/simple_room/main.cpp b/examples/simple_room/main.cpp index 5d69c70..7293755 100644 --- a/examples/simple_room/main.cpp +++ b/examples/simple_room/main.cpp @@ -9,6 +9,7 @@ #include #include "livekit/livekit.h" +#include "wav_audio_source.h" // TODO(shijing), remove this livekit_ffi.h as it should be internal only. #include "livekit_ffi.h" @@ -126,19 +127,13 @@ void runNoiseCaptureLoop(const std::shared_ptr &source) { const int frame_ms = 10; const int samples_per_channel = sample_rate * frame_ms / 1000; - std::mt19937 rng(std::random_device{}()); - std::uniform_int_distribution noise_dist(-5000, 5000); + WavAudioSource WavAudioSource("data/welcome.wav", 48000, 1, false); using Clock = std::chrono::steady_clock; auto next_deadline = Clock::now(); while (g_running.load(std::memory_order_relaxed)) { AudioFrame frame = AudioFrame::create(sample_rate, num_channels, samples_per_channel); - const std::size_t total_samples = - static_cast(num_channels) * - static_cast(samples_per_channel); - for (std::size_t i = 0; i < total_samples; ++i) { - frame.data()[i] = noise_dist(rng); - } + WavAudioSource.fillFrame(frame); try { source->captureFrame(frame); } catch (const std::exception &e) { diff --git a/examples/simple_room/wav_audio_source.cpp b/examples/simple_room/wav_audio_source.cpp new file mode 100644 index 0000000..b519b81 --- /dev/null +++ b/examples/simple_room/wav_audio_source.cpp @@ -0,0 +1,162 @@ +/* + * Copyright 2025 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 "wav_audio_source.h" + +#include +#include +#include + +// -------------------------------------------------- +// Minimal WAV loader (16-bit PCM only) +// -------------------------------------------------- +WavData load_wav16(const std::string &path) { + std::ifstream file(path, std::ios::binary); + if (!file) { + throw std::runtime_error("Failed to open WAV file: " + path + + " (If this file exists in the repo, ensure Git " + "LFS is installed and run `git lfs pull`)"); + } + + auto read_u32 = [&](uint32_t &out_value) { + file.read(reinterpret_cast(&out_value), 4); + }; + auto read_u16 = [&](uint16_t &out_value) { + file.read(reinterpret_cast(&out_value), 2); + }; + + char riff[4]; + file.read(riff, 4); + if (std::strncmp(riff, "RIFF", 4) != 0) { + throw std::runtime_error("Not a RIFF file"); + } + + uint32_t chunk_size = 0; + read_u32(chunk_size); + + char wave[4]; + file.read(wave, 4); + if (std::strncmp(wave, "WAVE", 4) != 0) { + throw std::runtime_error("Not a WAVE file"); + } + + uint16_t audio_format = 0; + uint16_t num_channels = 0; + uint32_t sample_rate = 0; + uint16_t bits_per_sample = 0; + + bool have_fmt = false; + bool have_data = false; + std::vector samples; + + while (!have_data && file) { + char sub_id[4]; + file.read(sub_id, 4); + + uint32_t sub_size = 0; + read_u32(sub_size); + + if (std::strncmp(sub_id, "fmt ", 4) == 0) { + have_fmt = true; + + read_u16(audio_format); + read_u16(num_channels); + read_u32(sample_rate); + + uint32_t byte_rate = 0; + uint16_t block_align = 0; + read_u32(byte_rate); + read_u16(block_align); + read_u16(bits_per_sample); + + if (sub_size > 16) { + file.seekg(sub_size - 16, std::ios::cur); + } + + if (audio_format != 1) { + throw std::runtime_error("Only PCM WAV supported"); + } + if (bits_per_sample != 16) { + throw std::runtime_error("Only 16-bit WAV supported"); + } + + } else if (std::strncmp(sub_id, "data", 4) == 0) { + if (!have_fmt) { + throw std::runtime_error("data chunk appeared before fmt chunk"); + } + + have_data = true; + const std::size_t count = sub_size / sizeof(int16_t); + samples.resize(count); + file.read(reinterpret_cast(samples.data()), sub_size); + + } else { + // Unknown chunk: skip it + file.seekg(sub_size, std::ios::cur); + } + } + + if (!have_data) { + throw std::runtime_error("No data chunk in WAV file"); + } + + WavData out; + out.sample_rate = static_cast(sample_rate); + out.num_channels = static_cast(num_channels); + out.samples = std::move(samples); + return out; +} + +WavAudioSource::WavAudioSource(const std::string &path, + int expected_sample_rate, int expected_channels, + bool loop_enabled) + : loop_enabled_(loop_enabled) { + wav_ = load_wav16(path); + + if (wav_.sample_rate != expected_sample_rate) { + throw std::runtime_error("WAV sample rate mismatch"); + } + if (wav_.num_channels != expected_channels) { + throw std::runtime_error("WAV channel count mismatch"); + } + + sample_rate_ = wav_.sample_rate; + num_channels_ = wav_.num_channels; + + playhead_ = 0; +} + +void WavAudioSource::fillFrame(AudioFrame &frame) { + const std::size_t frame_samples = + static_cast(frame.num_channels()) * + static_cast(frame.samples_per_channel()); + + int16_t *dst = frame.data().data(); + const std::size_t total_wav_samples = wav_.samples.size(); + + for (std::size_t i = 0; i < frame_samples; ++i) { + if (playhead_ < total_wav_samples) { + dst[i] = wav_.samples[playhead_]; + ++playhead_; + } else if (loop_enabled_ && total_wav_samples > 0) { + playhead_ = 0; + dst[i] = wav_.samples[playhead_]; + ++playhead_; + } else { + dst[i] = 0; + } + } +} diff --git a/examples/simple_room/wav_audio_source.h b/examples/simple_room/wav_audio_source.h new file mode 100644 index 0000000..51a101c --- /dev/null +++ b/examples/simple_room/wav_audio_source.h @@ -0,0 +1,56 @@ + +/* + * Copyright 2025 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 "livekit/livekit.h" +#include +#include +#include +#include + +// Simple WAV container for 16-bit PCM files +struct WavData { + int sample_rate = 0; + int num_channels = 0; + std::vector samples; +}; + +// Helper that loads 16-bit PCM WAV (16-bit, PCM only) +WavData loadWav16(const std::string &path); + +using namespace livekit; + +class WavAudioSource { +public: + // loop_enabled: whether to loop when reaching the end + WavAudioSource(const std::string &path, int expected_sample_rate, + int expected_channels, bool loop_enabled = true); + + // Fill a frame with the next chunk of audio. + void fillFrame(AudioFrame &frame); + +private: + void initLoopDelayCounter(); + + WavData wav_; + std::size_t playhead_ = 0; + + const bool loop_enabled_; + int sample_rate_; + int num_channels_; +};