diff --git a/.clang b/.clang index 8b5eec8..579743b 100644 --- a/.clang +++ b/.clang @@ -1 +1 @@ --I ./include -std=c++11 +-I ./include -I ./external/Catch/include/ -std=c++11 diff --git a/CMakeLists.txt b/CMakeLists.txt index 1d00662..1dd458e 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -3,28 +3,96 @@ set(CMAKE_CXX_STANDARD 11) # Set the project name project (concatenator) -set(Boost_USE_STATIC_LIBS OFF) -set(Boost_USE_MULTITHREADED ON) -set(Boost_USE_STATIC_RUNTIME OFF) -find_package(Boost 1.60.0 COMPONENTS program_options log log_setup thread date_time filesystem system) +set(CMAKE_MODULE_PATH ${CMAKE_MODULE_PATH} "${CMAKE_SOURCE_DIR}/cmake/Modules/") + +############################################################################### +### Boost Settings +find_package(Boost 1.60.0 COMPONENTS program_options log log_setup thread date_time filesystem system REQUIRED) +find_package(LibSndFile REQUIRED) if (NOT FO_BOOST_STATIC_LINK) add_definitions(-DBOOST_ALL_NO_LIB -DBOOST_ALL_DYN_LINK -DBOOST_LOG_DYN_LINK) endif() +set(Boost_USE_STATIC_LIBS OFF) +set(Boost_USE_MULTITHREADED ON) +set(Boost_USE_STATIC_RUNTIME OFF) +############################################################################### + # Set build flags -set(CMAKE_CXX_FLAGS "-g -Wall ") +set(CMAKE_CXX_FLAGS "-g -Wall") # Set cmake to output executable to the bin directory set(CMAKE_RUNTIME_OUTPUT_DIRECTORY ${CMAKE_BINARY_DIR}/bin) -include_directories(include) -# Find all the source fies in the src directory -file(GLOB SOURCES "src/*.cpp") -# Build the executable from the source files found -if(Boost_FOUND) - include_directories(${Boost_INCLUDE_DIRS}) - add_executable(concatenator ${SOURCES}) - target_link_libraries(concatenator ${Boost_LIBRARIES}) + +set(CONCATENATOR_SOURCE_DIR ${PROJECT_SOURCE_DIR}/src) +set(CONCATENATOR_INCLUDE_DIR ${PROJECT_SOURCE_DIR}/include) +set(CONCATENATOR_TEST_DIR ${PROJECT_SOURCE_DIR}/test) + +set(CONCATENATOR_SOURCE_FILES + ${CONCATENATOR_SOURCE_DIR}/Concatenator.cpp + ${CONCATENATOR_SOURCE_DIR}/AudioDatabase.cpp + ${CONCATENATOR_SOURCE_DIR}/AudioFile.cpp + ${CONCATENATOR_SOURCE_DIR}/Logger.cpp + ${CONCATENATOR_SOURCE_DIR}/ArgumentParser.cpp +) +set(CONCATENATOR_HEADER_FILES + ${CONCATENATOR_INCLUDE_DIR}/ArgumentParser.h + ${CONCATENATOR_INCLUDE_DIR}/AudioDatabase.h + ${CONCATENATOR_INCLUDE_DIR}/AudioFile.h + ${CONCATENATOR_INCLUDE_DIR}/Logger.h +) +set(CONCATENATOR_TEST_SOURCES + ${CONCATENATOR_TEST_DIR}/Concatenator_Test.cpp + ${CONCATENATOR_TEST_DIR}/Basic_Tests.cpp +) + +include_directories(${CONCATENATOR_INCLUDE_DIR}) +include_directories(${Boost_INCLUDE_DIRS}) +include_directories(${LIBSNDFILE_INCLUDE_DIR}) +add_subdirectory(external) + +add_executable(concatenator ${CONCATENATOR_SOURCE_FILES} ${CONCATENATOR_HEADER_FILES}) + +# Link to external libraries +target_link_libraries(concatenator ${Boost_LIBRARIES}) +target_link_libraries(concatenator ${LIBSNDFILE_LIBRARIES}) + +# Test build options (this code adapted from: https://github.com/ComicSansMS/GhulbusBase/blob/master/CMakeLists.txt) +option(BUILD_TESTS "Determines whether to build tests." ON) +if(BUILD_TESTS) + enable_testing() + + if(NOT TARGET Catch) + include(ExternalProject) + if(WIN32) + set(FETCH_EXTERNAL_CATCH + URL https://github.com/philsquared/Catch/archive/v1.2.1-develop.12.zip + URL_HASH MD5=cda228922a1c9248364c99a3ff9cd9fa) + else() + set(FETCH_EXTERNAL_CATCH + URL https://github.com/philsquared/Catch/archive/v1.2.1-develop.12.tar.gz + URL_HASH MD5=a8dfb7be899a6e7fb30bd55d53426122) + endif() + ExternalProject_Add(Catch-External + PREFIX ${CMAKE_BINARY_DIR}/external/Catch + ${FETCH_EXTERNAL_CATCH} + CONFIGURE_COMMAND "" + BUILD_COMMAND "" + INSTALL_COMMAND ${CMAKE_COMMAND} -E copy ${CMAKE_BINARY_DIR}/external/Catch/src/Catch-External/single_include/catch.hpp + ${CMAKE_BINARY_DIR}/external/Catch/include/catch.hpp + ) + add_library(Catch INTERFACE) + add_dependencies(Catch Catch-External) + + target_include_directories(Catch INTERFACE ${CMAKE_BINARY_DIR}/external/Catch/include) + endif() + + + add_executable(Concatenator_Test ${CONCATENATOR_TEST_SOURCES}) + target_link_libraries(Concatenator_Test Catch) + add_test(NAME TestBase COMMAND Concatenator_Test) endif() + diff --git a/cmake/Modules/FindLibSndFile.cmake b/cmake/Modules/FindLibSndFile.cmake new file mode 100644 index 0000000..0d4e5dc --- /dev/null +++ b/cmake/Modules/FindLibSndFile.cmake @@ -0,0 +1,34 @@ +# - Try to find libsndfile +# Once done, this will define +# +# LIBSNDFILE_FOUND - system has libsndfile +# LIBSNDFILE_INCLUDE_DIRS - the libsndfile include directories +# LIBSNDFILE_LIBRARIES - link these to use libsndfile + +# Use pkg-config to get hints about paths +find_package(PkgConfig QUIET) +if(PKG_CONFIG_FOUND) + pkg_check_modules(LIBSNDFILE_PKGCONF sndfile) +endif(PKG_CONFIG_FOUND) + +# Include dir +find_path(LIBSNDFILE_INCLUDE_DIR + NAMES sndfile.h + PATHS ${LIBSNDFILE_PKGCONF_INCLUDE_DIRS} +) + +# Library +find_library(LIBSNDFILE_LIBRARY + NAMES sndfile libsndfile-1 + PATHS ${LIBSNDFILE_PKGCONF_LIBRARY_DIRS} +) + +find_package(PackageHandleStandardArgs) +find_package_handle_standard_args(LibSndFile DEFAULT_MSG LIBSNDFILE_LIBRARY LIBSNDFILE_INCLUDE_DIR) + +if(LIBSNDFILE_FOUND) + set(LIBSNDFILE_LIBRARIES ${LIBSNDFILE_LIBRARY}) + set(LIBSNDFILE_INCLUDE_DIRS ${LIBSNDFILE_INCLUDE_DIR}) +endif(LIBSNDFILE_FOUND) + +mark_as_advanced(LIBSNDFILE_LIBRARY LIBSNDFILE_LIBRARIES LIBSNDFILE_INCLUDE_DIR LIBSNDFILE_INCLUDE_DIRS) diff --git a/external/CMakeLists.txt b/external/CMakeLists.txt new file mode 100644 index 0000000..e69de29 diff --git a/include/ArgumentParser.h b/include/ArgumentParser.h index a48653c..5ca9074 100644 --- a/include/ArgumentParser.h +++ b/include/ArgumentParser.h @@ -1,7 +1,13 @@ #include -#include "boost/program_options.hpp" +#include +#include +#include +#include +using std::vector; +using std::string; namespace po = boost::program_options; +namespace fs = boost::filesystem; class ArgumentParser { public: @@ -10,12 +16,25 @@ class ArgumentParser { ArgumentParser(const ArgumentParser&); ArgumentParser& operator=(const ArgumentParser&); int parseargs(int argc, char** argv); + po::variables_map::size_type count(char const *ref); + + const po::variable_value& operator [](char const *b) const; + //std::string& operator [](char const *b); private: + // Stores values for arguments parsed from the command line + po::variables_map vm; //Create a positional options object for parsing input, output etc //positional arguments from command line. po::positional_options_description positionalOptions; - // - po::variables_map vm; po::options_description desc; }; + +class ConcatenatorArgParse : public ArgumentParser { + public: + vector get_analyses() { return (*this)["analyses"].as>(); } + string get_source_db() { return (*this)["source"].as(); } + string get_target_db() { return (*this)["target"].as(); } + fs::path get_tar_audio_dir() { return ((*this)["tar_audio"].empty() ? fs::path("") : fs::path((*this)["tar_audio"].as())); } + fs::path get_src_audio_dir() { return ((*this)["src_audio"].empty() ? fs::path("") : fs::path((*this)["src_audio"].as())); } +}; diff --git a/include/AudioDatabase.h b/include/AudioDatabase.h index e69de29..0ba6e3c 100644 --- a/include/AudioDatabase.h +++ b/include/AudioDatabase.h @@ -0,0 +1,88 @@ +#include +#include +#include +#include +#include +#include +#include "logger.h" + +using std::string; +using std::cout; +using std::endl; +using std::list; +using std::vector; + +/*! + * A class that encapsulates a collection of AudioFile objects in order to + * perform analysis and synthesis operations on batches of audio files. +*/ + +class AudioDatabase { + public: + AudioDatabase( + const std::string database_dir, + vector& analyses, + Logger* log + ); + void load_database(boost::filesystem::path source_dir, bool reanalyse=false); + + private: + boost::filesystem::path database_dir; + boost::filesystem::path audio_dir; + // Define a set that stores the locations of audiofiles in the database. + std::set audio_file_set; + std::map database_dirs; + Logger* log; + + void validate_analysis_list(); + bool validate_filetype(const boost::filesystem::path& filepath); + void create_subdirs(); + void organise_audio(boost::filesystem::path source_dir, bool symlink=true); + void register_audio(); +}; + +/*! A function that determines whether a string value is found in the container. +*/ +template +bool in_array(string &value, const container &array) +{ + boost::algorithm::to_upper(value); + return std::find(array.begin(), array.end(), value) != array.end(); +} + +/*! Check that analysis strings provided are supported by the database object + + \param iterator - An iterator pointing to where to begin checking strings are valid. + \param end - An iterator pointing to the point at which to stop analysing strings. +*/ +template +std::list check_analyses_valid(Iter iterator, Iter end) +{ + static std::list valid_analyses = { + "RMS", + "ZEROX", + "FFT", + "SPCCNTR", + "SPCSPRD", + "SPCFLUX", + "SPCCF", + "SPCFLATNESS", + "F0", + "PEAK", + "CENTROID", + "VARIANCE", + "KURTOSIS", + "SKEWNESS", + "HARM_RATIO" + }; + std::list invalid; + + while(iterator != end) + { + if(!in_array(*iterator, valid_analyses)) { + invalid.push_back(*iterator); + } + ++iterator; + } + return invalid; +} diff --git a/include/AudioFile.h b/include/AudioFile.h index e69de29..a2548c1 100644 --- a/include/AudioFile.h +++ b/include/AudioFile.h @@ -0,0 +1,19 @@ +#include +#include + +class AudioFile { + public: + AudioFile(const char * &name, const int &mode=SFM_RDWR, const int &format=0, const int &channels=0, const int &samplerate=0); + int open(const int &mode=SFM_READ, const int &format=0, const int &channels=0, const int &samplerate=0); + protected: + SndfileHandle file; + SF_INFO* file_info; + private: + std::string name; +}; + +class AnalysedAudioFile : public AudioFile { + public: + private: +}; + diff --git a/include/logger.h b/include/logger.h index 91774f8..32abf25 100644 --- a/include/logger.h +++ b/include/logger.h @@ -1,3 +1,5 @@ +#ifndef LOGGER_H +#define LOGGER_H #include #include #include @@ -8,7 +10,7 @@ class Logger { public: Logger(); - ~Logger() {}; + ~Logger(); void trace(std::string str); void debug(std::string str); void info(std::string str); @@ -21,8 +23,8 @@ class Logger { static void log_formatter(boost::log::record_view const& rec, boost::log::formatting_ostream& strm); // Define types for logging backends - typedef boost::log::sinks::asynchronous_sink console_backend; - typedef boost::log::sinks::asynchronous_sink file_backend; + typedef boost::log::sinks::synchronous_sink console_backend; + typedef boost::log::sinks::synchronous_sink file_backend; // Define a sink for console output and for log file output boost::shared_ptr console_sink; @@ -31,3 +33,4 @@ class Logger { //Define a logger boost::log::sources::severity_logger< boost::log::trivial::severity_level > lg; }; +#endif diff --git a/src/ArgumentParser.cpp b/src/ArgumentParser.cpp index 70110f3..24eefeb 100644 --- a/src/ArgumentParser.cpp +++ b/src/ArgumentParser.cpp @@ -1,29 +1,53 @@ #include "ArgumentParser.h" +#include #include using namespace std; ArgumentParser::ArgumentParser() : desc("Allowed options") { // Add positional arguments to specify source, target and output database locations. - positionalOptions.add("source_db", 1); - positionalOptions.add("target_db", 1); - positionalOptions.add("output_db", 1); + positionalOptions.add("source", 1); + positionalOptions.add("target", 1); + positionalOptions.add("output", 1); // Add optional arguments to allow control over application settings from the command line. desc.add_options() ("help,h", "produce help message") - ("compression", po::value(), "set compression level") + ("source", po::value()->required(), "Source location") + ("target", po::value()->required(), "Target location") + ("output", po::value()->required(), "Output location") + ("analyses,a", po::value>()->multitoken(), "Analysis " + "strings specifying analyses to use for database comparison.") + ("src_audio", po::value(), "Specifies the " + "directory to create the source database and store analyses in. If " + "not specified then the " "source directory will be used directly.") + ("tar_audio", po::value(), "Specifies the " + "directory to create the target database and store analyses in. If " + "not specified then the target directory will be used directly.") ; } int ArgumentParser::parseargs(int argc, char** argv) { po::store(po::command_line_parser(argc, argv).options(desc).positional(positionalOptions).run(), vm); - po::notify(vm); // If help option is specified then output help message if (vm.count("help")) { cout << desc << "\n"; return 1; } + + po::notify(vm); + + if (vm["analyses"].empty()) { + throw runtime_error("No analysis strings provided as arguments."); + } return 0; } + +const po::variable_value& ArgumentParser::operator [](char const *b) const { + return vm[b]; +} + +po::variables_map::size_type ArgumentParser::count(char const *ref) { + return vm.count(ref); +} diff --git a/src/AudioDatabase.cpp b/src/AudioDatabase.cpp index e69de29..60cc7db 100644 --- a/src/AudioDatabase.cpp +++ b/src/AudioDatabase.cpp @@ -0,0 +1,202 @@ +#include +#include +#include +#include "AudioDatabase.h" +#include +#include +#include +#include +#include +#include + +namespace fs = boost::filesystem; + +using namespace std; + +AudioDatabase::AudioDatabase( + const string database_dir, + vector& analyses, + Logger* log + ) +{ + this->log = log; + + log->info("Database directory: " + database_dir); + + // Remove duplicate strings from vector of analyses. + std::vector::iterator it; + it = std::unique (analyses.begin(), analyses.end()); + analyses.resize(std::distance(analyses.begin(),it)); + + // Check that all analysis strings supplied refer to valid analyses. + list invalid = check_analyses_valid(analyses.begin(), analyses.end()); + if(!invalid.empty()) { + string err = "The following analysis string(s) supplied to the AudioDatabase constructor are not valid: "; + string invalid_strings = boost::algorithm::join(invalid, " "); + throw std::runtime_error(err + invalid_strings); + } + + database_dirs.insert({"root", fs::path(database_dir)}); + this->audio_dir = fs::path(audio_dir); +} + +void AudioDatabase::validate_analysis_list() +{ +} + +void AudioDatabase::load_database(fs::path source_dir, bool reanalyse) +{ + // Make sure the database root directory exists. + try + { + if(create_directory(database_dirs["root"])) + { + log->debug("Database directory created: " + database_dir.string()); + } + else if(exists(database_dirs["root"])) + { + log->debug("Database directory already exists: " + database_dir.string()); + } + } + catch(boost::filesystem::filesystem_error &e) + { + throw std::runtime_error("Database directory could not be created: " + database_dir.string()); + } + + + // Create a folder hierachy used to store audio and analysis data used by the database. + create_subdirs(); + + if(source_dir.empty()) { + source_dir = database_dirs["audio"]; + log->debug("Source directory not provided. Setting to:" + source_dir.string()); + } + + if(!exists(source_dir)) { + throw std::runtime_error("Source audio directory does not exist: " + source_dir.string()); + } + + // Only organise audio if new audio is to be added from a new location. + if(source_dir != database_dirs["audio"]) { + // Copy/create links to audio files that are to be used as part of the database. + organise_audio(source_dir); + } + + // Find all audio in the database and store references for use later... + register_audio(); + +} + +void AudioDatabase::create_subdirs() +{ + array directory_names = {{ + fs::path("audio"), + fs::path("data") + }}; + + for(const auto& name : directory_names) { + fs::path subdir = database_dirs["root"]/name; + try + { + if(create_directory(subdir)) { + log->info("Subdirectory created: " + subdir.string()); + } + else if(exists(database_dirs["root"])) + { + log->info("Subdirectory already exists: " + subdir.string()); + } + } + catch(boost::filesystem::filesystem_error &e) + { + throw std::runtime_error("Subdirectory could not be created: " + database_dirs["root"].string()); + } + database_dirs.insert({name.string(), subdir}); + } +} + +bool AudioDatabase::validate_filetype(const fs::path& filepath) +{ + // Define patterns to validate files found based on their file extensions. + static array valid_filetypes = {{ + ".wav", + ".aif", + ".aiff", + ".flac" + }}; + + for(const auto& filetype : valid_filetypes) + { + // compare file extension with valid extension strings to find match. + bool valid = (filetype.compare(filepath.extension().string()) == 0); + if(valid) { + return true; + } + } + return false; +} + +void AudioDatabase::organise_audio(fs::path source_dir, bool symlink) +{ + + log->info("Organising audio directory at: " + database_dirs["audio"].string()); + // Define the destination for copying/linking all valid audio files found. + for(fs::recursive_directory_iterator iter(source_dir), end; iter != end; ++iter) + { + // Don't search the audio directory of the database if this is a subdirectory of the source directory provided. + if (iter->path() == database_dirs["audio"]) + { + iter.disable_recursion_pending(); + } + + if(!validate_filetype(iter->path())) { + log->debug("File: " + iter->path().string() + " isn't a supported audiofile. Skipping..."); + continue; + } + + + fs::path destination_file = database_dirs["audio"]/iter->path().filename(); + + if(symlink) { + // Try to symlink the file to the audio directory of the database. + try { + fs::create_symlink(iter->path(), destination_file); + log->debug("Linked: " + iter->path().string() + " to: " + destination_file.string()); + } + catch(boost::filesystem::filesystem_error &e){ + // If symbolic linking fails then the file probably already exists at the location. + log->debug("Failed to link: " + iter->path().string() + " to " + destination_file.string() + " File may already exists."); + } + } + else { + // If it is in the database as a symlink, but a full copy is required + if(fs::exists(destination_file) && !fs::is_symlink(destination_file)) { + log->debug("File already exists: " + iter->path().string()); + continue; + } + + // Copy file, overwriting any previously created symbolic links. + try { + fs::remove(destination_file); + fs::copy_file(iter->path(), destination_file, fs::copy_option::overwrite_if_exists); + log->debug("Copied: " + iter->path().string() + " to: " + destination_file.string()); + } + catch(boost::filesystem::filesystem_error &e){ + // If symbolic linking fails then the file probably already exists at the location. + log->debug("Failed to copy source file to: " + destination_file.string() + " File may already exists."); + } + } + } +} + +void AudioDatabase::register_audio() +{ + // Clear any previous entries from set. + audio_file_set.clear(); + for(auto& entry : boost::make_iterator_range(fs::directory_iterator(database_dirs["audio"]), {})) + { + if(validate_filetype(entry.path())) { + log->info("Registered audio file: " + entry.path().string()); + audio_file_set.insert(entry.path()); + } + } +} diff --git a/src/AudioFile.cpp b/src/AudioFile.cpp index 20348cb..e2af864 100644 --- a/src/AudioFile.cpp +++ b/src/AudioFile.cpp @@ -1,12 +1,23 @@ -#include +#include "AudioFile.h" +#include +#include +#include +using namespace std; -class AudioFile { - public: - private: - SndfileHandle file; -}; +AudioFile::AudioFile(const char * &name, const int &mode, const int &format, const int &channels, const int &samplerate) +{ + this->name = name; + open(mode, format, channels, samplerate); +} + +int AudioFile::open(const int &mode, const int &format, const int &channels, const int &samplerate) +{ + switch(mode){ + case SFM_READ: file = SndfileHandle(name); + case SFM_WRITE: file = SndfileHandle(name, SFM_WRITE, format, channels, samplerate); + case SFM_RDWR: file = SndfileHandle(name, SFM_RDWR, format, channels, samplerate); + } + + return 0; +} -class AnalysedAudioFile : public AudioFile { - public: - private: -}; diff --git a/src/concatenator.cpp b/src/concatenator.cpp index d307d82..a770858 100644 --- a/src/concatenator.cpp +++ b/src/concatenator.cpp @@ -1,15 +1,68 @@ #include -#include "Logger.h" +#include "logger.h" #include "ArgumentParser.h" #include "AudioDatabase.h" +#include +#include using namespace std; -int main(int argc, char** argv) { - Logger log = Logger(); - - ArgumentParser argparse = ArgumentParser(); - argparse.parseargs(argc, argv); - log.error("My pretty little error!"); - return 0; +namespace +{ + const size_t ERROR_IN_COMMAND_LINE = 1; + const size_t SUCCESS = 0; + const size_t ERROR_UNHANDLED_EXCEPTION = 2; + +} + +int main(int argc, char** argv) { + // Initialize a logger object to be used for message handeling throughout + // the program + Logger log = Logger(); + /* + try + { + */ + + // Initialize object to parse arguments supplied by user from command + // line + ConcatenatorArgParse argparse = ConcatenatorArgParse(); + // Parse arguments and exit program if specified (through use of --help + // or -h flag) + if(argparse.parseargs(argc, argv)) { + return SUCCESS; + } + + vector analyses = argparse.get_analyses(); + + // Initialize the source audio database object with arguments provided from the command line. + AudioDatabase source_db = AudioDatabase( + argparse.get_source_db(), + analyses, + &log + ); + source_db.load_database(argparse.get_src_audio_dir()); + + /* + // Initialize the target audio database object with arguments provided from the command line. + AudioDatabase target_db = AudioDatabase( + argparse.get_target_db(), + analyses, + &log + ); + target_db.load_database(argparse.get_tar_audio_dir()); + */ + + /* + } + catch(std::exception& e) + { + string error("Unhandled Exception reached the top of main:\n"); + error.append(e.what()); + + log.error(error); + throw; + } + */ + return SUCCESS; } diff --git a/src/logger.cpp b/src/logger.cpp index 9abf143..a401ed3 100644 --- a/src/logger.cpp +++ b/src/logger.cpp @@ -69,6 +69,10 @@ Logger::Logger() { }; +Logger::~Logger() { + Logger::file_sink->flush(); + Logger::console_sink->flush(); +} void Logger::trace(std::string str) { BOOST_LOG_SEV(lg, logging::trivial::trace) << str; } @@ -87,8 +91,12 @@ void Logger::warning(std::string str) { void Logger::error(std::string str) { BOOST_LOG_SEV(lg, logging::trivial::error) << str; + Logger::file_sink->flush(); + Logger::console_sink->flush(); } void Logger::fatal(std::string str) { BOOST_LOG_SEV(lg, logging::trivial::fatal) << str; + Logger::file_sink->flush(); + Logger::console_sink->flush(); } diff --git a/test/Basic_Tests.cpp b/test/Basic_Tests.cpp new file mode 100644 index 0000000..fc502ef --- /dev/null +++ b/test/Basic_Tests.cpp @@ -0,0 +1 @@ +#include "catch.hpp" diff --git a/test/Concatenator_Test.cpp b/test/Concatenator_Test.cpp new file mode 100644 index 0000000..2926188 --- /dev/null +++ b/test/Concatenator_Test.cpp @@ -0,0 +1,4 @@ +#define CATCH_CONFIG_MAIN // This tells Catch to provide a main() - only do this in one cpp file +#include + +