• Home
  • Features
  • Pricing
  • Docs
  • Announcements
  • Sign In

ossia / libossia / 31275445545

08 Aug 2026 07:50PM UTC coverage: 50.828% (+2.5%) from 48.367%
31275445545

push

github

jcelerier
build: version Boost.Asio's global symbols on Windows

Since 1.91, Asio names its global symbols through BOOST_ASIO_VERSIONED_NAME,
which with no version namespace expands to the bare asio_ prefix - byte for
byte the names standalone Asio uses. Before 1.91 they carried a boost_asio_
prefix and could not collide.

Where Asio is compiled separately we emit strong definitions of them, so
anything else in the link carrying its own standalone Asio collides with us.
score's LSL addon bundles Asio and compiles asio/impl/src.hpp, and the two meet
as

  lld-link : error : duplicate symbol: asio_signal_handler
    >>> defined at ossia_x64.lib(unity_0_cxx.obj)
    >>> defined at lsl.lib(asio_objects.obj)

which is what stopped score's MSVC job linking as soon as our Boost floor moved
to 1.91.

Enabling the version namespace renames ours - asio_v103801_bdemo_signal_handler
- and leaves standalone Asio's alone. It holds together only with the
_WIN32_WINNT pin above: the namespace tag encodes the Asio configuration, so
without a fixed configuration it varies per translation unit and the duplicate
symbol merely becomes an undefined one. It also requires that nothing declare
an Asio type outside that namespace, which is what the libremidi update in the
previous commit settles.

Set both at the top level, so that the dependencies we build as subdirectories
are configured the same way, and PUBLIC on the ossia target, because this is
part of our ABI rather than only of how we are built. The namespace is an
inline namespace, so it is baked into the mangled name of every Asio type our
headers expose: resolve.cpp explicitly instantiates resolve_sync_v4 for
boost::asio::ip::udp and ::tcp, and a consumer that disagrees about the
namespace names a different specialisation and does not link.

Verified against Boost 1.91: libossia builds clean with VS2026, ossia_x64.lib
exports asio_v103801_bdemo_signal_handler and no longer contains the bare
asio_signal_handler at all; and sco... (continued)

29314 of 57673 relevant lines covered (50.83%)

182772.1 hits per line

Source File
Press 'n' to go to next uncovered line, 'b' for previous

98.87
/tests/Dataflow/SoundTest.cpp
1

2
#define _USE_MATH_DEFINES
3
#define DR_WAV_IMPLEMENTATION 1
4
#include <ossia/dataflow/execution_state.hpp>
5
#include <ossia/dataflow/nodes/sound_mmap.hpp>
6
#include <ossia/dataflow/nodes/sound_ref.hpp>
7

8
#include "include_catch.hpp"
9

10
#include <ossia/detail/thread.hpp>
11

12
#include <algorithm>
13
#include <cmath>
14
#include <cstdio>
15
#include <numeric>
16

17
TEST_CASE("test_sound_ref", "test_sound_ref")
1✔
18
{
19
  using namespace ossia;
20
  ossia::set_thread_pinned(ossia::thread_type::Ui, 0);
1✔
21
  nodes::sound_ref snd;
1✔
22
  snd.set_sound(ossia::audio_array{{0.1, 0.2, 0.3, 0.4}});
2✔
23

24
  execution_state e;
1✔
25
  e.bufferSize = 9;
1✔
26

27
  snd.requested_tokens.push_back(
1✔
28
      simple_token_request{.prev_date = 0_tv, .date = 0_tv, .offset = 0_tv});
1✔
29
  snd.requested_tokens.push_back(
1✔
30
      simple_token_request{.prev_date = 0_tv, .date = 4_tv, .offset = 0_tv});
1✔
31
  snd.requested_tokens.push_back(
1✔
32
      simple_token_request{.prev_date = 0_tv, .date = 0_tv, .offset = 4_tv});
1✔
33
  snd.requested_tokens.push_back(
1✔
34
      simple_token_request{.prev_date = 0_tv, .date = 4_tv, .offset = 4_tv});
1✔
35
  snd.requested_tokens.push_back(
1✔
36
      simple_token_request{.prev_date = 0_tv, .date = 0_tv, .offset = 8_tv});
1✔
37
  snd.requested_tokens.push_back(
1✔
38
      simple_token_request{.prev_date = 0_tv, .date = 1_tv, .offset = 8_tv});
1✔
39

40
  for(auto tk : snd.requested_tokens)
7✔
41
  {
42
    snd.run(tk, {&e});
6✔
43
  }
44

45
  auto op = snd.root_outputs()[0]->target<audio_port>()->get();
1✔
46
  audio_vector expected{
47
      audio_channel{0.1f, 0.2f, 0.3f, 0.4f, 0.1f, 0.2f, 0.3f, 0.4f, 0.1f}};
2✔
48
  for(int i = 0; i < 9; i++)
10✔
49
  {
50
    REQUIRE(expected[0][i] - op[0][i] < 0.00001);
9✔
51
  }
52
  REQUIRE(op == expected);
1✔
53
}
3✔
54

55
TEST_CASE(
1✔
56
    "sound_writes_a_span_that_reads_no_new_sample",
57
    "sound_writes_a_span_that_reads_no_new_sample")
58
{
59
  using namespace ossia;
60
  nodes::sound_ref snd;
1✔
61
  ossia::audio_array data;
1✔
62
  data.resize(1);
1✔
63
  data[0].assign(128, 1.0f);
1✔
64
  snd.set_sound(std::move(data));
1✔
65

66
  execution_state e;
1✔
67
  e.bufferSize = 64;
1✔
68
  e.sampleRate = 44100;
1✔
69
  e.modelToSamplesRatio = 1. / 16000.;
1✔
70
  e.samplesToModelRatio = 16000.;
1✔
71

72
  // 1020000 flicks is 63.75 samples into the buffer and the tick ends exactly
73
  // on sample 64, so the span is the single sample 63. It consumes a quarter
74
  // of a source sample, so the read count floors to zero while the write count
75
  // is one: the two are floors of differently phased quantities and a region
76
  // starting off a sample boundary hits this on its first tick.
77
  ossia::token_request tk;
1✔
78
  tk.prev_date = 0_tv;
1✔
79
  tk.date = ossia::time_value{4000};
1✔
80
  tk.offset = ossia::time_value{1020000};
1✔
81
  tk.speed = 1.;
1✔
82
  tk.signature = {4, 4};
1✔
83
  tk.tempo = 120.;
1✔
84

85
  const auto si = ossia::snd::sample_info(e.bufferSize, e.modelToSamplesRatio, tk);
1✔
86
  REQUIRE(si.samples_to_read == 0);
1✔
87
  REQUIRE(si.samples_to_write == 1);
1✔
88

89
  snd.run(tk, {&e});
1✔
90

91
  // Reading nothing new is not a reason to write nothing: skipping the span
92
  // leaves a sample of silence at the start of every region that does not
93
  // begin on a sample boundary.
94
  auto& op = *snd.root_outputs()[0]->target<audio_port>();
1✔
95
  REQUIRE(op.channels() >= 1);
1✔
96
  REQUIRE(op.channel(0).size() >= 64);
1✔
97
  REQUIRE(op.channel(0)[63] != 0.);
1✔
98
}
1✔
99

100
#if defined(__GNUC__) || defined(__clang__)
101
// http://www-mmsp.ece.mcgill.ca/Documents/AudioFormats/WAVE/WAVE.html
102
// https://gist.github.com/Jon-Schneider/8b7c53d27a7a13346a643dac9c19d34f
103
struct __attribute__((packed, aligned(8))) test_wave
104
{
105
  static constexpr int nc = 1;
106
  static constexpr int F = 44100;
107
  static constexpr int ns = 1;
108
  static constexpr int M = 4;
109

110
  // RIFF Header
111
  char ckID[4] = {'R', 'I', 'F', 'F'};
112
  int cksize = 4 + 48 + 12 + (8 + M * nc * ns + 0);
113
  char WAVEID[4] = {'W', 'A', 'V', 'E'};
114

115
  // Format Header
116
  char fmt[4] = {'f', 'm', 't', ' '};
117
  int fmt_cksize = 18;
118
  short wFormatTag = 3;
119
  short nChannels = nc;
120
  int nSamplesPerSec = F;
121
  int nAvgBytesPerSec = F * M * nc;
122
  short nBlockAlign = M * nc;
123
  short wBitsPerSample = 8 * M;
124
  short cbSize = 0;
125

126
  // Facts
127
  char factID[4] = {'f', 'a', 'c', 't'};
128
  int factSize = 4;
129
  int dwSampleLength = nc * ns;
130

131
  // Data
132
  char data_header[4] = {'d', 'a', 't', 'a'};
133
  int data_bytes = 4 * 1 * 32;
134

135
  float v[M * nc * ns] = {0.1, 0.2, 0.3, 0.4};
136
};
137

138
TEST_CASE("test_sound_mmap", "test_sound_mmap")
1✔
139
{
140
  using namespace ossia;
141
  ossia::set_thread_pinned(ossia::thread_type::Ui, 0);
1✔
142
  nodes::sound_mmap snd;
1✔
143

144
  test_wave w;
1✔
145
  drwav_handle h{&w, sizeof(test_wave)};
1✔
146
  float v[4]{};
1✔
147
  h.read_pcm_frames_f32(4, v);
1✔
148
  REQUIRE(v[0] == 0.1f);
1✔
149
  REQUIRE(v[1] == 0.2f);
1✔
150
  REQUIRE(v[2] == 0.3f);
1✔
151
  REQUIRE(v[3] == 0.4f);
1✔
152

153
  snd.set_sound(h);
1✔
154

155
  execution_state e;
1✔
156
  e.bufferSize = 9;
1✔
157

158
  snd.requested_tokens.push_back(
1✔
159
      simple_token_request{.prev_date = 0_tv, .date = 0_tv, .offset = 0_tv});
1✔
160
  snd.requested_tokens.push_back(
1✔
161
      simple_token_request{.prev_date = 0_tv, .date = 4_tv, .offset = 0_tv});
1✔
162
  snd.requested_tokens.push_back(
1✔
163
      simple_token_request{.prev_date = 0_tv, .date = 0_tv, .offset = 4_tv});
1✔
164
  snd.requested_tokens.push_back(
1✔
165
      simple_token_request{.prev_date = 0_tv, .date = 4_tv, .offset = 4_tv});
1✔
166
  snd.requested_tokens.push_back(
1✔
167
      simple_token_request{.prev_date = 0_tv, .date = 0_tv, .offset = 8_tv});
1✔
168
  snd.requested_tokens.push_back(
1✔
169
      simple_token_request{.prev_date = 0_tv, .date = 1_tv, .offset = 8_tv});
1✔
170

171
  for(auto tk : snd.requested_tokens)
7✔
172
  {
173
    snd.run(tk, {&e});
6✔
174
    auto op = snd.root_outputs()[0]->target<audio_port>()->get();
6✔
175
  }
6✔
176

177
  auto op = snd.root_outputs()[0]->target<audio_port>()->get();
1✔
178
  audio_vector expected{
179
      audio_channel{0.1f, 0.2f, 0.3f, 0.4f, 0.1f, 0.2f, 0.3f, 0.4f, 0.1f}};
2✔
180
  for(int i = 0; i < 9; i++)
10✔
181
  {
182
    REQUIRE(expected[0][i] - op[0][i] < 0.00001);
9✔
183
  }
184
  REQUIRE(op == expected);
1✔
185
}
2✔
186
#endif
187

188
#if defined(OSSIA_ENABLE_RUBBERBAND)
189
// Sweep test: measures alignment between a sound dropped mid-playback and one
190
// that has been playing from the start. Reports xcorr via stderr; not a
191
// CI-style PASS/FAIL.
192
namespace
193
{
194
struct sync_measurement
195
{
196
  int best_lag_samples{};
197
  double best_correlation{};
198
  double rms_expected{};
199
  double rms_dropped{};
200
  double rms_difference_at_best_lag{};
201
};
202

203
// Normalized cross-correlation; positive lag = dropped trails expected.
204
inline sync_measurement
205
measure_sync(
34✔
206
    const std::vector<float>& expected, const std::vector<float>& dropped,
207
    int window_start, int window_len, int max_lag)
208
{
209
  sync_measurement m{};
34✔
210

211
  auto rms = [](const float* p, int n) {
68✔
212
    long double s = 0;
68✔
213
    for(int i = 0; i < n; i++)
235,588✔
214
      s += (long double)p[i] * (long double)p[i];
235,520✔
215
    return std::sqrt(double(s / std::max(1, n)));
68✔
216
  };
217

218
  m.rms_expected = rms(expected.data() + window_start, window_len);
34✔
219
  m.rms_dropped = rms(dropped.data() + window_start, window_len);
34✔
220

221
  double best_xcorr = -1e30;
34✔
222
  int best_lag = 0;
34✔
223
  for(int lag = -max_lag; lag <= max_lag; lag++)
110,660✔
224
  {
225
    int es = window_start;
110,626✔
226
    int ds = window_start + lag;
110,626✔
227
    if(ds < 0)
110,626✔
228
    {
229
      es += -ds;
7,168✔
230
      ds = 0;
7,168✔
231
    }
232
    const int len = std::min(
110,626✔
233
        window_len,
234
        std::min(int(expected.size()) - es, int(dropped.size()) - ds));
110,626✔
235
    if(len <= 0)
110,626✔
236
      continue;
×
237
    long double s = 0;
110,626✔
238
    long double e_sq = 0;
110,626✔
239
    long double d_sq = 0;
110,626✔
240
    for(int i = 0; i < len; i++)
374,975,394✔
241
    {
242
      const double ev = expected[es + i];
374,864,768✔
243
      const double dv = dropped[ds + i];
374,864,768✔
244
      s += (long double)ev * (long double)dv;
374,864,768✔
245
      e_sq += (long double)ev * (long double)ev;
374,864,768✔
246
      d_sq += (long double)dv * (long double)dv;
374,864,768✔
247
    }
248
    const double denom = std::sqrt(double(e_sq) * double(d_sq));
110,626✔
249
    if(denom <= 0)
110,626✔
250
      continue;
1,025✔
251
    const double xcorr = double(s) / denom;
109,601✔
252
    if(xcorr > best_xcorr)
109,601✔
253
    {
254
      best_xcorr = xcorr;
2,295✔
255
      best_lag = lag;
2,295✔
256
    }
257
  }
258
  m.best_lag_samples = best_lag;
34✔
259
  m.best_correlation = best_xcorr;
34✔
260

261
  // RMS of (expected[i] - dropped[i + best_lag]) over the window
262
  long double diff_sq = 0;
34✔
263
  int count = 0;
34✔
264
  for(int i = 0; i < window_len; i++)
117,794✔
265
  {
266
    const int ei = window_start + i;
117,760✔
267
    const int di = window_start + i + best_lag;
117,760✔
268
    if(di < 0 || di >= (int)dropped.size())
117,760✔
269
      continue;
7,705✔
270
    if(ei < 0 || ei >= (int)expected.size())
110,055✔
271
      continue;
×
272
    const double d = double(expected[ei]) - double(dropped[di]);
110,055✔
273
    diff_sq += (long double)d * (long double)d;
110,055✔
274
    count++;
110,055✔
275
  }
276
  m.rms_difference_at_best_lag = std::sqrt(double(diff_sq / std::max(1, count)));
34✔
277
  return m;
68✔
278
}
279

280
inline constexpr int64_t samples_to_flicks(int64_t samples, int sampleRate)
12,156✔
281
{
282
  return samples * (ossia::flicks_per_second<int64_t> / sampleRate);
12,156✔
283
}
284

285
inline std::vector<float>
286
run_stretcher_continuously(
60✔
287
    ossia::nodes::sound_ref& snd, ossia::execution_state& e,
288
    int64_t start_sample, int64_t end_sample, int bufferSize, int sampleRate,
289
    double tempo)
290
{
291
  std::vector<float> out;
60✔
292
  out.reserve(end_sample - start_sample);
60✔
293

294
  const int64_t buffer_flicks = samples_to_flicks(bufferSize, sampleRate);
60✔
295

296
  for(int64_t cursor = start_sample; cursor + bufferSize <= end_sample;
6,108✔
297
      cursor += bufferSize)
6,048✔
298
  {
299
    const int64_t cursor_flicks = samples_to_flicks(cursor, sampleRate);
6,048✔
300
    const int64_t end_flicks = samples_to_flicks(end_sample, sampleRate);
6,048✔
301
    ossia::token_request tk{
302
        ossia::time_value{cursor_flicks},
303
        ossia::time_value{cursor_flicks + buffer_flicks},
6,048✔
304
        ossia::time_value{end_flicks},
305
        ossia::time_value{0},
306
        1.0,
307
        {4, 4},
308
        tempo};
6,048✔
309

310
    snd.run(tk, ossia::exec_state_facade{&e});
6,048✔
311

312
    auto& ch = snd.audio_out.data.channel(0);
6,048✔
313
    for(int i = 0; i < bufferSize; i++)
1,554,336✔
314
      out.push_back(i < (int)ch.size() ? float(ch[i]) : 0.f);
1,548,288✔
315
  }
316
  return out;
60✔
317
}
×
318

319
inline std::unique_ptr<ossia::nodes::sound_ref>
320
make_sined_node(
54✔
321
    const ossia::audio_array& data, ossia::audio_stretch_mode mode,
322
    int sampleRate, int channels, double tempo)
323
{
324
  auto n = std::make_unique<ossia::nodes::sound_ref>();
54✔
325
  n->set_sound(data);
54✔
326
  // The 1-arg set_sound() forces stretch mode to None; override it here.
327
  n->m_resampler.reset(0, mode, channels, sampleRate);
54✔
328
  n->set_native_tempo(tempo);
54✔
329
  return n;
54✔
330
}
×
331
} // namespace
332

333
TEST_CASE("rubberband_drop_sync_unit_ratio", "[rubberband][sync]")
1✔
334
{
335
  using namespace ossia;
336

337
  // graph_node dtor asserts on thread kind.
338
  ossia::set_thread_pinned(ossia::thread_type::Ui, 0);
1✔
339

340
  constexpr int sampleRate = 44100;
1✔
341
  constexpr int bufferSize = 256;
1✔
342
  constexpr int channels = 1;
1✔
343
  constexpr double tempo = 120.0;
1✔
344
  constexpr int64_t T_drop = 24064;
1✔
345
  constexpr int64_t T_measure = 16384;
1✔
346
  constexpr int64_t T_total = T_drop + T_measure + bufferSize;
1✔
347

348
  // 1 kHz sine modulated by a 5 Hz AM envelope so xcorr has a unique peak.
349
  const int64_t file_samples = T_total + 8192;
1✔
350
  audio_array data;
1✔
351
  data.resize(1);
1✔
352
  data[0].resize(file_samples);
1✔
353
  const double freq = 1000.0;
1✔
354
  const double env_freq = 5.0;
1✔
355
  for(int64_t i = 0; i < file_samples; i++)
48,897✔
356
  {
357
    const double env
48,896✔
358
        = 0.5 + 0.5 * std::sin(2.0 * M_PI * env_freq * double(i) / sampleRate);
48,896✔
359
    data[0][i] = 0.5f * float(std::sin(2.0 * M_PI * freq * double(i) / sampleRate) * env);
48,896✔
360
  }
361

362
  execution_state e;
1✔
363
  e.bufferSize = bufferSize;
1✔
364
  e.sampleRate = sampleRate;
1✔
365
  e.modelToSamplesRatio = double(sampleRate) / double(flicks_per_second<int64_t>);
1✔
366

367
  auto run_case = [&](const char* label, audio_stretch_mode mode) {
7✔
368
    auto A = make_sined_node(data, mode, sampleRate, channels, tempo);
7✔
369
    auto B = make_sined_node(data, mode, sampleRate, channels, tempo);
7✔
370

371
    const auto outA = run_stretcher_continuously(*A, e, 0, T_total, bufferSize, sampleRate, tempo);
7✔
372

373
    // Mimic add_time_process()'s transport(m_date) call.
374
    B->transport(ossia::time_value{samples_to_flicks(T_drop, sampleRate)});
7✔
375
    const auto outB = run_stretcher_continuously(
376
        *B, e, T_drop, T_total, bufferSize, sampleRate, tempo);
7✔
377

378
    REQUIRE(outA.size() >= (size_t)(T_drop + T_measure));
7✔
379
    REQUIRE(outB.size() >= (size_t)T_measure);
7✔
380

381
    auto transient = measure_sync(
7✔
382
        std::vector<float>(outA.begin() + T_drop, outA.end()),
14✔
383
        outB, 0, 1024, 1024);
384
    auto steady = measure_sync(
7✔
385
        std::vector<float>(outA.begin() + T_drop, outA.end()),
14✔
386
        outB, (int)T_measure - 4096, 4096, 1024);
387

388
    std::fprintf(
7✔
389
        stderr,
390
        "\n[%s] transient: lag=%d samples (%.2f ms)  xcorr=%.4f  "
391
        "rmsA=%.4f rmsB=%.4f diff=%.4f\n",
392
        label, transient.best_lag_samples,
393
        1000.0 * transient.best_lag_samples / sampleRate,
7✔
394
        transient.best_correlation, transient.rms_expected,
395
        transient.rms_dropped, transient.rms_difference_at_best_lag);
396
    std::fprintf(
7✔
397
        stderr,
398
        "[%s] steady:    lag=%d samples (%.2f ms)  xcorr=%.4f  "
399
        "rmsA=%.4f rmsB=%.4f diff=%.4f\n",
400
        label, steady.best_lag_samples,
401
        1000.0 * steady.best_lag_samples / sampleRate,
7✔
402
        steady.best_correlation, steady.rms_expected, steady.rms_dropped,
403
        steady.rms_difference_at_best_lag);
404

405
    return steady;
14✔
406
  };
7✔
407

408
  // None/Repitch reference points: no OLA reconstruction.
409
  run_case("none      ", audio_stretch_mode::None);
1✔
410
#if defined(OSSIA_ENABLE_LIBSAMPLERATE)
411
  run_case("repitch   ", audio_stretch_mode::Repitch);
1✔
412
#endif
413

414
  using ps = rubberband_stretcher::prime_strategy;
415
  struct
416
  {
417
    const char* name;
418
    ps strat;
419
  } variants[] = {
1✔
420
      {"rb_noPrime    ", ps::NoPrime},
421
      {"rb_zeroPadOnly", ps::ZeroPadOnly},
422
      {"rb_bareRecipe ", ps::BareRecipe},
423
      {"rb_extendDrain", ps::ExtendedDrain},
424
      {"rb_preRoll    ", ps::PreRollRealAudio},
425
  };
426

427
  sync_measurement best{};
1✔
428
  best.best_correlation = -1.0;
1✔
429
  const char* best_name = nullptr;
1✔
430
  for(auto& v : variants)
6✔
431
  {
432
    rubberband_stretcher::s_prime_strategy = v.strat;
5✔
433
    auto s = run_case(v.name, audio_stretch_mode::RubberBandStandard);
5✔
434
    if(s.best_correlation > best.best_correlation)
5✔
435
    {
436
      best = s;
1✔
437
      best_name = v.name;
1✔
438
    }
439
  }
440
  std::fprintf(
1✔
441
      stderr,
442
      "\n>>> best @ ratio=1.0: %s with xcorr=%.4f, lag=%d samples (%.2f ms)\n",
443
      best_name ? best_name : "?", best.best_correlation,
444
      best.best_lag_samples,
445
      1000.0 * best.best_lag_samples / sampleRate);
1✔
446

447
  rubberband_stretcher::s_prime_strategy = ps::BareRecipe;
1✔
448

449
  // CHECK not REQUIRE so failure still prints measurements.
450
  CHECK(best.best_correlation > 0.9);
1✔
451
}
1✔
452

453
TEST_CASE("rubberband_drop_sync_tempo_shift", "[rubberband][sync]")
1✔
454
{
455
  // file=100 BPM, timeline=120 BPM (stretch ratio 100/120 ≈ 0.833).
456
  using namespace ossia;
457

458
  ossia::set_thread_pinned(ossia::thread_type::Ui, 0);
1✔
459

460
  constexpr int sampleRate = 44100;
1✔
461
  constexpr int bufferSize = 256;
1✔
462
  constexpr int channels = 1;
1✔
463
  constexpr double file_tempo = 100.0;
1✔
464
  constexpr double timeline_tempo = 120.0;
1✔
465
  constexpr int64_t T_drop = 24064;
1✔
466
  constexpr int64_t T_measure = 16384;
1✔
467
  constexpr int64_t T_total = T_drop + T_measure + bufferSize;
1✔
468

469
  const int64_t file_samples = T_total + 32768;
1✔
470
  audio_array data;
1✔
471
  data.resize(1);
1✔
472
  data[0].resize(file_samples);
1✔
473
  const double freq = 1000.0;
1✔
474
  const double env_freq = 5.0;
1✔
475
  for(int64_t i = 0; i < file_samples; i++)
73,473✔
476
  {
477
    const double env
73,472✔
478
        = 0.5 + 0.5 * std::sin(2.0 * M_PI * env_freq * double(i) / sampleRate);
73,472✔
479
    data[0][i] = 0.5f * float(std::sin(2.0 * M_PI * freq * double(i) / sampleRate) * env);
73,472✔
480
  }
481

482
  execution_state e;
1✔
483
  e.bufferSize = bufferSize;
1✔
484
  e.sampleRate = sampleRate;
1✔
485
  e.modelToSamplesRatio = double(sampleRate) / double(flicks_per_second<int64_t>);
1✔
486

487
  // via_sampler = legacy bug path; stretch_aware = the new overload.
488
  enum class seek_mode
489
  {
490
    via_sampler,
491
    stretch_aware,
492
  };
493

494
  auto run_case = [&](const char* label, audio_stretch_mode mode, seek_mode sm) {
14✔
495
    auto A = make_sined_node(data, mode, sampleRate, channels, file_tempo);
14✔
496
    auto B = make_sined_node(data, mode, sampleRate, channels, file_tempo);
14✔
497

498
    const auto outA = run_stretcher_continuously(
499
        *A, e, 0, T_total, bufferSize, sampleRate, timeline_tempo);
14✔
500

501
    if(sm == seek_mode::via_sampler)
14✔
502
    {
503
      B->transport(ossia::time_value{samples_to_flicks(T_drop, sampleRate)});
7✔
504
    }
505
    else
506
    {
507
      ossia::tick_transport_info tinfo{};
7✔
508
      tinfo.current_tempo = timeline_tempo;
7✔
509
      B->transport(ossia::time_value{samples_to_flicks(T_drop, sampleRate)}, tinfo);
7✔
510
    }
7✔
511
    const auto outB = run_stretcher_continuously(
512
        *B, e, T_drop, T_total, bufferSize, sampleRate, timeline_tempo);
14✔
513

514
    REQUIRE(outA.size() >= (size_t)(T_drop + T_measure));
14✔
515
    REQUIRE(outB.size() >= (size_t)T_measure);
14✔
516

517
    auto steady = measure_sync(
14✔
518
        std::vector<float>(outA.begin() + T_drop, outA.end()),
28✔
519
        outB, (int)T_measure - 4096, 4096, 2048);
520

521
    std::fprintf(
14✔
522
        stderr,
523
        "\n[tempo %.0f→%.0f, %s] steady: lag=%d samples (%.2f ms)  "
524
        "xcorr=%.4f  rmsA=%.4f rmsB=%.4f diff=%.4f\n",
525
        file_tempo, timeline_tempo, label, steady.best_lag_samples,
526
        1000.0 * steady.best_lag_samples / sampleRate,
14✔
527
        steady.best_correlation, steady.rms_expected, steady.rms_dropped,
528
        steady.rms_difference_at_best_lag);
529

530
    return steady;
28✔
531
  };
14✔
532

533
  std::fprintf(stderr, "\n--- tempo-shift, via_sampler seek (production path) ---\n");
1✔
534
  run_case("none      ", audio_stretch_mode::None, seek_mode::via_sampler);
1✔
535
#if defined(OSSIA_ENABLE_LIBSAMPLERATE)
536
  run_case("repitch   ", audio_stretch_mode::Repitch, seek_mode::via_sampler);
1✔
537
#endif
538

539
  using ps = rubberband_stretcher::prime_strategy;
540
  struct
541
  {
542
    const char* name;
543
    ps strat;
544
  } variants[] = {
1✔
545
      {"rb_noPrime    ", ps::NoPrime},
546
      {"rb_zeroPadOnly", ps::ZeroPadOnly},
547
      {"rb_bareRecipe ", ps::BareRecipe},
548
      {"rb_extendDrain", ps::ExtendedDrain},
549
      {"rb_preRoll    ", ps::PreRollRealAudio},
550
  };
551

552
  auto run_strategies = [&](seek_mode sm) {
2✔
553
    sync_measurement best{};
2✔
554
    best.best_correlation = -1.0;
2✔
555
    const char* best_name = nullptr;
2✔
556
    for(auto& v : variants)
12✔
557
    {
558
      rubberband_stretcher::s_prime_strategy = v.strat;
10✔
559
      auto s = run_case(v.name, audio_stretch_mode::RubberBandStandard, sm);
10✔
560
      if(s.best_correlation > best.best_correlation)
10✔
561
      {
562
        best = s;
10✔
563
        best_name = v.name;
10✔
564
      }
565
    }
566
    std::fprintf(
2✔
567
        stderr,
568
        "\n>>> best %s: %s with xcorr=%.4f, lag=%d samples (%.2f ms)\n",
569
        sm == seek_mode::via_sampler ? "via_sampler" : "stretch_aware",
570
        best_name ? best_name : "?", best.best_correlation,
571
        best.best_lag_samples,
572
        1000.0 * best.best_lag_samples / sampleRate);
2✔
573
  };
2✔
574

575
  run_strategies(seek_mode::via_sampler);
1✔
576

577
  std::fprintf(stderr, "\n--- tempo-shift, stretch_aware seek (proposed fix) ---\n");
1✔
578
  run_case("none      ", audio_stretch_mode::None, seek_mode::stretch_aware);
1✔
579
#if defined(OSSIA_ENABLE_LIBSAMPLERATE)
580
  run_case("repitch   ", audio_stretch_mode::Repitch, seek_mode::stretch_aware);
1✔
581
#endif
582
  run_strategies(seek_mode::stretch_aware);
1✔
583

584
  rubberband_stretcher::s_prime_strategy = ps::BareRecipe;
1✔
585
}
1✔
586

587
TEST_CASE("rubberband_drop_then_paste_sync", "[rubberband][sync]")
1✔
588
{
589
  // Scenario A: both nodes are added during playback (drop + paste).
590
  using namespace ossia;
591

592
  ossia::set_thread_pinned(ossia::thread_type::Ui, 0);
1✔
593

594
  constexpr int sampleRate = 44100;
1✔
595
  constexpr int bufferSize = 256;
1✔
596
  constexpr int channels = 1;
1✔
597
  constexpr double file_tempo = 100.0;
1✔
598
  constexpr double timeline_tempo = 120.0;
1✔
599
  constexpr int64_t T_drop = 24064;
1✔
600
  constexpr int64_t T_paste = 48128;
1✔
601
  constexpr int64_t T_measure = 16384;
1✔
602
  constexpr int64_t T_total = T_paste + T_measure + bufferSize;
1✔
603

604
  const int64_t file_samples = T_total + 32768;
1✔
605
  audio_array data;
1✔
606
  data.resize(1);
1✔
607
  data[0].resize(file_samples);
1✔
608
  const double freq = 1000.0;
1✔
609
  const double env_freq = 5.0;
1✔
610
  for(int64_t i = 0; i < file_samples; i++)
97,537✔
611
  {
612
    const double env
97,536✔
613
        = 0.5 + 0.5 * std::sin(2.0 * M_PI * env_freq * double(i) / sampleRate);
97,536✔
614
    data[0][i]
97,536✔
615
        = 0.5f * float(std::sin(2.0 * M_PI * freq * double(i) / sampleRate) * env);
195,072✔
616
  }
617

618
  execution_state e;
1✔
619
  e.bufferSize = bufferSize;
1✔
620
  e.sampleRate = sampleRate;
1✔
621
  e.modelToSamplesRatio = double(sampleRate) / double(flicks_per_second<int64_t>);
1✔
622

623
  auto run_case = [&](const char* label, audio_stretch_mode mode) {
6✔
624
    auto orig = make_sined_node(data, mode, sampleRate, channels, file_tempo);
6✔
625
    auto paste = make_sined_node(data, mode, sampleRate, channels, file_tempo);
6✔
626

627
    ossia::tick_transport_info tinfo{};
6✔
628
    tinfo.current_tempo = timeline_tempo;
6✔
629

630
    orig->transport(ossia::time_value{samples_to_flicks(T_drop, sampleRate)}, tinfo);
6✔
631
    const int64_t orig_after_seek = orig->m_resampler.next_sample_to_read();
6✔
632

633
    auto outOrig_pre = run_stretcher_continuously(
634
        *orig, e, T_drop, T_paste, bufferSize, sampleRate, timeline_tempo);
6✔
635
    const int64_t orig_at_T_paste = orig->m_resampler.next_sample_to_read();
6✔
636

637
    auto outOrig_post = run_stretcher_continuously(
638
        *orig, e, T_paste, T_total, bufferSize, sampleRate, timeline_tempo);
6✔
639
    std::vector<float> outOrig;
6✔
640
    outOrig.reserve(outOrig_pre.size() + outOrig_post.size());
6✔
641
    outOrig.insert(outOrig.end(), outOrig_pre.begin(), outOrig_pre.end());
6✔
642
    outOrig.insert(outOrig.end(), outOrig_post.begin(), outOrig_post.end());
6✔
643

644
    paste->transport(ossia::time_value{samples_to_flicks(T_paste, sampleRate)}, tinfo);
6✔
645
    const int64_t paste_after_seek = paste->m_resampler.next_sample_to_read();
6✔
646
    const auto outPaste = run_stretcher_continuously(
647
        *paste, e, T_paste, T_total, bufferSize, sampleRate, timeline_tempo);
6✔
648

649
    std::fprintf(
6✔
650
        stderr,
651
        "  [%s] file_pos: orig_seek=%lld  orig@T_paste=%lld  paste_seek=%lld  diff=%lld\n",
652
        label, (long long)orig_after_seek, (long long)orig_at_T_paste,
653
        (long long)paste_after_seek,
654
        (long long)(orig_at_T_paste - paste_after_seek));
6✔
655

656
    REQUIRE(outOrig.size() >= (size_t)(T_paste - T_drop + T_measure));
6✔
657
    REQUIRE(outPaste.size() >= (size_t)T_measure);
6✔
658

659
    const std::vector<float> expected(
660
        outOrig.begin() + (T_paste - T_drop), outOrig.end());
6✔
661
    auto steady = measure_sync(
6✔
662
        expected, outPaste, (int)T_measure - 4096, 4096, 2048);
663

664
    std::fprintf(
6✔
665
        stderr,
666
        "[drop+paste, %s] steady: lag=%d samples (%.2f ms)  xcorr=%.4f  "
667
        "rmsA=%.4f rmsB=%.4f diff=%.4f\n",
668
        label, steady.best_lag_samples,
669
        1000.0 * steady.best_lag_samples / sampleRate,
6✔
670
        steady.best_correlation, steady.rms_expected, steady.rms_dropped,
671
        steady.rms_difference_at_best_lag);
672
    return steady;
12✔
673
  };
6✔
674

675
  std::fprintf(stderr,
1✔
676
      "\n--- drop-during-playback + paste-during-playback (scenario A) ---\n");
677
  run_case("none      ", audio_stretch_mode::None);
1✔
678
#if defined(OSSIA_ENABLE_LIBSAMPLERATE)
679
  run_case("repitch   ", audio_stretch_mode::Repitch);
1✔
680
#endif
681

682
  using ps = rubberband_stretcher::prime_strategy;
683
  struct
684
  {
685
    const char* name;
686
    ps strat;
687
  } variants[] = {
1✔
688
      {"rb_zeroPadOnly", ps::ZeroPadOnly},
689
      {"rb_bareRecipe ", ps::BareRecipe},
690
      {"rb_extendDrain", ps::ExtendedDrain},
691
      {"rb_preRoll    ", ps::PreRollRealAudio},
692
  };
693

694
  for(auto& v : variants)
5✔
695
  {
696
    rubberband_stretcher::s_prime_strategy = v.strat;
4✔
697
    run_case(v.name, audio_stretch_mode::RubberBandStandard);
4✔
698
  }
699

700
  rubberband_stretcher::s_prime_strategy = ps::BareRecipe;
1✔
701
}
1✔
702
#endif
STATUS · Troubleshooting · Open an Issue · Sales · Support · CAREERS · ENTERPRISE · START FREE TRIAL · SCHEDULE DEMO
ANNOUNCEMENTS · TWITTER · TOS & SLA · Supported CI Services · What's a CI service? · Automated Testing

© 2026 Coveralls, Inc