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