Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
13 changes: 13 additions & 0 deletions .github/workflows/build.yml
Original file line number Diff line number Diff line change
Expand Up @@ -356,20 +356,32 @@ jobs:
name: masterpiece-windows-setup
path: dist/masterpiece-windows-setup.exe
if-no-files-found: error
# A CI build is worth keeping only until someone has looked at it.
# The default is 90 days, and at a few hundred megabytes a run that
# fills the account's storage on its own.
retention-days: 7

- uses: actions/upload-artifact@v4
if: runner.os == 'Linux'
with:
name: ${{ matrix.artifact }}-deb
path: dist/masterpiece-linux-*.deb
if-no-files-found: error
# A CI build is worth keeping only until someone has looked at it.
# The default is 90 days, and at a few hundred megabytes a run that
# fills the account's storage on its own.
retention-days: 7

- uses: actions/upload-artifact@v4
if: matrix.mac
with:
name: ${{ matrix.artifact }}
path: dist/${{ matrix.artifact }}.zip
if-no-files-found: error
# A CI build is worth keeping only until someone has looked at it.
# The default is 90 days, and at a few hundred megabytes a run that
# fills the account's storage on its own.
retention-days: 7

- uses: actions/upload-artifact@v4
if: '!matrix.mac'
Expand All @@ -380,3 +392,4 @@ jobs:
build/${{ matrix.preset }}/apps/MasterpiecePlugin/**/*.vst3
build/${{ matrix.preset }}/apps/MasterpiecePlugin/**/*.lv2
if-no-files-found: warn
retention-days: 7
21 changes: 21 additions & 0 deletions apps/MasterpieceApp/Main.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -483,6 +483,27 @@ class MasterpieceApp : public juce::JUCEApplication {
proc_->setPreloadStops(std::move(wanted));
}

// --organ-root <dir>: where this organ's OrganInstallationPackages is,
// for a layout the definition's own path cannot reveal.
{
const int at = args.indexOf("--organ-root");
if (at >= 0 && at + 1 < args.size())
proc_->setOrganRootOverride(juce::File(args[at + 1]));
}

// --preload-ranks 2,4,14: load exactly these ranks. For organs whose
// stops reach their pipes through pallets, where the drawn stops name no
// ranks and --preload-drawn cannot narrow the load.
{
const int at = args.indexOf("--preload-ranks");
if (at >= 0 && at + 1 < args.size()) {
std::vector<mp::Id> ranks;
for (const auto& s : juce::StringArray::fromTokens(args[at + 1], ",", ""))
if (s.getIntValue() > 0) ranks.push_back(s.getIntValue());
proc_->setPreloadRanks(std::move(ranks));
}
}

if (consolePage > 0) {
auto* win = win_.get();
// Chained rather than assigned: a take list may already have claimed
Expand Down
2 changes: 2 additions & 0 deletions src/mp_audio/CMakeLists.txt
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,8 @@ add_library(mp_audio STATIC
Convolver.h Convolver.cpp
${CMAKE_CURRENT_SOURCE_DIR}/../mp_ui/Ui.h
${CMAKE_CURRENT_SOURCE_DIR}/../mp_ui/Ui.cpp
${CMAKE_CURRENT_SOURCE_DIR}/../mp_ui/BmpImage.h
${CMAKE_CURRENT_SOURCE_DIR}/../mp_ui/BmpImage.cpp
${CMAKE_CURRENT_SOURCE_DIR}/../mp_ui/Console.h
${CMAKE_CURRENT_SOURCE_DIR}/../mp_ui/Console.cpp
${CMAKE_CURRENT_SOURCE_DIR}/../mp_ui/Settings.h
Expand Down
54 changes: 48 additions & 6 deletions src/mp_audio/MasterpieceProcessor.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -871,6 +871,8 @@ juce::String MasterpieceProcessor::settingsBody() const {
text << "stream " << (samples_.streamReleases() ? 1 : 0) << "\n";
text << "streamhead " << juce::String(samples_.streamHeadFrames()) << "\n";
text << "preload " << juce::String(preloadHead_) << "\n";
if (organRootOverride_.getFullPathName().isNotEmpty())
text << "root " << organRootOverride_.getFullPathName() << "\n";
text << "simple " << (sw.simpleWavOnly ? 1 : 0) << "\n";
text << "wind " << (sw.enableWindModel ? 1 : 0) << "\n";
text << "tremulant " << (sw.enableTremulant ? 1 : 0) << "\n";
Expand Down Expand Up @@ -945,6 +947,9 @@ void MasterpieceProcessor::applySettingsLine(const juce::String& key,
else if (key == "stream") samples_.setStreamReleases(on);
else if (key == "streamhead") samples_.setStreamHeadFrames(val.getLargeIntValue());
else if (key == "preload") preloadHead_ = val.getLargeIntValue();
// Where this organ's OrganInstallationPackages actually is, for a layout
// the definition's path cannot reveal. Taken whole: a path may have spaces.
else if (key == "root") organRootOverride_ = val.isEmpty() ? juce::File() : juce::File(val);
else if (key == "simple") sw.simpleWavOnly = on;
else if (key == "wind") sw.enableWindModel = on;
else if (key == "tremulant") sw.enableTremulant = on;
Expand Down Expand Up @@ -1180,6 +1185,8 @@ bool MasterpieceProcessor::writeGlobalFile() const {
text << "reopenlast " << (reopenLastOrgan_ ? 1 : 0) << "\n";
text << "loadticks "
<< (loadTicks_.load(std::memory_order_acquire) ? 1 : 0) << "\n";
if (cacheDir_.getFullPathName().isNotEmpty())
text << "cachedir " << cacheDir_.getFullPathName() << "\n";
if (lastOrgan_.getFullPathName().isNotEmpty())
text << "lastorgan " << lastOrgan_.getFullPathName() << "\n";

Expand Down Expand Up @@ -1222,6 +1229,11 @@ bool MasterpieceProcessor::loadGlobalDefaults() {
reopenLastOrgan_ = val.getIntValue() != 0;
} else if (key == "loadticks") {
loadTicks_.store(val.getIntValue() != 0, std::memory_order_release);
} else if (key == "cachedir") {
// A path, taken whole: the sample cache can be gigabytes, and a player
// with a small fast disk and a large slow one wants to choose which of
// them holds it.
cacheDir_ = val.isEmpty() ? juce::File() : juce::File(val);
} else if (key == "lastorgan") {
lastOrgan_ = juce::File(val);
} else if (key == "favourite") {
Expand Down Expand Up @@ -1292,6 +1304,32 @@ void MasterpieceProcessor::setReopenLastOrgan(bool on) {
writeGlobalFile();
}

juce::File MasterpieceProcessor::defaultCacheDirectory() {
return juce::File::getSpecialLocation(juce::File::userApplicationDataDirectory)
.getChildFile("Masterpiece")
.getChildFile("cache");
}

juce::File MasterpieceProcessor::cacheDirectory() const {
// The folder the player chose, as long as it can be created: a cache on a
// drive that is not plugged in must not stop an organ from loading. It only
// means this load is not cached.
if (cacheDir_.getFullPathName().isNotEmpty()) {
cacheDir_.createDirectory();
if (cacheDir_.isDirectory()) return cacheDir_;
}
return defaultCacheDirectory();
}

void MasterpieceProcessor::setCacheDirectory(const juce::File& dir) {
if (dir == cacheDir_) return;
cacheDir_ = dir;
samples_.setCacheDir(cacheDirectory().getFullPathName().toStdString());
// Written at once, like the other general preferences: where the cache
// lives is a property of the machine, not of the organ that is open.
writeGlobalFile();
}

void MasterpieceProcessor::setLoadTicks(bool on) {
if (loadTicks_.load(std::memory_order_acquire) == on) return;
loadTicks_.store(on, std::memory_order_release);
Expand Down Expand Up @@ -2413,6 +2451,15 @@ MasterpieceProcessor::LoadResult MasterpieceProcessor::loadOrgan(
OdfLoader loader;
OdfLoader::Options opts;
opts.organRootDir = root.getFullPathName().toStdString();
// A folder the player named for this organ wins over anything derived from
// the definition's own path. Some layouts cannot be worked out from the
// path at all: a link followed on the way in can leave the definition in a
// tree that holds no packages, and only the player knows where they are.
if (organRootOverride_.isDirectory()) {
opts.organRootDir = organRootOverride_.getFullPathName().toStdString();
juce::Logger::writeToLog("load: organ root set by hand: " +
organRootOverride_.getFullPathName());
}

OrganModel loaded;
if (!loader.load(odfFile.getFullPathName().toStdString(), opts, loaded,
Expand Down Expand Up @@ -2738,12 +2785,7 @@ MasterpieceProcessor::LoadResult MasterpieceProcessor::loadOrgan(
// What the cache is keyed to: which organ, and whether its definition has
// changed since the cache was written. Both are cheap to read and neither
// is guessable from the model alone.
samples_.setCacheDir(
juce::File::getSpecialLocation(juce::File::userApplicationDataDirectory)
.getChildFile("Masterpiece")
.getChildFile("cache")
.getFullPathName()
.toStdString());
samples_.setCacheDir(cacheDirectory().getFullPathName().toStdString());
samples_.setCacheIdentity(
organKey(),
odfFile.getFullPathName().toStdString() + "|" +
Expand Down
18 changes: 18 additions & 0 deletions src/mp_audio/MasterpieceProcessor.h
Original file line number Diff line number Diff line change
Expand Up @@ -477,6 +477,15 @@ class MasterpieceProcessor : public juce::AudioProcessor {
// Audible load progress: a swift tap at each 10% of a load. Off unless
// asked. Global, never per organ: it suits the room, not the instrument.
bool loadTicks() const { return loadTicks_.load(std::memory_order_acquire); }

// Where the sample cache is written. A cache is as large as the organs
// played through it, so a machine with a small fast disk and a large slow
// one has to be told which to use. An empty file means the default place,
// beside the other settings; setting one saves the choice at once.
juce::File cacheDirectory() const;
void setCacheDirectory(const juce::File& dir);
static juce::File defaultCacheDirectory();
juce::File cacheDirectorySetting() const { return cacheDir_; }
void setLoadTicks(bool on);
// Session-only form of the above: flips the switch without writing the
// global file. Headless tools use this so a measurement render never
Expand Down Expand Up @@ -666,6 +675,13 @@ class MasterpieceProcessor : public juce::AudioProcessor {
// out of the same installation packages the audio comes from.
const std::string& organRootDir() const { return organRootDir_; }

// Where this organ's OrganInstallationPackages lives, when the definition's
// own path does not lead there -- a folder linked in from another tree, for
// instance. Empty means work it out from the path, which is the usual case.
// Set before loading; saved with the organ's other settings.
juce::File organRootOverride() const { return organRootOverride_; }
void setOrganRootOverride(const juce::File& dir) { organRootOverride_ = dir; }

// Where the engine gets sample audio. Injected rather than owned, so the
// preloaded and streaming backing stores share one voice path (ADR-004) and
// tests can hand it a synthesised tone.
Expand Down Expand Up @@ -839,6 +855,7 @@ class MasterpieceProcessor : public juce::AudioProcessor {
SwitchNetwork switches_;
std::unordered_set<Id> engagedSwitches_;
std::string organRootDir_;
juce::File organRootOverride_;
// A drawstop on the console IS a switch; clicking it must draw the stop, not
// merely animate the picture. Built at load so the audio thread never
// searches for it.
Expand Down Expand Up @@ -894,6 +911,7 @@ class MasterpieceProcessor : public juce::AudioProcessor {
int64_t preloadHead_ = 0;
bool reopenLastOrgan_ = true;
std::atomic<bool> loadTicks_{false};
juce::File cacheDir_; // empty: the default place
// Next 10% threshold to tap at, 10 through 100. Reset by whoever starts a
// load and advanced by the audio thread, so both sides use an atomic and
// neither waits on the other.
Expand Down
36 changes: 26 additions & 10 deletions src/mp_core/OdfLoader.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,7 @@
#include <functional>
#include <cstdlib>
#include <filesystem>
#include <vector>
#include <fstream>
#include <sstream>
#include <unordered_set>
Expand Down Expand Up @@ -1877,19 +1878,34 @@ std::string deriveOrganRoot(const std::string& odfPath) {
const std::filesystem::path logicalRoot = organRootFrom(odf);
if (hasInstallationPackages(logicalRoot)) return logicalRoot.string();

// The logical root has no OrganInstallationPackages sibling -- try again
// with the ODF's own symlinks resolved. A set with OrganDefinitions moved
// onto another drive and linked back in can hand back a path whose plain
// textual parent is no longer where OrganInstallationPackages lives; asking
// the filesystem what the path actually resolves to finds it again. This
// never applies to a set with no installation packages at all: if the
// canonical root does not have one either, the logical answer is kept.
// The logical root has no OrganInstallationPackages sibling. A set whose
// folders are linked in from elsewhere can hand back a path whose textual
// parent is not where the packages live, so the search widens:
//
// * the same path with its symlinks resolved -- OrganDefinitions moved to
// another drive and linked back in resolves to where it really is;
// * the ancestors of both, because a link can land the definition several
// levels below the folder that holds the packages.
//
// A set that genuinely has no packages keeps the logical answer, so nothing
// about a loose ODF changes.
std::error_code ec;
std::vector<std::filesystem::path> starts{logicalRoot};
const std::filesystem::path canonicalOdf =
std::filesystem::weakly_canonical(odf, ec);
if (!ec && canonicalOdf != odf) {
const std::filesystem::path canonicalRoot = organRootFrom(canonicalOdf);
if (hasInstallationPackages(canonicalRoot)) return canonicalRoot.string();
if (!ec && canonicalOdf != odf) starts.push_back(organRootFrom(canonicalOdf));

// Four levels is past any layout we have seen and stops well short of a
// drive's root, where a stray folder of that name would be someone else's.
constexpr int kMaxAncestors = 4;
for (const auto& start : starts) {
std::filesystem::path dir = start;
for (int up = 0; up <= kMaxAncestors; ++up) {
if (hasInstallationPackages(dir)) return dir.string();
const std::filesystem::path parent = dir.parent_path();
if (parent.empty() || parent == dir) break;
dir = parent;
}
}
return logicalRoot.string();
}
Expand Down
Loading
Loading