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