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
2 changes: 1 addition & 1 deletion CMakeLists.txt
Original file line number Diff line number Diff line change
Expand Up @@ -31,7 +31,7 @@ endif()

# sendspin-cpp

set(SENDSPIN_GIT_TAG "v0.9.5" CACHE STRING
set(SENDSPIN_GIT_TAG "90cbd80864bf763fb6a4100f23b1ad2e128c8a75" CACHE STRING
"sendspin-cpp git tag or commit to build against")

# player, metadata, controller (group transport verbs) and source; no FORCE, so -D overrides still win.
Expand Down
2 changes: 1 addition & 1 deletion docs/ROADMAP.md
Original file line number Diff line number Diff line change
Expand Up @@ -34,7 +34,7 @@ passing such a URL today.
### 3. Upstream gaps in sendspin-cpp

Each needs a library change; nothing this repo can call fixes them. Re-check on every
`SENDSPIN_GIT_TAG` bump (currently v0.9.5).
`SENDSPIN_GIT_TAG` bump (currently `90cbd808`, past v0.9.5).

- **No log sink hook.** The host `SS_LOG*` macros are bare `fprintf(stderr, ...)` gated on one
global level. So library lines carry no timestamp under `-f`, cannot be filtered per tag
Expand Down
10 changes: 3 additions & 7 deletions src/alsa_sink.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,7 @@
namespace sendspin_cli {

using sendspin::LogLevel;
using sendspin::SendspinClient;

static constexpr const char* LOG_TAG = LOG_TAG_AUDIO;

Expand Down Expand Up @@ -79,12 +80,6 @@ void alsa_error_handler(const char* file, int line, const char* function, int er
(err != 0) ? snd_strerror(err) : "");
}

int64_t now_us() {
return std::chrono::duration_cast<std::chrono::microseconds>(
std::chrono::steady_clock::now().time_since_epoch())
.count();
}

/// Maps the stream's bit depth onto the interleaved little-endian PCM format ALSA wants.
bool alsa_format_for(uint8_t bits_per_sample, snd_pcm_format_t& format) {
for (size_t i = 0; i < PROBE_BIT_DEPTHS.size(); ++i) {
Expand Down Expand Up @@ -632,7 +627,8 @@ size_t AlsaAudioSink::write(const uint8_t* data, size_t length, uint32_t timeout
if (frames_done > 0 && this->pcm_ != nullptr && this->rate_ > 0) {
snd_pcm_sframes_t delay = 0;
if (snd_pcm_delay(this->pcm_, &delay) == 0 && delay >= 0) {
finish_us = now_us() + ((static_cast<int64_t>(delay) * 1000000) / this->rate_);
finish_us = SendspinClient::now_us() +
((static_cast<int64_t>(delay) * 1000000) / this->rate_);
have_timing = true;
// Taken with the timestamp under the same lock, so nothing reports between them.
gap_frames = this->recovery_.take_discarded_frames();
Expand Down
12 changes: 4 additions & 8 deletions src/alsa_source.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -17,13 +17,15 @@
#include "alsa_sink.h"
#include "log.h"

#include <chrono>
#include <sendspin/client.h>

#include <utility>
#include <vector>

namespace sendspin_cli {

using sendspin::LogLevel;
using sendspin::SendspinClient;

static constexpr const char* LOG_TAG = LOG_TAG_AUDIO;

Expand All @@ -35,12 +37,6 @@ constexpr unsigned int PERIOD_TIME_US = 20000;

constexpr int64_t US_PER_S = 1000000;

int64_t now_us() {
return std::chrono::duration_cast<std::chrono::microseconds>(
std::chrono::steady_clock::now().time_since_epoch())
.count();
}

/// ALSA's packed little-endian format for a capture bit depth.
snd_pcm_format_t alsa_format_for(uint8_t bit_depth) {
switch (bit_depth) {
Expand Down Expand Up @@ -247,7 +243,7 @@ int AlsaAudioSource::read(uint8_t* data, size_t length, uint32_t timeout_ms,

// Frames queued ahead of this read: the oldest is the first sample returned.
snd_pcm_sframes_t delay = 0;
const int64_t now = now_us();
const int64_t now = SendspinClient::now_us();
const bool have_delay = snd_pcm_delay(this->pcm_, &delay) == 0 && delay >= 0;

const snd_pcm_sframes_t frames =
Expand Down
11 changes: 4 additions & 7 deletions src/coreaudio_sink.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,8 @@
#include "log.h"
#include "pcm_volume.h"

#include <sendspin/client.h>

#include <mach/mach_time.h>

#include <algorithm>
Expand All @@ -32,6 +34,7 @@
namespace sendspin_cli {

using sendspin::LogLevel;
using sendspin::SendspinClient;

static constexpr const char* LOG_TAG = LOG_TAG_AUDIO;

Expand All @@ -47,12 +50,6 @@ constexpr size_t MIN_RING_FRAMES = 1024;
constexpr std::array<const char*, PROBE_BIT_DEPTHS.size()> PROBE_FORMAT_NAMES{"SInt8", "SInt16",
"SInt24", "SInt32"};

int64_t now_us() {
return std::chrono::duration_cast<std::chrono::microseconds>(
std::chrono::steady_clock::now().time_since_epoch())
.count();
}

/// Asks the unit whether it will take this exact format on its input scope; the AU converts on
/// top of what the device itself does, so this is what a stream would really get.
bool unit_accepts(AudioUnit unit, uint32_t sample_rate, uint8_t channels, uint8_t bits) {
Expand Down Expand Up @@ -863,7 +860,7 @@ OSStatus CoreAudioSink::property_listener(AudioObjectID /*object*/, UInt32 count
OSStatus CoreAudioSink::render_callback(void* user_data, AudioUnitRenderActionFlags* /*flags*/,
const AudioTimeStamp* timestamp, UInt32 /*bus*/,
UInt32 frames, AudioBufferList* data) {
const int64_t entered_us = now_us();
const int64_t entered_us = SendspinClient::now_us();
const uint64_t entered_host = mach_absolute_time();

auto* self = static_cast<CoreAudioSink*>(user_data);
Expand Down
11 changes: 4 additions & 7 deletions src/coreaudio_source.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,8 @@
#include "coreaudio_permission.h"
#include "log.h"

#include <sendspin/client.h>

#include <mach/mach_time.h>

#include <algorithm>
Expand All @@ -28,6 +30,7 @@
namespace sendspin_cli {

using sendspin::LogLevel;
using sendspin::SendspinClient;

static constexpr const char* LOG_TAG = LOG_TAG_AUDIO;

Expand All @@ -48,12 +51,6 @@ constexpr uint64_t TAP_SILENCE_WARN_S = 10;
constexpr UInt32 INPUT_BUS = 1;
constexpr UInt32 OUTPUT_BUS = 0;

int64_t now_us() {
return std::chrono::duration_cast<std::chrono::microseconds>(
std::chrono::steady_clock::now().time_since_epoch())
.count();
}

/// The device's own rate as a whole number of Hz; 0 when it cannot be read.
uint32_t device_rate(AudioDeviceID device) {
return static_cast<uint32_t>(std::llround(ca_nominal_sample_rate(device)));
Expand Down Expand Up @@ -539,7 +536,7 @@ OSStatus CoreAudioSource::property_listener(AudioObjectID /*object*/, UInt32 cou
OSStatus CoreAudioSource::input_callback(void* user_data, AudioUnitRenderActionFlags* flags,
const AudioTimeStamp* timestamp, UInt32 bus, UInt32 frames,
AudioBufferList* /*data*/) {
const int64_t entered_us = now_us();
const int64_t entered_us = SendspinClient::now_us();
const uint64_t entered_host = mach_absolute_time();

auto* self = static_cast<CoreAudioSource*>(user_data);
Expand Down
10 changes: 2 additions & 8 deletions src/null_sink.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -20,12 +20,12 @@

#include <algorithm>
#include <array>
#include <chrono>
#include <cstdio>

namespace sendspin_cli {

using sendspin::LogLevel;
using sendspin::SendspinClient;

static constexpr const char* LOG_TAG = LOG_TAG_AUDIO;

Expand All @@ -37,12 +37,6 @@ constexpr std::array<uint8_t, SILENCE_CHUNK> SILENCE{};

constexpr int64_t US_PER_S = 1000000;

int64_t now_us() {
return std::chrono::duration_cast<std::chrono::microseconds>(
std::chrono::steady_clock::now().time_since_epoch())
.count();
}

/// Rounds `bytes` down to whole frames; a frame size of 0 means no stream, so no alignment.
size_t align_to_frame(size_t bytes, size_t bytes_per_frame) {
if (bytes_per_frame == 0) {
Expand Down Expand Up @@ -157,7 +151,7 @@ void NullAudioSink::report_played(size_t bytes) {
return;
}
// Never behind real time, so a stream that paused resumes from now rather than the past.
this->playhead_us_ = std::max(this->playhead_us_, now_us()) +
this->playhead_us_ = std::max(this->playhead_us_, SendspinClient::now_us()) +
(static_cast<int64_t>(frames) * US_PER_S) / static_cast<int64_t>(rate);
this->on_frames_played(frames, this->playhead_us_);
}
Expand Down
19 changes: 9 additions & 10 deletions src/null_source.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,8 @@

#include "null_source.h"

#include <sendspin/client.h>

#include <algorithm>
#include <chrono>
#include <cmath>
Expand All @@ -22,19 +24,15 @@

namespace sendspin_cli {

using sendspin::SendspinClient;

namespace {

constexpr double TONE_HZ = 440.0;
constexpr double TONE_AMPLITUDE = 0.25;
constexpr double TWO_PI = 6.283185307179586;
constexpr int64_t US_PER_S = 1000000;

int64_t now_us() {
return std::chrono::duration_cast<std::chrono::microseconds>(
std::chrono::steady_clock::now().time_since_epoch())
.count();
}

} // namespace

NullAudioSource::NullAudioSource(NullSourceSignal signal) : signal_(signal) {}
Expand All @@ -60,7 +58,7 @@ bool NullAudioSource::open(const StreamFormat& format) {
this->format_ = format;
this->bytes_per_frame_ =
static_cast<size_t>(format.channels) * (static_cast<size_t>(format.bit_depth) / 8U);
this->start_us_ = now_us();
this->start_us_ = SendspinClient::now_us();
this->frames_ = 0;
return this->bytes_per_frame_ != 0 && format.sample_rate != 0;
}
Expand All @@ -77,12 +75,13 @@ int NullAudioSource::read(uint8_t* data, size_t length, uint32_t timeout_ms,
};

// Wait for the whole read to have "happened", as a device would.
const int64_t wait_us = std::min<int64_t>(time_of(this->frames_ + wanted) - now_us(),
static_cast<int64_t>(timeout_ms) * 1000);
const int64_t wait_us =
std::min<int64_t>(time_of(this->frames_ + wanted) - SendspinClient::now_us(),
static_cast<int64_t>(timeout_ms) * 1000);
if (wait_us > 0) {
std::this_thread::sleep_for(std::chrono::microseconds(wait_us));
}
const int64_t elapsed_frames = (now_us() - this->start_us_) * rate / US_PER_S;
const int64_t elapsed_frames = (SendspinClient::now_us() - this->start_us_) * rate / US_PER_S;
const int64_t due = elapsed_frames - static_cast<int64_t>(this->frames_);
if (due <= 0) {
return 0;
Expand Down
11 changes: 4 additions & 7 deletions src/pipewire_sink.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,8 @@
#include "log.h"
#include "pcm_volume.h"

#include <sendspin/client.h>

#include <spa/param/audio/format-utils.h>
#include <spa/pod/builder.h>
#include <spa/support/log.h>
Expand All @@ -34,6 +36,7 @@
namespace sendspin_cli {

using sendspin::LogLevel;
using sendspin::SendspinClient;

static constexpr const char* LOG_TAG = LOG_TAG_AUDIO;

Expand All @@ -42,12 +45,6 @@ namespace {
/// The name this stream carries in the graph.
constexpr const char* PIPEWIRE_APP_NAME = "sendspin-cli";

int64_t now_us() {
return std::chrono::duration_cast<std::chrono::microseconds>(
std::chrono::steady_clock::now().time_since_epoch())
.count();
}

/// SPA format per PROBE_BIT_DEPTHS entry, exactly as the player emits; PipeWire converts.
constexpr std::array<spa_audio_format, PROBE_BIT_DEPTHS.size()> PROBE_FORMATS{
SPA_AUDIO_FORMAT_S8, SPA_AUDIO_FORMAT_S16_LE, SPA_AUDIO_FORMAT_S24_LE, SPA_AUDIO_FORMAT_S32_LE};
Expand Down Expand Up @@ -588,7 +585,7 @@ void PipeWireSink::stream_state_cb(void* userdata, enum pw_stream_state /*old*/,

void PipeWireSink::stream_process_cb(void* userdata) {
auto* self = static_cast<PipeWireSink*>(userdata);
const int64_t entered_us = now_us();
const int64_t entered_us = SendspinClient::now_us();

// Safe unlocked: pw_stream_disconnect() waits for this callback to return.
pw_buffer* buffer = pw_stream_dequeue_buffer(self->stream_);
Expand Down
17 changes: 15 additions & 2 deletions src/pipewire_source.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -16,13 +16,16 @@

#include "log.h"

#include <sendspin/client.h>

#include <spa/param/audio/format-utils.h>
#include <spa/pod/builder.h>
#include <spa/utils/result.h>

#include <algorithm>
#include <cerrno>
#include <chrono>
#include <ctime>
#include <initializer_list>
#include <optional>
#include <utility>
Expand All @@ -31,6 +34,7 @@
namespace sendspin_cli {

using sendspin::LogLevel;
using sendspin::SendspinClient;

static constexpr const char* LOG_TAG = LOG_TAG_AUDIO;

Expand All @@ -49,6 +53,13 @@ constexpr uint32_t RING_MS = 1000;

constexpr int64_t US_PER_S = 1000000;

/// CLOCK_MONOTONIC, the clock pw_time::now is on, in microseconds.
int64_t monotonic_us() {
timespec ts{};
clock_gettime(CLOCK_MONOTONIC, &ts);
return (static_cast<int64_t>(ts.tv_sec) * US_PER_S) + (ts.tv_nsec / 1000);
}

/// SPA's packed little-endian format for a capture bit depth.
spa_audio_format spa_format_for(uint8_t bit_depth) {
switch (bit_depth) {
Expand Down Expand Up @@ -340,8 +351,10 @@ void PipeWireAudioSource::stream_process_cb(void* userdata) {
static_cast<double>(time.rate.denom)) +
(static_cast<double>(time.buffered + frames) /
static_cast<double>(self->rate_));
// `now` is CLOCK_MONOTONIC, which is what steady_clock reads here.
const int64_t first_us = (time.now / 1000) - static_cast<int64_t>(age_s * 1e6);
// `now` is on CLOCK_MONOTONIC; its age there carries onto the client clock.
const int64_t graph_age_us = monotonic_us() - (time.now / 1000);
const int64_t first_us =
SendspinClient::now_us() - graph_age_us - static_cast<int64_t>(age_s * 1e6);
self->origin_us_.store(first_us - static_cast<int64_t>(self->frames_written_) *
US_PER_S / self->rate_);
}
Expand Down
11 changes: 4 additions & 7 deletions src/portaudio_sink.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,8 @@
#include "log.h"
#include "pcm_volume.h"

#include <sendspin/client.h>

#include <algorithm>
#include <array>
#include <chrono>
Expand All @@ -28,6 +30,7 @@
namespace sendspin_cli {

using sendspin::LogLevel;
using sendspin::SendspinClient;

static constexpr const char* LOG_TAG = LOG_TAG_AUDIO;

Expand All @@ -39,12 +42,6 @@ constexpr double RING_LATENCY_MULTIPLE = 3.0;
/// Absolute floor on the ring, for a device that reports no latency at all.
constexpr size_t MIN_RING_FRAMES = 1024;

int64_t now_us() {
return std::chrono::duration_cast<std::chrono::microseconds>(
std::chrono::steady_clock::now().time_since_epoch())
.count();
}

/// How -l names each PA_FORMATS entry.
constexpr std::array<const char*, PROBE_BIT_DEPTHS.size()> PROBE_FORMAT_NAMES{"paInt8", "paInt16",
"paInt24", "paInt32"};
Expand Down Expand Up @@ -705,7 +702,7 @@ void PortAudioSink::update_target_multiplier_() {
int PortAudioSink::pa_callback(const void* /*input*/, void* output, unsigned long frame_count,
const PaStreamCallbackTimeInfo* time_info,
PaStreamCallbackFlags /*status_flags*/, void* user_data) {
const int64_t entered_us = now_us();
const int64_t entered_us = SendspinClient::now_us();

auto* self = static_cast<PortAudioSink*>(user_data);
auto* out = static_cast<uint8_t*>(output);
Expand Down
Loading
Loading