Skip to content

Commit e15f505

Browse files
committed
MIDI note allocation
1 parent 12569e7 commit e15f505

File tree

4 files changed

+145
-14
lines changed

4 files changed

+145
-14
lines changed

runtime/elem/BlockEvents.h

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,11 +21,25 @@ struct MidiEvent {
2121
MidiEvent(uint8_t byte0, uint8_t byte1, uint8_t byte2)
2222
: message(byte0, byte1, byte2)
2323
{}
24+
25+
MidiEvent(MidiEvent const& other)
26+
: message(other.message)
27+
{}
2428
};
2529

2630
struct AssignedMidiEvent {
2731
choc::midi::ShortMessage message;
2832
size_t voiceIndex;
33+
34+
AssignedMidiEvent(uint8_t byte0, uint8_t byte1, uint8_t byte2)
35+
: message(byte0, byte1, byte2)
36+
, voiceIndex(0)
37+
{}
38+
39+
AssignedMidiEvent(choc::midi::ShortMessage const& msg)
40+
: message(msg)
41+
, voiceIndex(0)
42+
{}
2943
};
3044

3145
// Type-erased event structure that can hold any event type within a certain

runtime/elem/DefaultNodeTypes.h

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -144,7 +144,9 @@ namespace elem
144144
callback("capture", GenericNodeFactory<CaptureNode<FloatType>>());
145145

146146
// MIDI Event nodes
147-
callback("midinotein", GenericNodeFactory<MidiNoteInNode<FloatType>>());
147+
callback("midinotein", GenericNodeFactory<MidiNoteInNode<FloatType>>());
148+
callback("midinoteallocate", GenericNodeFactory<MidiNoteAllocateNode<FloatType>>());
149+
callback("midinoteunpack", GenericNodeFactory<MidiNoteUnpackNode<FloatType>>());
148150
}
149151
};
150152

runtime/elem/GraphRenderSequence.h

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -109,6 +109,8 @@ namespace elem
109109
auto& outputEvents = m_eventsBufferPool.produce(node->getId(), outlets);
110110

111111
renderOps.push_back([node, &outputEvents, outputChannels = std::move(outputChannels)](BlockContext<FloatType> const& rootCtx) mutable {
112+
outputEvents.clear();
113+
112114
node->process(BlockContext<FloatType> {
113115
rootCtx.inputData,
114116
rootCtx.numInputChannels,
@@ -146,10 +148,15 @@ namespace elem
146148
auto outputChannels = m_bufferPool.produce(node->getId(), outlets);
147149
auto inputChannels = m_bufferPool.consume(inlets);
148150

149-
auto& inputEvents = m_eventsBufferPool.consume(inlets);
151+
// Always produce before consume! Otherwise the pool might hand out the same buffer
152+
// for input and output events, which would get cleared at the beginning of the op
153+
// below.
150154
auto& outputEvents = m_eventsBufferPool.produce(node->getId(), outlets);
155+
auto& inputEvents = m_eventsBufferPool.consume(inlets);
151156

152157
renderOps.push_back([node, &inputEvents, &outputEvents, outputChannels = std::move(outputChannels), inputChannels = std::move(inputChannels)](BlockContext<FloatType> const& rootCtx) mutable {
158+
outputEvents.clear();
159+
153160
node->process(BlockContext<FloatType> {
154161
const_cast<const FloatType**>(inputChannels.data()),
155162
inputChannels.size(),

runtime/elem/builtins/MIDI.h

Lines changed: 120 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -6,35 +6,140 @@
66
namespace 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

Comments
 (0)