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

ossia / score / 35899133125

23 Sep 2026 05:57PM UTC coverage: 41.488% (+1.2%) from 40.337%
35899133125

push

github

jcelerier
3rdparty: follow the merged libossia

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01PWVTQ1pm3X2Uz7goFgULtC

123236 of 297041 relevant lines covered (41.49%)

113626.05 hits per line

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

65.07
/src/plugins/score-plugin-media/Media/MediaFileHandle.cpp
1
#include "MediaFileHandle.hpp"
2

3
#include <Audio/Settings/Model.hpp>
4
#include <Media/AudioDecoder.hpp>
5
#include <Media/Libav.hpp>
6
#include <Media/RMSData.hpp>
7

8
#include <score/application/GUIApplicationContext.hpp>
9
#include <score/document/DocumentContext.hpp>
10
#include <score/serialization/DataStreamVisitor.hpp>
11
#include <score/serialization/JSONVisitor.hpp>
12
#include <score/tools/Bind.hpp>
13
#include <score/tools/File.hpp>
14

15
#include <core/document/Document.hpp>
16

17
#include <ossia/dataflow/audio_stretch_mode.hpp>
18
#include <ossia/detail/apply.hpp>
19
#include <ossia/detail/libav.hpp>
20
#include <ossia/detail/ssize.hpp>
21

22
#include <QDebug>
23
#include <QFileInfo>
24
#include <QRegularExpression>
25
#include <QtGlobal>
26

27
#include <optional>
28
namespace Media
29
{
30

31
// TODO if it's smaller than e.g. 1 megabyte, it would be worth
32
// loading it in memory entirely..
33
// TODO might make sense to do resampling during execution if it's not too
34
// expensive?
35
static DecodingMethod needsDecoding(const QString& path, int rate)
69✔
36
{
37
  const auto sz = QFileInfo{path}.size();
69✔
38

39
#if defined(__EMSCRIPTEN__)
40
  // wasm/MEMFS policy:
41
  //  - mmap (drwav) is unavailable/unsafe over MEMFS -> never choose it,
42
  //  - large media must stream instead of fully decoding into the (2GB-capped)
43
  //    heap: route >48MB and all video to LibavStream (lazy, on-demand decode),
44
  //  - small files decode fully to RAM (Libav) for fast random access / waveform.
45
  {
46
    constexpr qint64 large_threshold = 48ll * 1024 * 1024;
47
    if(sz > large_threshold || AudioFile::isSupportedVideo(QFile{path}))
48
      return DecodingMethod::LibavStream;
49
    return DecodingMethod::Libav;
50
  }
51
#else
52
  constexpr qint64 large_threshold = 4096ll * 1024 * 1024;
69✔
53

54
  // Rate mismatches are converted in the graph, so only decodability and RAM
55
  // cost decide here -- unless the graph has no converter at all.
56
  const auto rate_ok = [rate](const AudioInfo& info) {
136✔
57
    return ossia::graph_resampling || info.fileRate == rate;
67✔
58
  };
59

60
  if(path.endsWith("wav", Qt::CaseInsensitive)
138✔
61
     || path.endsWith("w64", Qt::CaseInsensitive))
69✔
62
  {
63
    const auto& info = probe(path);
68✔
64

65
    if(info && (info->flags & AudioInfo::DrwavCanDecode) && rate_ok(*info))
68✔
66
      return DecodingMethod::Mmap;
67✔
67
    else if(sz > large_threshold)
1✔
68
        return DecodingMethod::LibavStream;
×
69
    else
70
      return DecodingMethod::Libav;
1✔
71
  }
72
  else if(
73
      path.endsWith("aiff", Qt::CaseInsensitive)
2✔
74
      || path.endsWith("aif", Qt::CaseInsensitive)
1✔
75
      || path.endsWith("aifc", Qt::CaseInsensitive)
1✔
76
      || path.endsWith("caf", Qt::CaseInsensitive))
1✔
77
  {
78
    const auto& info = probe(path);
×
79
    // Sndfile reads the whole file into RAM, so it stays behind the size guard.
80
    if(info && (info->flags & AudioInfo::SndfileCanDecode) && sz <= large_threshold
×
81
       && rate_ok(*info))
×
82
      return DecodingMethod::Sndfile;
×
83
    else if(sz > large_threshold)
×
84
      return DecodingMethod::LibavStream;
×
85
    else
86
      return DecodingMethod::Libav;
×
87
  }
88
  else
89
  {
90
    if(AudioFile::isSupportedVideo(QFile{path}))
1✔
91
      return DecodingMethod::LibavStream;
×
92
    else if(sz > large_threshold)
1✔
93
      return DecodingMethod::LibavStream;
×
94
    else
95
      return DecodingMethod::Libav;
1✔
96
  }
97
#endif
98
}
69✔
99

100
static std::optional<DecodingMethod> forcedDecodingMethod()
10✔
101
{
102
  std::optional<DecodingMethod> res;
10✔
103
  const auto e = qEnvironmentVariable("SCORE_AUDIO_DECODING_METHOD").toLower();
10✔
104
  if(e.isEmpty())
10✔
105
    return res;
10✔
106
  if(e == "libav_ram")
×
107
    return DecodingMethod::Libav;
×
108
  if(e == "libav_stream")
×
109
    return DecodingMethod::LibavStream;
×
110
  if(e == "mmap")
×
111
    return DecodingMethod::Mmap;
×
112
  if(e == "sndfile")
×
113
    return DecodingMethod::Sndfile;
×
114
  return res;
×
115
}
10✔
116

117
AudioFile::AudioFile()
248✔
118
{
124✔
119
  m_impl = Handle{};
124✔
120
  m_rms = new RMSData{};
124✔
121
}
×
122

123
AudioFile::~AudioFile()
123✔
124
{
×
125
  delete m_rms;
123✔
126
}
123✔
127

128
void AudioFile::load(DecodingSetup opt)
83✔
129
{
130
  m_originalFile = opt.filePath;
83✔
131
  m_file = opt.absoluteFilePath;
83✔
132
  m_track = opt.track;
83✔
133

134
  const auto& audioSettings = score::GUIAppContext().settings<Audio::Settings::Model>();
83✔
135
  const auto rate = audioSettings.getRate();
83✔
136

137
  static const auto forced_method = forcedDecodingMethod();
83✔
138
  if(forced_method)
83✔
139
    opt.method = *forced_method;
×
140
  else if(m_track != -1)
83✔
141
    opt.method = DecodingMethod::LibavStream;
4✔
142
  else if(opt.method == DecodingMethod::Invalid)
79✔
143
    opt.method = needsDecoding(m_file, rate);
69✔
144

145
  switch(opt.method)
83✔
146
  {
147
    case DecodingMethod::Libav:
148
      load_libav(rate);
2✔
149
      break;
2✔
150
    case DecodingMethod::Mmap:
151
      load_drwav();
75✔
152
      break;
75✔
153
    case DecodingMethod::Sndfile:
154
      load_sndfile();
1✔
155
      break;
1✔
156
    case DecodingMethod::LibavStream:
157
      if(m_track == -1)
5✔
158
      {
159
        const auto& info = probe(m_file);
1✔
160
        // The drwav and sndfile probes leave -1; those formats hold one stream.
161
        if(info && info->audioStream >= 0)
1✔
162
        {
163
          m_track = info->audioStream;
1✔
164
        }
1✔
165
        else
166
          m_track = 0;
×
167
      }
1✔
168
      load_libav_stream();
5✔
169
      break;
5✔
170
    default:
171
      break;
×
172
  }
173
}
83✔
174

175
int64_t AudioFile::decodedSamples() const
168✔
176
{
177
  struct
178
  {
179
    int64_t operator()(ossia::monostate) const noexcept { return 0; }
×
180
    int64_t operator()(const libav_ptr& r) const noexcept { return r->decoder.decoded; }
15✔
181
    int64_t operator()(const libav_stream_ptr& r) const noexcept { return r.samples; }
×
182
    int64_t operator()(const sndfile_ptr& r) const noexcept { return r.decoder.decoded; }
2✔
183
    int64_t operator()(const mmap_ptr& r) const noexcept
151✔
184
    {
185
      return r.wav.totalPCMFrameCount();
151✔
186
    }
187
  } _;
188
  return ossia::apply(_, m_impl);
168✔
189
}
190

191
bool AudioFile::isSupported(const QFile& file)
5✔
192
{
193
  constexpr auto rex
5✔
194
      = ".(wav|mp3|m4a|ogg|flac|aif|aiff|aifc|w64|ape|wv|wma|aac|caf|opus|ac3|dts|dtshd)"
195
        "$";
196
  return file.exists()
8✔
197
         && file.fileName().contains(
8✔
198
             QRegularExpression(rex, QRegularExpression::CaseInsensitiveOption));
3✔
199
}
×
200

201
bool AudioFile::isSupportedVideo(const QFile& file)
1✔
202
{
203
  constexpr auto rex = ".(mkv|mov|mp4|h264|avi|hap|mpg|mpeg|imf|mxf|mts|m2ts|mj2|webm|y4m|nut|ts)$";
1✔
204
  return file.exists()
1✔
205
         && file.fileName().contains(
1✔
206
             QRegularExpression(rex, QRegularExpression::CaseInsensitiveOption));
×
207
}
×
208

209
int64_t AudioFile::samples() const
335✔
210
{
211
  struct
212
  {
213
    int64_t operator()(ossia::monostate) const noexcept { return 0; }
1✔
214
    int64_t operator()(const libav_ptr& r) const noexcept
×
215
    {
216
      const auto& samples = r->handle->data;
×
217
      return samples.size() > 0 ? samples[0].size() : 0;
×
218
    }
219
    int64_t operator()(const libav_stream_ptr& r) const noexcept { return r.samples; }
2✔
220
    int64_t operator()(const sndfile_ptr& r) const noexcept
×
221
    {
222
      const auto& samples = r.handle->data;
×
223
      return samples.size() > 0 ? samples[0].size() : 0;
×
224
    }
225
    int64_t operator()(const mmap_ptr& r) const noexcept
332✔
226
    {
227
      return r.wav.totalPCMFrameCount();
332✔
228
    }
229
  } _;
230
  return ossia::apply(_, m_impl);
335✔
231
}
232

233
int64_t AudioFile::channels() const
826✔
234
{
235
  struct
236
  {
237
    int64_t operator()(ossia::monostate) const noexcept { return 0; }
12✔
238
    int64_t operator()(const libav_ptr& r) const noexcept
9✔
239
    {
240
      return r->handle->data.size();
9✔
241
    }
242
    int64_t operator()(const libav_stream_ptr& r) const noexcept { return r.channels; }
2✔
243
    int64_t operator()(const sndfile_ptr& r) const noexcept
3✔
244
    {
245
      return r.handle->data.size();
3✔
246
    }
247
    int64_t operator()(const mmap_ptr& r) const noexcept { return r.wav.channels(); }
799✔
248
  } _;
249
  return ossia::apply(_, m_impl);
826✔
250
}
251

252
const RMSData& AudioFile::rms() const
309✔
253
{
254
  SCORE_ASSERT(m_rms);
309✔
255
  return *m_rms;
309✔
256
}
257

258
std::shared_ptr<const WaveformSummary> AudioFile::waveformSummary() const noexcept
52✔
259
{
260
  // Only once the file is complete: a table that grew behind the threads
261
  // walking it would need locking on every read.
262
  if(!finishedDecoding())
52✔
263
    return {};
×
264

265
  std::lock_guard _{m_summaryMutex};
52✔
266
  if(m_summary)
52✔
267
    return m_summary;
5✔
268
  if(m_summaryUnavailable)
47✔
269
    return {};
×
270

271
  ViewHandle h{m_impl};
47✔
272
  if(!h.supports_summary())
47✔
273
  {
274
    // What a source can do does not change under us, so this never retries.
275
    m_summaryUnavailable = true;
×
276
    return {};
×
277
  }
278

279
  const int64_t frames = decodedSamples();
47✔
280
  const int32_t chan = channels();
47✔
281
  if(frames <= 0 || chan <= 0)
47✔
282
    return {};
×
283

284
  auto s = std::make_shared<WaveformSummary>();
47✔
285
  s->channels = chan;
47✔
286
  s->frames = frames;
47✔
287
  s->bucket = WaveformSummary::bucketFor(frames, chan);
47✔
288
  s->bucketCount = (frames + s->bucket - 1) / s->bucket;
47✔
289
  s->data = std::make_unique<FloatPair[]>(std::size_t(s->bucketCount) * chan);
47✔
290

291
  if(!h.build_summary(*s))
47✔
292
  {
293
    m_summaryUnavailable = true;
×
294
    return {};
×
295
  }
296

297
  m_summary = std::move(s);
47✔
298
  return m_summary;
47✔
299
}
52✔
300

301
std::optional<double> AudioFile::knownTempo() const noexcept
38✔
302
{
303
  auto& db = AudioDecoder::database();
38✔
304
  if(auto it = db.find(this->m_file); it != db.end())
38✔
305
  {
306
    return it->tempo;
38✔
307
  }
308
  return {};
×
309
}
38✔
310

311
AudioFileManager::AudioFileManager() noexcept
8✔
312
{
8✔
313
  auto& audioSettings = score::GUIAppContext().settings<Audio::Settings::Model>();
8✔
314
  con(audioSettings, &Audio::Settings::Model::RateChanged, this,
16✔
315
      [this](auto newRate) { m_handles.clear(); });
8✔
316
}
8✔
317

318
AudioFileManager::~AudioFileManager() noexcept { }
8✔
319

320
AudioFileManager& AudioFileManager::instance() noexcept
85✔
321
{
322
  static AudioFileManager m;
85✔
323
  return m;
85✔
324
}
325

326
std::shared_ptr<AudioFile>
327
AudioFileManager::get(const QString& path, int stream, const score::DocumentContext& ctx)
79✔
328
{
329
  // TODO what would be a good garbage collection mechanism ?
330
  auto abspath = score::locateFilePath(path, ctx);
79✔
331
  StreamInfo k{abspath, stream};
79✔
332
  if(auto it = m_handles.find(k); it != m_handles.end())
79✔
333
  {
334
    return it->second;
12✔
335
  }
336

337
  auto r = std::make_shared<AudioFile>();
67✔
338
  r->load({path, abspath, DecodingMethod::Invalid, stream});
67✔
339
  m_handles.insert({k, r});
67✔
340
  return r;
67✔
341
}
146✔
342

343
std::shared_ptr<AudioFile> AudioFileManager::get(const QString& abspath, int stream)
6✔
344
{
345
  // TODO what would be a good garbage collection mechanism ?
346
  StreamInfo k{abspath, stream};
6✔
347
  if(auto it = m_handles.find(k); it != m_handles.end())
6✔
348
  {
349
    return it->second;
2✔
350
  }
351

352
  auto r = std::make_shared<AudioFile>();
4✔
353
  r->load({abspath, abspath, DecodingMethod::Invalid, stream});
4✔
354
  m_handles.insert({k, r});
4✔
355
  return r;
4✔
356
}
10✔
357

358
AudioFile::ViewHandle::ViewHandle(const AudioFile::Handle& handle)
105✔
359
{
360
  struct
361
  {
362
    view_impl_t& self;
363
    void operator()(ossia::monostate) const noexcept { }
×
364
    void operator()(const libav_ptr& r) const noexcept { self = RAMView{r->data}; }
6✔
365
    void operator()(const libav_stream_ptr& r) const noexcept
×
366
    {
367
#if SCORE_HAS_LIBAV
368
      auto ptr = std::make_shared<ossia::libav_handle>();
×
369
      ptr->open(r.path, r.stream, 0);
×
370
      if(ptr)
×
371
      {
372
        self = StreamView{std::move(ptr)};
×
373
      }
×
374
#endif
375
    }
×
376
    void operator()(const sndfile_ptr& r) const noexcept { self = RAMView{r.data}; }
3✔
377
    void operator()(const mmap_ptr& r) const noexcept
96✔
378
    {
379
      if(r.wav)
96✔
380
      {
381
        self = MmapView{r.wav};
96✔
382
      }
96✔
383
    }
96✔
384
  } _{*this};
105✔
385

386
  ossia::apply(_, handle);
105✔
387
}
105✔
388

389
ossia::audio_array AudioFile::getAudioArray() const
×
390
{
391
  struct
392
  {
393
    int64_t frames{};
394
    ossia::audio_array out;
395

396
    void operator()(ossia::monostate) noexcept { }
×
397
    void operator()(const Media::AudioFile::RAMView& av) noexcept
×
398
    {
399
      const int channels = av.data.size();
×
400
      out.resize(channels);
×
401
      for(int i = 0; i < channels; i++)
×
402
      {
403
        out[i].assign(av.data[i], av.data[i] + frames);
×
404
      }
×
405
    }
×
406

407
    void operator()(const Media::AudioFile::MmapView& av) noexcept
×
408
    {
409
      const int channels = av.wav.channels();
×
410
      out.resize(channels);
×
411

412
      // OPTIMIZEME we can just read directly into out[c][i]..
413
      auto data = std::make_unique<float[]>(frames * channels);
×
414
      drwav_read_pcm_frames_f32(av.wav.wav(), frames, data.get());
×
415
      for(int i = 0; i < channels; i++)
×
416
      {
417
        out[i].resize(frames);
×
418
      }
×
419

420
      for(int64_t i = 0; i < frames; i++)
×
421
      {
422
        for(int c = 0; c < channels; c++)
×
423
        {
424
          out[c][i] = data.get()[i * channels + c];
×
425
        }
×
426
      }
×
427
    }
×
428

429
    void operator()(const Media::AudioFile::StreamView& av) noexcept { }
×
430

431
  } vis{this->decodedSamples(), {}};
×
432

433
  ossia::visit(vis, this->handle());
×
434

435
  return vis.out;
×
436
}
×
437

438
std::optional<AudioInfo> probe_drwav(const QFileInfo& fi);
439
std::optional<AudioInfo> probe(const QString& path)
79✔
440
{
441
  // FIXME we have to reload everything when the sample rate changes !!
442
  QString real_path = QFileInfo{path}.canonicalFilePath();
79✔
443
  auto& db = AudioDecoder::database();
79✔
444
  auto it = db.find(real_path);
79✔
445
  if(it == db.end())
79✔
446
  {
447
    QFileInfo fi{path};
74✔
448
    if(!fi.exists() || !fi.isFile() || !fi.isReadable())
74✔
449
      return std::nullopt;
1✔
450

451
    const auto& suffix = fi.suffix().toLower();
73✔
452
    if(suffix == "wav" || suffix == "w64")
73✔
453
    {
454
      if(auto ret = probe_drwav(fi))
73✔
455
      {
456
        db.insert(path, *ret);
73✔
457
        if(path != real_path)
73✔
458
          db.insert(real_path, *ret);
×
459
        return ret;
73✔
460
      }
461
    }
×
462

463
    if(suffix == "wav" || suffix == "w64" || suffix == "aif" || suffix == "aiff"
×
464
       || suffix == "aifc" || suffix == "caf")
×
465
    {
466
      if(auto ret = SndfileDecoder::do_probe(path))
×
467
      {
468
        db.insert(path, *ret);
×
469
        if(path != real_path)
×
470
          db.insert(real_path, *ret);
×
471
        return ret;
×
472
      }
473
    }
×
474

475
    if(auto ret = AudioDecoder::do_probe(path))
×
476
    {
477
      db.insert(path, *ret);
×
478
      if(path != real_path)
×
479
        db.insert(real_path, *ret);
×
480
      return ret;
×
481
    }
482
    return std::nullopt;
×
483
  }
74✔
484
  else
485
  {
486
    return *it;
5✔
487
  }
488
}
79✔
489
}
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