Prepare for optional timespan name during export.
[ardour.git] / libs / ardour / export_handler.cc
1 /*
2     Copyright (C) 2008-2009 Paul Davis
3     Author: Sakari Bergen
4
5     This program is free software; you can redistribute it and/or modify
6     it under the terms of the GNU General Public License as published by
7     the Free Software Foundation; either version 2 of the License, or
8     (at your option) any later version.
9
10     This program is distributed in the hope that it will be useful,
11     but WITHOUT ANY WARRANTY; without even the implied warranty of
12     MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
13     GNU General Public License for more details.
14
15     You should have received a copy of the GNU General Public License
16     along with this program; if not, write to the Free Software
17     Foundation, Inc., 675 Mass Ave, Cambridge, MA 02139, USA.
18
19 */
20
21 #include "ardour/export_handler.h"
22
23 #include "pbd/gstdio_compat.h"
24 #include <glibmm.h>
25 #include <glibmm/convert.h>
26
27 #include "pbd/convert.h"
28
29 #include "ardour/audiofile_tagger.h"
30 #include "ardour/debug.h"
31 #include "ardour/export_graph_builder.h"
32 #include "ardour/export_timespan.h"
33 #include "ardour/export_channel_configuration.h"
34 #include "ardour/export_status.h"
35 #include "ardour/export_format_specification.h"
36 #include "ardour/export_filename.h"
37 #include "ardour/soundcloud_upload.h"
38 #include "ardour/system_exec.h"
39 #include "pbd/openuri.h"
40 #include "pbd/basename.h"
41 #include "ardour/session_metadata.h"
42
43 #include "i18n.h"
44
45 using namespace std;
46 using namespace PBD;
47
48 namespace ARDOUR
49 {
50
51 /*** ExportElementFactory ***/
52
53 ExportElementFactory::ExportElementFactory (Session & session) :
54   session (session)
55 {
56
57 }
58
59 ExportElementFactory::~ExportElementFactory ()
60 {
61
62 }
63
64 ExportTimespanPtr
65 ExportElementFactory::add_timespan ()
66 {
67         return ExportTimespanPtr (new ExportTimespan (session.get_export_status(), session.frame_rate()));
68 }
69
70 ExportChannelConfigPtr
71 ExportElementFactory::add_channel_config ()
72 {
73         return ExportChannelConfigPtr (new ExportChannelConfiguration (session));
74 }
75
76 ExportFormatSpecPtr
77 ExportElementFactory::add_format ()
78 {
79         return ExportFormatSpecPtr (new ExportFormatSpecification (session));
80 }
81
82 ExportFormatSpecPtr
83 ExportElementFactory::add_format (XMLNode const & state)
84 {
85         return ExportFormatSpecPtr (new ExportFormatSpecification (session, state));
86 }
87
88 ExportFormatSpecPtr
89 ExportElementFactory::add_format_copy (ExportFormatSpecPtr other)
90 {
91         return ExportFormatSpecPtr (new ExportFormatSpecification (*other));
92 }
93
94 ExportFilenamePtr
95 ExportElementFactory::add_filename ()
96 {
97         return ExportFilenamePtr (new ExportFilename (session));
98 }
99
100 ExportFilenamePtr
101 ExportElementFactory::add_filename_copy (ExportFilenamePtr other)
102 {
103         return ExportFilenamePtr (new ExportFilename (*other));
104 }
105
106 /*** ExportHandler ***/
107
108 ExportHandler::ExportHandler (Session & session)
109   : ExportElementFactory (session)
110   , session (session)
111   , graph_builder (new ExportGraphBuilder (session))
112   , export_status (session.get_export_status ())
113   , normalizing (false)
114   , cue_tracknum (0)
115   , cue_indexnum (0)
116 {
117 }
118
119 ExportHandler::~ExportHandler ()
120 {
121         graph_builder->cleanup (export_status->aborted () );
122 }
123
124 /** Add an export to the `to-do' list */
125 bool
126 ExportHandler::add_export_config (ExportTimespanPtr timespan, ExportChannelConfigPtr channel_config,
127                                   ExportFormatSpecPtr format, ExportFilenamePtr filename,
128                                   BroadcastInfoPtr broadcast_info)
129 {
130         FileSpec spec (channel_config, format, filename, broadcast_info);
131         config_map.insert (make_pair (timespan, spec));
132
133         return true;
134 }
135
136 void
137 ExportHandler::do_export ()
138 {
139         /* Count timespans */
140
141         export_status->init();
142         std::set<ExportTimespanPtr> timespan_set;
143         for (ConfigMap::iterator it = config_map.begin(); it != config_map.end(); ++it) {
144                 bool new_timespan = timespan_set.insert (it->first).second;
145                 if (new_timespan) {
146                         export_status->total_frames += it->first->get_length();
147                 }
148         }
149         export_status->total_timespans = timespan_set.size();
150
151         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                         se->ReadStdout.connect_same_thread(command_connection, boost::bind(&ExportHandler::command_output, this, _1, _2));
359                         if (se->start (2) == 0) {
360                                 // successfully started
361                                 while (se->is_running ()) {
362                                         // wait for system exec to terminate
363                                         Glib::usleep (1000);
364                                 }
365                         } else {
366                                 error << "post-export hook failed! " << fmt->command() << endmsg;
367                         }
368                         delete (se);
369                 }
370
371                 if (fmt->soundcloud_upload()) {
372                         SoundcloudUploader *soundcloud_uploader = new SoundcloudUploader;
373                         std::string token = soundcloud_uploader->Get_Auth_Token(soundcloud_username, soundcloud_password);
374                         DEBUG_TRACE (DEBUG::Soundcloud, string_compose(
375                                                 "uploading %1 - username=%2, password=%3, token=%4",
376                                                 filename, soundcloud_username, soundcloud_password, token) );
377                         std::string path = soundcloud_uploader->Upload (
378                                         filename,
379                                         PBD::basename_nosuffix(filename), // title
380                                         token,
381                                         soundcloud_make_public,
382                                         soundcloud_downloadable,
383                                         this);
384
385                         if (path.length() != 0) {
386                                 info << string_compose ( _("File %1 uploaded to %2"), filename, path) << endmsg;
387                                 if (soundcloud_open_page) {
388                                         DEBUG_TRACE (DEBUG::Soundcloud, string_compose ("opening %1", path) );
389                                         open_uri(path.c_str());  // open the soundcloud website to the new file
390                                 }
391                         } else {
392                                 error << _("upload to Soundcloud failed. Perhaps your email or password are incorrect?\n") << endmsg;
393                         }
394                         delete soundcloud_uploader;
395                 }
396                 config_map.erase (config_map.begin());
397         }
398
399         start_timespan ();
400 }
401
402 /*** CD Marker stuff ***/
403
404 struct LocationSortByStart {
405     bool operator() (Location *a, Location *b) {
406             return a->start() < b->start();
407     }
408 };
409
410 void
411 ExportHandler::export_cd_marker_file (ExportTimespanPtr timespan, ExportFormatSpecPtr file_format,
412                                       std::string filename, CDMarkerFormat format)
413 {
414         string filepath = get_cd_marker_filename(filename, format);
415
416         try {
417                 void (ExportHandler::*header_func) (CDMarkerStatus &);
418                 void (ExportHandler::*track_func) (CDMarkerStatus &);
419                 void (ExportHandler::*index_func) (CDMarkerStatus &);
420
421                 switch (format) {
422                 case CDMarkerTOC:
423                         header_func = &ExportHandler::write_toc_header;
424                         track_func = &ExportHandler::write_track_info_toc;
425                         index_func = &ExportHandler::write_index_info_toc;
426                         break;
427                 case CDMarkerCUE:
428                         header_func = &ExportHandler::write_cue_header;
429                         track_func = &ExportHandler::write_track_info_cue;
430                         index_func = &ExportHandler::write_index_info_cue;
431                         break;
432                 case MP4Chaps:
433                         header_func = &ExportHandler::write_mp4ch_header;
434                         track_func = &ExportHandler::write_track_info_mp4ch;
435                         index_func = &ExportHandler::write_index_info_mp4ch;
436                         break;
437                 default:
438                         return;
439                 }
440
441                 CDMarkerStatus status (filepath, timespan, file_format, filename);
442
443                 (this->*header_func) (status);
444
445                 /* Get locations and sort */
446
447                 Locations::LocationList const & locations (session.locations()->list());
448                 Locations::LocationList::const_iterator i;
449                 Locations::LocationList temp;
450
451                 for (i = locations.begin(); i != locations.end(); ++i) {
452                         if ((*i)->start() >= timespan->get_start() && (*i)->end() <= timespan->get_end() && (*i)->is_cd_marker() && !(*i)->is_session_range()) {
453                                 temp.push_back (*i);
454                         }
455                 }
456
457                 if (temp.empty()) {
458                         // TODO One index marker for whole thing
459                         return;
460                 }
461
462                 LocationSortByStart cmp;
463                 temp.sort (cmp);
464                 Locations::LocationList::const_iterator nexti;
465
466                 /* Start actual marker stuff */
467
468                 framepos_t last_end_time = timespan->get_start();
469                 status.track_position = 0;
470
471                 for (i = temp.begin(); i != temp.end(); ++i) {
472
473                         status.marker = *i;
474
475                         if ((*i)->start() < last_end_time) {
476                                 if ((*i)->is_mark()) {
477                                         /* Index within track */
478
479                                         status.index_position = (*i)->start() - timespan->get_start();
480                                         (this->*index_func) (status);
481                                 }
482
483                                 continue;
484                         }
485
486                         /* A track, defined by a cd range marker or a cd location marker outside of a cd range */
487
488                         status.track_position = last_end_time - timespan->get_start();
489                         status.track_start_frame = (*i)->start() - timespan->get_start();  // everything before this is the pregap
490                         status.track_duration = 0;
491
492                         if ((*i)->is_mark()) {
493                                 // a mark track location needs to look ahead to the next marker's start to determine length
494                                 nexti = i;
495                                 ++nexti;
496
497                                 if (nexti != temp.end()) {
498                                         status.track_duration = (*nexti)->start() - last_end_time;
499
500                                         last_end_time = (*nexti)->start();
501                                 } else {
502                                         // this was the last marker, use timespan end
503                                         status.track_duration = timespan->get_end() - last_end_time;
504
505                                         last_end_time = timespan->get_end();
506                                 }
507                         } else {
508                                 // range
509                                 status.track_duration = (*i)->end() - last_end_time;
510
511                                 last_end_time = (*i)->end();
512                         }
513
514                         (this->*track_func) (status);
515                 }
516
517         } catch (std::exception& e) {
518                 error << string_compose (_("an error occured while writing a TOC/CUE file: %1"), e.what()) << endmsg;
519                 ::g_unlink (filepath.c_str());
520         } catch (Glib::Exception& e) {
521                 error << string_compose (_("an error occured while writing a TOC/CUE file: %1"), e.what()) << endmsg;
522                 ::g_unlink (filepath.c_str());
523         }
524 }
525
526 string
527 ExportHandler::get_cd_marker_filename(std::string filename, CDMarkerFormat format)
528 {
529         /* do not strip file suffix because there may be more than one format,
530            and we do not want the CD marker file from one format to overwrite
531            another (e.g. foo.wav.cue > foo.aiff.cue)
532         */
533
534         switch (format) {
535         case CDMarkerTOC:
536                 return filename + ".toc";
537         case CDMarkerCUE:
538                 return filename + ".cue";
539         case MP4Chaps:
540         {
541                 unsigned lastdot = filename.find_last_of('.');
542                 return filename.substr(0,lastdot) + ".chapters.txt";
543         }
544         default:
545                 return filename + ".marker"; // Should not be reached when actually creating a file
546         }
547 }
548
549 void
550 ExportHandler::write_cue_header (CDMarkerStatus & status)
551 {
552         string title = status.timespan->name().compare ("Session") ? status.timespan->name() : (string) session.name();
553
554         // Album metadata
555         string barcode      = SessionMetadata::Metadata()->barcode();
556         string album_artist = SessionMetadata::Metadata()->album_artist();
557         string album_title  = SessionMetadata::Metadata()->album();
558
559         status.out << "REM Cue file generated by " << PROGRAM_NAME << endl;
560
561         if (barcode != "")
562                 status.out << "CATALOG " << barcode << endl;
563
564         if (album_artist != "")
565                 status.out << "PERFORMER " << cue_escape_cdtext (album_artist) << endl;
566
567         if (album_title != "")
568                 title = album_title;
569
570         status.out << "TITLE " << cue_escape_cdtext (title) << endl;
571
572         /*  The original cue sheet spec mentions five file types
573                 WAVE, AIFF,
574                 BINARY   = "header-less" audio (44.1 kHz, 16 Bit, little endian),
575                 MOTOROLA = "header-less" audio (44.1 kHz, 16 Bit, big endian),
576                 and MP3
577
578                 We try to use these file types whenever appropriate and
579                 default to our own names otherwise.
580         */
581         status.out << "FILE \"" << Glib::path_get_basename(status.filename) << "\" ";
582         if (!status.format->format_name().compare ("WAV")  || !status.format->format_name().compare ("BWF")) {
583                 status.out  << "WAVE";
584         } else if (status.format->format_id() == ExportFormatBase::F_RAW &&
585                    status.format->sample_format() == ExportFormatBase::SF_16 &&
586                    status.format->sample_rate() == ExportFormatBase::SR_44_1) {
587                 // Format is RAW 16bit 44.1kHz
588                 if (status.format->endianness() == ExportFormatBase::E_Little) {
589                         status.out << "BINARY";
590                 } else {
591                         status.out << "MOTOROLA";
592                 }
593         } else {
594                 // no special case for AIFF format it's name is already "AIFF"
595                 status.out << status.format->format_name();
596         }
597         status.out << endl;
598 }
599
600 void
601 ExportHandler::write_toc_header (CDMarkerStatus & status)
602 {
603         string title = status.timespan->name().compare ("Session") ? status.timespan->name() : (string) session.name();
604
605         // Album metadata
606         string barcode      = SessionMetadata::Metadata()->barcode();
607         string album_artist = SessionMetadata::Metadata()->album_artist();
608         string album_title  = SessionMetadata::Metadata()->album();
609
610         if (barcode != "")
611                 status.out << "CATALOG \"" << barcode << "\"" << endl;
612
613         if (album_title != "")
614                 title = album_title;
615
616         status.out << "CD_DA" << endl;
617         status.out << "CD_TEXT {" << endl << "  LANGUAGE_MAP {" << endl << "    0 : EN" << endl << "  }" << endl;
618         status.out << "  LANGUAGE 0 {" << endl << "    TITLE " << toc_escape_cdtext (title) << endl ;
619         status.out << "    PERFORMER " << toc_escape_cdtext (album_artist) << endl;
620         status.out << "  }" << endl << "}" << endl;
621 }
622
623 void
624 ExportHandler::write_mp4ch_header (CDMarkerStatus & status)
625 {
626         status.out << "00:00:00.000 Intro" << endl;
627 }
628
629 void
630 ExportHandler::write_track_info_cue (CDMarkerStatus & status)
631 {
632         gchar buf[18];
633
634         snprintf (buf, sizeof(buf), "  TRACK %02d AUDIO", status.track_number);
635         status.out << buf << endl;
636
637         status.out << "    FLAGS" ;
638         if (status.marker->cd_info.find("scms") != status.marker->cd_info.end())  {
639                 status.out << " SCMS ";
640         } else {
641                 status.out << " DCP ";
642         }
643
644         if (status.marker->cd_info.find("preemph") != status.marker->cd_info.end())  {
645                 status.out << " PRE";
646         }
647         status.out << endl;
648
649         if (status.marker->cd_info.find("isrc") != status.marker->cd_info.end())  {
650                 status.out << "    ISRC " << status.marker->cd_info["isrc"] << endl;
651         }
652
653         if (status.marker->name() != "") {
654                 status.out << "    TITLE " << cue_escape_cdtext (status.marker->name()) << endl;
655         }
656
657         if (status.marker->cd_info.find("performer") != status.marker->cd_info.end()) {
658                 status.out <<  "    PERFORMER " << cue_escape_cdtext (status.marker->cd_info["performer"]) << endl;
659         }
660
661         if (status.marker->cd_info.find("composer") != status.marker->cd_info.end()) {
662                 status.out << "    SONGWRITER " << cue_escape_cdtext (status.marker->cd_info["composer"]) << endl;
663         }
664
665         if (status.track_position != status.track_start_frame) {
666                 frames_to_cd_frames_string (buf, status.track_position);
667                 status.out << "    INDEX 00" << buf << endl;
668         }
669
670         frames_to_cd_frames_string (buf, status.track_start_frame);
671         status.out << "    INDEX 01" << buf << endl;
672
673         status.index_number = 2;
674         status.track_number++;
675 }
676
677 void
678 ExportHandler::write_track_info_toc (CDMarkerStatus & status)
679 {
680         gchar buf[18];
681
682         status.out << endl << "TRACK AUDIO" << endl;
683
684         if (status.marker->cd_info.find("scms") != status.marker->cd_info.end())  {
685                 status.out << "NO ";
686         }
687         status.out << "COPY" << endl;
688
689         if (status.marker->cd_info.find("preemph") != status.marker->cd_info.end())  {
690                 status.out << "PRE_EMPHASIS" << endl;
691         } else {
692                 status.out << "NO PRE_EMPHASIS" << endl;
693         }
694
695         if (status.marker->cd_info.find("isrc") != status.marker->cd_info.end())  {
696                 status.out << "ISRC \"" << status.marker->cd_info["isrc"] << "\"" << endl;
697         }
698
699         status.out << "CD_TEXT {" << endl << "  LANGUAGE 0 {" << endl;
700         status.out << "     TITLE " << toc_escape_cdtext (status.marker->name()) << endl;
701
702         status.out << "     PERFORMER ";
703         if (status.marker->cd_info.find("performer") != status.marker->cd_info.end()) {
704                 status.out << toc_escape_cdtext (status.marker->cd_info["performer"]) << endl;
705         } else {
706                 status.out << "\"\"" << endl;
707         }
708
709         if (status.marker->cd_info.find("composer") != status.marker->cd_info.end()) {
710                 status.out  << "     SONGWRITER " << toc_escape_cdtext (status.marker->cd_info["composer"]) << endl;
711         }
712
713         if (status.marker->cd_info.find("isrc") != status.marker->cd_info.end()) {
714                 status.out  << "     ISRC \"";
715                 status.out << status.marker->cd_info["isrc"].substr(0,2) << "-";
716                 status.out << status.marker->cd_info["isrc"].substr(2,3) << "-";
717                 status.out << status.marker->cd_info["isrc"].substr(5,2) << "-";
718                 status.out << status.marker->cd_info["isrc"].substr(7,5) << "\"" << endl;
719         }
720
721         status.out << "  }" << endl << "}" << endl;
722
723         frames_to_cd_frames_string (buf, status.track_position);
724         status.out << "FILE " << toc_escape_filename (status.filename) << ' ' << buf;
725
726         frames_to_cd_frames_string (buf, status.track_duration);
727         status.out << buf << endl;
728
729         frames_to_cd_frames_string (buf, status.track_start_frame - status.track_position);
730         status.out << "START" << buf << endl;
731 }
732
733 void ExportHandler::write_track_info_mp4ch (CDMarkerStatus & status)
734 {
735         gchar buf[18];
736
737         frames_to_chapter_marks_string(buf, status.track_start_frame);
738         status.out << buf << " " << status.marker->name() << endl;
739 }
740
741 void
742 ExportHandler::write_index_info_cue (CDMarkerStatus & status)
743 {
744         gchar buf[18];
745
746         snprintf (buf, sizeof(buf), "    INDEX %02d", cue_indexnum);
747         status.out << buf;
748         frames_to_cd_frames_string (buf, status.index_position);
749         status.out << buf << endl;
750
751         cue_indexnum++;
752 }
753
754 void
755 ExportHandler::write_index_info_toc (CDMarkerStatus & status)
756 {
757         gchar buf[18];
758
759         frames_to_cd_frames_string (buf, status.index_position - status.track_position);
760         status.out << "INDEX" << buf << endl;
761 }
762
763 void
764 ExportHandler::write_index_info_mp4ch (CDMarkerStatus & status)
765 {
766 }
767
768 void
769 ExportHandler::frames_to_cd_frames_string (char* buf, framepos_t when)
770 {
771         framecnt_t remainder;
772         framecnt_t fr = session.nominal_frame_rate();
773         int mins, secs, frames;
774
775         mins = when / (60 * fr);
776         remainder = when - (mins * 60 * fr);
777         secs = remainder / fr;
778         remainder -= secs * fr;
779         frames = remainder / (fr / 75);
780         sprintf (buf, " %02d:%02d:%02d", mins, secs, frames);
781 }
782
783 void
784 ExportHandler::frames_to_chapter_marks_string (char* buf, framepos_t when)
785 {
786         framecnt_t remainder;
787         framecnt_t fr = session.nominal_frame_rate();
788         int hours, mins, secs, msecs;
789
790         hours = when / (3600 * fr);
791         remainder = when - (hours * 3600 * fr);
792         mins = remainder / (60 * fr);
793         remainder -= mins * 60 * fr;
794         secs = remainder / fr;
795         remainder -= secs * fr;
796         msecs = (remainder * 1000) / fr;
797         sprintf (buf, "%02d:%02d:%02d.%03d", hours, mins, secs, msecs);
798 }
799
800 std::string
801 ExportHandler::toc_escape_cdtext (const std::string& txt)
802 {
803         Glib::ustring check (txt);
804         std::string out;
805         std::string latin1_txt;
806         char buf[5];
807
808         try {
809                 latin1_txt = Glib::convert (txt, "ISO-8859-1", "UTF-8");
810         } catch (Glib::ConvertError& err) {
811                 throw Glib::ConvertError (err.code(), string_compose (_("Cannot convert %1 to Latin-1 text"), txt));
812         }
813
814         out = '"';
815
816         for (std::string::const_iterator c = latin1_txt.begin(); c != latin1_txt.end(); ++c) {
817
818                 if ((*c) == '"') {
819                         out += "\\\"";
820                 } else if ((*c) == '\\') {
821                         out += "\\134";
822                 } else if (isprint (*c)) {
823                         out += *c;
824                 } else {
825                         snprintf (buf, sizeof (buf), "\\%03o", (int) (unsigned char) *c);
826                         out += buf;
827                 }
828         }
829
830         out += '"';
831
832         return out;
833 }
834
835 std::string
836 ExportHandler::toc_escape_filename (const std::string& txt)
837 {
838         std::string out;
839
840         out = '"';
841
842         // We iterate byte-wise not character-wise over a UTF-8 string here,
843         // because we only want to translate backslashes and double quotes
844         for (std::string::const_iterator c = txt.begin(); c != txt.end(); ++c) {
845
846                 if (*c == '"') {
847                         out += "\\\"";
848                 } else if (*c == '\\') {
849                         out += "\\134";
850                 } else {
851                         out += *c;
852                 }
853         }
854
855         out += '"';
856
857         return out;
858 }
859
860 std::string
861 ExportHandler::cue_escape_cdtext (const std::string& txt)
862 {
863         std::string latin1_txt;
864         std::string out;
865
866         try {
867                 latin1_txt = Glib::convert (txt, "ISO-8859-1", "UTF-8");
868         } catch (Glib::ConvertError& err) {
869                 throw Glib::ConvertError (err.code(), string_compose (_("Cannot convert %1 to Latin-1 text"), txt));
870         }
871
872         // does not do much mor than UTF-8 to Latin1 translation yet, but
873         // that may have to change if cue parsers in burning programs change
874         out = '"' + latin1_txt + '"';
875
876         return out;
877 }
878
879 } // namespace ARDOUR