OSSIA
Open Scenario System for Interactive Application
Loading...
Searching...
No Matches
sound.hpp
1#pragma once
2#include <ossia/audio/fade.hpp>
3#include <ossia/dataflow/audio_stretch_mode.hpp>
4#include <ossia/dataflow/node_process.hpp>
5#include <ossia/dataflow/nodes/media.hpp>
6#include <ossia/dataflow/nodes/timestretch/raw_stretcher.hpp>
7#include <ossia/dataflow/nodes/timestretch/repitch_stretcher.hpp>
8#include <ossia/dataflow/nodes/timestretch/rubberband_stretcher.hpp>
9#include <ossia/dataflow/port.hpp>
10#include <ossia/dataflow/sample_to_float.hpp>
11#include <ossia/detail/pod_vector.hpp>
12#include <ossia/detail/variant.hpp>
13
14#include <algorithm>
15
16namespace ossia
17{
18namespace snd
19{
20struct sample_read_info
21{
22 int64_t samples_to_read{};
23 int64_t samples_to_write{};
24};
25
26inline auto
27sample_info(int64_t bufferSize, double durationRatio, const ossia::token_request& t)
28{
29 sample_read_info _;
30 if(t.paused())
31 return _;
32
33 if(t.speed == 0.0)
34 return _;
35
36 _.samples_to_read = t.physical_read_duration(durationRatio);
37
38 const auto room = t.safe_physical_write_duration(durationRatio, bufferSize);
39 const auto tick_dur = t.physical_write_duration(durationRatio);
40 _.samples_to_write = std::min(tick_dur, std::max<int64_t>(0, room));
41
42 return _;
43}
44
45// Output samples per input sample of the material.
46inline double rate_ratio(int64_t material_rate, int64_t graph_rate) noexcept
47{
48 if(material_rate <= 0 || graph_rate <= 0 || material_rate == graph_rate)
49 return 1.;
50 return double(graph_rate) / double(material_rate);
51}
52
53inline void
54perform_upmix(const std::size_t upmix, const std::size_t chan, ossia::audio_port& ap)
55{
56 // Upmix
57 if(upmix != 0)
58 {
59 if(upmix < chan)
60 {
61 /* TODO
62 // Downmix
63 switch(upmix)
64 {
65 case 1:
66 {
67 for(std::size_t i = 1; i < chan; i++)
68 {
69 if(ap.channel(0).size() < ap.channel(i).size())
70 ap.channel(0).resize(ap.channel(i).size());
71
72 for(std::size_t j = 0; j < ap.channel(i).size(); j++)
73 ap.channel(0)[j] += ap.channel(i)[j];
74 }
75 }
76 default:
77 // TODO
78 break;
79 }
80 */
81 }
82 else if(upmix > chan)
83 {
84 switch(chan)
85 {
86 case 1: {
87 for(std::size_t chan = 1; chan < upmix; ++chan)
88 ap.channel(chan).assign(ap.channel(0).begin(), ap.channel(0).end());
89 break;
90 }
91 default:
92 // TODO
93 break;
94 }
95 }
96 }
97}
98
99inline void perform_start_offset(const std::size_t start, ossia::audio_port& ap)
100{
101 if(start != 0)
102 {
103 ap.get().insert(ap.get().begin(), start, ossia::audio_channel{});
104 }
105}
106}
107
108template <typename T>
109struct at_end
110{
111 T func;
112 at_end(T t)
113 : func{t}
114 {
115 }
116 ~at_end() { func(); }
117};
118
119struct resampler
120{
121 enum
122 {
123 RawStretcher = 0,
124 RubberbandStretcher = 1,
125 RepitchStretcher = 2
126 };
127 [[nodiscard]] int64_t next_sample_to_read() const noexcept
128 {
129 return ossia::visit(
130 [](auto& stretcher) noexcept { return stretcher.next_sample_to_read; },
131 m_stretch);
132 }
133
134 // Warm-up input samples needed before stretcher output aligns with input.
135 [[nodiscard]] int64_t start_delay() const noexcept
136 {
137 return ossia::visit(
138 [](auto& stretcher) noexcept -> int64_t { return stretcher.start_delay(); },
139 m_stretch);
140 }
141
142 void transport(int64_t date)
143 {
144 ossia::visit(
145 [=](auto& stretcher) noexcept { return stretcher.transport(date); }, m_stretch);
146 }
147
152 void reset(
153 int64_t date, ossia::audio_stretch_mode mode, std::size_t channels,
154 std::size_t materialSampleRate, std::size_t graphSampleRate = 0)
155 {
156 if(graphSampleRate == 0)
157 graphSampleRate = materialSampleRate;
158 [[maybe_unused]] const bool converts
159 = materialSampleRate != graphSampleRate && materialSampleRate > 0
160 && graphSampleRate > 0;
161 // TODO use the date parameter to buffer ! else transport won't work
162 switch(mode)
163 {
164 default:
165 case ossia::audio_stretch_mode::None: {
166#if defined(OSSIA_ENABLE_LIBSAMPLERATE)
167 if(converts)
168 {
169 constexpr auto preset = get_samplerate_preset(
170 ossia::audio_stretch_mode::Repitch);
171 if(auto s = ossia::get_if<RepitchStretcher>(&m_stretch);
172 s && s->repitchers.size() == channels && s->preset == preset)
173 {
174 s->transport(date);
175 }
176 else
177 {
178 m_stretch.emplace<repitch_stretcher>(preset, channels, 1024, date);
179 }
180 break;
181 }
182#endif
183 if(auto s = ossia::get_if<RawStretcher>(&m_stretch))
184 {
185 s->transport(date);
186 }
187 else
188 {
189 m_stretch.emplace<RawStretcher>(date);
190 }
191 break;
192 }
193
194#if defined(OSSIA_ENABLE_RUBBERBAND)
195 case ossia::audio_stretch_mode::RubberBandStandard:
196 case ossia::audio_stretch_mode::RubberBandPercussive:
197 case ossia::audio_stretch_mode::RubberBandStandardHQ:
198 case ossia::audio_stretch_mode::RubberBandPercussiveHQ: {
199 const auto preset = get_rubberband_preset(mode);
200 if(auto s = ossia::get_if<RubberbandStretcher>(&m_stretch);
201 s && s->options == preset)
202 {
203 s->transport(date);
204 }
205 else
206 {
207 m_stretch.emplace<rubberband_stretcher>(
208 preset, channels, materialSampleRate, date);
209 }
210 ossia::get<RubberbandStretcher>(m_stretch)
211 .set_rate_ratio(snd::rate_ratio(materialSampleRate, graphSampleRate));
212 break;
213 }
214#endif
215
216#if defined(OSSIA_ENABLE_LIBSAMPLERATE)
217 case ossia::audio_stretch_mode::Repitch:
218 case ossia::audio_stretch_mode::RepitchMediumQ:
219 case ossia::audio_stretch_mode::RepitchFastestQ: {
220 const auto preset = get_samplerate_preset(mode);
221 if(auto s = ossia::get_if<RepitchStretcher>(&m_stretch);
222 s && s->repitchers.size() == channels && s->preset == preset)
223 {
224 s->transport(date);
225 }
226 else
227 {
228 // FIXME why 1024 here ?!
229 m_stretch.emplace<repitch_stretcher>(preset, channels, 1024, date);
230 }
231 break;
232 }
233#endif
234 }
235
236 // A mode whose backend is compiled out falls back to raw_stretcher.
237 m_stretching = (mode != ossia::audio_stretch_mode::None)
238 && (m_stretch.index() != RawStretcher);
239 }
240
241 template <typename T>
242 void
243 run(T& audio_fetcher, const ossia::token_request& t, ossia::exec_state_facade e,
244 double tempo_ratio, double rate_ratio, std::size_t chan, std::size_t len,
245 int64_t samples_to_read, int64_t samples_to_write, int64_t samples_offset,
246 const ossia::mutable_audio_span<double>& ap)
247 {
248 ossia::visit(
249 [&](auto& stretcher) {
250 stretcher.run(
251 audio_fetcher, t, e, tempo_ratio, rate_ratio, chan, len, samples_to_read,
252 samples_to_write, samples_offset, ap);
253 },
254 m_stretch);
255 }
256
258 [[nodiscard]] bool stretch() const noexcept { return m_stretching; }
259
260private:
261 bool m_stretching{};
262
263 ossia::variant<
264 raw_stretcher
265#if defined(OSSIA_ENABLE_RUBBERBAND)
266 ,
267 rubberband_stretcher
268#endif
269#if defined(OSSIA_ENABLE_LIBSAMPLERATE)
270 ,
271 repitch_stretcher
272#endif
273 >
274 m_stretch;
275};
276
277struct sound_processing_info
278{
279 time_value m_prev_date{time_value::infinite_min};
280
281 time_value m_loop_duration{};
282 time_value m_start_offset{};
283
284 double tempo{};
285
286 int64_t m_loop_duration_samples{};
287 int64_t m_start_offset_samples{};
288
289 double m_last_stretch{1.0};
290 ossia::resampler m_resampler{};
291
292 bool m_loops{};
293
294 void set_loop_info(
295 ossia::time_value loop_duration, ossia::time_value start_offset, bool loops)
296 {
297 m_loop_duration = loop_duration;
298 m_start_offset = start_offset;
299 m_loops = loops;
300 }
301
302 void set_resampler(ossia::resampler&& r)
303 {
304 auto date = m_resampler.next_sample_to_read();
305 m_resampler = std::move(r);
306 m_resampler.transport(date);
307 }
308
309 void set_native_tempo(double v) { tempo = v; }
310
311 // File sample an already-playing sound has reached at a given model date,
312 // i.e. where a sound dropped in mid-playback must seek to align with it.
313 //
314 // When stretching, both the model clock and the file consumption scale
315 // with the live tempo: the interval advances its date by
316 // tempo / root_tempo model samples per physical sample, and the stretcher
317 // consumes tempo / file_tempo file samples per physical sample. Their
318 // ratio is root_tempo / file_tempo whatever the transport tempo, tempo
319 // curve or speed are doing, so the file position is a pure function of
320 // the model date and the file's own tempo. Scaling by the *live* tempo
321 // here - as this used to do - lands wrong by a factor of
322 // live_tempo / root_tempo, i.e. it is only right at 120 BPM.
323 //
324 // In raw mode the file advances at one sample per physical sample while
325 // the model still runs at tempo / root_tempo, so there the live tempo is
326 // exactly what is needed to unscale the date; when it is not known
327 // (timeline_tempo == 0), the date is used as-is.
328 [[nodiscard]] int64_t file_sample_for_model_time(
329 time_value date, double timeline_tempo,
330 int file_sample_rate) const noexcept
331 {
332 const int64_t base = to_sample(date, file_sample_rate);
333 if(m_resampler.stretch() && tempo > 0.0)
334 return int64_t(std::llround(double(base) * ossia::root_tempo / tempo));
335
336 const double abs_tempo = std::abs(timeline_tempo);
337 if(abs_tempo > 0.0)
338 return int64_t(std::llround(double(base) * ossia::root_tempo / abs_tempo));
339 return base;
340 }
341
344 double update_stretch(
345 const ossia::token_request& t, const ossia::exec_state_facade& e,
346 double rate_ratio = 1.) noexcept
347 {
348 double stretch_ratio = 1.;
349 double model_ratio = 1.;
350 if(tempo != 0. && m_resampler.stretch())
351 {
352 model_ratio = ossia::root_tempo / this->tempo;
353 stretch_ratio = this->tempo / t.tempo;
354 }
355
356 if(!(rate_ratio > 0.))
357 rate_ratio = 1.;
358 const double to_material = e.modelToSamples() * model_ratio / rate_ratio;
359
360 m_loop_duration_samples = m_loop_duration.impl * to_material;
361 m_start_offset_samples = m_start_offset.impl * to_material;
362 return stretch_ratio;
363 }
364};
365
366class sound_node
367 : public ossia::nonowning_graph_node
368 , public sound_processing_info
369{
370public:
371 virtual void transport(time_value date) = 0;
372
373 // Tempo-aware overload; resampling subclasses override to seek the file
374 // pointer correctly when file_tempo != timeline_tempo. current_tempo == 0
375 // means unknown and falls back to the non-stretch path.
376 virtual void transport(
377 time_value date, const ossia::tick_transport_info& transport_info)
378 {
379 (void)transport_info;
380 transport(date);
381 }
382};
383
384class dummy_sound_node final : public sound_node
385{
386public:
387 ossia::audio_outlet audio_out;
388 dummy_sound_node()
389 {
390 // Add a dummy outlet so that interval can connect propagation to it
391 m_outlets.push_back(&audio_out);
392 }
393
394 std::string label() const noexcept override { return "dummy_sound_node"; }
395
396 void transport(time_value date) override { }
397
398 void run(const ossia::token_request& t, ossia::exec_state_facade e) noexcept override
399 {
400 }
401};
402
403#if defined(OSSIA_SCENARIO_DATAFLOW)
404class sound_process final : public ossia::node_process
405{
406public:
407 using ossia::node_process::node_process;
408
409protected:
410 void state(const ossia::token_request& req) override
411 {
412 // TODO here we should also pass the execution state so that we can
413 // leverage the timing info & transform loop_duration / start_offset in
414 // samples right here...
415 static_cast<sound_node&>(*this->node)
416 .set_loop_info(m_loop_duration, m_start_offset, m_loops);
417
418 // Start offset and looping are done manually inside the sound nodes
419 // since it is much more efficient in this case
420 // (see fetch_audio)
421 node->request(req);
422 }
423
424 void offset_impl(time_value date) override
425 {
426 static_cast<sound_node&>(*this->node).transport(date);
427 }
428 void transport_impl(time_value date) override
429 {
430 static_cast<sound_node&>(*this->node).transport(date);
431 }
432 void transport_impl(
433 time_value date, const ossia::tick_transport_info& info) override
434 {
435 static_cast<sound_node&>(*this->node).transport(date, info);
436 }
437};
438#endif
439
440}
Definition git_info.h:7
The time_value class.
Definition ossia/editor/scenario/time_value.hpp:30