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