comparison 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
comparison
equal deleted inserted replaced
-1:000000000000 0:add35537fdbb
1 // Copyright 2011, Ian Hobson.
2 //
3 // This file is part of gpsynth.
4 //
5 // gpsynth is free software: you can redistribute it and/or modify
6 // it under the terms of the GNU General Public License as published by
7 // the Free Software Foundation, either version 3 of the License, or
8 // (at your option) any later version.
9 //
10 // gpsynth is distributed in the hope that it will be useful,
11 // but WITHOUT ANY WARRANTY; without even the implied warranty of
12 // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
13 // GNU General Public License for more details.
14 //
15 // You should have received a copy of the GNU General Public License
16 // along with gpsynth in the file COPYING.
17 // If not, see http://www.gnu.org/licenses/.
18
19 #include "sc_evaluator.hpp"
20
21 #include "boost_ex.hpp"
22 #include "logger.hpp"
23 #include "sc_converter.hpp"
24 #include "statistics.hpp"
25
26 #include "boost/filesystem.hpp"
27 #include "boost/format.hpp"
28 #include "boost/lexical_cast.hpp"
29 #include "boost/process.hpp"
30 #include "boost/thread.hpp"
31 #include "boost/math/special_functions/fpclassify.hpp"
32
33 #include <algorithm>
34 #include <cmath>
35 #include <functional>
36 #include <numeric>
37
38 namespace bp = boost::process;
39
40 namespace sc {
41
42 const int g_max_file_wait_time_ms = 10000;
43 const int g_file_wait_increment_ms = 500;
44 const int g_sclang_timeout_s = 10;
45 const double g_fitness_failure_value = 100;
46 const std::string g_score_variable_name = "x";
47
48 // Worker class, runs in own thread
49 class EvaluatorWorker {
50 sc::Converter converter_;
51 int thread_id_;
52 bool quit_threads_;
53 Evaluator* parent_;
54 dsp::FileComparer file_comparer_;
55 // work thread communicates with parent, handles work queue
56 boost::thread work_thread_;
57 // render thread launches child processes
58 boost::thread render_thread_;
59 boost::mutex render_mutex_;
60 // render duration in seconds
61 double render_length_;
62 std::string render_command_path_;
63 std::string render_osc_path_;
64 std::string render_output_path_;
65 // flag for render thread that a synth is ready for rendering
66 bool do_render_;
67 // condition to notify render thread that work is available
68 boost::condition_variable render_start_;
69 // condition to notify work thread that work has been done
70 boost::condition_variable render_finished_;
71 // stores handle to active child process
72 boost::shared_ptr<bp::child> render_process_;
73
74 public:
75 EvaluatorWorker(Evaluator* parent,
76 int thread_id,
77 const dsp::FileComparer& target)
78 : parent_(parent),
79 thread_id_(thread_id),
80 converter_(parent->grammar_),
81 quit_threads_(false),
82 file_comparer_(target),
83 render_length_(file_comparer_.TargetDuration()),
84 do_render_(false)
85 {
86 work_thread_ = boost::thread(&EvaluatorWorker::DoWork, this);
87 render_thread_ = boost::thread(&EvaluatorWorker::DoSCRender, this);
88 }
89
90 ~EvaluatorWorker() {
91 // tell threads to quit
92 quit_threads_ = true;
93 // work thread
94 try {
95 boost::system_time timeout = boost::get_system_time();
96 timeout += boost::posix_time::seconds(g_sclang_timeout_s);
97 parent_->queue_condition_.notify_all();
98 work_thread_.timed_join(timeout);
99 // render thread
100 timeout = boost::get_system_time();
101 timeout += boost::posix_time::seconds(g_sclang_timeout_s);
102 render_start_.notify_one();
103 render_thread_.timed_join(timeout);
104 }
105 catch (const std::exception& e) {
106 std::stringstream message;
107 message << "~sc::EvaluatorWorker - Exception: " << e.what() << '\n';
108 WorkerLog(message.str());
109 }
110 }
111
112 private:
113
114 void DoWork() {
115 while (!quit_threads_) {
116 // wait for some work
117 {
118 boost::mutex::scoped_lock lock(parent_->work_mutex_);
119 while (parent_->work_queue_.empty() && !quit_threads_) {
120 parent_->queue_condition_.wait(lock);
121 }
122 }
123 // keep going until the work queue is empty
124 while (!quit_threads_) {
125 sg::Graph* graph = NULL;
126 {
127 // check there's still work to do
128 boost::mutex::scoped_lock lock(parent_->work_mutex_);
129 if (parent_->work_queue_.empty()) {
130 // wait for all threads to reach this point before continuing
131 lock.unlock();
132 parent_->work_done_barrier_->wait();
133 lock.lock();
134 // notify main thread that work's finished
135 parent_->work_done_condition_.notify_one();
136 break;
137 }
138 // get the next graph from the queue
139 graph = parent_->work_queue_.front();
140 parent_->work_queue_.pop();
141 }
142 // get the graph properties
143 sg::Graph& graph_ref = *graph;
144 sg::GraphProperties& graph_properties = graph_ref[boost::graph_bundle];
145 double fitness;
146 try {
147 // rate the graph
148 fitness = RateGraph(graph_ref);
149 } catch (const std::exception& e) {
150 std::stringstream message;
151 message << "Exception: " << e.what() << '\n';
152 WorkerLog(message.str());
153 fitness = g_fitness_failure_value;
154 }
155 if (boost::math::isinf(fitness)) {
156 fitness = g_fitness_failure_value;
157 } else if (boost::math::isnan(fitness)) {
158 fitness = g_fitness_failure_value;
159 }
160 graph_properties.fitness_ = fitness;
161 // std::stringstream message;
162 // message << "Rated " << graph_properties.id_ << ":" << fitness << '\n';
163 // WorkerLog(message.str());
164 {
165 // decrement the graph queue count
166 boost::mutex::scoped_lock lock(parent_->work_mutex_);
167 parent_->graphs_to_rate_--;
168 parent_->work_done_condition_.notify_one();
169 }
170 }
171 }
172 }
173
174 void WorkerLog(const std::string& message) {
175 std::stringstream formatter;
176 formatter << "\n[" << thread_id_ << "] " << message;
177 logger::Log(formatter.str());
178 }
179
180 double RateGraph(sg::Graph& graph) {
181 int id = graph[boost::graph_bundle].id_;
182 std::stringstream name;
183 name << "gpsynth_"
184 << std::setw(parent_->synth_name_zero_pad_length_) << std::setfill('0')
185 << id;
186 std::string synth_name = name.str();
187 // convert the graph to a synthdef and score
188 std::string synthdef = converter_.ToSynthDef(graph,
189 synth_name,
190 false,
191 parent_->synthdef_folder_);
192 std::string score = converter_.ToScore(graph,
193 synth_name,
194 g_score_variable_name,
195 false, // prepare for non-realtime
196 parent_->synthdef_folder_,
197 render_length_);
198 {
199 // prepare the synthdef and score for the render thread
200 boost::mutex::scoped_lock lock(render_mutex_);
201 render_command_path_ = parent_->sc_folder_ + synth_name + ".sc";
202 render_osc_path_ = parent_->osc_folder_ + synth_name + ".osc";
203 render_output_path_ = parent_->audio_folder_+ synth_name + ".aiff";
204 // prepare render script for sclang
205 std::ofstream command_file(render_command_path_.c_str());
206 // write the synthdef and the score
207 command_file << synthdef << '\n' << score;
208 // tell sclang to write the score as OSC data
209 command_file << "Score.write(" << g_score_variable_name << ", \""
210 << render_osc_path_ << "\");\n";
211 // tell sclang to quit at end of script
212 command_file << "\n0.exit;";
213 // finished, close the file
214 command_file.close();
215 // tell the render thread that work is available
216 do_render_ = true;
217 boost::system_time timeout = boost::get_system_time();
218 timeout += boost::posix_time::seconds(g_sclang_timeout_s);
219 render_start_.notify_one();
220 // wait for the render thread to complete the job
221 if (!render_finished_.timed_wait(lock, timeout)) {
222 // sclang or scsynth timed out, kill process and set fitness to failure
223 // WorkerLog("Render thread timed out.\n");
224 try {
225 render_process_->terminate();
226 }
227 catch (boost::system::system_error& e) {
228 // don't need to do anything here, process may already be terminated.
229 }
230 do_render_ = false;
231 // maximum error
232 return g_fitness_failure_value;
233 }
234 }
235 // check that a rendered file has been produced
236 if (!boost::filesystem::exists(render_output_path_)) {
237 // file missing, something's gone wrong..
238 WorkerLog("Render output missing.\n");
239 return g_fitness_failure_value;
240 }
241 // store render path with graph for future reference
242 graph[boost::graph_bundle].render_path_ = render_output_path_;
243 // compare the rendered file to the target
244 return CompareToTarget(render_output_path_);
245 }
246
247 void DoSCRender() {
248 std::vector<std::string> args;
249 while (!quit_threads_) {
250 // wait for a command to render
251 {
252 boost::mutex::scoped_lock lock(render_mutex_);
253 do_render_ = false;
254 while (!do_render_ && !quit_threads_) {
255 render_start_.wait(lock);
256 }
257 if (quit_threads_) {
258 return;
259 }
260 }
261 // Call sclang to get OSC command for scsynth
262 args.clear();
263 args.push_back("-d");
264 args.push_back(parent_->sc_app_path_);
265 args.push_back(render_command_path_);
266 // replace process pipes to suppress output
267 bp::context closed_streams;
268 closed_streams.streams[bp::stdout_id] = bp::behavior::close();
269 closed_streams.streams[bp::stderr_id] = bp::behavior::close();
270 bp::child process = bp::create_child(parent_->sclang_path_,
271 args,
272 closed_streams);
273 // store process handle to allow work thread to kill process if it's hung
274 {
275 boost::mutex::scoped_lock lock(render_mutex_);
276 render_process_.reset(new bp::child(process));
277 }
278 // wait for sclang to finish
279 process.wait();
280 if (!do_render_) {
281 // if do_render_ is false, process was killed by work thread
282 continue;
283 }
284 // check that sclang generated an osc command
285 if (!boost::filesystem::exists(render_osc_path_)) {
286 // sclang failed, notify work thread and continue
287 boost::mutex::scoped_lock lock(render_mutex_);
288 render_finished_.notify_one();
289 continue;
290 }
291 // render osc with scsynth
292 args.clear();
293 args.push_back("-N");
294 args.push_back(render_osc_path_);
295 args.push_back("_"); // no input file
296 args.push_back(render_output_path_);
297 args.push_back("44100");
298 args.push_back("AIFF");
299 args.push_back("int16");
300 // output channels
301 args.push_back("-o");
302 args.push_back("1");
303 // don't publish to rendevous
304 args.push_back("-R");
305 args.push_back("0");
306 // don't load synthdefs
307 args.push_back("-D");
308 args.push_back("0");
309 // plugins path
310 args.push_back("-U");
311 args.push_back(parent_->sc_plugins_path_);
312 // call scsynth
313 process = bp::create_child(parent_->scsynth_path_, args, closed_streams);
314 {
315 boost::mutex::scoped_lock lock(render_mutex_);
316 render_process_.reset(new bp::child(process));
317 }
318 // wait for scsynth to finish rendering
319 process.wait();
320 if (!do_render_) {
321 // process timed out, killed by work thread
322 continue;
323 }
324 // job done, notify work thread
325 {
326 boost::mutex::scoped_lock lock(render_mutex_);
327 render_finished_.notify_one();
328 }
329 }
330 }
331
332 double CompareToTarget(const std::string& file_path) {
333 return file_comparer_.CompareFile(file_path);
334 }
335 };
336
337
338 ////////////////////////////////////////////////////////////////////////////////
339 ////////////////////////////////////////////////////////////////////////////////
340
341 Evaluator::Evaluator(const sc::Grammar& grammar,
342 const std::string& sc_app_path,
343 const dsp::FileComparer& target,
344 int core_limit /* = 0 */)
345 : grammar_(grammar),
346 listener_(NULL),
347 sc_app_path_(sc_app_path),
348 sc_plugins_path_(sc_app_path + "/plugins"),
349 sclang_path_(sc_app_path + "/sclang"),
350 scsynth_path_(sc_app_path + "/scsynth")
351 {
352 SetWorkFolder("/tmp/");
353 int workers = boost::thread::hardware_concurrency();
354 if (core_limit > 0 && core_limit < workers) {
355 workers = core_limit;
356 }
357 work_done_barrier_.reset(new boost::barrier(workers));
358 for (int worker_id = 0; worker_id < workers; worker_id++) {
359 workers_.push_back(EvaluatorWorkerPtr(new EvaluatorWorker(this,
360 worker_id,
361 target)));
362 }
363 }
364
365 Evaluator::~Evaluator()
366 {}
367
368
369 void Evaluator::RateGraphs(std::vector<sg::Graph>& graphs) {
370 // make sure necessary subfolders exist in work folder
371 audio_folder_ = work_folder_ + "/audio/";
372 osc_folder_ = work_folder_ + "/osc/";
373 sc_folder_ = work_folder_ + "/sc/";
374 synthdef_folder_ = work_folder_ + "/synthdefs/";
375 boost::filesystem::create_directories(audio_folder_);
376 boost::filesystem::create_directories(osc_folder_);
377 boost::filesystem::create_directories(sc_folder_);
378 boost::filesystem::create_directories(synthdef_folder_);
379 // find zero padding length for synth names
380 synth_name_zero_pad_length_ = std::log10(graphs.size()) + 1;
381 // store the graphs in the work queue
382 boost::mutex::scoped_lock lock(work_mutex_);
383 graphs_to_rate_ = static_cast<int>(graphs.size());
384 for (int i = 0; i < graphs_to_rate_; i++) {
385 work_queue_.push(&graphs[i]);
386 }
387 // wait for graphs to be rated
388 std::size_t last_rated_count = 0;
389 while (graphs_to_rate_ > 0) {
390 queue_condition_.notify_all();
391 work_done_condition_.wait(lock);
392 if (listener_ != NULL) {
393 std::size_t rated_count = graphs.size() - graphs_to_rate_;
394 if (rated_count != last_rated_count) {
395 last_rated_count = rated_count;
396 listener_->GraphRatedNotification(rated_count);
397 }
398 }
399 }
400 // remove OSC folder, no longer required
401 boost::filesystem::remove_all(osc_folder_);
402 }
403
404 void Evaluator::SetWorkFolder(const std::string& path) {
405 work_folder_ = path;
406 }
407
408 } // sc namespace