radically rethink export/bounce/freeze code design. probably not 100% done by freeze...
[ardour.git] / libs / ardour / export_handler.cc
1 /*
2     Copyright (C) 2008-2009 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 #include <glibmm/convert.h>
25
26 #include "pbd/convert.h"
27 #include "pbd/filesystem.h"
28
29 #include "ardour/ardour.h"
30 #include "ardour/configuration.h"
31 #include "ardour/export_graph_builder.h"
32 #include "ardour/export_timespan.h"
33 #include "ardour/export_channel_configuration.h"
34 #include "ardour/export_status.h"
35 #include "ardour/export_format_specification.h"
36 #include "ardour/export_filename.h"
37 #include "ardour/export_failed.h"
38
39 #include "i18n.h"
40
41 using namespace std;
42 using namespace PBD;
43
44 namespace ARDOUR
45 {
46
47 /*** ExportElementFactory ***/
48
49 ExportElementFactory::ExportElementFactory (Session & session) :
50   session (session)
51 {
52
53 }
54
55 ExportElementFactory::~ExportElementFactory ()
56 {
57
58 }
59
60 ExportTimespanPtr
61 ExportElementFactory::add_timespan ()
62 {
63         return ExportTimespanPtr (new ExportTimespan (session.get_export_status(), session.frame_rate()));
64 }
65
66 ExportChannelConfigPtr
67 ExportElementFactory::add_channel_config ()
68 {
69         return ExportChannelConfigPtr (new ExportChannelConfiguration (session));
70 }
71
72 ExportFormatSpecPtr
73 ExportElementFactory::add_format ()
74 {
75         return ExportFormatSpecPtr (new ExportFormatSpecification (session));
76 }
77
78 ExportFormatSpecPtr
79 ExportElementFactory::add_format (XMLNode const & state)
80 {
81         return ExportFormatSpecPtr (new ExportFormatSpecification (session, state));
82 }
83
84 ExportFormatSpecPtr
85 ExportElementFactory::add_format_copy (ExportFormatSpecPtr other)
86 {
87         return ExportFormatSpecPtr (new ExportFormatSpecification (*other));
88 }
89
90 ExportFilenamePtr
91 ExportElementFactory::add_filename ()
92 {
93         return ExportFilenamePtr (new ExportFilename (session));
94 }
95
96 ExportFilenamePtr
97 ExportElementFactory::add_filename_copy (ExportFilenamePtr other)
98 {
99         return ExportFilenamePtr (new ExportFilename (*other));
100 }
101
102 /*** ExportHandler ***/
103
104 ExportHandler::ExportHandler (Session & session)
105   : ExportElementFactory (session)
106   , session (session)
107   , graph_builder (new ExportGraphBuilder (session))
108   , export_status (session.get_export_status ())
109   , realtime (false)
110   , normalizing (false)
111   , cue_tracknum (0)
112   , cue_indexnum (0)
113 {
114 }
115
116 ExportHandler::~ExportHandler ()
117 {
118         // TODO remove files that were written but not finsihed
119 }
120
121 bool
122 ExportHandler::add_export_config (ExportTimespanPtr timespan, ExportChannelConfigPtr channel_config,
123                                   ExportFormatSpecPtr format, ExportFilenamePtr filename,
124                                   BroadcastInfoPtr broadcast_info)
125 {
126         FileSpec spec (channel_config, format, filename, broadcast_info);
127         ConfigPair pair (timespan, spec);
128         config_map.insert (pair);
129
130         return true;
131 }
132
133 void
134 ExportHandler::do_export (bool rt)
135 {
136         /* Count timespans */
137
138         export_status->init();
139         std::set<ExportTimespanPtr> timespan_set;
140         for (ConfigMap::iterator it = config_map.begin(); it != config_map.end(); ++it) {
141                 bool new_timespan = timespan_set.insert (it->first).second;
142                 if (new_timespan) {
143                         export_status->total_frames += it->first->get_length();
144                 }
145         }
146         export_status->total_timespans = timespan_set.size();
147
148         /* Start export */
149
150         realtime = rt;
151         start_timespan ();
152 }
153
154 void
155 ExportHandler::start_timespan ()
156 {
157         export_status->timespan++;
158
159         if (config_map.empty()) {
160                 // freewheeling has to be stopped from outside the process cycle
161                 export_status->running = false;
162                 return;
163         }
164
165         current_timespan = config_map.begin()->first;
166         export_status->total_frames_current_timespan = current_timespan->get_length();
167         export_status->timespan_name = current_timespan->name();
168         export_status->processed_frames_current_timespan = 0;
169
170         /* Register file configurations to graph builder */
171
172         timespan_bounds = config_map.equal_range (current_timespan);
173         graph_builder->reset ();
174         graph_builder->set_current_timespan (current_timespan);
175         for (ConfigMap::iterator it = timespan_bounds.first; it != timespan_bounds.second; ++it) {
176                 // Filenames can be shared across timespans
177                 FileSpec & spec = it->second;
178                 spec.filename->set_timespan (it->first);
179                 graph_builder->add_config (spec);
180         }
181
182         /* start export */
183
184         normalizing = false;
185         session.ProcessExport.connect_same_thread (process_connection, boost::bind (&ExportHandler::process, this, _1));
186         process_position = current_timespan->get_start();
187         session.start_audio_export (process_position, realtime);
188 }
189
190 int
191 ExportHandler::process (framecnt_t frames)
192 {
193         if (!export_status->running) {
194                 return 0;
195         } else if (normalizing) {
196                 return process_normalize ();
197         } else {
198                 return process_timespan (frames);
199         }
200 }
201
202 int
203 ExportHandler::process_timespan (framecnt_t frames)
204 {
205         /* update position */
206
207         framecnt_t frames_to_read = 0;
208         framepos_t const end = current_timespan->get_end();
209
210         bool const last_cycle = (process_position + frames >= end);
211
212         if (last_cycle) {
213                 frames_to_read = end - process_position;
214                 export_status->stop = true;
215         } else {
216                 frames_to_read = frames;
217         }
218
219         process_position += frames_to_read;
220         export_status->processed_frames += frames_to_read;
221         export_status->processed_frames_current_timespan += frames_to_read;
222
223         /* Do actual processing */
224         int ret = graph_builder->process (frames_to_read, last_cycle);
225
226         /* Start normalizing if necessary */
227         if (last_cycle) {
228                 normalizing = graph_builder->will_normalize();
229                 if (normalizing) {
230                         export_status->total_normalize_cycles = graph_builder->get_normalize_cycle_count();
231                         export_status->current_normalize_cycle = 0;
232                 } else {
233                         finish_timespan ();
234                         return 0;
235                 }
236         }
237
238         return ret;
239 }
240
241 int
242 ExportHandler::process_normalize ()
243 {
244         if (graph_builder->process_normalize ()) {
245                 finish_timespan ();
246                 export_status->normalizing = false;
247         } else {
248                 export_status->normalizing = true;
249         }
250
251         export_status->current_normalize_cycle++;
252
253         return 0;
254 }
255
256 void
257 ExportHandler::finish_timespan ()
258 {
259         while (config_map.begin() != timespan_bounds.second) {
260
261                 ExportFormatSpecPtr fmt = config_map.begin()->second.format;
262
263                 if (fmt->with_cue()) {
264                         export_cd_marker_file (current_timespan, fmt, config_map.begin()->second.filename->get_path(fmt), CDMarkerCUE);
265                 } 
266
267                 if (fmt->with_toc()) {
268                         export_cd_marker_file (current_timespan, fmt, config_map.begin()->second.filename->get_path(fmt), CDMarkerTOC);
269                 }
270
271                 config_map.erase (config_map.begin());
272         }
273
274         start_timespan ();
275 }
276
277 /*** CD Marker sutff ***/
278
279 struct LocationSortByStart {
280     bool operator() (Location *a, Location *b) {
281             return a->start() < b->start();
282     }
283 };
284
285 void
286 ExportHandler::export_cd_marker_file (ExportTimespanPtr timespan, ExportFormatSpecPtr file_format,
287                                       std::string filename, CDMarkerFormat format)
288 {
289         string filepath = get_cd_marker_filename(filename, format);
290
291         try {
292                 void (ExportHandler::*header_func) (CDMarkerStatus &);
293                 void (ExportHandler::*track_func) (CDMarkerStatus &);
294                 void (ExportHandler::*index_func) (CDMarkerStatus &);
295
296                 switch (format) {
297                 case CDMarkerTOC:
298                         header_func = &ExportHandler::write_toc_header;
299                         track_func = &ExportHandler::write_track_info_toc;
300                         index_func = &ExportHandler::write_index_info_toc;
301                         break;
302                 case CDMarkerCUE:
303                         header_func = &ExportHandler::write_cue_header;
304                         track_func = &ExportHandler::write_track_info_cue;
305                         index_func = &ExportHandler::write_index_info_cue;
306                         break;
307                 default:
308                         return;
309                 }
310
311                 CDMarkerStatus status (filepath, timespan, file_format, filename);
312
313                 if (!status.out) {
314                         error << string_compose(_("Editor: cannot open \"%1\" as export file for CD marker file"), filepath) << endmsg;
315                         return;
316                 }
317
318                 (this->*header_func) (status);
319
320                 /* Get locations and sort */
321
322                 Locations::LocationList const & locations (session.locations()->list());
323                 Locations::LocationList::const_iterator i;
324                 Locations::LocationList temp;
325
326                 for (i = locations.begin(); i != locations.end(); ++i) {
327                         if ((*i)->start() >= timespan->get_start() && (*i)->end() <= timespan->get_end() && (*i)->is_cd_marker() && !(*i)->is_session_range()) {
328                                 temp.push_back (*i);
329                         }
330                 }
331
332                 if (temp.empty()) {
333                         // TODO One index marker for whole thing
334                         return;
335                 }
336
337                 LocationSortByStart cmp;
338                 temp.sort (cmp);
339                 Locations::LocationList::const_iterator nexti;
340
341                 /* Start actual marker stuff */
342
343                 framepos_t last_end_time = timespan->get_start(), last_start_time = timespan->get_start();
344                 status.track_position = last_start_time - timespan->get_start();
345
346                 for (i = temp.begin(); i != temp.end(); ++i) {
347
348                         status.marker = *i;
349
350                         if ((*i)->start() < last_end_time) {
351                                 if ((*i)->is_mark()) {
352                                         /* Index within track */
353
354                                         status.index_position = (*i)->start() - timespan->get_start();
355                                         (this->*index_func) (status);
356                                 }
357
358                                 continue;
359                         }
360
361                         /* A track, defined by a cd range marker or a cd location marker outside of a cd range */
362
363                         status.track_position = last_end_time - timespan->get_start();
364                         status.track_start_frame = (*i)->start() - timespan->get_start();  // everything before this is the pregap
365                         status.track_duration = 0;
366
367                         if ((*i)->is_mark()) {
368                                 // a mark track location needs to look ahead to the next marker's start to determine length
369                                 nexti = i;
370                                 ++nexti;
371
372                                 if (nexti != temp.end()) {
373                                         status.track_duration = (*nexti)->start() - last_end_time;
374
375                                         last_start_time = (*i)->start();
376                                         last_end_time = (*nexti)->start();
377                                 } else {
378                                         // this was the last marker, use timespan end
379                                         status.track_duration = timespan->get_end() - last_end_time;
380
381                                         last_start_time = (*i)->start();
382                                         last_end_time = timespan->get_end();
383                                 }
384                         } else {
385                                 // range
386                                 status.track_duration = (*i)->end() - last_end_time;
387
388                                 last_start_time = (*i)->start();
389                                 last_end_time = (*i)->end();
390                         }
391
392                         (this->*track_func) (status);
393                 }
394
395         } catch (std::exception& e) {
396                 error << string_compose (_("an error occured while writing a TOC/CUE file: %1"), e.what()) << endmsg;
397                 ::unlink (filepath.c_str());
398         } catch (Glib::Exception& e) {
399                 error << string_compose (_("an error occured while writing a TOC/CUE file: %1"), e.what()) << endmsg;
400                 ::unlink (filepath.c_str());
401         }
402 }
403
404 string
405 ExportHandler::get_cd_marker_filename(std::string filename, CDMarkerFormat format)
406 {
407         /* do not strip file suffix because there may be more than one format, 
408            and we do not want the CD marker file from one format to overwrite
409            another (e.g. foo.wav.cue > foo.aiff.cue)
410         */
411
412         switch (format) {
413           case CDMarkerTOC:
414                 return filename + ".toc";
415           case CDMarkerCUE:
416                 return filename + ".cue";
417           default:
418                 return filename + ".marker"; // Should not be reached when actually creating a file
419         }
420 }
421
422 void
423 ExportHandler::write_cue_header (CDMarkerStatus & status)
424 {
425         string title = status.timespan->name().compare ("Session") ? status.timespan->name() : (string) session.name();
426
427         status.out << "REM Cue file generated by " << PROGRAM_NAME << endl;
428         status.out << "TITLE " << cue_escape_cdtext (title) << endl;
429
430         /*  The original cue sheet sepc metions five file types
431                 WAVE, AIFF,
432                 BINARY   = "header-less" audio (44.1 kHz, 16 Bit, little endian),
433                 MOTOROLA = "header-less" audio (44.1 kHz, 16 Bit, big endian),
434                 and MP3
435                 
436                 We try to use these file types whenever appropriate and 
437                 default to our own names otherwise.
438         */
439         status.out << "FILE \"" << Glib::path_get_basename(status.filename) << "\" ";
440         if (!status.format->format_name().compare ("WAV")  || !status.format->format_name().compare ("BWF")) {
441                 status.out  << "WAVE";
442         } else if (status.format->format_id() == ExportFormatBase::F_RAW &&
443                    status.format->sample_format() == ExportFormatBase::SF_16 &&
444                    status.format->sample_rate() == ExportFormatBase::SR_44_1) {
445                 // Format is RAW 16bit 44.1kHz
446                 if (status.format->endianness() == ExportFormatBase::E_Little) {
447                         status.out << "BINARY";
448                 } else {
449                         status.out << "MOTOROLA";
450                 }
451         } else {
452                 // no special case for AIFF format it's name is already "AIFF"
453                 status.out << status.format->format_name();
454         }
455         status.out << endl;
456 }
457
458 void
459 ExportHandler::write_toc_header (CDMarkerStatus & status)
460 {
461         string title = status.timespan->name().compare ("Session") ? status.timespan->name() : (string) session.name();
462
463         status.out << "CD_DA" << endl;
464         status.out << "CD_TEXT {" << endl << "  LANGUAGE_MAP {" << endl << "    0 : EN" << endl << "  }" << endl;
465         status.out << "  LANGUAGE 0 {" << endl << "    TITLE " << toc_escape_cdtext (title) << endl ;
466         status.out << "    PERFORMER \"\"" << endl << "  }" << endl << "}" << endl;
467 }
468
469 void
470 ExportHandler::write_track_info_cue (CDMarkerStatus & status)
471 {
472         gchar buf[18];
473
474         snprintf (buf, sizeof(buf), "  TRACK %02d AUDIO", status.track_number);
475         status.out << buf << endl;
476
477         status.out << "    FLAGS" ;
478         if (status.marker->cd_info.find("scms") != status.marker->cd_info.end())  {
479                 status.out << " SCMS ";
480         } else {
481                 status.out << " DCP ";
482         }
483
484         if (status.marker->cd_info.find("preemph") != status.marker->cd_info.end())  {
485                 status.out << " PRE";
486         }
487         status.out << endl;
488
489         if (status.marker->cd_info.find("isrc") != status.marker->cd_info.end())  {
490                 status.out << "    ISRC " << status.marker->cd_info["isrc"] << endl;
491         }
492
493         if (status.marker->name() != "") {
494                 status.out << "    TITLE " << cue_escape_cdtext (status.marker->name()) << endl;
495         }
496
497         if (status.marker->cd_info.find("performer") != status.marker->cd_info.end()) {
498                 status.out <<  "    PERFORMER " << cue_escape_cdtext (status.marker->cd_info["performer"]) << endl;
499         }
500
501         if (status.marker->cd_info.find("composer") != status.marker->cd_info.end()) {
502                 status.out << "    SONGWRITER " << cue_escape_cdtext (status.marker->cd_info["composer"]) << endl;
503         }
504
505         if (status.track_position != status.track_start_frame) {
506                 frames_to_cd_frames_string (buf, status.track_position);
507                 status.out << "    INDEX 00" << buf << endl;
508         }
509
510         frames_to_cd_frames_string (buf, status.track_start_frame);
511         status.out << "    INDEX 01" << buf << endl;
512
513         status.index_number = 2;
514         status.track_number++;
515 }
516
517 void
518 ExportHandler::write_track_info_toc (CDMarkerStatus & status)
519 {
520         gchar buf[18];
521
522         status.out << endl << "TRACK AUDIO" << endl;
523
524         if (status.marker->cd_info.find("scms") != status.marker->cd_info.end())  {
525                 status.out << "NO ";
526         }
527         status.out << "COPY" << endl;
528
529         if (status.marker->cd_info.find("preemph") != status.marker->cd_info.end())  {
530                 status.out << "PRE_EMPHASIS" << endl;
531         } else {
532                 status.out << "NO PRE_EMPHASIS" << endl;
533         }
534
535         if (status.marker->cd_info.find("isrc") != status.marker->cd_info.end())  {
536                 status.out << "ISRC \"" << status.marker->cd_info["isrc"] << "\"" << endl;
537         }
538
539         status.out << "CD_TEXT {" << endl << "  LANGUAGE 0 {" << endl;
540         status.out << "     TITLE " << toc_escape_cdtext (status.marker->name()) << endl;
541         
542         status.out << "     PERFORMER ";
543         if (status.marker->cd_info.find("performer") != status.marker->cd_info.end()) {
544                 status.out << toc_escape_cdtext (status.marker->cd_info["performer"]) << endl;
545         } else {
546                 status.out << "\"\"" << endl;
547         }
548         
549         if (status.marker->cd_info.find("composer") != status.marker->cd_info.end()) {
550                 status.out  << "     SONGWRITER " << toc_escape_cdtext (status.marker->cd_info["composer"]) << endl;
551         }
552
553         if (status.marker->cd_info.find("isrc") != status.marker->cd_info.end()) {
554                 status.out  << "     ISRC \"";
555                 status.out << status.marker->cd_info["isrc"].substr(0,2) << "-";
556                 status.out << status.marker->cd_info["isrc"].substr(2,3) << "-";
557                 status.out << status.marker->cd_info["isrc"].substr(5,2) << "-";
558                 status.out << status.marker->cd_info["isrc"].substr(7,5) << "\"" << endl;
559         }
560
561         status.out << "  }" << endl << "}" << endl;
562
563         frames_to_cd_frames_string (buf, status.track_position);
564         status.out << "FILE " << toc_escape_filename (status.filename) << ' ' << buf;
565
566         frames_to_cd_frames_string (buf, status.track_duration);
567         status.out << buf << endl;
568
569         frames_to_cd_frames_string (buf, status.track_start_frame - status.track_position);
570         status.out << "START" << buf << endl;
571 }
572
573 void
574 ExportHandler::write_index_info_cue (CDMarkerStatus & status)
575 {
576         gchar buf[18];
577
578         snprintf (buf, sizeof(buf), "    INDEX %02d", cue_indexnum);
579         status.out << buf;
580         frames_to_cd_frames_string (buf, status.index_position);
581         status.out << buf << endl;
582
583         cue_indexnum++;
584 }
585
586 void
587 ExportHandler::write_index_info_toc (CDMarkerStatus & status)
588 {
589         gchar buf[18];
590
591         frames_to_cd_frames_string (buf, status.index_position - status.track_position);
592         status.out << "INDEX" << buf << endl;
593 }
594
595 void
596 ExportHandler::frames_to_cd_frames_string (char* buf, framepos_t when)
597 {
598         framecnt_t remainder;
599         framecnt_t fr = session.nominal_frame_rate();
600         int mins, secs, frames;
601
602         mins = when / (60 * fr);
603         remainder = when - (mins * 60 * fr);
604         secs = remainder / fr;
605         remainder -= secs * fr;
606         frames = remainder / (fr / 75);
607         sprintf (buf, " %02d:%02d:%02d", mins, secs, frames);
608 }
609
610 std::string
611 ExportHandler::toc_escape_cdtext (const std::string& txt)
612 {
613         Glib::ustring check (txt);
614         std::string out;
615         std::string latin1_txt;
616         char buf[5];
617
618         try {
619                 latin1_txt = Glib::convert (txt, "ISO-8859-1", "UTF-8");
620         } catch (Glib::ConvertError& err) {
621                 throw Glib::ConvertError (err.code(), string_compose (_("Cannot convert %1 to Latin-1 text"), txt));
622         }
623
624         out = '"';
625
626         for (std::string::const_iterator c = latin1_txt.begin(); c != latin1_txt.end(); ++c) {
627
628                 if ((*c) == '"') {
629                         out += "\\\"";
630                 } else if ((*c) == '\\') {
631                         out += "\\134";
632                 } else if (isprint (*c)) {
633                         out += *c;
634                 } else {
635                         snprintf (buf, sizeof (buf), "\\%03o", (int) (unsigned char) *c);
636                         out += buf;
637                 }
638         }
639         
640         out += '"';
641
642         return out;
643 }
644
645 std::string
646 ExportHandler::toc_escape_filename (const std::string& txt)
647 {
648         std::string out;
649
650         out = '"';
651
652         // We iterate byte-wise not character-wise over a UTF-8 string here,
653         // because we only want to translate backslashes and double quotes
654         for (std::string::const_iterator c = txt.begin(); c != txt.end(); ++c) {
655
656                 if (*c == '"') {
657                         out += "\\\"";
658                 } else if (*c == '\\') {
659                         out += "\\134";
660                 } else {
661                         out += *c;
662                 }
663         }
664         
665         out += '"';
666
667         return out;
668 }
669
670 std::string
671 ExportHandler::cue_escape_cdtext (const std::string& txt)
672 {
673         std::string latin1_txt;
674         std::string out;
675         
676         try {
677                 latin1_txt = Glib::convert (txt, "ISO-8859-1", "UTF-8");
678         } catch (Glib::ConvertError& err) {
679                 throw Glib::ConvertError (err.code(), string_compose (_("Cannot convert %1 to Latin-1 text"), txt));
680         }
681         
682         // does not do much mor than UTF-8 to Latin1 translation yet, but
683         // that may have to change if cue parsers in burning programs change 
684         out = '"' + latin1_txt + '"';
685
686         return out;
687 }
688
689 } // namespace ARDOUR