Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions .gitattributes
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
data/welcome.wav filter=lfs diff=lfs merge=lfs -text
4 changes: 3 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -51,7 +54,6 @@ export LIVEKIT_TOKEN=<jwt-token>

Press Ctrl-C to exit the example.


## 🧰 Recommended Setup
### macOS
```bash
Expand Down
3 changes: 3 additions & 0 deletions data/welcome.wav
Git LFS file not shown
13 changes: 12 additions & 1 deletion examples/CMakeLists.txt
Original file line number Diff line number Diff line change
@@ -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
)
11 changes: 3 additions & 8 deletions examples/simple_room/main.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@
#include <vector>

#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"
Expand Down Expand Up @@ -126,19 +127,13 @@ void runNoiseCaptureLoop(const std::shared_ptr<AudioSource> &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<int16_t> 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<std::size_t>(num_channels) *
static_cast<std::size_t>(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) {
Expand Down
162 changes: 162 additions & 0 deletions examples/simple_room/wav_audio_source.cpp
Original file line number Diff line number Diff line change
@@ -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 <cstring>
#include <fstream>
#include <stdexcept>

// --------------------------------------------------
// 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<char *>(&out_value), 4);
};
auto read_u16 = [&](uint16_t &out_value) {
file.read(reinterpret_cast<char *>(&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<int16_t> 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<char *>(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<int>(sample_rate);
out.num_channels = static_cast<int>(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<std::size_t>(frame.num_channels()) *
static_cast<std::size_t>(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;
}
}
}
56 changes: 56 additions & 0 deletions examples/simple_room/wav_audio_source.h
Original file line number Diff line number Diff line change
@@ -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 <cstddef>
#include <cstdint>
#include <string>
#include <vector>

// Simple WAV container for 16-bit PCM files
struct WavData {
int sample_rate = 0;
int num_channels = 0;
std::vector<int16_t> 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_;
};