Skip to content

Commit fc819ac

Browse files
committed
More refactoring towards multichannel support
1 parent bda344f commit fc819ac

File tree

12 files changed

+666
-204
lines changed

12 files changed

+666
-204
lines changed

modules/tracktion_core/tracktion_TestConfig.h

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@
1313
#define TRACKTION_UNIT_TESTS_ALGORITHM 1
1414

1515
// Defined in tracktion_engine
16+
#define GRAPH_UNIT_TESTS_CHANNELREMAPPINGNODE 1
1617
#define GRAPH_UNIT_TESTS_WAVENODE 1
1718
#define GRAPH_UNIT_TESTS_MIDINODE 1
1819
#define GRAPH_UNIT_TESTS_RACKNODE 1

modules/tracktion_engine/model/tracks/tracktion_AudioTrack.cpp

Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -606,6 +606,38 @@ bool AudioTrack::canPlayMidi() const
606606
return false;
607607
}
608608

609+
//==============================================================================
610+
ChannelConfiguration AudioTrack::getChannelConfiguration() const
611+
{
612+
ChannelConfiguration result;
613+
bool hasAnyClips = false;
614+
615+
for (auto clip : getClips())
616+
{
617+
if (auto audioClip = dynamic_cast<AudioClipBase*> (clip))
618+
{
619+
auto clipConfig = audioClip->getActiveChannelConfiguration();
620+
621+
if (! hasAnyClips)
622+
{
623+
result = clipConfig;
624+
hasAnyClips = true;
625+
}
626+
else if (clipConfig.getNumChannels() > result.getNumChannels())
627+
{
628+
// Use the largest format when clips differ
629+
result = clipConfig;
630+
}
631+
}
632+
}
633+
634+
// Default to stereo if no audio clips
635+
if (! hasAnyClips)
636+
return ChannelConfiguration::stereo();
637+
638+
return result;
639+
}
640+
609641
//==============================================================================
610642
ClipSlotList& AudioTrack::getClipSlotList()
611643
{

modules/tracktion_engine/model/tracks/tracktion_AudioTrack.h

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -86,6 +86,14 @@ class AudioTrack : public ClipTrack,
8686
bool canPlayAudio() const;
8787
bool canPlayMidi() const;
8888

89+
//==============================================================================
90+
/** Returns the channel configuration for this track, derived from its clips.
91+
If all clips have the same channel format, returns that format.
92+
If clips differ, returns the largest format.
93+
Returns stereo if there are no clips.
94+
*/
95+
ChannelConfiguration getChannelConfiguration() const;
96+
8997
//==============================================================================
9098
/** Returns the ClipSlotList for this track. */
9199
ClipSlotList& getClipSlotList();
Lines changed: 189 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,189 @@
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+
namespace tracktion { inline namespace engine
12+
{
13+
14+
//==============================================================================
15+
ChannelRemappingNode::ChannelRemappingNode (std::unique_ptr<tracktion::graph::Node> inputNode,
16+
ChannelMap channelMapToUse)
17+
: mode (Mode::ExplicitMapping),
18+
input (std::move (inputNode)),
19+
channelMap (std::move (channelMapToUse))
20+
{
21+
jassert (input != nullptr);
22+
23+
// Need to clear buffers since we use add() for accumulation
24+
setOptimisations ({ tracktion::graph::ClearBuffers::yes,
25+
tracktion::graph::AllocateAudioBuffer::yes });
26+
}
27+
28+
ChannelRemappingNode::ChannelRemappingNode (std::unique_ptr<tracktion::graph::Node> processorNode,
29+
ChannelConfiguration inputConfig,
30+
ChannelConfiguration processorConfig)
31+
: mode (Mode::ProcessorPassthrough),
32+
input (std::move (processorNode)),
33+
inputChannelConfig (std::move (inputConfig)),
34+
processorChannelConfig (std::move (processorConfig))
35+
{
36+
jassert (input != nullptr);
37+
jassert (! inputChannelConfig.isEmpty());
38+
jassert (! processorChannelConfig.isEmpty());
39+
40+
// Cache the original input node - it's the first direct input of the processor
41+
// We need this for passthrough mode to access channels beyond what the processor handles
42+
auto processorInputs = input->getDirectInputNodes();
43+
44+
if (! processorInputs.empty())
45+
originalInputNode = processorInputs[0];
46+
47+
setOptimisations ({ tracktion::graph::ClearBuffers::no,
48+
tracktion::graph::AllocateAudioBuffer::yes });
49+
}
50+
51+
//==============================================================================
52+
tracktion::graph::NodeProperties ChannelRemappingNode::getNodeProperties()
53+
{
54+
auto props = input->getNodeProperties();
55+
56+
constexpr size_t channelRemappingMagicHash = size_t (0x6368616e72656d); // "chanrem"
57+
58+
if (mode == Mode::ExplicitMapping)
59+
{
60+
props.hasAudio = ! channelMap.isEmpty();
61+
props.numberOfChannels = channelMap.getRequiredOutputChannels();
62+
63+
// Include mapping in hash
64+
for (const auto& [src, dest] : channelMap.entries)
65+
{
66+
hash_combine (props.nodeID, src);
67+
hash_combine (props.nodeID, dest);
68+
}
69+
}
70+
else // ProcessorPassthrough
71+
{
72+
const auto numInputChannels = inputChannelConfig.getNumChannels();
73+
const auto numProcessorChannels = processorChannelConfig.getNumChannels();
74+
75+
// Output channel count depends on mode:
76+
// - Passthrough mode (input > processor): output = input channels
77+
// - Expansion mode (input < processor): output = processor channels
78+
props.numberOfChannels = std::max (numInputChannels, numProcessorChannels);
79+
80+
hash_combine (props.nodeID, numInputChannels);
81+
hash_combine (props.nodeID, numProcessorChannels);
82+
}
83+
84+
if (props.nodeID != 0)
85+
hash_combine (props.nodeID, channelRemappingMagicHash);
86+
87+
return props;
88+
}
89+
90+
std::vector<tracktion::graph::Node*> ChannelRemappingNode::getDirectInputNodes()
91+
{
92+
return { input.get() };
93+
}
94+
95+
bool ChannelRemappingNode::isReadyToProcess()
96+
{
97+
return input->hasProcessed();
98+
}
99+
100+
void ChannelRemappingNode::prepareToPlay (const tracktion::graph::PlaybackInitialisationInfo&)
101+
{
102+
// Nothing special needed - input handles its own preparation
103+
}
104+
105+
void ChannelRemappingNode::process (ProcessContext& pc)
106+
{
107+
if (mode == Mode::ExplicitMapping)
108+
processExplicitMapping (pc);
109+
else
110+
processProcessorPassthrough (pc);
111+
}
112+
113+
//==============================================================================
114+
void ChannelRemappingNode::processExplicitMapping (ProcessContext& pc)
115+
{
116+
auto inputBuffers = input->getProcessedOutput();
117+
118+
// Always pass through MIDI
119+
pc.buffers.midi.copyFrom (inputBuffers.midi);
120+
121+
// Remap audio channels - use add() to accumulate (allows multiple sources to one dest)
122+
for (const auto& [src, dest] : channelMap.entries)
123+
{
124+
if (src < static_cast<int> (inputBuffers.audio.getNumChannels())
125+
&& dest < static_cast<int> (pc.buffers.audio.getNumChannels()))
126+
{
127+
add (pc.buffers.audio.getChannel (static_cast<choc::buffer::ChannelCount> (dest)),
128+
inputBuffers.audio.getChannel (static_cast<choc::buffer::ChannelCount> (src)));
129+
}
130+
}
131+
}
132+
133+
void ChannelRemappingNode::processProcessorPassthrough (ProcessContext& pc)
134+
{
135+
auto& destAudio = pc.buffers.audio;
136+
auto& destMidi = pc.buffers.midi;
137+
138+
const auto numInputChannels = inputChannelConfig.getNumChannels();
139+
const auto numProcessorChannels = processorChannelConfig.getNumChannels();
140+
141+
// Get the processor's output
142+
auto processorOutput = input->getProcessedOutput();
143+
const auto numProcessorOutputChannels = static_cast<int> (processorOutput.audio.getNumChannels());
144+
145+
// Copy MIDI from processor
146+
destMidi.copyFrom (processorOutput.midi);
147+
148+
// Copy processed channels from processor output
149+
const auto numChannelsToCopyFromProcessor = std::min (numProcessorOutputChannels,
150+
static_cast<int> (destAudio.getNumChannels()));
151+
152+
if (numChannelsToCopyFromProcessor > 0)
153+
{
154+
auto destProcessedChannels = destAudio.getFirstChannels (static_cast<choc::buffer::ChannelCount> (numChannelsToCopyFromProcessor));
155+
auto srcProcessedChannels = processorOutput.audio.getFirstChannels (static_cast<choc::buffer::ChannelCount> (numChannelsToCopyFromProcessor));
156+
copy (destProcessedChannels, srcProcessedChannels);
157+
}
158+
159+
// PASSTHROUGH MODE: Copy extra channels from original input that the processor didn't handle
160+
if (numInputChannels > numProcessorChannels && originalInputNode != nullptr)
161+
{
162+
auto originalInput = originalInputNode->getProcessedOutput();
163+
const auto numOriginalChannels = static_cast<int> (originalInput.audio.getNumChannels());
164+
const auto numPassthroughChannels = numInputChannels - numProcessorChannels;
165+
166+
// Copy channels from processor channel count onwards
167+
for (int ch = 0; ch < numPassthroughChannels; ++ch)
168+
{
169+
const auto srcChannel = numProcessorChannels + ch;
170+
const auto destChannel = numProcessorChannels + ch;
171+
172+
if (srcChannel < numOriginalChannels && destChannel < static_cast<int> (destAudio.getNumChannels()))
173+
{
174+
copy (destAudio.getChannel (static_cast<choc::buffer::ChannelCount> (destChannel)),
175+
originalInput.audio.getChannel (static_cast<choc::buffer::ChannelCount> (srcChannel)));
176+
}
177+
else if (destChannel < static_cast<int> (destAudio.getNumChannels()))
178+
{
179+
// Source channel doesn't exist, fill with silence
180+
destAudio.getChannel (static_cast<choc::buffer::ChannelCount> (destChannel)).clear();
181+
}
182+
}
183+
}
184+
185+
// EXPANSION MODE: The processor already outputs enough channels, nothing extra to do
186+
// (processor output was already copied above)
187+
}
188+
189+
}} // namespace tracktion { inline namespace engine

0 commit comments

Comments
 (0)