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