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
4 changes: 2 additions & 2 deletions docs/integration-guide.md
Original file line number Diff line number Diff line change
Expand Up @@ -193,7 +193,7 @@ auto& source = client.add_source(source_config);

An invalid config (see [SourceRoleConfig](#sourceroleconfig)) leaves the role inert; a valid config whose buffers cannot be allocated fails `client.start()` instead. An `OPUS` role streams Opus only to a server whose `server/hello` lists it, and the same capture as PCM otherwise.

Feed the capture to `write_audio()` from one capture thread, with the capture time of each buffer's first sample on the client's clock (`std::chrono::steady_clock` on host, `esp_timer_get_time()` on ESP-IDF), or 0 to have the write stamped as ending now:
Feed the capture to `write_audio()` from one capture thread, with the capture time of each buffer's first sample on the client's clock (`SendspinClient::now_us()`), or 0 to have the write stamped as ending now:

```cpp
source.write_audio(pcm_bytes, len, capture_time_us);
Expand Down Expand Up @@ -276,7 +276,7 @@ player.notify_audio_played(frames_played, current_timestamp_us);
```

- `frames_played`: Number of audio frames (not bytes) just played
- `timestamp`: Client timestamp in microseconds when the audio will finish playing (e.g., from `std::chrono::steady_clock`)
- `timestamp`: Client timestamp in microseconds when the audio will finish playing (e.g., from `SendspinClient::now_us()`)

This method is thread-safe and is expected to be called from an audio callback thread.

Expand Down
8 changes: 4 additions & 4 deletions examples/common/portaudio_sink.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,8 @@

#include "portaudio_sink.h"

#include "sendspin/client.h"

#include <algorithm>
#include <chrono>
#include <cmath>
Expand Down Expand Up @@ -314,9 +316,7 @@ void PortAudioSink::clear() {
int PortAudioSink::pa_callback(const void* /*input*/, void* output, unsigned long frame_count,
const PaStreamCallbackTimeInfo* time_info,
PaStreamCallbackFlags /*status_flags*/, void* user_data) {
int64_t now_us = std::chrono::duration_cast<std::chrono::microseconds>(
std::chrono::steady_clock::now().time_since_epoch())
.count();
int64_t now_us = SendspinClient::now_us();

auto* self = static_cast<PortAudioSink*>(user_data);
size_t bytes_requested = frame_count * self->bytes_per_frame_;
Expand All @@ -339,7 +339,7 @@ int PortAudioSink::pa_callback(const void* /*input*/, void* output, unsigned lon
// outputBufferDacTime is when the first sample of this buffer will hit the DAC.
// We compute the finish time (when the last frame exits the DAC) by adding
// the frame duration. The offset from currentTime to DAC time is converted
// from PA's clock domain to esp_timer's clock domain.
// from PA's clock domain to the client clock.
if (bytes_read > 0 && self->on_frames_played) {
uint32_t frames_played = static_cast<uint32_t>(bytes_read / self->bytes_per_frame_);

Expand Down
4 changes: 1 addition & 3 deletions examples/source_client/main.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -273,9 +273,7 @@ class PortAudioCapture {
if (time_info != nullptr && time_info->currentTime > 0 &&
time_info->inputBufferAdcTime > 0 &&
time_info->inputBufferAdcTime <= time_info->currentTime) {
int64_t now_us = std::chrono::duration_cast<std::chrono::microseconds>(
std::chrono::steady_clock::now().time_since_epoch())
.count();
int64_t now_us = SendspinClient::now_us();
double age_us = (time_info->currentTime - time_info->inputBufferAdcTime) * 1000000.0;
capture_us = now_us - static_cast<int64_t>(std::round(age_us));
}
Expand Down
13 changes: 3 additions & 10 deletions examples/tui_client/main.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -90,13 +90,6 @@ using namespace sendspin;
static constexpr uint16_t DEFAULT_SENDSPIN_PORT = SendspinClientConfig::DEFAULT_SERVER_PORT;
static const char* SENDSPIN_PATH = "/sendspin";

// Big-endian helpers for binary visualizer data parsing
static int64_t now_us() {
return std::chrono::duration_cast<std::chrono::microseconds>(
std::chrono::steady_clock::now().time_since_epoch())
.count();
}

// Null audio write callback (used when PortAudio is not available)
static size_t null_audio_write(uint8_t*, size_t length, uint32_t) {
return length;
Expand Down Expand Up @@ -929,14 +922,14 @@ int main(int argc, char* argv[]) {

void on_beat(int64_t /*client_timestamp*/, bool /*downbeat*/) override {
std::lock_guard<std::mutex> lock(state.mutex);
int64_t current_us = now_us();
int64_t current_us = SendspinClient::now_us();
state.vis_beat = true;
state.vis_beat_expire_us = current_us + 100000; // 100ms flash
}

void on_peak(int64_t /*client_timestamp*/, uint8_t /*strength*/) override {
std::lock_guard<std::mutex> lock(state.mutex);
int64_t current_us = now_us();
int64_t current_us = SendspinClient::now_us();
state.vis_peak = true;
state.vis_peak_expire_us = current_us + 100000; // 100ms flash
}
Expand Down Expand Up @@ -1080,7 +1073,7 @@ int main(int argc, char* argv[]) {

// Smooth visualizer display values every tick (10ms), only when visible
if (vis_showing) {
int64_t current_us = now_us();
int64_t current_us = SendspinClient::now_us();
std::lock_guard<std::mutex> lock(state.mutex);

// Smooth decay: display values chase target values each tick.
Expand Down
3 changes: 3 additions & 0 deletions include/sendspin/client.h
Original file line number Diff line number Diff line change
Expand Up @@ -318,6 +318,9 @@ class SendspinClient {
/// @brief Returns the current log level (host builds only, INFO on ESP-IDF)
static LogLevel get_log_level();

/// @brief Returns the current time on the client clock in microseconds. Any thread.
static int64_t now_us();

// ========================================
// Lifecycle
// ========================================
Expand Down
5 changes: 5 additions & 0 deletions src/client.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,7 @@
#include "platform/logging.h"
#include "platform/memory.h"
#include "platform/network_info.h"
#include "platform/time.h"
#include "record_store.h"
#include "teardown_tracker.h"
#ifdef SENDSPIN_ENABLE_ARTWORK
Expand Down Expand Up @@ -321,6 +322,10 @@ LogLevel SendspinClient::get_log_level() {
return static_cast<LogLevel>(platform_get_log_level());
}

int64_t SendspinClient::now_us() {
return platform_time_us();
}

// ============================================================================
// Lifecycle
// ============================================================================
Expand Down
11 changes: 10 additions & 1 deletion src/platform/time.h
Original file line number Diff line number Diff line change
Expand Up @@ -50,14 +50,23 @@ inline TickType_t platform_ms_to_ticks(uint32_t timeout_ms) {
#else // Host

#include <chrono>
#include <ctime>

namespace sendspin {

/// @brief Returns monotonic time in microseconds
/// @return Microseconds elapsed since an arbitrary epoch (steady_clock).
/// @return Microseconds elapsed since an arbitrary epoch (CLOCK_MONOTONIC_RAW on Linux,
/// steady_clock elsewhere).
inline int64_t platform_time_us() {
#if defined(__linux__)
timespec ts{};
clock_gettime(CLOCK_MONOTONIC_RAW, &ts);
auto now = std::chrono::seconds(ts.tv_sec) + std::chrono::nanoseconds(ts.tv_nsec);
return std::chrono::duration_cast<std::chrono::microseconds>(now).count();
#else
auto now = std::chrono::steady_clock::now();
return std::chrono::duration_cast<std::chrono::microseconds>(now.time_since_epoch()).count();
#endif
}

} // namespace sendspin
Expand Down
Loading