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