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