66namespace elem
77{
88
9+ // A simple identity node for midi events.
10+ //
11+ // Essentially the equivalent of the Identity node (el.in) for audio
12+ // signal processing.
913 template <typename FloatType>
1014 struct MidiNoteInNode : public GraphNode <FloatType> {
1115 using GraphNode<FloatType>::GraphNode;
1216
17+ void process (BlockContext<FloatType> const & ctx) override {
18+ ctx.inputEvents .template processEventsOfType <MidiEvent>([&](size_t time, MidiEvent const & event) {
19+ ctx.outputEvents .addEvent (time, MidiEvent (event));
20+ });
21+ }
22+ };
23+
24+ // Maps incoming MidiEvents to AssignedMidiEvents with polyphonic
25+ // voice assignment.
26+ //
27+ // This uses MPE style voice allocation; where assigned voice is designated
28+ // by channel number. That means that we clobber the incoming channel number
29+ // assigned to the original event and rewrite it with a new number corresponding
30+ // to the assigned voice.
31+ template <typename FloatType>
32+ struct MidiNoteAllocateNode : public GraphNode <FloatType> {
33+ using GraphNode<FloatType>::GraphNode;
34+
1335 int setProperty (std::string const & key, js::Value const & val) override
1436 {
15- // TODO: allow channel filtering? voice?
16- // i.e. user sets a property here to tell us which notes to react to and
17- // which to ignore
37+ if (key == " voices" ) {
38+ if (!val.isNumber ())
39+ return ReturnCode::InvalidPropertyType ();
40+
41+ // Supports 1-16 voices
42+ auto v = static_cast <size_t >(std::min (16.0 , std::max (1.0 , (js::Number) val)));
43+ numVoices.store (v);
44+ }
45+
46+ return GraphNode<FloatType>::setProperty (key, val);
47+ }
48+
49+ void process (BlockContext<FloatType> const & ctx) override {
50+ ctx.inputEvents .template processEventsOfType <MidiEvent>([&](size_t time, MidiEvent const & event) {
51+ auto * bytes = event.message .data ();
52+
53+ if (event.message .isNoteOn ()) {
54+ auto voiceIndex = getFreeVoice ();
55+ auto outEvent = MidiEvent ((bytes[0 ] & 0xf0 ) | static_cast <uint8_t >(voiceIndex), bytes[1 ], bytes[2 ]);
56+
57+ ctx.outputEvents .addEvent (time, std::move (outEvent));
58+ voiceMap[voiceIndex] = bytes[1 ];
59+ }
60+
61+ if (event.message .isNoteOff ()) {
62+ auto assignedVoice = std::find (voiceMap.begin (), voiceMap.end (), bytes[1 ]);
63+
64+ if (assignedVoice != voiceMap.end ()) {
65+ auto voiceIndex = std::distance (voiceMap.begin (), assignedVoice);
66+ auto outEvent = MidiEvent ((bytes[0 ] & 0xf0 ) | static_cast <uint8_t >(voiceIndex), bytes[1 ], bytes[2 ]);
67+
68+ ctx.outputEvents .addEvent (time, std::move (outEvent));
69+ // Clear the mapping
70+ //
71+ // TODO: Technically 0 is a valid midi note; maybe just use an int
72+ // and let -1 be "unallocated"?
73+ voiceMap[voiceIndex] = 0 ;
74+ }
75+ }
76+ });
77+ }
78+
79+ size_t getFreeVoice () {
80+ auto out = nextFreeVoice;
81+
82+ // TODO: This is round robin, need better
83+ if (++nextFreeVoice >= (numVoices.load () - 1 ))
84+ nextFreeVoice = 0 ;
85+
86+ return out;
87+ }
88+
89+ // Maps the ith voice to the ith position in the array, where we capture
90+ // the note value the voice was last assigned
91+ std::array<uint8_t , 16 > voiceMap;
92+ std::atomic<size_t > numVoices = 1 ;
93+ size_t nextFreeVoice = 0 ;
94+ };
95+
96+ template <typename FloatType>
97+ struct MidiNoteUnpackNode : public GraphNode <FloatType> {
98+ using GraphNode<FloatType>::GraphNode;
99+
100+ int setProperty (std::string const & key, js::Value const & val) override
101+ {
102+ // TODO: Filter by channel too?
103+ if (key == " voice" ) {
104+ if (!val.isNumber ())
105+ return ReturnCode::InvalidPropertyType ();
106+
107+ auto v = static_cast <size_t >(std::max (0.0 , (js::Number) val));
108+ targetVoiceIndex.store (v);
109+ filterByVoice.store (true );
110+ }
111+
18112 return GraphNode<FloatType>::setProperty (key, val);
19113 }
20114
21115 void process (BlockContext<FloatType> const & ctx) override {
22116 size_t framesProcessed = 0 ;
117+ auto voiceFilter = filterByVoice.load ();
118+ auto voiceIndex = targetVoiceIndex.load ();
23119
24120 ctx.inputEvents .template processEventsOfType <MidiEvent>([&](size_t time, MidiEvent const & event) {
25- if (time < ctx.numSamples && (event. message . isNoteOn () || event. message . isNoteOff ())) {
26- auto framesRemaining = ctx. numSamples - framesProcessed ;
121+ if (time >= ctx.numSamples )
122+ return ;
27123
28- std::fill_n (ctx.outputData [0 ] + framesProcessed, framesRemaining, noteFreq);
124+ if (!event.message .isNoteOn () && !event.message .isNoteOff ())
125+ return ;
29126
30- if (ctx.numOutputChannels > 1 ) {
31- std::fill_n (ctx.outputData [1 ] + framesProcessed, framesRemaining, noteVelocity);
32- }
127+ if (voiceFilter && (voiceIndex != event.message .getChannel0to15 ()))
128+ return ;
129+
130+ auto framesRemaining = ctx.numSamples - framesProcessed;
131+ std::fill_n (ctx.outputData [0 ] + framesProcessed, framesRemaining, noteFreq);
33132
34- noteFreq = event.message .getNoteNumber ().getFrequency ();
35- noteVelocity = event.message .getVelocity () / (FloatType) 127 ;
36- framesProcessed = time;
133+ if (ctx.numOutputChannels > 1 ) {
134+ std::fill_n (ctx.outputData [1 ] + framesProcessed, framesRemaining, noteVelocity);
37135 }
136+
137+ noteFreq = event.message .getNoteNumber ().getFrequency ();
138+ noteVelocity = event.message .isNoteOff ()
139+ ? FloatType (0 )
140+ : event.message .getVelocity () / (FloatType) 127 ;
141+
142+ framesProcessed = time;
38143 });
39144
40145 auto framesRemaining = ctx.numSamples - framesProcessed;
@@ -45,6 +150,9 @@ namespace elem
45150 }
46151 }
47152
153+ std::atomic<size_t > targetVoiceIndex = 0 ;
154+ std::atomic<bool > filterByVoice = false ;
155+
48156 FloatType noteFreq = 0 ;
49157 FloatType noteVelocity = 0 ;
50158 };
0 commit comments