From b9c95906282b69bf673d36f6f3bda38786e6a2bb Mon Sep 17 00:00:00 2001 From: DiyunZ Date: Thu, 27 Aug 2026 17:00:00 -0500 Subject: [PATCH 1/3] Improve CMOD error reporting for issue #102 Report typed project, output, and internal failures with actionable context. Check XML/configuration, evaluator references and indices, child counts, and output failures instead of silently continuing or crashing. Refs cmp-illinois/DISSCO#102. Verified full Windows build, 36/36 CTest tests, 31 pre-fix failure comparisons, and unchanged comprehensive audio/score source/particel output. --- CMOD/CMakeLists.txt | 56 ++++++ CMOD/ERROR_REPORTING.md | 47 +++++ CMOD/src/CmodError.h | 42 ++++ CMOD/src/Event.cpp | 28 ++- CMOD/src/Main.cpp | 42 ++-- CMOD/src/NotationScore.cpp | 7 + CMOD/src/Piece-experimental.cpp | 178 +++++++++++------ CMOD/src/Piece.h | 4 - CMOD/src/Utilities.cpp | 181 +++++++++++++----- CMOD/tests/ErrorReportingFormatTest.cpp | 27 +++ CMOD/tests/ErrorReportingTest.cmake | 136 +++++++++++++ CMOD/tests/FunctionErrorTest.cmake | 119 ++++++++++++ CMOD/tests/fixtures/ErrorReporting.dissco | 64 +++++++ .../tests/fixtures/ScoreErrorReporting.dissco | 172 +++++++++++++++++ 14 files changed, 971 insertions(+), 132 deletions(-) create mode 100644 CMOD/ERROR_REPORTING.md create mode 100644 CMOD/src/CmodError.h create mode 100644 CMOD/tests/ErrorReportingFormatTest.cpp create mode 100644 CMOD/tests/ErrorReportingTest.cmake create mode 100644 CMOD/tests/FunctionErrorTest.cmake create mode 100644 CMOD/tests/fixtures/ErrorReporting.dissco create mode 100644 CMOD/tests/fixtures/ScoreErrorReporting.dissco diff --git a/CMOD/CMakeLists.txt b/CMOD/CMakeLists.txt index 5d5d66bf..43670de8 100644 --- a/CMOD/CMakeLists.txt +++ b/CMOD/CMakeLists.txt @@ -99,4 +99,60 @@ if(WIN32 AND SNDFILE_DLL) ) endif() +if(BUILD_TESTING) + add_executable(CMOD_ErrorReportingFormatTest tests/ErrorReportingFormatTest.cpp) + target_include_directories(CMOD_ErrorReportingFormatTest PRIVATE "${CMAKE_CURRENT_SOURCE_DIR}/src") + add_test(NAME CMOD.ErrorReporting.Format COMMAND CMOD_ErrorReportingFormatTest) + + foreach(error_case IN ITEMS + no_arguments missing_project malformed_xml invalid_root missing_configuration + missing_field invalid_integer zero_threads invalid_boolean + children_without_layer negative_children negative_fractional_children + negative_duration empty_score + output_directory valid valid_synthesis) + add_test(NAME CMOD.ErrorReporting.${error_case} + COMMAND ${CMAKE_COMMAND} + -DCMOD_EXECUTABLE=$ + -DPROJECT_FIXTURE=${CMAKE_CURRENT_SOURCE_DIR}/tests/fixtures/ErrorReporting.dissco + -DTEST_WORK_DIR=${CMAKE_CURRENT_BINARY_DIR}/error-reporting/${error_case} + -DTEST_CASE=${error_case} + -P ${CMAKE_CURRENT_SOURCE_DIR}/tests/ErrorReportingTest.cmake + ) + set_tests_properties(CMOD.ErrorReporting.${error_case} PROPERTIES TIMEOUT 20) + endforeach() + + foreach(error_case IN ITEMS + project_expression event_expression nested_expression missing_top + missing_object unknown_function select_negative select_object + null_context null_static_context nonfinite_expression + missing_markov_library invalid_markov_count temporary_library_write) + add_test(NAME CMOD.ErrorReporting.${error_case} + COMMAND ${CMAKE_COMMAND} + -DCMOD_EXECUTABLE=$ + -DPROJECT_FIXTURE=${CMAKE_CURRENT_SOURCE_DIR}/tests/fixtures/ErrorReporting.dissco + -DTEST_WORK_DIR=${CMAKE_CURRENT_BINARY_DIR}/function-errors + -DTEST_CASE=${error_case} + -P ${CMAKE_CURRENT_SOURCE_DIR}/tests/FunctionErrorTest.cmake + ) + set_tests_properties(CMOD.ErrorReporting.${error_case} PROPERTIES TIMEOUT 20) + endforeach() + + set(score_cases lilypond score_file) + find_program(LILYPOND_EXECUTABLE lilypond) + if(LILYPOND_EXECUTABLE) + list(APPEND score_cases valid_score) + endif() + foreach(error_case IN LISTS score_cases) + add_test(NAME CMOD.ErrorReporting.${error_case} + COMMAND ${CMAKE_COMMAND} + -DCMOD_EXECUTABLE=$ + -DPROJECT_FIXTURE=${CMAKE_CURRENT_SOURCE_DIR}/tests/fixtures/ScoreErrorReporting.dissco + -DTEST_WORK_DIR=${CMAKE_CURRENT_BINARY_DIR}/error-reporting/${error_case} + -DTEST_CASE=${error_case} + -P ${CMAKE_CURRENT_SOURCE_DIR}/tests/ErrorReportingTest.cmake + ) + set_tests_properties(CMOD.ErrorReporting.${error_case} PROPERTIES TIMEOUT 20) + endforeach() +endif() + message(STATUS "DONE!") diff --git a/CMOD/ERROR_REPORTING.md b/CMOD/ERROR_REPORTING.md new file mode 100644 index 00000000..ce56a120 --- /dev/null +++ b/CMOD/ERROR_REPORTING.md @@ -0,0 +1,47 @@ +# CMOD error reporting + +CMOD writes diagnostics to standard error. LASSIE's Process Output window +already displays that stream and marks failed processes in red. + +| Exit code | Meaning | +| --- | --- | +| 0 | The requested build completed. | +| 1 | A diagnosed project-input or output failure. | +| 2 | An unexpected C++ exception or a diagnosed internal error. | + +For example, a missing configuration field produces: + +```text +CMOD project error: A required project setting is missing. +Project: Example.dissco +Context: ProjectConfiguration.NumberOfChannels +Suggestion: Restore this setting in Project Properties, then save the project in LASSIE. +Build failed. +``` + +Project diagnostics cover unreadable or malformed project XML, missing or invalid +configuration, invalid numeric expressions and nested functions, missing object +references, invalid Select indices, functions used without an event context, +invalid child counts, and empty score staffs. Output diagnostics cover directory +creation, temporary library files, audio/score writes, and LilyPond failures. +Unexpected C++ exceptions ask the user to send developers the project, seed, +and diagnostic. Hard process faults and remaining legacy direct-exit paths +retain their existing handling; they are not made recoverable by these exceptions. + +## Adding a diagnostic + +Throw `CmodError` with a category, a specific reason, input context, and a +corrective action. Add outer context while rethrowing at a boundary that knows +the project field or expression. Do not discard an underlying parser's reason, +or classify an arbitrary `std::exception` as a user-input error. + +`Main.cpp` reports the exception once and returns a nonzero exit code. A failed +run must not reach its `Build complete.` message. + +## Tests + +With `BUILD_TESTING=ON`, run `ctest --test-dir -R CMOD.ErrorReporting +--output-on-failure`. The CLI tests use isolated fixture copies and bounded +stdin-driven subprocesses. The successful score-output test is registered when +LilyPond is available; missing-LilyPond and score-write failure tests do not +require it. diff --git a/CMOD/src/CmodError.h b/CMOD/src/CmodError.h new file mode 100644 index 00000000..3d0f95d2 --- /dev/null +++ b/CMOD/src/CmodError.h @@ -0,0 +1,42 @@ +#ifndef CMOD_ERROR_H +#define CMOD_ERROR_H + +#include +#include +#include + +// Expected failures retain the input context and a useful next step while +// unwinding to the command-line boundary. Unexpected exceptions are internal. +class CmodError : public std::runtime_error { +public: + enum class Kind { Project, Output, Internal }; + + CmodError(Kind kind, const std::string& message, + const std::string& context, const std::string& suggestion) + : std::runtime_error(message), kind_(kind), context_(context), + suggestion_(suggestion) {} + + void addContext(const std::string& context) { + context_ = context + (context_.empty() ? "" : " -> " + context_); + } + + int exitCode() const { return kind_ == Kind::Internal ? 2 : 1; } + + void report(std::ostream& output, const std::string& project) const { + const char* category = kind_ == Kind::Project ? "project" + : kind_ == Kind::Output ? "output" : "internal"; + output << "CMOD " << category << " error: " << what() << '\n' + << "Project: " << project << '\n'; + if (!context_.empty()) + output << "Context: " << context_ << '\n'; + output << "Suggestion: " << suggestion_ << '\n' + << "Build failed." << std::endl; + } + +private: + Kind kind_; + std::string context_; + std::string suggestion_; +}; + +#endif diff --git a/CMOD/src/Event.cpp b/CMOD/src/Event.cpp index 87c9e377..6f682808 100644 --- a/CMOD/src/Event.cpp +++ b/CMOD/src/Event.cpp @@ -29,6 +29,9 @@ Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA. #include "Sieve.h" #include "Random.h" #include "Bottom.h" +#include "CmodError.h" +#include +#include //----------------------------------------------------------------------------// //Checked @@ -143,14 +146,23 @@ Event::Event(pugi::xml_node _element, } + const auto checkedChildCount = [this](double value) { + if (!std::isfinite(value) || value < 0 || value > std::numeric_limits::max()) { + throw CmodError(CmodError::Kind::Project, + "The number of children is outside the supported range.", + "Event '" + name + "' -> NumberOfChildren: " + to_string(value), + "Use a finite value between 0 and " + to_string(std::numeric_limits::max()) + "."); + } + return static_cast(value); + }; pugi::xml_node flagElement = GFEC(numChildrenElement); if (XMLTC(flagElement) =="0"){ // Continuum pugi::xml_node entry1Element = GNES(flagElement); if (XMLTC(entry1Element)==""){ - numChildren = childTypeElements.size(); + numChildren = checkedChildCount(static_cast(childTypeElements.size())); } else { - numChildren =(int) utilities->evaluate(XMLTC(entry1Element), (void*)this); + numChildren = checkedChildCount(utilities->evaluate(XMLTC(entry1Element), (void*)this)); } } else if (XMLTC(flagElement) == "1"){ // Densitiy @@ -165,7 +177,7 @@ Event::Event(pugi::xml_node _element, // cout<<"density:"<< density<<", area:"< NumberOfChildren", + "Add child events to this event's Layers, or set Number of Children to zero."); + } + if (type <=3){ //top, high, mid, low thisEventElement = GNES(thisEventElement); diff --git a/CMOD/src/Main.cpp b/CMOD/src/Main.cpp index c18dfedd..ad1aa5f3 100644 --- a/CMOD/src/Main.cpp +++ b/CMOD/src/Main.cpp @@ -46,6 +46,7 @@ Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA. #include #include "Note.h" #include "SignalHandlers.h" +#include "CmodError.h" //added by Sever must be a more elegant way #include @@ -53,7 +54,7 @@ Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA. #include using namespace std; -int main(int parameterCount, char **parameterList) { +static int runCmod(int parameterCount, char **parameterList) { // Rubin Du 2024: Installed custom signal handler to print stack trace on segfault signal(SIGSEGV, segfaultHandler); @@ -69,14 +70,18 @@ int main(int parameterCount, char **parameterList) { if(parameterCount >= 2) path = parameterList[1]; if(path == "--help" || path == "-help" || path == "help") { - cout << "Usage: cmod Runs CMOD in the current directory." << endl; - cout << " cmod Runs CMOD in the directory." << endl; + cout << "Usage: cmod Builds the specified project." << endl; //cout << " cmod " << endl; //cout << " Renders a specific mask of sounds." << endl; cout << " cmod help Displays this help." << endl; return 0; } + if (path.empty()) { + throw CmodError(CmodError::Kind::Project, "No project file was specified.", + "Command line", "Run cmod ."); + } + const filesystem::path projectPath(path); filesystem::path workingDirectory = projectPath.parent_path(); if (workingDirectory.empty()) @@ -88,14 +93,8 @@ int main(int parameterCount, char **parameterList) { //Determine the project name. string projectName = projectPath.stem().string(); - //Determine project sound file output. - PieceHelper::createSoundFilesDirectory(workingPath); - PieceHelper::createScoreFilesDirectory(workingPath); - //Create the piece! - Piece* piece = new Piece(workingPath, projectName); - const bool buildSucceeded = piece->completedSuccessfully(); - delete piece; + Piece piece(workingPath, projectName); //delete outputFile; //Sever time_t endTime; @@ -108,6 +107,27 @@ int main(int parameterCount, char **parameterList) { printf("Computation Time: %02d:%02d:%02d.\n", hr, min, sec); - return buildSucceeded ? 0 : 1; + return 0; } + +int main(int parameterCount, char **parameterList) { + const char* project = parameterCount >= 2 ? parameterList[1] : "(not specified)"; + try { + return runCmod(parameterCount, parameterList); + } catch (const CmodError& error) { + error.report(cerr, project); + return error.exitCode(); + } catch (const std::exception& error) { + CmodError failure(CmodError::Kind::Internal, error.what(), "Building project", + "Report this problem to the DISSCO developers with the project, seed, and this diagnostic."); + failure.report(cerr, project); + return failure.exitCode(); + } catch (...) { + CmodError failure(CmodError::Kind::Internal, "An unexpected failure occurred.", + "Building project", + "Report this problem to the DISSCO developers with the project, seed, and this diagnostic."); + failure.report(cerr, project); + return failure.exitCode(); + } +} diff --git a/CMOD/src/NotationScore.cpp b/CMOD/src/NotationScore.cpp index e5d61eeb..eccba6e9 100644 --- a/CMOD/src/NotationScore.cpp +++ b/CMOD/src/NotationScore.cpp @@ -1,4 +1,5 @@ #include "NotationScore.h" +#include "CmodError.h" NotationScore::NotationScore() : score_title_("Score"), @@ -128,6 +129,12 @@ void NotationScore::InsertNote(Note* n) { void NotationScore::Build() { if (!is_built_) { for(int i=0 ; i::iterator iter = score_staff[i].begin(); diff --git a/CMOD/src/Piece-experimental.cpp b/CMOD/src/Piece-experimental.cpp index 2b613180..5a256312 100644 --- a/CMOD/src/Piece-experimental.cpp +++ b/CMOD/src/Piece-experimental.cpp @@ -32,9 +32,11 @@ Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA. #include "Output.h" #include "Random.h" #include "Utilities.h" +#include "CmodError.h" #include #include +#include #include #include #include @@ -75,9 +77,9 @@ static bool directoryExists(const string& path) { return stat(path.c_str(), &info) == 0 && (info.st_mode & S_IFDIR); } -static bool createDirectoryIfMissing(const string& path) { +static void createDirectoryIfMissing(const string& path) { if (directoryExists(path)) { - return true; + return; } #ifdef _WIN32 @@ -87,12 +89,12 @@ static bool createDirectoryIfMissing(const string& path) { #endif if (result == 0 || directoryExists(path)) { - return true; + return; } - cerr << "Error: could not create directory " << path - << " (" << strerror(errno) << ")" << endl; - return false; + throw CmodError(CmodError::Kind::Output, + string("Cannot create output directory: ") + strerror(errno), path, + "Check write permission and free disk space; a regular file must not occupy the output directory path."); } //----------------------------------------------------------------------------// @@ -162,9 +164,7 @@ void PieceHelper::createSoundFilesDirectory(string path) { void PieceHelper::createScoreFilesDirectory(string path) { string scoreDir = PieceHelper::getFixedPath(path) + "ScoreFiles"; - if (!createDirectoryIfMissing(scoreDir)) { - return; - } + createDirectoryIfMissing(scoreDir); vector files = vector(); getDirectoryList(scoreDir, files); @@ -242,60 +242,90 @@ void Piece::Print() { } +static string configurationValue(pugi::xml_node configuration, const char* field) { + pugi::xml_node element = configuration.child(field); + if (!element) { + throw CmodError(CmodError::Kind::Project, "A required project setting is missing.", + string("ProjectConfiguration.") + field, + "Restore this setting in Project Properties, then save the project in LASSIE."); + } + return XMLTC(element); +} + +static int positiveConfigurationInt(pugi::xml_node configuration, const char* field) { + const string text = configurationValue(configuration, field); + std::istringstream input(text); + int value = 0; + string extra; + if (!(input >> value) || (input >> extra) || value <= 0) { + throw CmodError(CmodError::Kind::Project, "Expected a positive integer, got '" + text + "'.", + string("ProjectConfiguration.") + field, + "Set this Project Properties value to a whole number greater than zero."); + } + return value; +} + +static bool configurationBool(pugi::xml_node configuration, const char* field) { + const string value = configurationValue(configuration, field); + if (value != "True" && value != "False") { + throw CmodError(CmodError::Kind::Project, "Expected True or False, got '" + value + "'.", + string("ProjectConfiguration.") + field, + "Set this option in Project Properties and save the project in LASSIE."); + } + return value == "True"; +} + Piece::Piece(string _workingPath, string _projectTitle){ path = _workingPath; projectName = _projectTitle; //Change working directory. - chdir(_workingPath.c_str()); + std::error_code directoryError; + std::filesystem::current_path(_workingPath, directoryError); + if (directoryError) { + throw CmodError(CmodError::Kind::Project, + "Cannot open project directory: " + directoryError.message(), + _workingPath, + "Check that the project directory exists and is accessible."); + } //Parse .dissco File pugi::xml_document disscoDoc; string disscoFile = _projectTitle+ ".dissco"; - disscoDoc.load_file(disscoFile.c_str()); + const pugi::xml_parse_result parseResult = disscoDoc.load_file(disscoFile.c_str()); + if (!parseResult) { + throw CmodError(CmodError::Kind::Project, + string("Cannot read project XML: ") + parseResult.description(), + disscoFile + " at byte " + to_string(parseResult.offset), + "Check that the .dissco file exists, is readable, and contains valid XML; reopen and save it in LASSIE."); + } pugi::xml_node root = disscoDoc.document_element(); - pugi::xml_node configurations = GFEC(root); - pugi::xml_node element = GFEC(configurations); - title = XMLTC(element); - element = GNES(element); - fileFlags = XMLTC(element); - element = GNES(element); - fileList = XMLTC(element); - element = GNES(element); - pieceStartTime = XMLTC(element); - element = GNES(element); - pieceDuration = XMLTC(element); - element = GNES(element); - soundSynthesis = (XMLTC(element).compare("True")==0)?true:false; - element = GNES(element); - scorePrinting = (XMLTC(element).compare("True")==0)?true:false; - element = GNES(element); - - // multistaffs - grandStaff = (XMLTC(element).compare("True")==0)?true:false; + if (string(root.name()) != "ProjectRoot" || !root.child("ProjectConfiguration")) { + throw CmodError(CmodError::Kind::Project, + "Expected ProjectRoot containing ProjectConfiguration.", + disscoFile, + "Open a DISSCO .dissco project and save it in LASSIE; other XML files are not projects."); + } + pugi::xml_node configurations = root.child("ProjectConfiguration"); + title = configurationValue(configurations, "Title"); + fileFlags = configurationValue(configurations, "FileFlag"); + fileList = configurationValue(configurations, "TopEvent"); + pieceStartTime = configurationValue(configurations, "PieceStartTime"); + pieceDuration = configurationValue(configurations, "Duration"); + soundSynthesis = configurationBool(configurations, "Synthesis"); + scorePrinting = configurationBool(configurations, "Score"); + grandStaff = configurationBool(configurations, "GrandStaff"); cout <<"grandStaff: " << grandStaff << endl; - element = GNES(element); - - // get the staffs number - - numberOfStaff = atoi(XMLTC(element).c_str()); + numberOfStaff = positiveConfigurationInt(configurations, "NumberOfStaff"); cout <<"numberOfStaff: " << numberOfStaff << endl; - element = GNES(element); - - numChannels = atoi(XMLTC(element).c_str()); + numChannels = positiveConfigurationInt(configurations, "NumberOfChannels"); cout << "Channel: " << numChannels << "\n"; - element = GNES(element); - - sampleRate = atoi(XMLTC(element).c_str()); + sampleRate = positiveConfigurationInt(configurations, "SampleRate"); cout << "Sample Rate: "<< sampleRate << "\n"; - element = GNES(element); - - sampleSize = atoi(XMLTC(element).c_str()); + sampleSize = positiveConfigurationInt(configurations, "SampleSize"); cout << "Sample Size: "<< sampleSize << "\n"; - element = GNES(element); - numThreads = atoi(XMLTC(element).c_str()); - element = GNES(element); - bool outputParticel = (XMLTC(element).compare("True")==0)?true:false; + numThreads = positiveConfigurationInt(configurations, "NumberOfThreads"); + bool outputParticel = configurationBool(configurations, "OutputParticel"); if (soundSynthesis) { PieceHelper::createSoundFilesDirectory(""); @@ -307,7 +337,7 @@ Piece::Piece(string _workingPath, string _projectTitle){ //check if seed exists string seed; - element = GNES(element); + pugi::xml_node element = configurations.child("Seed"); if(element.first_child()){ seed = XMLTC(element); } @@ -348,8 +378,22 @@ Piece::Piece(string _workingPath, string _projectTitle){ // setup TimeSpan and Tempo TimeSpan pieceSpan; - pieceSpan.start = utilities->evaluate(pieceStartTime, NULL); - pieceSpan.duration = utilities->evaluate(pieceDuration, NULL); + auto evaluateSetting = [this](const string& value, const char* field) { + try { + return utilities->evaluate(value, NULL); + } catch (CmodError& error) { + error.addContext(string("ProjectConfiguration.") + field); + throw; + } + }; + pieceSpan.start = evaluateSetting(pieceStartTime, "PieceStartTime"); + pieceSpan.duration = evaluateSetting(pieceDuration, "Duration"); + if (!std::isfinite(pieceSpan.duration) || pieceSpan.duration <= 0) { + throw CmodError(CmodError::Kind::Project, + "The piece duration must be finite and greater than zero.", + "ProjectConfiguration.Duration: " + pieceDuration, + "Set Duration to a positive number of seconds, or an expression that produces one."); + } Tempo mainTempo; //Though we supply this, "Top" will provide its own tempo. // multistaffs @@ -395,9 +439,13 @@ Piece::Piece(string _workingPath, string _projectTitle){ MultiTrack* renderedScore = utilities->doneCMOD(); string soundFilename = getNextSoundFile(); //Write to file. - if (!AuWriter::write(*renderedScore, soundFilename)) - buildSucceeded = false; + const bool written = AuWriter::write(*renderedScore, soundFilename); delete renderedScore; + if (!written) { + throw CmodError(CmodError::Kind::Output, "Could not write the rendered audio file.", + soundFilename, + "Check the SoundFiles directory, write permission, and free disk space."); + } } if (scorePrinting) { cout << "Piece::Piece: " << "Score output " << endl; @@ -412,17 +460,25 @@ Piece::Piece(string _workingPath, string _projectTitle){ string temp = projectName + ".ly"; const char* projectNameCstr = temp.c_str(); score_file.open(projectNameCstr); + if (!score_file) { + throw CmodError(CmodError::Kind::Output, "Could not create the score source file.", + temp, "Check write permission and make sure this path is not a directory."); + } score_file << Output::notation_score_; score_file.close(); + if (!score_file) { + throw CmodError(CmodError::Kind::Output, "Could not finish writing the score source file.", + temp, "Check write permission and free disk space, then run the project again."); + } // execute lilypond to create pdf file string lilypondCommand = "lilypond \"" + projectName + ".ly\""; int lilypondStatus = system(lilypondCommand.c_str()); if (lilypondStatus != 0) { - buildSucceeded = false; - cerr << "Error: LilyPond failed to generate " - << projectName << ".pdf" << endl; + throw CmodError(CmodError::Kind::Output, "LilyPond failed to generate the score PDF.", + projectName + ".ly (status " + to_string(lilypondStatus) + ")", + "Check that LilyPond is installed and on PATH, and review its diagnostic above for score errors."); } else { PieceHelper::createScoreFilesDirectory(""); @@ -443,10 +499,10 @@ Piece::Piece(string _workingPath, string _projectTitle){ string targetPdf = "ScoreFiles/" + projectName + suffix + ".pdf"; if (rename(sourcePdf.c_str(), targetPdf.c_str()) != 0) { - buildSucceeded = false; - cerr << "Error: could not move " << sourcePdf - << " to " << targetPdf - << " (" << strerror(errno) << ")" << endl; + throw CmodError(CmodError::Kind::Output, + string("Could not move the generated score PDF: ") + strerror(errno), + sourcePdf + " -> " + targetPdf, + "Check the ScoreFiles directory and write permission, and close any program locking the PDF."); } } @@ -461,7 +517,7 @@ Piece::Piece(string _workingPath, string _projectTitle){ cout << endl; cout << "-----------------------------------------------------------" << endl; - cout << (buildSucceeded ? "Build complete." : "Build failed.") << endl; + cout << "Build complete." << endl; cout << "-----------------------------------------------------------" << endl << endl; cout.flush(); diff --git a/CMOD/src/Piece.h b/CMOD/src/Piece.h index 3b1989d3..5fa92c7c 100644 --- a/CMOD/src/Piece.h +++ b/CMOD/src/Piece.h @@ -115,9 +115,6 @@ class Piece { **/ ~Piece(); - /// Returns false when any requested output could not be generated. - bool completedSuccessfully() const { return buildSucceeded; } - /** * Prints information about the piece. **/ @@ -173,7 +170,6 @@ class Piece { int sampleRate; int sampleSize; int numThreads; - bool buildSucceeded = true; }; diff --git a/CMOD/src/Utilities.cpp b/CMOD/src/Utilities.cpp index c97253d3..2fd2229d 100644 --- a/CMOD/src/Utilities.cpp +++ b/CMOD/src/Utilities.cpp @@ -30,11 +30,13 @@ Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA. // Maintained by Fanbo Xiang 2018 //----------------------------------------------------------------------------// #include "Utilities.h" +#include "CmodError.h" #include "Random.h" #include "Event.h" #include "Piece.h" #include "Patter.h" #include "ProbabilityEnvelope.h" // consider moving this into LASS.h +#include #include #include #include @@ -55,6 +57,15 @@ Utilities::Utilities(pugi::xml_node root, samplingRate(_samplingRate), piece(_piece){ + for (const char* section : {"EnvelopeLibrary", "MarkovModelLibrary", "Events"}) { + if (!root.child(section)) { + throw CmodError(CmodError::Kind::Project, + "Missing required project section '" + string(section) + "'.", + "ProjectRoot/" + string(section), + "Restore the missing section, or resave the original project in LASSIE."); + } + } + // New LASS Score if (soundSynthesis){ score = new Score (numThreads, numChannels, _samplingRate ); @@ -66,12 +77,24 @@ Utilities::Utilities(pugi::xml_node root, // Construct Envelope library - pugi::xml_node envelopeLibraryElement = GNES(GNES(GFEC(root))); + pugi::xml_node envelopeLibraryElement = root.child("EnvelopeLibrary"); string envLibContent = XMLTranscode(envelopeLibraryElement); string fileString = "lib.temp"; FILE* file = fopen(fileString.c_str(), "w"); - fputs (envLibContent.c_str(), file); - fclose(file); + if (file == NULL) { + throw CmodError(CmodError::Kind::Output, + "Cannot create the temporary envelope library file.", + "File: " + fileString, + "Check that the project folder is writable and that lib.temp is not a directory."); + } + const bool writeSucceeded = fputs(envLibContent.c_str(), file) >= 0; + const bool closeSucceeded = fclose(file) == 0; + if (!writeSucceeded || !closeSucceeded) { + throw CmodError(CmodError::Kind::Output, + "Cannot write the temporary envelope library file.", + "File: " + fileString, + "Check free disk space and write permissions for the project folder."); + } envelopeLibrary = new EnvelopeLibrary(); envelopeLibrary->loadLibraryNewFormat((char*)fileString.c_str()); @@ -79,16 +102,19 @@ Utilities::Utilities(pugi::xml_node root, std::filesystem::remove(fileString, removalError); // Construct Markov Model Library - pugi::xml_node markovModelLibraryElement = GNES(envelopeLibraryElement); - string tagName = markovModelLibraryElement.name(); - if (tagName != "MarkovModelLibrary") { - cout << "Project is outdated, please save the project in the latest version of DISSCO" << endl; - exit(1); - } + pugi::xml_node markovModelLibraryElement = root.child("MarkovModelLibrary"); string data = XMLTC(markovModelLibraryElement); std::stringstream ss(data); - int size; - ss >> size; + string countText; + ss >> countText; + std::istringstream countInput(countText); + int size = 0; + if (!(countInput >> size) || !countInput.eof() || size < 0) { + throw CmodError(CmodError::Kind::Project, + "MarkovModelLibrary model count must be a nonnegative integer.", + "MarkovModelLibrary: " + countText, + "Set the model count to the number of saved Markov models, or resave the project in LASSIE."); + } markovModelLibrary.resize(size); string modelText, line; getline(ss, line, '\n'); @@ -108,7 +134,7 @@ Utilities::Utilities(pugi::xml_node root, //events and other objects - pugi::xml_node eventElements = GNES(markovModelLibraryElement); + pugi::xml_node eventElements = root.child("Events"); pugi::xml_node thisEventElement = GFEC(eventElements); //Counters to assign numbers to the events. Experimental @@ -267,50 +293,52 @@ Utilities::~Utilities(){ //----------------------------------------------------------------------------// pugi::xml_node Utilities::getEventElement(EventType _type, string _eventName){ - map::iterator it; + const auto lookup = [_type, &_eventName]( + const map& elements) { + const auto it = elements.find(_eventName); + if (it == elements.end()) { + throw CmodError(CmodError::Kind::Project, + "Cannot find event or object '" + _eventName + + "' of type " + to_string(static_cast(_type)) + ".", + "Event/object reference", + "Check the referenced name and type in LASSIE, or restore the missing object."); + } + return it->second; + }; switch((int)_type){ case 0: - it = topEventElements.find(_eventName); - break; + return lookup(topEventElements); case 1: - it = highEventElements.find(_eventName); - break; + return lookup(highEventElements); case 2: - it = midEventElements.find(_eventName); - break; + return lookup(midEventElements); case 3: - it = lowEventElements.find(_eventName); - break; + return lookup(lowEventElements); case 4: - it = bottomEventElements.find(_eventName); - break; + return lookup(bottomEventElements); case 5: - it = spectrumElements.find(_eventName); - break; + return lookup(spectrumElements); case 6: - it = envelopeElements.find(_eventName); - break; + return lookup(envelopeElements); case 7: - it = sieveElements.find(_eventName); - break; + return lookup(sieveElements); case 8: - it = spatializationElements.find(_eventName); - break; + return lookup(spatializationElements); case 9: - it = patternElements.find(_eventName); - break; + return lookup(patternElements); case 10: - it = reverbElements.find(_eventName); - break; + return lookup(reverbElements); case 12: - it = notesElements.find(_eventName); - break; + return lookup(notesElements); case 13: - it = filterElements.find(_eventName); - break; + return lookup(filterElements); } - return it->second; + throw CmodError(CmodError::Kind::Project, + "Unsupported event/object type " + to_string(static_cast(_type)) + + " for '" + _eventName + "'.", + "Event/object reference", + "Use a supported event/object type and recreate the reference in LASSIE."); } @@ -353,6 +381,14 @@ Sieve* Utilities::evaluateSieve(std::string _input, void* _object){ double Utilities::evaluate(std::string _input, void* _object){ if (_input == "") return 0; + const auto errorContext = [this, _object, &_input]() { + string context = "Expression: " + _input; + if (_object != NULL && _object != piece) { + context = "Event '" + static_cast(_object)->getEventName() + + "' -> " + context; + } + return context; + }; string workingString = _input; // Test if there is any function in this string (look for ), if so, // replace the function with the evaluated number. Repeat until all the @@ -363,7 +399,13 @@ double Utilities::evaluate(std::string _input, void* _object){ size_t locOfEndFun = findTheEndOfFirstFunction (workingString); functionStringLength = ((int) locOfEndFun) + 6 - ((int) locOfFun);//6 is the length of "" string functionString = workingString.substr(locOfFun, functionStringLength); - string evaluatedFunction = evaluateFunction( functionString, _object); + string evaluatedFunction; + try { + evaluatedFunction = evaluateFunction(functionString, _object); + } catch (CmodError& error) { + error.addContext(errorContext()); + throw; + } string front = workingString.substr(0, locOfFun); string back = workingString.substr(((int)locOfEndFun) +6); workingString = front + evaluatedFunction + back; @@ -373,21 +415,25 @@ double Utilities::evaluate(std::string _input, void* _object){ } // evaluate the expression to the final result mu::Parser p; - p.SetExpr(workingString); double result; try { + p.SetExpr(workingString); result = p.Eval(); //cout << "utilities result: " << result << endl; - } catch (mu::ParserError) { - cerr << "Oooops, we find a typo in your project." << endl; - if (_object != NULL){ - cerr << "The string we see is located in Event (or Bottom): " << ((Event*)_object)->getEventName() << endl; - } - cerr << "The string we see is: " << _input << endl; - exit(1); + } catch (const mu::ParserError& error) { + throw CmodError(CmodError::Kind::Project, + "Cannot evaluate numeric expression: " + error.GetMsg(), + errorContext(), + "Check the expression's operators, parentheses, and function arguments in LASSIE."); } + if (!std::isfinite(result)) { + throw CmodError(CmodError::Kind::Project, + "Numeric expression produced a non-finite value.", + errorContext(), + "Check for division by zero and invalid function inputs; the result must be finite."); + } return result; } @@ -569,6 +615,20 @@ string Utilities::evaluateFunction(string _functionString,void* _object){ pugi::xml_node functionNameElement = GFEC(root); string functionName = functionNameElement.child_value(); + const bool needsEvent = functionName == "GetPattern" + || functionName == "CURRENT_TYPE" + || functionName == "CURRENT_CHILD_NUM" + || functionName == "CURRENT_PARTIAL_NUM" + || functionName == "AVAILABLE_EDU" + || functionName == "CURRENT_LAYER" + || functionName == "PREVIOUS_CHILD_DURATION"; + if (needsEvent && (_object == NULL || _object == piece)) { + throw CmodError(CmodError::Kind::Project, + "Function '" + functionName + "' requires an event context.", + "Function: " + _functionString, + "Use this function in an event, or replace it with a constant in Project Properties."); + } + // check the function name and call the proper method for evaluation if(functionName.compare("RandomInt")==0){ resultString = function_RandomInt(root, _object); @@ -652,6 +712,12 @@ string Utilities::evaluateFunction(string _functionString,void* _object){ else if (functionName.compare("PREVIOUS_CHILD_DURATION")==0){ resultString = static_function_PREVIOUS_CHILD_DURATION( _object); } + else { + throw CmodError(CmodError::Kind::Project, + "Unknown numeric function '" + functionName + "'.", + "Function: " + _functionString, + "Choose a supported numeric function in LASSIE and check its name."); + } return resultString; } @@ -713,7 +779,7 @@ string Utilities::static_function_CURRENT_CHILD_NUM(void* _object){ string Utilities::static_function_CURRENT_PARTIAL_NUM(void* _object){ if (_object !=NULL){ - double resultNum = ((Bottom*)_object)->getCurrPartialNum(); + double resultNum = static_cast(_object)->getCurrPartialNum(); char result [50]; sprintf(result, "%f", resultNum); return string(result); @@ -1204,6 +1270,17 @@ string Utilities::function_Random(pugi::xml_node _functionElement, void* _object //----------------------------------------------------------------------------// +static size_t checkedSelectIndex(double value, size_t listSize) { + if (!std::isfinite(value) || value < 0 || value >= listSize) { + throw CmodError(CmodError::Kind::Project, + "Select index " + to_string(value) + + " is outside the list of " + to_string(listSize) + " entries.", + "Function: Select", + "Use a nonnegative index smaller than the number of entries in the Select list."); + } + return static_cast(value); +} + string Utilities::function_Select(pugi::xml_node _functionElement, void* _object){ pugi::xml_node listElement = GNES(GFEC(_functionElement)); @@ -1211,7 +1288,7 @@ string Utilities::function_Select(pugi::xml_node _functionElement, void* _object std::vector list = listElementToStringVector(listElement); - int index = (int)evaluate(XMLTranscode(indexElement), _object); + const size_t index = checkedSelectIndex(evaluate(XMLTranscode(indexElement), _object), list.size()); char result [50]; /* for(int i=0; i list = listElementToStringVector(listElement); - int index = (int)evaluate(XMLTranscode(indexElement), _object); + const size_t index = checkedSelectIndex(evaluate(XMLTranscode(indexElement), _object), list.size()); return list[index]; } diff --git a/CMOD/tests/ErrorReportingFormatTest.cpp b/CMOD/tests/ErrorReportingFormatTest.cpp new file mode 100644 index 00000000..399d8516 --- /dev/null +++ b/CMOD/tests/ErrorReportingFormatTest.cpp @@ -0,0 +1,27 @@ +#include "CmodError.h" + +#include +#include + +static bool check(CmodError::Kind kind, const char* category, int exitCode) { + CmodError error(kind, "Failure reason", "Inner context", "Corrective action"); + error.addContext("Outer context"); + std::ostringstream output; + error.report(output, "Example.dissco"); + const std::string expected = std::string("CMOD ") + category + " error: Failure reason\n" + "Project: Example.dissco\n" + "Context: Outer context -> Inner context\n" + "Suggestion: Corrective action\n" + "Build failed.\n"; + if (output.str() != expected || error.exitCode() != exitCode) { + std::cerr << "Incorrect " << category << " diagnostic:\n" << output.str(); + return false; + } + return true; +} + +int main() { + return check(CmodError::Kind::Project, "project", 1) + && check(CmodError::Kind::Output, "output", 1) + && check(CmodError::Kind::Internal, "internal", 2) ? 0 : 1; +} diff --git a/CMOD/tests/ErrorReportingTest.cmake b/CMOD/tests/ErrorReportingTest.cmake new file mode 100644 index 00000000..a2101ef5 --- /dev/null +++ b/CMOD/tests/ErrorReportingTest.cmake @@ -0,0 +1,136 @@ +if(NOT DEFINED CMOD_EXECUTABLE OR NOT DEFINED TEST_WORK_DIR) + message(FATAL_ERROR "CMOD_EXECUTABLE and TEST_WORK_DIR are required") +endif() + +file(MAKE_DIRECTORY "${TEST_WORK_DIR}") +if(NOT DEFINED TEST_CASE) + set(TEST_CASE missing_project) +endif() +set(project "${TEST_WORK_DIR}/${TEST_CASE}.dissco") +set(expected "${TEST_CASE}.dissco") +set(expected_result 1) +set(expected_category "project") +set(command "${CMOD_EXECUTABLE}") +set(project_argument "${project}") +if(TEST_CASE STREQUAL "no_arguments") + set(project_argument) + set(expected "No project file was specified") +elseif(TEST_CASE STREQUAL "missing_project") + # Leave the project absent. +else() + file(READ "${PROJECT_FIXTURE}" xml) + if(TEST_CASE STREQUAL "valid" OR TEST_CASE STREQUAL "valid_synthesis" + OR TEST_CASE STREQUAL "valid_score") + set(expected_result 0) + if(TEST_CASE STREQUAL "valid_synthesis") + string(REPLACE "False" "True" xml "${xml}") + endif() + elseif(TEST_CASE STREQUAL "malformed_xml") + string(REPLACE "" "" xml "${xml}") + set(expected "byte") + elseif(TEST_CASE STREQUAL "invalid_root") + set(xml "") + set(expected "ProjectRoot") + elseif(TEST_CASE STREQUAL "missing_configuration") + set(xml "") + set(expected "ProjectConfiguration") + elseif(TEST_CASE STREQUAL "missing_field") + string(REPLACE " 2\n" "" xml "${xml}") + set(expected "NumberOfChannels") + elseif(TEST_CASE STREQUAL "invalid_integer") + string(REPLACE "44100" "44100oops" xml "${xml}") + set(expected "SampleRate") + elseif(TEST_CASE STREQUAL "zero_threads") + string(REPLACE "1" "0" xml "${xml}") + set(expected "NumberOfThreads") + elseif(TEST_CASE STREQUAL "invalid_boolean") + string(REPLACE "False" "Maybe" xml "${xml}") + set(expected "Synthesis") + elseif(TEST_CASE STREQUAL "output_directory") + string(REPLACE "False" "True" xml "${xml}") + file(WRITE "${TEST_WORK_DIR}/SoundFiles" "This regular file blocks the output directory.") + set(expected "SoundFiles") + set(expected_category "output") + elseif(TEST_CASE STREQUAL "children_without_layer") + string(REPLACE "0\n " + "1\n " xml "${xml}") + set(expected "NumberOfChildren") + elseif(TEST_CASE STREQUAL "negative_children") + string(REPLACE "0\n " + "-1\n " xml "${xml}") + set(expected "NumberOfChildren") + elseif(TEST_CASE STREQUAL "negative_fractional_children") + string(REPLACE "0\n " + "-0.5\n " xml "${xml}") + set(expected "NumberOfChildren") + elseif(TEST_CASE STREQUAL "negative_duration") + string(REPLACE "1" "-1" xml "${xml}") + set(expected "ProjectConfiguration.Duration") + elseif(TEST_CASE STREQUAL "empty_score") + string(REPLACE "False" "True" xml "${xml}") + set(expected "staff") + elseif(TEST_CASE STREQUAL "lilypond") + if(WIN32) + set(ENV{PATH} "$ENV{SystemRoot}/System32;$ENV{SystemRoot}") + else() + set(ENV{PATH} "") + endif() + set(expected "LilyPond") + set(expected_category "output") + elseif(TEST_CASE STREQUAL "score_file") + file(MAKE_DIRECTORY "${TEST_WORK_DIR}/score_file.ly") + set(expected "score_file.ly") + set(expected_category "output") + else() + message(FATAL_ERROR "Unknown test case: ${TEST_CASE}") + endif() + file(WRITE "${project}" "${xml}") +endif() +set(run_input "${TEST_WORK_DIR}/input.txt") +file(WRITE "${run_input}" "1\n") +file(GLOB previous_audio "${TEST_WORK_DIR}/SoundFiles/*.aiff") +list(LENGTH previous_audio previous_audio_count) +file(GLOB previous_scores "${TEST_WORK_DIR}/ScoreFiles/*.pdf") +list(LENGTH previous_scores previous_score_count) + +execute_process( + COMMAND ${command} ${project_argument} + INPUT_FILE "${run_input}" + WORKING_DIRECTORY "${TEST_WORK_DIR}" + RESULT_VARIABLE result + OUTPUT_VARIABLE stdout + ERROR_VARIABLE stderr + TIMEOUT 10 +) +file(WRITE "${TEST_WORK_DIR}/stdout.txt" "${stdout}") +file(WRITE "${TEST_WORK_DIR}/stderr.txt" "${stderr}") + +if(expected_result EQUAL 0) + if(NOT "${result}" STREQUAL "0" OR NOT stdout MATCHES "Build complete\\.") + message(FATAL_ERROR "Valid project failed (${result}).\n${stdout}\n${stderr}") + endif() + if(TEST_CASE STREQUAL "valid_synthesis") + file(GLOB audio "${TEST_WORK_DIR}/SoundFiles/*.aiff") + list(LENGTH audio audio_count) + math(EXPR expected_count "${previous_audio_count} + 1") + if(NOT audio_count EQUAL expected_count) + message(FATAL_ERROR "Successful synthesis did not produce one new audio file") + endif() + endif() + if(TEST_CASE STREQUAL "valid_score") + file(GLOB scores "${TEST_WORK_DIR}/ScoreFiles/*.pdf") + list(LENGTH scores score_count) + math(EXPR expected_count "${previous_score_count} + 1") + if(NOT score_count EQUAL expected_count) + message(FATAL_ERROR "Successful score output did not produce one new PDF") + endif() + endif() +elseif(NOT "${result}" STREQUAL "1" + OR NOT stderr MATCHES "CMOD ${expected_category} error:" + OR NOT stderr MATCHES "${expected}" + OR NOT stderr MATCHES "Suggestion:" + OR stdout MATCHES "Build complete\\.") + message(FATAL_ERROR + "${TEST_CASE} must produce an actionable ${expected_category} error (exit 1).\n" + "Actual exit: ${result}\nstdout:\n${stdout}\nstderr:\n${stderr}") +endif() diff --git a/CMOD/tests/FunctionErrorTest.cmake b/CMOD/tests/FunctionErrorTest.cmake new file mode 100644 index 00000000..3ba7c4de --- /dev/null +++ b/CMOD/tests/FunctionErrorTest.cmake @@ -0,0 +1,119 @@ +if(NOT DEFINED CMOD_EXECUTABLE OR NOT DEFINED PROJECT_FIXTURE + OR NOT DEFINED TEST_WORK_DIR OR NOT DEFINED TEST_CASE) + message(FATAL_ERROR "CMOD function test variables were not provided") +endif() + +file(READ "${PROJECT_FIXTURE}" project_xml) +set(expected_details "CMOD project error:" "Suggestion:" "Build failed.") +if(TEST_CASE STREQUAL "project_expression") + string(REPLACE "1" "1+" + project_xml "${project_xml}") + list(APPEND expected_details "1+" "Unexpected end of expression") +elseif(TEST_CASE STREQUAL "event_expression") + string(REPLACE "1" + "1+" project_xml "${project_xml}") + list(APPEND expected_details "Event '0'" "1+" "Unexpected end of expression") +elseif(TEST_CASE STREQUAL "nested_expression") + set(expression "2*(Random1+2)") + string(REPLACE "1" + "${expression}" + project_xml "${project_xml}") + list(APPEND expected_details "Event '0'" "${expression}" + "1+" "Unexpected end of expression") +elseif(TEST_CASE STREQUAL "missing_top") + string(REPLACE "0" "MissingTop" + project_xml "${project_xml}") + list(APPEND expected_details "MissingTop" "type 0") +elseif(TEST_CASE STREQUAL "missing_object") + set(expression "ChooseLReadSIVFileMissingSieve") + string(REPLACE "1" + "${expression}" + project_xml "${project_xml}") + list(APPEND expected_details "Event '0'" "MissingSieve" "type 7") +elseif(TEST_CASE STREQUAL "unknown_function") + set(expression "MissingFunction") + string(REPLACE "1" + "${expression}" + project_xml "${project_xml}") + list(APPEND expected_details "Event '0'" "Unknown numeric function" "MissingFunction") +elseif(TEST_CASE STREQUAL "select_negative") + set(expression "Select10-1") + string(REPLACE "1" + "${expression}" + project_xml "${project_xml}") + list(APPEND expected_details "Event '0'" "Select index" "outside" "-1") +elseif(TEST_CASE STREQUAL "select_object") + set(expression "ChooseLSelectReadSIVFileunused1") + string(REPLACE "1" + "${expression}" + project_xml "${project_xml}") + list(APPEND expected_details "Event '0'" "Select index" "outside") +elseif(TEST_CASE STREQUAL "null_context") + set(expression "GetPatternIN_ORDER0MakePattern1") + string(REPLACE "1" "${expression}" + project_xml "${project_xml}") + list(APPEND expected_details "GetPattern" "requires an event context" "${expression}") +elseif(TEST_CASE STREQUAL "null_static_context") + set(expression "CURRENT_CHILD_NUM") + string(REPLACE "1" "${expression}" + project_xml "${project_xml}") + list(APPEND expected_details "CURRENT_CHILD_NUM" "requires an event context") +elseif(TEST_CASE STREQUAL "nonfinite_expression") + string(REPLACE "1" + "1/0" project_xml "${project_xml}") + list(APPEND expected_details "Event '0'" "non-finite" "Expression: 1/0") +elseif(TEST_CASE STREQUAL "missing_markov_library") + string(REPLACE "0" "" + project_xml "${project_xml}") + list(APPEND expected_details "Missing required project section" "MarkovModelLibrary") +elseif(TEST_CASE STREQUAL "invalid_markov_count") + string(REPLACE "0" + "0oops" project_xml "${project_xml}") + list(APPEND expected_details "MarkovModelLibrary" "model count") +elseif(TEST_CASE STREQUAL "temporary_library_write") + set(expected_details "CMOD output error:" "lib.temp" "Suggestion:" "Build failed.") +else() + message(FATAL_ERROR "Unknown function error test case: ${TEST_CASE}") +endif() + +set(case_dir "${TEST_WORK_DIR}/${TEST_CASE}") +file(MAKE_DIRECTORY "${case_dir}") +if(TEST_CASE STREQUAL "temporary_library_write") + file(MAKE_DIRECTORY "${case_dir}/lib.temp") +endif() +set(project "${case_dir}/FunctionError.dissco") +set(run_input "${case_dir}/input.txt") +file(WRITE "${project}" "${project_xml}") +file(WRITE "${run_input}" "1\n") + +execute_process( + COMMAND "${CMOD_EXECUTABLE}" "${project}" + INPUT_FILE "${run_input}" + WORKING_DIRECTORY "${case_dir}" + RESULT_VARIABLE result + OUTPUT_VARIABLE stdout + ERROR_VARIABLE stderr + TIMEOUT 10 +) +file(WRITE "${case_dir}/stdout.txt" "${stdout}") +file(WRITE "${case_dir}/stderr.txt" "${stderr}") + +set(failure "") +if(NOT "${result}" STREQUAL "1") + set(failure "Expected exit 1, got ${result}") +elseif(stdout MATCHES "Build complete\\." OR stderr MATCHES "Build complete\\.") + set(failure "An invalid project was reported as complete") +else() + list(APPEND expected_details "FunctionError.dissco") + foreach(detail IN LISTS expected_details) + string(FIND "${stderr}" "${detail}" position) + if(position EQUAL -1) + set(failure "Missing error detail: ${detail}") + break() + endif() + endforeach() +endif() +if(NOT failure STREQUAL "") + message(FATAL_ERROR + "${TEST_CASE}: ${failure}\nstdout:\n${stdout}\nstderr:\n${stderr}") +endif() diff --git a/CMOD/tests/fixtures/ErrorReporting.dissco b/CMOD/tests/fixtures/ErrorReporting.dissco new file mode 100644 index 00000000..bdb9400d --- /dev/null +++ b/CMOD/tests/fixtures/ErrorReporting.dissco @@ -0,0 +1,64 @@ + + + + ErrorReporting + + 0 + 0 + 1 + False + False + False + 1 + 2 + 44100 + 16 + 1 + False + 102 + + + + + + + 0 + + + 0 + 0 + 1 + 60 + 44 + + 0 + 0 + 2 + 1 + 1 + 60 + + + 0 + 0 + + + + + 0 + 0 + 1 + + + 0 + 1 + 1 + + + + + + + + + diff --git a/CMOD/tests/fixtures/ScoreErrorReporting.dissco b/CMOD/tests/fixtures/ScoreErrorReporting.dissco new file mode 100644 index 00000000..18930fe1 --- /dev/null +++ b/CMOD/tests/fixtures/ScoreErrorReporting.dissco @@ -0,0 +1,172 @@ + + + + MinimalScore + + + 0 + 0 + 1 + False + True + False + 1 + 2 + 22050 + 16 + 1 + False + 102 + + + + + + + + + + 0 + + + 0 + 0 + 60 + 60 + + 4 + 4 + + + 0 + 0 + 2 + 1 + 1 + 60 + + + 0 + 1 + + + + + 0 + 0 + 60 + + + 0 + 1 + 1 + + + + + + + + n08ScoreNote + 4 + 1 + + + 1 + + + 1 + + + + + + + + + + + 4 + n08ScoreNote + 60 + 60 + + 4 + 4 + + + 0 + 0 + 2 + 1 + 1 + 60 + + + 0 + 1 + + + + + 0 + 0 + 60 + + + + + 0 + 1 + 1 + + + + + + + + noteArticulated + 12 + 1 + + + 1 + + + 1 + + + + + + + + + + 0 + 0 + 72 + + + 0.1 + 0 + + + + + + + + + 12 + noteArticulated + + 0 + + accent + + + + + \ No newline at end of file From a4b88e29cf53010d93e9b926982e4f78520714bd Mon Sep 17 00:00:00 2001 From: DiyunZ Date: Thu, 27 Aug 2026 17:08:07 -0500 Subject: [PATCH 2/3] Remove error-reporting tests from published branch --- CMOD/CMakeLists.txt | 56 ------ CMOD/ERROR_REPORTING.md | 8 - CMOD/tests/ErrorReportingFormatTest.cpp | 27 --- CMOD/tests/ErrorReportingTest.cmake | 136 -------------- CMOD/tests/FunctionErrorTest.cmake | 119 ------------ CMOD/tests/fixtures/ErrorReporting.dissco | 64 ------- .../tests/fixtures/ScoreErrorReporting.dissco | 172 ------------------ 7 files changed, 582 deletions(-) delete mode 100644 CMOD/tests/ErrorReportingFormatTest.cpp delete mode 100644 CMOD/tests/ErrorReportingTest.cmake delete mode 100644 CMOD/tests/FunctionErrorTest.cmake delete mode 100644 CMOD/tests/fixtures/ErrorReporting.dissco delete mode 100644 CMOD/tests/fixtures/ScoreErrorReporting.dissco diff --git a/CMOD/CMakeLists.txt b/CMOD/CMakeLists.txt index 43670de8..5d5d66bf 100644 --- a/CMOD/CMakeLists.txt +++ b/CMOD/CMakeLists.txt @@ -99,60 +99,4 @@ if(WIN32 AND SNDFILE_DLL) ) endif() -if(BUILD_TESTING) - add_executable(CMOD_ErrorReportingFormatTest tests/ErrorReportingFormatTest.cpp) - target_include_directories(CMOD_ErrorReportingFormatTest PRIVATE "${CMAKE_CURRENT_SOURCE_DIR}/src") - add_test(NAME CMOD.ErrorReporting.Format COMMAND CMOD_ErrorReportingFormatTest) - - foreach(error_case IN ITEMS - no_arguments missing_project malformed_xml invalid_root missing_configuration - missing_field invalid_integer zero_threads invalid_boolean - children_without_layer negative_children negative_fractional_children - negative_duration empty_score - output_directory valid valid_synthesis) - add_test(NAME CMOD.ErrorReporting.${error_case} - COMMAND ${CMAKE_COMMAND} - -DCMOD_EXECUTABLE=$ - -DPROJECT_FIXTURE=${CMAKE_CURRENT_SOURCE_DIR}/tests/fixtures/ErrorReporting.dissco - -DTEST_WORK_DIR=${CMAKE_CURRENT_BINARY_DIR}/error-reporting/${error_case} - -DTEST_CASE=${error_case} - -P ${CMAKE_CURRENT_SOURCE_DIR}/tests/ErrorReportingTest.cmake - ) - set_tests_properties(CMOD.ErrorReporting.${error_case} PROPERTIES TIMEOUT 20) - endforeach() - - foreach(error_case IN ITEMS - project_expression event_expression nested_expression missing_top - missing_object unknown_function select_negative select_object - null_context null_static_context nonfinite_expression - missing_markov_library invalid_markov_count temporary_library_write) - add_test(NAME CMOD.ErrorReporting.${error_case} - COMMAND ${CMAKE_COMMAND} - -DCMOD_EXECUTABLE=$ - -DPROJECT_FIXTURE=${CMAKE_CURRENT_SOURCE_DIR}/tests/fixtures/ErrorReporting.dissco - -DTEST_WORK_DIR=${CMAKE_CURRENT_BINARY_DIR}/function-errors - -DTEST_CASE=${error_case} - -P ${CMAKE_CURRENT_SOURCE_DIR}/tests/FunctionErrorTest.cmake - ) - set_tests_properties(CMOD.ErrorReporting.${error_case} PROPERTIES TIMEOUT 20) - endforeach() - - set(score_cases lilypond score_file) - find_program(LILYPOND_EXECUTABLE lilypond) - if(LILYPOND_EXECUTABLE) - list(APPEND score_cases valid_score) - endif() - foreach(error_case IN LISTS score_cases) - add_test(NAME CMOD.ErrorReporting.${error_case} - COMMAND ${CMAKE_COMMAND} - -DCMOD_EXECUTABLE=$ - -DPROJECT_FIXTURE=${CMAKE_CURRENT_SOURCE_DIR}/tests/fixtures/ScoreErrorReporting.dissco - -DTEST_WORK_DIR=${CMAKE_CURRENT_BINARY_DIR}/error-reporting/${error_case} - -DTEST_CASE=${error_case} - -P ${CMAKE_CURRENT_SOURCE_DIR}/tests/ErrorReportingTest.cmake - ) - set_tests_properties(CMOD.ErrorReporting.${error_case} PROPERTIES TIMEOUT 20) - endforeach() -endif() - message(STATUS "DONE!") diff --git a/CMOD/ERROR_REPORTING.md b/CMOD/ERROR_REPORTING.md index ce56a120..332e74eb 100644 --- a/CMOD/ERROR_REPORTING.md +++ b/CMOD/ERROR_REPORTING.md @@ -37,11 +37,3 @@ or classify an arbitrary `std::exception` as a user-input error. `Main.cpp` reports the exception once and returns a nonzero exit code. A failed run must not reach its `Build complete.` message. - -## Tests - -With `BUILD_TESTING=ON`, run `ctest --test-dir -R CMOD.ErrorReporting ---output-on-failure`. The CLI tests use isolated fixture copies and bounded -stdin-driven subprocesses. The successful score-output test is registered when -LilyPond is available; missing-LilyPond and score-write failure tests do not -require it. diff --git a/CMOD/tests/ErrorReportingFormatTest.cpp b/CMOD/tests/ErrorReportingFormatTest.cpp deleted file mode 100644 index 399d8516..00000000 --- a/CMOD/tests/ErrorReportingFormatTest.cpp +++ /dev/null @@ -1,27 +0,0 @@ -#include "CmodError.h" - -#include -#include - -static bool check(CmodError::Kind kind, const char* category, int exitCode) { - CmodError error(kind, "Failure reason", "Inner context", "Corrective action"); - error.addContext("Outer context"); - std::ostringstream output; - error.report(output, "Example.dissco"); - const std::string expected = std::string("CMOD ") + category + " error: Failure reason\n" - "Project: Example.dissco\n" - "Context: Outer context -> Inner context\n" - "Suggestion: Corrective action\n" - "Build failed.\n"; - if (output.str() != expected || error.exitCode() != exitCode) { - std::cerr << "Incorrect " << category << " diagnostic:\n" << output.str(); - return false; - } - return true; -} - -int main() { - return check(CmodError::Kind::Project, "project", 1) - && check(CmodError::Kind::Output, "output", 1) - && check(CmodError::Kind::Internal, "internal", 2) ? 0 : 1; -} diff --git a/CMOD/tests/ErrorReportingTest.cmake b/CMOD/tests/ErrorReportingTest.cmake deleted file mode 100644 index a2101ef5..00000000 --- a/CMOD/tests/ErrorReportingTest.cmake +++ /dev/null @@ -1,136 +0,0 @@ -if(NOT DEFINED CMOD_EXECUTABLE OR NOT DEFINED TEST_WORK_DIR) - message(FATAL_ERROR "CMOD_EXECUTABLE and TEST_WORK_DIR are required") -endif() - -file(MAKE_DIRECTORY "${TEST_WORK_DIR}") -if(NOT DEFINED TEST_CASE) - set(TEST_CASE missing_project) -endif() -set(project "${TEST_WORK_DIR}/${TEST_CASE}.dissco") -set(expected "${TEST_CASE}.dissco") -set(expected_result 1) -set(expected_category "project") -set(command "${CMOD_EXECUTABLE}") -set(project_argument "${project}") -if(TEST_CASE STREQUAL "no_arguments") - set(project_argument) - set(expected "No project file was specified") -elseif(TEST_CASE STREQUAL "missing_project") - # Leave the project absent. -else() - file(READ "${PROJECT_FIXTURE}" xml) - if(TEST_CASE STREQUAL "valid" OR TEST_CASE STREQUAL "valid_synthesis" - OR TEST_CASE STREQUAL "valid_score") - set(expected_result 0) - if(TEST_CASE STREQUAL "valid_synthesis") - string(REPLACE "False" "True" xml "${xml}") - endif() - elseif(TEST_CASE STREQUAL "malformed_xml") - string(REPLACE "" "" xml "${xml}") - set(expected "byte") - elseif(TEST_CASE STREQUAL "invalid_root") - set(xml "") - set(expected "ProjectRoot") - elseif(TEST_CASE STREQUAL "missing_configuration") - set(xml "") - set(expected "ProjectConfiguration") - elseif(TEST_CASE STREQUAL "missing_field") - string(REPLACE " 2\n" "" xml "${xml}") - set(expected "NumberOfChannels") - elseif(TEST_CASE STREQUAL "invalid_integer") - string(REPLACE "44100" "44100oops" xml "${xml}") - set(expected "SampleRate") - elseif(TEST_CASE STREQUAL "zero_threads") - string(REPLACE "1" "0" xml "${xml}") - set(expected "NumberOfThreads") - elseif(TEST_CASE STREQUAL "invalid_boolean") - string(REPLACE "False" "Maybe" xml "${xml}") - set(expected "Synthesis") - elseif(TEST_CASE STREQUAL "output_directory") - string(REPLACE "False" "True" xml "${xml}") - file(WRITE "${TEST_WORK_DIR}/SoundFiles" "This regular file blocks the output directory.") - set(expected "SoundFiles") - set(expected_category "output") - elseif(TEST_CASE STREQUAL "children_without_layer") - string(REPLACE "0\n " - "1\n " xml "${xml}") - set(expected "NumberOfChildren") - elseif(TEST_CASE STREQUAL "negative_children") - string(REPLACE "0\n " - "-1\n " xml "${xml}") - set(expected "NumberOfChildren") - elseif(TEST_CASE STREQUAL "negative_fractional_children") - string(REPLACE "0\n " - "-0.5\n " xml "${xml}") - set(expected "NumberOfChildren") - elseif(TEST_CASE STREQUAL "negative_duration") - string(REPLACE "1" "-1" xml "${xml}") - set(expected "ProjectConfiguration.Duration") - elseif(TEST_CASE STREQUAL "empty_score") - string(REPLACE "False" "True" xml "${xml}") - set(expected "staff") - elseif(TEST_CASE STREQUAL "lilypond") - if(WIN32) - set(ENV{PATH} "$ENV{SystemRoot}/System32;$ENV{SystemRoot}") - else() - set(ENV{PATH} "") - endif() - set(expected "LilyPond") - set(expected_category "output") - elseif(TEST_CASE STREQUAL "score_file") - file(MAKE_DIRECTORY "${TEST_WORK_DIR}/score_file.ly") - set(expected "score_file.ly") - set(expected_category "output") - else() - message(FATAL_ERROR "Unknown test case: ${TEST_CASE}") - endif() - file(WRITE "${project}" "${xml}") -endif() -set(run_input "${TEST_WORK_DIR}/input.txt") -file(WRITE "${run_input}" "1\n") -file(GLOB previous_audio "${TEST_WORK_DIR}/SoundFiles/*.aiff") -list(LENGTH previous_audio previous_audio_count) -file(GLOB previous_scores "${TEST_WORK_DIR}/ScoreFiles/*.pdf") -list(LENGTH previous_scores previous_score_count) - -execute_process( - COMMAND ${command} ${project_argument} - INPUT_FILE "${run_input}" - WORKING_DIRECTORY "${TEST_WORK_DIR}" - RESULT_VARIABLE result - OUTPUT_VARIABLE stdout - ERROR_VARIABLE stderr - TIMEOUT 10 -) -file(WRITE "${TEST_WORK_DIR}/stdout.txt" "${stdout}") -file(WRITE "${TEST_WORK_DIR}/stderr.txt" "${stderr}") - -if(expected_result EQUAL 0) - if(NOT "${result}" STREQUAL "0" OR NOT stdout MATCHES "Build complete\\.") - message(FATAL_ERROR "Valid project failed (${result}).\n${stdout}\n${stderr}") - endif() - if(TEST_CASE STREQUAL "valid_synthesis") - file(GLOB audio "${TEST_WORK_DIR}/SoundFiles/*.aiff") - list(LENGTH audio audio_count) - math(EXPR expected_count "${previous_audio_count} + 1") - if(NOT audio_count EQUAL expected_count) - message(FATAL_ERROR "Successful synthesis did not produce one new audio file") - endif() - endif() - if(TEST_CASE STREQUAL "valid_score") - file(GLOB scores "${TEST_WORK_DIR}/ScoreFiles/*.pdf") - list(LENGTH scores score_count) - math(EXPR expected_count "${previous_score_count} + 1") - if(NOT score_count EQUAL expected_count) - message(FATAL_ERROR "Successful score output did not produce one new PDF") - endif() - endif() -elseif(NOT "${result}" STREQUAL "1" - OR NOT stderr MATCHES "CMOD ${expected_category} error:" - OR NOT stderr MATCHES "${expected}" - OR NOT stderr MATCHES "Suggestion:" - OR stdout MATCHES "Build complete\\.") - message(FATAL_ERROR - "${TEST_CASE} must produce an actionable ${expected_category} error (exit 1).\n" - "Actual exit: ${result}\nstdout:\n${stdout}\nstderr:\n${stderr}") -endif() diff --git a/CMOD/tests/FunctionErrorTest.cmake b/CMOD/tests/FunctionErrorTest.cmake deleted file mode 100644 index 3ba7c4de..00000000 --- a/CMOD/tests/FunctionErrorTest.cmake +++ /dev/null @@ -1,119 +0,0 @@ -if(NOT DEFINED CMOD_EXECUTABLE OR NOT DEFINED PROJECT_FIXTURE - OR NOT DEFINED TEST_WORK_DIR OR NOT DEFINED TEST_CASE) - message(FATAL_ERROR "CMOD function test variables were not provided") -endif() - -file(READ "${PROJECT_FIXTURE}" project_xml) -set(expected_details "CMOD project error:" "Suggestion:" "Build failed.") -if(TEST_CASE STREQUAL "project_expression") - string(REPLACE "1" "1+" - project_xml "${project_xml}") - list(APPEND expected_details "1+" "Unexpected end of expression") -elseif(TEST_CASE STREQUAL "event_expression") - string(REPLACE "1" - "1+" project_xml "${project_xml}") - list(APPEND expected_details "Event '0'" "1+" "Unexpected end of expression") -elseif(TEST_CASE STREQUAL "nested_expression") - set(expression "2*(Random1+2)") - string(REPLACE "1" - "${expression}" - project_xml "${project_xml}") - list(APPEND expected_details "Event '0'" "${expression}" - "1+" "Unexpected end of expression") -elseif(TEST_CASE STREQUAL "missing_top") - string(REPLACE "0" "MissingTop" - project_xml "${project_xml}") - list(APPEND expected_details "MissingTop" "type 0") -elseif(TEST_CASE STREQUAL "missing_object") - set(expression "ChooseLReadSIVFileMissingSieve") - string(REPLACE "1" - "${expression}" - project_xml "${project_xml}") - list(APPEND expected_details "Event '0'" "MissingSieve" "type 7") -elseif(TEST_CASE STREQUAL "unknown_function") - set(expression "MissingFunction") - string(REPLACE "1" - "${expression}" - project_xml "${project_xml}") - list(APPEND expected_details "Event '0'" "Unknown numeric function" "MissingFunction") -elseif(TEST_CASE STREQUAL "select_negative") - set(expression "Select10-1") - string(REPLACE "1" - "${expression}" - project_xml "${project_xml}") - list(APPEND expected_details "Event '0'" "Select index" "outside" "-1") -elseif(TEST_CASE STREQUAL "select_object") - set(expression "ChooseLSelectReadSIVFileunused1") - string(REPLACE "1" - "${expression}" - project_xml "${project_xml}") - list(APPEND expected_details "Event '0'" "Select index" "outside") -elseif(TEST_CASE STREQUAL "null_context") - set(expression "GetPatternIN_ORDER0MakePattern1") - string(REPLACE "1" "${expression}" - project_xml "${project_xml}") - list(APPEND expected_details "GetPattern" "requires an event context" "${expression}") -elseif(TEST_CASE STREQUAL "null_static_context") - set(expression "CURRENT_CHILD_NUM") - string(REPLACE "1" "${expression}" - project_xml "${project_xml}") - list(APPEND expected_details "CURRENT_CHILD_NUM" "requires an event context") -elseif(TEST_CASE STREQUAL "nonfinite_expression") - string(REPLACE "1" - "1/0" project_xml "${project_xml}") - list(APPEND expected_details "Event '0'" "non-finite" "Expression: 1/0") -elseif(TEST_CASE STREQUAL "missing_markov_library") - string(REPLACE "0" "" - project_xml "${project_xml}") - list(APPEND expected_details "Missing required project section" "MarkovModelLibrary") -elseif(TEST_CASE STREQUAL "invalid_markov_count") - string(REPLACE "0" - "0oops" project_xml "${project_xml}") - list(APPEND expected_details "MarkovModelLibrary" "model count") -elseif(TEST_CASE STREQUAL "temporary_library_write") - set(expected_details "CMOD output error:" "lib.temp" "Suggestion:" "Build failed.") -else() - message(FATAL_ERROR "Unknown function error test case: ${TEST_CASE}") -endif() - -set(case_dir "${TEST_WORK_DIR}/${TEST_CASE}") -file(MAKE_DIRECTORY "${case_dir}") -if(TEST_CASE STREQUAL "temporary_library_write") - file(MAKE_DIRECTORY "${case_dir}/lib.temp") -endif() -set(project "${case_dir}/FunctionError.dissco") -set(run_input "${case_dir}/input.txt") -file(WRITE "${project}" "${project_xml}") -file(WRITE "${run_input}" "1\n") - -execute_process( - COMMAND "${CMOD_EXECUTABLE}" "${project}" - INPUT_FILE "${run_input}" - WORKING_DIRECTORY "${case_dir}" - RESULT_VARIABLE result - OUTPUT_VARIABLE stdout - ERROR_VARIABLE stderr - TIMEOUT 10 -) -file(WRITE "${case_dir}/stdout.txt" "${stdout}") -file(WRITE "${case_dir}/stderr.txt" "${stderr}") - -set(failure "") -if(NOT "${result}" STREQUAL "1") - set(failure "Expected exit 1, got ${result}") -elseif(stdout MATCHES "Build complete\\." OR stderr MATCHES "Build complete\\.") - set(failure "An invalid project was reported as complete") -else() - list(APPEND expected_details "FunctionError.dissco") - foreach(detail IN LISTS expected_details) - string(FIND "${stderr}" "${detail}" position) - if(position EQUAL -1) - set(failure "Missing error detail: ${detail}") - break() - endif() - endforeach() -endif() -if(NOT failure STREQUAL "") - message(FATAL_ERROR - "${TEST_CASE}: ${failure}\nstdout:\n${stdout}\nstderr:\n${stderr}") -endif() diff --git a/CMOD/tests/fixtures/ErrorReporting.dissco b/CMOD/tests/fixtures/ErrorReporting.dissco deleted file mode 100644 index bdb9400d..00000000 --- a/CMOD/tests/fixtures/ErrorReporting.dissco +++ /dev/null @@ -1,64 +0,0 @@ - - - - ErrorReporting - - 0 - 0 - 1 - False - False - False - 1 - 2 - 44100 - 16 - 1 - False - 102 - - - - - - - 0 - - - 0 - 0 - 1 - 60 - 44 - - 0 - 0 - 2 - 1 - 1 - 60 - - - 0 - 0 - - - - - 0 - 0 - 1 - - - 0 - 1 - 1 - - - - - - - - - diff --git a/CMOD/tests/fixtures/ScoreErrorReporting.dissco b/CMOD/tests/fixtures/ScoreErrorReporting.dissco deleted file mode 100644 index 18930fe1..00000000 --- a/CMOD/tests/fixtures/ScoreErrorReporting.dissco +++ /dev/null @@ -1,172 +0,0 @@ - - - - MinimalScore - - - 0 - 0 - 1 - False - True - False - 1 - 2 - 22050 - 16 - 1 - False - 102 - - - - - - - - - - 0 - - - 0 - 0 - 60 - 60 - - 4 - 4 - - - 0 - 0 - 2 - 1 - 1 - 60 - - - 0 - 1 - - - - - 0 - 0 - 60 - - - 0 - 1 - 1 - - - - - - - - n08ScoreNote - 4 - 1 - - - 1 - - - 1 - - - - - - - - - - - 4 - n08ScoreNote - 60 - 60 - - 4 - 4 - - - 0 - 0 - 2 - 1 - 1 - 60 - - - 0 - 1 - - - - - 0 - 0 - 60 - - - - - 0 - 1 - 1 - - - - - - - - noteArticulated - 12 - 1 - - - 1 - - - 1 - - - - - - - - - - 0 - 0 - 72 - - - 0.1 - 0 - - - - - - - - - 12 - noteArticulated - - 0 - - accent - - - - - \ No newline at end of file From c881b191f2c0c3862fc402150e188d5dc0ca32dc Mon Sep 17 00:00:00 2001 From: DiyunZ Date: Thu, 27 Aug 2026 18:33:41 -0500 Subject: [PATCH 3/3] Improve remaining runtime error diagnostics --- CMOD/src/Bottom.cpp | 404 +++++++++++++++++++--------- CMOD/src/Event.cpp | 399 +++++++++++++++++----------- CMOD/src/Event.h | 14 +- CMOD/src/Matrix.cpp | 8 +- CMOD/src/ModParser.cpp | 68 ++++- CMOD/src/NotationScore.cpp | 26 +- CMOD/src/Note.cpp | 22 +- CMOD/src/Output.cpp | 26 ++ CMOD/src/Patter.cpp | 54 ++-- CMOD/src/Random.cpp | 36 ++- CMOD/src/Section.cpp | 71 +++-- CMOD/src/Sieve.cpp | 88 ++++++- CMOD/src/SignalHandlers.cpp | 21 +- CMOD/src/Utilities.cpp | 421 ++++++++++++++++++++---------- CMOD/src/Utilities.h | 1 + LASSIE/src/windows/PostWindow.cpp | 32 ++- LASSIE/src/windows/PostWindow.hpp | 1 + 17 files changed, 1150 insertions(+), 542 deletions(-) diff --git a/CMOD/src/Bottom.cpp b/CMOD/src/Bottom.cpp index b0473991..2565785e 100644 --- a/CMOD/src/Bottom.cpp +++ b/CMOD/src/Bottom.cpp @@ -25,6 +25,7 @@ //----------------------------------------------------------------------------// #include "Bottom.h" +#include "CmodError.h" #include "ModifierUsage.hpp" #include "Random.h" #include "Output.h" @@ -36,6 +37,7 @@ #include #include #include +#include #include #include #include @@ -49,6 +51,13 @@ struct Bottom::ModifierUsageRuntime { namespace { +void warnBottom(const string& name, const string& field, + const string& message, const string& suggestion) { + cerr << "CMOD warning: " << message << '\n' + << "Context: Bottom '" << name << "' / " << field << '\n' + << "Suggestion: " << suggestion << endl; +} + bool parseStrictDouble(const char* text, double& value) { if (text == NULL || *text == '\0') { return false; @@ -212,8 +221,10 @@ void Bottom::buildChildren(){ checkEvent(buildDiscrete()); } else { - cerr << "Unknown build method: " << method << endl << "Aborting." << endl; - exit(1); + throw CmodError(CmodError::Kind::Project, + "Unknown child build method '" + method + "'.", + "Bottom '" + name + "' / Child Event Definition / DefinitionFlag", + "Choose Continuum (0), Sweep (1), or Discrete (2) as the child build method."); } } @@ -258,7 +269,7 @@ void Bottom::modifyChildren(){ //Incomplete Override //----------------------------------------------------------------------------// -void Bottom::constructChild(SoundAndNoteWrapper* _soundNoteWrapper) { +void Bottom::constructChild(SoundAndNoteWrapper* _soundNoteWrapper) try { //Just to get the checkpoint. Not used any other time. checkPoint = (_soundNoteWrapper->ts.start - ts.start) / ts.duration; if (name.substr(0,1) == "s"){ @@ -270,11 +281,19 @@ void Bottom::constructChild(SoundAndNoteWrapper* _soundNoteWrapper) { buildNote(_soundNoteWrapper); return; } + throw CmodError(CmodError::Kind::Project, + "Bottom name '" + name + "' does not identify a sound or note event.", + "Bottom Name", + "Start sound Bottom names with 's' and note Bottom names with 'n', then update references to the renamed event."); // else if (name.substr(0,2) == "ns" || name.substr(0,2) == "sn"){ // buildSound(_soundNoteWrapper); // buildNote(_soundNoteWrapper); // return; // } +} catch (CmodError& error) { + error.addContext("Bottom '" + name + "' / child #" + + std::to_string(currChildNum + 1) + " ('" + _soundNoteWrapper->name + "')"); + throw; } //----------------------------------------------------------------------------// @@ -493,11 +512,19 @@ list Bottom::getNotes() { float Bottom::computeBaseFreq() { float baseFreqResult; - pugi::xml_node freqFlagElement = GFEC(frequencyElement); - pugi::xml_node continuumFlagElement = GNES(freqFlagElement); - pugi::xml_node valueElement = GNES(continuumFlagElement); - pugi::xml_node valueElement2 = GNES(valueElement); - if (utilities->evaluate(XMLTC(freqFlagElement),(void*) this)==2) {//contiruum + pugi::xml_node freqFlagElement = frequencyElement.child("FrequencyFlag"); + pugi::xml_node continuumFlagElement = frequencyElement.child("FrequencyContinuumFlag"); + pugi::xml_node valueElement = frequencyElement.child("FrequencyEntry1"); + pugi::xml_node valueElement2 = frequencyElement.child("FrequencyEntry2"); + const string context = "Bottom '" + name + "' / Frequency"; + const double frequencyMode = utilities->evaluate(XMLTC(freqFlagElement), this); + if (frequencyMode != 0 && frequencyMode != 1 && frequencyMode != 2) { + throw CmodError(CmodError::Kind::Project, + "Unknown FrequencyFlag value " + std::to_string(frequencyMode) + ".", + context, + "Choose Equal Temperament (0), Fundamental (1), or Continuum (2) in the Bottom event's Frequency settings."); + } + if (frequencyMode == 2) {//continuum /* 2nd arg is a string (HERTZ or POW2) */ if (utilities->evaluate(XMLTC(continuumFlagElement), NULL)==0) { //Hertz @@ -508,21 +535,45 @@ float Bottom::computeBaseFreq() { /* 3rd arg is a float (power of 2) */ float step = utilities->evaluate(XMLTC(valueElement), (void*)this); if(step <= log2(MINFREQ/C0) || step >= log2(CEILING/C0)) { - cerr << "BaseFreq: power of 2 out of range: " << step << endl; - cerr << " log2(MINFREQ/C0) > step < log2(CEILING/C0)" << endl; + cerr << "CMOD warning: " << context << " / FrequencyEntry1: power-of-two step " + << step << " is outside the audible range (" + << log2(MINFREQ/C0) << " to " << log2(CEILING/C0) << "). " + << "Check the power-of-two expression; partial frequencies outside " + << MINFREQ << " to " << CEILING << " Hz are clamped." << endl; } baseFreqResult = C0 * pow(2, step); } - } else if (utilities->evaluate(XMLTC(freqFlagElement), (void*) this)==0) { //equal tempered + } else if (frequencyMode == 0) { //equal tempered /* 2nd arg is an int */ - wellTempPitch = utilities->evaluate(XMLTC(valueElement), (void*)this); + const double pitch = utilities->evaluate(XMLTC(valueElement), this); + if (pitch < std::numeric_limits::min() + || pitch > std::numeric_limits::max()) { + throw CmodError(CmodError::Kind::Project, + "Equal-tempered pitch " + std::to_string(pitch) + " is outside the integer range.", + context + " / FrequencyEntry1", + "Check the pitch expression and choose a pitch that produces a finite, positive frequency."); + } + wellTempPitch = static_cast(pitch); baseFreqResult = C0 * pow(WELL_TEMP_INCR, wellTempPitch); } else {// fundamental /* 2nd arg is (float)fundamental_freq, 3rd arg is (int)overtone_num */ float fund_freq = utilities->evaluate(XMLTC(valueElement), (void*)this); - int overtone_step = utilities->evaluate(XMLTC(valueElement2), (void*)this); + const double overtone = utilities->evaluate(XMLTC(valueElement2), this); + if (overtone < 1 || overtone > std::numeric_limits::max()) { + throw CmodError(CmodError::Kind::Project, + "Overtone number evaluated to " + std::to_string(overtone) + ".", + context + " / FrequencyEntry2", + "Set the overtone number to a positive count within the integer range."); + } + int overtone_step = static_cast(overtone); baseFreqResult = fund_freq * overtone_step; } + if (!std::isfinite(baseFreqResult) || baseFreqResult <= 0) { + throw CmodError(CmodError::Kind::Project, + "Base frequency evaluated to " + std::to_string(baseFreqResult) + " Hz.", + context + " / FrequencyEntry1: " + XMLTC(valueElement), + "Choose a finite frequency greater than zero; check the pitch, power-of-two, or fundamental/overtone expression."); + } return baseFreqResult; } @@ -539,6 +590,12 @@ float Bottom::computeLoudness() { // loudval -= 0.4 * diff; // cout << "bottom loudness: " << loudval << endl; // cout << "bottom expval: " << expVal << endl; + if (!std::isfinite(loudval) || loudval < 0) { + throw CmodError(CmodError::Kind::Project, + "Loudness evaluated to " + std::to_string(loudval) + " sones.", + "Bottom '" + name + "' / Loudness: " + XMLTC(loudnessElement), + "Use a finite, non-negative loudness in sones and check the expression that computes it."); + } return loudval; } @@ -576,8 +633,18 @@ float Bottom::computeCarrierPhase() { int Bottom::computeNumPartials(float baseFreq, pugi::xml_node _spectrum) { - pugi::xml_node numPartialElement = GNES(GNES(GFEC(_spectrum))); - int numPartsResult = utilities->evaluate(XMLTC(numPartialElement), (void*) this); + pugi::xml_node numPartialElement = _spectrum.child("NumberOfPartials"); + const double requested = utilities->evaluate(XMLTC(numPartialElement), this); + const string context = "Bottom '" + name + "' / spectrum '" + + XMLTC(_spectrum.child("Name")) + "' / Number of Partials"; + if (!std::isfinite(requested) || requested < 1 + || requested > std::numeric_limits::max()) { + throw CmodError(CmodError::Kind::Project, + "Number of Partials evaluated to " + std::to_string(requested) + ".", + context, + "Set Number of Partials to a positive count within the integer range, and check the expression that computes it."); + } + int numPartsResult = static_cast(requested); // Decrease numPartials until p < CEILING // (CEILING is a global def from define.h) @@ -586,9 +653,12 @@ int Bottom::computeNumPartials(float baseFreq, pugi::xml_node _spectrum) { } if(numPartsResult <= 0) { - cerr << "Error: Bottom::computeNumPartials got 0, baseFrequency=" - << baseFreq << endl; - exit(1); + throw CmodError(CmodError::Kind::Project, + "No partials fit below CMOD's frequency ceiling of " + + std::to_string(static_cast(CEILING)) + + " Hz; the base frequency is " + std::to_string(baseFreq) + " Hz.", + context, + "Lower the Bottom event's Frequency so at least its fundamental partial fits below the frequency ceiling."); } return numPartsResult; @@ -631,7 +701,20 @@ void Bottom::setPartialSpectrum(Partial& part, int partNum, pugi::xml_node _elem partialEnvElement=GNES(partialEnvElement); counter--; } - Envelope* waveShape = (Envelope*) utilities->evaluateObject(XMLTC(partialEnvElement),(void*)this, eventEnv ); + const string envelope = XMLTC(partialEnvElement); + if (envelope.empty()) { + throw CmodError(CmodError::Kind::Project, + "Partial #" + std::to_string(partNum + 1) + " has no wave-shape envelope.", + "Bottom '" + name + "' / spectrum '" + XMLTC(_element.child("Name")) + "' / Spectrum", + "Provide an envelope for each requested partial, or lower Number of Partials to match the configured envelopes."); + } + Envelope* waveShape = (Envelope*) utilities->evaluateObject(envelope, this, eventEnv); + if (waveShape == NULL) { + throw CmodError(CmodError::Kind::Project, + "Partial #" + std::to_string(partNum + 1) + " did not resolve to an envelope.", + "Bottom '" + name + "' / spectrum '" + XMLTC(_element.child("Name")) + "' / Spectrum", + "Check the partial's envelope expression and its referenced envelope definition."); + } part.setParam(WAVE_SHAPE, *waveShape ); delete waveShape; } @@ -656,6 +739,12 @@ void Bottom::applySpatialization(Sound* s, int numPartials) { string apply = XMLTC(applyHowElement); pugi::xml_node channelsElement = GNES(applyHowElement); + if (apply != "SOUND" && apply != "PARTIAL") { + throw CmodError(CmodError::Kind::Project, + "Invalid spatialization Apply value '" + apply + "'.", + "Bottom '" + name + "' / Spatialization / Apply", + "Choose SOUND to spatialize the whole sound or PARTIAL to spatialize individual partials."); + } if (method.compare("STEREO")==0) { //will be a list of envs, of length 1 if applyhow == SOUND, or @@ -674,9 +763,10 @@ void Bottom::applySpatialization(Sound* s, int numPartials) { spatializationPolar(s, channelsElement, apply, numPartials); } else { - cout << "spat_method = " << method << endl; - cout << "SOUND_SPATIALIZATION has invalid method! Use STEREO, MULTI_PAN, or POLAR" << endl; - exit(1); + throw CmodError(CmodError::Kind::Project, + "Unknown spatialization Method '" + method + "'.", + "Bottom '" + name + "' / Spatialization / Method", + "Choose STEREO, MULTI_PAN, or POLAR and provide the corresponding channel envelopes."); } } @@ -699,7 +789,9 @@ void Bottom::spatializationStereo(Sound *s, if (applyHow == "SOUND") { envstr = XMLTC(envelopeElement); if (envstr == "") { - cerr << "WARNING: spatializationStereo got empty envelope for sound; ignoring" << endl; + warnBottom(name, "Spatialization / STEREO / Channels", + "The sound has no panning envelope; stereo spatialization is skipped.", + "Add the sound's panning envelope to the spatialization definition."); // ^ this cannot be wrapped into computeSpatializationStereo // since returning an empty Pan object is ambiguous // but refactoring the function to return a pointer introduces memory hazards @@ -713,7 +805,9 @@ void Bottom::spatializationStereo(Sound *s, for (int i = 0; i < numParts; i++) { envstr = XMLTC(envelopeElement); if (envstr == "") { - cerr << "WARNING: spatializationStereo got empty envelope for partial " << i << "; ignoring" << endl; + warnBottom(name, "Spatialization / STEREO / Channels / partial #" + std::to_string(i + 1), + "This partial has no panning envelope; its spatialization is skipped.", + "Add a panning envelope for this partial if it should be spatialized."); } else { Pan stereoPan = computeSpatializationStereo(envstr); s->get(i).setSpatializer(stereoPan); @@ -724,12 +818,6 @@ void Bottom::spatializationStereo(Sound *s, } } - else { - cerr << "Error: " << applyHow << " is an invalid way to apply spatialization! " - << "Use SOUND or PARTIAL" << endl; - - } - } //----------------------------------------------------------------------------// @@ -797,8 +885,16 @@ void Bottom::spatializationMultiPan(Sound *s, } if (applyHow == "SOUND") { + if (isPartialValid.empty()) { + throw CmodError(CmodError::Kind::Project, + "MULTI_PAN has no channel envelopes for the sound.", + "Bottom '" + name + "' / Spatialization / Channels", + "Add a non-empty envelope for each channel in the MULTI_PAN spatialization definition."); + } if (isPartialValid.at(0) == false) { - cerr << "WARNING: spatializationMultiPan got empty envelope for sound; ignoring" << endl; + warnBottom(name, "Spatialization / MULTI_PAN / Channels", + "At least one channel envelope is empty; spatialization of the sound is skipped.", + "Provide a non-empty envelope for every channel of the sound."); return; } MultiPan multipan = computeSpatializationMultiPan(mults.at(0)); @@ -807,11 +903,15 @@ void Bottom::spatializationMultiPan(Sound *s, } else if (applyHow == "PARTIAL") { for (unsigned i = 0; (int)i < numParts; i++) { // apply multipan to each partial if (mults.size() <= i){ - cout << "WARNING: spatializationMultiPan got empty envelopes for partial " << i << " and onwards; ignoring" << endl; + warnBottom(name, "Spatialization / MULTI_PAN / Channels / partial #" + std::to_string(i + 1), + "No channel envelopes remain; spatialization of this and later partials is skipped.", + "Add channel envelopes for any remaining partials that should be spatialized."); break; } if (isPartialValid.at(i) == false) { - cerr << "WARNING: spatializationMultiPan got empty envelope for partial " << i << "; ignoring" << endl; + warnBottom(name, "Spatialization / MULTI_PAN / Channels / partial #" + std::to_string(i + 1), + "A channel envelope is empty; spatialization of this partial is skipped.", + "Provide a non-empty envelope for every channel of this partial."); continue; } MultiPan multipan = computeSpatializationMultiPan(mults.at(i)); @@ -819,11 +919,6 @@ void Bottom::spatializationMultiPan(Sound *s, } } - else { - cerr << "Error: " << applyHow << " is an invalid way to apply spatialization! " - << "Use SOUND or PARTIAL" << endl; - - } } //----------------------------------------------------------------------------// @@ -864,7 +959,9 @@ void Bottom::spatializationPolar(Sound *s, theta = XMLTC(thetaElement); radius = XMLTC(radiusElement); if (theta == "" || radius == "") { - cerr << "WARNING: spatializationPolar got empty envelope for sound; ignoring" << endl; + warnBottom(name, "Spatialization / POLAR / Channels", + "The sound is missing a theta or radius envelope; polar spatialization is skipped.", + "Provide both theta and radius envelopes in the spatialization definition."); } else { MultiPan multipan = computeSpatializationPolar(theta, radius); s->setSpatializer(multipan); @@ -876,7 +973,9 @@ void Bottom::spatializationPolar(Sound *s, theta = XMLTC(thetaElement); radius = XMLTC(radiusElement); if (theta == "" || radius == "") { - cerr << "WARNING: spatializationPolar got empty envelope for partial " << i << "; ignoring" << endl; + warnBottom(name, "Spatialization / POLAR / Channels / partial #" + std::to_string(i + 1), + "This partial is missing a theta or radius envelope; its spatialization is skipped.", + "Provide both envelopes for this partial if it should be spatialized."); } else { MultiPan multipan = computeSpatializationPolar(theta, radius); s->get(i).setSpatializer(multipan); @@ -886,12 +985,6 @@ void Bottom::spatializationPolar(Sound *s, } } - else { - cerr << "Error: " << applyHow << " is an invalid way to apply spatialization! " - << "Use SOUND or PARTIAL" << endl; - - } - } //----------------------------------------------------------------------------// @@ -965,8 +1058,10 @@ void Bottom::applyFilter(Sound* s){ else if (type == "LSF") typeInt =5; else if (type == "HSF") typeInt =6; else { - cout<<"Filter Type not recognized."<evaluateRev((void*) this); + if (!reverbElement) return; // Reverberation is optional. //this call will return a rev function, just in case users use "select" here. //The string here is just a dummy since the callee will find the right rev @@ -1030,6 +1126,12 @@ void Bottom::applyReverberation(Sound *s, int numPartials) { pugi::xml_node applyHowElement = GNES(GFEC(reverbElement)); string rev_apply = XMLTC(applyHowElement); + if (rev_apply != "SOUND" && rev_apply != "PARTIAL") { + throw CmodError(CmodError::Kind::Project, + "Invalid reverb Apply value '" + rev_apply + "'.", + "Bottom '" + name + "' / Reverb / Apply", + "Choose SOUND to reverberate the whole sound or PARTIAL for per-partial reverberation."); + } // Number of parameters varies between methods. But unlike spatialization, // this "everything else" part is NOT enclosed in a element; @@ -1048,9 +1150,11 @@ void Bottom::applyReverberation(Sound *s, int numPartials) { reverberationAdvanced(s, paramsElement, rev_apply, numPartials); } - else { - cerr << "WARNING: Invalid method/syntax in reverb!" << endl; - cerr << " Method = " << rev_method << endl; + else { + throw CmodError(CmodError::Kind::Project, + "Unknown reverb method '" + rev_method + "'.", + "Bottom '" + name + "' / Reverb / Method", + "Choose REV_Simple, REV_Medium, or REV_Advanced, or remove reverberation if it is not needed."); } } @@ -1079,18 +1183,19 @@ void Bottom::reverberationSimple(Sound *s, // Add the reverb obj to the partial. It appears that this is already implemented in LASS/src/Partial.cpp. s->get(i).use_reverb(reverbObj); sizeElement = GNES(sizeElement); - if (!sizeElement) { - cerr << "WARNING: reverberationSimple parameters undefined since partial " - << i + 1 << "; ignoring" << endl; + if (i + 1 < numPartials && !sizeElement) { + warnBottom(name, "Reverb / REV_Simple / Sizes", + "Room sizes are missing for partial #" + std::to_string(i + 2) + + " and later partials; their reverberation is skipped.", + "Add room-size entries for the remaining partials if they should have reverberation."); break; } } if (sizeElement) { - cerr << "WARNING: reverberationSimple parameters defined beyond partial " - << numPartials - 1 << "; ignoring" << endl; + warnBottom(name, "Reverb / REV_Simple / Sizes", + "Extra room-size entries beyond the " + std::to_string(numPartials) + " sound partials are ignored.", + "Remove unused entries or increase Number of Partials if those entries were intended to be used."); } - } else { - cout << "WARNING: No specifier for reverb, cannot apply." << endl; } } @@ -1104,9 +1209,10 @@ Reverb* Bottom::computeReverberationSimple(pugi::xml_node sizeElement, int iPart string envstr = XMLTC(sizeElement); if (envstr == "") { - cerr << "WARNING: Fewer partials set in reverb string than configured in spectrum. Defaulting "; - if (iPartial == -1) cerr << "sound to room size 0" << endl; - else cerr << "partial " << iPartial << " to room size 0" << endl; + warnBottom(name, "Reverb / REV_Simple / Room Size / " + + (iPartial < 0 ? string("sound") : "partial #" + std::to_string(iPartial + 1)), + "Room Size is empty; using the default value 0.", + "Set Room Size in the reverb definition if a non-default room is intended."); roomSize = 0.0; } else { roomSize = utilities->evaluate(envstr, (void*)this); @@ -1156,20 +1262,21 @@ void Bottom::reverberationMedium(Sound *s, allPassElement = GNES(allPassElement); delayElement = GNES(delayElement); - if (!percentElement || !spreadElement || !allPassElement || !delayElement) { - cerr << "WARNING: reverberationMedium parameters undefined since partial " - << i + 1 << "; ignoring" << endl; + if (i + 1 < numPartials + && (!percentElement || !spreadElement || !allPassElement || !delayElement)) { + warnBottom(name, "Reverb / REV_Medium / partial #" + std::to_string(i + 2), + "Reverb parameters are missing for this and later partials; their reverberation is skipped.", + "Provide a Percent envelope, Spread, All Pass gain, and Delay for each remaining partial that should have reverb."); break; } } if (percentElement || spreadElement || allPassElement || delayElement) { - cerr << "WARNING: reverberationMedium parameters defined beyond partial " - << numPartials - 1 << "; ignoring" << endl; + warnBottom(name, "Reverb / REV_Medium", + "Extra parameter entries beyond the " + std::to_string(numPartials) + " sound partials are ignored.", + "Match the reverb parameter lists to the intended number of partials."); } - } else { - cout << "WARNING: No specifier for reverb, cannot apply." << endl; } } @@ -1184,9 +1291,10 @@ Reverb* Bottom::computeReverberationMedium(pugi::xml_node percentElement, //second input is percent reverb envelope string envstr = XMLTC(percentElement); if (envstr == "") { - cerr << "WARNING: reverberationMedium got empty envelope for "; - if (iPartial == -1) cerr << "sound; ignoring" << endl; - else cerr << "partial " << iPartial << "; ignoring" << endl; + warnBottom(name, "Reverb / REV_Medium / Percent / " + + (iPartial < 0 ? string("sound") : "partial #" + std::to_string(iPartial + 1)), + "The Percent envelope is empty; this reverberation effect is skipped.", + "Provide a Percent envelope if this sound or partial should have reverberation."); return NULL; } Envelope* percent_rev = @@ -1197,10 +1305,19 @@ Reverb* Bottom::computeReverberationMedium(pugi::xml_node percentElement, float gain_all_pass = utilities->evaluate(XMLTC(allPassElement),this); float delay = utilities->evaluate(XMLTC(delayElement),this); + if (!std::isfinite(delay) || delay < 0) { + delete percent_rev; + throw CmodError(CmodError::Kind::Project, + "Reverb Delay evaluated to " + std::to_string(delay) + " seconds.", + "Bottom '" + name + "' / Reverb / REV_Medium / Delay", + "Use a finite, non-negative Delay in seconds; zero disables this reverberation effect."); + } if (delay == 0) { - cerr << "WARNING: reverberationMedium got 0 delay for "; - if (iPartial == -1) cerr << "sound; ignoring" << endl; - else cerr << "partial " << iPartial << "; ignoring" << endl; + warnBottom(name, "Reverb / REV_Medium / Delay / " + + (iPartial < 0 ? string("sound") : "partial #" + std::to_string(iPartial + 1)), + "Delay is zero; this reverberation effect is skipped.", + "Use a positive Delay in seconds if reverberation is intended."); + delete percent_rev; return NULL; } @@ -1253,22 +1370,22 @@ void Bottom::reverberationAdvanced(Sound *s, allPassElement = GNES(allPassElement); delayElement = GNES(delayElement); - if (!percentElement || !combGainListElement || !lpGainListElement || - !allPassElement || !delayElement) { - cerr << "WARNING: reverberationAdvanced parameters undefined since partial " - << i + 1 << "; ignoring" << endl; + if (i + 1 < numPartials && (!percentElement || !combGainListElement + || !lpGainListElement || !allPassElement || !delayElement)) { + warnBottom(name, "Reverb / REV_Advanced / partial #" + std::to_string(i + 2), + "Reverb parameters are missing for this and later partials; their reverberation is skipped.", + "Provide Percent, Comb Gain List, LP Gain List, All Pass gain, and Delay entries for each remaining partial that should have reverb."); break; } } if (percentElement || combGainListElement || lpGainListElement || allPassElement || delayElement) { - cerr << "WARNING: reverberationAdvanced parameters defined beyond partial " - << numPartials - 1 << "; ignoring" << endl; + warnBottom(name, "Reverb / REV_Advanced", + "Extra parameter entries beyond the " + std::to_string(numPartials) + " sound partials are ignored.", + "Match the reverb parameter lists to the intended number of partials."); } - } else { - cout << "WARNING: No specifier for reverb, cannot apply." << endl; } } @@ -1283,9 +1400,10 @@ Reverb* Bottom::computeReverberationAdvanced(pugi::xml_node percentElement, //second input is percent reverb envelope string envstr = XMLTC(percentElement); if (envstr == "") { - cerr << "WARNING: reverberationAdvanced got empty envelope for "; - if (iPartial == -1) cerr << "sound; ignoring" << endl; - else cerr << "partial " << iPartial << "; ignoring" << endl; + warnBottom(name, "Reverb / REV_Advanced / Percent / " + + (iPartial < 0 ? string("sound") : "partial #" + std::to_string(iPartial + 1)), + "The Percent envelope is empty; this reverberation effect is skipped.", + "Provide a Percent envelope if this sound or partial should have reverberation."); return NULL; } Envelope* percent_rev = @@ -1295,10 +1413,11 @@ Reverb* Bottom::computeReverberationAdvanced(pugi::xml_node percentElement, vector stringListC = utilities->listElementToStringVector(combGainListElement); if (stringListC.size() != 6) { - cerr << "WARNING: reverb comb gain list for "; - if (iPartial == -1) cerr << "sound must contain 6 items!" << endl; - else cerr << "partial " << iPartial << " must contain 6 items!" << endl; - return NULL; + delete percent_rev; + throw CmodError(CmodError::Kind::Project, + "Comb Gain List has " + std::to_string(stringListC.size()) + " entries; expected 6.", + "Bottom '" + name + "' / Reverb / REV_Advanced / Comb Gain List", + "Provide exactly six comma-separated comb-filter gain values."); } vector comb_gain_list; @@ -1311,10 +1430,11 @@ Reverb* Bottom::computeReverberationAdvanced(pugi::xml_node percentElement, vector stringListG = utilities->listElementToStringVector(lpGainListElement); if (stringListG.size() != 6) { - cerr << "WARNING: reverb lp gain list for "; - if (iPartial == -1) cerr << "sound must contain 6 items!" << endl; - else cerr << "partial " << iPartial << " must contain 6 items!" << endl; - return NULL; + delete percent_rev; + throw CmodError(CmodError::Kind::Project, + "LP Gain List has " + std::to_string(stringListG.size()) + " entries; expected 6.", + "Bottom '" + name + "' / Reverb / REV_Advanced / LP Gain List", + "Provide exactly six comma-separated low-pass gain values."); } vector lp_gain_list; @@ -1327,10 +1447,19 @@ Reverb* Bottom::computeReverberationAdvanced(pugi::xml_node percentElement, float gain_all_pass = utilities->evaluate(XMLTC(allPassElement),this); float delay = utilities->evaluate(XMLTC(delayElement),this); + if (!std::isfinite(delay) || delay < 0) { + delete percent_rev; + throw CmodError(CmodError::Kind::Project, + "Reverb Delay evaluated to " + std::to_string(delay) + " seconds.", + "Bottom '" + name + "' / Reverb / REV_Advanced / Delay", + "Use a finite, non-negative Delay in seconds; zero disables this reverberation effect."); + } if (delay == 0) { - cerr << "WARNING: reverberationAdvanced got 0 delay for "; - if (iPartial == -1) cerr << "sound; ignoring" << endl; - else cerr << "partial " << iPartial << "; ignoring" << endl; + warnBottom(name, "Reverb / REV_Advanced / Delay / " + + (iPartial < 0 ? string("sound") : "partial #" + std::to_string(iPartial + 1)), + "Delay is zero; this reverberation effect is skipped.", + "Use a positive Delay in seconds if reverberation is intended."); + delete percent_rev; return NULL; } @@ -1512,19 +1641,21 @@ void Bottom::initializeModifierUsage(pugi::xml_node modifierUsageElement) { CompileOptions compileOptions; compileOptions.overallUsageMode = OverallUsageMode::Skip; CompileResult compiled = compile(std::move(config), compileOptions); + string diagnostics; for (const string& diagnostic : adapterDiagnostics) { - cerr << "Bottom::ModifierUsage configuration error in " << name - << ": " << diagnostic << endl; + diagnostics += diagnostic + " "; } for (const Diagnostic& diagnostic : compiled.diagnostics) { - cerr << "Bottom::ModifierUsage configuration error in " << name - << ": " << diagnostic.message << endl; + diagnostics += diagnostic.message + " "; } - if (adapterDiagnostics.empty() && compiled.program.has_value()) { - modifierUsageRuntime->program.emplace( - std::move(*compiled.program)); + if (!diagnostics.empty() || !compiled.program.has_value()) { + throw CmodError(CmodError::Kind::Project, + "Invalid Modifier Usage configuration: " + diagnostics, + "Bottom '" + name + "' / Modifier Usage", + "Correct the listed Modifier Usage settings: version 1, per-sound or per-bottom scope, unique IDs, and ON chances between 0 and 1."); } + modifierUsageRuntime->program.emplace(std::move(*compiled.program)); } //-----------------------------------------------------------------------------/ @@ -1533,22 +1664,17 @@ void Bottom::applyModifierUsage(Sound *s, int numPartials) { using dissco::modifier_usage::ModifierId; using dissco::modifier_usage::Selection; - if (!modifierUsageRuntime->program.has_value()) { - // Diagnostics were emitted once when this Bottom was constructed. - return; - } - struct RuntimeModifier { std::unique_ptr effect; bool applyByPartial = false; }; std::unordered_map> modifiersById; - bool runtimeValid = true; - auto runtimeError = [this, &runtimeValid](const string& message) { - runtimeValid = false; - cerr << "Bottom::ModifierUsage runtime error in " << name - << ": " << message << endl; + auto runtimeError = [this](const string& message) { + throw CmodError(CmodError::Kind::Project, + message, + "Bottom '" + name + "' / Modifiers", + "Correct the named modifier's type, application mode, or required parameters in the Bottom event (including inherited modifiers), then run again."); }; pugi::xml_document mergedModifiersDoc; @@ -1582,8 +1708,17 @@ void Bottom::applyModifierUsage(Sound *s, int numPartials) { continue; } - const int modTypeCode = static_cast( - utilities->evaluate(XMLTC(modifierElement.child("Type")), this)); + const string typeExpression = XMLTC(modifierElement.child("Type")); + const double typeValue = utilities->evaluate(typeExpression, this); + if (!std::isfinite(typeValue) || typeValue < 0 || typeValue > 7 + || std::floor(typeValue) != typeValue) { + std::ostringstream value; + value << typeValue; + runtimeError("modifier '" + usageId + "' has invalid Type " + value.str() + + " (expression: " + typeExpression + "); choose an integer type " + "from 0 (TREMOLO) through 7 (PHASE_MOD)."); + } + const int modTypeCode = static_cast(typeValue); string modType; switch (modTypeCode) { case 0: modType = "TREMOLO"; break; @@ -1596,17 +1731,20 @@ void Bottom::applyModifierUsage(Sound *s, int numPartials) { case 7: modType = "PHASE_MOD"; break; default: runtimeError("modifier '" + usageId - + "' has an unknown Type value."); + + "' has unknown Type " + std::to_string(modTypeCode) + + "; choose a modifier type from 0 (TREMOLO) through 7 (PHASE_MOD)."); continue; } - const int applyHowCode = static_cast( - utilities->evaluate(XMLTC(modifierElement.child("ApplyHow")), this)); - if (applyHowCode != 0 && applyHowCode != 1) { - runtimeError("modifier '" + usageId - + "' has an invalid ApplyHow value."); - continue; + const string applyExpression = XMLTC(modifierElement.child("ApplyHow")); + const double applyValue = utilities->evaluate(applyExpression, this); + if (!std::isfinite(applyValue) || (applyValue != 0 && applyValue != 1)) { + std::ostringstream value; + value << applyValue; + runtimeError("modifier '" + usageId + "' has invalid ApplyHow " + value.str() + + " (expression: " + applyExpression + "); use 0 (SOUND) or 1 (PARTIAL)."); } + const int applyHowCode = static_cast(applyValue); const bool applyByPartial = applyHowCode == 1; const string ampStr = XMLTC(modifierElement.child("Amplitude")); @@ -1695,7 +1833,10 @@ void Bottom::applyModifierUsage(Sound *s, int numPartials) { } if (envelopeCount < requiredEnvelopeCount) { runtimeError("modifier '" + usageId - + "' is missing a required parameter envelope."); + + "' (" + modType + ") is missing a required parameter envelope: " + + (isUnavailable(ampStr) ? "Amplitude " : "") + + (requiredEnvelopeCount >= 2 && isUnavailable(rateStr) ? "Rate " : "") + + (requiredEnvelopeCount >= 3 && isUnavailable(widthStr) ? "Width " : "")); continue; } modifiersById[usageId].push_back( @@ -1799,17 +1940,17 @@ void Bottom::applyModifierUsage(Sound *s, int numPartials) { + "' has no runtime effect."); } } - if (!runtimeValid) { - return; - } - Selection selection; try { selection = modifierUsageRuntime->program->select( []() { return Random::Rand(); }); + } catch (const CmodError&) { + throw; } catch (const std::exception& error) { - runtimeError(error.what()); - return; + throw CmodError(CmodError::Kind::Internal, + "Modifier selection failed: " + string(error.what()), + "Bottom '" + name + "' / Modifier Usage selection", + "Report this diagnostic to the DISSCO developers with the project and seed."); } // Selection IDs are already in Program order. A selected PARTIAL logical @@ -1890,7 +2031,10 @@ void Bottom::generatePartials(Sound* newsound, float frequency, float loudness, float strength = loudness*distance/256*2; //strength is normalized between 0 and 2 if ((frequency < 233) || frequency > 932){ - cout << "Error in genratePartials: frequency out of range" << endl; + warnBottom(name, "Generate Spectrum / Frequency", + "Frequency is " + std::to_string(frequency) + + " Hz, outside Spectrum_Gen's reference range of 233 to 932 Hz; generation continues.", + "Check the Bottom event's Frequency, or use explicit spectrum partial envelopes if this wider range is intentional."); } //calculate scale for Partials double scaleTable[3][3][20] = { diff --git a/CMOD/src/Event.cpp b/CMOD/src/Event.cpp index 6f682808..0bb346d3 100644 --- a/CMOD/src/Event.cpp +++ b/CMOD/src/Event.cpp @@ -56,8 +56,7 @@ Event::Event(pugi::xml_node _element, restartsRemaining(0), currChildNum(0), childType(0), matrix(0), - utilities(_utilities), - discreteFailedResponse("") + utilities(_utilities) { //Initialize parameters @@ -74,7 +73,14 @@ Event::Event(pugi::xml_node _element, maxChildDur = (float)utilities->evaluate(XMLTC(thisEventElement), (void*)this); thisEventElement = GNES(thisEventElement); - int newEDUPerBeat = (int) utilities->evaluate(XMLTC(thisEventElement),(void*)this); + const double eduPerBeat = utilities->evaluate(XMLTC(thisEventElement),(void*)this); + if (!std::isfinite(eduPerBeat) || eduPerBeat < 1 || eduPerBeat > std::numeric_limits::max()) { + throw CmodError(CmodError::Kind::Project, + "EDU Per Beat must produce a positive integer within CMOD's timing range.", + "Event '" + name + "' -> EDU Per Beat: " + to_string(eduPerBeat), + "Set EDU Per Beat to a positive integer, such as 60; zero cannot define the timing grid."); + } + int newEDUPerBeat = static_cast(eduPerBeat); Ratio k(newEDUPerBeat,1); Tempo fvTempo; // File-Value Tempo @@ -86,7 +92,45 @@ Event::Event(pugi::xml_node _element, thisEventElement = GNES(thisEventElement); fvTempo.setTempo(getTempoStringFromDOMElement(thisEventElement)); - fvTempo.getTempoBeat(); + // Ratio multiplies before reducing. Validate the same intermediate + // products in a wider type before timing or score code evaluates them. + const Ratio timingEDUs = fvTempo.getEDUPerTimeSignatureBeat(); + const Ratio timingBeatsPerBar = fvTempo.getTimeSignatureBeatsPerBar(); + const Ratio timingBeat = fvTempo.getTimeSignatureBeat(); + const Ratio timingTempoBeat = fvTempo.getTempoBeat(); + const Ratio timingBPM = fvTempo.getTempoBeatsPerMinute(); + const string timingContext = "Event '" + name + "' -> Time Signature: " + + fvTempo.getTimeSignature() + "; EDU Per Beat: " + timingEDUs.toPrettyString() + + "; Tempo: " + timingBPM.toPrettyString(); + const auto checkedTimingRatio = [&timingContext](Ratio left, Ratio right, + bool divide, const string& field) { + const long long numerator = static_cast(left.Num()) * + (divide ? right.Den() : right.Num()); + const long long denominator = static_cast(left.Den()) * + (divide ? right.Num() : right.Den()); + if (numerator <= 0 || denominator <= 0 || + numerator > std::numeric_limits::max() || + denominator > std::numeric_limits::max()) { + throw CmodError(CmodError::Kind::Project, + "The derived " + field + " is outside CMOD's supported integer timing range.", + timingContext + " -> " + field + ": " + to_string(numerator) + "/" + to_string(denominator), + "Reduce EDU Per Beat or the Time Signature values, or simplify the Tempo fraction. " + "Each intermediate timing numerator and denominator must fit within 1 to " + + to_string(std::numeric_limits::max()) + "."); + } + return Ratio(static_cast(numerator), static_cast(denominator)); + }; + checkedTimingRatio(timingEDUs, timingBeatsPerBar, false, "EDU per bar"); + const Ratio beatsPerTempoBeat = checkedTimingRatio(timingTempoBeat, timingBeat, true, "beats per tempo beat"); + const Ratio tempoBeatsPerBeat = checkedTimingRatio(timingBeat, timingTempoBeat, true, "tempo beats per beat"); + checkedTimingRatio(timingBeatsPerBar, tempoBeatsPerBeat, false, "tempo beats per bar"); + const Ratio beatsPerMinute = checkedTimingRatio(timingBPM, beatsPerTempoBeat, false, "beats per minute"); + checkedTimingRatio(Ratio(60), timingBPM, true, "tempo beat duration"); + const Ratio secondsPerBeat = checkedTimingRatio(Ratio(60), beatsPerMinute, true, "time-signature beat duration"); + checkedTimingRatio(timingEDUs, beatsPerTempoBeat, false, "EDU per tempo beat"); + const Ratio edusPerMinute = checkedTimingRatio(timingEDUs, beatsPerMinute, false, "EDU per minute"); + checkedTimingRatio(edusPerMinute, Ratio(60), true, "EDU per second"); + checkedTimingRatio(secondsPerBeat, timingEDUs, true, "EDU duration"); fvTempo.setStartTime(tempo.getStartTime()); @@ -137,11 +181,14 @@ Event::Event(pugi::xml_node _element, layerElements.push_back(layerElement); pugi::xml_node childPackage = GFEC(GNES(GFEC(layerElement))); + vector layerNames; while(childPackage){ childTypeElements.push_back(childPackage); + layerNames.push_back(XMLTC(GFEC(childPackage))); childPackage = GNES(childPackage); } + layerVect.push_back(layerNames); layerElement = GNES(layerElement); } @@ -171,6 +218,12 @@ Event::Event(pugi::xml_node _element, pugi::xml_node underOneElement = GNES(areaElement); double density = utilities->evaluate( XMLTC(densityElement),(void*)this); double area = utilities->evaluate( XMLTC(areaElement),(void*)this); + if (area == 0) { + throw CmodError(CmodError::Kind::Project, + "The Density calculation divides by an Area of zero.", + "Event '" + name + "' -> Number of Children -> Density -> Area: 0", + "Set Area to a nonzero value, or choose a different method for Number of Children."); + } // cout << "areaElement=" << areaElement << endl; double underOne = utilities->evaluate( XMLTC(underOneElement),(void*)this); double soundsPsec = pow(2, density * area - underOne); //this can't be right.. @@ -197,6 +250,20 @@ Event::Event(pugi::xml_node _element, "Add child events to this event's Layers, or set Number of Children to zero."); } + if (numChildren > 0 && XMLTC(methodFlagElement) != "2") { + const auto validateUnit = [this](pugi::xml_node element, const string& field) { + const string value = XMLTC(element); + if (value != "0" && value != "1" && value != "2") { + throw CmodError(CmodError::Kind::Project, + "The " + field + " is missing or not recognized.", + "Event '" + name + "' -> Child Event Definition -> " + field + ": '" + value + "'", + "Choose Fraction (0), EDU (1), or Seconds (2) for this timing field."); + } + }; + validateUnit(childStartTypeFlag, "Start Time Unit"); + validateUnit(childDurationTypeFlag, "Duration Unit"); + } + if (type <=3){ //top, high, mid, low thisEventElement = GNES(thisEventElement); @@ -284,6 +351,22 @@ string Event::getTempoStringFromDOMElement(pugi::xml_node _element){ thisElement = GNES(thisElement); double valueEntry = utilities->evaluate(XMLTC(thisElement),(void*)this); + const auto checkTempoValue = [this](double value, const string& field) { + if (!std::isfinite(value) || value <= 0 || value > std::numeric_limits::max()) { + throw CmodError(CmodError::Kind::Project, + "A Tempo value is zero, negative, or too large for CMOD's timing representation.", + "Event '" + name + "' -> Tempo -> " + field + ": " + to_string(value), + "Use a positive finite Tempo value no larger than " + + to_string(std::numeric_limits::max()) + "."); + } + }; + checkTempoValue(valueEntry, "Value"); + if (methodFlag != "0" && methodFlag != "1") { + throw CmodError(CmodError::Kind::Project, + "The Tempo method is not recognized.", + "Event '" + name + "' -> Tempo -> MethodFlag: '" + methodFlag + "'", + "Choose a note-value Tempo (0) or a fractional Tempo (1)."); + } if (prefix == "1"){ stringbuffer = stringbuffer + "dotted "; @@ -323,43 +406,26 @@ string Event::getTempoStringFromDOMElement(pugi::xml_node _element){ stringbuffer = stringbuffer + "thirtysecond = "; } - if (methodFlag == "0") {// tempo as note value - - char tempobuffer[20]; - sprintf(tempobuffer, "%f", valueEntry); - stringbuffer = stringbuffer + string(tempobuffer); + // Preserve the existing six-decimal precision without overflowing fixed + // buffers or the integer numerator while formatting a Tempo. + long long numerator = std::llround(valueEntry * 1000000.0); + long long denominator = 1000000; + if (methodFlag == "1") { + checkTempoValue(fractionEntry1, "Fraction numerator"); + numerator = std::llround(fractionEntry1 * 60.0 * 1000000.0); + denominator = std::llround(valueEntry * 1000000.0); } - - else { // tempo as fraction - //"entry1" notes in "value" seconds - //entry : value = actual number : 60 - //entry1 * 60 / value = actual number - - - double entry1 = fractionEntry1 * 60; - double den = valueEntry; - - char tempobuffer [20]; - sprintf(tempobuffer, "%f", entry1); - string numString = string(tempobuffer); - - sprintf (tempobuffer,"%f", den); - string denString = string(tempobuffer); - - string ratioNumber = numString + "/" + denString; - Ratio ratio = Ratio(ratioNumber); - - sprintf(tempobuffer, "%d", ratio.Num()); - - if (ratio.Den() ==1){ - stringbuffer = stringbuffer + string(tempobuffer) ; - } - else{ - stringbuffer = stringbuffer + string(tempobuffer) + "/"; - sprintf(tempobuffer, "%d", ratio.Den()); - stringbuffer = stringbuffer + string(tempobuffer); - } + Rational ratio(numerator, denominator); + if (numerator <= 0 || denominator <= 0 || + ratio.Num() > std::numeric_limits::max() || + ratio.Den() > std::numeric_limits::max()) { + throw CmodError(CmodError::Kind::Project, + "The Tempo cannot be represented by CMOD's integer timing ratios.", + "Event '" + name + "' -> Tempo -> Value: " + to_string(valueEntry), + "Use a positive Tempo with fewer decimal places and smaller fractional values; " + "the value must remain positive when rounded to six decimal places."); } + stringbuffer += ratio.toPrettyString(); return stringbuffer; } @@ -374,15 +440,25 @@ string Event::getTimeSignatureStringFromDOMElement(pugi::xml_node _element){ */ + const auto signatureEntry = [this](pugi::xml_node element, const string& field) { + const double value = utilities->evaluate(XMLTC(element),(void*)this); + if (!std::isfinite(value) || value < 1 || value > std::numeric_limits::max()) { + throw CmodError(CmodError::Kind::Project, + "The Time Signature must have a positive numerator and denominator.", + "Event '" + name + "' -> Time Signature -> " + field + ": " + to_string(value), + "Use positive integers for both Time Signature fields, such as 4/4."); + } + return static_cast(value); + }; pugi::xml_node thisElement = GFEC(_element); - int entry1 = utilities->evaluate(XMLTC(thisElement),(void*)this); + int entry1 = signatureEntry(thisElement, "Numerator"); char charbuffer[20]; sprintf(charbuffer, "%d", entry1); string stringbuffer = string(charbuffer); thisElement = GNES(thisElement); - int entry2 = utilities->evaluate(XMLTC(thisElement),(void*)this); + int entry2 = signatureEntry(thisElement, "Denominator"); sprintf(charbuffer, "%d", entry2); string returnString = stringbuffer + "/"+ string(charbuffer); @@ -427,9 +503,10 @@ void Event::buildChildren() { //Make sure that the temporary child events array is clear. if(temporaryChildEvents.size() > 0) { - cerr << "WARNING: temporaryChildEvents should not contain data." << endl; - cerr << "There may be a bug in the code. Please report." << endl; - exit(1); + throw CmodError(CmodError::Kind::Internal, + "Child generation started with unfinished temporary events.", + "Event '" + name + "' -> child generation", + "Report this error with the project file and the complete diagnostic."); } /* old code. --Ming-ching May 06, 2013 @@ -449,8 +526,10 @@ void Event::buildChildren() { else if (method == "2") checkEvent(buildDiscrete()); else { - cerr << "Unknown build method: " << method << endl << "Aborting." << endl; - exit(1); + throw CmodError(CmodError::Kind::Project, + "The child generation method is not recognized.", + "Event '" + name + "' -> Child Event Definition -> DefinitionFlag: '" + method + "'", + "Choose Continuum (0), Sweep (1), or Discrete (2) as the child generation method."); } } @@ -536,6 +615,22 @@ void Event::findLeafChildren(vector & leafChildren){ //----------------------------------------------------------------------------- +int Event::checkedChildType(double value) const { + if (!std::isfinite(value) || value < 0 || value >= childTypeElements.size() || + value > std::numeric_limits::max()) { + throw CmodError(CmodError::Kind::Project, + "The child Type selects an event that is not listed in Layers.", + "Event '" + name + "' -> child " + to_string(currChildNum + 1) + + " of " + to_string(numChildren) + + " -> Child Event Definition -> Type: " + to_string(value), + "Choose a Type index from 0 to " + + to_string(static_cast(childTypeElements.size()) - 1) + + " for the " + to_string(childTypeElements.size()) + + " child events listed in Layers, or add the missing child event."); + } + return static_cast(value); +} + bool Event::buildContinuum() { string startType = XMLTC(childStartTypeFlag); string durType = XMLTC(childDurationTypeFlag); @@ -585,14 +680,14 @@ bool Event::buildContinuum() { tsChild.start = rawChildStartTime * ts.duration; // convert to seconds tsChild.startEDU = Ratio(0, 0); // floating point is not exact: NaN } else { - cerr << "Event::buildContinuum -- invalid or missing start type!" << endl; - cerr << " startType = " << startType << endl; - cerr << " in file " << name << endl; - exit(1); + throw CmodError(CmodError::Kind::Project, + "The child Start Time unit is missing or not recognized.", + "Event '" + name + "' -> Child Event Definition -> Start Time Unit: '" + startType + "'", + "Choose Fraction (0), EDU (1), or Seconds (2) for Start Time."); } // get the type - childType = utilities->evaluate(XMLTC(childTypeElement),(void*)this); + childType = checkedChildType(utilities->evaluate(XMLTC(childTypeElement),(void*)this)); childName = XMLTC(GFEC(childTypeElements[childType])); // get the duration @@ -625,10 +720,10 @@ bool Event::buildContinuum() { tsChild.duration = maxChildDur; // enforce limit tsChild.durationEDU = Ratio(0, 0); // floating point is not exact: NaN } else { - cerr << "Event::buildContinuum -- invalid or missing duration type!" << endl; - cerr << " durtype = " << durType << endl; - cerr << " in file " << name << endl; - exit(1); + throw CmodError(CmodError::Kind::Project, + "The child Duration unit is missing or not recognized.", + "Event '" + name + "' -> Child Event Definition -> Duration Unit: '" + durType + "'", + "Choose Fraction (0), EDU (1), or Seconds (2) for Duration."); } } @@ -718,15 +813,13 @@ bool Event::buildSweep() { checkPoint = tsPrevious.end / ts.duration; if (checkPoint > 1) { - cerr << "Event::Sweep -- Error1: tsChild.start outside range of " - << "parent duration." << endl; - cerr << " childStime=" << tsChild.start << ", parentDur=" - << ts.duration << endl; - cerr << " in file: " << name << ", childNum=" - << currChildNum << endl; - cerr << "currChildNum=" << currChildNum << " tsPrevious.end=" - << tsPrevious.end << " checkPoint=" << checkPoint << endl; - exit(1); + throw CmodError(CmodError::Kind::Project, + "Sweep cannot place the next child because the preceding children extend beyond the parent duration.", + "Event '" + name + "' -> Sweep -> child " + to_string(currChildNum + 1) + + " of " + to_string(numChildren) + " -> previous end: " + to_string(tsPrevious.end) + + " seconds; parent duration: " + to_string(ts.duration) + " seconds", + "Reduce Number of Children to Create or the child Duration, or extend the parent duration. " + "Check that the Start Time and Duration units match the entered values."); } // get the start time @@ -772,7 +865,7 @@ bool Event::buildSweep() { } // get the type - childType = utilities->evaluate(XMLTC(childTypeElement),(void*)this); + childType = checkedChildType(utilities->evaluate(XMLTC(childTypeElement),(void*)this)); childName = XMLTC(GFEC(childTypeElements[childType])); // get the duration @@ -845,13 +938,13 @@ bool Event::buildSweep() { checkPoint = tsChild.start / ts.duration; if (checkPoint > 1) { - cerr << "Event::Sweep -- Error2: tsChild.start outside range of " - << "parent duration." << endl; - cerr << " childStime=" << tsChild.start << ", parentDur=" - << ts.duration << endl; - cerr << " in file: " << name << ", childNum=" - << currChildNum << endl; - exit(1); + throw CmodError(CmodError::Kind::Project, + "A Sweep child starts after the parent event has ended.", + "Event '" + name + "' -> Sweep -> child " + to_string(currChildNum + 1) + + " of " + to_string(numChildren) + " -> start: " + to_string(tsChild.start) + + " seconds; parent duration: " + to_string(ts.duration) + " seconds", + "Reduce Number of Children to Create or the child Duration, or extend the parent duration. " + "Check the Start Time and Duration units."); } if (utilities->getOutputParticel()){ @@ -911,87 +1004,38 @@ Event::~Event() { } -//----------------------------------------------------------------------------// -//Checked - -string* waitForDiscreteResponse(Event* event){ - string response = ""; - cin >>response; - string * retval = new string(response); - - event->setDiscreteFailedResponse(response); - return retval; -} - - //----------------------------------------------------------------------------// //Checked void Event::tryToRestart(void) { - //Decrement restarts, or if there are none left, ask for fewer children. + // Retry random placements, but never change the requested child count. if(restartsRemaining > 0) { restartsRemaining--; - cout << "Failed to build child " << currChildNum << " of " << numChildren - << " in file " << name << ". There are " << restartsRemaining - << " tries remaining." << endl; + cout << "Retrying Discrete generation for event '" << name + << "': cannot place child " << currChildNum + 1 << " of " << numChildren + << ". " << restartsRemaining << " retries remain." << endl; } else { - //Ask for permission to build with less children. - cout << "Event::tryToRestart - currChildNum=" << currChildNum - << " numChildren=" << numChildren << endl; - cerr << "No tries remain. Try building with one less child? (Y/n)" << endl; - - bool inputAccepted = false; - string answer = ""; - while (!inputAccepted){ - discreteFailedResponse = ""; - string* myfail = nullptr; - discreteWaitForInputIfFailedThread = std::thread( - [this, &myfail]() { myfail = waitForDiscreteResponse(this); }); - discreteWaitForInputIfFailedThread.join(); - string thisfail = *myfail; - /*int counter = 30; - while (counter != 0){ - if (discreteFailedResponse !=""){ - break; - } - cout<<" Seconds before default action: "<< counter<<"\r"<< flush; - sleep(1); - counter --; - }*/ - - //warning! memory leak here! There is a problem killing thread waiting for cin. need to figure this out. - // --Ming-ching May 06, 2013) - //pthread_cancel(discreteWaitForInputIfFailedThread); - - //answer = (counter ==0)? "y" : discreteFailedResponse; - answer = thisfail; - delete myfail; - if (answer == "y" || answer == "Y" || answer =="n" || answer == "N") inputAccepted = true; - else { - cout<<"Please enter 'y' or 'n'."< child " + to_string(currChildNum + 1) + + " of " + to_string(numChildren) + " -> parent duration: " + + to_string(ts.duration) + " seconds", + "Reduce Number of Children to Create, shorten the Duration Sieve values, " + "or provide more allowed Attack Sieve positions within the parent duration. " + "Check that the layer weights and probability envelopes allow these placements."); } //Start over by clearing the event arrays and resetting the for-loop index. // NOTE: SHOULD BE -1 currChildNum = -1; - for (unsigned i = 0; i < childEvents.size(); i++) + for (unsigned i = 0; i < temporaryChildEvents.size(); i++) delete temporaryChildEvents[i]; temporaryChildEvents.clear(); //Clear the temporary event list. - childSoundsAndNotes.clear(); //sever 5/29/2016 + for (auto* child : childSoundsAndNotes) delete child; + childSoundsAndNotes.clear(); patternStorage.clear(); } @@ -1125,9 +1169,12 @@ void Event::checkEvent(bool buildResult) { } //Make sure the childType indexes correctly. - if (childType >= (int)childTypeElements.size() ) { - cerr << "There is a mismatch between childType and typeVect." << endl; - exit(1); + if (childType < 0 || childType >= (int)childTypeElements.size() ) { + throw CmodError(CmodError::Kind::Internal, + "Child generation returned an invalid Type index.", + "Event '" + name + "' -> child " + to_string(currChildNum + 1) + + " -> Type: " + to_string(childType), + "Report this error with the project file and the complete diagnostic."); } //Create new event. @@ -1137,7 +1184,7 @@ void Event::checkEvent(bool buildResult) { string childEventName = XMLTC(GFEC(discretePackage)); pugi::xml_node childElement = utilities->getEventElement(childEventType, childEventName); - Event* e; + Event* e = NULL; if (childEventType == eventBottom){ e = (Event*) new Bottom(childElement, tsChild, childType, tempo, utilities, spatializationElement, reverberationElement, filterElement, modifiersIncludingAncestorsElement); @@ -1156,7 +1203,7 @@ void Event::checkEvent(bool buildResult) { } } - temporaryChildEvents.push_back(e); + if (e != NULL) temporaryChildEvents.push_back(e); } @@ -1205,10 +1252,14 @@ int Event::getCurrentLayer() { int countInLayer = 0; for(unsigned i = 0; i < layerVect.size(); i++) { countInLayer += layerVect[i].size(); - if(childType < countInLayer) + if(childType >= 0 && childType < countInLayer) return i; } - cerr << "Unable to get layer number in file " << name << endl; exit(1); + throw CmodError(CmodError::Kind::Project, + "CURRENT_LAYER has no available child event to look up.", + "Event '" + name + "' -> CURRENT_LAYER -> Type: " + to_string(childType), + "Add child events to Layers and use CURRENT_LAYER in a child-generation expression " + "after the child Type is available."); } @@ -1360,25 +1411,54 @@ void Event::buildMatrix(bool discrete) { vector durEnvs; vector numTypesInLayers; - if (discrete) { - attackSiv = (Sieve*) utilities->evaluateObject( - XMLTC(AttackSieveElement), - (void*) this, eventSiv); - - durSiv = (Sieve*) utilities->evaluateObject( - XMLTC(DurationSieveElement), - (void*) this, eventSiv); - } else { - attackSiv = utilities->evaluateSieve(XMLTC(childStartTimeElement), (void*) this); - durSiv = utilities->evaluateSieve(XMLTC(childDurationElement), (void*) this); + const auto readSieve = [this, discrete](pugi::xml_node element, const string& field) { + try { + return discrete + ? static_cast(utilities->evaluateObject(XMLTC(element), this, eventSiv)) + : utilities->evaluateSieve(XMLTC(element), this); + } catch (CmodError& error) { + error.addContext("Event '" + name + "' -> Child Event Definition -> " + field); + throw; + } + }; + attackSiv = readSieve(discrete ? AttackSieveElement : childStartTimeElement, "Attack Sieve"); + durSiv = readSieve(discrete ? DurationSieveElement : childDurationElement, "Duration Sieve"); + + if (attackSiv == NULL || attackSiv->GetNumItems() == 0 || + durSiv == NULL || durSiv->GetNumItems() == 0) { + const string field = attackSiv == NULL || attackSiv->GetNumItems() == 0 + ? "Attack Sieve" : "Duration Sieve"; + delete attackSiv; + delete durSiv; + throw CmodError(CmodError::Kind::Project, + "The " + field + " contains no usable values.", + "Event '" + name + "' -> Child Event Definition -> " + field, + "Check the sieve's Low/High limits, Elements, and Offset so at least one value remains."); } double weightSum = 0; for (unsigned i = 0; i < childTypeElements.size(); i ++){ double prob = utilities->evaluate(XMLTC(GNES(GNES(GFEC(childTypeElements[i])))), (void*) this); + if (!std::isfinite(prob) || prob < 0) { + delete attackSiv; + delete durSiv; + throw CmodError(CmodError::Kind::Project, + "A child event's probability Weight is negative or non-finite.", + "Event '" + name + "' -> Layers -> child Type " + to_string(i) + + " -> Weight: " + to_string(prob), + "Use finite, nonnegative weights, with at least one positive child weight."); + } typeProbs.push_back(prob); weightSum += prob; } + if (!std::isfinite(weightSum) || weightSum <= 0) { + delete attackSiv; + delete durSiv; + throw CmodError(CmodError::Kind::Project, + "The child event probability weights have no positive finite total.", + "Event '" + name + "' -> Layers -> Weight total: " + to_string(weightSum), + "Give at least one child event a positive Weight and keep all weights finite and nonnegative."); + } for (unsigned i = 0; i < typeProbs.size(); i ++){ typeProbs[i] = typeProbs[i] / weightSum; @@ -1463,12 +1543,20 @@ void Event::buildMatrix(bool discrete) { //----------------------------------------------------------------------------// int Event::verify_valid(int endTime){ + // Numeric EDU timing is already exact; sieve alignment is optional. + if (!Utilities::isSieveFunction(childStartTimeElement)) return endTime; int beatEDUs = tempo.getEDUPerTimeSignatureBeat().Num(); //cout << " beatEDUs=" << beatEDUs << endl; if (sieveSweep == NULL){ sieveSweep = utilities->evaluateSieve(XMLTC(childStartTimeElement), (void*) this); + if (sieveSweep == NULL || sieveSweep->GetNumItems() == 0) { + throw CmodError(CmodError::Kind::Project, + "The Sweep Start Time sieve contains no usable values.", + "Event '" + name + "' -> Sweep -> Child Event Definition -> Start Time", + "Check the sieve's Low/High limits, Elements, and Offset so at least one value remains."); + } vector attProbs; vector attTimes; sieveSweep->FillInVectors(attTimes, attProbs); @@ -1483,6 +1571,9 @@ int Event::verify_valid(int endTime){ } int length = attackSweep.size(); + if (length == 0) { + return endTime; + } int low = 0; int high = length - 1; int eTime = endTime % beatEDUs; diff --git a/CMOD/src/Event.h b/CMOD/src/Event.h index 32e89a00..e35b031c 100644 --- a/CMOD/src/Event.h +++ b/CMOD/src/Event.h @@ -226,7 +226,6 @@ class Event { //Number of restarts remaining. int restartsRemaining; static const int restartsNormallyAllowed = 6; - static const int restartsAllowedWithFewerChildren = 10; ///Restarts the build process if necessary (for buildDiscrete). void tryToRestart(void); @@ -257,11 +256,6 @@ class Event { pugi::xml_node childStartTypeFlag; pugi::xml_node childDurationTypeFlag; - // This thing sorta works, but killing a thread waiting for cin causes - // memory leak.. -- Ming-ching May 06, 2013 - std::thread discreteWaitForInputIfFailedThread; - string discreteFailedResponse; - public: @@ -393,12 +387,6 @@ class Event { **/ void addPattern(std::string _string, Patter* _pat); - /** - * todo: incomplete function - **/ - void setDiscreteFailedResponse(string _input) - { discreteFailedResponse = _input;} - //------------- Private helper functions ------------// protected: @@ -451,6 +439,8 @@ class Event { string getTimeSignatureStringFromDOMElement(pugi::xml_node _element); + int checkedChildType(double value) const; + void buildMatrix(bool discrete); /** diff --git a/CMOD/src/Matrix.cpp b/CMOD/src/Matrix.cpp index d1e478ba..94f7b8fd 100644 --- a/CMOD/src/Matrix.cpp +++ b/CMOD/src/Matrix.cpp @@ -241,6 +241,7 @@ void Matrix::setTypeProbs(vector typeProbVect) { //----------------------------------------------------------------------------// MatPoint Matrix::chooseSweep(int remain) { MatPoint chosenPt = choose(); + if (chosenPt.type == -1) return chosenPt; // set probs to 0 for every point starting before the end of chosenPt removeSweepConflicts(chosenPt); @@ -259,6 +260,7 @@ MatPoint Matrix::chooseContinuum() { //---------------------------------------------------------------------------// MatPoint Matrix::chooseDiscrete(int remain) { MatPoint chosenPt = choose(); + if (chosenPt.type == -1) return chosenPt; // remove conflicts in the matrix (set probs to 0) removeConflicts(chosenPt); @@ -361,7 +363,7 @@ bool Matrix::normalizeMatrix() { } if (matrSum == 0) { - cerr << "MATRIX - ERROR: Sum of matrix is 0! (We're out of space)." << endl; + // Event reports the failed placement with the project and child context. return false; // indicate failure } @@ -405,6 +407,10 @@ void Matrix::recomputeTypeProbs(int chosenType, int remaining) { //----------------------------------------------------------------------------// int Matrix::verify_valid(int endTime){ + // A valid sieve may contain only attacks after the first beat. Without a + // beat-local anchor, keep the already valid EDU endpoint unchanged. + if (short_attime.empty()) return endTime; + int length = short_attime.size(); int low = 0; diff --git a/CMOD/src/ModParser.cpp b/CMOD/src/ModParser.cpp index b14084b1..79316df0 100644 --- a/CMOD/src/ModParser.cpp +++ b/CMOD/src/ModParser.cpp @@ -1,4 +1,13 @@ #include "ModParser.h" +#include "CmodError.h" +#include +#include + +static CmodError invalidModExpression(const std::string& message) { + return CmodError(CmodError::Kind::Project, message, + "Sieve -> MODS expression", + "Use positive integer moduli, matching parentheses, and U, I, or ~ operators. Provide an offset for each modulus."); +} ModParser::Token::Token(int n, int minVal, int maxVal, int offset) : n(n) @@ -33,11 +42,11 @@ int ModParser::Token::getInt() { std::list ModParser::modList(int mod, int min, int max, int offset) { std::list result; - int startNum = min + offset + (mod - (min % mod)) % mod; + long long startNum = static_cast(min) + offset + (mod - (min % mod)) % mod; startNum -= mod * ((startNum - min)/mod); - for (int i = startNum; i <= max; i += mod) { - result.push_back(i); + for (long long i = startNum; i <= max; i += mod) { + result.push_back(static_cast(i)); } return result; @@ -107,6 +116,10 @@ void ModParser::parseOperator(std::vector& operands, std::stac char op = operators.top(); operators.pop(); std::list result; + const size_t required = op == '~' ? 1 : 2; + if (operands.size() < required) { + throw invalidModExpression("MODS operator '" + std::string(1, op) + "' is missing an operand."); + } if (op == '~') { result = listComplement(operands.back().getList(), minVal, maxVal); @@ -155,43 +168,82 @@ void ModParser::parseExpr(const std::string& exp, int minVal, int maxVal) { std::stack operators; unsigned chNum = 0; int modIndex = 0; + bool needsOperand = true; while (chNum < exp.size()) { char ch = exp[chNum]; - if (isdigit(ch)) { + if (std::isdigit(static_cast(ch))) { + if (!needsOperand) { + throw invalidModExpression("MODS expression is missing an operator before position " + std::to_string(chNum + 1) + "."); + } int num = 0; - while (isdigit(ch) && chNum < exp.size()) { + while (chNum < exp.size() && std::isdigit(static_cast(exp[chNum]))) { + ch = exp[chNum]; + if (num > (std::numeric_limits::max() - (ch - '0')) / 10) { + throw invalidModExpression("MODS modulus exceeds the supported integer range."); + } num *= 10; num += ch - '0'; ++chNum; - ch = exp[chNum]; + } + if (num == 0) { + throw invalidModExpression("MODS modulus 0 would cause division by zero."); + } + if (modIndex >= _offsets.size()) { + throw invalidModExpression("MODS modulus " + std::to_string(modIndex + 1) + " has no matching offset."); } operands.push_back(Token(num, minVal, maxVal, _offsets[modIndex])); _mods.push_back(num); ++modIndex; + needsOperand = false; } else if (precedence(ch) != -1) { - while (!operators.empty() + if ((ch == '~') != needsOperand) { + throw invalidModExpression("MODS operator '" + std::string(1, ch) + "' is misplaced or missing an operand."); + } + while (ch != '~' && !operators.empty() && precedence(operators.top()) != -1 && precedence(operators.top()) >= precedence(ch)) { parseOperator(operands, operators, minVal, maxVal); } operators.push(ch); ++chNum; + needsOperand = true; } else if (ch == '(') { + if (!needsOperand) { + throw invalidModExpression("MODS expression is missing an operator before an opening parenthesis."); + } operators.push('('); ++chNum; } else if (ch == ')') { - while (operators.top() != '(') { + if (needsOperand) { + throw invalidModExpression("MODS closing parenthesis has no preceding operand."); + } + while (!operators.empty() && operators.top() != '(') { parseOperator(operands, operators, minVal, maxVal); } + if (operators.empty()) { + throw invalidModExpression("MODS closing parenthesis has no matching opening parenthesis."); + } operators.pop(); ++chNum; } else { + if (!std::isspace(static_cast(ch))) { + throw invalidModExpression("MODS expression contains unsupported character '" + std::string(1, ch) + "'."); + } ++chNum; } } + if (needsOperand) { + throw invalidModExpression("MODS expression '" + exp + "' is empty or ends without an operand."); + } while (!operators.empty()) { + if (operators.top() == '(') { + throw invalidModExpression("MODS opening parenthesis has no matching closing parenthesis."); + } parseOperator(operands, operators, minVal, maxVal); } + if (operands.size() != 1) { + throw invalidModExpression("MODS expression does not reduce to one sieve."); + } _elements = operands.back().getList(); } diff --git a/CMOD/src/NotationScore.cpp b/CMOD/src/NotationScore.cpp index eccba6e9..f5c8f74b 100644 --- a/CMOD/src/NotationScore.cpp +++ b/CMOD/src/NotationScore.cpp @@ -63,7 +63,7 @@ void NotationScore::RegisterTempo(Tempo& tempo,int staffNum) { ++section_iter; } - if (score_staff[staffNum].empty() || *section_iter != ts) { + if (section_iter == score_staff[staffNum].end() || *section_iter != ts) { score_staff[staffNum].insert(section_iter, Section(ts)); } } @@ -90,16 +90,20 @@ void NotationScore::InsertNote(Note* n) { n->setStaffNum(staffSum-1); } if (score_staff[n->getStaffNum()].empty()) { - cerr << "Cannot add note to score without any sections!" << endl; - exit(1); + throw CmodError(CmodError::Kind::Internal, + "A note was generated before its score tempo was registered.", + "Score output, staff " + to_string(n->getStaffNum()), + "Report this error to the DISSCO developers with the project file, seed, and full output."); } vector
::iterator section_iter = score_staff[n->getStaffNum()].begin(); while (section_iter != score_staff[n->getStaffNum()].end() && !(*section_iter).InsertNote(n)) ++section_iter; if (section_iter == score_staff[n->getStaffNum()].end()) { - cerr << "Note does not belong to any section in the score!" << endl; - exit(1); + throw CmodError(CmodError::Kind::Internal, + "A note could not be assigned to a registered tempo section.", + "Score output, staff " + to_string(n->getStaffNum()), + "Report this error to the DISSCO developers with the project file, seed, and full output."); } } @@ -129,6 +133,7 @@ void NotationScore::InsertNote(Note* n) { void NotationScore::Build() { if (!is_built_) { for(int i=0 ; iGetTimeSignature().time_signature_; bool print_time_signature = first_section || current_time_signature != previous_time_signature; iter->Build(print_time_signature); + } catch (CmodError& error) { + error.addContext("Score output, staff " + to_string(i)); + throw; + } } is_built_ = true; } @@ -253,6 +262,13 @@ ostream& operator<<(ostream& output_stream, pitchSum = pitchSum + 1; } } + if (pitchSum == 0) { + throw CmodError(CmodError::Kind::Project, + "No notes with a notatable duration remain in this score section.", + "Score output, staff " + to_string(i) + ", section starting at " + + to_string(iter->GetStartTimeGlobal()) + " seconds", + "Check note durations and EDU settings. Give notes a positive duration that can be represented in the score, or disable score output."); + } avePitchNum = avePitchNum / pitchSum; // if the average pitch number isn't smaller than 48, choose treble // else choose bass diff --git a/CMOD/src/Note.cpp b/CMOD/src/Note.cpp index df7c171f..e704933c 100644 --- a/CMOD/src/Note.cpp +++ b/CMOD/src/Note.cpp @@ -24,6 +24,7 @@ Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA. //----------------------------------------------------------------------------// #include "Note.h" +#include "CmodError.h" #include "Event.h" #include "Output.h" #include "Rational.h" @@ -34,6 +35,7 @@ Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA. #include #include #include +#include using namespace std; @@ -107,11 +109,15 @@ void Note::setPitchWellTempered(int absPitchNum) { octaveNum = pitchNum / 12; octavePitch = pitchNum % 12; + if (octavePitch < 0) { + octavePitch += 12; + --octaveNum; + } pitchName = pitchNames[octavePitch]; string pitch = OutNames[octavePitch]; - string signs[8] = {",,,",",,",",","","'","''","'''","''''"}; - string sign = signs[octaveNum]; + string sign = octaveNum < 3 ? string(3 - octaveNum, ',') + : string(octaveNum - 3, '\''); chord_tones.clear(); chord_tones.push_back({pitch + sign, modifiers, INT_MIN}); rebuildPitchOutput(); @@ -130,7 +136,9 @@ int Note::HertzToPitch(float freqHz) { int pitchNum; if ( freqHz >= CEILING || freqHz <= MINFREQ) { - cerr << "WARNING: frequency out of range" << endl; + cerr << "Warning: Note Frequency is " << freqHz << " Hz, outside the nominal " + << MINFREQ << " to " << CEILING << " Hz range; using the nearest tempered pitch. " + << "Suggestion: Check the Bottom event's Frequency setting if this pitch is not intended." << endl; } pitchNum = rint(12 * log2(freqHz / C0)); @@ -176,9 +184,11 @@ void Note::setLoudnessMark(int dynamicNum, vector dynamicNames) { void Note::setLoudnessSones(float sones) { loudnessNum = -1; // cout << " sones: " << sones << endl; - if(sones < 0 || sones > 256) { - cerr << "Note received invalid value for sones!" << endl; - exit(1); + if(!std::isfinite(sones) || sones < 0 || sones > 256) { + throw CmodError(CmodError::Kind::Project, + "Note loudness is " + to_string(sones) + " sones, outside the supported range.", + "Note loudness (sones)", + "Set Loudness to a finite value from 0 to 256 sones for note events."); } else if(sones <= 4) { loudnessMark = "ppp"; } else if(sones <= 8) { diff --git a/CMOD/src/Output.cpp b/CMOD/src/Output.cpp index ca18909e..9cffbf09 100644 --- a/CMOD/src/Output.cpp +++ b/CMOD/src/Output.cpp @@ -18,6 +18,7 @@ Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA. */ #include "Output.h" +#include "CmodError.h" #include "Note.h" // #include "Note.cpp" @@ -137,6 +138,12 @@ string OutputNode::sanitize(string name) { void Output::writeLineToParticel(string line) { if(!particelFile) return; *particelFile << line << endl; + if (!*particelFile) { + throw CmodError(CmodError::Kind::Output, + "Could not write the Particel report.", + "Particel output (.particel)", + "Check that the project folder is writable and the disk has free space."); + } } @@ -170,6 +177,14 @@ void Output::initialize(string particelFilename) { if(particelFilename != "") { particelFile = new ofstream(); particelFile->open(particelFilename.c_str()); + if (!*particelFile) { + delete particelFile; + particelFile = nullptr; + throw CmodError(CmodError::Kind::Output, + "Could not create the Particel report.", + particelFilename, + "Check write permissions and make sure the output path is not a folder or a locked file."); + } } } @@ -180,9 +195,20 @@ void Output::free(void) { delete top; top = nullptr; + bool reportFailed = false; + if (particelFile) { + particelFile->close(); + reportFailed = particelFile->fail(); + } delete particelFile; particelFile = nullptr; level = -1; + if (reportFailed) { + throw CmodError(CmodError::Kind::Output, + "Could not finish writing the Particel report.", + "Particel output (.particel)", + "Check write permissions and available disk space before running the project again."); + } } diff --git a/CMOD/src/Patter.cpp b/CMOD/src/Patter.cpp index 02096847..3e2bfa67 100644 --- a/CMOD/src/Patter.cpp +++ b/CMOD/src/Patter.cpp @@ -26,6 +26,7 @@ Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA. #include "Patter.h" #include "Random.h" +#include "CmodError.h" //---------------------------------------------------------------------------// @@ -83,32 +84,17 @@ void Patter::moveOrigin( int newOrigin ) { //----------------------------------------------------------------------------// int Patter::GetNextValue(string method, int newOrigin) { - int returnValue = 0; // Initialize to 0 + if (method != "IN_ORDER") { + throw CmodError(CmodError::Kind::Project, + "GetPattern method '" + method + "' is not implemented in this CMOD version.", + "Function: GetPattern -> Method", + "Choose IN_ORDER, or replace GetPattern with a supported selection function."); + } if (origin != newOrigin) { moveOrigin(newOrigin); } - - if (method == "IN_ORDER") { - returnValue = patty[nextIndex]; - nextIndex = (nextIndex + 1) % patty.size(); - - } else if (method == "OTHER") { - // ValuePick; - // ChooseFromList; - // SimpleIrand - } else if (method == "TYPE_CLUSTERS") { // stochos FUNCS - //value = (int)ReadComputeFloat(checkPoint, offset); - - } else if (method == "TIME_DEPEND") { // tone-row - // value = Patter::TimeDepend(checkPoint); - - } else if (method == "PROBABILITY") { - // value = - - } else { - cerr << "Patter::GetNextValue - method not available" << endl; - exit(1); - } + const int returnValue = patty[nextIndex]; + nextIndex = (nextIndex + 1) % patty.size(); return returnValue; } @@ -128,21 +114,20 @@ void Patter::SimplePat() { //---------------------------------------------------------------------------// void Patter::Expand(string method, int modulo, int low, int high) { + if (method != "EQUIVALENCE") { + throw CmodError(CmodError::Kind::Project, + "ExpandPattern method '" + method + "' is not implemented in this CMOD version.", + "Function: ExpandPattern -> Method", + "Choose EQUIVALENCE, or remove the ExpandPattern function."); + } // don't call Expand if origin = 0; delay until GetPattern is called! if(origin == 0) { expMethod = method; expModulo = modulo; expLow = low; expHigh = high; - } else if(method == "EQUIVALENCE") { - Equivalence(modulo,low,high); - } else if(method == "SYMMETRIES") { - Symmetries(modulo,low,high); - } else if(method == "DISTORT") { - Distort(modulo,low,high); } else { - cerr << "Patter::Expand - no method available" << endl; - exit(1); + Equivalence(modulo,low,high); } } @@ -209,7 +194,12 @@ void Patter::Equivalence(int modulo, int low, int high) { } if (probs.size() == 0) { - cout << "Patter::Equivalence() error: probs array empty!" << endl; + throw CmodError(CmodError::Kind::Project, + "ExpandPattern cannot produce a value within range " + + std::to_string(low) + " through " + std::to_string(high) + + " from " + std::to_string(lastNum) + " with modulo " + std::to_string(modulo) + ".", + "Function: ExpandPattern -> EQUIVALENCE -> interval " + std::to_string(location), + "Widen the range or adjust the origin, intervals, and modulo so at least one equivalent value is available."); } // normalize the probability array, and order from 0 to 1 diff --git a/CMOD/src/Random.cpp b/CMOD/src/Random.cpp index efcb2abd..dff0d39b 100644 --- a/CMOD/src/Random.cpp +++ b/CMOD/src/Random.cpp @@ -24,6 +24,7 @@ Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA. //----------------------------------------------------------------------------// #include "Random.h" +#include "CmodError.h" //----------------------------------------------------------------------------// @@ -98,14 +99,27 @@ double Random::Rand(Random::distribution_type distribution) { //----------------------------------------------------------------------------// int Random::RandInt(int lowNum, int highNum) { - int range = (highNum - lowNum) + 1; - int result = lowNum + (int)( range * Rand() ); - return result; + if (lowNum > highNum) { + throw CmodError(CmodError::Kind::Project, + "RandomInt lower bound " + std::to_string(lowNum) + + " exceeds upper bound " + std::to_string(highNum) + ".", + "Function: RandomInt -> Low/High", + "Set the lower bound to a value less than or equal to the upper bound."); + } + const long long range = static_cast(highNum) - lowNum + 1; + return static_cast(lowNum + static_cast(range * Rand())); } //----------------------------------------------------------------------------// int Random::RandOrderInt(int low, int high, int id) { + if (low > high) { + throw CmodError(CmodError::Kind::Project, + "RandomOrderInt lower bound " + std::to_string(low) + + " exceeds upper bound " + std::to_string(high) + ".", + "Function: RandomOrderInt -> Low/High", + "Set the lower bound to a value less than or equal to the upper bound."); + } static map > choicesMap; // Initialize choices if a new random order function is found, or @@ -158,13 +172,11 @@ int Random::ChooseFromProb(vector probs) { } } - std::cerr << "Error in Random::ChooseFromProb" << std::endl; - std::cerr << " probs.size()=" << probs.size() << ", randNum=" << randomNumber << std::endl; - std::cerr << " probs=< "; - for (unsigned i = 0; i < probs.size(); i++) - std::cerr << probs[i] << " "; - std::cerr << ">" << std::endl; - exit(1); + throw CmodError(CmodError::Kind::Internal, + "Generated probability table of " + std::to_string(probs.size()) + + " entries cannot select random value " + std::to_string(randomNumber) + ".", + "Random selection -> cumulative probabilities", + "Report this problem with the project and seed to the DISSCO developers."); } //----------------------------------------------------------------------------// @@ -196,8 +208,8 @@ double Random::PreferedValueDistribution(double value, double checkPoint) { vector Random::InitializeChoices(int low, int high) { vector choices; - for (int i = low; i <= high; i++) { - choices.push_back(i); + for (long long i = low; i <= high; i++) { + choices.push_back(static_cast(i)); } return choices; } diff --git a/CMOD/src/Section.cpp b/CMOD/src/Section.cpp index 91e5f011..ad0dec1a 100644 --- a/CMOD/src/Section.cpp +++ b/CMOD/src/Section.cpp @@ -1,4 +1,5 @@ #include "Section.h" +#include "CmodError.h" string Section::prev_loudness; @@ -263,8 +264,10 @@ int Section::CalculateEDUsFromSecondsInTempo(float seconds) { void Section::Build(bool notate_time_signature) { if (!is_built_) { if (is_edu_limit_ && remaining_edus_ == 0) { - cerr << "Section cannot be built without exact edu allotment" << endl; - exit(1); + throw CmodError(CmodError::Kind::Project, + "Two tempo sections start too close together to leave a notatable duration.", + "Score section starting at " + to_string(GetStartTimeGlobal()) + " seconds", + "Separate the tempo changes by at least one EDU, or use a finer EDU Per Beat setting."); } section_flat_.clear(); @@ -300,8 +303,10 @@ const list& Section::GetSectionFlat() { return section_flat_; } - cerr << "Cannot get flattened unbuilt section!" << endl; - exit(1); + throw CmodError(CmodError::Kind::Internal, + "Score output requested a section before it was built.", + "Score section starting at " + to_string(GetStartTimeGlobal()) + " seconds", + "Report this error to the DISSCO developers with the project file, seed, and full output."); } bool Section::operator<(const TimeSignature& time_signature) const { @@ -467,6 +472,13 @@ void Section::Notate() { void Section::CapEnding() { int cur_bar_edus = 0; list last_bar = PopLastBarNotes(); + + if (last_bar.empty()) { + throw CmodError(CmodError::Kind::Project, + "No notes with a notatable duration remain before this tempo transition.", + "Score section starting at " + to_string(GetStartTimeGlobal()) + " seconds", + "Check note durations and EDU settings. Give this section positive note durations, or disable score output."); + } if (!last_bar.empty()) { cur_bar_edus = last_bar.back()->end_t - last_bar.front()->start_t; @@ -478,12 +490,15 @@ void Section::CapEnding() { int total_edus_to_use = remaining_edus_ + cur_bar_edus; if (remaining_edus_ < 0) { - cerr << "Sections overlap" << endl; - exit(1); + throw CmodError(CmodError::Kind::Project, + "Notes extend " + to_string(-remaining_edus_) + " EDU past the next tempo section.", + "Score section starting at " + to_string(GetStartTimeGlobal()) + " seconds", + "Shorten the preceding notes or move the next tempo change later so score sections do not overlap."); } else if (remaining_edus_ == 0 && cur_bar_edus == 0) { return; // Sections align perfectly! } else { int pow_2 = 0; + int best_pow_2 = 0; int min_err = INT_MAX; int ts_num, ts_den; while (time_signature_.beat_edus_ % TimeSignature::Power(2, pow_2) == 0) { @@ -492,6 +507,7 @@ void Section::CapEnding() { ts_num = total_edus_to_use / tmp_beat_edus; ts_den = time_signature_.unit_note_ * TimeSignature::Power(2, pow_2); min_err = 0; + best_pow_2 = pow_2; break; // Overhanging time forms a dyadic time signature } else { // Form a dyadic time signature by adding sound or rest with the least error @@ -501,12 +517,13 @@ void Section::CapEnding() { ts_num = num_beats; // Add time ts_den = time_signature_.unit_note_ * TimeSignature::Power(2, pow_2); min_err = err; + best_pow_2 = pow_2; } } ++pow_2; } - int beat_divisor = TimeSignature::Power(2, pow_2); + int beat_divisor = TimeSignature::Power(2, best_pow_2); Tempo new_tempo(time_signature_.tempo_); new_tempo.setEDUPerTimeSignatureBeat(time_signature_.beat_edus_ / beat_divisor); @@ -518,30 +535,34 @@ void Section::CapEnding() { cap_->SetDurationEDUS(-1); int offset = last_bar.front()->start_t; + Note* last_note = last_bar.back(); while (!last_bar.empty()) { // Make notes in the cap start from 0 while preserving the original // attack of every individual pitch in a grouped chord. last_bar.front()->shiftEDUs(-offset); if (!cap_->InsertNote(last_bar.front())) { - cerr << "Note could not be inserted into end cap. " << - "This should not happen under any circumstance." << endl; - exit(1); + throw CmodError(CmodError::Kind::Internal, + "A note could not be assigned to the end of its tempo section.", + "Score section starting at " + to_string(GetStartTimeGlobal()) + " seconds", + "Report this error to the DISSCO developers with the project file, seed, and full output."); } last_bar.pop_front(); } if (min_err != 0 && remaining_edus_ == 0) { // No extra time and leftover sound does not fill time signature - Note* extra_space = new Note(*last_bar.back()); // TODO - what if we get a tie over the last bar - extra_space->start_t = last_bar.back()->start_t; + Note* extra_space = new Note(*last_note); // TODO - what if we get a tie over the last bar + extra_space->start_t = last_note->end_t; extra_space->end_t = extra_space->start_t + min_err; extra_space->split = 1; cap_->InsertNote(extra_space); } if (min_err != 0) { - cout << Note::int_to_str(new_tempo.calculateSecondsFromEDUs(min_err)) - << " seconds added to stitch sections." << endl; + cout << "Warning: Score section starting at " << GetStartTimeGlobal() + << " seconds was extended by " << new_tempo.calculateSecondsFromEDUs(min_err) + << " seconds to fit a notatable time signature. " + << "Suggestion: Align tempo changes to the timing grid if this extension is not intended." << endl; } // Only notate time signature if different @@ -768,7 +789,8 @@ list Section::PopFirstBar() { list::iterator note_iter = section_flat_.begin(); int num_items_in_bar = 0; bool first_barline_seen = false; - while ((note_iter != section_flat_.end() && !first_barline_seen) || (*note_iter)->type != NoteType::kBarline) { + while (note_iter != section_flat_.end() && + (!first_barline_seen || (*note_iter)->type != NoteType::kBarline)) { bar.push_back(*note_iter); if ((*note_iter)->type == NoteType::kBarline) @@ -777,9 +799,11 @@ list Section::PopFirstBar() { ++note_iter; ++num_items_in_bar; } - if (!first_barline_seen) { - cerr << "Could not locate first bar in section" << endl; - exit(1); + if (!first_barline_seen || note_iter == section_flat_.end()) { + throw CmodError(CmodError::Kind::Internal, + "A score section is missing a complete bar during a tempo transition.", + "Score section starting at " + to_string(GetStartTimeGlobal()) + " seconds", + "Report this error to the DISSCO developers with the project file, seed, and full output."); } // Remove the first bar from section_flat_ @@ -797,9 +821,10 @@ list Section::PopLastBarNotes() { Note* last_barline = 0; list::iterator note_iter = section_flat_.begin(); - list::iterator next = ++section_flat_.begin(); int num_items_in_bar = 0; - for (; note_iter != section_flat_.end(); ++note_iter, ++next) { + for (; note_iter != section_flat_.end(); ++note_iter) { + list::iterator next = note_iter; + ++next; Note* note = *note_iter; if (note->type == NoteType::kNote) { @@ -817,8 +842,10 @@ list Section::PopLastBarNotes() { } if (last_barline == 0) { - cerr << "Could not locate last bar for stitching" << endl; - exit(1); + throw CmodError(CmodError::Kind::Project, + "No notatable bar was generated before a tempo transition.", + "Score section starting at " + to_string(GetStartTimeGlobal()) + " seconds", + "Check that the section contains notes with positive durations compatible with its EDU and time signature settings."); } // Remove the last bar from section_flat_ diff --git a/CMOD/src/Sieve.cpp b/CMOD/src/Sieve.cpp index 136a4a0b..9f6f7e9c 100644 --- a/CMOD/src/Sieve.cpp +++ b/CMOD/src/Sieve.cpp @@ -27,6 +27,8 @@ Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA. #include "Sieve.h" #include "Random.h" #include "ModParser.h" +#include "CmodError.h" +#include //---------------------------------------------------------------------------// Sieve::Sieve() { @@ -60,6 +62,13 @@ void Sieve::BuildFromExpr(int minVal, int maxVal, ModParser mp(offsetVect); mp.parseExpr(expr, minVal, maxVal); eList = mp.getElements(); + if (eList.empty()) { + throw CmodError(CmodError::Kind::Project, + "Sieve has no elements within range " + std::to_string(minVal) + + " through " + std::to_string(maxVal) + ".", + "Sieve -> MODS expression: " + expr, + "Widen the range or adjust the moduli and offsets so the sieve contains at least one element."); + } //Sieve::print_eList(); Sieve::Weights(mp.getMods(), wMethod, wArgVect, mp.getOffsets()); } @@ -72,6 +81,13 @@ void Sieve::Build(int minVal, int maxVal, vector eArgVect, vector wArgVect, vector offsetVect) { Sieve::Elements(minVal, maxVal, eMethod, eArgVect, offsetVect); + if (eList.empty()) { + throw CmodError(CmodError::Kind::Project, + "Sieve has no elements within range " + std::to_string(minVal) + + " through " + std::to_string(maxVal) + ".", + "Sieve -> Elements", + "Widen the range or adjust the element list and offsets so the sieve contains at least one element."); + } Sieve::Weights(eArgVect, wMethod, wArgVect, offsetVect); } @@ -79,6 +95,13 @@ void Sieve::Build(int minVal, int maxVal, //---------------------------------------------------------------------------// void Sieve::FillInVectors(vector& intVect, vector& doubleVect) { + if (eList.size() != wList.size()) { + throw CmodError(CmodError::Kind::Project, + "Sieve has " + std::to_string(eList.size()) + " elements but " + + std::to_string(wList.size()) + " weights.", + "Sieve -> Weights", + "Provide a weight for each retained element, or use PERIODIC weights."); + } //cout << "Sieve::FillInVectors - eList.size()=" << eList.size() << " wList.size()=" << wList.size() << endl; @@ -129,6 +152,13 @@ int Sieve::Modify(Envelope *env, string method) { //---------------------------------------------------------------------------// int Sieve::ChooseL() { + if (eList.empty() || eList.size() != wList.size()) { + throw CmodError(CmodError::Kind::Project, + "Sieve cannot choose from " + std::to_string(eList.size()) + " elements and " + + std::to_string(wList.size()) + " weights.", + "Function: ChooseL -> Sieve", + "Provide at least one element and a weight for each element."); + } double randomNumber = Random::Rand(); list::iterator eIter = eList.begin(); @@ -138,6 +168,13 @@ int Sieve::ChooseL() { eIter++; wIter++; } + if (eIter == eList.end() || !std::isfinite(*wIter) || *wIter < 0) { + throw CmodError(CmodError::Kind::Project, + "Sieve weights do not provide a valid selection for random value " + + std::to_string(randomNumber) + ".", + "Function: ChooseL -> Sieve weights", + "Use finite nonnegative weights that cover the full selection range; a cumulative weight list must end at 1 or greater."); + } return *eIter; } @@ -148,25 +185,25 @@ int Sieve::ChooseL() { void Sieve::Elements(int minVal, int maxVal, const char *method, vector eArgVect, std::vector offsetVect) { + if ((strcmp(method, "MEANINGFUL") == 0 || strcmp(method, "MODS") == 0) + && offsetVect.size() < eArgVect.size()) { + throw CmodError(CmodError::Kind::Project, + "Sieve has " + std::to_string(eArgVect.size()) + " elements/moduli but only " + + std::to_string(offsetVect.size()) + " offsets.", + "Sieve -> Offsets", + "Provide one offset per element or modulus (use 0 when no offset is needed)."); + } if(strcmp(method, "MEANINGFUL") == 0) { //only meaningful elem. Sieve::Meaningful(minVal, maxVal, eArgVect, offsetVect); } else if(strcmp(method, "MODS") == 0) { //uses moduli Sieve::Multiples(minVal, maxVal, eArgVect, offsetVect); } else if(strcmp(method, "FAKE") == 0) { //all elem, same weight Sieve::Fake(minVal, maxVal); - } else if(strcmp(method, "FIBONACCI") == 0) { //Fibonacci sieve - cerr << " see harmSieve" << endl; - exit(1); - } else if(strcmp(method, "OVERTONES") == 0) { //overtone series - cerr << "utility::SieveElements - overtones not available yet" << endl; - exit(1); - } else if(strcmp(method, "MULT_PARAMS") == 0) { //multiple parameters - cerr << "utility::SieveElements - multiple params not available yet" - << endl; - exit(1); } else { - cerr << "no method to build sieve: "<< method << endl; - exit(1); + throw CmodError(CmodError::Kind::Project, + "Sieve element method '" + string(method) + "' is not supported.", + "Sieve -> Method", + "Choose MEANINGFUL, MODS, or FAKE in the sieve editor."); } } @@ -185,8 +222,10 @@ void Sieve::Weights(std::vector eArgVect, } else if(strcmp(method, "INCLUDE") == 0) { Sieve::IncludeWeights(wArgVect); } else { - cerr << "Sieve::Weights - no method for asigning weights" << endl; - exit(1); + throw CmodError(CmodError::Kind::Project, + "Sieve weight method '" + string(method) + "' is not supported.", + "Sieve -> WeightMethod", + "Choose PERIODIC, HIERARCHIC, or INCLUDE in the sieve editor."); } } @@ -299,6 +338,20 @@ void Sieve::PeriodicWeights(const vector& wArgVect) { void Sieve::HierarchicWeights(const std::vector& eArgVect, std::vector wArgVect, std::vector offsetVect) { + if (wArgVect.size() > eArgVect.size() || wArgVect.size() > offsetVect.size()) { + throw CmodError(CmodError::Kind::Project, + "Sieve HIERARCHIC weights do not have matching moduli and offsets.", + "Sieve -> HIERARCHIC weights", + "Provide a modulus and offset for every hierarchical weight."); + } + for (size_t i = 0; i < wArgVect.size(); ++i) { + if (eArgVect[i] == 0) { + throw CmodError(CmodError::Kind::Project, + "Sieve HIERARCHIC modulus " + std::to_string(i + 1) + " is 0.", + "Sieve -> HIERARCHIC weights", + "Use nonzero moduli, or choose PERIODIC/INCLUDE weights for an element list containing 0."); + } + } unsigned whichMod; double probability; @@ -333,6 +386,13 @@ cout << "Sieve::Hierarchic - eList.end=" << eList.end() << " eArgVect.size()=" //---------------------------------------------------------------------------// void Sieve::IncludeWeights(const vector& wArgVect) { + if (wArgVect.size() < eList.size()) { + throw CmodError(CmodError::Kind::Project, + "Sieve INCLUDE has " + std::to_string(wArgVect.size()) + " weights for " + + std::to_string(eList.size()) + " elements.", + "Sieve -> INCLUDE weights", + "Provide a weight for every retained element, or choose PERIODIC weights."); + } //for(int i = 0; i < wArgVect.size(); i++) { for(unsigned i = 0; i < eList.size(); i++) { if((int)i >= skip && i < eList.size() + skip) { diff --git a/CMOD/src/SignalHandlers.cpp b/CMOD/src/SignalHandlers.cpp index 662ab4cd..49a676cc 100644 --- a/CMOD/src/SignalHandlers.cpp +++ b/CMOD/src/SignalHandlers.cpp @@ -1,15 +1,30 @@ #include "SignalHandlers.h" void segfaultHandler(int signal) { + static const char diagnostic[] = + "CMOD internal error: Unexpected invalid memory access (segmentation fault).\n" + "Context: CMOD runtime\n" + "Suggestion: Send the DISSCO developers the project file, seed, and full output. " + "This crash alone does not identify an invalid project setting.\n" + "Build failed.\n"; +#ifdef _WIN32 + _write(STDERR_FILENO, diagnostic, sizeof(diagnostic) - 1); +#else + write(STDERR_FILENO, diagnostic, sizeof(diagnostic) - 1); +#endif void *buf[BACKTRACE_NUM + 2]; size_t size = backtrace(buf, BACKTRACE_NUM + 2); // Do a backtrace of the stack char **messages = backtrace_symbols(buf, size); std::cerr << "--------------------------------------------------------------------------------\n"; - std::cerr << "Segmentation Fault, printing stacktrace of " << BACKTRACE_NUM << " most recent function calls:\n\n"; + if (size > 2 && messages != nullptr) { + std::cerr << "Stack trace (up to " << BACKTRACE_NUM << " most recent function calls):\n\n"; + } else { + std::cerr << "A stack trace is not available on this platform.\n"; + } - for (size_t i = 2; i < size; ++i) { //Skip the first two frames since they are this function and the signal generator + for (size_t i = 2; messages != nullptr && i < size; ++i) { //Skip the first two frames since they are this function and the signal generator size_t len=std::strlen(messages[i]); char* parser; @@ -64,4 +79,4 @@ void terminateHandler() { // Unimplemented void abortHandler() { exit(1); -} \ No newline at end of file +} diff --git a/CMOD/src/Utilities.cpp b/CMOD/src/Utilities.cpp index 2fd2229d..f99fd3b1 100644 --- a/CMOD/src/Utilities.cpp +++ b/CMOD/src/Utilities.cpp @@ -37,11 +37,74 @@ Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA. #include "Patter.h" #include "ProbabilityEnvelope.h" // consider moving this into LASS.h #include +#include #include +#include #include +#include #include #include +static int checkedEnvelopeNumber(double number, int size, + const string& function) { + if (number < 1 || number >= static_cast(size) + 1) { + throw CmodError(CmodError::Kind::Project, + function + " envelope number " + to_string(number) + + " is outside the library of " + to_string(size) + " envelopes.", + "Function: " + function + " -> Envelope number", + "Choose an existing envelope from the library. Envelope numbers start at 1; add an envelope if the library is empty."); + } + return static_cast(number); +} + +class ObjectReferenceGuard { +public: + ObjectReferenceGuard(std::vector& references, + pugi::xml_node node, const string& kind, const string& name) + : references_(references) { + if (std::find(references.begin(), references.end(), node) != references.end()) { + throw CmodError(CmodError::Kind::Project, + "A circular " + kind + " reference reaches '" + name + "' again.", + kind + " object: " + name, + "Remove the reference back to this object so its definition can be evaluated without a cycle."); + } + references_.push_back(node); + } + ~ObjectReferenceGuard() { references_.pop_back(); } +private: + std::vector& references_; +}; + +static string requiredFunctionArgument(pugi::xml_node node, + const string& function, const string& field) { + const string expression = Utilities::XMLTranscode(node); + if (!node || expression.find_first_not_of(" \t\r\n") == string::npos) { + throw CmodError(CmodError::Kind::Project, + function + " is missing its " + field + " argument.", + "Function: " + function + " -> " + field, + "Enter a value or expression for this argument in the function editor."); + } + return expression; +} + +static int checkedIntegerArgument(double value, const string& function, + const string& field) { + const double integer = std::trunc(value); + if (!std::isfinite(value) || integer < std::numeric_limits::min() + || integer > std::numeric_limits::max()) { + std::ostringstream originalValue; + originalValue << std::setprecision(std::numeric_limits::max_digits10) << value; + throw CmodError(CmodError::Kind::Project, + function + " " + field + " value " + originalValue.str() + + " is outside the supported integer range " + + to_string(std::numeric_limits::min()) + " through " + + to_string(std::numeric_limits::max()) + ".", + "Function: " + function + " -> " + field, + "Use a finite value whose integer part is within this range; fractional parts are truncated toward zero."); + } + return static_cast(integer); +} + Utilities::Utilities(pugi::xml_node root, string _workingPath, bool _soundSynthesis, @@ -115,18 +178,61 @@ Utilities::Utilities(pugi::xml_node root, "MarkovModelLibrary: " + countText, "Set the model count to the number of saved Markov models, or resave the project in LASSIE."); } - markovModelLibrary.resize(size); string modelText, line; getline(ss, line, '\n'); for (int i = 0; i < size; i++) { - getline(ss, line, '\n'); + if (!getline(ss, line, '\n')) { + throw CmodError(CmodError::Kind::Project, + "Markov model " + to_string(i) + " is missing from the declared library.", + "MarkovModelLibrary -> model count " + to_string(size), + "Restore the missing model data, or correct the model count by resaving the library in LASSIE."); + } + std::istringstream stateCountInput(line); + int stateCount = 0; + if (!(stateCountInput >> stateCount) || stateCount < 0 + || (stateCountInput >> std::ws, !stateCountInput.eof())) { + throw CmodError(CmodError::Kind::Project, + "Markov model " + to_string(i) + " has an invalid or missing state count.", + "MarkovModelLibrary -> model " + to_string(i), + "Restore the model's state count and data, or resave the model in LASSIE."); + } modelText = line + '\n'; + const auto validateModelLine = [i](const string& text, unsigned long long expected, + const string& field, bool probability) { + std::istringstream values(text); + for (unsigned long long j = 0; j < expected; ++j) { + double value = 0; + if (!(values >> value) || !std::isfinite(value) || (probability && value < 0) + || (!probability && std::abs(value) > std::numeric_limits::max())) { + throw CmodError(CmodError::Kind::Project, + "Markov model " + to_string(i) + " has missing or invalid " + field + + " at entry " + to_string(j + 1) + ".", + "MarkovModelLibrary -> model " + to_string(i) + " -> " + field, + "Provide one finite value per state and a complete matrix of nonnegative transition probabilities in the Markov model editor."); + } + } + values >> std::ws; + if (!values.eof()) { + throw CmodError(CmodError::Kind::Project, + "Markov model " + to_string(i) + " has extra or invalid data after its " + field + ".", + "MarkovModelLibrary -> model " + to_string(i) + " -> " + field, + "Make the model's state count, values, initial probabilities, and transition matrix dimensions agree."); + } + }; + line.clear(); getline(ss, line, '\n'); + validateModelLine(line, stateCount, "state values", false); modelText += line + '\n'; + line.clear(); getline(ss, line, '\n'); + validateModelLine(line, stateCount, "initial probabilities", true); modelText += line + '\n'; + line.clear(); getline(ss, line, '\n'); + validateModelLine(line, static_cast(stateCount) * stateCount, + "transition probabilities", true); modelText += line; + markovModelLibrary.emplace_back(); markovModelLibrary[i].from_str(modelText); markovModelLibrary[i].normalize(); } @@ -750,9 +856,7 @@ Sieve* Utilities::evaluateSieveFunction(string _functionString,void* _object){ string Utilities::static_function_CURRENT_TYPE(void* _object){ if (_object !=NULL){ double resultNum = ((Event*)_object)->getCurrentChildType(); - char result [50]; - sprintf(result, "%f", resultNum); - return string(result); + return to_string(resultNum); } else { cerr<<"Utilities:Warning! static_function_CURRENT_TYPE has no object to look up."<getCurrentChild(); - char result [50]; - sprintf(result, "%f", resultNum); - return string(result); + return to_string(resultNum); } else { cerr<<"Utilities:Warning! static_function_CURRENT_NUM has no object to look up."<(_object)->getCurrPartialNum(); - char result [50]; - sprintf(result, "%f", resultNum); - return string(result); + return to_string(resultNum); } else { cerr<<"Utilities:Warning! static_function_CURRENT_PARTIAL_NUM has no object to look up."<(_object)->getEventName() << "' -> "; + cerr << "Function: CURRENT_DENSITY\n" + << "Suggestion: Replace it with a constant or a supported expression for the desired density.\n"; return "0"; } //----------------------------------------------------------------------------// string Utilities::static_function_CURRENT_SEGMENT(void* _object){ - //should recieve an envelope as the _object? --Ming-ching May 07 2013 - cout<<"Utilities:Warning! static_function_CURRENT_SEGMENT is not implemented in CMOD 2.0 yet."<(_object)->getEventName() << "' -> "; + cerr << "Function: CURRENT_SEGMENT\n" + << "Suggestion: Replace it with a constant or a supported expression for the desired segment.\n"; return "0"; } @@ -812,9 +919,7 @@ string Utilities::static_function_CURRENT_SEGMENT(void* _object){ string Utilities::static_function_AVAILABLE_EDU(void* _object){ if (_object !=NULL){ double resultNum = ((Event*)_object)->getAvailableEDU(); - char result [50]; - sprintf(result, "%f", resultNum); - return string(result); + return to_string(resultNum); } else { cerr<<"Utilities:Warning! static_function_AVAILABLE_EDU has no object to look up."<getCurrentLayer(); - char result [50]; - sprintf(result, "%f", resultNum); - return string(result); + return to_string(resultNum); } else { cerr<<"Utilities:Warning! static_function_CURRENT_LAYER has no object to look up."<getPreviousChildEndTime(); - char result [50]; - sprintf(result, "%f", resultNum); // cout << "Utilities::static_function_PREVIOUS_CHILD_DURATION - resultNum=" // << resultNum << endl; - return string(result); + return to_string(resultNum); } else { cerr<<"Utilities:Warning! static_function_PREVIOUS_CHILD_DURATION has no object to look up."<= markovModelLibrary.size()) { + throw CmodError(CmodError::Kind::Project, + "Markov model index " + to_string(modelIndex) + + " is outside the library of " + to_string(markovModelLibrary.size()) + " models.", + "Function: Markov -> model index", + "Choose a saved Markov model in LASSIE. Model indices start at 0; add a model if the library is empty."); + } + const size_t entry = static_cast(modelIndex); + if (markovModelLibrary[entry].getStateSize() == 0) { + throw CmodError(CmodError::Kind::Project, + "Markov model " + to_string(entry) + " has no states to sample.", + "Function: Markov -> model index", + "Add states and probabilities to this model, or select a populated model."); + } float resultNum = markovModelLibrary[entry].nextSample(Random::Rand()); - char result [50]; - sprintf(result, "%f", resultNum); - return string(result); + return to_string(resultNum); } //----------------------------------------------------------------------------// @@ -888,16 +999,22 @@ string Utilities::function_LN(pugi::xml_node _functionElement, void* _object){ double entry = evaluate(XMLTranscode(elementIter ),_object); double resultNum = ( 1. / pow(2.71828, entry) ); - char result [50]; - sprintf(result, "%f", resultNum); - return string(result); + return to_string(resultNum); } //----------------------------------------------------------------------------// string Utilities::function_Fibonacci(pugi::xml_node _functionElement, void* _object){ pugi::xml_node elementIter = GNES(GFEC(_functionElement)); - int entry = evaluate(XMLTranscode(elementIter ),_object); + const double value = evaluate(XMLTranscode(elementIter), _object); + if (value >= 47) { + throw CmodError(CmodError::Kind::Project, + "Fibonacci entry " + to_string(value) + " exceeds the supported maximum of 46.", + "Function: Fibonacci -> Entry", + "Use an entry at most 46; larger Fibonacci values exceed CMOD's integer range."); + } + if (value <= 2) return "1"; + const int entry = static_cast(value); int numA = 1; int numB = 1; @@ -907,9 +1024,7 @@ string Utilities::function_Fibonacci(pugi::xml_node _functionElement, void* _obj numA = swap; } int resultNum = numB; - char result [50]; - sprintf(result, "%i", resultNum); - return string(result); + return to_string(resultNum); } //----------------------------------------------------------------------------// @@ -942,11 +1057,14 @@ string Utilities::function_Decay(pugi::xml_node _functionElement, void* _object) decay = base * pow(rate, index); } else if (type == "LINEAR") { decay = base - (rate * index); + } else { + throw CmodError(CmodError::Kind::Project, + "Decay type '" + type + "' is not supported.", + "Function: Decay -> Type", + "Choose EXPONENTIAL or LINEAR in the Decay function editor."); } - char result [50]; - sprintf(result, "%f", decay); - return string(result); + return to_string(decay); } //----------------------------------------------------------------------------// @@ -978,19 +1096,29 @@ string Utilities::function_Stochos(pugi::xml_node _functionElement, void* _objec pugi::xml_node elementIter = GNES(GFEC(_functionElement)); string method = XMLTC(elementIter); + if (method != "FUNCTIONS" && method != "RANGE_DISTRIB") { + throw CmodError(CmodError::Kind::Project, + "Stochos method '" + method + "' is not supported.", + "Function: Stochos -> Method", + "Choose FUNCTIONS or RANGE_DISTRIB in the Stochos function editor."); + } elementIter = GNES(elementIter); pugi::xml_node envElementIter = GFEC(elementIter); - vector envVect; + vector> envVect; while (envElementIter!=NULL) { - //cou << MLTC(envElementIter)< Envelopes", + "Add at least one envelope for FUNCTIONS, or three envelopes per RANGE_DISTRIB group."); + } elementIter = GNES(elementIter); - int offset = (int) evaluate ( XMLTC(elementIter), _object); + const double offsetValue = evaluate(XMLTC(elementIter), _object); float returnVal = 0.0; if(method == "FUNCTIONS") { @@ -1011,44 +1139,26 @@ string Utilities::function_Stochos(pugi::xml_node _functionElement, void* _objec float limit[2]; // distribution within given range; takes 3 envs: min, MAX, val in between - if((int)envVect.size() <= 3 * offset) { - cerr << "Error - Stochos - Not enough envelopes on the list: envVect.size=" - << envVect.size() << " 3*offset=" << 3 * offset << endl; - if (_object != NULL) { - cerr << " in file " << ((Event*)_object)->getEventName() << endl; - } - exit(1); + if (offsetValue < 0 || offsetValue >= envVect.size() / 3) { + throw CmodError(CmodError::Kind::Project, + "Stochos offset " + to_string(offsetValue) + + " does not select a complete group of 3 envelopes from the list of " + + to_string(envVect.size()) + " envelopes.", + "Function: Stochos -> RANGE_DISTRIB -> Offset", + "Use a nonnegative group offset (starting at 0) and supply minimum, maximum, and distribution envelopes for that group."); } + const size_t offset = static_cast(offsetValue); for(int i = 0; i < 2; i++) { - if(envVect[3 * offset + i] != NULL) { - limit[i] = envVect[3 * offset + i]->getValue(checkpoint, 1); - } else { - cerr << "Stochos - NULL envelope. Trying to access envy[" - << 3 * offset + i<< "]=" << envVect[3 * offset + i] << endl; - } + limit[i] = envVect[3 * offset + i]->getValue(checkpoint, 1); } - if(envVect[3 * offset + 2]) { - returnVal = envVect[3 * offset + 2]->getValue(Random::Rand(), 1); - } else { - cerr << "Stochos - NULL envelope. Trying to access envVect[" - << 3 * offset + 2 << "]=" << envVect[ 3 * offset + 2] << endl; - } + returnVal = envVect[3 * offset + 2]->getValue(Random::Rand(), 1); returnVal *= (limit[1] - limit[0]); returnVal += limit[0]; - } else { - cerr << "Stochos --- invalid method! Use FUNCTIONS or RANGE_DISTRIB" << endl; - exit(1); } - for (unsigned i = 0; i < envVect.size(); i ++){ - delete envVect[i]; - } - - char result [50]; - sprintf(result, "%f", returnVal); - return string(result); + return to_string(returnVal); } @@ -1058,9 +1168,7 @@ string Utilities::function_ValuePick(pugi::xml_node _functionElement, void* _obj Sieve* si = sieve_ValuePick(_functionElement, _object); int resultNum = si->ChooseL(); delete si; - char result [50]; - sprintf(result, "%i", resultNum); - return string(result); + return to_string(resultNum); } //----------------------------------------------------------------------------// @@ -1197,9 +1305,7 @@ string Utilities::function_ChooseL(pugi::xml_node _functionElement, void* _objec Sieve* svPtr = (Sieve*)evaluateObject(sivFunctionString, _object, eventSiv); double resultNum = svPtr->ChooseL(); delete svPtr; - char result [50]; - sprintf(result, "%f", resultNum); - return string(result); + return to_string(resultNum); } //----------------------------------------------------------------------------// @@ -1246,9 +1352,7 @@ string Utilities::function_Randomizer(pugi::xml_node _functionElement, void* _ob double devVal = baseVal * percDev; double resultNum = Random::Rand(baseVal - devVal, baseVal + devVal); - char result [50]; - sprintf(result, "%f", resultNum); - return string(result); + return to_string(resultNum); } @@ -1259,12 +1363,10 @@ string Utilities::function_Random(pugi::xml_node _functionElement, void* _object pugi::xml_node lowBoundElement = GNES(GFEC(_functionElement)); pugi::xml_node highBoundElement = GNES(lowBoundElement); - double lowBound = evaluate(XMLTranscode(lowBoundElement ),_object); - double highBound = evaluate(XMLTranscode(highBoundElement), _object); + double lowBound = evaluate(requiredFunctionArgument(lowBoundElement, "Random", "Low"), _object); + double highBound = evaluate(requiredFunctionArgument(highBoundElement, "Random", "High"), _object); - char result [50]; - sprintf(result, "%f", Random::Rand(lowBound, highBound)); - return string(result); + return to_string(Random::Rand(lowBound, highBound)); } @@ -1285,24 +1387,17 @@ string Utilities::function_Select(pugi::xml_node _functionElement, void* _object pugi::xml_node listElement = GNES(GFEC(_functionElement)); pugi::xml_node indexElement = GNES(listElement); + if (XMLTranscode(listElement).find_first_not_of(" \t\r\n") == string::npos) { + throw CmodError(CmodError::Kind::Project, + "Select cannot choose from an empty list.", + "Function: Select -> List", + "Add at least one value or object to the Select list."); + } std::vector list = listElementToStringVector(listElement); - const size_t index = checkedSelectIndex(evaluate(XMLTranscode(indexElement), _object), list.size()); - char result [50]; -/* -for(int i=0; i List", + "Add at least one value or object to the Select list."); + } std::vector list = listElementToStringVector(listElement); - const size_t index = checkedSelectIndex(evaluate(XMLTranscode(indexElement), _object), list.size()); + const size_t index = checkedSelectIndex(evaluate(requiredFunctionArgument(indexElement, "Select", "Index"), _object), list.size()); return list[index]; } @@ -1348,34 +1449,34 @@ string Utilities::function_GetPattern(pugi::xml_node _functionElement, void* _ob } double returnValue = pattern->GetNextValue(method, origin); - char result [50]; - sprintf(result, "%f", returnValue); - return string(result); + return to_string(returnValue); } //----------------------------------------------------------------------------/ string Utilities::function_RandomInt(pugi::xml_node _functionElement, void* _object){ - pugi::xml_node lowBoundElement = GNES(GFEC(_functionElement)); - pugi::xml_node highBoundElement = GNES(lowBoundElement); - - int lowBound = (int)evaluate(XMLTranscode(lowBoundElement), _object); - int highBound = (int)evaluate(XMLTranscode(highBoundElement), _object); - char result [50]; - sprintf(result, "%d", Random::RandInt(lowBound, highBound)); - return string(result); + pugi::xml_node lowBoundElement = _functionElement.child("Low"); + pugi::xml_node highBoundElement = _functionElement.child("High"); + + const int lowBound = checkedIntegerArgument( + evaluate(requiredFunctionArgument(lowBoundElement, "RandomInt", "Low"), _object), "RandomInt", "Low"); + const int highBound = checkedIntegerArgument( + evaluate(requiredFunctionArgument(highBoundElement, "RandomInt", "High"), _object), "RandomInt", "High"); + return to_string(Random::RandInt(lowBound, highBound)); } //---------------------------------------------------------------------------// string Utilities::function_RandomOrderInt(pugi::xml_node _functionElement, void* _object) { - pugi::xml_node lowBoundElement = GNES(GFEC(_functionElement)); - pugi::xml_node highBoundElement = GNES(lowBoundElement); + pugi::xml_node lowBoundElement = _functionElement.child("Low"); + pugi::xml_node highBoundElement = _functionElement.child("High"); pugi::xml_node idElement = GNES(highBoundElement); - int lowBound = (int)evaluate(XMLTranscode(lowBoundElement), _object); - int highBound = (int)evaluate(XMLTranscode(highBoundElement), _object); + const int lowBound = checkedIntegerArgument( + evaluate(requiredFunctionArgument(lowBoundElement, "RandomOrderInt", "Low"), _object), "RandomOrderInt", "Low"); + const int highBound = checkedIntegerArgument( + evaluate(requiredFunctionArgument(highBoundElement, "RandomOrderInt", "High"), _object), "RandomOrderInt", "High"); int id = (int) evaluate(XMLTranscode(idElement), _object); // Event* currentEvent = ((Event*)_object); @@ -1391,9 +1492,7 @@ string Utilities::function_RandomOrderInt(pugi::xml_node _functionElement, void* // << endl; // } - char result [50]; - sprintf(result, "%d", Random::RandOrderInt(lowBound, highBound, id)); - return string(result); + return to_string(Random::RandOrderInt(lowBound, highBound, id)); } //---------------------------------------------------------------------------// @@ -1403,7 +1502,8 @@ string Utilities::function_RandomDensity(pugi::xml_node _functionElement, void* pugi::xml_node lowBoundElement = GNES(envelopeNumberElement); pugi::xml_node highBoundElement = GNES(lowBoundElement); - int envelopeNumber = (int)evaluate(XMLTranscode(envelopeNumberElement), _object); + const int envelopeNumber = checkedEnvelopeNumber( + evaluate(XMLTranscode(envelopeNumberElement), _object), envelopeLibrary->size(), "RandomDensity"); double lowBound = evaluate(XMLTranscode(lowBoundElement), _object); double highBound = evaluate(XMLTranscode(highBoundElement), _object); @@ -1416,9 +1516,7 @@ string Utilities::function_RandomDensity(pugi::xml_node _functionElement, void* double resultNumber = env.sample(rand) * (highBound - lowBound) + lowBound; // cout << "lowbound: " << lowBound << ", highbound: " << highBound << ", result: " << resultNumber << endl; - char result [50]; - sprintf(result, "%lf", resultNumber); - return string(result); + return to_string(resultNumber); } //----------------------------------------------------------------------------// @@ -1451,6 +1549,7 @@ Sieve* Utilities::getSieveHelper(void* _object, pugi::xml_node _SIVFunction){ if (XMLTranscode(functionNameElement).compare("ReadSIVFile")==0){ string fileName = XMLTranscode(GNES(functionNameElement)); pugi::xml_node k = getEventElement(eventSiv, fileName); + ObjectReferenceGuard reference(resolvingObjectReferences, k, "sieve", fileName); return getSieveHelper(_object, GNES(GNES(GFEC(k)))); } @@ -1548,8 +1647,10 @@ Sieve* Utilities::getSieveHelper(void* _object, pugi::xml_node _SIVFunction){ } // Otherwise, the function fails. - cerr<<"Utilities::Warning! Sieve Construction Failed"<3 // 1.0 - int envelopeNumber = evaluate(XMLTranscode(_functionElement), _object); + const int envelopeNumber = checkedEnvelopeNumber( + evaluate(XMLTranscode(_functionElement), _object), envelopeLibrary->size(), "EnvLib"); Envelope* env = envelopeLibrary->getEnvelope(envelopeNumber); //cout <<"EnvLib: #"<object name pugi::xml_node file = getEventElement(eventEnv, XMLTranscode(_functionElement)); + ObjectReferenceGuard reference(resolvingObjectReferences, file, "envelope", XMLTranscode(_functionElement)); // // 6 @@ -2025,6 +2135,33 @@ Envelope* Utilities::makeEnvelope(pugi::xml_node _functionElement, void* _object double scale = evaluate(XMLTranscode(GNES(_functionElement)), _object); + const auto childCount = [](pugi::xml_node node) { + size_t count = 0; + for (; node; node = node.next_sibling()) { + if (node.type() == pugi::node_element) ++count; + } + return count; + }; + const size_t xCount = childCount(x); + const size_t yCount = childCount(y); + const size_t typeCount = childCount(t); + const size_t propertyCount = childCount(p); + if (xCount < 2 || xCount != yCount) { + throw CmodError(CmodError::Kind::Project, + "MakeEnvelope has " + to_string(xCount) + " X points and " + + to_string(yCount) + " Y points.", + "Function: MakeEnvelope -> Xs/Ys", + "Provide at least two matching X/Y point pairs in the envelope editor."); + } + if (typeCount != xCount - 1 || propertyCount != xCount - 1) { + throw CmodError(CmodError::Kind::Project, + "MakeEnvelope has " + to_string(xCount) + " points, " + + to_string(typeCount) + " segment interpolation types, and " + + to_string(propertyCount) + " segment length properties.", + "Function: MakeEnvelope -> Types/Pros", + "Provide one interpolation type and one FIXED/FLEXIBLE property for each segment between adjacent points."); + } + // create the collection of points vector points; @@ -2066,8 +2203,10 @@ Envelope* Utilities::makeEnvelope(pugi::xml_node _functionElement, void* _object seg.interType = EXPONENTIAL; } else { - cerr << "Error in MakeEnvelope: Unrecognized interpolation type" << endl; - exit(1); + throw CmodError(CmodError::Kind::Project, + "MakeEnvelope interpolation type '" + XMLTranscode(t) + "' is not supported.", + "Function: MakeEnvelope -> segment " + to_string(segments.size() + 1), + "Choose LINEAR, SPLINE, or EXPONENTIAL for this segment."); } if (XMLTranscode(p).compare("FIXED")==0) { @@ -2077,8 +2216,10 @@ Envelope* Utilities::makeEnvelope(pugi::xml_node _functionElement, void* _object seg.lengthType = FLEXIBLE; } else { - cerr << "Error in MakeEnvelope: Unrecognized envelope length stretch type." << endl; - exit(1); + throw CmodError(CmodError::Kind::Project, + "MakeEnvelope length property '" + XMLTranscode(p) + "' is not supported.", + "Function: MakeEnvelope -> segment " + to_string(segments.size() + 1), + "Choose FIXED or FLEXIBLE for this segment's length property."); } segments.push_back(seg); diff --git a/CMOD/src/Utilities.h b/CMOD/src/Utilities.h index a7cf7012..edca39dc 100644 --- a/CMOD/src/Utilities.h +++ b/CMOD/src/Utilities.h @@ -453,6 +453,7 @@ std::map notesEventnames; // Storage of LASS Parsed/generated Envelopes vector< MarkovModel > markovModelLibrary; + std::vector resolvingObjectReferences; // Piece Configurations bool soundSynthesis = true; diff --git a/LASSIE/src/windows/PostWindow.cpp b/LASSIE/src/windows/PostWindow.cpp index da87f446..73327d2a 100644 --- a/LASSIE/src/windows/PostWindow.cpp +++ b/LASSIE/src/windows/PostWindow.cpp @@ -55,6 +55,7 @@ PostWindow::PostWindow(QProcess *process, QWidget *parent) connect(runProc, &QAction::triggered, this, &PostWindow::runProcess); connect(proc, &QProcess::stateChanged, this, [this, termProc, killProc, runProc]{ + if (proc->state() == QProcess::Starting) stopRequested = false; if(proc->state() != QProcess::NotRunning){ termProc->setEnabled(true); killProc->setEnabled(true); @@ -68,6 +69,8 @@ PostWindow::PostWindow(QProcess *process, QWidget *parent) connect(proc, &QProcess::started, this, [this] { resetProcessOutputState(); + appendColored(QStringLiteral("CMOD executable: %1").arg(proc->program()), + Qt::black); }); connect(proc, &QProcess::finished, this, @@ -79,7 +82,14 @@ PostWindow::PostWindow(QProcess *process, QWidget *parent) runProc->setEnabled(true); const bool succeeded = exitStatus == QProcess::NormalExit && exitCode == 0; - if (succeeded) { + if (stopRequested) { + if (!succeeded) recolorStderr(Qt::red); + appendColored( + QStringLiteral("*** Process exited after your stop request (%1; exit code %2) ***") + .arg(exitStatus == QProcess::CrashExit ? QStringLiteral("abnormal exit") + : succeeded ? QStringLiteral("normal exit") : QStringLiteral("failure")) + .arg(exitCode), Qt::red); + } else if (succeeded) { appendColored( "*** Process exited normally (exit code 0) ***", Qt::black); @@ -92,6 +102,15 @@ PostWindow::PostWindow(QProcess *process, QWidget *parent) "*** Process crashed (abnormal exit; exit code %1) ***") .arg(exitCode); appendColored(summary, Qt::red); + if (exitStatus == QProcess::CrashExit) { + appendColored( + QStringLiteral( + "CMOD stopped unexpectedly before the build completed.\n" + "Suggestion: Check the CMOD executable path above. If the problem persists, " + "send the DISSCO developers the project file, seed, and full output. " + "The exit code alone does not identify the cause."), + Qt::red); + } } }); @@ -99,8 +118,12 @@ PostWindow::PostWindow(QProcess *process, QWidget *parent) [this](QProcess::ProcessError error) { if (error == QProcess::FailedToStart) { appendColored( - QStringLiteral("*** Process failed to start: %1 ***") - .arg(proc->errorString()), + QStringLiteral( + "*** Process failed to start: %1 ***\n" + "CMOD executable: %2\n" + "Suggestion: Check that this executable exists and can be run. " + "Rebuild or reinstall DISSCO if the executable or its required libraries are missing.") + .arg(proc->errorString(), proc->program()), Qt::red); } }); @@ -134,6 +157,7 @@ void PostWindow::closeEvent(QCloseEvent *event) switch(ret){ case QMessageBox::Yes: + stopRequested = true; proc->kill(); break; case QMessageBox::No: @@ -234,12 +258,14 @@ void PostWindow::clearOutput() void PostWindow::termProcess() { + stopRequested = true; proc->terminate(); appendColored("*** User requested process terminate ***", Qt::red); } void PostWindow::killProcess() { + stopRequested = true; proc->kill(); appendColored("*** Process killed by user ***", Qt::red); } diff --git a/LASSIE/src/windows/PostWindow.hpp b/LASSIE/src/windows/PostWindow.hpp index b217367e..be8b86e2 100644 --- a/LASSIE/src/windows/PostWindow.hpp +++ b/LASSIE/src/windows/PostWindow.hpp @@ -33,6 +33,7 @@ private slots: QTextEdit *textEdit; QProcess *proc; bool autoscroll = true; + bool stopRequested = false; QStringDecoder stdoutDecoder{QStringDecoder::Utf8}; QStringDecoder stderrDecoder{QStringDecoder::Utf8}; QVector stderrRanges;