diff --git a/docs/integration-guide.md b/docs/integration-guide.md index d352e8dc..e18059f7 100644 --- a/docs/integration-guide.md +++ b/docs/integration-guide.md @@ -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); @@ -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. diff --git a/examples/common/portaudio_sink.cpp b/examples/common/portaudio_sink.cpp index 10e73c50..cdc5f692 100644 --- a/examples/common/portaudio_sink.cpp +++ b/examples/common/portaudio_sink.cpp @@ -14,6 +14,8 @@ #include "portaudio_sink.h" +#include "sendspin/client.h" + #include #include #include @@ -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::steady_clock::now().time_since_epoch()) - .count(); + int64_t now_us = SendspinClient::now_us(); auto* self = static_cast(user_data); size_t bytes_requested = frame_count * self->bytes_per_frame_; @@ -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(bytes_read / self->bytes_per_frame_); diff --git a/examples/source_client/main.cpp b/examples/source_client/main.cpp index 852bb9f3..12a5d904 100644 --- a/examples/source_client/main.cpp +++ b/examples/source_client/main.cpp @@ -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::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(std::round(age_us)); } diff --git a/examples/tui_client/main.cpp b/examples/tui_client/main.cpp index c28ffd63..44f9ed90 100644 --- a/examples/tui_client/main.cpp +++ b/examples/tui_client/main.cpp @@ -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::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; @@ -929,14 +922,14 @@ int main(int argc, char* argv[]) { void on_beat(int64_t /*client_timestamp*/, bool /*downbeat*/) override { std::lock_guard 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 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 } @@ -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 lock(state.mutex); // Smooth decay: display values chase target values each tick. diff --git a/include/sendspin/client.h b/include/sendspin/client.h index 06a6f9c6..665472d7 100644 --- a/include/sendspin/client.h +++ b/include/sendspin/client.h @@ -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 // ======================================== diff --git a/src/client.cpp b/src/client.cpp index 04c8545e..ae2ce5a9 100644 --- a/src/client.cpp +++ b/src/client.cpp @@ -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 @@ -321,6 +322,10 @@ LogLevel SendspinClient::get_log_level() { return static_cast(platform_get_log_level()); } +int64_t SendspinClient::now_us() { + return platform_time_us(); +} + // ============================================================================ // Lifecycle // ============================================================================ diff --git a/src/platform/time.h b/src/platform/time.h index df16896d..3d9ae353 100644 --- a/src/platform/time.h +++ b/src/platform/time.h @@ -50,14 +50,23 @@ inline TickType_t platform_ms_to_ticks(uint32_t timeout_ms) { #else // Host #include +#include 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(now).count(); +#else auto now = std::chrono::steady_clock::now(); return std::chrono::duration_cast(now.time_since_epoch()).count(); +#endif } } // namespace sendspin