OSSIA
Open Scenario System for Interactive Application
dataflow/nodes/midi.hpp
1 #pragma once
2 #include <ossia/dataflow/graph_node.hpp>
3 #include <ossia/dataflow/node_process.hpp>
4 #include <ossia/dataflow/port.hpp>
5 #include <ossia/detail/flat_multiset.hpp>
6 
7 namespace ossia::nodes
8 {
9 
10 using midi_size_t = uint8_t;
11 struct note_data
12 {
13  time_value start{};
14  time_value duration{};
15 
16  midi_size_t pitch{};
17  midi_size_t velocity{};
18 };
19 
20 struct note_comparator
21 {
22  using is_transparent = std::true_type;
23  bool operator()(const note_data& lhs, const note_data& rhs) const
24  {
25  return lhs.start < rhs.start;
26  }
27  bool operator()(const note_data& lhs, int64_t rhs) const
28  {
29  return lhs.start.impl < rhs;
30  }
31 };
32 
33 class midi final : public ossia::nonowning_graph_node
34 {
35  ossia::midi_outlet midi_out;
36 
37 public:
38  using note_set = ossia::flat_multiset<note_data, note_comparator>;
39  explicit midi(int64_t notes)
40  {
41  m_outlets.push_back(&midi_out);
42  int64_t to_reserve = std::max(notes * 1.1, 128.);
43  m_notes.reserve(to_reserve);
44  m_orig_notes.reserve(to_reserve);
45  m_playing_notes.reserve(to_reserve);
46  m_to_stop.reserve(64);
47  }
48 
49  ~midi() override = default;
50 
51  void set_channel(int c) { m_channel = c; }
52 
53  void add_note(note_data nd)
54  {
55  m_orig_notes.insert(nd);
56  if(nd.start > m_prev_date)
57  {
58  m_notes.insert(nd);
59  }
60  }
61 
62  void remove_note(note_data nd)
63  {
64  m_orig_notes.erase(nd);
65  m_notes.erase(nd);
66  auto it = m_playing_notes.find(nd);
67  if(it != m_playing_notes.end())
68  {
69  m_to_stop.insert(nd);
70  m_playing_notes.erase(it);
71  }
72  }
73 
74  void replace_notes(note_set&& notes)
75  {
76  for(auto& note : m_playing_notes)
77  m_to_stop.insert(note);
78  m_playing_notes.clear();
79 
80  using namespace std;
81  swap(m_orig_notes, notes);
82  m_notes.clear();
83 
84  auto start_it = m_orig_notes.lower_bound(m_prev_date.impl);
85  if(start_it != m_orig_notes.end())
86  {
87  m_notes.tree().get_sequence_ref().assign(start_it, m_orig_notes.end());
88  }
89  }
90 
91  void transport(ossia::time_value date)
92  {
93  requestTransport = true;
94  m_transport_date = date;
95  }
96 
97  void transport_impl(ossia::time_value date)
98  {
99  // 1. Send note-offs
100  m_to_stop.insert(m_playing_notes.begin(), m_playing_notes.end());
101  m_playing_notes.clear();
102 
103  // 2. Re-add following notes
104  if(date < m_prev_date)
105  {
106  if(date == 0_tv)
107  {
108  m_notes = m_orig_notes;
109  }
110  else
111  {
112  auto min_it = m_orig_notes.lower_bound({date});
113  auto max_it = m_orig_notes.lower_bound({m_prev_date});
114 
115  if(min_it != m_orig_notes.end())
116  m_notes.insert(min_it, max_it);
117 
118  // all of these will have it->start < date
119  for(auto it = m_orig_notes.begin(); it != min_it; ++it)
120  {
121  if((it->start + it->duration) > date)
122  {
123  m_notes.insert(*it);
124  }
125  }
126  }
127  }
128  else if(date > m_prev_date)
129  {
130  // remove previous notes
131  auto min_it = m_notes.lower_bound({date});
132  if(min_it != m_notes.begin() && min_it != m_notes.end())
133  {
134  std::advance(min_it, -1);
135  m_notes.erase(m_notes.begin(), min_it);
136  }
137  // todo resume current notes
138  }
139 
140  m_prev_date = date;
141  }
142 
143  void update_note(note_data oldNote, note_data newNote)
144  {
145  // OPTIMIZEME
146  remove_note(oldNote);
147  add_note(newNote);
148  }
149 
150  void set_notes(note_set&& notes)
151  {
152  m_notes = std::move(notes);
153  m_orig_notes = m_notes;
154 
155  auto max_it = m_notes.lower_bound({m_prev_date});
156  if(max_it != m_notes.begin()) // TODO handle the begin case correctly
157  m_notes.erase(m_notes.begin(), max_it);
158  }
159 
160  bool mustStop{};
161  bool requestTransport{};
162  bool doTransport{};
163 
164 private:
165  void run(const ossia::token_request& t, ossia::exec_state_facade e) noexcept override
166  {
167  struct scope_guard
168  {
169  midi& self;
170  const ossia::token_request& t;
171  ~scope_guard()
172  {
173  self.m_prev_date = t.date;
174 
175  if(self.requestTransport)
176  {
177  self.transport_impl(self.m_transport_date);
178  self.requestTransport = false;
179  }
180  }
181  } guard{*this, t};
182 
183  ossia::midi_port& mp = *midi_out;
184  const auto samplesratio = e.modelToSamples();
185  const auto tick_start = t.physical_start(samplesratio);
186 
187  for(const note_data& note : m_to_stop)
188  {
189  mp.messages.push_back(
190  libremidi::channel_events::note_off(m_channel, note.pitch, 0));
191  mp.messages.back().timestamp = tick_start;
192  }
193  m_to_stop.clear();
194 
195  if(mustStop)
196  {
197  for(auto& note : m_playing_notes)
198  {
199  mp.messages.push_back(
200  libremidi::channel_events::note_off(m_channel, note.pitch, 0));
201  mp.messages.back().timestamp = tick_start;
202  }
203 
204  m_notes = m_orig_notes;
205  m_playing_notes.clear();
206 
207  mustStop = false;
208  }
209  else
210  {
211  if(m_notes.empty() && m_playing_notes.empty())
212  return;
213  if(doTransport)
214  {
215  auto it = m_notes.begin();
216 
217  while(it != m_notes.end() && it->start < t.date)
218  {
219  auto& note = *it;
220  mp.messages.push_back(
221  libremidi::channel_events::note_on(m_channel, note.pitch, note.velocity));
222  mp.messages.back().timestamp = tick_start;
223  m_playing_notes.insert(note);
224  it = m_notes.erase(it);
225  }
226 
227  doTransport = false;
228  }
229 
230  if(t.forward())
231  {
232  // First send note offs
233  for(auto it = m_playing_notes.begin(); it != m_playing_notes.end();)
234  {
235  note_data& note = const_cast<note_data&>(*it);
236  auto end_time = note.start + note.duration;
237 
238  if(t.in_range({end_time}))
239  {
240  mp.messages.push_back(
241  libremidi::channel_events::note_off(m_channel, note.pitch, 0));
242  mp.messages.back().timestamp
243  = t.to_physical_time_in_tick(end_time, samplesratio);
244 
245  it = m_playing_notes.erase(it);
246  }
247  else
248  {
249  ++it;
250  }
251  }
252 
253  // Look for all the messages
254  auto max_it = m_notes.lower_bound({t.date});
255  for(auto it = m_notes.begin(); it < max_it;)
256  {
257  note_data& note = const_cast<note_data&>(*it);
258  auto start_time = note.start;
259  if(start_time >= t.prev_date && start_time < t.date)
260  {
261  // Send note_on
262  mp.messages.push_back(libremidi::channel_events::note_on(
263  m_channel, note.pitch, note.velocity));
264  mp.messages.back().timestamp
265  = t.to_physical_time_in_tick(start_time, samplesratio);
266 
267  m_playing_notes.insert(note);
268  it = m_notes.erase(it);
269  max_it = std::lower_bound(
270  it, m_notes.end(), t.date.impl + 1, note_comparator{});
271  }
272  else
273  {
274  ++it;
275  }
276  }
277  }
278  }
279  }
280 
281  note_set m_notes;
282  note_set m_orig_notes;
283  note_set m_playing_notes;
284  note_set m_to_stop;
285  time_value m_prev_date{};
286  time_value m_transport_date{};
287 
288  int m_channel{};
289 };
290 
291 class midi_node_process final : public ossia::node_process
292 {
293 public:
294  using ossia::node_process::node_process;
295 
296  void transport_impl(ossia::time_value date) override
297  {
298  midi& n = *static_cast<midi*>(node.get());
299  n.transport(date);
300  }
301 
302  void stop() override
303  {
304  midi& n = *static_cast<midi*>(node.get());
305  n.request(ossia::token_request{});
306  n.mustStop = true;
307  }
308 };
309 }
constexpr OSSIA_INLINE auto max(const T a, const U b) noexcept -> typename std::conditional<(sizeof(T) > sizeof(U)), T, U >::type
max function tailored for values
Definition: math.hpp:96
The time_value class.
Definition: ossia/editor/scenario/time_value.hpp:28