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