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