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