use pbd's gstdio compatibility wrapper
[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                 if (!status.out) {
429                         error << string_compose(_("Editor: cannot open \"%1\" as export file for CD marker file"), filepath) << endmsg;
430                         return;
431                 }
432
433                 (this->*header_func) (status);
434
435                 /* Get locations and sort */
436
437                 Locations::LocationList const & locations (session.locations()->list());
438                 Locations::LocationList::const_iterator i;
439                 Locations::LocationList temp;
440
441                 for (i = locations.begin(); i != locations.end(); ++i) {
442                         if ((*i)->start() >= timespan->get_start() && (*i)->end() <= timespan->get_end() && (*i)->is_cd_marker() && !(*i)->is_session_range()) {
443                                 temp.push_back (*i);
444                         }
445                 }
446
447                 if (temp.empty()) {
448                         // TODO One index marker for whole thing
449                         return;
450                 }
451
452                 LocationSortByStart cmp;
453                 temp.sort (cmp);
454                 Locations::LocationList::const_iterator nexti;
455
456                 /* Start actual marker stuff */
457
458                 framepos_t last_end_time = timespan->get_start();
459                 status.track_position = 0;
460
461                 for (i = temp.begin(); i != temp.end(); ++i) {
462
463                         status.marker = *i;
464
465                         if ((*i)->start() < last_end_time) {
466                                 if ((*i)->is_mark()) {
467                                         /* Index within track */
468
469                                         status.index_position = (*i)->start() - timespan->get_start();
470                                         (this->*index_func) (status);
471                                 }
472
473                                 continue;
474                         }
475
476                         /* A track, defined by a cd range marker or a cd location marker outside of a cd range */
477
478                         status.track_position = last_end_time - timespan->get_start();
479                         status.track_start_frame = (*i)->start() - timespan->get_start();  // everything before this is the pregap
480                         status.track_duration = 0;
481
482                         if ((*i)->is_mark()) {
483                                 // a mark track location needs to look ahead to the next marker's start to determine length
484                                 nexti = i;
485                                 ++nexti;
486
487                                 if (nexti != temp.end()) {
488                                         status.track_duration = (*nexti)->start() - last_end_time;
489
490                                         last_end_time = (*nexti)->start();
491                                 } else {
492                                         // this was the last marker, use timespan end
493                                         status.track_duration = timespan->get_end() - last_end_time;
494
495                                         last_end_time = timespan->get_end();
496                                 }
497                         } else {
498                                 // range
499                                 status.track_duration = (*i)->end() - last_end_time;
500
501                                 last_end_time = (*i)->end();
502                         }
503
504                         (this->*track_func) (status);
505                 }
506
507         } catch (std::exception& e) {
508                 error << string_compose (_("an error occured while writing a TOC/CUE file: %1"), e.what()) << endmsg;
509                 ::g_unlink (filepath.c_str());
510         } catch (Glib::Exception& e) {
511                 error << string_compose (_("an error occured while writing a TOC/CUE file: %1"), e.what()) << endmsg;
512                 ::g_unlink (filepath.c_str());
513         }
514 }
515
516 string
517 ExportHandler::get_cd_marker_filename(std::string filename, CDMarkerFormat format)
518 {
519         /* do not strip file suffix because there may be more than one format,
520            and we do not want the CD marker file from one format to overwrite
521            another (e.g. foo.wav.cue > foo.aiff.cue)
522         */
523
524         switch (format) {
525         case CDMarkerTOC:
526                 return filename + ".toc";
527         case CDMarkerCUE:
528                 return filename + ".cue";
529         case MP4Chaps:
530         {
531                 unsigned lastdot = filename.find_last_of('.');
532                 return filename.substr(0,lastdot) + ".chapters.txt";
533         }
534         default:
535                 return filename + ".marker"; // Should not be reached when actually creating a file
536         }
537 }
538
539 void
540 ExportHandler::write_cue_header (CDMarkerStatus & status)
541 {
542         string title = status.timespan->name().compare ("Session") ? status.timespan->name() : (string) session.name();
543
544         // Album metadata
545         string barcode      = SessionMetadata::Metadata()->barcode();
546         string album_artist = SessionMetadata::Metadata()->album_artist();
547         string album_title  = SessionMetadata::Metadata()->album();
548
549         status.out << "REM Cue file generated by " << PROGRAM_NAME << endl;
550
551         if (barcode != "")
552                 status.out << "CATALOG " << barcode << endl;
553
554         if (album_artist != "")
555                 status.out << "PERFORMER " << cue_escape_cdtext (album_artist) << endl;
556
557         if (album_title != "")
558                 title = album_title;
559
560         status.out << "TITLE " << cue_escape_cdtext (title) << endl;
561
562         /*  The original cue sheet spec mentions five file types
563                 WAVE, AIFF,
564                 BINARY   = "header-less" audio (44.1 kHz, 16 Bit, little endian),
565                 MOTOROLA = "header-less" audio (44.1 kHz, 16 Bit, big endian),
566                 and MP3
567                 
568                 We try to use these file types whenever appropriate and 
569                 default to our own names otherwise.
570         */
571         status.out << "FILE \"" << Glib::path_get_basename(status.filename) << "\" ";
572         if (!status.format->format_name().compare ("WAV")  || !status.format->format_name().compare ("BWF")) {
573                 status.out  << "WAVE";
574         } else if (status.format->format_id() == ExportFormatBase::F_RAW &&
575                    status.format->sample_format() == ExportFormatBase::SF_16 &&
576                    status.format->sample_rate() == ExportFormatBase::SR_44_1) {
577                 // Format is RAW 16bit 44.1kHz
578                 if (status.format->endianness() == ExportFormatBase::E_Little) {
579                         status.out << "BINARY";
580                 } else {
581                         status.out << "MOTOROLA";
582                 }
583         } else {
584                 // no special case for AIFF format it's name is already "AIFF"
585                 status.out << status.format->format_name();
586         }
587         status.out << endl;
588 }
589
590 void
591 ExportHandler::write_toc_header (CDMarkerStatus & status)
592 {
593         string title = status.timespan->name().compare ("Session") ? status.timespan->name() : (string) session.name();
594
595         // Album metadata
596         string barcode      = SessionMetadata::Metadata()->barcode();
597         string album_artist = SessionMetadata::Metadata()->album_artist();
598         string album_title  = SessionMetadata::Metadata()->album();
599
600         if (barcode != "")
601                 status.out << "CATALOG \"" << barcode << "\"" << endl;
602
603         if (album_title != "")
604                 title = album_title;
605
606         status.out << "CD_DA" << endl;
607         status.out << "CD_TEXT {" << endl << "  LANGUAGE_MAP {" << endl << "    0 : EN" << endl << "  }" << endl;
608         status.out << "  LANGUAGE 0 {" << endl << "    TITLE " << toc_escape_cdtext (title) << endl ;
609         status.out << "    PERFORMER " << toc_escape_cdtext (album_artist) << endl;
610         status.out << "  }" << endl << "}" << endl;
611 }
612
613 void
614 ExportHandler::write_mp4ch_header (CDMarkerStatus & status)
615 {
616         status.out << "00:00:00.000 Intro" << endl;
617 }
618
619 void
620 ExportHandler::write_track_info_cue (CDMarkerStatus & status)
621 {
622         gchar buf[18];
623
624         snprintf (buf, sizeof(buf), "  TRACK %02d AUDIO", status.track_number);
625         status.out << buf << endl;
626
627         status.out << "    FLAGS" ;
628         if (status.marker->cd_info.find("scms") != status.marker->cd_info.end())  {
629                 status.out << " SCMS ";
630         } else {
631                 status.out << " DCP ";
632         }
633
634         if (status.marker->cd_info.find("preemph") != status.marker->cd_info.end())  {
635                 status.out << " PRE";
636         }
637         status.out << endl;
638
639         if (status.marker->cd_info.find("isrc") != status.marker->cd_info.end())  {
640                 status.out << "    ISRC " << status.marker->cd_info["isrc"] << endl;
641         }
642
643         if (status.marker->name() != "") {
644                 status.out << "    TITLE " << cue_escape_cdtext (status.marker->name()) << endl;
645         }
646
647         if (status.marker->cd_info.find("performer") != status.marker->cd_info.end()) {
648                 status.out <<  "    PERFORMER " << cue_escape_cdtext (status.marker->cd_info["performer"]) << endl;
649         }
650
651         if (status.marker->cd_info.find("composer") != status.marker->cd_info.end()) {
652                 status.out << "    SONGWRITER " << cue_escape_cdtext (status.marker->cd_info["composer"]) << endl;
653         }
654
655         if (status.track_position != status.track_start_frame) {
656                 frames_to_cd_frames_string (buf, status.track_position);
657                 status.out << "    INDEX 00" << buf << endl;
658         }
659
660         frames_to_cd_frames_string (buf, status.track_start_frame);
661         status.out << "    INDEX 01" << buf << endl;
662
663         status.index_number = 2;
664         status.track_number++;
665 }
666
667 void
668 ExportHandler::write_track_info_toc (CDMarkerStatus & status)
669 {
670         gchar buf[18];
671
672         status.out << endl << "TRACK AUDIO" << endl;
673
674         if (status.marker->cd_info.find("scms") != status.marker->cd_info.end())  {
675                 status.out << "NO ";
676         }
677         status.out << "COPY" << endl;
678
679         if (status.marker->cd_info.find("preemph") != status.marker->cd_info.end())  {
680                 status.out << "PRE_EMPHASIS" << endl;
681         } else {
682                 status.out << "NO PRE_EMPHASIS" << endl;
683         }
684
685         if (status.marker->cd_info.find("isrc") != status.marker->cd_info.end())  {
686                 status.out << "ISRC \"" << status.marker->cd_info["isrc"] << "\"" << endl;
687         }
688
689         status.out << "CD_TEXT {" << endl << "  LANGUAGE 0 {" << endl;
690         status.out << "     TITLE " << toc_escape_cdtext (status.marker->name()) << endl;
691         
692         status.out << "     PERFORMER ";
693         if (status.marker->cd_info.find("performer") != status.marker->cd_info.end()) {
694                 status.out << toc_escape_cdtext (status.marker->cd_info["performer"]) << endl;
695         } else {
696                 status.out << "\"\"" << endl;
697         }
698         
699         if (status.marker->cd_info.find("composer") != status.marker->cd_info.end()) {
700                 status.out  << "     SONGWRITER " << toc_escape_cdtext (status.marker->cd_info["composer"]) << endl;
701         }
702
703         if (status.marker->cd_info.find("isrc") != status.marker->cd_info.end()) {
704                 status.out  << "     ISRC \"";
705                 status.out << status.marker->cd_info["isrc"].substr(0,2) << "-";
706                 status.out << status.marker->cd_info["isrc"].substr(2,3) << "-";
707                 status.out << status.marker->cd_info["isrc"].substr(5,2) << "-";
708                 status.out << status.marker->cd_info["isrc"].substr(7,5) << "\"" << endl;
709         }
710
711         status.out << "  }" << endl << "}" << endl;
712
713         frames_to_cd_frames_string (buf, status.track_position);
714         status.out << "FILE " << toc_escape_filename (status.filename) << ' ' << buf;
715
716         frames_to_cd_frames_string (buf, status.track_duration);
717         status.out << buf << endl;
718
719         frames_to_cd_frames_string (buf, status.track_start_frame - status.track_position);
720         status.out << "START" << buf << endl;
721 }
722
723 void ExportHandler::write_track_info_mp4ch (CDMarkerStatus & status)
724 {
725         gchar buf[18];
726
727         frames_to_chapter_marks_string(buf, status.track_start_frame);
728         status.out << buf << " " << status.marker->name() << endl;
729 }
730
731 void
732 ExportHandler::write_index_info_cue (CDMarkerStatus & status)
733 {
734         gchar buf[18];
735
736         snprintf (buf, sizeof(buf), "    INDEX %02d", cue_indexnum);
737         status.out << buf;
738         frames_to_cd_frames_string (buf, status.index_position);
739         status.out << buf << endl;
740
741         cue_indexnum++;
742 }
743
744 void
745 ExportHandler::write_index_info_toc (CDMarkerStatus & status)
746 {
747         gchar buf[18];
748
749         frames_to_cd_frames_string (buf, status.index_position - status.track_position);
750         status.out << "INDEX" << buf << endl;
751 }
752
753 void
754 ExportHandler::write_index_info_mp4ch (CDMarkerStatus & status)
755 {
756 }
757
758 void
759 ExportHandler::frames_to_cd_frames_string (char* buf, framepos_t when)
760 {
761         framecnt_t remainder;
762         framecnt_t fr = session.nominal_frame_rate();
763         int mins, secs, frames;
764
765         mins = when / (60 * fr);
766         remainder = when - (mins * 60 * fr);
767         secs = remainder / fr;
768         remainder -= secs * fr;
769         frames = remainder / (fr / 75);
770         sprintf (buf, " %02d:%02d:%02d", mins, secs, frames);
771 }
772
773 void
774 ExportHandler::frames_to_chapter_marks_string (char* buf, framepos_t when)
775 {
776         framecnt_t remainder;
777         framecnt_t fr = session.nominal_frame_rate();
778         int hours, mins, secs, msecs;
779
780         hours = when / (3600 * fr);
781         remainder = when - (hours * 3600 * fr);
782         mins = remainder / (60 * fr);
783         remainder -= mins * 60 * fr;
784         secs = remainder / fr;
785         remainder -= secs * fr;
786         msecs = (remainder * 1000) / fr;
787         sprintf (buf, "%02d:%02d:%02d.%03d", hours, mins, secs, msecs);
788 }
789
790 std::string
791 ExportHandler::toc_escape_cdtext (const std::string& txt)
792 {
793         Glib::ustring check (txt);
794         std::string out;
795         std::string latin1_txt;
796         char buf[5];
797
798         try {
799                 latin1_txt = Glib::convert (txt, "ISO-8859-1", "UTF-8");
800         } catch (Glib::ConvertError& err) {
801                 throw Glib::ConvertError (err.code(), string_compose (_("Cannot convert %1 to Latin-1 text"), txt));
802         }
803
804         out = '"';
805
806         for (std::string::const_iterator c = latin1_txt.begin(); c != latin1_txt.end(); ++c) {
807
808                 if ((*c) == '"') {
809                         out += "\\\"";
810                 } else if ((*c) == '\\') {
811                         out += "\\134";
812                 } else if (isprint (*c)) {
813                         out += *c;
814                 } else {
815                         snprintf (buf, sizeof (buf), "\\%03o", (int) (unsigned char) *c);
816                         out += buf;
817                 }
818         }
819         
820         out += '"';
821
822         return out;
823 }
824
825 std::string
826 ExportHandler::toc_escape_filename (const std::string& txt)
827 {
828         std::string out;
829
830         out = '"';
831
832         // We iterate byte-wise not character-wise over a UTF-8 string here,
833         // because we only want to translate backslashes and double quotes
834         for (std::string::const_iterator c = txt.begin(); c != txt.end(); ++c) {
835
836                 if (*c == '"') {
837                         out += "\\\"";
838                 } else if (*c == '\\') {
839                         out += "\\134";
840                 } else {
841                         out += *c;
842                 }
843         }
844         
845         out += '"';
846
847         return out;
848 }
849
850 std::string
851 ExportHandler::cue_escape_cdtext (const std::string& txt)
852 {
853         std::string latin1_txt;
854         std::string out;
855         
856         try {
857                 latin1_txt = Glib::convert (txt, "ISO-8859-1", "UTF-8");
858         } catch (Glib::ConvertError& err) {
859                 throw Glib::ConvertError (err.code(), string_compose (_("Cannot convert %1 to Latin-1 text"), txt));
860         }
861         
862         // does not do much mor than UTF-8 to Latin1 translation yet, but
863         // that may have to change if cue parsers in burning programs change 
864         out = '"' + latin1_txt + '"';
865
866         return out;
867 }
868
869 } // namespace ARDOUR