Loading...
Searching...
No Matches
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
8namespace Nodes
9{
10namespace Quantifier
11{
12struct 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