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