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