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