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