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