diff src/sc_evaluator.cpp @ 0:add35537fdbb tip

Initial import
author irh <ian.r.hobson@gmail.com>
date Thu, 25 Aug 2011 11:05:55 +0100
parents
children
line wrap: on
line diff
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/src/sc_evaluator.cpp	Thu Aug 25 11:05:55 2011 +0100
@@ -0,0 +1,408 @@
+//  Copyright 2011, Ian Hobson.
+//
+//  This file is part of gpsynth.
+//
+//  gpsynth is free software: you can redistribute it and/or modify
+//  it under the terms of the GNU General Public License as published by
+//  the Free Software Foundation, either version 3 of the License, or
+//  (at your option) any later version.
+//
+//  gpsynth is distributed in the hope that it will be useful,
+//  but WITHOUT ANY WARRANTY; without even the implied warranty of
+//  MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+//  GNU General Public License for more details.
+//
+//  You should have received a copy of the GNU General Public License
+//  along with gpsynth in the file COPYING. 
+//  If not, see http://www.gnu.org/licenses/.
+
+#include "sc_evaluator.hpp"
+
+#include "boost_ex.hpp"
+#include "logger.hpp"
+#include "sc_converter.hpp"
+#include "statistics.hpp"
+
+#include "boost/filesystem.hpp"
+#include "boost/format.hpp"
+#include "boost/lexical_cast.hpp"
+#include "boost/process.hpp"
+#include "boost/thread.hpp"
+#include "boost/math/special_functions/fpclassify.hpp"
+
+#include <algorithm>
+#include <cmath>
+#include <functional>
+#include <numeric>
+
+namespace bp = boost::process;
+
+namespace sc {
+  
+  const int g_max_file_wait_time_ms = 10000;
+  const int g_file_wait_increment_ms = 500;
+  const int g_sclang_timeout_s = 10;
+  const double g_fitness_failure_value = 100;
+  const std::string g_score_variable_name = "x";
+ 
+// Worker class, runs in own thread
+class EvaluatorWorker {
+  sc::Converter converter_;
+  int thread_id_;
+  bool quit_threads_;
+  Evaluator* parent_;
+  dsp::FileComparer file_comparer_;
+  // work thread communicates with parent, handles work queue
+  boost::thread work_thread_;
+  // render thread launches child processes
+  boost::thread render_thread_;
+  boost::mutex render_mutex_;
+  // render duration in seconds
+  double render_length_;
+  std::string render_command_path_;
+  std::string render_osc_path_;
+  std::string render_output_path_;
+  // flag for render thread that a synth is ready for rendering
+  bool do_render_;
+  // condition to notify render thread that work is available
+  boost::condition_variable render_start_;
+  // condition to notify work thread that work has been done
+  boost::condition_variable render_finished_;
+  // stores handle to active child process
+  boost::shared_ptr<bp::child> render_process_;
+  
+public:
+  EvaluatorWorker(Evaluator* parent,
+                  int thread_id,
+                  const dsp::FileComparer& target)
+  : parent_(parent),
+    thread_id_(thread_id),
+    converter_(parent->grammar_),
+    quit_threads_(false),
+    file_comparer_(target),
+    render_length_(file_comparer_.TargetDuration()),
+    do_render_(false)
+  {
+    work_thread_ = boost::thread(&EvaluatorWorker::DoWork, this);
+    render_thread_ = boost::thread(&EvaluatorWorker::DoSCRender, this);
+  }
+  
+  ~EvaluatorWorker() {
+    // tell threads to quit
+    quit_threads_ = true;
+    // work thread
+    try {
+      boost::system_time timeout = boost::get_system_time();
+      timeout += boost::posix_time::seconds(g_sclang_timeout_s);
+      parent_->queue_condition_.notify_all();
+      work_thread_.timed_join(timeout);
+      // render thread
+      timeout = boost::get_system_time();
+      timeout += boost::posix_time::seconds(g_sclang_timeout_s);
+      render_start_.notify_one();
+      render_thread_.timed_join(timeout);      
+    }
+    catch (const std::exception& e) {
+      std::stringstream message;
+      message << "~sc::EvaluatorWorker - Exception: " << e.what() << '\n';
+      WorkerLog(message.str());
+    }
+  }
+  
+private:
+  
+  void DoWork() {
+    while (!quit_threads_) {
+      // wait for some work
+      {
+        boost::mutex::scoped_lock lock(parent_->work_mutex_);
+        while (parent_->work_queue_.empty() && !quit_threads_) {
+          parent_->queue_condition_.wait(lock);
+        }
+      }
+      // keep going until the work queue is empty 
+      while (!quit_threads_) {
+        sg::Graph* graph = NULL;
+        {
+          // check there's still work to do
+          boost::mutex::scoped_lock lock(parent_->work_mutex_);
+          if (parent_->work_queue_.empty()) {
+            // wait for all threads to reach this point before continuing
+            lock.unlock();
+            parent_->work_done_barrier_->wait();
+            lock.lock();
+            // notify main thread that work's finished
+            parent_->work_done_condition_.notify_one();
+            break;
+          }
+          // get the next graph from the queue
+          graph = parent_->work_queue_.front();
+          parent_->work_queue_.pop();
+        }
+        // get the graph properties
+        sg::Graph& graph_ref = *graph;
+        sg::GraphProperties& graph_properties = graph_ref[boost::graph_bundle];
+        double fitness;
+        try {
+          // rate the graph
+          fitness = RateGraph(graph_ref);
+        } catch (const std::exception& e) {
+          std::stringstream message;
+          message << "Exception: " << e.what() << '\n';
+          WorkerLog(message.str());
+          fitness = g_fitness_failure_value;
+        }
+        if (boost::math::isinf(fitness)) {
+          fitness = g_fitness_failure_value;
+        } else if (boost::math::isnan(fitness)) {
+          fitness = g_fitness_failure_value;
+        }
+        graph_properties.fitness_ = fitness;
+//        std::stringstream message;
+//        message << "Rated " << graph_properties.id_ << ":" << fitness << '\n';
+//        WorkerLog(message.str());
+        {
+          // decrement the graph queue count
+          boost::mutex::scoped_lock lock(parent_->work_mutex_);
+          parent_->graphs_to_rate_--;
+          parent_->work_done_condition_.notify_one();
+        }
+      }
+    }
+  }
+  
+  void WorkerLog(const std::string& message) {
+    std::stringstream formatter;
+    formatter << "\n[" << thread_id_ << "] " << message;
+    logger::Log(formatter.str());
+  }
+  
+  double RateGraph(sg::Graph& graph) {
+    int id = graph[boost::graph_bundle].id_;
+    std::stringstream name;
+    name << "gpsynth_"
+         << std::setw(parent_->synth_name_zero_pad_length_) << std::setfill('0')
+         << id;
+    std::string synth_name = name.str();
+    // convert the graph to a synthdef and score
+    std::string synthdef = converter_.ToSynthDef(graph,
+                                                 synth_name,
+                                                 false,
+                                                 parent_->synthdef_folder_);
+    std::string score = converter_.ToScore(graph,
+                                           synth_name,
+                                           g_score_variable_name,
+                                           false, // prepare for non-realtime
+                                           parent_->synthdef_folder_,
+                                           render_length_);
+    {
+      // prepare the synthdef and score for the render thread
+      boost::mutex::scoped_lock lock(render_mutex_);
+      render_command_path_ = parent_->sc_folder_ + synth_name + ".sc";
+      render_osc_path_ = parent_->osc_folder_ + synth_name + ".osc";
+      render_output_path_ = parent_->audio_folder_+ synth_name + ".aiff";
+      // prepare render script for sclang
+      std::ofstream command_file(render_command_path_.c_str());
+      // write the synthdef and the score
+      command_file << synthdef << '\n' << score;
+      // tell sclang to write the score as OSC data
+      command_file << "Score.write(" << g_score_variable_name << ", \"" 
+                   << render_osc_path_ << "\");\n";
+      // tell sclang to quit at end of script
+      command_file << "\n0.exit;";
+      // finished, close the file
+      command_file.close();
+      // tell the render thread that work is available
+      do_render_ = true;
+      boost::system_time timeout = boost::get_system_time();
+      timeout += boost::posix_time::seconds(g_sclang_timeout_s);
+      render_start_.notify_one();
+      // wait for the render thread to complete the job
+      if (!render_finished_.timed_wait(lock, timeout)) {
+        // sclang or scsynth timed out, kill process and set fitness to failure
+        // WorkerLog("Render thread timed out.\n");
+        try {
+          render_process_->terminate();
+        }
+        catch (boost::system::system_error& e) {
+          // don't need to do anything here, process may already be terminated.
+        }
+        do_render_ = false;
+        // maximum error
+        return g_fitness_failure_value;
+      }
+    }
+    // check that a rendered file has been produced
+    if (!boost::filesystem::exists(render_output_path_)) {
+      // file missing, something's gone wrong..
+      WorkerLog("Render output missing.\n");
+      return g_fitness_failure_value;
+    }
+    // store render path with graph for future reference
+    graph[boost::graph_bundle].render_path_ = render_output_path_;
+    // compare the rendered file to the target
+    return CompareToTarget(render_output_path_);
+  }
+  
+  void DoSCRender() {
+    std::vector<std::string> args;
+    while (!quit_threads_) {
+      // wait for a command to render
+      {
+        boost::mutex::scoped_lock lock(render_mutex_);
+        do_render_ = false;
+        while (!do_render_ && !quit_threads_) {
+          render_start_.wait(lock);
+        }
+        if (quit_threads_) {
+          return;
+        }
+      }
+      // Call sclang to get OSC command for scsynth
+      args.clear();
+      args.push_back("-d");
+      args.push_back(parent_->sc_app_path_);
+      args.push_back(render_command_path_);
+      // replace process pipes to suppress output
+      bp::context closed_streams;
+      closed_streams.streams[bp::stdout_id] = bp::behavior::close();
+      closed_streams.streams[bp::stderr_id] = bp::behavior::close();
+      bp::child process = bp::create_child(parent_->sclang_path_,
+                                           args,
+                                           closed_streams);
+      // store process handle to allow work thread to kill process if it's hung
+      {
+        boost::mutex::scoped_lock lock(render_mutex_);
+        render_process_.reset(new bp::child(process));
+      }
+      // wait for sclang to finish
+      process.wait();
+      if (!do_render_) {
+        // if do_render_ is false, process was killed by work thread 
+        continue;
+      }
+      // check that sclang generated an osc command
+      if (!boost::filesystem::exists(render_osc_path_)) {
+        // sclang failed, notify work thread and continue
+        boost::mutex::scoped_lock lock(render_mutex_);
+        render_finished_.notify_one();
+        continue;
+      }
+      // render osc with scsynth
+      args.clear();
+      args.push_back("-N");
+      args.push_back(render_osc_path_);
+      args.push_back("_"); // no input file
+      args.push_back(render_output_path_);
+      args.push_back("44100");
+      args.push_back("AIFF");
+      args.push_back("int16");
+      // output channels
+      args.push_back("-o");
+      args.push_back("1");
+      // don't publish to rendevous
+      args.push_back("-R");
+      args.push_back("0");
+      // don't load synthdefs
+      args.push_back("-D");
+      args.push_back("0");
+      // plugins path
+      args.push_back("-U");
+      args.push_back(parent_->sc_plugins_path_);
+      // call scsynth
+      process = bp::create_child(parent_->scsynth_path_, args, closed_streams);
+      {
+        boost::mutex::scoped_lock lock(render_mutex_);
+        render_process_.reset(new bp::child(process));
+      }
+      // wait for scsynth to finish rendering
+      process.wait();
+      if (!do_render_) {
+        // process timed out, killed by work thread
+        continue;
+      }
+      // job done, notify work thread
+      {
+        boost::mutex::scoped_lock lock(render_mutex_);
+        render_finished_.notify_one();
+      }
+    }
+  }
+  
+  double CompareToTarget(const std::string& file_path) {
+    return file_comparer_.CompareFile(file_path);
+  }
+};
+
+
+////////////////////////////////////////////////////////////////////////////////
+////////////////////////////////////////////////////////////////////////////////
+
+Evaluator::Evaluator(const sc::Grammar& grammar,
+                     const std::string& sc_app_path,
+                     const dsp::FileComparer& target,
+                     int core_limit /* = 0 */)
+: grammar_(grammar),
+  listener_(NULL),
+  sc_app_path_(sc_app_path),
+  sc_plugins_path_(sc_app_path + "/plugins"),
+  sclang_path_(sc_app_path + "/sclang"),
+  scsynth_path_(sc_app_path + "/scsynth")
+{
+  SetWorkFolder("/tmp/");
+  int workers = boost::thread::hardware_concurrency();
+  if (core_limit > 0 && core_limit < workers) {
+    workers = core_limit;
+  }
+  work_done_barrier_.reset(new boost::barrier(workers));
+  for (int worker_id = 0; worker_id < workers; worker_id++) {
+    workers_.push_back(EvaluatorWorkerPtr(new EvaluatorWorker(this,
+                                                              worker_id,
+                                                              target)));
+  }
+}
+
+Evaluator::~Evaluator()
+{}
+
+
+void Evaluator::RateGraphs(std::vector<sg::Graph>& graphs) {
+  // make sure necessary subfolders exist in work folder
+  audio_folder_ = work_folder_ + "/audio/";
+  osc_folder_ = work_folder_ + "/osc/";
+  sc_folder_ = work_folder_ + "/sc/";
+  synthdef_folder_ = work_folder_ + "/synthdefs/";
+  boost::filesystem::create_directories(audio_folder_);
+  boost::filesystem::create_directories(osc_folder_);
+  boost::filesystem::create_directories(sc_folder_);
+  boost::filesystem::create_directories(synthdef_folder_);
+  // find zero padding length for synth names
+  synth_name_zero_pad_length_ = std::log10(graphs.size()) + 1;
+  // store the graphs in the work queue
+  boost::mutex::scoped_lock lock(work_mutex_);
+  graphs_to_rate_ = static_cast<int>(graphs.size());
+  for (int i = 0; i < graphs_to_rate_; i++) {
+    work_queue_.push(&graphs[i]);
+  }
+  // wait for graphs to be rated
+  std::size_t last_rated_count = 0;
+  while (graphs_to_rate_ > 0) {
+    queue_condition_.notify_all();
+    work_done_condition_.wait(lock);
+    if (listener_ != NULL) {
+      std::size_t rated_count = graphs.size() - graphs_to_rate_;
+      if (rated_count != last_rated_count) {
+        last_rated_count = rated_count;
+        listener_->GraphRatedNotification(rated_count);
+      }
+    }
+  }
+  // remove OSC folder, no longer required
+  boost::filesystem::remove_all(osc_folder_);
+}
+  
+void Evaluator::SetWorkFolder(const std::string& path) {
+  work_folder_ = path;
+}
+  
+} // sc namespace