fast region export -- don't call process()
[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 "pbd/gstdio_compat.h"
24 #include <glibmm.h>
25 #include <glibmm/convert.h>
26
27 #include "pbd/convert.h"
28
29 #include "ardour/audioengine.h"
30 #include "ardour/audiofile_tagger.h"
31 #include "ardour/debug.h"
32 #include "ardour/export_graph_builder.h"
33 #include "ardour/export_timespan.h"
34 #include "ardour/export_channel_configuration.h"
35 #include "ardour/export_status.h"
36 #include "ardour/export_format_specification.h"
37 #include "ardour/export_filename.h"
38 #include "ardour/soundcloud_upload.h"
39 #include "ardour/system_exec.h"
40 #include "pbd/openuri.h"
41 #include "pbd/basename.h"
42 #include "ardour/session_metadata.h"
43
44 #include "pbd/i18n.h"
45
46 using namespace std;
47 using namespace PBD;
48
49 namespace ARDOUR
50 {
51
52 /*** ExportElementFactory ***/
53
54 ExportElementFactory::ExportElementFactory (Session & session) :
55   session (session)
56 {
57
58 }
59
60 ExportElementFactory::~ExportElementFactory ()
61 {
62
63 }
64
65 ExportTimespanPtr
66 ExportElementFactory::add_timespan ()
67 {
68         return ExportTimespanPtr (new ExportTimespan (session.get_export_status(), session.frame_rate()));
69 }
70
71 ExportChannelConfigPtr
72 ExportElementFactory::add_channel_config ()
73 {
74         return ExportChannelConfigPtr (new ExportChannelConfiguration (session));
75 }
76
77 ExportFormatSpecPtr
78 ExportElementFactory::add_format ()
79 {
80         return ExportFormatSpecPtr (new ExportFormatSpecification (session));
81 }
82
83 ExportFormatSpecPtr
84 ExportElementFactory::add_format (XMLNode const & state)
85 {
86         return ExportFormatSpecPtr (new ExportFormatSpecification (session, state));
87 }
88
89 ExportFormatSpecPtr
90 ExportElementFactory::add_format_copy (ExportFormatSpecPtr other)
91 {
92         return ExportFormatSpecPtr (new ExportFormatSpecification (*other));
93 }
94
95 ExportFilenamePtr
96 ExportElementFactory::add_filename ()
97 {
98         return ExportFilenamePtr (new ExportFilename (session));
99 }
100
101 ExportFilenamePtr
102 ExportElementFactory::add_filename_copy (ExportFilenamePtr other)
103 {
104         return ExportFilenamePtr (new ExportFilename (*other));
105 }
106
107 /*** ExportHandler ***/
108
109 ExportHandler::ExportHandler (Session & session)
110   : ExportElementFactory (session)
111   , session (session)
112   , graph_builder (new ExportGraphBuilder (session))
113   , export_status (session.get_export_status ())
114   , post_processing (false)
115   , cue_tracknum (0)
116   , cue_indexnum (0)
117 {
118 }
119
120 ExportHandler::~ExportHandler ()
121 {
122         graph_builder->cleanup (export_status->aborted () );
123 }
124
125 /** Add an export to the `to-do' list */
126 bool
127 ExportHandler::add_export_config (ExportTimespanPtr timespan, ExportChannelConfigPtr channel_config,
128                                   ExportFormatSpecPtr format, ExportFilenamePtr filename,
129                                   BroadcastInfoPtr broadcast_info)
130 {
131         FileSpec spec (channel_config, format, filename, broadcast_info);
132         config_map.insert (make_pair (timespan, spec));
133
134         return true;
135 }
136
137 void
138 ExportHandler::do_export ()
139 {
140         /* Count timespans */
141
142         export_status->init();
143         std::set<ExportTimespanPtr> timespan_set;
144         for (ConfigMap::iterator it = config_map.begin(); it != config_map.end(); ++it) {
145                 bool new_timespan = timespan_set.insert (it->first).second;
146                 if (new_timespan) {
147                         export_status->total_frames += it->first->get_length();
148                 }
149         }
150         export_status->total_timespans = timespan_set.size();
151
152         if (export_status->total_timespans > 1) {
153                 // always include timespan if there's more than one.
154                 for (ConfigMap::iterator it = config_map.begin(); it != config_map.end(); ++it) {
155                         FileSpec & spec = it->second;
156                         spec.filename->include_timespan = true;
157                 }
158         }
159
160         /* Start export */
161
162         Glib::Threads::Mutex::Lock l (export_status->lock());
163         start_timespan ();
164 }
165
166 void
167 ExportHandler::start_timespan ()
168 {
169         export_status->timespan++;
170
171         if (config_map.empty()) {
172                 // freewheeling has to be stopped from outside the process cycle
173                 export_status->set_running (false);
174                 return;
175         }
176
177         /* finish_timespan pops the config_map entry that has been done, so
178            this is the timespan to do this time
179         */
180         current_timespan = config_map.begin()->first;
181
182         export_status->total_frames_current_timespan = current_timespan->get_length();
183         export_status->timespan_name = current_timespan->name();
184         export_status->processed_frames_current_timespan = 0;
185
186         /* Register file configurations to graph builder */
187
188         /* Here's the config_map entries that use this timespan */
189         timespan_bounds = config_map.equal_range (current_timespan);
190         graph_builder->reset ();
191         graph_builder->set_current_timespan (current_timespan);
192         handle_duplicate_format_extensions();
193         bool realtime = current_timespan->realtime ();
194         bool region_export = true;
195         for (ConfigMap::iterator it = timespan_bounds.first; it != timespan_bounds.second; ++it) {
196                 // Filenames can be shared across timespans
197                 FileSpec & spec = it->second;
198                 spec.filename->set_timespan (it->first);
199                 switch (spec.channel_config->region_processing_type ()) {
200                         case RegionExportChannelFactory::None:
201                         case RegionExportChannelFactory::Processed:
202                                 region_export = false;
203                                 break;
204                         default:
205                                 break;
206                 }
207                 graph_builder->add_config (spec, realtime);
208         }
209
210         // ExportDialog::update_realtime_selection does not allow this
211         assert (!region_export || !realtime);
212
213         /* start export */
214
215         post_processing = false;
216         session.ProcessExport.connect_same_thread (process_connection, boost::bind (&ExportHandler::process, this, _1));
217         process_position = current_timespan->get_start();
218         // TODO check if it's a RegionExport.. set flag to skip  process_without_events()
219         session.start_audio_export (process_position, realtime, region_export);
220 }
221
222 void
223 ExportHandler::handle_duplicate_format_extensions()
224 {
225         typedef std::map<std::string, int> ExtCountMap;
226
227         ExtCountMap counts;
228         for (ConfigMap::iterator it = timespan_bounds.first; it != timespan_bounds.second; ++it) {
229                 counts[it->second.format->extension()]++;
230         }
231
232         bool duplicates_found = false;
233         for (ExtCountMap::iterator it = counts.begin(); it != counts.end(); ++it) {
234                 if (it->second > 1) { duplicates_found = true; }
235         }
236
237         // Set this always, as the filenames are shared...
238         for (ConfigMap::iterator it = timespan_bounds.first; it != timespan_bounds.second; ++it) {
239                 it->second.filename->include_format_name = duplicates_found;
240         }
241 }
242
243 int
244 ExportHandler::process (framecnt_t frames)
245 {
246         if (!export_status->running ()) {
247                 return 0;
248         } else if (post_processing) {
249                 Glib::Threads::Mutex::Lock l (export_status->lock());
250                 if (AudioEngine::instance()->freewheeling ()) {
251                         return post_process ();
252                 } else {
253                         // wait until we're freewheeling
254                         return 0;
255                 }
256         } else {
257                 Glib::Threads::Mutex::Lock l (export_status->lock());
258                 return process_timespan (frames);
259         }
260 }
261
262 int
263 ExportHandler::process_timespan (framecnt_t frames)
264 {
265         export_status->active_job = ExportStatus::Exporting;
266         /* update position */
267
268         framecnt_t frames_to_read = 0;
269         framepos_t const end = current_timespan->get_end();
270
271         bool const last_cycle = (process_position + frames >= end);
272
273         if (last_cycle) {
274                 frames_to_read = end - process_position;
275                 export_status->stop = true;
276         } else {
277                 frames_to_read = frames;
278         }
279
280         process_position += frames_to_read;
281         export_status->processed_frames += frames_to_read;
282         export_status->processed_frames_current_timespan += frames_to_read;
283
284         /* Do actual processing */
285         int ret = graph_builder->process (frames_to_read, last_cycle);
286
287         /* Start post-processing/normalizing if necessary */
288         if (last_cycle) {
289                 post_processing = graph_builder->need_postprocessing ();
290                 if (post_processing) {
291                         export_status->total_postprocessing_cycles = graph_builder->get_postprocessing_cycle_count();
292                         export_status->current_postprocessing_cycle = 0;
293                 } else {
294                         finish_timespan ();
295                         return 0;
296                 }
297         }
298
299         return ret;
300 }
301
302 int
303 ExportHandler::post_process ()
304 {
305         if (graph_builder->post_process ()) {
306                 finish_timespan ();
307                 export_status->active_job = ExportStatus::Exporting;
308         } else {
309                 if (graph_builder->realtime ()) {
310                         export_status->active_job = ExportStatus::Encoding;
311                 } else {
312                         export_status->active_job = ExportStatus::Normalizing;
313                 }
314         }
315
316         export_status->current_postprocessing_cycle++;
317
318         return 0;
319 }
320
321 void
322 ExportHandler::command_output(std::string output, size_t size)
323 {
324         std::cerr << "command: " << size << ", " << output << std::endl;
325         info << output << endmsg;
326 }
327
328 void
329 ExportHandler::finish_timespan ()
330 {
331         graph_builder->get_analysis_results (export_status->result_map);
332
333         while (config_map.begin() != timespan_bounds.second) {
334
335                 ExportFormatSpecPtr fmt = config_map.begin()->second.format;
336                 std::string filename = config_map.begin()->second.filename->get_path(fmt);
337                 if (fmt->with_cue()) {
338                         export_cd_marker_file (current_timespan, fmt, filename, CDMarkerCUE);
339                 }
340
341                 if (fmt->with_toc()) {
342                         export_cd_marker_file (current_timespan, fmt, filename, CDMarkerTOC);
343                 }
344
345                 if (fmt->with_mp4chaps()) {
346                         export_cd_marker_file (current_timespan, fmt, filename, MP4Chaps);
347                 }
348
349                 Session::Exported (current_timespan->name(), filename); /* EMIT SIGNAL */
350
351                 /* close file first, otherwise TagLib enounters an ERROR_SHARING_VIOLATION
352                  * The process cannot access the file because it is being used.
353                  * ditto for post-export and upload.
354                  */
355                 graph_builder->reset ();
356
357                 if (fmt->tag()) {
358                         /* TODO: check Umlauts and encoding in filename.
359                          * TagLib eventually calls CreateFileA(),
360                          */
361                         export_status->active_job = ExportStatus::Tagging;
362                         AudiofileTagger::tag_file(filename, *SessionMetadata::Metadata());
363                 }
364
365                 if (!fmt->command().empty()) {
366                         SessionMetadata const & metadata (*SessionMetadata::Metadata());
367
368 #if 0   // would be nicer with C++11 initialiser...
369                         std::map<char, std::string> subs {
370                                 { 'f', filename },
371                                 { 'd', Glib::path_get_dirname(filename)  + G_DIR_SEPARATOR },
372                                 { 'b', PBD::basename_nosuffix(filename) },
373                                 ...
374                         };
375 #endif
376                         export_status->active_job = ExportStatus::Command;
377                         PBD::ScopedConnection command_connection;
378                         std::map<char, std::string> subs;
379
380                         std::stringstream track_number;
381                         track_number << metadata.track_number ();
382                         std::stringstream total_tracks;
383                         total_tracks << metadata.total_tracks ();
384                         std::stringstream year;
385                         year << metadata.year ();
386
387                         subs.insert (std::pair<char, std::string> ('a', metadata.artist ()));
388                         subs.insert (std::pair<char, std::string> ('b', PBD::basename_nosuffix (filename)));
389                         subs.insert (std::pair<char, std::string> ('c', metadata.copyright ()));
390                         subs.insert (std::pair<char, std::string> ('d', Glib::path_get_dirname (filename) + G_DIR_SEPARATOR));
391                         subs.insert (std::pair<char, std::string> ('f', filename));
392                         subs.insert (std::pair<char, std::string> ('l', metadata.lyricist ()));
393                         subs.insert (std::pair<char, std::string> ('n', session.name ()));
394                         subs.insert (std::pair<char, std::string> ('s', session.path ()));
395                         subs.insert (std::pair<char, std::string> ('o', metadata.conductor ()));
396                         subs.insert (std::pair<char, std::string> ('t', metadata.title ()));
397                         subs.insert (std::pair<char, std::string> ('z', metadata.organization ()));
398                         subs.insert (std::pair<char, std::string> ('A', metadata.album ()));
399                         subs.insert (std::pair<char, std::string> ('C', metadata.comment ()));
400                         subs.insert (std::pair<char, std::string> ('E', metadata.engineer ()));
401                         subs.insert (std::pair<char, std::string> ('G', metadata.genre ()));
402                         subs.insert (std::pair<char, std::string> ('L', total_tracks.str ()));
403                         subs.insert (std::pair<char, std::string> ('M', metadata.mixer ()));
404                         subs.insert (std::pair<char, std::string> ('N', current_timespan->name())); // =?= config_map.begin()->first->name ()
405                         subs.insert (std::pair<char, std::string> ('O', metadata.composer ()));
406                         subs.insert (std::pair<char, std::string> ('P', metadata.producer ()));
407                         subs.insert (std::pair<char, std::string> ('S', metadata.disc_subtitle ()));
408                         subs.insert (std::pair<char, std::string> ('T', track_number.str ()));
409                         subs.insert (std::pair<char, std::string> ('Y', year.str ()));
410                         subs.insert (std::pair<char, std::string> ('Z', metadata.country ()));
411
412                         ARDOUR::SystemExec *se = new ARDOUR::SystemExec(fmt->command(), subs);
413                         info << "Post-export command line : {" << se->to_s () << "}" << endmsg;
414                         se->ReadStdout.connect_same_thread(command_connection, boost::bind(&ExportHandler::command_output, this, _1, _2));
415                         int ret = se->start (2);
416                         if (ret == 0) {
417                                 // successfully started
418                                 while (se->is_running ()) {
419                                         // wait for system exec to terminate
420                                         Glib::usleep (1000);
421                                 }
422                         } else {
423                                 error << "Post-export command FAILED with Error: " << ret << endmsg;
424                         }
425                         delete (se);
426                 }
427
428                 if (fmt->soundcloud_upload()) {
429                         SoundcloudUploader *soundcloud_uploader = new SoundcloudUploader;
430                         std::string token = soundcloud_uploader->Get_Auth_Token(soundcloud_username, soundcloud_password);
431                         DEBUG_TRACE (DEBUG::Soundcloud, string_compose(
432                                                 "uploading %1 - username=%2, password=%3, token=%4",
433                                                 filename, soundcloud_username, soundcloud_password, token) );
434                         std::string path = soundcloud_uploader->Upload (
435                                         filename,
436                                         PBD::basename_nosuffix(filename), // title
437                                         token,
438                                         soundcloud_make_public,
439                                         soundcloud_downloadable,
440                                         this);
441
442                         if (path.length() != 0) {
443                                 info << string_compose ( _("File %1 uploaded to %2"), filename, path) << endmsg;
444                                 if (soundcloud_open_page) {
445                                         DEBUG_TRACE (DEBUG::Soundcloud, string_compose ("opening %1", path) );
446                                         open_uri(path.c_str());  // open the soundcloud website to the new file
447                                 }
448                         } else {
449                                 error << _("upload to Soundcloud failed. Perhaps your email or password are incorrect?\n") << endmsg;
450                         }
451                         delete soundcloud_uploader;
452                 }
453                 config_map.erase (config_map.begin());
454         }
455
456         start_timespan ();
457 }
458
459 /*** CD Marker stuff ***/
460
461 struct LocationSortByStart {
462     bool operator() (Location *a, Location *b) {
463             return a->start() < b->start();
464     }
465 };
466
467 void
468 ExportHandler::export_cd_marker_file (ExportTimespanPtr timespan, ExportFormatSpecPtr file_format,
469                                       std::string filename, CDMarkerFormat format)
470 {
471         string filepath = get_cd_marker_filename(filename, format);
472
473         try {
474                 void (ExportHandler::*header_func) (CDMarkerStatus &);
475                 void (ExportHandler::*track_func) (CDMarkerStatus &);
476                 void (ExportHandler::*index_func) (CDMarkerStatus &);
477
478                 switch (format) {
479                 case CDMarkerTOC:
480                         header_func = &ExportHandler::write_toc_header;
481                         track_func = &ExportHandler::write_track_info_toc;
482                         index_func = &ExportHandler::write_index_info_toc;
483                         break;
484                 case CDMarkerCUE:
485                         header_func = &ExportHandler::write_cue_header;
486                         track_func = &ExportHandler::write_track_info_cue;
487                         index_func = &ExportHandler::write_index_info_cue;
488                         break;
489                 case MP4Chaps:
490                         header_func = &ExportHandler::write_mp4ch_header;
491                         track_func = &ExportHandler::write_track_info_mp4ch;
492                         index_func = &ExportHandler::write_index_info_mp4ch;
493                         break;
494                 default:
495                         return;
496                 }
497
498                 CDMarkerStatus status (filepath, timespan, file_format, filename);
499
500                 (this->*header_func) (status);
501
502                 /* Get locations and sort */
503
504                 Locations::LocationList const & locations (session.locations()->list());
505                 Locations::LocationList::const_iterator i;
506                 Locations::LocationList temp;
507
508                 for (i = locations.begin(); i != locations.end(); ++i) {
509                         if ((*i)->start() >= timespan->get_start() && (*i)->end() <= timespan->get_end() && (*i)->is_cd_marker() && !(*i)->is_session_range()) {
510                                 temp.push_back (*i);
511                         }
512                 }
513
514                 if (temp.empty()) {
515                         // TODO One index marker for whole thing
516                         return;
517                 }
518
519                 LocationSortByStart cmp;
520                 temp.sort (cmp);
521                 Locations::LocationList::const_iterator nexti;
522
523                 /* Start actual marker stuff */
524
525                 framepos_t last_end_time = timespan->get_start();
526                 status.track_position = 0;
527
528                 for (i = temp.begin(); i != temp.end(); ++i) {
529
530                         status.marker = *i;
531
532                         if ((*i)->start() < last_end_time) {
533                                 if ((*i)->is_mark()) {
534                                         /* Index within track */
535
536                                         status.index_position = (*i)->start() - timespan->get_start();
537                                         (this->*index_func) (status);
538                                 }
539
540                                 continue;
541                         }
542
543                         /* A track, defined by a cd range marker or a cd location marker outside of a cd range */
544
545                         status.track_position = last_end_time - timespan->get_start();
546                         status.track_start_frame = (*i)->start() - timespan->get_start();  // everything before this is the pregap
547                         status.track_duration = 0;
548
549                         if ((*i)->is_mark()) {
550                                 // a mark track location needs to look ahead to the next marker's start to determine length
551                                 nexti = i;
552                                 ++nexti;
553
554                                 if (nexti != temp.end()) {
555                                         status.track_duration = (*nexti)->start() - last_end_time;
556
557                                         last_end_time = (*nexti)->start();
558                                 } else {
559                                         // this was the last marker, use timespan end
560                                         status.track_duration = timespan->get_end() - last_end_time;
561
562                                         last_end_time = timespan->get_end();
563                                 }
564                         } else {
565                                 // range
566                                 status.track_duration = (*i)->end() - last_end_time;
567
568                                 last_end_time = (*i)->end();
569                         }
570
571                         (this->*track_func) (status);
572                 }
573
574         } catch (std::exception& e) {
575                 error << string_compose (_("an error occurred while writing a TOC/CUE file: %1"), e.what()) << endmsg;
576                 ::g_unlink (filepath.c_str());
577         } catch (Glib::Exception& e) {
578                 error << string_compose (_("an error occurred while writing a TOC/CUE file: %1"), e.what()) << endmsg;
579                 ::g_unlink (filepath.c_str());
580         }
581 }
582
583 string
584 ExportHandler::get_cd_marker_filename(std::string filename, CDMarkerFormat format)
585 {
586         /* do not strip file suffix because there may be more than one format,
587            and we do not want the CD marker file from one format to overwrite
588            another (e.g. foo.wav.cue > foo.aiff.cue)
589         */
590
591         switch (format) {
592         case CDMarkerTOC:
593                 return filename + ".toc";
594         case CDMarkerCUE:
595                 return filename + ".cue";
596         case MP4Chaps:
597         {
598                 unsigned lastdot = filename.find_last_of('.');
599                 return filename.substr(0,lastdot) + ".chapters.txt";
600         }
601         default:
602                 return filename + ".marker"; // Should not be reached when actually creating a file
603         }
604 }
605
606 void
607 ExportHandler::write_cue_header (CDMarkerStatus & status)
608 {
609         string title = status.timespan->name().compare ("Session") ? status.timespan->name() : (string) session.name();
610
611         // Album metadata
612         string barcode      = SessionMetadata::Metadata()->barcode();
613         string album_artist = SessionMetadata::Metadata()->album_artist();
614         string album_title  = SessionMetadata::Metadata()->album();
615
616         status.out << "REM Cue file generated by " << PROGRAM_NAME << endl;
617
618         if (barcode != "")
619                 status.out << "CATALOG " << barcode << endl;
620
621         if (album_artist != "")
622                 status.out << "PERFORMER " << cue_escape_cdtext (album_artist) << endl;
623
624         if (album_title != "")
625                 title = album_title;
626
627         status.out << "TITLE " << cue_escape_cdtext (title) << endl;
628
629         /*  The original cue sheet spec mentions five file types
630                 WAVE, AIFF,
631                 BINARY   = "header-less" audio (44.1 kHz, 16 Bit, little endian),
632                 MOTOROLA = "header-less" audio (44.1 kHz, 16 Bit, big endian),
633                 and MP3
634
635                 We try to use these file types whenever appropriate and
636                 default to our own names otherwise.
637         */
638         status.out << "FILE \"" << Glib::path_get_basename(status.filename) << "\" ";
639         if (!status.format->format_name().compare ("WAV")  || !status.format->format_name().compare ("BWF")) {
640                 status.out  << "WAVE";
641         } else if (status.format->format_id() == ExportFormatBase::F_RAW &&
642                    status.format->sample_format() == ExportFormatBase::SF_16 &&
643                    status.format->sample_rate() == ExportFormatBase::SR_44_1) {
644                 // Format is RAW 16bit 44.1kHz
645                 if (status.format->endianness() == ExportFormatBase::E_Little) {
646                         status.out << "BINARY";
647                 } else {
648                         status.out << "MOTOROLA";
649                 }
650         } else {
651                 // no special case for AIFF format it's name is already "AIFF"
652                 status.out << status.format->format_name();
653         }
654         status.out << endl;
655 }
656
657 void
658 ExportHandler::write_toc_header (CDMarkerStatus & status)
659 {
660         string title = status.timespan->name().compare ("Session") ? status.timespan->name() : (string) session.name();
661
662         // Album metadata
663         string barcode      = SessionMetadata::Metadata()->barcode();
664         string album_artist = SessionMetadata::Metadata()->album_artist();
665         string album_title  = SessionMetadata::Metadata()->album();
666
667         if (barcode != "")
668                 status.out << "CATALOG \"" << barcode << "\"" << endl;
669
670         if (album_title != "")
671                 title = album_title;
672
673         status.out << "CD_DA" << endl;
674         status.out << "CD_TEXT {" << endl << "  LANGUAGE_MAP {" << endl << "    0 : EN" << endl << "  }" << endl;
675         status.out << "  LANGUAGE 0 {" << endl << "    TITLE " << toc_escape_cdtext (title) << endl ;
676         status.out << "    PERFORMER " << toc_escape_cdtext (album_artist) << endl;
677         status.out << "  }" << endl << "}" << endl;
678 }
679
680 void
681 ExportHandler::write_mp4ch_header (CDMarkerStatus & status)
682 {
683         status.out << "00:00:00.000 Intro" << endl;
684 }
685
686 void
687 ExportHandler::write_track_info_cue (CDMarkerStatus & status)
688 {
689         gchar buf[18];
690
691         snprintf (buf, sizeof(buf), "  TRACK %02d AUDIO", status.track_number);
692         status.out << buf << endl;
693
694         status.out << "    FLAGS" ;
695         if (status.marker->cd_info.find("scms") != status.marker->cd_info.end())  {
696                 status.out << " SCMS ";
697         } else {
698                 status.out << " DCP ";
699         }
700
701         if (status.marker->cd_info.find("preemph") != status.marker->cd_info.end())  {
702                 status.out << " PRE";
703         }
704         status.out << endl;
705
706         if (status.marker->cd_info.find("isrc") != status.marker->cd_info.end())  {
707                 status.out << "    ISRC " << status.marker->cd_info["isrc"] << endl;
708         }
709
710         if (status.marker->name() != "") {
711                 status.out << "    TITLE " << cue_escape_cdtext (status.marker->name()) << endl;
712         }
713
714         if (status.marker->cd_info.find("performer") != status.marker->cd_info.end()) {
715                 status.out <<  "    PERFORMER " << cue_escape_cdtext (status.marker->cd_info["performer"]) << endl;
716         }
717
718         if (status.marker->cd_info.find("composer") != status.marker->cd_info.end()) {
719                 status.out << "    SONGWRITER " << cue_escape_cdtext (status.marker->cd_info["composer"]) << endl;
720         }
721
722         if (status.track_position != status.track_start_frame) {
723                 frames_to_cd_frames_string (buf, status.track_position);
724                 status.out << "    INDEX 00" << buf << endl;
725         }
726
727         frames_to_cd_frames_string (buf, status.track_start_frame);
728         status.out << "    INDEX 01" << buf << endl;
729
730         status.index_number = 2;
731         status.track_number++;
732 }
733
734 void
735 ExportHandler::write_track_info_toc (CDMarkerStatus & status)
736 {
737         gchar buf[18];
738
739         status.out << endl << "TRACK AUDIO" << endl;
740
741         if (status.marker->cd_info.find("scms") != status.marker->cd_info.end())  {
742                 status.out << "NO ";
743         }
744         status.out << "COPY" << endl;
745
746         if (status.marker->cd_info.find("preemph") != status.marker->cd_info.end())  {
747                 status.out << "PRE_EMPHASIS" << endl;
748         } else {
749                 status.out << "NO PRE_EMPHASIS" << endl;
750         }
751
752         if (status.marker->cd_info.find("isrc") != status.marker->cd_info.end())  {
753                 status.out << "ISRC \"" << status.marker->cd_info["isrc"] << "\"" << endl;
754         }
755
756         status.out << "CD_TEXT {" << endl << "  LANGUAGE 0 {" << endl;
757         status.out << "     TITLE " << toc_escape_cdtext (status.marker->name()) << endl;
758
759         status.out << "     PERFORMER ";
760         if (status.marker->cd_info.find("performer") != status.marker->cd_info.end()) {
761                 status.out << toc_escape_cdtext (status.marker->cd_info["performer"]) << endl;
762         } else {
763                 status.out << "\"\"" << endl;
764         }
765
766         if (status.marker->cd_info.find("composer") != status.marker->cd_info.end()) {
767                 status.out  << "     SONGWRITER " << toc_escape_cdtext (status.marker->cd_info["composer"]) << endl;
768         }
769
770         if (status.marker->cd_info.find("isrc") != status.marker->cd_info.end()) {
771                 status.out  << "     ISRC \"";
772                 status.out << status.marker->cd_info["isrc"].substr(0,2) << "-";
773                 status.out << status.marker->cd_info["isrc"].substr(2,3) << "-";
774                 status.out << status.marker->cd_info["isrc"].substr(5,2) << "-";
775                 status.out << status.marker->cd_info["isrc"].substr(7,5) << "\"" << endl;
776         }
777
778         status.out << "  }" << endl << "}" << endl;
779
780         frames_to_cd_frames_string (buf, status.track_position);
781         status.out << "FILE " << toc_escape_filename (status.filename) << ' ' << buf;
782
783         frames_to_cd_frames_string (buf, status.track_duration);
784         status.out << buf << endl;
785
786         frames_to_cd_frames_string (buf, status.track_start_frame - status.track_position);
787         status.out << "START" << buf << endl;
788 }
789
790 void ExportHandler::write_track_info_mp4ch (CDMarkerStatus & status)
791 {
792         gchar buf[18];
793
794         frames_to_chapter_marks_string(buf, status.track_start_frame);
795         status.out << buf << " " << status.marker->name() << endl;
796 }
797
798 void
799 ExportHandler::write_index_info_cue (CDMarkerStatus & status)
800 {
801         gchar buf[18];
802
803         snprintf (buf, sizeof(buf), "    INDEX %02d", cue_indexnum);
804         status.out << buf;
805         frames_to_cd_frames_string (buf, status.index_position);
806         status.out << buf << endl;
807
808         cue_indexnum++;
809 }
810
811 void
812 ExportHandler::write_index_info_toc (CDMarkerStatus & status)
813 {
814         gchar buf[18];
815
816         frames_to_cd_frames_string (buf, status.index_position - status.track_position);
817         status.out << "INDEX" << buf << endl;
818 }
819
820 void
821 ExportHandler::write_index_info_mp4ch (CDMarkerStatus & status)
822 {
823 }
824
825 void
826 ExportHandler::frames_to_cd_frames_string (char* buf, framepos_t when)
827 {
828         framecnt_t remainder;
829         framecnt_t fr = session.nominal_frame_rate();
830         int mins, secs, frames;
831
832         mins = when / (60 * fr);
833         remainder = when - (mins * 60 * fr);
834         secs = remainder / fr;
835         remainder -= secs * fr;
836         frames = remainder / (fr / 75);
837         sprintf (buf, " %02d:%02d:%02d", mins, secs, frames);
838 }
839
840 void
841 ExportHandler::frames_to_chapter_marks_string (char* buf, framepos_t when)
842 {
843         framecnt_t remainder;
844         framecnt_t fr = session.nominal_frame_rate();
845         int hours, mins, secs, msecs;
846
847         hours = when / (3600 * fr);
848         remainder = when - (hours * 3600 * fr);
849         mins = remainder / (60 * fr);
850         remainder -= mins * 60 * fr;
851         secs = remainder / fr;
852         remainder -= secs * fr;
853         msecs = (remainder * 1000) / fr;
854         sprintf (buf, "%02d:%02d:%02d.%03d", hours, mins, secs, msecs);
855 }
856
857 std::string
858 ExportHandler::toc_escape_cdtext (const std::string& txt)
859 {
860         Glib::ustring check (txt);
861         std::string out;
862         std::string latin1_txt;
863         char buf[5];
864
865         try {
866                 latin1_txt = Glib::convert_with_fallback (txt, "ISO-8859-1", "UTF-8", "_");
867         } catch (Glib::ConvertError& err) {
868                 throw Glib::ConvertError (err.code(), string_compose (_("Cannot convert %1 to Latin-1 text"), txt));
869         }
870
871         out = '"';
872
873         for (std::string::const_iterator c = latin1_txt.begin(); c != latin1_txt.end(); ++c) {
874
875                 if ((*c) == '"') {
876                         out += "\\\"";
877                 } else if ((*c) == '\\') {
878                         out += "\\134";
879                 } else if (isprint (*c)) {
880                         out += *c;
881                 } else {
882                         snprintf (buf, sizeof (buf), "\\%03o", (int) (unsigned char) *c);
883                         out += buf;
884                 }
885         }
886
887         out += '"';
888
889         return out;
890 }
891
892 std::string
893 ExportHandler::toc_escape_filename (const std::string& txt)
894 {
895         std::string out;
896
897         out = '"';
898
899         // We iterate byte-wise not character-wise over a UTF-8 string here,
900         // because we only want to translate backslashes and double quotes
901         for (std::string::const_iterator c = txt.begin(); c != txt.end(); ++c) {
902
903                 if (*c == '"') {
904                         out += "\\\"";
905                 } else if (*c == '\\') {
906                         out += "\\134";
907                 } else {
908                         out += *c;
909                 }
910         }
911
912         out += '"';
913
914         return out;
915 }
916
917 std::string
918 ExportHandler::cue_escape_cdtext (const std::string& txt)
919 {
920         std::string latin1_txt;
921         std::string out;
922
923         try {
924                 latin1_txt = Glib::convert (txt, "ISO-8859-1", "UTF-8");
925         } catch (Glib::ConvertError& err) {
926                 throw Glib::ConvertError (err.code(), string_compose (_("Cannot convert %1 to Latin-1 text"), txt));
927         }
928
929         // does not do much mor than UTF-8 to Latin1 translation yet, but
930         // that may have to change if cue parsers in burning programs change
931         out = '"' + latin1_txt + '"';
932
933         return out;
934 }
935
936 } // namespace ARDOUR