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