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