Loading...
Searching...
No Matches
Looper.hpp
1#pragma once
2#include <Fx/Types.hpp>
3
4#include <ossia/dataflow/exec_state_facade.hpp>
5#include <ossia/dataflow/nodes/media.hpp>
6#include <ossia/dataflow/token_request.hpp>
7
8#include <halp/audio.hpp>
9#include <halp/controls.hpp>
10#include <halp/layout.hpp>
11#include <halp/meta.hpp>
12
13namespace Nodes::AudioLooper
14{
15struct Node
16{
17 halp_meta(name, "Looper (audio)")
18 halp_meta(c_name, "Looper (audio)")
19 halp_meta(category, "Audio/Utilities")
20 halp_meta(author, "ossia score")
21 halp_meta(manual_url, "https://ossia.io/score-docs/processes/audio-looper.html")
22 halp_meta(description, "Loop audio")
23 halp_meta(uuid, "a0ad4227-ac3d-448b-a19b-19581ed4e2c6");
24 halp_meta(recommended_height, 65);
25
26 enum class LoopMode
27 {
28 Play,
29 Record,
30 Overdub,
31 Stop
32 };
33
34 enum class Postaction
35 {
36 Play,
37 Overdub
38 };
39
43 enum class Restart
44 {
45 Always,
47 };
48
49 // Order matters: this replaced a toggle, and a stored 0/1 has to keep
50 // meaning what it did.
51 enum class Passthrough
52 {
53 None,
54 Record,
55 Full
56 };
57
59 {
60 static clang_buggy_consteval auto name() { return std::string_view{"Passthrough"}; }
61 enum widget
62 {
63 combobox
64 };
65 struct range
66 {
67 decltype(avnd::enum_names<Passthrough>()) values = avnd::enum_names<Passthrough>();
69 };
71 operator Passthrough() const noexcept { return value; }
72 auto& operator=(Passthrough t) noexcept
73 {
74 value = t;
75 return *this;
76 }
77 };
78
79 struct ins
80 {
81 halp::dynamic_audio_bus<"in", double> audio;
82
83 struct : halp::enum_t<LoopMode, "Loop">
84 {
85 using halp::enum_t<LoopMode, "Loop">::operator=;
86 halp_meta(
87 description,
88 "What the looper is doing: playing the loop, recording a new one over "
89 "it, overdubbing on top of it, or nothing.")
90 } mode;
91
92 struct : quant_selector<"Quantif">
93 {
94 halp_meta(
95 description,
96 "Waits for the next point of this grid before changing mode, so a "
97 "loop starts and ends in time. Whole is a bar. None changes at once.")
98 } quantif;
99
100 struct : passthrough_selector
101 {
102 using passthrough_selector::operator=;
103 halp_meta(
104 description,
105 "Whether the input is heard as well as the loop. Record passthrough "
106 "is heard only while recording or overdubbing; Full is heard always.")
107 } passthrough;
108
109 struct : halp::enum_t<Postaction, "Post-action">
110 {
111 using halp::enum_t<Postaction, "Post-action">::operator=;
112 halp_meta(
113 description,
114 "What recording gives way to once it has run for Bars bars, without "
115 "the mode being touched.")
116 } postaction;
117
118 struct : halp::spinbox_i32<"Bars", halp::irange{0, 64, 4}>
119 {
120 halp_meta(
121 description,
122 "How long a recording runs before the post-action takes over, and so "
123 "how long the loop it leaves is. Zero leaves recording to be ended by "
124 "hand, and the loop is then as long as it was played for.")
125 } postaction_bars;
126
127 struct : halp::enum_t<Restart, "Restart"> {
128 using halp::enum_t<Restart, "Restart">::operator=;
129 halp_meta(
130 description,
131 "Where the loop is read from when the mode changes. Always reads it "
132 "from the beginning again; Recording lets playing and overdubbing "
133 "carry on from where they were.")
134 } restart;
135 } inputs;
136 struct
137 {
138 halp::mimic_audio_bus<"out", &ins::audio> audio;
139 } outputs;
140
141 struct State
142 {
143 LoopMode quantizedPlayMode{LoopMode::Stop};
144 LoopMode actualMode{LoopMode::Stop};
145 ossia::audio_vector audio;
146 int64_t playbackPos{};
147 ossia::time_value recordStart{};
148 ossia::quarter_note recordStartBar{-1.};
149 ossia::quarter_note recordEndBar{-1.};
150 int actualChannels = 0;
151 float quantif{0.0};
152 std::optional<ossia::time_value> this_buffer_quantif_time;
153 std::optional<int64_t> this_buffer_quantif_sample;
154 int64_t tickStartSample{};
155 int postaction_bars{};
156 double sampleRate{48000.};
159 bool endedByPostaction{false};
160 bool faded{false};
161
162 static constexpr int64_t default_buffer_size = 192000 * 32;
163 void reset_elapsed() { }
164 int channels() const noexcept { return actualChannels; }
165 void set_channels(int chans)
166 {
167 if(chans == actualChannels)
168 return;
169
170 const int prev_channels = actualChannels;
171 actualChannels = chans;
172 if(std::ssize(audio) < chans)
173 audio.resize(chans);
174
175 // Channels that come back after the input narrowed must line up with the
176 // ones that stayed: play() and overdub() index them all with one position.
177 const int64_t len = prev_channels > 0 ? std::ssize(audio[0]) : 0;
178 for(int i = prev_channels; i < chans; i++)
179 {
180 if(std::ssize(audio[i]) != len)
181 {
182 audio[i].reserve(std::max(int64_t(audio[i].capacity()), default_buffer_size));
183 audio[i].resize(len);
184 }
185 }
186 }
187
188 State()
189 {
190 audio.resize(2);
191 for(auto& vec : audio)
192 vec.reserve(default_buffer_size);
193 }
194 } state;
195
200 void enterMode(LoopMode m, Restart restart)
201 {
202 state.quantizedPlayMode = m;
203 state.actualMode = m;
204 if(m == LoopMode::Record || restart == Restart::Always)
205 state.playbackPos = 0;
206 }
207
208 void fade(const ossia::token_request& tk)
209 {
210 // Every mode change goes through here; the ramp must only ever be applied
211 // to material that was recorded since the last one.
212 if(state.faded)
213 return;
214 state.faded = true;
215
216 const double sr = state.sampleRate;
217 const double bar_samples
218 = sr * 4. * (double(tk.signature.upper) / tk.signature.lower) * (60. / tk.tempo);
219 const double total_samples = std::floor(state.postaction_bars * bar_samples);
220
221 // The bar count says when the post-action takes over, so it is the length
222 // of a recording the post-action ended -- trimming the rounding off the
223 // last bar. A recording stopped by hand is as long as it was played for,
224 // and stretching it out to that count is how a loop ended early came back
225 // with bars of silence at the end of it.
226 const bool quantify_length = state.endedByPostaction && (state.quantif > 0.f)
227 && (state.postaction_bars > 0)
228 && (state.channels() > 0);
229 if(quantify_length)
230 {
231 if(total_samples < state.audio[0].size())
232 {
233 for(auto& a : state.audio)
234 a.resize(total_samples);
235 }
236 }
237
238 // Apply a small fade on the first and last samples
239 for(auto& chan : state.audio)
240 {
241 const int64_t samples = std::ssize(chan);
242 if(int64_t min_n = std::min(samples, int64_t(128)); min_n > 0)
243 {
244 float f = 1. / min_n;
245 float ff = 0.;
246 for(int64_t i = 0; i < min_n; i++)
247 {
248 chan[i] *= ff;
249 ff += f;
250 }
251
252 for(int64_t i = samples - min_n; i < samples; i++)
253 {
254 chan[i] *= ff;
255 ff -= f;
256 }
257 }
258 }
259
260 // If there are less samples than expected we extend
261 if(quantify_length)
262 {
263 if(total_samples > state.audio[0].size())
264 {
265 for(auto& a : state.audio)
266 a.resize(total_samples);
267 }
268 }
269 }
270
271 void changeAction(const ossia::token_request& tk)
272 {
273 // Zero bars means no automatic post-action: the end bar would otherwise be
274 // the start bar and preAction() would leave Record on its first tick.
275 if(state.quantizedPlayMode == LoopMode::Record && state.postaction_bars > 0)
276 {
277 state.recordStart = tk.prev_date;
278 state.recordStartBar = tk.musical_start_position;
279 state.recordEndBar = tk.musical_start_position
280 + (4. * double(tk.signature.upper) / tk.signature.lower)
281 * state.postaction_bars;
282 state.endedByPostaction = false;
283 state.reset_elapsed();
284 }
285 else
286 {
287 state.recordStart = ossia::time_value{-1LL};
288 state.recordStartBar = -1.;
289 state.recordEndBar = -1.;
290 state.reset_elapsed();
291 }
292
293 fade(tk);
294 }
295
296 void checkPostAction(
297 const std::string& postaction, int postaction_bars, const ossia::token_request& tk,
298 State& st)
299 {
300 }
301
302 ossia::exec_state_facade ossia_state;
303
304 using tick = ossia::token_request;
305 void operator()(const tick& tk)
306 {
307 using namespace ossia;
308
309 if(tk.date == tk.prev_date)
310 return;
311
312 const LoopMode m = this->inputs.mode;
313 const int postaction_bars = this->inputs.postaction_bars;
314 const Postaction postaction = this->inputs.postaction;
315 const float quantif = this->inputs.quantif.value;
316 const Passthrough passthrough = this->inputs.passthrough;
317 const Restart restart = this->inputs.restart;
318
319 state.this_buffer_quantif_time = std::nullopt;
320 state.this_buffer_quantif_sample = std::nullopt;
321 state.postaction_bars = postaction_bars;
322 state.sampleRate = ossia_state.sampleRate();
323
324 // The sample pointers we were handed already start at this tick's first
325 // sample; every span below is expressed relative to that.
326 state.tickStartSample = ossia_state.timings(tk).start_sample;
327
328 if(quantif != 0 && tk.prev_date != 0_tv)
329 {
330 state.quantif = quantif;
331 // A point on the tick's very first sample counts. Bar lines land there
332 // whenever the bar divides evenly into the buffer -- which at 120bpm and
333 // 48kHz is every buffer of 256 frames or fewer -- and refusing them left
334 // the mode waiting for a point that would never come.
335 if(auto pt = tk.get_quantification_point(1. / quantif);
336 pt && pt->date >= tk.prev_date)
337 {
338 const double ratio = ossia_state.modelToSamples();
339 state.this_buffer_quantif_time = pt->date;
340 // Through the point's musical position, not its date: the date is
341 // truncated to a whole flick, and flooring that into a sample rounds
342 // twice, so the loop point would land a sample before the metronome
343 // click on the very bar line it is quantized to.
344 state.this_buffer_quantif_sample
345 = tk.physical_start(ratio) + tk.physical_position(pt->position, ratio);
346 }
347 }
348 else
349 {
350 state.quantif = 0.0f;
351 }
352
353 if(m != state.quantizedPlayMode)
354 {
355 if(quantif != 0 && tk.prev_date != 0_tv)
356 {
357 if(auto& time = state.this_buffer_quantif_time)
358 {
359 // tempo = 60 -> 1 quarter = 1 second
360 // tempo = 100 -> 1 quarter = 1 * 60/100 = 0.6 second
361 // tempo = 100 -> 1 bar = 2 second
362 // tempo = 120 -> 1 bar = 2 second
363 // tempo = 120 -> 1 bar = 2 second
364 if(*time > tk.prev_date)
365 {
366 // Finish what we were doing until the quantization date. The span
367 // is half-open, so the two halves tile the tick exactly; ending a
368 // flick early instead rounds down to a sample nobody writes.
369 {
370 auto sub_tk = tk;
371 sub_tk.set_end_time(*time);
372
373 preAction(sub_tk, postaction, postaction_bars, passthrough, restart);
374 }
375
376 // We can switch to the new mode
377 enterMode(m, restart);
378
379 // Remaining of the tick
380 {
381 auto sub_tk = tk;
382 sub_tk.set_start_time(*time);
383
384 changeAction(sub_tk);
385 preAction(sub_tk, postaction, postaction_bars, passthrough, restart);
386 }
387 }
388 else
389 {
390 // We can switch to the new mode
391 enterMode(m, restart);
392
393 changeAction(tk);
394 preAction(tk, postaction, postaction_bars, passthrough, restart);
395 }
396 }
397 else
398 {
399 // We cannot switch yet
400 preAction(tk, postaction, postaction_bars, passthrough, restart);
401 }
402 }
403 else
404 {
405 // No quantization, we can switch to the new mode
406 enterMode(m, restart);
407
408 changeAction(tk);
409 preAction(tk, postaction, postaction_bars, passthrough, restart);
410 }
411 }
412 else
413 {
414 // No change
415 preAction(tk, postaction, postaction_bars, passthrough, restart);
416 }
417 }
418
419 void preAction(
420 ossia::token_request tk, Postaction postaction, int postaction_bars,
421 Passthrough passthrough, Restart restart)
422 {
423 using namespace ossia;
424 if(state.recordStartBar == -1.)
425 {
426 action(tk, passthrough);
427 }
428 else
429 {
430 // If we are in the token request which steps into the bar in which
431 // we change because we are e.g. 4 bars after the recording started
432 auto switch_to_main_mode = [&] {
433 switch(postaction)
434 {
435 case Postaction::Play:
436 state.actualMode = LoopMode::Play;
437 break;
438 case Postaction::Overdub:
439 state.actualMode = LoopMode::Overdub;
440 break;
441 default:
442 state.actualMode = LoopMode::Stop;
443 break;
444 }
445
446 state.recordStart = ossia::time_value{-1};
447 state.recordStartBar = -1.;
448 state.endedByPostaction = true;
449 state.reset_elapsed();
450 if(restart == Restart::Always)
451 state.playbackPos = 0;
452 };
453
454 // Change of bar at the first sample
455 if(tk.musical_start_last_bar >= state.recordEndBar)
456 {
457 switch_to_main_mode();
458 fade(tk);
459 action(tk, passthrough);
460 }
461
462 // Change of bar in the middle
463 else if(tk.musical_end_last_bar >= state.recordEndBar)
464 {
465 const auto quant_date = tk.get_quantification_date(1.0);
466 if(quant_date && *quant_date > tk.prev_date && *quant_date < tk.date)
467 {
468 const ossia::time_value t = *quant_date;
469
470 // Finish what we were doing until the quantization date
471 {
472 auto sub_tk = tk;
473 sub_tk.set_end_time(t);
474 action(sub_tk, passthrough);
475 }
476
477 // We can switch to the new mode
478 switch_to_main_mode();
479 fade(tk);
480
481 // Remaining of the tick
482 {
483 auto sub_tk = tk;
484 sub_tk.set_start_time(t);
485
486 action(sub_tk, passthrough);
487 }
488 }
489 else if(tk.musical_end_position - tk.musical_end_last_bar < 1e-9)
490 {
491 // The bar line is this tick's own end, so the whole of it still
492 // belongs to what came before -- and a point there is not reported,
493 // the tick's end being exclusive. The next tick starts on the line
494 // and takes the switch; ending here instead drops this tick's
495 // recording, which comes back as a buffer of silence in the loop.
496 action(tk, passthrough);
497 }
498 else
499 {
500 // The bar line is not locatable inside the tick: switch on its first
501 // sample rather than leaving the buffer unwritten.
502 switch_to_main_mode();
503 fade(tk);
504 action(tk, passthrough);
505 }
506 }
507
508 // No change of bar yet, we continue
509 else
510 {
511 action(tk, passthrough);
512 }
513 }
514 }
515
516 void action(const ossia::token_request& tk, Passthrough pt)
517 {
518 auto timings = ossia_state.timings(tk);
519 action(timings.start_sample - state.tickStartSample, timings.length, pt);
520 }
521
522 void action(int64_t start, int64_t length, Passthrough pt)
523 {
524 // Record passthrough is what its name says: the input while something is
525 // being taken in, and nothing else. Only Full is heard the rest of the time.
526 const bool echo_while_taking_in = pt != Passthrough::None;
527 const bool echo_otherwise = pt == Passthrough::Full;
528 switch(state.actualMode)
529 {
530 case LoopMode::Play:
531 if(state.channels() == 0 || state.audio[0].size() == 0)
532 echo_otherwise ? stop(start, length) : silence(start, length);
533 else
534 {
535 play(start, length);
536 if(echo_otherwise)
537 mix_input(start, length);
538 }
539 break;
540 case LoopMode::Stop:
541 echo_otherwise ? stop(start, length) : silence(start, length);
542 break;
543 case LoopMode::Record:
544 echo_while_taking_in ? record(start, length) : record_noecho(start, length);
545 break;
546 case LoopMode::Overdub:
547 echo_while_taking_in ? overdub(start, length) : overdub_noecho(start, length);
548 break;
549 }
550 }
551
552 void play(int64_t first_pos, int64_t samples)
553 {
554 auto& p2 = outputs.audio;
555 const int64_t last = first_pos + samples;
556 const int out_chans = p2.channels;
557 const int loop_chans = std::min(state.channels(), out_chans);
558
559 int64_t k = state.playbackPos;
560 for(int i = 0; i < loop_chans; i++)
561 {
562 auto& out = p2.samples[i];
563 auto& record = state.audio[i];
564 const int64_t chan_samples = std::ssize(record);
565
566 if(chan_samples <= 0)
567 {
568 for(int64_t j = first_pos; j < last; j++)
569 out[j] = 0.;
570 continue;
571 }
572
573 // The loop is free to be shorter than the tick, and to end anywhere
574 // inside it.
575 k = state.playbackPos % chan_samples;
576 for(int64_t j = first_pos; j < last; j++)
577 {
578 out[j] = record[k];
579 if(++k == chan_samples)
580 k = 0;
581 }
582 }
583
584 // The bus can carry more channels than the loop was recorded with.
585 for(int i = loop_chans; i < out_chans; i++)
586 {
587 auto& out = p2.samples[i];
588 for(int64_t j = first_pos; j < last; j++)
589 out[j] = 0.;
590 }
591
592 state.playbackPos = k;
593 }
594
595 // We just copy input to output
598 void silence(int64_t first_pos, int64_t samples)
599 {
600 auto& p2 = outputs.audio;
601 const int64_t last = first_pos + samples;
602 for(int i = 0; i < p2.channels; i++)
603 {
604 auto& out = p2.samples[i];
605 for(int64_t j = first_pos; j < last; j++)
606 out[j] = 0.;
607 }
608 }
609
610 void mix_input(int64_t first_pos, int64_t samples)
611 {
612 auto& p1 = inputs.audio;
613 auto& p2 = outputs.audio;
614 const int chans = std::min(p1.channels, p2.channels);
615 const int64_t last = first_pos + samples;
616 for(int i = 0; i < chans; i++)
617 {
618 auto& in = p1.samples[i];
619 auto& out = p2.samples[i];
620 for(int64_t j = first_pos; j < last; j++)
621 out[j] += in[j];
622 }
623 }
624
625 void stop(int64_t first_pos, int64_t samples)
626 {
627 auto& p1 = inputs.audio;
628 auto& p2 = outputs.audio;
629 const auto chans = p1.channels;
630 const int64_t last = first_pos + samples;
631
632 for(int i = 0; i < chans; i++)
633 {
634 auto& in = p1.samples[i];
635 auto& out = p2.samples[i];
636
637 for(int64_t j = first_pos; j < last; j++)
638 {
639 out[j] = in[j];
640 }
641 }
642 }
643
644 void record(int64_t first_pos, int64_t samples)
645 {
646 auto& p1 = inputs.audio;
647 auto& p2 = outputs.audio;
648 // Copy input to output, and append input to buffer
649 const auto chans = p1.channels;
650 state.set_channels(chans);
651 const int64_t last = first_pos + samples;
652
653 for(int i = 0; i < chans; i++)
654 {
655 auto& in = p1.samples[i];
656 auto& out = p2.samples[i];
657 auto& record = state.audio[i];
658
659 record.resize(state.playbackPos + samples);
660 int64_t k = state.playbackPos;
661
662 for(int64_t j = first_pos; j < last; j++)
663 {
664 out[j] = in[j];
665 record[k] = in[j];
666 k++;
667 }
668 }
669 state.playbackPos += samples;
670 state.faded = false;
671 }
672
673 void record_noecho(int64_t first_pos, int64_t samples)
674 {
675 auto& p1 = inputs.audio;
676 auto& p2 = outputs.audio;
677 // Append input to buffer, and silence the output
678 const auto chans = p1.channels;
679 state.set_channels(chans);
680 const int64_t last = first_pos + samples;
681
682 for(int i = 0; i < chans; i++)
683 {
684 auto& in = p1.samples[i];
685 auto& record = state.audio[i];
686
687 record.resize(state.playbackPos + samples);
688 int64_t k = state.playbackPos;
689
690 for(int64_t j = first_pos; j < last; j++)
691 {
692 record[k] = in[j];
693 k++;
694 }
695 }
696
697 for(int i = 0; i < p2.channels; i++)
698 {
699 auto& out = p2.samples[i];
700 for(int64_t j = first_pos; j < last; j++)
701 out[j] = 0.;
702 }
703
704 state.playbackPos += samples;
705 state.faded = false;
706 }
707
708 void overdub(int64_t first_pos, int64_t samples)
709 {
710 auto& p1 = inputs.audio;
711 auto& p2 = outputs.audio;
712
713 // Mix input into the buffer, and copy the result to the output
714 const auto chans = p1.channels;
715 state.set_channels(chans);
716 const int64_t last = first_pos + samples;
717
718 int64_t k = state.playbackPos;
719 for(int i = 0; i < chans; i++)
720 {
721 auto& in = p1.samples[i];
722 auto& out = p2.samples[i];
723 auto& record = state.audio[i];
724 const int64_t record_samples = std::ssize(record);
725
726 // Nothing to layer onto: an overdub of an empty loop is an echo.
727 if(record_samples <= 0)
728 {
729 for(int64_t j = first_pos; j < last; j++)
730 out[j] = in[j];
731 continue;
732 }
733
734 k = state.playbackPos % record_samples;
735 for(int64_t j = first_pos; j < last; j++)
736 {
737 record[k] += in[j];
738 out[j] = record[k];
739
740 if(++k == record_samples)
741 k = 0;
742 }
743 }
744 state.playbackPos = k;
745 state.faded = false;
746 }
747
748 void overdub_noecho(int64_t first_pos, int64_t samples)
749 {
750 auto& p1 = inputs.audio;
751 auto& p2 = outputs.audio;
752
753 // Mix input into the buffer, and copy what was there to the output
754 const auto chans = p1.channels;
755 state.set_channels(chans);
756 const int64_t last = first_pos + samples;
757
758 int64_t k = state.playbackPos;
759 for(int i = 0; i < chans; i++)
760 {
761 auto& in = p1.samples[i];
762 auto& out = p2.samples[i];
763 auto& record = state.audio[i];
764 const int64_t record_samples = std::ssize(record);
765
766 if(record_samples <= 0)
767 {
768 for(int64_t j = first_pos; j < last; j++)
769 out[j] = 0.;
770 continue;
771 }
772
773 k = state.playbackPos % record_samples;
774 for(int64_t j = first_pos; j < last; j++)
775 {
776 out[j] = record[k];
777 record[k] += in[j];
778
779 if(++k == record_samples)
780 k = 0;
781 }
782 }
783 state.playbackPos = k;
784 state.faded = false;
785 }
786
787 struct ui
788 {
789 halp_meta(layout, halp::layouts::hbox)
790 struct
791 {
792 halp_meta(layout, halp::layouts::vbox)
793 halp_meta(background, halp::colors::background_mid)
794 halp::control<&ins::mode> f;
795 halp::control<&ins::quantif> q;
796 halp::control<&ins::passthrough> p;
797 } left;
798 struct
799 {
800 halp_meta(layout, halp::layouts::vbox)
801 halp_meta(background, halp::colors::background_mid)
802 halp::control<&ins::postaction> p;
803 halp::control<&ins::postaction_bars> b;
804 halp::control<&ins::restart> r;
805 } right;
806 };
807};
808}
Utilities for OSSIA data structures.
Definition DeviceInterface.hpp:35
bool endedByPostaction
Definition Looper.hpp:159
Definition Looper.hpp:80
Definition Looper.hpp:788
Definition Looper.hpp:16
void silence(int64_t first_pos, int64_t samples)
Definition Looper.hpp:598
void enterMode(LoopMode m, Restart restart)
Definition Looper.hpp:200
Restart
Definition Looper.hpp:44
@ Always
Every change of mode reads the loop from its beginning again.
@ Recording
Only a recording does; playing and overdubbing carry straight on.
Passthrough
Definition Looper.hpp:52
@ Record
Heard while recording and overdubbing; playback is the loop alone.
@ None
The input is never heard, in any mode.
@ Full
Always heard, mixed over the loop during playback.
Definition Types.hpp:49