tracktion-engine 3.0-10-g034fdde4aa5
Tracktion Engine — High level data model for audio applications

« « « Anklang Documentation
Loading...
Searching...
No Matches
tracktion_MidiNode.cpp
Go to the documentation of this file.
1 /*
2 ,--. ,--. ,--. ,--.
3 ,-' '-.,--.--.,--,--.,---.| |,-.,-' '-.`--' ,---. ,--,--, Copyright 2024
4 '-. .-'| .--' ,-. | .--'| /'-. .-',--.| .-. || \ Tracktion Software
5 | | | | \ '-' \ `--.| \ \ | | | |' '-' '| || | Corporation
6 `---' `--' `--`--'`---'`--'`--' `---' `--' `---' `--''--' www.tracktion.com
7
8 Tracktion Engine uses a GPL/commercial licence - see LICENCE.md for details.
9*/
10
12
13#define TRACKTION_SANITY_CHECK_MIDI_BUFFERS 0
14
15namespace tracktion { inline namespace engine
16{
17
18MidiNode::MidiNode (std::vector<juce::MidiMessageSequence> sequences,
20 juce::Range<int> midiChannelNumbers,
21 bool useMPE,
22 juce::Range<double> editTimeRange,
23 LiveClipLevel liveClipLevel,
24 ProcessState& processStateToUse,
25 EditItemID editItemIDToUse,
26 std::function<bool()> shouldBeMuted)
27 : TracktionEngineNode (processStateToUse),
28 ms (std::move (sequences)),
29 timeBase (tb),
30 channelNumbers (midiChannelNumbers),
31 useMPEChannelMode (useMPE),
32 editRange (editTimeRange),
33 clipLevel (liveClipLevel),
34 editItemID (editItemIDToUse),
35 shouldBeMutedDelegate (std::move (shouldBeMuted)),
36 wasMute (liveClipLevel.isMute())
37{
38 // -1 from the channel numbers end here as Range end is exclusive
39 jassert (channelNumbers.getStart() > 0 && (channelNumbers.getEnd() - 1) <= 16);
40
41 for (auto& s : ms)
42 s.updateMatchedPairs();
43
44 controllerMessagesScratchBuffer.ensureStorageAllocated (32);
45}
46
47tracktion::graph::NodeProperties MidiNode::getNodeProperties()
48{
50 props.hasMidi = true;
51 props.nodeID = (size_t) editItemID.getRawID();
52 return props;
53}
54
55void MidiNode::prepareToPlay (const tracktion::graph::PlaybackInitialisationInfo& info)
56{
57 sampleRate = info.sampleRate;
58
59 if (info.nodeGraphToReplace != nullptr)
60 shouldCreateMessagesForTime = findNodeWithID<MidiNode> (*info.nodeGraphToReplace, getNodeProperties().nodeID) == nullptr;
61}
62
63bool MidiNode::isReadyToProcess()
64{
65 return true;
66}
67
68void MidiNode::process (ProcessContext& pc)
69{
70 SCOPED_REALTIME_CHECK
71 const auto timelineRange = getTimelineSampleRange();
72
73 if (timelineRange.isEmpty())
74 return;
75
76 if (shouldBeMutedDelegate && shouldBeMutedDelegate())
77 return;
78
79 if (ms.size() > 0 && timelineRange.getStart() < lastStart)
80 if (++currentSequence >= ms.size())
81 currentSequence = 0;
82
83 lastStart = timelineRange.getStart();
84
85 if (timeBase == MidiList::TimeBase::beats)
86 {
87 const auto sectionEditTime = getEditBeatRange();
88
89 if (sectionEditTime.isEmpty()
90 || sectionEditTime.getEnd().inBeats() <= editRange.getStart()
91 || sectionEditTime.getStart().inBeats() >= editRange.getEnd())
92 return;
93
94 const auto secondsPerBeat = getEditTimeRange().getLength().inSeconds() / sectionEditTime.getLength().inBeats();
95
96 processSection (pc,
97 { sectionEditTime.getStart().inBeats(), sectionEditTime.getEnd().inBeats() },
98 secondsPerBeat, ms[currentSequence]);
99 }
100 else
101 {
102 const auto sectionEditTime = getEditTimeRange();
103
104 if (sectionEditTime.isEmpty()
105 || sectionEditTime.getEnd().inSeconds() <= editRange.getStart()
106 || sectionEditTime.getStart().inSeconds() >= editRange.getEnd())
107 return;
108
109 processSection (pc,
110 { sectionEditTime.getStart().inSeconds(), sectionEditTime.getEnd().inSeconds() },
111 1.0, ms[currentSequence]);
112 }
113}
114
115void MidiNode::processSection (Node::ProcessContext& pc,
116 juce::Range<double> sectionEditRange,
117 double secondsPerTimeBase,
119{
120 if (sectionEditRange.isEmpty()
121 || sectionEditRange.getEnd() <= editRange.getStart()
122 || sectionEditRange.getStart() >= editRange.getEnd())
123 return;
124
125 const auto localTime = sectionEditRange - editRange.getStart();
126 const bool mute = clipLevel.isMute();
127
128 if (mute)
129 {
130 if (mute != wasMute)
131 {
132 wasMute = mute;
133 MidiNodeHelpers::createNoteOffs (pc.buffers.midi, sequence, midiSourceID, localTime.getStart(), {}, getPlayHead().isPlaying());
134 }
135
136 return;
137 }
138
139 if (! getPlayHeadState().isContiguousWithPreviousBlock() || localTime.getStart() <= 0.00001 || shouldCreateMessagesForTime)
140 {
141 MidiNodeHelpers::createMessagesForTime (pc.buffers.midi, sequence, localTime.getStart(),
142 channelNumbers, clipLevel, useMPEChannelMode, midiSourceID,
143 controllerMessagesScratchBuffer);
144 shouldCreateMessagesForTime = false;
145 }
146
147 auto numEvents = sequence.getNumEvents();
148
149 if (numEvents != 0)
150 {
151 currentIndex = juce::jlimit (0, numEvents - 1, currentIndex);
152
153 if (sequence.getEventTime (currentIndex) >= localTime.getStart())
154 {
155 while (currentIndex > 0 && sequence.getEventTime (currentIndex - 1) >= localTime.getStart())
156 --currentIndex;
157 }
158 else
159 {
160 while (currentIndex < numEvents && sequence.getEventTime (currentIndex) < localTime.getStart())
161 ++currentIndex;
162 }
163 }
164
165 auto volScale = clipLevel.getGain();
166 const auto lastBlockOfLoop = getPlayHeadState().isLastBlockOfLoop();
167 const double durationOfOneSample = sectionEditRange.getLength() / pc.numSamples;
168
169 for (;;)
170 {
171 if (auto meh = sequence.getEventPointer (currentIndex++))
172 {
173 auto eventTime = meh->message.getTimeStamp();
174
175 // This correction here is to avoid rounding errors converting to and from sample position and times
176 const auto timeCorrection = lastBlockOfLoop ? (meh->message.isNoteOff() ? 0.0 : durationOfOneSample) : 0.0;
177
178 if (eventTime >= (localTime.getEnd() - timeCorrection))
179 break;
180
181 eventTime -= localTime.getStart();
182
183 if (eventTime >= 0.0)
184 {
185 juce::MidiMessage m (meh->message);
186 m.multiplyVelocity (volScale);
187 const auto eventTimeSeconds = eventTime * secondsPerTimeBase;
188 pc.buffers.midi.addMidiMessage (m, eventTimeSeconds, midiSourceID);
189 }
190 }
191 else
192 {
193 break;
194 }
195 }
196
197 // N.B. if the note-off is added on the last time it may not be sent to the plugin which can break the active note-state.
198 // To avoid this, make sure any added messages are nudged back by 0.00001s
199 if (getPlayHeadState().isLastBlockOfLoop())
200 MidiNodeHelpers::createNoteOffs (pc.buffers.midi, sequence, midiSourceID,
201 localTime.getEnd(),
202 localTime.getLength() - 0.00001,
203 getPlayHead().isPlaying());
204
205 #if TRACKTION_SANITY_CHECK_MIDI_BUFFERS
206 MidiNodeHelpers::sanityCheckMidiBuffer (pc.buffers.midi, localTime.getLength());
207 #endif
208}
209
210
211
212}} // namespace tracktion { inline namespace engine
constexpr ValueType getStart() const noexcept
constexpr bool isEmpty() const noexcept
constexpr ValueType getEnd() const noexcept
constexpr ValueType getLength() const noexcept
TimeBase
Determines MIDI event timing.
Struct to describe a single iteration of a process call.
#define jassert(expression)
T move(T... args)
Type jlimit(Type lowerLimit, Type upperLimit, Type valueToConstrain) noexcept
Holds some really basic properties of a node.
Passed into Nodes when they are being initialised, to give them useful contextual information that th...
typedef size_t