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