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