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

ossia / libossia / 35780360009

22 Sep 2026 08:26PM UTC coverage: 52.026% (+0.02%) from 52.003%
35780360009

Pull #948

github

web-flow
Merge 9db9e8ef0 into 9d83ce436
Pull Request #948: Audio tree off the audio thread, scalar-to-vector rescaling, MIDI bounds

100 of 166 new or added lines in 7 files covered. (60.24%)

20 existing lines in 7 files now uncovered.

31602 of 60743 relevant lines covered (52.03%)

175125.74 hits per line

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

56.18
/src/ossia/dataflow/data.cpp
1
#include <ossia/dataflow/data_copy.hpp>
2
#include <ossia/dataflow/value_port.hpp>
3
#include <ossia/detail/algorithms.hpp>
4
#include <ossia/detail/apply.hpp>
5
#include <ossia/detail/logger.hpp>
6
#include <ossia/network/common/complex_type.hpp>
7
#include <ossia/network/common/value_bounding.hpp>
8
#include <ossia/network/common/value_mapping.hpp>
9
#include <ossia/network/dataspace/dataspace_visitors.hpp>
10
#include <ossia/network/value/format_value.hpp>
11

12
namespace ossia
13
{
14
namespace
15
{
16

17
static inline ossia::complex_type
18
get_complex_type(const ossia::net::parameter_base& other) noexcept
79 ✔
19
{
20
  if(const auto u = other.get_unit())
79 ✔
21
    return u;
79 ✔
22
  return other.get_value_type();
45 ✔
23
}
24

25
//! Whether both sides declare a unit of the same dataspace, in which case the
26
//! conversion decides what the numbers are and the callers bound instead of
27
//! rescaling. Units of different dataspaces say nothing about each other.
28
bool units_govern(
99 ✔
29
    const ossia::complex_type& source_type, const ossia::complex_type& sink_type) noexcept
30
{
31
  auto src_unit = source_type.target<ossia::unit_t>();
99 ✔
32
  auto tgt_unit = sink_type.target<ossia::unit_t>();
99 ✔
33
  return src_unit && tgt_unit && bool(*src_unit) && bool(*tgt_unit)
46 ✔
34
         && ossia::check_units_convertible(*src_unit, *tgt_unit);
145 ✔
35
}
36

37
void clip_value(ossia::value& v, const ossia::domain& sink_domain)
9 ✔
38
{
39
  if(auto bounded
9 ✔
40
     = ossia::bound_value(sink_domain, std::move(v), ossia::bounding_mode::CLIP);
9 ✔
41
     bounded.valid())
9 ✔
42
    v = std::move(bounded);
9 ✔
43
}
9 ✔
44

45
// TODO we should take a visitor that takes (value, source_domain, sink_domain)
46
// for each argument and does the right thing
47
struct process_float_control_visitor
48
{
49
  float src_min{};
50
  float dst_min{};
51
  float ratio{};
52

53
  void operator()(ossia::impulse) const noexcept { }
×
54

55
  void operator()(int32_t& v) const noexcept { v = dst_min + ratio * (v - src_min); }
×
56
  void operator()(float& v) const noexcept { v = dst_min + ratio * (v - src_min); }
5 ✔
57
  void operator()(bool& v) const noexcept { v = dst_min + ratio * (v - src_min); }
×
58

59
  void operator()(char& v) const noexcept { v = dst_min + ratio * (v - src_min); }
60
  void operator()(std::string& str) const noexcept
×
61
  {
62
    for(char& v : str)
×
63
      v = dst_min + ratio * (v - src_min);
×
64
  }
×
65

66
  void operator()(ossia::vec2f& v) const noexcept
×
67
  {
68
    v[0] = dst_min + ratio * (v[0] - src_min);
×
69
    v[1] = dst_min + ratio * (v[1] - src_min);
×
70
  }
×
71
  void operator()(ossia::vec3f& v) const noexcept
×
72
  {
73
    v[0] = dst_min + ratio * (v[0] - src_min);
×
74
    v[1] = dst_min + ratio * (v[1] - src_min);
×
75
    v[2] = dst_min + ratio * (v[2] - src_min);
×
76
  }
×
77
  void operator()(ossia::vec4f& v) const noexcept
×
78
  {
79
    v[0] = dst_min + ratio * (v[0] - src_min);
×
80
    v[1] = dst_min + ratio * (v[1] - src_min);
×
81
    v[2] = dst_min + ratio * (v[2] - src_min);
×
82
    v[3] = dst_min + ratio * (v[3] - src_min);
×
83
  }
×
84

85
  void operator()(std::vector<ossia::value>& v) const noexcept
×
86
  {
87
    for(auto& value : v)
×
88
      value.apply(*this);
×
89
  }
×
90
  void operator()(value_map_type& v) const noexcept
×
91
  {
92
    for(auto& [k, value] : v)
×
93
      value.apply(*this);
×
94
  }
×
95

96
  void operator()() const noexcept { }
×
97
};
98

99
bool should_process_control(
19 ✔
100
    const value_port& source_port, const value_port& sink_port) noexcept
101
{
102
  return (source_port.domain && sink_port.domain)
27 ✔
103
         || (source_port.has_effective_type() && sink_port.has_effective_type())
27 ✔
104
      // FIXME   || source_port.tween_date
105
      ;
106
}
107

108
void process_float_control(
5 ✔
109
    ossia::value& v, float src_min, float src_max, float dst_min, float dst_max)
110
{
111
  const float sub = src_max - src_min;
5 ✔
112
  if(sub == 0.f)
5 ✔
113
    return;
×
114

115
  const float ratio = (dst_max - dst_min) / sub;
5 ✔
116
  v.apply(process_float_control_visitor{src_min, dst_min, ratio});
5 ✔
117
}
118

119
// The effective types are computed once by the caller: copying one per value can
120
// allocate.
121
void process_control_value(
13 ✔
122
    ossia::value& v, const value_port& source_port, const value_port& sink_port,
123
    const ossia::complex_type& source_type, const ossia::complex_type& sink_type) noexcept
124
{
125
  // Same rule as adapt_value(), which is what the address path uses.
126
  if(units_govern(source_type, sink_type))
13 ✔
127
  {
128
    process_control_value(v, source_type, sink_type);
5 ✔
129
    if(sink_port.domain)
5 ✔
130
      clip_value(v, sink_port.domain);
4 ✔
131
  }
132
  else if(source_port.domain && sink_port.domain && source_type && sink_type)
16 ✔
133
    process_control_value(
3 ✔
134
        v, source_port.domain, sink_port.domain, source_type, sink_type);
3 ✔
135
  else if(source_type && sink_type)
8 ✔
136
    process_control_value(v, source_type, sink_type);
3 ✔
137
  else if(source_port.domain && sink_port.domain)
2 ✔
138
    process_control_value(v, source_port.domain, sink_port.domain);
2 ✔
139
  // FIXME  if(source_port.tween_date)
140
  // FIXME  {
141
  // FIXME    // TODO
142
  // FIXME  }
143
}
13 ✔
144
}
145

146
static bool filter_value(
83 ✔
147
    value& source, const ossia::destination_index& /* source_index */,
148
    const ossia::destination_index& res_index, const ossia::complex_type& source_type,
149
    const ossia::complex_type& res_type)
150
{
151
  // 1. Convert from source unit to destination unit.
152
  // Different dataspaces have nothing to convert between: pass the value through.
153
  auto src_unit = source_type.target<ossia::unit_t>();
83 ✔
154
  auto tgt_unit = res_type.target<ossia::unit_t>();
83 ✔
155
  if(src_unit && tgt_unit && *src_unit != *tgt_unit
38 ✔
156
     && ossia::check_units_convertible(*src_unit, *tgt_unit))
121 ✔
157
  {
158
    // A value whose shape does not fit its unit converts to nothing at all.
159
    if(auto converted = ossia::convert(source, *src_unit, *tgt_unit); converted.valid())
8 ✔
160
      source = std::move(converted);
8 ✔
161
  }
162
  else
163
  {
164
    auto src_type = source_type.target<ossia::val_type>();
75 ✔
165
    auto tgt_type = res_type.target<ossia::val_type>();
75 ✔
166
    if(src_type && tgt_type && *src_type != *tgt_type && res_index.empty())
75 ✔
167
    {
168
      ossia::convert_inplace(source, *tgt_type);
×
169
    }
170
  }
171

172
  if(source.valid() && !res_index.empty())
83 ✔
173
  {
174
    source = get_value_at_index(source, res_index);
29 ✔
175
  }
176

177
  return units_govern(source_type, res_type);
83 ✔
178
}
179

180
//! Convert a value into the sink's unit, then either bound it or rescale it.
181
//!
182
//! Converting preserves the quantity; mapping between two domains rescales it.
183
//! Doing both would rescale with bounds expressed in the unit the value just
184
//! left, so when units decide, the sink's domain only bounds, and it bounds
185
//! whether or not the source has a domain of its own.
186
static void adapt_value(
78 ✔
187
    ossia::value& v, const ossia::destination_index& idx,
188
    const ossia::complex_type& source_type, const ossia::complex_type& sink_type,
189
    const ossia::domain& source_domain, const ossia::domain& sink_domain)
190
{
191
  const bool governed = filter_value(v, {}, idx, source_type, sink_type);
78 ✔
192

193
  if(governed)
78 ✔
194
  {
195
    if(sink_domain)
11 ✔
196
      clip_value(v, sink_domain);
5 ✔
197
  }
198
  else if(source_domain && sink_domain)
67 ✔
199
  {
200
    map_value(v, idx, source_domain, sink_domain);
15 ✔
201
  }
202
}
78 ✔
203

204
struct is_vector_domain_visitor
205
{
NEW
206
  bool operator()() const noexcept { return false; }
×
NEW
207
  bool operator()(const ossia::vector_domain&) const noexcept { return true; }
×
208
  template <std::size_t N>
NEW
209
  bool operator()(const ossia::vecf_domain<N>&) const noexcept
×
210
  {
NEW
211
    return true;
×
212
  }
213
  template <typename T>
214
  bool operator()(const T&) const noexcept
5 ✔
215
  {
216
    return false;
5 ✔
217
  }
218
};
219

220
//! Whether a domain bounds each component separately.
221
static bool is_vector_domain(const ossia::domain& dom) noexcept
5 ✔
222
{
223
  return ossia::apply(is_vector_domain_visitor{}, dom.v);
5 ✔
224
}
225

226
void process_control_value(
5 ✔
227
    ossia::value& v, const ossia::domain& source_domain,
228
    const ossia::domain& sink_domain) noexcept
229
{
230
  // Two per-component domains have to be mapped component by component: a
231
  // single ratio taken across all of them rescales each by the widest one.
232
  if(is_vector_domain(source_domain) && is_vector_domain(sink_domain))
5 ✔
233
  {
NEW
234
    map_value(v, ossia::destination_index{}, source_domain, sink_domain);
×
NEW
235
    return;
×
236
  }
237

238
  auto [src_min, src_max] = ossia::get_float_minmax(source_domain);
5 ✔
239
  auto [tgt_min, tgt_max] = ossia::get_float_minmax(sink_domain);
5 ✔
240

241
  if(src_min && src_max && tgt_min && tgt_max)
5 ✔
242
  {
243
    process_float_control(v, *src_min, *src_max, *tgt_min, *tgt_max);
5 ✔
244
  }
245
}
246

247
void process_control_value(
11 ✔
248
    ossia::value& v, const ossia::complex_type& source_type,
249
    const ossia::complex_type& sink_type) noexcept
250
{
251
  // Same guards as filter_value().
252
  auto src_unit = source_type.target<ossia::unit_t>();
11 ✔
253
  auto tgt_unit = sink_type.target<ossia::unit_t>();
11 ✔
254
  if(src_unit && tgt_unit && !ossia::check_units_convertible(*src_unit, *tgt_unit))
11 ✔
255
    return;
2 ✔
256

257
  if(auto converted = convert(v, source_type, sink_type); converted.valid())
9 ✔
258
    v = std::move(converted);
9 ✔
259
}
260

261
void process_control_value(
3 ✔
262
    ossia::value& v, const ossia::domain& source_domain,
263
    const ossia::domain& sink_domain, const ossia::complex_type& source_type,
264
    const ossia::complex_type& sink_type) noexcept
265
{
266
  // Same rule as adapt_value(), which is what the address path uses.
267
  if(units_govern(source_type, sink_type))
3 ✔
268
  {
269
    process_control_value(v, source_type, sink_type);
×
270
    if(sink_domain)
×
271
      clip_value(v, sink_domain);
×
272
  }
273
  else
274
  {
275
    process_control_value(v, source_domain, sink_domain);
3 ✔
276
    process_control_value(v, source_type, sink_type);
3 ✔
277
  }
278
}
3 ✔
279

280
void ensure_vector_sizes(const audio_vector& src_vec, audio_vector& sink_vec)
×
281
{
282
  const auto src_chans = src_vec.size();
×
283
  const auto sink_chans = sink_vec.size();
×
284
  if(sink_chans < src_chans)
×
285
    audio_buffer_pool::set_channels(sink_vec, src_chans);
×
286

287
  for(std::size_t chan = 0; chan < src_chans; chan++)
×
288
  {
289
    const std::size_t N = src_vec[chan].size();
×
290
    if(sink_vec[chan].size() < N)
×
291
      sink_vec[chan].resize(N);
×
292
  }
293
}
×
294

295
void mix(const audio_vector& src_vec, audio_vector& sink_vec)
×
296
{
297
  if(BOOST_UNLIKELY(src_vec.size() != 0 && sink_vec.size() == 0))
×
298
  {
299
    const auto channels = src_vec.size();
×
300
    audio_buffer_pool::set_channels(sink_vec, channels);
×
301
    for(std::size_t c = 0; c < channels; c++)
×
302
    {
303
      const auto& src = src_vec[c];
×
304
      sink_vec[c].assign(src.begin(), src.end());
×
305
    }
306
    return;
×
307
  }
308
  else if(BOOST_LIKELY(src_vec.size() == sink_vec.size()))
×
309
  {
310
    for(std::size_t chan = 0, src_chans = src_vec.size(); chan < src_chans; chan++)
×
311
    {
312
      auto& src = src_vec[chan];
×
313
      auto& sink = sink_vec[chan];
×
314
      if(sink.empty())
×
315
      {
316
        sink = src;
×
317
      }
318
      else
319
      {
320
        const std::size_t N = src.size();
×
321

322
        if(BOOST_UNLIKELY(sink.size() < N))
×
323
        {
324
          sink.resize(N);
×
325
        }
326

327
        auto src_p = src.data();
×
328
        auto sink_p = sink.data();
×
329
        for(std::size_t i = 0; i < N; i++)
×
330
          sink_p[i] += src_p[i];
×
331
      }
332
    }
333
  }
334
  else
335
  {
336
    ensure_vector_sizes(src_vec, sink_vec);
×
337
    // Just copy the channels without much thoughts
338
    for(std::size_t chan = 0, src_chans = src_vec.size(); chan < src_chans; chan++)
×
339
    {
340
      auto& src = src_vec[chan];
×
341
      auto& sink = sink_vec[chan];
×
342
      const std::size_t N = src.size();
×
343
      auto src_p = src.data();
×
344
      auto sink_p = sink.data();
×
345

346
      for(std::size_t i = 0; i < N; i++)
×
347
        sink_p[i] += src_p[i];
×
348
    }
349
  }
350
}
351

352
void audio_buffer_pool::set_channels(audio_vector& samples, std::size_t channels)
254,612 ✔
353
{
354
  if(samples.size() == channels)
254,612 ✔
355
    return;
6,050 ✔
356

357
  auto& pool = audio_buffer_pool::instance();
248,562 ✔
358
  while(samples.size() > channels)
248,673 ✔
359
  {
360
    auto chan = std::move(samples.back());
111 ✔
361
    chan.clear();
111 ✔
362

363
    pool.release(std::move(chan));
111 ✔
364
    samples.pop_back();
111 ✔
365
  }
111 ✔
366

367
  samples.reserve(channels);
248,562 ✔
368
  while(samples.size() < channels)
500,270 ✔
369
  {
370
    samples.push_back(pool.acquire());
503,416 ✔
371
  }
372
}
373

374
void value_port::write_value(const value& v, int64_t timestamp)
20 ✔
375
{
376
  switch(mix_method)
20 ✔
377
  {
378
    case data_mix_method::mix_replace: {
×
379
      auto it = ossia::find_if(data, [&](const ossia::timed_value& val) {
×
380
        return val.timestamp == timestamp;
×
381
      });
×
382
      if(it != data.end())
×
383
      {
384
        it->value = v;
×
385
      }
386
      else
387
      {
388
        data.emplace_back(v, timestamp);
×
389
      }
390
      break;
×
391
    }
392
    case data_mix_method::mix_append: {
20 ✔
393
      this->data.emplace_back(v, timestamp);
20 ✔
394
      break;
20 ✔
395
    }
396
    case data_mix_method::mix_merge: {
×
397
      // TODO;
398
      break;
×
399
    }
400
  }
401
}
20 ✔
402

403
void value_port::write_value(value&& v, int64_t timestamp)
134 ✔
404
{
405
  switch(mix_method)
134 ✔
406
  {
407
    case data_mix_method::mix_replace: {
5 ✔
408
      auto it = ossia::find_if(data, [&](const ossia::timed_value& val) {
14 ✔
409
        return val.timestamp == timestamp;
4 ✔
410
      });
5 ✔
411
      if(it != data.end())
10 ✔
412
      {
413
        it->value = std::move(v);
4 ✔
414
      }
415
      else
416
      {
417
        data.emplace_back(std::move(v), timestamp);
3 ✔
418
      }
419
      break;
5 ✔
420
    }
421
    case data_mix_method::mix_append: {
129 ✔
422
      this->data.emplace_back(std::move(v), timestamp);
129 ✔
423
      break;
129 ✔
424
    }
425
    case data_mix_method::mix_merge: {
×
426
      // TODO;
427
      break;
×
428
    }
429
  }
430
}
134 ✔
431

432

433
void value_port::add_local_value(const ossia::typed_value& other)
10 ✔
434
{
435
  // These values come from the local environment
436
  // Convert to the correct type / index
437
  const ossia::complex_type sink_type = effective_type();
10 ✔
438
  if(other.index == index && other.type == sink_type)
19 ✔
439
  {
440
    write_value(other.value, other.timestamp);
5 ✔
441
    return;
5 ✔
442
  }
443

444
  auto v = other.value;
5 ✔
445
  // The same component on both sides is already extracted: only the unit or
446
  // the type can differ.
447
  if(other.index == index)
5 ✔
448
    filter_value(v, {}, {}, other.type, sink_type);
4 ✔
449
  else
450
    filter_value(v, other.index, index, other.type, sink_type);
1 ✔
451
  write_value(v, other.timestamp);
5 ✔
452
}
10 ✔
453

454
void value_port::add_global_values(
30 ✔
455
    const net::parameter_base& other, const value_vector<ossia::value>& vec)
456
{
457
  const ossia::complex_type source_type = get_complex_type(other);
30 ✔
458
  const ossia::complex_type sink_type = effective_type();
30 ✔
459
  const auto& other_domain = other.get_domain();
30 ✔
460

461
  // Nothing to adapt to
462
  if(index.empty() && !this->domain && (source_type == sink_type || !source_type))
50 ✔
463
  {
464
    for(const ossia::value& v : vec)
×
465
      write_value(v, 0);
×
466
    return;
×
467
  }
468

469
  for(ossia::value v : vec)
147 ✔
470
  {
471
    adapt_value(v, index, source_type, sink_type, other_domain, this->domain);
29 ✔
472
    write_value(std::move(v), 0); // TODO put correct timestamps here
29 ✔
473
  }
29 ✔
474
}
30 ✔
475

476
void value_port::add_global_value(
49 ✔
477
    const ossia::net::parameter_base& other, ossia::value val)
478
{
479
  const ossia::complex_type source_type = get_complex_type(other);
49 ✔
480

481
  adapt_value(
98 ✔
482
      val, index, source_type, effective_type(), other.get_domain(), this->domain);
49 ✔
483

484
  write_value(std::move(val), 0);
49 ✔
485
}
49 ✔
486

487
void value_port::add_port_values(const value_port& other)
19 ✔
488
{
489
  // These values come from another node: we just copy them blindly
490
  if(should_process_control(other, *this))
19 ✔
491
  {
492
    // Once, not per value: building an effective type copies it.
493
    const ossia::complex_type source_type = other.effective_type();
13 ✔
494
    const ossia::complex_type sink_type = effective_type();
13 ✔
495
    switch(mix_method)
13 ✔
496
    {
497
      case data_mix_method::mix_replace: {
×
498
        for(const auto& v : other.get_data())
×
499
        {
500
          auto it = ossia::find_if(data, [&](const ossia::timed_value& val) {
×
501
            return val.timestamp == v.timestamp;
×
502
          });
×
503
          if(it != data.end())
×
504
          {
505
            it->value = v.value;
×
506
            process_control_value(it->value, other, *this, source_type, sink_type);
×
507
          }
508
          else
509
          {
510
            data.emplace_back(v);
×
511
            process_control_value(data.back().value, other, *this, source_type, sink_type);
×
512
          }
513
        }
514
        break;
×
515
      }
516
      case data_mix_method::mix_append: {
13 ✔
517
        auto it = data.insert(data.end(), other.data.begin(), other.data.end());
26 ✔
518
        for(const auto end = data.end(); it != end; ++it)
52 ✔
519
        {
520
          process_control_value(it->value, other, *this, source_type, sink_type);
13 ✔
521
        }
522
        break;
13 ✔
523
      }
524
      case data_mix_method::mix_merge: {
×
525
        // TODO;
526
        break;
×
527
      }
528
    }
529
  }
13 ✔
530
  else
531
  {
532
    switch(mix_method)
6 ✔
533
    {
534
      case data_mix_method::mix_replace: {
×
535
        for(const auto& v : other.get_data())
×
536
        {
537
          auto it = ossia::find_if(data, [&](const ossia::timed_value& val) {
×
538
            return val.timestamp == v.timestamp;
×
539
          });
×
540
          if(it != data.end())
×
541
          {
542
            it->value = v.value;
×
543
          }
544
          else
545
          {
546
            data.emplace_back(v);
×
547
          }
548
        }
549
        break;
×
550
      }
551
      case data_mix_method::mix_append: {
6 ✔
552
        data.insert(data.end(), other.data.begin(), other.data.end());
12 ✔
553
        break;
6 ✔
554
      }
555
      case data_mix_method::mix_merge: {
×
556
        // TODO;
557
        break;
×
558
      }
559
    }
560
  }
561
}
19 ✔
562

563
void value_port::set_data(const value_vector<ossia::timed_value>& vec)
×
564
{
565
  data = vec;
×
566
}
×
567

568
void value_port::clear()
165 ✔
569
{
570
  data.clear();
165 ✔
571
}
165 ✔
572

573
}
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