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 SoundTest sweeps; production uses ZeroPadOnly.
58 enum class prime_strategy : uint8_t
59 {
60 NoPrime,
61 ZeroPadOnly,
62 BareRecipe,
63 ExtendedDrain,
64 PreRollRealAudio,
65 };
66 static inline prime_strategy s_prime_strategy{prime_strategy::ZeroPadOnly};
67
68 rubberband_stretcher(
69 uint32_t opt, std::size_t channels, std::size_t sampleRate, int64_t pos)
70 : m_rubberBand{std::make_unique<RubberBand::RubberBandStretcher>(
71 sampleRate, channels, opt)}
72 , next_sample_to_read{pos}
73 , options{opt}
74 {
75 // Pre-size the prime() zero-pad here so the audio thread never allocates.
76 // If a later setPitchScale() inflates the required size, prime() no-ops.
77 const std::size_t pad
78 = m_rubberBand ? m_rubberBand->getPreferredStartPad() : 0;
79 if(channels > 0 && pad > 0)
80 {
81 m_prime_zero_storage.assign(channels * pad, 0.f);
82 m_prime_zero_ptrs.resize(channels);
83 for(std::size_t c = 0; c < channels; ++c)
84 {
85 m_prime_zero_ptrs[c] = m_prime_zero_storage.data() + c * pad;
86 }
87 }
88 }
89
90 rubberband_stretcher(const rubberband_stretcher&) = delete;
91 rubberband_stretcher& operator=(const rubberband_stretcher&) = delete;
92 rubberband_stretcher(rubberband_stretcher&&) = default;
93 rubberband_stretcher& operator=(rubberband_stretcher&&) = default;
94
95 std::unique_ptr<RubberBand::RubberBandStretcher> m_rubberBand;
96 int64_t next_sample_to_read = 0;
97 uint32_t options{};
98 bool m_needs_prime{true};
99
100 // Zero-pad scratch buffer for prime(), sized in the ctor.
101 std::vector<float> m_prime_zero_storage;
102 std::vector<float*> m_prime_zero_ptrs;
103
104 [[nodiscard]] int64_t start_delay() const noexcept
105 {
106 return m_rubberBand ? int64_t(m_rubberBand->getStartDelay()) : 0;
107 }
108
109 void transport(int64_t date)
110 {
111 m_rubberBand->reset();
112 next_sample_to_read = date;
113 m_needs_prime = true;
114 }
115
116 template <typename T>
117 void
118 run(T& audio_fetcher, const ossia::token_request& t, ossia::exec_state_facade e,
119 double tempo_ratio, const std::size_t chan, const std::size_t len,
120 int64_t samples_to_read, const int64_t samples_to_write,
121 const int64_t samples_offset, const ossia::mutable_audio_span<double>& ap) noexcept
122 {
123 const double abs_tempo_ratio = std::min(70., std::abs(tempo_ratio));
124 if(abs_tempo_ratio != m_rubberBand->getTimeRatio())
125 {
126 m_rubberBand->setTimeRatio(abs_tempo_ratio);
127 }
128
129 // Lazy pre-roll on first run after ctor/transport(); see prime().
130 if(m_needs_prime) [[unlikely]]
131 {
132 prime(audio_fetcher, chan, t.forward());
133 m_needs_prime = false;
134 }
135
136 // TODO : if T::sample_type == float we could leverage it directly as
137 // input
138 const int max_chan = std::max(chan, m_rubberBand->getChannelCount());
139 const int frames = std::max((int64_t)16, samples_to_read);
140 float** const input = (float**)alloca(sizeof(float*) * max_chan);
141 float** const output = (float**)alloca(sizeof(float*) * max_chan);
142 for(std::size_t i = 0; i < chan; i++)
143 {
144 input[i] = (float*)alloca(sizeof(float) * frames);
145 output[i] = (float*)alloca(sizeof(float) * samples_to_write);
146 }
147 for(std::size_t i = chan; i < m_rubberBand->getChannelCount(); i++)
148 {
149 input[i] = (float*)alloca(sizeof(float) * frames);
150 std::fill_n(input[i], frames, 0.f);
151 output[i] = (float*)alloca(sizeof(float) * samples_to_write);
152 }
153
154 if(t.forward())
155 {
156 while(m_rubberBand->available() < samples_to_write)
157 {
158 audio_fetcher.fetch_audio(next_sample_to_read, samples_to_read, input);
159
160 m_rubberBand->process(input, samples_to_read, false);
161
162 next_sample_to_read += samples_to_read;
163 samples_to_read = 16;
164 }
165
166 m_rubberBand->retrieve(
167 output, std::min((int)samples_to_write, m_rubberBand->available()));
168
169 for(std::size_t i = 0; i < chan; i++)
170 {
171 for(int64_t j = 0; j < samples_to_write; j++)
172 {
173 ap[i][j + samples_offset] = double(output[i][j]);
174 }
175 }
176 }
177 else
178 {
179 // Backward playback:
180 while(m_rubberBand->available() < samples_to_write)
181 {
182 audio_fetcher.fetch_audio_backward(next_sample_to_read, samples_to_read, input);
183
184 m_rubberBand->process(input, samples_to_read, false);
185
186 next_sample_to_read -= samples_to_read;
187 samples_to_read = 16;
188 }
189
190 const int retrieved = m_rubberBand->retrieve(
191 output, std::min((int)samples_to_write, m_rubberBand->available()));
192
193 for(std::size_t i = 0; i < chan; i++)
194 {
195 for(int64_t j = 0; j < samples_to_write; j++)
196 {
197 ap[i][j + samples_offset] = double(output[i][j]);
198 }
199 }
200 }
201 }
202
203private:
204 // Run the s_prime_strategy variant before the first audible frame.
205 template <typename T>
206 void prime(T& audio_fetcher, std::size_t chan, bool forward) noexcept
207 {
208 if(!m_rubberBand || chan == 0)
209 return;
210
211 const auto strategy = s_prime_strategy;
212 if(strategy == prime_strategy::NoPrime)
213 return;
214
215 if(strategy == prime_strategy::PreRollRealAudio)
216 {
217 // pad ≈ windowSize/2 per RubberBand::getPreferredStartPad docs.
218 const int64_t window = 2 * int64_t(m_rubberBand->getPreferredStartPad());
219
220 // Fall through to ZeroPadOnly if there isn't enough past content.
221 if(window > 0 && (forward ? next_sample_to_read >= window : true))
222 {
223 constexpr int prime_fetch_chunk = 256;
224 float** const input
225 = (float**)alloca(sizeof(float*) * m_rubberBand->getChannelCount());
226 float** const output
227 = (float**)alloca(sizeof(float*) * m_rubberBand->getChannelCount());
228 for(std::size_t i = 0; i < m_rubberBand->getChannelCount(); i++)
229 {
230 input[i] = (float*)alloca(sizeof(float) * prime_fetch_chunk);
231 output[i] = (float*)alloca(sizeof(float) * prime_fetch_chunk);
232 }
233
234 if(forward)
235 next_sample_to_read -= window;
236 else
237 next_sample_to_read += window;
238
239 int64_t fed = 0;
240 while(fed < window)
241 {
242 const int64_t chunk
243 = std::min<int64_t>(prime_fetch_chunk, window - fed);
244 if(forward)
245 {
246 audio_fetcher.fetch_audio(next_sample_to_read, chunk, input);
247 next_sample_to_read += chunk;
248 }
249 else
250 {
251 audio_fetcher.fetch_audio_backward(next_sample_to_read, chunk, input);
252 next_sample_to_read -= chunk;
253 }
254 m_rubberBand->process(input, int(chunk), false);
255 fed += chunk;
256 }
257
258 // Drain pre-K outputs; feed zeros once real input is exhausted
259 // to avoid consuming the file beyond K.
260 const int64_t target_drain
261 = std::max<int64_t>(0, window - int64_t(m_rubberBand->getStartDelay()));
262 int64_t drained = 0;
263 int safety = 0;
264 while(drained < target_drain && safety++ < 4096)
265 {
266 while(m_rubberBand->available() <= 0)
267 {
268 std::fill_n(input[0], prime_fetch_chunk, 0.f);
269 for(std::size_t i = 1; i < m_rubberBand->getChannelCount(); i++)
270 std::fill_n(input[i], prime_fetch_chunk, 0.f);
271 m_rubberBand->process(input, prime_fetch_chunk, false);
272 }
273 const int avail = m_rubberBand->available();
274 const int to_take = int(std::min<int64_t>(
275 target_drain - drained,
276 std::min<int64_t>(avail, prime_fetch_chunk)));
277 m_rubberBand->retrieve(output, to_take);
278 drained += to_take;
279 }
280 return;
281 }
282 }
283
284 const int64_t toPad = int64_t(m_rubberBand->getPreferredStartPad());
285 if(toPad <= 0)
286 return;
287
288 // Bail without allocating if a later setPitchScale() outgrew the buffer.
289 if(int64_t(m_prime_zero_storage.size()) < int64_t(chan) * toPad
290 || int64_t(m_prime_zero_ptrs.size()) < int64_t(chan))
291 {
292 return;
293 }
294
295 m_rubberBand->process(
296 m_prime_zero_ptrs.data(), int(toPad), /*final=*/false);
297
298 if(strategy == prime_strategy::ZeroPadOnly
299 || strategy == prime_strategy::PreRollRealAudio)
300 return;
301
302 const int64_t drain_target
303 = (strategy == prime_strategy::ExtendedDrain) ? 2 * toPad
304 : int64_t(m_rubberBand->getStartDelay());
305 if(drain_target <= 0)
306 return;
307
308 constexpr int prime_fetch_chunk = 256;
309 float** const input
310 = (float**)alloca(sizeof(float*) * m_rubberBand->getChannelCount());
311 float** const output
312 = (float**)alloca(sizeof(float*) * m_rubberBand->getChannelCount());
313 for(std::size_t i = 0; i < m_rubberBand->getChannelCount(); i++)
314 {
315 input[i] = (float*)alloca(sizeof(float) * prime_fetch_chunk);
316 output[i] = (float*)alloca(sizeof(float) * prime_fetch_chunk);
317 std::fill_n(input[i], prime_fetch_chunk, 0.f);
318 }
319
320 int64_t drained = 0;
321 int safety = 0;
322 while(drained < drain_target && safety++ < 4096)
323 {
324 while(m_rubberBand->available() <= 0)
325 {
326 if(forward)
327 {
328 audio_fetcher.fetch_audio(
329 next_sample_to_read, prime_fetch_chunk, input);
330 next_sample_to_read += prime_fetch_chunk;
331 }
332 else
333 {
334 audio_fetcher.fetch_audio_backward(
335 next_sample_to_read, prime_fetch_chunk, input);
336 next_sample_to_read -= prime_fetch_chunk;
337 }
338 m_rubberBand->process(input, prime_fetch_chunk, false);
339 }
340
341 const int available = m_rubberBand->available();
342 const int to_take = int(std::min<int64_t>(
343 drain_target - drained, std::min<int64_t>(available, prime_fetch_chunk)));
344 m_rubberBand->retrieve(output, to_take);
345 drained += to_take;
346 }
347 }
348};
349}
350#else
351#include <ossia/dataflow/nodes/timestretch/raw_stretcher.hpp>
352
353namespace ossia
354{
355static constexpr uint32_t get_rubberband_preset(ossia::audio_stretch_mode mode)
356{
357 return 0;
358}
359using rubberband_stretcher = raw_stretcher;
360}
361#endif
Definition git_info.h:7