Quantifier.hpp
1 #pragma once
2 #include <Fx/Types.hpp>
3 
4 #include <halp/controls.hpp>
5 #include <halp/meta.hpp>
6 #include <halp/midi.hpp>
7 
8 namespace Nodes
9 {
10 namespace Quantifier
11 {
12 struct Node
13 {
14  halp_meta(name, "Midi quantify")
15  halp_meta(c_name, "Quantifier")
16  halp_meta(category, "Midi")
17  halp_meta(manual_url, "https://ossia.io/score-docs/processes/midi-utilities.html#quantifier")
18  halp_meta(author, "ossia score")
19  halp_meta(description, "Quantifies a MIDI input")
20  halp_meta(uuid, "b8e2e5ad-17e4-43de-8d79-660a29d5c4f4")
21 
22  struct
23  {
24  halp::midi_bus<"in", libremidi::message> midi;
25  quant_selector<"Quantization"> start_quant;
26  halp::hslider_f32<"Tightness", halp::range{0.f, 1.f, 0.8f}> tightness;
27  duration_selector<"Duration"> duration;
28  halp::hslider_f32<"Tempo", halp::range{20., 300., 120.}> tempo;
29 
30  } inputs;
31  struct
32  {
33  midi_out midi;
34  } outputs;
35 
36  struct Note
37  {
38  uint8_t pitch{};
39  uint8_t vel{};
40  uint8_t chan{};
41  };
42 
43  struct NoteIn
44  {
45  Note note{};
46  int64_t date{};
47  };
48  std::vector<NoteIn> to_start;
49  std::vector<NoteIn> running_notes;
50 
51  // using control_policy = ossia::safe_nodes::default_tick_controls;
52  halp::setup setup;
53  void prepare(halp::setup s) { setup = s; }
54 
55  using tick = halp::tick_flicks;
56  void sequence(Note new_note, int64_t date)
57  {
58  for(auto note : to_start)
59  if(note.note.pitch == new_note.pitch)
60  return;
61  for(auto note : running_notes)
62  if(note.note.pitch == new_note.pitch)
63  return;
64  to_start.push_back(NoteIn{new_note, date});
65  }
66  void operator()(const tick& tk)
67  {
68  double start_q_ratio = inputs.start_quant.value;
69  double precision = inputs.tightness.value;
70  double duration = inputs.duration.value;
71  double tempo = inputs.tempo.value;
72 
73  // how much time does a whole note last at this tempo given the current sr
74  const auto whole_dur = 240.f / tempo; // in seconds
75  const auto whole_samples = whole_dur * setup.rate;
76 
77  for(const libremidi::message& in : inputs.midi)
78  {
79  // FIXME note processor
80  if(!in.is_note_on_or_off())
81  {
82  outputs.midi.push_back(in);
83  continue;
84  }
85 
86  Note note{in[1], in[2], (uint8_t)in.get_channel()};
87 
88  if(in.get_message_type() == libremidi::message_type::NOTE_ON && note.vel != 0)
89  {
90  if(start_q_ratio == 0.f) // No quantification, start directly
91  {
92  auto no = libremidi::channel_events::note_on(note.chan, note.pitch, note.vel);
93  no.timestamp = in.timestamp;
94 
95  outputs.midi.push_back(no);
96  if(duration > 0.f)
97  {
98  auto end = tk.position_in_frames + (int64_t)no.timestamp
99  + (int64_t)(whole_samples * duration);
100  this->running_notes.push_back({note, end});
101  }
102  else if(duration == 0.f)
103  {
104  // Stop at the next sample
105  auto noff = libremidi::channel_events::note_off(note.chan, note.pitch, note.vel);
106  noff.timestamp = no.timestamp;
107  outputs.midi.push_back(noff);
108  }
109  // else do nothing and just wait for a note off
110  }
111  else
112  {
113  // Find next time that matches the requested quantification
114  const auto start_q = whole_samples * start_q_ratio;
115  auto perf_date = int64_t(
116  start_q * int64_t(1 + (tk.position_in_frames + in.timestamp) / start_q));
117 
118  //FIXME (1. - precision) * tk.date.impl * st.modelToSamples() + precision * perf_date;
119  int64_t actual_date = perf_date;
120 
121  sequence(note, actual_date);
122  }
123  }
124  else
125  {
126  // Just stop
127  auto noff = libremidi::channel_events::note_off(note.chan, note.pitch, note.vel);
128  noff.timestamp = in.timestamp;
129  outputs.midi.push_back(noff);
130 
131  for(auto it = running_notes.begin(); it != running_notes.end();)
132  if(it->note.pitch == note.pitch)
133  it = running_notes.erase(it);
134  else
135  ++it;
136  }
137  }
138 
139  // TODO : also handle the case where we're quite close from the *previous*
140  // accessible value, eg we played a bit late
141  for(auto it = this->to_start.begin(); it != this->to_start.end();)
142  {
143  auto& note = *it;
144  // FIXME how does this follow live tempo changes ?
145  if(note.date >= tk.position_in_frames
146  && note.date < (tk.position_in_frames + tk.frames))
147  {
148  auto no = libremidi::channel_events::note_on(
149  note.note.chan, note.note.pitch, note.note.vel);
150  no.timestamp = note.date - tk.position_in_frames;
151  outputs.midi.push_back(no);
152 
153  if(duration > 0.f)
154  {
155  auto end = note.date + (int64_t)(whole_samples * duration);
156  this->running_notes.push_back({note.note, end});
157  }
158  else if(duration == 0.f)
159  {
160  // Stop at the next sample
161  auto noff = libremidi::channel_events::note_off(note.note.chan, note.note.pitch, note.note.vel);
162  noff.timestamp = no.timestamp;
163  outputs.midi.push_back(noff);
164  }
165 
166  it = this->to_start.erase(it);
167  }
168  else
169  {
170  ++it;
171  }
172  }
173 
174  for(auto it = this->running_notes.begin(); it != this->running_notes.end();)
175  {
176  auto& note = *it;
177  if(note.date >= tk.position_in_frames
178  && note.date < (tk.position_in_frames + tk.frames))
179  {
180  auto noff = libremidi::channel_events::note_off(note.note.chan, note.note.pitch, note.note.vel);
181  noff.timestamp = note.date - tk.position_in_frames;
182  outputs.midi.push_back(noff);
183  it = this->running_notes.erase(it);
184  }
185  else
186  {
187  ++it;
188  }
189  }
190  }
191 };
192 }
193 }
Definition: Quantifier.hpp:37
Definition: Quantifier.hpp:44
Definition: Quantifier.hpp:13
Definition: Types.hpp:48