Skip to content

Commit e59ffc3

Browse files
committed
gather-write directly to asio when using sync mode and pushing a sample of buffers.
1 parent 89252cd commit e59ffc3

File tree

8 files changed

+143
-52
lines changed

8 files changed

+143
-52
lines changed

examples/ReceiveDataInChunks.cpp

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -8,18 +8,18 @@
88
int main(int argc, char **argv) {
99
std::cout << "ReceiveDataInChunks" << std::endl;
1010
std::cout << "ReceiveDataInChunks StreamName max_buflen flush" << std::endl;
11-
std::cout << "- max_buffered -- duration in msec to buffer" << std::endl;
11+
std::cout << "- max_buffered -- duration in sec (or x100 samples if samplerate is 0) to buffer in the receiver" << std::endl;
1212
std::cout << "- flush -- set non-zero to flush data instead of pulling; useful for testing throughput" << std::endl;
1313

1414
try {
1515

1616
std::string name{argc > 1 ? argv[1] : "MyAudioStream"};
17-
double max_buflen = argc > 2 ? std::stod(argv[2]) : 360.;
17+
double max_buffered = argc > 2 ? std::stod(argv[2]) : 360.;
1818
bool flush = argc > 3;
1919
// resolve the stream of interest & make an inlet
20-
int32_t buf_samples = (int32_t)(max_buflen * 1000);
21-
lsl::stream_inlet inlet(lsl::resolve_stream("name", name).at(0), max_buflen,
22-
transp_bufsize_thousandths);
20+
lsl::stream_info inlet_info = lsl::resolve_stream("name", name).at(0);
21+
lsl::stream_inlet inlet(inlet_info,(int32_t)(max_buffered * 1000),
22+
0, true, transp_bufsize_thousandths);
2323

2424
// Use set_postprocessing to get the timestamps in a common base clock.
2525
// Do not use if this application will record timestamps to disk -- it is better to

examples/SendDataInChunks.cpp

Lines changed: 31 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -104,6 +104,7 @@ int main(int argc, char **argv) {
104104
int32_t chunk_rate = argc > 6 ? std::stol(argv[6]) : 10; // Chunks per second.
105105
bool nodata = argc > 7;
106106
bool do_sync = argc > 8 ? (bool)std::stol(argv[8]) : true;
107+
bool b_discontig = true && do_sync; // Set true to test gather-write operations.
107108

108109
int32_t chunk_samples = samplingrate > 0 ? std::max((samplingrate / chunk_rate), 1) : 100; // Samples per chunk.
109110
int32_t chunk_duration = 1000 / chunk_rate; // Milliseconds per chunk
@@ -127,32 +128,55 @@ int main(int argc, char **argv) {
127128
std::cout << "Stream UID: " << info.uid() << std::endl;
128129

129130
// Create a connection to our device.
130-
fake_device my_device(n_channels, (float)samplingrate);
131+
int dev_chans = b_discontig ? n_channels + 1 : n_channels;
132+
fake_device my_device(dev_chans, (float)samplingrate);
131133

132134
// Prepare buffer to get data from 'device'.
133135
// The buffer should be larger than you think you need. Here we make it 4x as large.
134-
std::vector<int16_t> chunk_buffer(4 * chunk_samples * n_channels);
135-
std::fill(chunk_buffer.begin(), chunk_buffer.end(), 0);
136+
std::vector<int16_t> dev_buffer(4 * chunk_samples * dev_chans);
137+
std::fill(dev_buffer.begin(), dev_buffer.end(), 0);
136138

137139
std::cout << "Now sending data..." << std::endl;
138140

139141
// Your device might have its own timer. Or you can decide how often to poll
140142
// your device, as we do here.
141-
auto next_chunk_time = std::chrono::high_resolution_clock::now();
143+
auto t_start = std::chrono::high_resolution_clock::now();
144+
auto next_chunk_time = t_start;
145+
int64_t samples_pushed = 0;
142146
for (unsigned c = 0;; c++) {
143147
// wait a bit
144148
next_chunk_time += std::chrono::milliseconds(chunk_duration);
145149
std::this_thread::sleep_until(next_chunk_time);
146150

147151
// Get data from device
148-
std::size_t returned_samples = my_device.get_data(chunk_buffer, nodata);
152+
std::size_t returned_samples = my_device.get_data(dev_buffer, nodata);
149153

150154
// send it to the outlet. push_chunk_multiplexed is one of the more complicated approaches.
151155
// other push_chunk methods are easier but slightly slower.
152156
double ts = lsl::local_clock();
153-
outlet.push_chunk_multiplexed(chunk_buffer.data(), returned_samples * n_channels, ts, true);
157+
if (!b_discontig) {
158+
outlet.push_chunk_multiplexed(dev_buffer.data(), returned_samples * n_channels, ts, true);
159+
} else {
160+
// The data on the device are discontiguous. Grab a buffer to the first channel,
161+
// skip the second, then a buffer to the reset of the samples.
162+
int16_t *discont[2];
163+
uint32_t bytes[2];
164+
for (int samp_ix = 0; samp_ix < returned_samples; samp_ix++) {
165+
discont[0] = &dev_buffer[dev_chans * samp_ix];
166+
bytes[0] = 1*sizeof(int16_t);
167+
discont[1] = &dev_buffer[dev_chans * samp_ix + 2];
168+
bytes[1] = (dev_chans - 2)*sizeof(int16_t);
169+
bool pushthrough = samp_ix == (returned_samples - 1);
170+
outlet.push_numeric_bufs(
171+
reinterpret_cast<const char **>(const_cast<const int16_t**>(&discont[0])),
172+
&bytes[0], 2, pushthrough ? ts : LSL_DEDUCED_TIMESTAMP, pushthrough);
173+
samples_pushed++;
174+
}
175+
// std::cout << "Pushed " << returned_samples << " samples." << std::endl;
176+
// std::chrono::seconds elapsed = std::chrono::duration_cast<std::chrono::seconds>(next_chunk_time - t_start);
177+
// std::cout << double(samples_pushed) / elapsed.count() << std::endl;
178+
}
154179
}
155-
156180
} catch (std::exception &e) { std::cerr << "Got an exception: " << e.what() << std::endl; }
157181
std::cout << "Press any key to exit. " << std::endl;
158182
std::cin.get();

include/lsl/outlet.h

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -99,7 +99,7 @@ extern LIBLSL_C_API int32_t lsl_push_sample_vtp(lsl_outlet out, const void *data
9999
* @see lsl_push_sample_ftp
100100
* @param out The lsl_outlet object through which to push the data.
101101
* @param data A pointer to values to push. The number of values pointed to must be no less than the number of channels in the sample.
102-
* @param lengths A pointer the number of elements to push for each channel (string lengths).
102+
* @param lengths A pointer the number of elements to push for each channel (string lengths, or number of bytes).
103103
*/
104104
extern LIBLSL_C_API int32_t lsl_push_sample_buf(lsl_outlet out, const char **data, const uint32_t *lengths);
105105
/** @copydoc lsl_push_sample_buf
@@ -108,6 +108,11 @@ extern LIBLSL_C_API int32_t lsl_push_sample_buft(lsl_outlet out, const char **da
108108
/** @copydoc lsl_push_sample_buft
109109
* @param pushthrough @see lsl_push_sample_ftp */
110110
extern LIBLSL_C_API int32_t lsl_push_sample_buftp(lsl_outlet out, const char **data, const uint32_t *lengths, double timestamp, int32_t pushthrough);
111+
/** @copydoc lsl_push_sample_buftp
112+
* @param nbufs Number of values pointed to in `data` and number of items in `lengths` -- doesn't assume one buffer
113+
* per channel but each array in data must be longer than each item in lengths.
114+
*/
115+
extern LIBLSL_C_API int32_t lsl_push_sample_buftpn(lsl_outlet out, const char **data, const uint32_t *lengths, double timestamp, int32_t pushthrough, uint32_t nbufs);
111116

112117
/** Push a chunk of multiplexed samples into the outlet. One timestamp per sample is provided.
113118
*

include/lsl_cpp.h

Lines changed: 15 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -499,7 +499,7 @@ class stream_outlet {
499499
}
500500

501501
/** Push a pointer to raw numeric data as one sample into the outlet.
502-
* This is the lowest-level function; performns no checking whatsoever. Can not be used for
502+
* This is the lowest-level function; performs no checking whatsoever. Cannot be used for
503503
* variable-size / string-formatted channels.
504504
* @param sample A pointer to the raw sample data to push.
505505
* @param timestamp Optionally the capture time of the sample, in agreement with local_clock();
@@ -512,6 +512,20 @@ class stream_outlet {
512512
lsl_push_sample_vtp(obj.get(), (sample), timestamp, pushthrough);
513513
}
514514

515+
/**
516+
* Push a pointer to an array of buffers of variable size as one sample into the outlet.
517+
*
518+
* @param bufs A pointer to an array of data buffers.
519+
* @param bytes An array of sizes (number of bytes) of buffers in bufs.
520+
* @param nbufs Total number of buffers.
521+
* @param timestamp Optionally the capture time of the sample, in agreement with local_clock();
522+
* @param pushthrough Whether to push the sample through to the receivers immediately instead of
523+
* concatenating with subsequent samples.
524+
*/
525+
void push_numeric_bufs(const char **bufs, uint32_t *bytes, uint32_t nbufs, double timestamp = 0.0, bool pushthrough = true) {
526+
lsl_push_sample_buftpn(obj.get(), bufs, bytes, timestamp, pushthrough, nbufs);
527+
}
528+
515529

516530
// ===================================================
517531
// === Pushing an chunk of samples into the outlet ===

src/lsl_outlet_c.cpp

Lines changed: 24 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -28,7 +28,7 @@ LIBLSL_C_API lsl_outlet lsl_create_outlet_ex(
2828
buf_samples /= 1000;
2929
buf_samples = (buf_samples > 0) ? buf_samples : 1;
3030
return create_object_noexcept<stream_outlet_impl>(
31-
*info, chunk_size, buf_samples);
31+
*info, chunk_size, buf_samples, flags);
3232
}
3333

3434
LIBLSL_C_API lsl_outlet lsl_create_outlet(
@@ -171,14 +171,32 @@ LIBLSL_C_API int32_t lsl_push_sample_buft(
171171

172172
LIBLSL_C_API int32_t lsl_push_sample_buftp(lsl_outlet out, const char **data,
173173
const uint32_t *lengths, double timestamp, int32_t pushthrough) {
174+
stream_outlet_impl *outimpl = out;
175+
return lsl_push_sample_buftpn(out, data, lengths, timestamp, pushthrough,
176+
(uint32_t)outimpl->info().channel_count());
177+
}
178+
179+
LIBLSL_C_API int32_t lsl_push_sample_buftpn(lsl_outlet out, const char **data,
180+
const uint32_t *lengths, double timestamp, int32_t pushthrough, uint32_t nbufs) {
181+
stream_outlet_impl *outimpl = out;
174182
try {
175-
stream_outlet_impl *outimpl = out;
176-
std::vector<std::string> tmp;
177-
for (uint32_t k = 0; k < (uint32_t)outimpl->info().channel_count(); k++)
178-
tmp.emplace_back(data[k], lengths[k]);
179-
return outimpl->push_sample_noexcept(&tmp[0], timestamp, pushthrough);
183+
if (outimpl->is_sync_blocking()) {
184+
// Convert input to a vector of asio buffers
185+
std::vector<asio::const_buffer> buffs;
186+
for (auto buf_ix = 0; buf_ix < nbufs; buf_ix++) {
187+
buffs.push_back(asio::buffer(data[buf_ix], lengths[buf_ix]));
188+
}
189+
return outimpl->push_sample_gather(buffs, timestamp, pushthrough);
190+
} else {
191+
std::vector<std::string> tmp;
192+
for (uint32_t k = 0; k < nbufs; k++)
193+
tmp.emplace_back(data[k], lengths[k]);
194+
return outimpl->push_sample_noexcept(&tmp[0], timestamp, pushthrough);
195+
}
180196
} catch (std::exception &e) {
181197
LOG_F(WARNING, "Unexpected error during push_sample: %s", e.what());
198+
if (!outimpl->is_sync_blocking() && outimpl->info().channel_format() != cft_string)
199+
LOG_F(ERROR, "lsl_push_sample_buftpn only compatible with string type or when outlet is using sync writes.");
182200
return lsl_internal_error;
183201
}
184202
}

src/stream_outlet_impl.cpp

Lines changed: 31 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -23,7 +23,7 @@ stream_outlet_impl::stream_outlet_impl(
2323
send_buffer_(std::make_shared<send_buffer>(max_capacity)),
2424
do_sync_(flags & transp_sync_blocking) {
2525

26-
if ((info.channel_format() == cft_string) && (flags & transp_sync_blocking)) {
26+
if ((info.channel_format() == cft_string) && do_sync_) {
2727
LOG_F(WARNING, "sync push not supported for string-formatted streams. Reverting to async.");
2828
do_sync_ = false;
2929
}
@@ -154,20 +154,8 @@ void stream_outlet_impl::push_numeric_raw(const void *data, double timestamp, bo
154154
smp->assign_untyped(data); // Note: Makes a copy!
155155
send_buffer_->push_sample(smp);
156156
} else {
157-
if (timestamp == DEDUCED_TIMESTAMP) {
158-
sync_buffs_.push_back(asio::buffer(&TAG_DEDUCED_TIMESTAMP, 1));
159-
} else {
160-
sync_buffs_.push_back(asio::buffer(&TAG_TRANSMITTED_TIMESTAMP, 1));
161-
sync_buffs_.push_back(asio::buffer(&timestamp, sizeof(timestamp)));
162-
}
163-
sync_buffs_.push_back(asio::buffer(data, smp->datasize()));
164-
if (pushthrough) {
165-
for (auto &tcp_server : tcp_servers_)
166-
tcp_server->write_all_blocking(sync_buffs_);
167-
sync_buffs_.clear();
168-
}
157+
enqueue_sync(asio::buffer(data, smp->datasize()), timestamp, pushthrough);
169158
}
170-
171159
}
172160

173161
bool stream_outlet_impl::have_consumers() { return send_buffer_->have_consumers(); }
@@ -176,6 +164,28 @@ bool stream_outlet_impl::wait_for_consumers(double timeout) {
176164
return send_buffer_->wait_for_consumers(timeout);
177165
}
178166

167+
void stream_outlet_impl::push_timestamp_sync(double timestamp) {
168+
if (timestamp == DEDUCED_TIMESTAMP) {
169+
sync_buffs_.emplace_back(asio::buffer(&TAG_DEDUCED_TIMESTAMP, 1));
170+
} else {
171+
sync_buffs_.emplace_back(asio::buffer(&TAG_TRANSMITTED_TIMESTAMP, 1));
172+
sync_buffs_.emplace_back(asio::buffer(&timestamp, sizeof(timestamp)));
173+
}
174+
}
175+
176+
void stream_outlet_impl::pushthrough_sync() {
177+
// LOG_F(INFO, "Pushing %u buffers.", sync_buffs_.size());
178+
for (auto &tcp_server : tcp_servers_)
179+
tcp_server->write_all_blocking(sync_buffs_);
180+
sync_buffs_.clear();
181+
}
182+
183+
void stream_outlet_impl::enqueue_sync(asio::const_buffer buff, double timestamp, bool pushthrough) {
184+
push_timestamp_sync(timestamp);
185+
sync_buffs_.push_back(buff);
186+
if (pushthrough) pushthrough_sync();
187+
}
188+
179189
template <class T>
180190
void stream_outlet_impl::enqueue(const T *data, double timestamp, bool pushthrough) {
181191
if (lsl::api_config::get_instance()->force_default_timestamps()) timestamp = 0.0;
@@ -185,18 +195,7 @@ void stream_outlet_impl::enqueue(const T *data, double timestamp, bool pushthrou
185195
smp->assign_typed(data);
186196
send_buffer_->push_sample(smp);
187197
} else {
188-
if (timestamp == DEDUCED_TIMESTAMP) {
189-
sync_buffs_.push_back(asio::buffer(&TAG_DEDUCED_TIMESTAMP, 1));
190-
} else {
191-
sync_buffs_.push_back(asio::buffer(&TAG_TRANSMITTED_TIMESTAMP, 1));
192-
sync_buffs_.push_back(asio::buffer(&timestamp, sizeof(timestamp)));
193-
}
194-
sync_buffs_.push_back(asio::buffer(data, smp->datasize()));
195-
if (pushthrough) {
196-
for (auto &tcp_server : tcp_servers_)
197-
tcp_server->write_all_blocking(sync_buffs_);
198-
sync_buffs_.clear();
199-
}
198+
enqueue_sync(asio::buffer(data, smp->datasize()), timestamp, pushthrough);
200199
}
201200
}
202201

@@ -208,4 +207,10 @@ template void stream_outlet_impl::enqueue<float>(const float *data, double, bool
208207
template void stream_outlet_impl::enqueue<double>(const double *data, double, bool);
209208
template void stream_outlet_impl::enqueue<std::string>(const std::string *data, double, bool);
210209

210+
void stream_outlet_impl::enqueue_sync_multi(std::vector<asio::const_buffer> buffs, double timestamp, bool pushthrough) {
211+
push_timestamp_sync(timestamp);
212+
sync_buffs_.insert( sync_buffs_.end(), buffs.begin(), buffs.end() );
213+
if (pushthrough) pushthrough_sync();
214+
}
215+
211216
} // namespace lsl

src/stream_outlet_impl.h

Lines changed: 23 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -137,7 +137,10 @@ class stream_outlet_impl {
137137
void push_sample(const std::string *data, double timestamp = 0.0, bool pushthrough = true) {
138138
enqueue(data, timestamp, pushthrough);
139139
}
140-
140+
lsl_error_code_t push_sample_gather(std::vector<asio::const_buffer> buffs, double timestamp = 0.0, bool pushthrough = true) {
141+
enqueue_sync_multi(buffs, timestamp, pushthrough);
142+
return lsl_no_error;
143+
}
141144

142145
template <typename T>
143146
inline lsl_error_code_t push_sample_noexcept(
@@ -295,13 +298,32 @@ class stream_outlet_impl {
295298
/// Wait until some consumer shows up.
296299
bool wait_for_consumers(double timeout = FOREVER);
297300

301+
/// If the outlet is intended to use synchronous blocking transfers
302+
bool is_sync_blocking() { return do_sync_; };
303+
298304
private:
299305
/// Instantiate a new server stack.
300306
void instantiate_stack(tcp tcp_protocol, udp udp_protocol);
301307

302308
/// Allocate and enqueue a new sample into the send buffer.
303309
template <class T> void enqueue(const T *data, double timestamp, bool pushthrough);
304310

311+
/// Append the appropriate timestamp tag and optionally timestamp onto sync_buffs_ for a single timestamp.
312+
void push_timestamp_sync(double timestamp);
313+
314+
/// push sync_buffs_ through each tcp server.
315+
void pushthrough_sync();
316+
317+
/// Append a single timestamp and single buffer to sync_buffs and optionally pushthrough the server.
318+
void enqueue_sync(asio::const_buffer buff, double timestamp, bool pushthrough);
319+
320+
/**
321+
* Append a single timestamp and multiple within-sample buffers to sync_buffs_.
322+
* This is useful when a sample is discontiguous in memory. It makes no assumptions about how
323+
* many channels are included in each buffer.
324+
*/
325+
void enqueue_sync_multi(std::vector<asio::const_buffer> buffs, double timestamp, bool pushthrough);
326+
305327
/**
306328
* Check whether some given number of channels matches the stream's channel_count.
307329
* Throws an error if not.

src/tcp_server.cpp

Lines changed: 8 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -226,12 +226,15 @@ void tcp_server::handle_accept_outcome(std::shared_ptr<client_session> newsessio
226226

227227
void tcp_server::write_all_blocking(std::vector<asio::const_buffer> buffs) {
228228
std::lock_guard<std::recursive_mutex> lock(inflight_mut_);
229-
std::size_t bytes_sent;
230-
asio::error_code ec;
229+
std::size_t bytes_sent = 0;
231230
for (const auto &x : inflight_ready_) {
232231
if (x.second && x.first->is_open()) {
233-
bytes_sent = x.first->send(buffs, 0, ec);
234-
if (ec) {
232+
try {
233+
// I couldn't figure out how to get the correct overload while providing
234+
// error_code& ec to the write function. So we use try-catch instead.
235+
bytes_sent = asio::write(*x.first, buffs);
236+
} catch (const asio::system_error &err) { // std::exception &e
237+
asio::error_code ec = err.code();
235238
switch(ec.value()) {
236239
case asio::error::broken_pipe:
237240
case asio::error::connection_reset:
@@ -243,7 +246,7 @@ void tcp_server::write_all_blocking(std::vector<asio::const_buffer> buffs) {
243246
// We leave it up to the client_session destructor to remove the socket.
244247
break;
245248
default:
246-
LOG_F(WARNING, "Unhandled write_all_blocking error: %s.", ec.message().c_str());
249+
LOG_F(WARNING, "Unhandled write_all_blocking error: %s.", err.what());
247250
}
248251
}
249252
}

0 commit comments

Comments
 (0)