diff --git a/BREAKING-CHANGES.md b/BREAKING-CHANGES.md index 8782a2b6443..c430b89bacd 100644 --- a/BREAKING-CHANGES.md +++ b/BREAKING-CHANGES.md @@ -2,6 +2,20 @@ ___ +### Change +`toBitSet (const juce::Array&)` now returns a bitset of only the tracks passed in. + +#### Possible Issues +Previously it ignored its argument and set a bit for every track in the Edit, so anything built from it addressed the whole Edit. Code that passed a subset of tracks - most visibly `Renderer::Parameters::tracksToDo` and `Renderer::measureStatistics()` - will now render or measure just that subset instead of everything. + +#### Workaround +Pass `getAllTracks (edit)` where the whole Edit really is wanted, or leave `Renderer::Parameters::tracksToDo` empty, which already means "all tracks". + +#### Rationale +The function used its argument only to reach the Edit and then looped over every track, which contradicted its documented behaviour and silently broke subset rendering. See issue #399. + +___ + ### Change `Plugin` has a new pure virtual method `getBusses()` which every subclass must implement. diff --git a/modules/tracktion_core/tracktion_TestConfig.h b/modules/tracktion_core/tracktion_TestConfig.h index 380a95c66b4..f3588c8843e 100644 --- a/modules/tracktion_core/tracktion_TestConfig.h +++ b/modules/tracktion_core/tracktion_TestConfig.h @@ -35,6 +35,7 @@ #define ENGINE_UNIT_TESTS_DAWPROJECT 1 #define ENGINE_UNIT_TESTS_DELAY_PLUGIN 1 #define ENGINE_UNIT_TESTS_EDIT 1 +#define ENGINE_UNIT_TESTS_EDIT_UTILITIES 1 #define ENGINE_UNIT_TESTS_EDITCLIP 1 #define ENGINE_UNIT_TESTS_EDIT_LOADER 1 #define ENGINE_UNIT_TESTS_EDIT_TIME 1 diff --git a/modules/tracktion_engine/model/edit/tracktion_EditUtilities.cpp b/modules/tracktion_engine/model/edit/tracktion_EditUtilities.cpp index 0d15e1c96e4..fa964b881c0 100644 --- a/modules/tracktion_engine/model/edit/tracktion_EditUtilities.cpp +++ b/modules/tracktion_engine/model/edit/tracktion_EditUtilities.cpp @@ -255,7 +255,7 @@ juce::BigInteger toBitSet (const juce::Array& tracks) { auto allTracks = getAllTracks (first->edit); - for (auto t : allTracks) + for (auto t : tracks) if (int index = allTracks.indexOf (t); index >= 0) bitset.setBit (index); } diff --git a/modules/tracktion_engine/model/edit/tracktion_EditUtilities.h b/modules/tracktion_engine/model/edit/tracktion_EditUtilities.h index 4f00993f0a1..ad803b778d3 100644 --- a/modules/tracktion_engine/model/edit/tracktion_EditUtilities.h +++ b/modules/tracktion_engine/model/edit/tracktion_EditUtilities.h @@ -95,8 +95,10 @@ bool containsTrack (const Edit&, const Track&); /** Returns the TrackOutput if the given track has one. */ TrackOutput* getTrackOutput (Track&); -/** Returns the set of tracks as a BigInteger with each bit corresponding to the - array of all tracks in an Edit. Used in Renderer. +/** Returns the given tracks as a BigInteger, with each set bit corresponding to a + track's index in the Edit's array of all tracks (@see getAllTracks). + The Edit is taken from the first track in the array; any tracks not in that Edit + are ignored. Used in Renderer. */ juce::BigInteger toBitSet (const juce::Array&); diff --git a/modules/tracktion_engine/model/edit/tracktion_EditUtilities.test.cpp b/modules/tracktion_engine/model/edit/tracktion_EditUtilities.test.cpp new file mode 100644 index 00000000000..2ee4b53a9d5 --- /dev/null +++ b/modules/tracktion_engine/model/edit/tracktion_EditUtilities.test.cpp @@ -0,0 +1,100 @@ +/* + ,--. ,--. ,--. ,--. + ,-' '-.,--.--.,--,--.,---.| |,-.,-' '-.`--' ,---. ,--,--, Copyright 2024 + '-. .-'| .--' ,-. | .--'| /'-. .-',--.| .-. || \ Tracktion Software + | | | | \ '-' \ `--.| \ \ | | | |' '-' '| || | Corporation + `---' `--' `--`--'`---'`--'`--' `---' `--' `---' `--''--' www.tracktion.com + + Tracktion Engine uses a GPL/commercial licence - see LICENCE.md for details. +*/ + +#if TRACKTION_UNIT_TESTS && ENGINE_UNIT_TESTS_EDIT_UTILITIES + +#include + +namespace tracktion::inline engine { + +//============================================================================== +//============================================================================== +TEST_SUITE ("tracktion_engine") +{ + TEST_CASE ("toBitSet: returns only the tracks it was given") + { + auto& engine = *Engine::getEngines()[0]; + auto edit = Edit::createSingleTrackEdit (engine, Edit::EditRole::forRendering); + edit->ensureNumberOfAudioTracks (3); + + auto audioTracks = getAudioTracks (*edit); + REQUIRE (audioTracks.size() == 3); + + const auto allTracks = getAllTracks (*edit); + + SUBCASE ("A single track sets a single bit") + { + const auto bits = toBitSet ({ audioTracks[0] }); + + CHECK (bits.countNumberOfSetBits() == 1); + CHECK (bits[allTracks.indexOf (audioTracks[0])]); + CHECK (! bits[allTracks.indexOf (audioTracks[1])]); + CHECK (! bits[allTracks.indexOf (audioTracks[2])]); + } + + SUBCASE ("A subset sets exactly those bits") + { + const auto bits = toBitSet ({ audioTracks[0], audioTracks[2] }); + + CHECK (bits.countNumberOfSetBits() == 2); + CHECK (bits[allTracks.indexOf (audioTracks[0])]); + CHECK (! bits[allTracks.indexOf (audioTracks[1])]); + CHECK (bits[allTracks.indexOf (audioTracks[2])]); + } + + SUBCASE ("All tracks sets every bit") + { + const auto bits = toBitSet (allTracks); + CHECK (bits.countNumberOfSetBits() == allTracks.size()); + } + + SUBCASE ("An empty array gives an empty bitset") + { + CHECK (toBitSet ({}).isZero()); + } + + SUBCASE ("Tracks from another Edit are ignored") + { + auto otherEdit = Edit::createSingleTrackEdit (engine, Edit::EditRole::forRendering); + auto otherTrack = getAudioTracks (*otherEdit)[0]; + + const auto bits = toBitSet ({ audioTracks[1], otherTrack }); + + CHECK (bits.countNumberOfSetBits() == 1); + CHECK (bits[allTracks.indexOf (audioTracks[1])]); + } + + SUBCASE ("Bit indices match Track::getIndexInEditTrackList") + { + for (auto t : allTracks) + { + const auto bits = toBitSet ({ t }); + + CHECK (bits.countNumberOfSetBits() == 1); + CHECK (bits[t->getIndexInEditTrackList()]); + } + } + + SUBCASE ("toTrackArray round-trips the tracks passed in") + { + const juce::Array subset { audioTracks[2], audioTracks[0] }; + const auto roundTripped = toTrackArray (*edit, toBitSet (subset)); + + CHECK (roundTripped.size() == subset.size()); + + for (auto t : subset) + CHECK (roundTripped.contains (t)); + } + } +} + +} // namespace tracktion::inline engine + +#endif diff --git a/modules/tracktion_engine/playback/graph/tracktion_EditNodeBuilder.test.cpp b/modules/tracktion_engine/playback/graph/tracktion_EditNodeBuilder.test.cpp index c2e2b0d2666..5a94e9d4c00 100644 --- a/modules/tracktion_engine/playback/graph/tracktion_EditNodeBuilder.test.cpp +++ b/modules/tracktion_engine/playback/graph/tracktion_EditNodeBuilder.test.cpp @@ -31,17 +31,6 @@ namespace editnode_test_helpers return createNodeForEdit (edit, params); } - static juce::BigInteger getTracksMask (const juce::Array& tracks) - { - juce::BigInteger tracksMask; - - for (auto t : tracks) - tracksMask.setBit (t->getIndexInEditTrackList()); - - jassert (tracksMask.countNumberOfSetBits() == tracks.size()); - return tracksMask; - } - static Renderer::Statistics logStats (Renderer::Statistics stats) { MESSAGE (("Stats: peak " + juce::String (stats.peak) + ", avg " + juce::String (stats.average) + ", duration " + juce::String (stats.audioDuration)).toStdString()); @@ -51,7 +40,7 @@ namespace editnode_test_helpers static void expectPeak (Edit& edit, TimeRange tr, juce::Array tracks, float expectedPeak) { auto blockSize = edit.engine.getDeviceManager().getBlockSize(); - auto stats = logStats (Renderer::measureStatistics ("", edit, tr, getTracksMask (tracks), blockSize)); + auto stats = logStats (Renderer::measureStatistics ("", edit, tr, toBitSet (tracks), blockSize)); CHECK_MESSAGE (juce::isWithin (stats.peak, expectedPeak, 0.01f), (juce::String ("Expected peak: ") + juce::String (expectedPeak, 4)).toStdString()); } @@ -59,7 +48,7 @@ namespace editnode_test_helpers static void expectRMS (Edit& edit, TimeRange tr, juce::Array tracks, float expectedRMS) { auto blockSize = edit.engine.getDeviceManager().getBlockSize(); - auto stats = logStats (Renderer::measureStatistics ("", edit, tr, getTracksMask (tracks), blockSize)); + auto stats = logStats (Renderer::measureStatistics ("", edit, tr, toBitSet (tracks), blockSize)); CHECK_MESSAGE (juce::isWithin (stats.average, expectedRMS, 0.01f), (juce::String ("Expected RMS: ") + juce::String (expectedRMS, 4)).toStdString()); } diff --git a/modules/tracktion_engine/plugins/tracktion_Plugins.test.cpp b/modules/tracktion_engine/plugins/tracktion_Plugins.test.cpp index 036dc38a897..e6d0f55b534 100644 --- a/modules/tracktion_engine/plugins/tracktion_Plugins.test.cpp +++ b/modules/tracktion_engine/plugins/tracktion_Plugins.test.cpp @@ -114,17 +114,6 @@ TEST_SUITE ("tracktion_engine") } } -static juce::BigInteger getTracksMask (const juce::Array& tracks) -{ - juce::BigInteger tracksMask; - - for (auto t : tracks) - tracksMask.setBit (t->getIndexInEditTrackList()); - - jassert (tracksMask.countNumberOfSetBits() == tracks.size()); - return tracksMask; -} - template static std::unique_ptr getSinFile (double sampleRate) { @@ -168,7 +157,7 @@ static std::unique_ptr getSinFile (double sampleRate) static void expectPeak (Edit& edit, TimeRange tr, juce::Array tracks, float expectedPeak) { auto blockSize = edit.engine.getDeviceManager().getBlockSize(); - auto stats = Renderer::measureStatistics ("PDC Tests", edit, tr, getTracksMask (tracks), blockSize); + auto stats = Renderer::measureStatistics ("PDC Tests", edit, tr, toBitSet (tracks), blockSize); MESSAGE ("Stats: peak " * juce::String (stats.peak).toStdString() * ", avg " * juce::String (stats.average).toStdString() * ", duration " * juce::String (stats.audioDuration).toStdString()); diff --git a/modules/tracktion_engine/tracktion_engine_model_1.cpp b/modules/tracktion_engine/tracktion_engine_model_1.cpp index 9abad2e8b57..dd61576b2d6 100644 --- a/modules/tracktion_engine/tracktion_engine_model_1.cpp +++ b/modules/tracktion_engine/tracktion_engine_model_1.cpp @@ -37,6 +37,7 @@ using namespace std::literals; #include "model/edit/tracktion_Edit.cpp" #include "model/edit/tracktion_Edit.test.cpp" #include "model/edit/tracktion_EditUtilities.cpp" +#include "model/edit/tracktion_EditUtilities.test.cpp" #include "model/edit/tracktion_Scene.cpp" #include "model/edit/tracktion_SourceFileReference.cpp" #include "model/edit/tracktion_SourceFileReference.test.cpp"