use new syntax for connecting to backend signals that enforces explicit connection...
[ardour.git] / libs / ardour / export_handler.cc
1 /*
2     Copyright (C) 2008 Paul Davis
3     Author: Sakari Bergen
4
5     This program 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 2 of the License, or
8     (at your option) any later version.
9
10     This program 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 this program; if not, write to the Free Software
17     Foundation, Inc., 675 Mass Ave, Cambridge, MA 02139, USA.
18
19 */
20
21 #include "ardour/export_handler.h"
22
23 #include <glibmm.h>
24
25 #include "pbd/convert.h"
26 #include "pbd/filesystem.h"
27
28 #include "ardour/ardour.h"
29 #include "ardour/configuration.h"
30 #include "ardour/export_timespan.h"
31 #include "ardour/export_channel_configuration.h"
32 #include "ardour/export_status.h"
33 #include "ardour/export_format_specification.h"
34 #include "ardour/export_filename.h"
35 #include "ardour/export_processor.h"
36 #include "ardour/export_failed.h"
37
38 using namespace std;
39 using namespace PBD;
40
41 namespace ARDOUR
42 {
43
44 /*** ExportElementFactory ***/
45
46 ExportElementFactory::ExportElementFactory (Session & session) :
47   session (session)
48 {
49
50 }
51
52 ExportElementFactory::~ExportElementFactory ()
53 {
54
55 }
56
57 ExportElementFactory::TimespanPtr
58 ExportElementFactory::add_timespan ()
59 {
60         return TimespanPtr (new ExportTimespan (session.get_export_status(), session.frame_rate()));
61 }
62
63 ExportElementFactory::ChannelConfigPtr
64 ExportElementFactory::add_channel_config ()
65 {
66         return ChannelConfigPtr (new ExportChannelConfiguration (session));
67 }
68
69 ExportElementFactory::FormatPtr
70 ExportElementFactory::add_format ()
71 {
72         return FormatPtr (new ExportFormatSpecification (session));
73 }
74
75 ExportElementFactory::FormatPtr
76 ExportElementFactory::add_format (XMLNode const & state)
77 {
78         return FormatPtr (new ExportFormatSpecification (session, state));
79 }
80
81 ExportElementFactory::FormatPtr
82 ExportElementFactory::add_format_copy (FormatPtr other)
83 {
84         return FormatPtr (new ExportFormatSpecification (*other));
85 }
86
87 ExportElementFactory::FilenamePtr
88 ExportElementFactory::add_filename ()
89 {
90         return FilenamePtr (new ExportFilename (session));
91 }
92
93 ExportElementFactory::FilenamePtr
94 ExportElementFactory::add_filename_copy (FilenamePtr other)
95 {
96         return FilenamePtr (new ExportFilename (*other));
97 }
98
99 /*** ExportHandler ***/
100
101 ExportHandler::ExportHandler (Session & session) 
102         : ExportElementFactory (session)
103         , session (session)
104         , export_status (session.get_export_status ())
105         , realtime (false)
106 {
107         processor.reset (new ExportProcessor (session));
108
109         ExportProcessor::WritingFile.connect (files_written_connection, boost::bind (&ExportHandler::add_file, this, _1));
110 }
111
112 ExportHandler::~ExportHandler ()
113 {
114         if (export_status->aborted()) {
115                 for (std::list<Glib::ustring>::iterator it = files_written.begin(); it != files_written.end(); ++it) {
116                         sys::remove (sys::path (*it));
117                 }
118         }
119
120         channel_config_connection.disconnect();
121         files_written_connection.disconnect();
122 }
123
124 void
125 ExportHandler::add_file (const Glib::ustring& str)
126 {
127         files_written.push_back (str);
128 }
129
130 bool
131 ExportHandler::add_export_config (TimespanPtr timespan, ChannelConfigPtr channel_config, FormatPtr format, FilenamePtr filename)
132 {
133         FileSpec spec (channel_config, format, filename);
134         ConfigPair pair (timespan, spec);
135         config_map.insert (pair);
136
137         return true;
138 }
139
140 /// Starts exporting the registered configurations
141 /** The following happens, when do_export is called:
142  * 1. Session is prepared in do_export
143  * 2. start_timespan is called, which then registers all necessary channel configs to a timespan
144  * 3. The timespan reads each unique channel into a tempfile and calls Session::stop_export when the end is reached
145  * 4. stop_export emits ExportReadFinished after stopping the transport, this ends up calling finish_timespan
146  * 5. finish_timespan registers all the relevant formats and filenames to relevant channel configurations
147  * 6. finish_timespan does a manual call to timespan_thread_finished, which gets the next channel configuration
148  *    for the current timespan, calling write_files for it
149  * 7. write_files writes the actual export files, composing them from the individual channels from tempfiles and
150  *    emits FilesWritten when it is done, which ends up calling timespan_thread_finished
151  * 8. Once all channel configs are written, a new timespan is started by calling start_timespan
152  * 9. When all timespans are written the session is taken out of export.
153  */
154
155 void
156 ExportHandler::do_export (bool rt)
157 {
158         /* Count timespans */
159
160         export_status->init();
161         std::set<TimespanPtr> timespan_set;
162         for (ConfigMap::iterator it = config_map.begin(); it != config_map.end(); ++it) {
163                 timespan_set.insert (it->first);
164         }
165         export_status->total_timespans = timespan_set.size();
166
167         /* Start export */
168
169         realtime = rt;
170
171         session.ExportReadFinished.connect (export_read_finished_connection, boost::bind (&ExportHandler::finish_timespan, this));
172         start_timespan ();
173 }
174
175 struct LocationSortByStart {
176     bool operator() (Location *a, Location *b) {
177             return a->start() < b->start();
178     }
179 };
180
181 void
182 ExportHandler::export_cd_marker_file (TimespanPtr timespan, FormatPtr file_format, std::string filename, CDMarkerFormat format)
183 {
184         string filepath;
185         string basename = Glib::path_get_basename(filename);
186
187         size_t ext_pos = basename.rfind('.');
188         if (ext_pos != string::npos) {
189                 basename = basename.substr(0, ext_pos); /* strip file extension, if there is one */
190         }
191
192         void (ExportHandler::*header_func) (CDMarkerStatus &);
193         void (ExportHandler::*track_func) (CDMarkerStatus &);
194         void (ExportHandler::*index_func) (CDMarkerStatus &);
195
196         switch (format) {
197           case CDMarkerTOC:
198                 filepath = Glib::build_filename(Glib::path_get_dirname(filename), basename + ".toc");
199                 header_func = &ExportHandler::write_toc_header;
200                 track_func = &ExportHandler::write_track_info_toc;
201                 index_func = &ExportHandler::write_index_info_toc;
202                 break;
203           case CDMarkerCUE:
204                 filepath = Glib::build_filename(Glib::path_get_dirname(filename), basename + ".cue");
205                 header_func = &ExportHandler::write_cue_header;
206                 track_func = &ExportHandler::write_track_info_cue;
207                 index_func = &ExportHandler::write_index_info_cue;
208                 break;
209           default:
210                 return;
211         }
212
213         CDMarkerStatus status (filepath, timespan, file_format, filename);
214
215         if (!status.out) {
216                 error << string_compose(_("Editor: cannot open \"%1\" as export file for CD marker file"), filepath) << endmsg;
217                 return;
218         }
219
220         (this->*header_func) (status);
221
222         /* Get locations and sort */
223
224         Locations::LocationList const & locations (session.locations()->list());
225         Locations::LocationList::const_iterator i;
226         Locations::LocationList temp;
227
228         for (i = locations.begin(); i != locations.end(); ++i) {
229                 if ((*i)->start() >= timespan->get_start() && (*i)->end() <= timespan->get_end() && (*i)->is_cd_marker() && !(*i)->is_end()) {
230                         temp.push_back (*i);
231                 }
232         }
233
234         if (temp.empty()) {
235                 // TODO One index marker for whole thing
236                 return;
237         }
238
239         LocationSortByStart cmp;
240         temp.sort (cmp);
241         Locations::LocationList::const_iterator nexti;
242
243         /* Start actual marker stuff */
244
245         nframes_t last_end_time = timespan->get_start(), last_start_time = timespan->get_start();
246         status.track_position = last_start_time - timespan->get_start();
247
248         for (i = temp.begin(); i != temp.end(); ++i) {
249
250                 status.marker = *i;
251
252                 if ((*i)->start() < last_end_time) {
253                         if ((*i)->is_mark()) {
254                                 /* Index within track */
255
256                                 status.index_position = (*i)->start() - timespan->get_start();
257                                 (this->*index_func) (status);
258                         }
259
260                         continue;
261                 }
262
263                 /* A track, defined by a cd range marker or a cd location marker outside of a cd range */
264
265                 status.track_position = last_end_time - timespan->get_start();
266                 status.track_start_frame = (*i)->start() - timespan->get_start();  // everything before this is the pregap
267                 status.track_duration = 0;
268
269                 if ((*i)->is_mark()) {
270                         // a mark track location needs to look ahead to the next marker's start to determine length
271                         nexti = i;
272                         ++nexti;
273
274                         if (nexti != temp.end()) {
275                                 status.track_duration = (*nexti)->start() - last_end_time;
276
277                                 last_start_time = (*i)->start();
278                                 last_end_time = (*nexti)->start();
279                         } else {
280                                 // this was the last marker, use timespan end
281                                 status.track_duration = timespan->get_end() - last_end_time;
282
283                                 last_start_time = (*i)->start();
284                                 last_end_time = timespan->get_end();
285                         }
286                 } else {
287                         // range
288                         status.track_duration = (*i)->end() - last_end_time;
289
290                         last_start_time = (*i)->start();
291                         last_end_time = (*i)->end();
292                 }
293
294                 (this->*track_func) (status);
295         }
296 }
297
298 void
299 ExportHandler::write_cue_header (CDMarkerStatus & status)
300 {
301         Glib::ustring title = status.timespan->name().compare ("Session") ? status.timespan->name() : (Glib::ustring) session.name();
302
303         status.out << "REM Cue file generated by Ardour" << endl;
304         status.out << "TITLE \"" << title << "\"" << endl;
305
306         /*  The cue sheet syntax has originally five file types:
307                 WAVE     : 44.1 kHz, 16 Bit (little endian)
308                 AIFF     : 44.1 kHz, 16 Bit (big endian)
309                 BINARY   : 44.1 kHz, 16 Bit (little endian)
310                 MOTOROLA : 44.1 kHz, 16 Bit (big endian)
311                 MP3
312
313                 We want to use cue sheets not only as CD images but also as general playlyist
314                 format, thus for WAVE and AIFF we don't care if it's really 44.1 kHz/16 Bit, the
315                 soundfile's header shows it anyway.  But for the raw formats, i.e. BINARY
316                 and MOTOROLA we do care, because no header would tell us about a different format.
317
318                 For all other formats we just make up our own file type.  MP3 is not supported
319                 at the moment.
320         */
321
322         status.out << "FILE \"" << Glib::path_get_basename(status.filename) << "\" ";
323         if (!status.format->format_name().compare ("WAV")) {
324                 status.out  << "WAVE";
325         } else if (status.format->format_name() == ExportFormatBase::F_RAW &&
326                    status.format->sample_format() == ExportFormatBase::SF_16 &&
327                    status.format->sample_rate() == ExportFormatBase::SR_44_1) {
328                 // Format is RAW 16bit 44.1kHz
329                 if (status.format->endianness() == ExportFormatBase::E_Little) {
330                         status.out << "BINARY";
331                 } else {
332                         status.out << "MOTOROLA";
333                 }
334         } else {
335                 // AIFF should return "AIFF"
336                 status.out << status.format->format_name();
337         }
338         status.out << endl;
339 }
340
341 void
342 ExportHandler::write_toc_header (CDMarkerStatus & status)
343 {
344         Glib::ustring title = status.timespan->name().compare ("Session") ? status.timespan->name() : (Glib::ustring) session.name();
345
346         status.out << "CD_DA" << endl;
347         status.out << "CD_TEXT {" << endl << "  LANGUAGE_MAP {" << endl << "    0 : EN" << endl << "  }" << endl;
348         status.out << "  LANGUAGE 0 {" << endl << "    TITLE \"" << title << "\"" << endl << "  }" << endl << "}" << endl;
349 }
350
351 void
352 ExportHandler::write_track_info_cue (CDMarkerStatus & status)
353 {
354         gchar buf[18];
355
356         snprintf (buf, sizeof(buf), "  TRACK %02d AUDIO", status.track_number);
357         status.out << buf << endl;
358
359         status.out << "    FLAGS" ;
360         if (status.marker->cd_info.find("scms") != status.marker->cd_info.end())  {
361                 status.out << " SCMS ";
362         } else {
363                 status.out << " DCP ";
364         }
365
366         if (status.marker->cd_info.find("preemph") != status.marker->cd_info.end())  {
367                 status.out << " PRE";
368         }
369         status.out << endl;
370
371         if (status.marker->cd_info.find("isrc") != status.marker->cd_info.end())  {
372                 status.out << "    ISRC " << status.marker->cd_info["isrc"] << endl;
373
374         }
375         if (status.marker->name() != "") {
376                 status.out << "    TITLE \"" << status.marker->name() << "\"" << endl;
377         }
378
379         if (status.marker->cd_info.find("performer") != status.marker->cd_info.end()) {
380                 status.out << "    PERFORMER \"" <<  status.marker->cd_info["performer"] << "\"" << endl;
381         }
382
383         if (status.marker->cd_info.find("string_composer") != status.marker->cd_info.end()) {
384                 status.out << "    SONGWRITER \"" << status.marker->cd_info["string_composer"]  << "\"" << endl;
385         }
386
387         if (status.track_position != status.track_start_frame) {
388                 frames_to_cd_frames_string (buf, status.track_position);
389                 status.out << "    INDEX 00" << buf << endl;
390         }
391
392         frames_to_cd_frames_string (buf, status.track_start_frame);
393         status.out << "    INDEX 01" << buf << endl;
394
395         status.index_number = 2;
396         status.track_number++;
397 }
398
399 void
400 ExportHandler::write_track_info_toc (CDMarkerStatus & status)
401 {
402         gchar buf[18];
403
404         status.out << endl << "TRACK AUDIO" << endl;
405
406         if (status.marker->cd_info.find("scms") != status.marker->cd_info.end())  {
407                 status.out << "NO ";
408         }
409         status.out << "COPY" << endl;
410
411         if (status.marker->cd_info.find("preemph") != status.marker->cd_info.end())  {
412                 status.out << "PRE_EMPHASIS" << endl;
413         } else {
414                 status.out << "NO PRE_EMPHASIS" << endl;
415         }
416
417         if (status.marker->cd_info.find("isrc") != status.marker->cd_info.end())  {
418                 status.out << "ISRC \"" << status.marker->cd_info["isrc"] << "\"" << endl;
419         }
420
421         status.out << "CD_TEXT {" << endl << "  LANGUAGE 0 {" << endl << "     TITLE \"" << status.marker->name() << "\"" << endl;
422         if (status.marker->cd_info.find("performer") != status.marker->cd_info.end()) {
423                 status.out << "     PERFORMER \"" << status.marker->cd_info["performer"]  << "\"" << endl;
424         }
425         if (status.marker->cd_info.find("string_composer") != status.marker->cd_info.end()) {
426                 status.out  << "     COMPOSER \"" << status.marker->cd_info["string_composer"] << "\"" << endl;
427         }
428
429         if (status.marker->cd_info.find("isrc") != status.marker->cd_info.end()) {
430                 status.out  << "     ISRC \"";
431                 status.out << status.marker->cd_info["isrc"].substr(0,2) << "-";
432                 status.out << status.marker->cd_info["isrc"].substr(2,3) << "-";
433                 status.out << status.marker->cd_info["isrc"].substr(5,2) << "-";
434                 status.out << status.marker->cd_info["isrc"].substr(7,5) << "\"" << endl;
435         }
436
437         status.out << "  }" << endl << "}" << endl;
438
439         frames_to_cd_frames_string (buf, status.track_position);
440         status.out << "FILE \"" << status.filename << "\" " << buf;
441
442         frames_to_cd_frames_string (buf, status.track_duration);
443         status.out << buf << endl;
444
445         frames_to_cd_frames_string (buf, status.track_start_frame - status.track_position);
446         status.out << "START" << buf << endl;
447 }
448
449 void
450 ExportHandler::write_index_info_cue (CDMarkerStatus & status)
451 {
452         gchar buf[18];
453
454         snprintf (buf, sizeof(buf), "    INDEX %02d", cue_indexnum);
455         status.out << buf;
456         frames_to_cd_frames_string (buf, status.index_position);
457         status.out << buf << endl;
458
459         cue_indexnum++;
460 }
461
462 void
463 ExportHandler::write_index_info_toc (CDMarkerStatus & status)
464 {
465         gchar buf[18];
466
467         frames_to_cd_frames_string (buf, status.index_position - status.track_position);
468         status.out << "INDEX" << buf << endl;
469 }
470
471 void
472 ExportHandler::frames_to_cd_frames_string (char* buf, nframes_t when)
473 {
474         nframes_t remainder;
475         nframes_t fr = session.nominal_frame_rate();
476         int mins, secs, frames;
477
478         mins = when / (60 * fr);
479         remainder = when - (mins * 60 * fr);
480         secs = remainder / fr;
481         remainder -= secs * fr;
482         frames = remainder / (fr / 75);
483         sprintf (buf, " %02d:%02d:%02d", mins, secs, frames);
484 }
485
486 void
487 ExportHandler::start_timespan ()
488 {
489         export_status->timespan++;
490
491         if (config_map.empty()) {
492                 export_status->finish ();
493                 return;
494         }
495
496         current_timespan = config_map.begin()->first;
497
498         /* Register channel configs with timespan */
499
500         timespan_bounds = config_map.equal_range (current_timespan);
501
502         for (ConfigMap::iterator it = timespan_bounds.first; it != timespan_bounds.second; ++it) {
503                 it->second.channel_config->register_with_timespan (current_timespan);
504         }
505
506         /* connect stuff and start export */
507
508         session.ProcessExport.connect (current_timespan->process_connection, boost::bind (&ExportTimespan::process, current_timespan, _1));
509         session.start_audio_export (current_timespan->get_start(), realtime);
510 }
511
512 void
513 ExportHandler::finish_timespan ()
514 {
515         current_timespan->process_connection.disconnect ();
516
517         /* Register formats and filenames to relevant channel configs */
518
519         export_status->total_formats = 0;
520         export_status->format = 0;
521
522         for (ConfigMap::iterator it = timespan_bounds.first; it != timespan_bounds.second; ++it) {
523
524                 export_status->total_formats++;
525
526                 /* Setup filename */
527
528                 it->second.filename->set_timespan (current_timespan);
529                 it->second.filename->set_channel_config (it->second.channel_config);
530
531                 /* Do actual registration */
532
533                 ChannelConfigPtr chan_config = it->second.channel_config;
534                 chan_config->register_file_config (it->second.format, it->second.filename);
535         }
536
537         /* Start writing files by doing a manual call to timespan_thread_finished */
538
539         current_map_it = timespan_bounds.first;
540         timespan_thread_finished ();
541 }
542
543 void
544 ExportHandler::timespan_thread_finished ()
545 {
546         channel_config_connection.disconnect();
547         export_read_finished_connection.disconnect ();
548
549         if (current_map_it != timespan_bounds.second) {
550
551                 /* Get next configuration as long as no new export process is started */
552
553                 ChannelConfigPtr cc = current_map_it->second.channel_config;
554                 while (!cc->write_files(processor)) {
555
556                         ++current_map_it;
557
558                         if (current_map_it == timespan_bounds.second) {
559
560                                 /* reached end of bounds, this call will end up in the else block below */
561
562                                 timespan_thread_finished ();
563                                 return;
564                         }
565
566                         cc = current_map_it->second.channel_config;
567                 }
568
569                 cc->FilesWritten.connect (channel_config_connection, boost::bind (&ExportHandler::timespan_thread_finished, this));
570                 ++current_map_it;
571
572         } else { /* All files are written from current timespan, reset timespan and start new */
573
574                 /* Unregister configs and remove configs with this timespan */
575
576                 for (ConfigMap::iterator it = timespan_bounds.first; it != timespan_bounds.second;) {
577                         it->second.channel_config->unregister_all ();
578
579                         ConfigMap::iterator to_erase = it;
580                         ++it;
581                         config_map.erase (to_erase);
582                 }
583
584                 /* Start new timespan */
585
586                 start_timespan ();
587
588         }
589 }
590
591 } // namespace ARDOUR