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