Skip to content

Commit 50c2397

Browse files
Sxian/clt 2296/simple room plays welcome audio file (#10)
* Change SimpleRoom to play a welcome wav file rather than noise * added git lfs to README * removed a comment
1 parent 82cd65e commit 50c2397

File tree

7 files changed

+240
-10
lines changed

7 files changed

+240
-10
lines changed

.gitattributes

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
data/welcome.wav filter=lfs diff=lfs merge=lfs -text

README.md

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,9 @@ This SDK enables native C++ applications to connect to LiveKit servers for real-
99
- **Rust / Cargo** (latest stable toolchain)
1010
- **Protobuf** compiler (`protoc`)
1111
- **macOS** users: System frameworks (CoreAudio, AudioToolbox, etc.) are automatically linked via CMake.
12+
- **Git LFS** (required for examples)
13+
Some example data files (e.g., audio assets) are stored using Git LFS.
14+
You must install Git LFS before cloning or pulling the repo if you want to run the examples.
1215

1316

1417
## 🧩 Clone the Repository
@@ -51,7 +54,6 @@ export LIVEKIT_TOKEN=<jwt-token>
5154

5255
Press Ctrl-C to exit the example.
5356

54-
5557
## 🧰 Recommended Setup
5658
### macOS
5759
```bash

data/welcome.wav

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
version https://git-lfs.github.com/spec/v1
2+
oid sha256:e5a61a07be0392ac689876d33b92b75dc73442217188ff5d7ed23e7597d1ae98
3+
size 666724

examples/CMakeLists.txt

Lines changed: 12 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,16 @@
11
cmake_minimum_required(VERSION 3.31.0)
22
project (livekit-examples)
33

4-
add_executable(SimpleRoom simple_room/main.cpp)
4+
add_executable(SimpleRoom
5+
simple_room/main.cpp
6+
simple_room/wav_audio_source.cpp
7+
simple_room/wav_audio_source.h
8+
)
9+
510
target_link_libraries(SimpleRoom livekit)
11+
12+
add_custom_command(TARGET SimpleRoom POST_BUILD
13+
COMMAND ${CMAKE_COMMAND} -E copy_directory
14+
${CMAKE_SOURCE_DIR}/data
15+
${CMAKE_CURRENT_BINARY_DIR}/data
16+
)

examples/simple_room/main.cpp

Lines changed: 3 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@
99
#include <vector>
1010

1111
#include "livekit/livekit.h"
12+
#include "wav_audio_source.h"
1213

1314
// TODO(shijing), remove this livekit_ffi.h as it should be internal only.
1415
#include "livekit_ffi.h"
@@ -129,19 +130,13 @@ void runNoiseCaptureLoop(const std::shared_ptr<AudioSource> &source) {
129130
const int frame_ms = 10;
130131
const int samples_per_channel = sample_rate * frame_ms / 1000;
131132

132-
std::mt19937 rng(std::random_device{}());
133-
std::uniform_int_distribution<int16_t> noise_dist(-5000, 5000);
133+
WavAudioSource WavAudioSource("data/welcome.wav", 48000, 1, false);
134134
using Clock = std::chrono::steady_clock;
135135
auto next_deadline = Clock::now();
136136
while (g_running.load(std::memory_order_relaxed)) {
137137
AudioFrame frame =
138138
AudioFrame::create(sample_rate, num_channels, samples_per_channel);
139-
const std::size_t total_samples =
140-
static_cast<std::size_t>(num_channels) *
141-
static_cast<std::size_t>(samples_per_channel);
142-
for (std::size_t i = 0; i < total_samples; ++i) {
143-
frame.data()[i] = noise_dist(rng);
144-
}
139+
WavAudioSource.fillFrame(frame);
145140
try {
146141
source->captureFrame(frame);
147142
} catch (const std::exception &e) {
Lines changed: 162 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,162 @@
1+
/*
2+
* Copyright 2025 LiveKit
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* http://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an “AS IS” BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
17+
#include "wav_audio_source.h"
18+
19+
#include <cstring>
20+
#include <fstream>
21+
#include <stdexcept>
22+
23+
// --------------------------------------------------
24+
// Minimal WAV loader (16-bit PCM only)
25+
// --------------------------------------------------
26+
WavData load_wav16(const std::string &path) {
27+
std::ifstream file(path, std::ios::binary);
28+
if (!file) {
29+
throw std::runtime_error("Failed to open WAV file: " + path +
30+
" (If this file exists in the repo, ensure Git "
31+
"LFS is installed and run `git lfs pull`)");
32+
}
33+
34+
auto read_u32 = [&](uint32_t &out_value) {
35+
file.read(reinterpret_cast<char *>(&out_value), 4);
36+
};
37+
auto read_u16 = [&](uint16_t &out_value) {
38+
file.read(reinterpret_cast<char *>(&out_value), 2);
39+
};
40+
41+
char riff[4];
42+
file.read(riff, 4);
43+
if (std::strncmp(riff, "RIFF", 4) != 0) {
44+
throw std::runtime_error("Not a RIFF file");
45+
}
46+
47+
uint32_t chunk_size = 0;
48+
read_u32(chunk_size);
49+
50+
char wave[4];
51+
file.read(wave, 4);
52+
if (std::strncmp(wave, "WAVE", 4) != 0) {
53+
throw std::runtime_error("Not a WAVE file");
54+
}
55+
56+
uint16_t audio_format = 0;
57+
uint16_t num_channels = 0;
58+
uint32_t sample_rate = 0;
59+
uint16_t bits_per_sample = 0;
60+
61+
bool have_fmt = false;
62+
bool have_data = false;
63+
std::vector<int16_t> samples;
64+
65+
while (!have_data && file) {
66+
char sub_id[4];
67+
file.read(sub_id, 4);
68+
69+
uint32_t sub_size = 0;
70+
read_u32(sub_size);
71+
72+
if (std::strncmp(sub_id, "fmt ", 4) == 0) {
73+
have_fmt = true;
74+
75+
read_u16(audio_format);
76+
read_u16(num_channels);
77+
read_u32(sample_rate);
78+
79+
uint32_t byte_rate = 0;
80+
uint16_t block_align = 0;
81+
read_u32(byte_rate);
82+
read_u16(block_align);
83+
read_u16(bits_per_sample);
84+
85+
if (sub_size > 16) {
86+
file.seekg(sub_size - 16, std::ios::cur);
87+
}
88+
89+
if (audio_format != 1) {
90+
throw std::runtime_error("Only PCM WAV supported");
91+
}
92+
if (bits_per_sample != 16) {
93+
throw std::runtime_error("Only 16-bit WAV supported");
94+
}
95+
96+
} else if (std::strncmp(sub_id, "data", 4) == 0) {
97+
if (!have_fmt) {
98+
throw std::runtime_error("data chunk appeared before fmt chunk");
99+
}
100+
101+
have_data = true;
102+
const std::size_t count = sub_size / sizeof(int16_t);
103+
samples.resize(count);
104+
file.read(reinterpret_cast<char *>(samples.data()), sub_size);
105+
106+
} else {
107+
// Unknown chunk: skip it
108+
file.seekg(sub_size, std::ios::cur);
109+
}
110+
}
111+
112+
if (!have_data) {
113+
throw std::runtime_error("No data chunk in WAV file");
114+
}
115+
116+
WavData out;
117+
out.sample_rate = static_cast<int>(sample_rate);
118+
out.num_channels = static_cast<int>(num_channels);
119+
out.samples = std::move(samples);
120+
return out;
121+
}
122+
123+
WavAudioSource::WavAudioSource(const std::string &path,
124+
int expected_sample_rate, int expected_channels,
125+
bool loop_enabled)
126+
: loop_enabled_(loop_enabled) {
127+
wav_ = load_wav16(path);
128+
129+
if (wav_.sample_rate != expected_sample_rate) {
130+
throw std::runtime_error("WAV sample rate mismatch");
131+
}
132+
if (wav_.num_channels != expected_channels) {
133+
throw std::runtime_error("WAV channel count mismatch");
134+
}
135+
136+
sample_rate_ = wav_.sample_rate;
137+
num_channels_ = wav_.num_channels;
138+
139+
playhead_ = 0;
140+
}
141+
142+
void WavAudioSource::fillFrame(AudioFrame &frame) {
143+
const std::size_t frame_samples =
144+
static_cast<std::size_t>(frame.num_channels()) *
145+
static_cast<std::size_t>(frame.samples_per_channel());
146+
147+
int16_t *dst = frame.data().data();
148+
const std::size_t total_wav_samples = wav_.samples.size();
149+
150+
for (std::size_t i = 0; i < frame_samples; ++i) {
151+
if (playhead_ < total_wav_samples) {
152+
dst[i] = wav_.samples[playhead_];
153+
++playhead_;
154+
} else if (loop_enabled_ && total_wav_samples > 0) {
155+
playhead_ = 0;
156+
dst[i] = wav_.samples[playhead_];
157+
++playhead_;
158+
} else {
159+
dst[i] = 0;
160+
}
161+
}
162+
}
Lines changed: 56 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,56 @@
1+
2+
/*
3+
* Copyright 2025 LiveKit
4+
*
5+
* Licensed under the Apache License, Version 2.0 (the "License");
6+
* you may not use this file except in compliance with the License.
7+
* You may obtain a copy of the License at
8+
*
9+
* http://www.apache.org/licenses/LICENSE-2.0
10+
*
11+
* Unless required by applicable law or agreed to in writing, software
12+
* distributed under the License is distributed on an “AS IS” BASIS,
13+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14+
* See the License for the specific language governing permissions and
15+
* limitations under the License.
16+
*/
17+
18+
#pragma once
19+
20+
#include "livekit/livekit.h"
21+
#include <cstddef>
22+
#include <cstdint>
23+
#include <string>
24+
#include <vector>
25+
26+
// Simple WAV container for 16-bit PCM files
27+
struct WavData {
28+
int sample_rate = 0;
29+
int num_channels = 0;
30+
std::vector<int16_t> samples;
31+
};
32+
33+
// Helper that loads 16-bit PCM WAV (16-bit, PCM only)
34+
WavData loadWav16(const std::string &path);
35+
36+
using namespace livekit;
37+
38+
class WavAudioSource {
39+
public:
40+
// loop_enabled: whether to loop when reaching the end
41+
WavAudioSource(const std::string &path, int expected_sample_rate,
42+
int expected_channels, bool loop_enabled = true);
43+
44+
// Fill a frame with the next chunk of audio.
45+
void fillFrame(AudioFrame &frame);
46+
47+
private:
48+
void initLoopDelayCounter();
49+
50+
WavData wav_;
51+
std::size_t playhead_ = 0;
52+
53+
const bool loop_enabled_;
54+
int sample_rate_;
55+
int num_channels_;
56+
};

0 commit comments

Comments
 (0)