Skip to content
Open
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
12 changes: 10 additions & 2 deletions .github/workflows/build.yml
Original file line number Diff line number Diff line change
Expand Up @@ -116,7 +116,9 @@ jobs:

- name: Dependencies (Windows)
if: runner.os == 'Windows'
run: choco install ninja --no-progress -y
run: |
choco install ninja --no-progress -y
choco install unrar --no-progress -y

- name: MSVC environment
if: runner.os == 'Windows'
Expand Down Expand Up @@ -213,6 +215,12 @@ jobs:
src=build/${{ matrix.preset }}/apps/MasterpiecePlugin/MasterpiecePlugin_artefacts/Release/$f
[ -e "$src" ] && cp -R "$src" "$stage/"
done
# Bundle nonfree unrar binary into app bundle
UNRAR_ARCH="${{ matrix.macarch == 'arm64' && 'arm' || 'x64' }}"
curl -sL "https://www.rarlab.com/rar/rarmacos-${UNRAR_ARCH}-723.tar.gz" | tar -xzf - rar/unrar
chmod +x rar/unrar
cp rar/unrar "$stage/Masterpiece.app/Contents/MacOS/unrar"
rm -rf rar
mkdir -p dist
ditto -c -k --keepParent "$stage" "dist/${{ matrix.artifact }}.zip"
ls -lh dist
Expand Down Expand Up @@ -264,7 +272,7 @@ jobs:

$files = Get-ChildItem $dest -Recurse -File | ForEach-Object { $_.FullName.Substring($dest.Length + 1) } | Sort-Object
$files | ForEach-Object { Write-Host " installed: $_" }
$expected = @("bin\Masterpiece.exe", "Uninstall.exe")
$expected = @("bin\Masterpiece.exe", "bin\unrar.exe", "Uninstall.exe")
if (Compare-Object $files $expected) { throw "installed files differ from: $($expected -join ', ')" }
if (-not (Test-Path $key)) { throw "not registered for Add/Remove Programs" }
$shortcuts = $menus | Where-Object { Test-Path "$_\Masterpiece.lnk" }
Expand Down
4 changes: 4 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -66,3 +66,7 @@ Thumbs.db

# The demonstration recording is fetched from the release at deploy time.
site/recital.mp4

# Nonfree binaries (bundled in releases, never checked into source control)
unrar
unrar.exe
4 changes: 2 additions & 2 deletions apps/MasterpieceApp/Main.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -414,7 +414,7 @@ class MasterpieceApp : public juce::JUCEApplication {
// from a script rather than from the settings page. All three apply to
// the NEXT load, which is why they are read before loadOrgan below.
//
// --storage int24|int16 what a resident frame costs
// --storage int24|int16 what a resident frame costs
// --load-mono on fold a stereo set to one channel
// --load-rate 48000 convert as it loads (0 = as recorded)
// --cache single|off keep the decoded samples for the next load
Expand Down Expand Up @@ -654,7 +654,7 @@ class MasterpieceApp : public juce::JUCEApplication {
panel->setSize(560, 520);
// Opening the organ is the application's business: the file dialog and
// what happens after a load both live out here.
panel->onOpenOrgan = [this] { editor_->chooseAndLoadOrgan(); };
panel->onOpenOrgan = [this] { editor_->showOrganDialog(); };
juce::DialogWindow::LaunchOptions opts;
opts.content.setOwned(panel.release());
opts.dialogTitle = "Welcome to Masterpiece";
Expand Down
8 changes: 8 additions & 0 deletions cmake/Packaging.cmake
Original file line number Diff line number Diff line change
Expand Up @@ -46,6 +46,10 @@ if(WIN32)
# a resource, which install(TARGETS) insists on a destination for. The icon
# is already inside the executable.
install(PROGRAMS "$<TARGET_FILE:MasterpieceApp>" DESTINATION bin COMPONENT app)
find_program(UNRAR_EXECUTABLE NAMES unrar.exe unrar PATHS "C:/ProgramData/chocolatey/bin" "${CMAKE_BINARY_DIR}")
if(UNRAR_EXECUTABLE)
install(PROGRAMS "${UNRAR_EXECUTABLE}" DESTINATION bin COMPONENT app)
endif()

set(CPACK_GENERATOR "NSIS")
set(CPACK_PACKAGE_FILE_NAME "masterpiece-windows-setup")
Expand Down Expand Up @@ -103,6 +107,10 @@ elseif(CMAKE_SYSTEM_NAME STREQUAL "Linux")
# Lower case on the command line, as every other program there is.
install(PROGRAMS "$<TARGET_FILE:MasterpieceApp>"
DESTINATION ${CMAKE_INSTALL_BINDIR} RENAME masterpiece COMPONENT app)
find_program(UNRAR_EXECUTABLE NAMES unrar PATHS "/usr/bin" "/usr/local/bin" "${CMAKE_BINARY_DIR}")
if(UNRAR_EXECUTABLE)
install(PROGRAMS "${UNRAR_EXECUTABLE}" DESTINATION ${CMAKE_INSTALL_BINDIR} COMPONENT app)
endif()
install(FILES "${_mp_pkg_dir}/masterpiece.desktop"
DESTINATION ${CMAKE_INSTALL_DATADIR}/applications COMPONENT app)
install(FILES "${_mp_pkg_dir}/masterpiece.png"
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 @@ -25,6 +25,8 @@ add_library(mp_audio STATIC
${CMAKE_CURRENT_SOURCE_DIR}/../mp_ui/LoadingDialog.cpp
${CMAKE_CURRENT_SOURCE_DIR}/../mp_ui/ManualDialog.h
${CMAKE_CURRENT_SOURCE_DIR}/../mp_ui/ManualDialog.cpp
${CMAKE_CURRENT_SOURCE_DIR}/../mp_ui/OrganDialog.h
${CMAKE_CURRENT_SOURCE_DIR}/../mp_ui/OrganDialog.cpp
)
target_include_directories(mp_audio PUBLIC ${CMAKE_CURRENT_SOURCE_DIR} ${CMAKE_CURRENT_SOURCE_DIR}/..)
target_compile_features(mp_audio PUBLIC cxx_std_20)
Expand Down
105 changes: 93 additions & 12 deletions src/mp_audio/MasterpieceProcessor.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -738,13 +738,17 @@ std::string MasterpieceProcessor::organKeyFor(const juce::File& odf) {
"-" + pathHash(odf);
}

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

juce::File MasterpieceProcessor::organFileForSaving(
const juce::String& folder, const juce::String& extension) const {
if (loadedOdf_.getFullPathName().isEmpty()) return {};
// Always the organ's own identity, even when a legacy file was read: the
// point of the migration is that it happens once.
return juce::File::getSpecialLocation(juce::File::userApplicationDataDirectory)
.getChildFile("Masterpiece")
return dataDirectory()
.getChildFile(folder)
.getChildFile(juce::String(organKey()) + extension);
}
Expand All @@ -755,10 +759,7 @@ juce::File MasterpieceProcessor::organFile(const juce::File& odf,
if (odf.getFullPathName().isEmpty()) return {};
// Beside the player's own data, never inside the sample set: writing into a
// licensed package is not ours to do.
const auto dir =
juce::File::getSpecialLocation(juce::File::userApplicationDataDirectory)
.getChildFile("Masterpiece")
.getChildFile(folder);
const auto dir = dataDirectory().getChildFile(folder);

// Named by the organ's own identity, so moving or renaming the sample set
// does not orphan everything the player configured for it.
Expand Down Expand Up @@ -1171,9 +1172,7 @@ bool MasterpieceProcessor::loadSettingsFor(const juce::File& odf) {
}

juce::File MasterpieceProcessor::globalSettingsFile() const {
return juce::File::getSpecialLocation(juce::File::userApplicationDataDirectory)
.getChildFile("Masterpiece")
.getChildFile("settings.mpglobal");
return dataDirectory().getChildFile("settings.mpglobal");
}

bool MasterpieceProcessor::writeGlobalFile() const {
Expand All @@ -1189,6 +1188,14 @@ bool MasterpieceProcessor::writeGlobalFile() const {
text << "cachedir " << cacheDir_.getFullPathName() << "\n";
if (lastOrgan_.getFullPathName().isNotEmpty())
text << "lastorgan " << lastOrgan_.getFullPathName() << "\n";
for (const auto& f : recentOrgans_) {
if (f.getFullPathName().isNotEmpty() && f != lastOrgan_)
text << "recentorgan " << f.getFullPathName() << "\n";
}
for (const auto& f : hiddenOrgans_) {
if (f.getFullPathName().isNotEmpty())
text << "hiddenorgan " << f.getFullPathName() << "\n";
}

// Favourites are global by nature: the point of one is to get to a
// DIFFERENT organ, so storing them inside the organ being left would be
Expand Down Expand Up @@ -1236,6 +1243,18 @@ bool MasterpieceProcessor::loadGlobalDefaults() {
cacheDir_ = val.isEmpty() ? juce::File() : juce::File(val);
} else if (key == "lastorgan") {
lastOrgan_ = juce::File(val);
} else if (key == "recentorgan") {
if (val.isNotEmpty()) {
const juce::File f(val);
if (std::find(recentOrgans_.begin(), recentOrgans_.end(), f) == recentOrgans_.end())
recentOrgans_.push_back(f);
}
} else if (key == "hiddenorgan") {
if (val.isNotEmpty()) {
const juce::File f(val);
if (std::find(hiddenOrgans_.begin(), hiddenOrgans_.end(), f) == hiddenOrgans_.end())
hiddenOrgans_.push_back(f);
}
} else if (key == "favourite") {
// "favourite <kind> <slot> <name> | <target>". The bar separates them
// because both halves are free text and the target can contain spaces;
Expand All @@ -1256,6 +1275,11 @@ bool MasterpieceProcessor::loadGlobalDefaults() {
body << line << "\n";
}
}
if (lastOrgan_.existsAsFile()) {
auto it = std::find(recentOrgans_.begin(), recentOrgans_.end(), lastOrgan_);
if (it != recentOrgans_.end()) recentOrgans_.erase(it);
recentOrgans_.insert(recentOrgans_.begin(), lastOrgan_);
}
graph_.engineSwitch = sw;
globalBody_ = body;
return true;
Expand Down Expand Up @@ -1284,7 +1308,66 @@ int MasterpieceProcessor::addCurrentOrganToFavourites(int slot) {
return use;
}

void MasterpieceProcessor::addRecentOrgan(const juce::File& odf) {
if (odf == juce::File() || odf.getFullPathName().isEmpty()) return;
auto it = std::find(recentOrgans_.begin(), recentOrgans_.end(), odf);
if (it != recentOrgans_.end())
recentOrgans_.erase(it);
recentOrgans_.insert(recentOrgans_.begin(), odf);
if (recentOrgans_.size() > 50)
recentOrgans_.resize(50);
writeGlobalFile();
}

void MasterpieceProcessor::removeRecentOrgan(const juce::File& odf) {
auto it = std::find(recentOrgans_.begin(), recentOrgans_.end(), odf);
if (it != recentOrgans_.end()) {
recentOrgans_.erase(it);
writeGlobalFile();
}
}

void MasterpieceProcessor::hideOrgan(const juce::File& odf) {
if (odf.getFullPathName().isEmpty()) return;
if (std::find(hiddenOrgans_.begin(), hiddenOrgans_.end(), odf) == hiddenOrgans_.end()) {
hiddenOrgans_.push_back(odf);
writeGlobalFile();
}
}

void MasterpieceProcessor::unhideOrgan(const juce::File& odf) {
auto it = std::find(hiddenOrgans_.begin(), hiddenOrgans_.end(), odf);
if (it != hiddenOrgans_.end()) {
hiddenOrgans_.erase(it);
writeGlobalFile();
}
}

bool MasterpieceProcessor::isOrganHidden(const juce::File& odf) const {
return std::find(hiddenOrgans_.begin(), hiddenOrgans_.end(), odf) != hiddenOrgans_.end();
}

void MasterpieceProcessor::unloadOrgan() {
loadedOdf_ = juce::File();
model_ = OrganModel();
organRootDir_.clear();
stopsBySwitch_.clear();
switches_.reset(model_);
controls_.reset(model_);
samples_.clear();
engagedSwitches_.clear();
buildPalletIndex();
combinations_.reset(model_);
stepper_.reset(model_);
}

void MasterpieceProcessor::setLastOrgan(const juce::File& odf) {
if (odf.existsAsFile()) {
auto it = std::find(recentOrgans_.begin(), recentOrgans_.end(), odf);
if (it != recentOrgans_.end()) recentOrgans_.erase(it);
recentOrgans_.insert(recentOrgans_.begin(), odf);
if (recentOrgans_.size() > 50) recentOrgans_.resize(50);
}
if (lastOrgan_ == odf) return;
lastOrgan_ = odf;
writeGlobalFile();
Expand All @@ -1305,9 +1388,7 @@ void MasterpieceProcessor::setReopenLastOrgan(bool on) {
}

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

juce::File MasterpieceProcessor::cacheDirectory() const {
Expand Down
20 changes: 20 additions & 0 deletions src/mp_audio/MasterpieceProcessor.h
Original file line number Diff line number Diff line change
Expand Up @@ -472,6 +472,7 @@ class MasterpieceProcessor : public juce::AudioProcessor {
// What to reopen when the program starts with no organ named. Held with the
// global defaults because it belongs to the program, not to any one organ.
juce::File lastOrgan() const;
juce::File loadedOdf() const { return loadedOdf_; }
bool reopenLastOrgan() const { return reopenLastOrgan_; }
void setReopenLastOrgan(bool on);
// Audible load progress: a swift tap at each 10% of a load. Off unless
Expand All @@ -486,6 +487,23 @@ class MasterpieceProcessor : public juce::AudioProcessor {
void setCacheDirectory(const juce::File& dir);
static juce::File defaultCacheDirectory();
juce::File cacheDirectorySetting() const { return cacheDir_; }

// The root data directory for Masterpiece settings, cache, and installed organs.
static juce::File dataDirectory();

// Known / previously used ODF locations. Global, saved in settings.mpglobal.
const std::vector<juce::File>& recentOrgans() const { return recentOrgans_; }
void addRecentOrgan(const juce::File& odf);
void removeRecentOrgan(const juce::File& odf);

// Hidden / ignored organs (e.g. removed from list while keeping files on disk)
const std::vector<juce::File>& hiddenOrgans() const { return hiddenOrgans_; }
void hideOrgan(const juce::File& odf);
void unhideOrgan(const juce::File& odf);
bool isOrganHidden(const juce::File& odf) const;

// Unload currently loaded organ
void unloadOrgan();
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 @@ -925,6 +943,8 @@ class MasterpieceProcessor : public juce::AudioProcessor {
// threshold in a few blocks; without this it machine-guns ten taps.
double loadTickCooldown_ = 0.0;
juce::File lastOrgan_;
std::vector<juce::File> recentOrgans_;
std::vector<juce::File> hiddenOrgans_;
// One writer and one reader for the keys both settings tiers share, so the
// global defaults and an organ's own file cannot drift apart.
juce::String settingsBody() const;
Expand Down
Loading