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