Loading...
Searching...
No Matches
PatternNode.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_set.hpp>
6#include <ossia/detail/math.hpp>
7
8#include <Patternist/PatternModel.hpp>
9#include <libremidi/ump_events.hpp>
10
11#include <algorithm>
12#include <cmath>
13
14namespace Patternist
15{
16// The model stores channels as 1-16, the wire format as 0-15
17inline uint8_t to_midi_channel(int c) noexcept
18{
19 return std::clamp(c - 1, 0, 15);
20}
21
22class pattern_node : public ossia::nonowning_graph_node
23{
24public:
25 ossia::midi_outlet out;
26 ossia::value_outlet accent_out;
27 ossia::value_outlet slide_out;
28 Pattern pattern;
29 ossia::flat_set<uint8_t> in_flight;
30
31 int current = 0;
32 int last = -1;
33 uint8_t channel{1};
34 // Channel the notes currently in flight were started on: a channel change
35 // must not leave them stranded on the previous one.
36 uint8_t in_flight_channel{1};
37 bool release_pending{};
38 bool mustStop{};
39
41 {
42 in_flight.reserve(32);
43 m_outlets.push_back(&out);
44 m_outlets.push_back(&accent_out);
45 m_outlets.push_back(&slide_out);
46 }
47
48 std::string label() const noexcept override { return "pattern_node"; }
49
50 bool legato(int note) const noexcept
51 {
52 for(const Lane& lane : pattern.lanes)
53 if(lane.note == note && ossia::valid_index(current, lane.pattern))
54 return lane.pattern[current] == Note::Legato;
55 return false;
56 }
57
58 void release_all(int64_t timestamp) noexcept
59 {
60 auto& mess = out.target<ossia::midi_port>()->messages;
61 for(uint8_t note : in_flight)
62 {
63 mess.push_back(libremidi::from_midi1::note_off(in_flight_channel, note, 0));
64 mess.back().timestamp = timestamp;
65 }
66 in_flight.clear();
67 }
68
69 void set_channel(uint8_t c) noexcept
70 {
71 if(c == channel)
72 return;
73 channel = c;
74 // Released from run(), where a timestamp inside the tick is available
75 release_pending = true;
76 }
77
78 void run(const ossia::token_request& tk, ossia::exec_state_facade st) noexcept override
79 {
80 using namespace ossia;
81
82 const double samplesratio = st.modelToSamples();
83 // The magnitude of the speed: dividing by a negative speed while rewinding
84 // turns the tick offset into a negative timestamp, which every consumer
85 // that windows on [tick_start; tick_start + frames[ drops - and a dropped
86 // note-off is a note stuck forever.
87 const double speed = tk.speed != 0. ? std::abs(tk.speed) : 1.;
88 // The span the producer handed us wins over reconstructing it from the
89 // offset, which the flick rounding can miss by a sample. Reconstruction
90 // stays for the tokens that carry no span - and for a null speed, where
91 // the model -> sample map is undefined and token_request asserts.
92 const int64_t tick_start
93 = tk.start_sample >= 0
94 ? int64_t(tk.start_sample)
95 : int64_t(std::floor(tk.offset.impl * samplesratio / speed));
96
97 // Before the empty-tick check: the token requested by stop() is empty.
98 if(mustStop)
99 {
100 release_all(tick_start);
101 mustStop = false;
102 return;
103 }
104
105 if(tk.model_read_duration() == 0_tv)
106 return;
107
108 if(tk.end_discontinuous)
109 {
110 // Stamping at 0 puts the message before the beginning of the tick as soon
111 // as the interval does not start on a buffer boundary; every consumer
112 // that windows on [tick_start; tick_start + frames[ then drops it, and
113 // since in_flight is cleared here the note is never released again.
114 release_all(tick_start);
115 return;
116 }
117
118 if(release_pending)
119 {
120 release_all(tick_start);
121 in_flight_channel = channel;
122 release_pending = false;
123 }
124
125 if(pattern.length <= 0)
126 return;
127
128 // TODO on bar change, reset to start of pattern?
129 // All of them, not just the first: a tick covers more than one step as soon
130 // as the division is small, the buffer large or the tempo high, and the
131 // single-date version would silently drop every step but one.
132 // get_quantification_dates walks the grid in tick order, so rewinding it
133 // hands back the steps the tick crosses in decreasing musical order. Walk
134 // the pattern the same way, or the sequence marches on while the timeline
135 // runs the other way.
136 const bool rewinding = tk.backward();
137 for(const auto& q : tk.get_quantification_dates(pattern.division))
138 {
139 // Through the tick's one musical-position -> sample map, not through the
140 // point's date: the date is truncated to a whole flick, so flooring it
141 // into a sample rounds twice and puts the step a sample before the
142 // metronome click on the same bar line.
143 const int64_t date = tick_start + tk.physical_position(q.position, samplesratio);
144 play_step(date, rewinding);
145 }
146 }
147
148 void play_step(int64_t date, bool rewinding = false) noexcept
149 {
150 // Forward, the step about to play is the current one and the next tick
151 // plays the one after. Rewinding mirrors that: step back first, so going
152 // out and back over the same ground crosses the same steps.
153 if(rewinding)
154 current = (current + pattern.length - 1) % pattern.length;
155 last = current;
156 auto& mess = out.target<ossia::midi_port>()->messages;
157
158 for(auto it = in_flight.begin(); it != in_flight.end();)
159 {
160 uint8_t note = *it;
161 if(!legato(note))
162 {
163 mess.push_back(libremidi::from_midi1::note_off(in_flight_channel, note, 0));
164 mess.back().timestamp = date;
165 it = in_flight.erase(it);
166 }
167 else
168 {
169 ++it;
170 }
171 }
172
173 in_flight_channel = channel;
174
175 for(Lane& lane : pattern.lanes)
176 {
177 if(lane.note <= 127 && ossia::valid_index(current, lane.pattern))
178 {
179 switch(lane.pattern[current])
180 {
181 case Note::Note:
182 mess.push_back(libremidi::from_midi1::note_on(channel, lane.note, 100));
183 mess.back().timestamp = date;
184 in_flight.insert(lane.note);
185 break;
186 case Note::Legato:
187 if(!in_flight.contains(lane.note))
188 {
189 mess.push_back(libremidi::from_midi1::note_on(channel, lane.note, 100));
190 mess.back().timestamp = date;
191 in_flight.insert(lane.note);
192 }
193 break;
194 case Note::Rest:
195 if(in_flight.contains(lane.note))
196 {
197 mess.push_back(
198 libremidi::from_midi1::note_off(in_flight_channel, lane.note, 0));
199 mess.back().timestamp = date;
200 in_flight.erase(lane.note);
201 }
202 break;
203 }
204 }
205 }
206
207 for(Lane& lane : pattern.lanes)
208 {
209 if(ossia::valid_index(current, lane.pattern))
210 {
211 if(lane.note == 255)
212 {
213 if(lane.pattern[current] != Note::Rest)
214 accent_out->write_value(1., date);
215 else
216 accent_out->write_value(0., date);
217 }
218 else if(lane.note == 254)
219 {
220 if(lane.pattern[current] != Note::Rest)
221 slide_out->write_value(1., date);
222 else
223 slide_out->write_value(0., date);
224 }
225 }
226 }
227
228 if(!rewinding)
229 current = (current + 1) % pattern.length;
230 }
231
232 // Writing to the outlet from here would be pointless: this runs outside of a
233 // tick, and init_outlet() clears every outlet before the node runs again.
234 void all_notes_off() noexcept override { mustStop = true; }
235};
236
240class pattern_node_process final : public ossia::node_process
241{
242public:
243 using ossia::node_process::node_process;
244
245 void stop() override
246 {
247 auto& n = *static_cast<pattern_node*>(node.get());
248 n.request(ossia::token_request{});
249 n.mustStop = true;
250 }
251};
252}
Definition PatternNode.hpp:241
Definition PatternNode.hpp:23
Definition PatternModel.hpp:25
Definition PatternModel.hpp:36