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