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

« « « Anklang Documentation
Loading...
Searching...
No Matches
tracktion_SlotControlNode.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
11#include "../../model/clips/tracktion_LaunchHandle.h"
12
13namespace tracktion { inline namespace engine
14{
15
16SlotControlNode::SlotControlNode (ProcessState& ps,
18 std::optional<BeatDuration> stopDuration_,
19 std::function<void (MonotonicBeat)> stopFunction_,
20 EditItemID slotID_,
22 : TracktionEngineNode (ps),
23 launchHandle (std::move (launchHandle_)),
24 stopDuration (stopDuration_),
25 stopFunction (std::move (stopFunction_)),
26 slotID (slotID_),
27 input (std::move (input_)),
28 localPlayheadState (ps.playHeadState.playHead),
29 localProcessState (localPlayheadState, *ps.getTempoSequence())
30{
31 assert (getProcessState().getTempoSequence() != nullptr);
32 assert (launchHandle);
33
34 for (auto n : transformNodes (*input))
35 {
36 orderedNodes.push_back (n);
37
38 if (n->getDirectInputNodes().empty())
39 leafNodes.push_back (n);
40
41 if (auto engineNode = dynamic_cast<TracktionEngineNode*> (n))
42 engineNode->setProcessState (localProcessState);
43
44 if (auto dynamicNode = dynamic_cast<DynamicallyOffsettableNodeBase*> (n))
45 offsetNodes.push_back (dynamicNode);
46
47 if (auto mn = dynamic_cast<LoopingMidiNode*> (n))
48 midiNode = mn;
49 }
50}
51
52const LaunchHandle& SlotControlNode::getLaunchHandle() const
53{
54 return *launchHandle;
55}
56
57const LaunchHandle* SlotControlNode::getLaunchHandleIfNotUnique() const
58{
59 return launchHandle.use_count() > 1 ? launchHandle.get() : nullptr;
60}
61
62tracktion::graph::NodeProperties SlotControlNode::getNodeProperties()
63{
64 auto props = input->getNodeProperties();
65 props.nodeID = static_cast<size_t> (slotID.getRawID());
66
67 return props;
68}
69
70std::vector<Node*> SlotControlNode::getDirectInputNodes()
71{
72 return {};
73}
74
75void SlotControlNode::prepareToPlay (const tracktion::graph::PlaybackInitialisationInfo& info)
76{
77 auto info2 = info;
78 info2.allocateAudioBuffer = {};
79 info2.deallocateAudioBuffer = {};
80
81 for (auto& i : orderedNodes)
82 i->initialise (info2);
83
84 // Find the lastSamples
85 const auto numChans = static_cast<size_t> (getNodeProperties().numberOfChannels);
86
87 if (numChans == 0)
88 return;
89
90 if (auto oldGraph = info.nodeGraphToReplace)
91 if (auto oldNode = findNodeWithID<SlotControlNode> (*oldGraph, (size_t) slotID.getRawID()))
92 if (oldNode->lastSamples && oldNode->lastSamples->size() == numChans)
93 lastSamples = oldNode->lastSamples;
94
95 if (lastSamples)
96 return;
97
98 lastSamples = std::make_shared<std::vector<float>> (numChans, 0.0f);
99}
100
101bool SlotControlNode::isReadyToProcess()
102{
103 for (auto& i : leafNodes)
104 if (! i->isReadyToProcess())
105 return false;
106
107 return true;
108}
109
110void SlotControlNode::prefetchBlock (juce::Range<int64_t> referenceSampleRange)
111{
112 for (auto& node : orderedNodes)
113 node->prepareForNextBlock (referenceSampleRange);
114}
115
116void SlotControlNode::process (ProcessContext& pc)
117{
118 // If the playhead has just stopped, let it run to fade the audio/stop MIDI notes etc.
119 if (wasPlaying && ! getPlayHead().isPlaying())
120 {
121 wasPlaying = false;
122 processStop (pc, 0.0);
123 return;
124 }
125
126 const auto syncRange = getProcessState().getSyncRange();
127
128 if (const auto editBeatRange = getEditBeatRange(); ! editBeatRange.isEmpty())
129 {
130 if (stopDuration)
131 {
132 if (auto playedMonotonicRange = launchHandle->getPlayedMonotonicRange())
133 {
134 const auto blockRange = MonotonicBeatRange { { syncRange.start.monotonicBeat.v, syncRange.end.monotonicBeat.v } };
135 const auto stopPoint = MonotonicBeat { playedMonotonicRange->v.getStart() + *stopDuration };
136
137 if (blockRange.v.contains (stopPoint.v))
138 {
139 const auto stopQueued = launchHandle->getQueuedStatus() == LaunchHandle::QueueState::stopQueued;
140 launchHandle->stop (stopPoint);
141
142 // If there was a stop already queued, ignore the follow action
143 if (! stopQueued && stopFunction)
144 stopFunction (stopPoint);
145 }
146 }
147 }
148
149 if (auto splitStatus = launchHandle->advance (syncRange);
150 ! splitStatus.range1.isEmpty())
151 processSplitSection (pc, splitStatus);
152 }
153 else if (launchHandle->getQueuedStatus() == LaunchHandle::QueueState::stopQueued)
154 {
155 launchHandle->advance (syncRange);
156 }
157}
158
159//==============================================================================
160void SlotControlNode::processSplitSection (ProcessContext& pc, LaunchHandle::SplitStatus status)
161{
162 const auto totalRange = status.range1.isEmpty() ? status.range2
163 : (status.range2.isEmpty() ? status.range1
164 : status.range1.getUnionWith (status.range2));
165 const juce::NormalisableRange blockRangeBeats (totalRange.getStart().inBeats(),
166 totalRange.getEnd().inBeats());
167
168 auto processSubSection = [this, &pc, &blockRangeBeats, editBeatRange = getEditBeatRange(), editTimeRange = getEditTimeRange()] (auto section, bool isPlaying, auto playStartTime)
169 {
170 const auto proportion = juce::Range (blockRangeBeats.convertTo0to1 (section.getStart().inBeats()),
171 blockRangeBeats.convertTo0to1 (section.getEnd().inBeats()));
172 const auto startFrame = (choc::buffer::FrameCount) std::llround (proportion.getStart() * pc.numSamples);
173 const auto endFrame = (choc::buffer::FrameCount) std::llround (proportion.getEnd() * pc.numSamples);
174
175 const auto sectionNumFrames = endFrame - startFrame;
176
177 if (sectionNumFrames == 0)
178 return;
179
180 const auto numRefSamples = pc.referenceSampleRange.getLength();
181 const auto startRefSample = pc.referenceSampleRange.getStart() + (int64_t) std::llround (proportion.getStart() * numRefSamples);
182 const auto endRefSample = pc.referenceSampleRange.getStart() + (int64_t) std::llround (proportion.getEnd() * numRefSamples);
183
184 const juce::Range subSectionReferenceSampleRange (startRefSample, endRefSample);
185
186 auto sectionBufferView = pc.buffers.audio.getFrameRange ({ startFrame, endFrame });
187 ProcessContext subSection {
188 sectionNumFrames, subSectionReferenceSampleRange,
189 { sectionBufferView, pc.buffers.midi }
190 };
191
192 const auto startBeat = editBeatRange.getStart() + editBeatRange.getLength() * proportion.getStart();
193 const auto endBeat = editBeatRange.getStart() + editBeatRange.getLength() * proportion.getEnd();
194 const auto startTime = editTimeRange.getStart() + editTimeRange.getLength() * proportion.getStart();
195 const auto endTime = editTimeRange.getStart() + editTimeRange.getLength() * proportion.getEnd();
196 processSection (subSection, { startBeat, endBeat }, { startTime, endTime }, section, isPlaying, playStartTime);
197 };
198
199 if (status.isSplit)
200 {
201 processSubSection (status.range1, status.playing1, status.playStartTime1);
202 processSubSection (status.range2, status.playing2, status.playStartTime2);
203 }
204 else
205 {
206 processSubSection (status.range1, status.playing1, status.playStartTime1);
207 }
208}
209
210void SlotControlNode::processSection (ProcessContext& pc, BeatRange editBeatRange, TimeRange editTimeRange, BeatRange unloopedClipBeatRange,
211 bool isPlaying, std::optional<BeatPosition> playStartTime)
212{
213 const juce::ScopeGuard scope { [this, isPlaying]
214 {
215 wasPlaying = isPlaying;
216 localPlayheadState.playheadJumped = false;
217 localPlayheadState.firstBlockOfLoop = false;
218 } };
219
220 if (! isPlaying)
221 {
222 if (wasPlaying != isPlaying)
223 processStop (pc, (editTimeRange.getStart() - getEditTimeRange().getStart()).inSeconds());
224 else
225 pc.buffers.audio.clear();
226
227 return;
228 }
229 else if (! wasPlaying)
230 {
231 // Force the playheadJumped state to true in order to resync MIDI streams etc.
232 localPlayheadState.playheadJumped = true;
233
234 // Set this flag to avoid fading in
235 localPlayheadState.firstBlockOfLoop = true;
236 }
237
238 localProcessState.setPlaybackSpeedRatio (getPlaybackSpeedRatio());
239 localProcessState.update (getSampleRate(), pc.referenceSampleRange,
240 ProcessState::UpdateContinuityFlags::no);
241 auto& ps = getProcessState();
242 localProcessState.setSyncRange (ps.getSyncRange());
243
244 // Update the offset for compatible Nodes
245 if (playStartTime)
246 {
247 const auto clipEditOffset = editBeatRange.getStart() - unloopedClipBeatRange.getStart();
248 const auto offset = clipEditOffset + toDuration (*playStartTime);
249
250 if (! almostEqual (lastOffset.inBeats(), offset.inBeats(), 0.0000001))
251 {
252 lastOffset = offset;
253
254 // Force the playheadJumped state to true in order to send note-offs.
255 localPlayheadState.playheadJumped = true;
256
257 for (auto n : offsetNodes)
258 n->setDynamicOffsetBeats (offset);
259 }
260 }
261
262 // Prepare ordered Nodes
263 for (auto& node : orderedNodes)
264 node->prepareForNextBlock (pc.referenceSampleRange);
265
266 // Process ordered Nodes
267 for (auto& node : orderedNodes)
268 node->process (pc.numSamples, pc.referenceSampleRange);
269
270 // Copy audio from processed source Node
271 auto sourceBuffers = input->getProcessedOutput();
272 assert (sourceBuffers.audio.size == pc.buffers.audio.size);
273 copyIfNotAliased (pc.buffers.audio, sourceBuffers.audio);
274 pc.buffers.midi.copyFrom (sourceBuffers.midi);
275
276 // Update last samples
277 if (lastSamples)
278 {
279 const auto numChannels = pc.buffers.audio.size.numChannels;
280 const auto numFrames = pc.buffers.audio.size.numFrames;
281 jassert (lastSamples->size() == static_cast<size_t> (numChannels));
282
283 for (choc::buffer::ChannelCount channel = 0; channel < numChannels; ++channel)
284 {
285 const auto dest = pc.buffers.audio.getIterator (channel).sample;
286 auto& lastSample = (*lastSamples)[(size_t) channel];
287 lastSample = dest[numFrames - 1];
288 }
289 }
290}
291
292void SlotControlNode::processStop (ProcessContext& pc, double timestampForMidiNoteOffs)
293{
294 if (midiNode)
295 {
296 midiNode->killActiveNotes (pc.buffers.midi, timestampForMidiNoteOffs);
297 }
298
299 // Fade out last sample
300 if (lastSamples)
301 {
302 auto& buffer = pc.buffers.audio;
303 const auto numChannels = buffer.size.numChannels;
304 const auto numFrames = buffer.size.numFrames;
305
306 if (const auto lastSampleFadeLength = std::min (numFrames, 40u);
307 lastSampleFadeLength > 0)
308 {
309 for (choc::buffer::ChannelCount channel = 0; channel < numChannels; ++channel)
310 {
311 if (channel < (choc::buffer::ChannelCount) lastSamples->size())
312 {
313 const auto dest = buffer.getIterator (channel).sample;
314 auto& lastSample = (*lastSamples)[(size_t) channel];
315
316 for (uint32_t i = 0; i < lastSampleFadeLength; ++i)
317 {
318 auto alpha = i / (float) lastSampleFadeLength;
319 dest[i] = lastSample * (1.0f - alpha);
320 }
321
322 lastSample = 0.0f;
323 }
324 else
325 {
326 buffer.getChannel (channel).clear ();
327 }
328 }
329 }
330 }
331
332 if (launchHandle->getQueuedStatus() == LaunchHandle::QueueState::stopQueued)
333 launchHandle->stop ({});
334
335 launchHandle->advance (getProcessState().getSyncRange());
336}
337
338}} // namespace tracktion { inline namespace engine
assert
Represents two beat ranges where the play state can be different in each.
Struct to describe a single iteration of a process call.
T is_pointer_v
#define jassert(expression)
typedef float
T min(T... args)
T move(T... args)
constexpr TimeDuration toDuration(TimePosition)
Converts a TimePosition to a TimeDuration.
T llround(T... args)
typedef int64_t
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