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