OSSIA
Open Scenario System for Interactive Application
Loading...
Searching...
No Matches
rubberband_stretcher.hpp
1#pragma once
2#include <ossia/detail/config.hpp>
3
4#if defined(OSSIA_ENABLE_RUBBERBAND)
5#include <ossia/dataflow/audio_port.hpp>
6#include <ossia/dataflow/audio_stretch_mode.hpp>
7#include <ossia/dataflow/graph_node.hpp>
8#include <ossia/dataflow/nodes/media.hpp>
9#include <ossia/dataflow/token_request.hpp>
10
11#if __has_include(<RubberBandStretcher.h>)
12#include <RubberBandStretcher.h>
13#elif __has_include(<rubberband/RubberBandStretcher.h>)
14#include <rubberband/RubberBandStretcher.h>
15#endif
16
17#include <algorithm>
18#include <cmath>
19#include <vector>
20
21namespace ossia
22{
23static constexpr auto get_rubberband_preset(ossia::audio_stretch_mode mode)
24{
25 using opt_t = RubberBand::RubberBandStretcher::Option;
26 using preset_t = RubberBand::RubberBandStretcher::PresetOption;
27 uint32_t preset = opt_t::OptionProcessRealTime | opt_t::OptionThreadingNever;
28 switch(mode)
29 {
30 case ossia::audio_stretch_mode::RubberBandStandard:
31 break;
32
33 case ossia::audio_stretch_mode::RubberBandPercussive:
34 preset |= preset_t::PercussiveOptions;
35 break;
36
37 case ossia::audio_stretch_mode::RubberBandStandardHQ:
38 preset |= RubberBand::RubberBandStretcher::OptionEngineFiner;
39 preset |= RubberBand::RubberBandStretcher::OptionPitchHighConsistency;
40 break;
41
42 case ossia::audio_stretch_mode::RubberBandPercussiveHQ:
43 preset |= preset_t::PercussiveOptions;
44 preset |= RubberBand::RubberBandStretcher::OptionEngineFiner;
45 preset |= RubberBand::RubberBandStretcher::OptionPitchHighConsistency;
46 break;
47
48 default:
49 break;
50 }
51
52 return preset;
53}
54
55struct rubberband_stretcher
56{
57 // Priming variants exposed for the sound sync test sweeps; production uses
58 // BareRecipe, the recipe the RubberBand documentation prescribes for
59 // real-time mode: pad the input with getPreferredStartPad() zeros and trim
60 // getStartDelay() samples from the output. ZeroPadOnly, the previous
61 // default, skips the trim and therefore plays every stretched sound
62 // getStartDelay() samples (~23 ms at 44.1 kHz) late - late relative to raw
63 // and repitched sounds, and late by a ratio-dependent amount, so two
64 // stretched files at different source tempos flam against each other too.
65 enum class prime_strategy : uint8_t
66 {
67 NoPrime,
68 ZeroPadOnly,
69 BareRecipe,
70 ExtendedDrain,
71 PreRollRealAudio,
72 };
73 static inline prime_strategy s_prime_strategy{prime_strategy::BareRecipe};
74
75 rubberband_stretcher(
76 uint32_t opt, std::size_t channels, std::size_t sampleRate, int64_t pos)
77 : m_rubberBand{std::make_unique<RubberBand::RubberBandStretcher>(
78 sampleRate, channels, opt)}
79 , next_sample_to_read{pos}
80 , options{opt}
81 {
82 m_channels = channels;
83 size_prime_pad();
84 }
85
88 void size_prime_pad()
89 {
90 const std::size_t pad
91 = m_rubberBand ? m_rubberBand->getPreferredStartPad() : 0;
92 if(m_channels > 0 && pad > 0)
93 {
94 m_prime_zero_storage.assign(m_channels * pad, 0.f);
95 m_prime_zero_ptrs.resize(m_channels);
96 for(std::size_t c = 0; c < m_channels; ++c)
97 {
98 m_prime_zero_ptrs[c] = m_prime_zero_storage.data() + c * pad;
99 }
100 }
101 }
102
103 rubberband_stretcher(const rubberband_stretcher&) = delete;
104 rubberband_stretcher& operator=(const rubberband_stretcher&) = delete;
105 rubberband_stretcher(rubberband_stretcher&&) = default;
106 rubberband_stretcher& operator=(rubberband_stretcher&&) = default;
107
108 std::unique_ptr<RubberBand::RubberBandStretcher> m_rubberBand;
109 int64_t next_sample_to_read = 0;
110 uint32_t options{};
111 bool m_needs_prime{true};
112
113 std::size_t m_channels{};
114
115 // Zero-pad scratch buffer for prime(), sized in the ctor.
116 std::vector<float> m_prime_zero_storage;
117 std::vector<float*> m_prime_zero_ptrs;
118
119 [[nodiscard]] int64_t start_delay() const noexcept
120 {
121 return m_rubberBand ? int64_t(m_rubberBand->getStartDelay()) : 0;
122 }
123
126 void set_rate_ratio(double ratio)
127 {
128 const double pitch = (ratio != 0.) ? 1. / ratio : 1.;
129 if(m_rubberBand && pitch != m_rubberBand->getPitchScale())
130 {
131 m_rubberBand->setPitchScale(pitch);
132 size_prime_pad();
133 }
134 }
135
136 void transport(int64_t date)
137 {
138 m_rubberBand->reset();
139 next_sample_to_read = date;
140 m_needs_prime = true;
141 }
142
143 template <typename T>
144 void
145 run(T& audio_fetcher, const ossia::token_request& t, ossia::exec_state_facade e,
146 double tempo_ratio, double rate_ratio, const std::size_t chan,
147 const std::size_t len, int64_t samples_to_read, const int64_t samples_to_write,
148 const int64_t samples_offset, const ossia::mutable_audio_span<double>& ap) noexcept
149 {
150 // Fed at the material's rate and drained one frame per graph frame, so a
151 // rate conversion is a time ratio; the pitch scale undoes the transposition
152 // that comes with it.
153 const double abs_tempo_ratio
154 = std::min(70., std::abs(tempo_ratio) * std::abs(rate_ratio));
155 if(abs_tempo_ratio != m_rubberBand->getTimeRatio())
156 {
157 m_rubberBand->setTimeRatio(abs_tempo_ratio);
158 }
159
160
161 // Lazy pre-roll on first run after ctor/transport(); see prime().
162 if(m_needs_prime) [[unlikely]]
163 {
164 prime(audio_fetcher, chan, t.forward());
165 m_needs_prime = false;
166 }
167
168 // TODO : if T::sample_type == float we could leverage it directly as
169 // input
170 const int max_chan = std::max(chan, m_rubberBand->getChannelCount());
171 const int frames = std::max((int64_t)16, samples_to_read);
172 float** const input = (float**)alloca(sizeof(float*) * max_chan);
173 float** const output = (float**)alloca(sizeof(float*) * max_chan);
174 for(std::size_t i = 0; i < chan; i++)
175 {
176 input[i] = (float*)alloca(sizeof(float) * frames);
177 output[i] = (float*)alloca(sizeof(float) * samples_to_write);
178 }
179 for(std::size_t i = chan; i < m_rubberBand->getChannelCount(); i++)
180 {
181 input[i] = (float*)alloca(sizeof(float) * frames);
182 std::fill_n(input[i], frames, 0.f);
183 output[i] = (float*)alloca(sizeof(float) * samples_to_write);
184 }
185
186 if(t.forward())
187 {
188 while(m_rubberBand->available() < samples_to_write)
189 {
190 audio_fetcher.fetch_audio(next_sample_to_read, samples_to_read, input);
191
192 m_rubberBand->process(input, samples_to_read, false);
193
194 next_sample_to_read += samples_to_read;
195 samples_to_read = 16;
196 }
197
198 m_rubberBand->retrieve(
199 output, std::min((int)samples_to_write, m_rubberBand->available()));
200
201 for(std::size_t i = 0; i < chan; i++)
202 {
203 for(int64_t j = 0; j < samples_to_write; j++)
204 {
205 ap[i][j + samples_offset] = double(output[i][j]);
206 }
207 }
208 }
209 else
210 {
211 // Backward playback:
212 while(m_rubberBand->available() < samples_to_write)
213 {
214 audio_fetcher.fetch_audio_backward(next_sample_to_read, samples_to_read, input);
215
216 m_rubberBand->process(input, samples_to_read, false);
217
218 next_sample_to_read -= samples_to_read;
219 samples_to_read = 16;
220 }
221
222 const int retrieved = m_rubberBand->retrieve(
223 output, std::min((int)samples_to_write, m_rubberBand->available()));
224
225 for(std::size_t i = 0; i < chan; i++)
226 {
227 for(int64_t j = 0; j < samples_to_write; j++)
228 {
229 ap[i][j + samples_offset] = double(output[i][j]);
230 }
231 }
232 }
233 }
234
235private:
236 // Run the s_prime_strategy variant before the first audible frame.
237 template <typename T>
238 void prime(T& audio_fetcher, std::size_t chan, bool forward) noexcept
239 {
240 if(!m_rubberBand || chan == 0)
241 return;
242
243 const auto strategy = s_prime_strategy;
244 if(strategy == prime_strategy::NoPrime)
245 return;
246
247 if(strategy == prime_strategy::PreRollRealAudio)
248 {
249 // pad ≈ windowSize/2 per RubberBand::getPreferredStartPad docs.
250 const int64_t window = 2 * int64_t(m_rubberBand->getPreferredStartPad());
251
252 // Fall through to ZeroPadOnly if there isn't enough past content.
253 if(window > 0 && (forward ? next_sample_to_read >= window : true))
254 {
255 constexpr int prime_fetch_chunk = 256;
256 float** const input
257 = (float**)alloca(sizeof(float*) * m_rubberBand->getChannelCount());
258 float** const output
259 = (float**)alloca(sizeof(float*) * m_rubberBand->getChannelCount());
260 for(std::size_t i = 0; i < m_rubberBand->getChannelCount(); i++)
261 {
262 input[i] = (float*)alloca(sizeof(float) * prime_fetch_chunk);
263 output[i] = (float*)alloca(sizeof(float) * prime_fetch_chunk);
264 }
265
266 if(forward)
267 next_sample_to_read -= window;
268 else
269 next_sample_to_read += window;
270
271 int64_t fed = 0;
272 while(fed < window)
273 {
274 const int64_t chunk
275 = std::min<int64_t>(prime_fetch_chunk, window - fed);
276 if(forward)
277 {
278 audio_fetcher.fetch_audio(next_sample_to_read, chunk, input);
279 next_sample_to_read += chunk;
280 }
281 else
282 {
283 audio_fetcher.fetch_audio_backward(next_sample_to_read, chunk, input);
284 next_sample_to_read -= chunk;
285 }
286 m_rubberBand->process(input, int(chunk), false);
287 fed += chunk;
288 }
289
290 // Drain pre-K outputs; feed zeros once real input is exhausted
291 // to avoid consuming the file beyond K.
292 const int64_t target_drain
293 = std::max<int64_t>(0, window - int64_t(m_rubberBand->getStartDelay()));
294 int64_t drained = 0;
295 int safety = 0;
296 while(drained < target_drain && safety++ < 4096)
297 {
298 while(m_rubberBand->available() <= 0)
299 {
300 std::fill_n(input[0], prime_fetch_chunk, 0.f);
301 for(std::size_t i = 1; i < m_rubberBand->getChannelCount(); i++)
302 std::fill_n(input[i], prime_fetch_chunk, 0.f);
303 m_rubberBand->process(input, prime_fetch_chunk, false);
304 }
305 const int avail = m_rubberBand->available();
306 const int to_take = int(std::min<int64_t>(
307 target_drain - drained,
308 std::min<int64_t>(avail, prime_fetch_chunk)));
309 m_rubberBand->retrieve(output, to_take);
310 drained += to_take;
311 }
312 return;
313 }
314 }
315
316 const int64_t toPad = int64_t(m_rubberBand->getPreferredStartPad());
317 if(toPad <= 0)
318 return;
319
320 // Bail without allocating if a later setPitchScale() outgrew the buffer.
321 if(int64_t(m_prime_zero_storage.size()) < int64_t(chan) * toPad
322 || int64_t(m_prime_zero_ptrs.size()) < int64_t(chan))
323 {
324 return;
325 }
326
327 m_rubberBand->process(
328 m_prime_zero_ptrs.data(), int(toPad), /*final=*/false);
329
330 if(strategy == prime_strategy::ZeroPadOnly
331 || strategy == prime_strategy::PreRollRealAudio)
332 return;
333
334 int64_t drain_target;
335 if(strategy == prime_strategy::ExtendedDrain)
336 {
337 drain_target = 2 * toPad;
338 }
339 else
340 {
341 drain_target = int64_t(m_rubberBand->getStartDelay());
342
343 // The R2 engine reports aWindowSize/2 scaled only by the pitch, never
344 // by the time ratio, but the sample where input 0 actually surfaces in
345 // the output moves with the ratio. Measured with the click-track
346 // harness (SoundSyncTest) over ratios 0.52..1.17, the position is
347 // startDelay + ~0.375 * pad * (1 - ratio) within R2's own transient
348 // jitter, so trim that much more (or less). R3 accounts for the ratio
349 // itself.
350 if(!(options & RubberBand::RubberBandStretcher::OptionEngineFiner))
351 {
352 drain_target += int64_t(std::llround(
353 0.375 * double(toPad) * (1.0 - m_rubberBand->getTimeRatio())));
354 }
355 if(drain_target < 0)
356 drain_target = 0;
357 }
358 if(drain_target <= 0)
359 return;
360
361 constexpr int prime_fetch_chunk = 256;
362 float** const input
363 = (float**)alloca(sizeof(float*) * m_rubberBand->getChannelCount());
364 float** const output
365 = (float**)alloca(sizeof(float*) * m_rubberBand->getChannelCount());
366 for(std::size_t i = 0; i < m_rubberBand->getChannelCount(); i++)
367 {
368 input[i] = (float*)alloca(sizeof(float) * prime_fetch_chunk);
369 output[i] = (float*)alloca(sizeof(float) * prime_fetch_chunk);
370 std::fill_n(input[i], prime_fetch_chunk, 0.f);
371 }
372
373 int64_t drained = 0;
374 int safety = 0;
375 while(drained < drain_target && safety++ < 4096)
376 {
377 while(m_rubberBand->available() <= 0)
378 {
379 if(forward)
380 {
381 audio_fetcher.fetch_audio(
382 next_sample_to_read, prime_fetch_chunk, input);
383 next_sample_to_read += prime_fetch_chunk;
384 }
385 else
386 {
387 audio_fetcher.fetch_audio_backward(
388 next_sample_to_read, prime_fetch_chunk, input);
389 next_sample_to_read -= prime_fetch_chunk;
390 }
391 m_rubberBand->process(input, prime_fetch_chunk, false);
392 }
393
394 const int available = m_rubberBand->available();
395 const int to_take = int(std::min<int64_t>(
396 drain_target - drained, std::min<int64_t>(available, prime_fetch_chunk)));
397 m_rubberBand->retrieve(output, to_take);
398 drained += to_take;
399 }
400 }
401};
402}
403#else
404#include <ossia/dataflow/nodes/timestretch/raw_stretcher.hpp>
405
406namespace ossia
407{
408static constexpr uint32_t get_rubberband_preset(ossia::audio_stretch_mode mode)
409{
410 return 0;
411}
412using rubberband_stretcher = raw_stretcher;
413}
414#endif
Definition git_info.h:7