enough with umpteen "i18n.h" files. Consolidate on pbd/i18n.h
[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 "pbd/gstdio_compat.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 "pbd/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         graph_builder->cleanup (export_status->aborted () );
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         if (export_status->total_timespans > 1) {
152                 // always include timespan if there's more than one.
153                 for (ConfigMap::iterator it = config_map.begin(); it != config_map.end(); ++it) {
154                         FileSpec & spec = it->second;
155                         spec.filename->include_timespan = true;
156                 }
157         }
158
159         /* Start export */
160
161         Glib::Threads::Mutex::Lock l (export_status->lock());
162         start_timespan ();
163 }
164
165 void
166 ExportHandler::start_timespan ()
167 {
168         export_status->timespan++;
169
170         if (config_map.empty()) {
171                 // freewheeling has to be stopped from outside the process cycle
172                 export_status->set_running (false);
173                 return;
174         }
175
176         /* finish_timespan pops the config_map entry that has been done, so
177            this is the timespan to do this time
178         */
179         current_timespan = config_map.begin()->first;
180
181         export_status->total_frames_current_timespan = current_timespan->get_length();
182         export_status->timespan_name = current_timespan->name();
183         export_status->processed_frames_current_timespan = 0;
184
185         /* Register file configurations to graph builder */
186
187         /* Here's the config_map entries that use this timespan */
188         timespan_bounds = config_map.equal_range (current_timespan);
189         graph_builder->reset ();
190         graph_builder->set_current_timespan (current_timespan);
191         handle_duplicate_format_extensions();
192         for (ConfigMap::iterator it = timespan_bounds.first; it != timespan_bounds.second; ++it) {
193                 // Filenames can be shared across timespans
194                 FileSpec & spec = it->second;
195                 spec.filename->set_timespan (it->first);
196                 graph_builder->add_config (spec);
197         }
198
199         /* start export */
200
201         normalizing = false;
202         session.ProcessExport.connect_same_thread (process_connection, boost::bind (&ExportHandler::process, this, _1));
203         process_position = current_timespan->get_start();
204         session.start_audio_export (process_position);
205 }
206
207 void
208 ExportHandler::handle_duplicate_format_extensions()
209 {
210         typedef std::map<std::string, int> ExtCountMap;
211
212         ExtCountMap counts;
213         for (ConfigMap::iterator it = timespan_bounds.first; it != timespan_bounds.second; ++it) {
214                 counts[it->second.format->extension()]++;
215         }
216
217         bool duplicates_found = false;
218         for (ExtCountMap::iterator it = counts.begin(); it != counts.end(); ++it) {
219                 if (it->second > 1) { duplicates_found = true; }
220         }
221
222         // Set this always, as the filenames are shared...
223         for (ConfigMap::iterator it = timespan_bounds.first; it != timespan_bounds.second; ++it) {
224                 it->second.filename->include_format_name = duplicates_found;
225         }
226 }
227
228 int
229 ExportHandler::process (framecnt_t frames)
230 {
231         if (!export_status->running ()) {
232                 return 0;
233         } else if (normalizing) {
234                 Glib::Threads::Mutex::Lock l (export_status->lock());
235                 return process_normalize ();
236         } else {
237                 Glib::Threads::Mutex::Lock l (export_status->lock());
238                 return process_timespan (frames);
239         }
240 }
241
242 int
243 ExportHandler::process_timespan (framecnt_t frames)
244 {
245         export_status->active_job = ExportStatus::Exporting;
246         /* update position */
247
248         framecnt_t frames_to_read = 0;
249         framepos_t const end = current_timespan->get_end();
250
251         bool const last_cycle = (process_position + frames >= end);
252
253         if (last_cycle) {
254                 frames_to_read = end - process_position;
255                 export_status->stop = true;
256         } else {
257                 frames_to_read = frames;
258         }
259
260         process_position += frames_to_read;
261         export_status->processed_frames += frames_to_read;
262         export_status->processed_frames_current_timespan += frames_to_read;
263
264         /* Do actual processing */
265         int ret = graph_builder->process (frames_to_read, last_cycle);
266
267         /* Start normalizing if necessary */
268         if (last_cycle) {
269                 normalizing = graph_builder->will_normalize();
270                 if (normalizing) {
271                         export_status->total_normalize_cycles = graph_builder->get_normalize_cycle_count();
272                         export_status->current_normalize_cycle = 0;
273                 } else {
274                         finish_timespan ();
275                         return 0;
276                 }
277         }
278
279         return ret;
280 }
281
282 int
283 ExportHandler::process_normalize ()
284 {
285         if (graph_builder->process_normalize ()) {
286                 finish_timespan ();
287                 export_status->active_job = ExportStatus::Exporting;
288         } else {
289                 export_status->active_job = ExportStatus::Normalizing;
290         }
291
292         export_status->current_normalize_cycle++;
293
294         return 0;
295 }
296
297 void
298 ExportHandler::command_output(std::string output, size_t size)
299 {
300         std::cerr << "command: " << size << ", " << output << std::endl;
301         info << output << endmsg;
302 }
303
304 void
305 ExportHandler::finish_timespan ()
306 {
307         graph_builder->get_analysis_results (export_status->result_map);
308
309         while (config_map.begin() != timespan_bounds.second) {
310
311                 ExportFormatSpecPtr fmt = config_map.begin()->second.format;
312                 std::string filename = config_map.begin()->second.filename->get_path(fmt);
313                 if (fmt->with_cue()) {
314                         export_cd_marker_file (current_timespan, fmt, filename, CDMarkerCUE);
315                 }
316
317                 if (fmt->with_toc()) {
318                         export_cd_marker_file (current_timespan, fmt, filename, CDMarkerTOC);
319                 }
320
321                 if (fmt->with_mp4chaps()) {
322                         export_cd_marker_file (current_timespan, fmt, filename, MP4Chaps);
323                 }
324
325                 Session::Exported (current_timespan->name(), filename); /* EMIT SIGNAL */
326
327                 /* close file first, otherwise TagLib enounters an ERROR_SHARING_VIOLATION
328                  * The process cannot access the file because it is being used.
329                  * ditto for post-export and upload.
330                  */
331                 graph_builder->reset ();
332
333                 if (fmt->tag()) {
334                         /* TODO: check Umlauts and encoding in filename.
335                          * TagLib eventually calls CreateFileA(),
336                          */
337                         export_status->active_job = ExportStatus::Tagging;
338                         AudiofileTagger::tag_file(filename, *SessionMetadata::Metadata());
339                 }
340
341                 if (!fmt->command().empty()) {
342                         SessionMetadata const & metadata (*SessionMetadata::Metadata());
343
344 #if 0   // would be nicer with C++11 initialiser...
345                         std::map<char, std::string> subs {
346                                 { 'f', filename },
347                                 { 'd', Glib::path_get_dirname(filename)  + G_DIR_SEPARATOR },
348                                 { 'b', PBD::basename_nosuffix(filename) },
349                                 ...
350                         };
351 #endif
352                         export_status->active_job = ExportStatus::Command;
353                         PBD::ScopedConnection command_connection;
354                         std::map<char, std::string> subs;
355
356                         std::stringstream track_number;
357                         track_number << metadata.track_number ();
358                         std::stringstream total_tracks;
359                         total_tracks << metadata.total_tracks ();
360                         std::stringstream year;
361                         year << metadata.year ();
362
363                         subs.insert (std::pair<char, std::string> ('a', metadata.artist ()));
364                         subs.insert (std::pair<char, std::string> ('b', PBD::basename_nosuffix (filename)));
365                         subs.insert (std::pair<char, std::string> ('c', metadata.copyright ()));
366                         subs.insert (std::pair<char, std::string> ('d', Glib::path_get_dirname (filename) + G_DIR_SEPARATOR));
367                         subs.insert (std::pair<char, std::string> ('f', filename));
368                         subs.insert (std::pair<char, std::string> ('l', metadata.lyricist ()));
369                         subs.insert (std::pair<char, std::string> ('n', session.name ()));
370                         subs.insert (std::pair<char, std::string> ('s', session.path ()));
371                         subs.insert (std::pair<char, std::string> ('o', metadata.conductor ()));
372                         subs.insert (std::pair<char, std::string> ('t', metadata.title ()));
373                         subs.insert (std::pair<char, std::string> ('z', metadata.organization ()));
374                         subs.insert (std::pair<char, std::string> ('A', metadata.album ()));
375                         subs.insert (std::pair<char, std::string> ('C', metadata.comment ()));
376                         subs.insert (std::pair<char, std::string> ('E', metadata.engineer ()));
377                         subs.insert (std::pair<char, std::string> ('G', metadata.genre ()));
378                         subs.insert (std::pair<char, std::string> ('L', total_tracks.str ()));
379                         subs.insert (std::pair<char, std::string> ('M', metadata.mixer ()));
380                         subs.insert (std::pair<char, std::string> ('N', current_timespan->name())); // =?= config_map.begin()->first->name ()
381                         subs.insert (std::pair<char, std::string> ('O', metadata.composer ()));
382                         subs.insert (std::pair<char, std::string> ('P', metadata.producer ()));
383                         subs.insert (std::pair<char, std::string> ('S', metadata.disc_subtitle ()));
384                         subs.insert (std::pair<char, std::string> ('T', track_number.str ()));
385                         subs.insert (std::pair<char, std::string> ('Y', year.str ()));
386                         subs.insert (std::pair<char, std::string> ('Z', metadata.country ()));
387
388                         ARDOUR::SystemExec *se = new ARDOUR::SystemExec(fmt->command(), subs);
389                         info << "Post-export command line : {" << se->to_s () << "}" << endmsg;
390                         se->ReadStdout.connect_same_thread(command_connection, boost::bind(&ExportHandler::command_output, this, _1, _2));
391                         int ret = se->start (2);
392                         if (ret == 0) {
393                                 // successfully started
394                                 while (se->is_running ()) {
395                                         // wait for system exec to terminate
396                                         Glib::usleep (1000);
397                                 }
398                         } else {
399                                 error << "Post-export command FAILED with Error: " << ret << endmsg;
400                         }
401                         delete (se);
402                 }
403
404                 if (fmt->soundcloud_upload()) {
405                         SoundcloudUploader *soundcloud_uploader = new SoundcloudUploader;
406                         std::string token = soundcloud_uploader->Get_Auth_Token(soundcloud_username, soundcloud_password);
407                         DEBUG_TRACE (DEBUG::Soundcloud, string_compose(
408                                                 "uploading %1 - username=%2, password=%3, token=%4",
409                                                 filename, soundcloud_username, soundcloud_password, token) );
410                         std::string path = soundcloud_uploader->Upload (
411                                         filename,
412                                         PBD::basename_nosuffix(filename), // title
413                                         token,
414                                         soundcloud_make_public,
415                                         soundcloud_downloadable,
416                                         this);
417
418                         if (path.length() != 0) {
419                                 info << string_compose ( _("File %1 uploaded to %2"), filename, path) << endmsg;
420                                 if (soundcloud_open_page) {
421                                         DEBUG_TRACE (DEBUG::Soundcloud, string_compose ("opening %1", path) );
422                                         open_uri(path.c_str());  // open the soundcloud website to the new file
423                                 }
424                         } else {
425                                 error << _("upload to Soundcloud failed. Perhaps your email or password are incorrect?\n") << endmsg;
426                         }
427                         delete soundcloud_uploader;
428                 }
429                 config_map.erase (config_map.begin());
430         }
431
432         start_timespan ();
433 }
434
435 /*** CD Marker stuff ***/
436
437 struct LocationSortByStart {
438     bool operator() (Location *a, Location *b) {
439             return a->start() < b->start();
440     }
441 };
442
443 void
444 ExportHandler::export_cd_marker_file (ExportTimespanPtr timespan, ExportFormatSpecPtr file_format,
445                                       std::string filename, CDMarkerFormat format)
446 {
447         string filepath = get_cd_marker_filename(filename, format);
448
449         try {
450                 void (ExportHandler::*header_func) (CDMarkerStatus &);
451                 void (ExportHandler::*track_func) (CDMarkerStatus &);
452                 void (ExportHandler::*index_func) (CDMarkerStatus &);
453
454                 switch (format) {
455                 case CDMarkerTOC:
456                         header_func = &ExportHandler::write_toc_header;
457                         track_func = &ExportHandler::write_track_info_toc;
458                         index_func = &ExportHandler::write_index_info_toc;
459                         break;
460                 case CDMarkerCUE:
461                         header_func = &ExportHandler::write_cue_header;
462                         track_func = &ExportHandler::write_track_info_cue;
463                         index_func = &ExportHandler::write_index_info_cue;
464                         break;
465                 case MP4Chaps:
466                         header_func = &ExportHandler::write_mp4ch_header;
467                         track_func = &ExportHandler::write_track_info_mp4ch;
468                         index_func = &ExportHandler::write_index_info_mp4ch;
469                         break;
470                 default:
471                         return;
472                 }
473
474                 CDMarkerStatus status (filepath, timespan, file_format, filename);
475
476                 (this->*header_func) (status);
477
478                 /* Get locations and sort */
479
480                 Locations::LocationList const & locations (session.locations()->list());
481                 Locations::LocationList::const_iterator i;
482                 Locations::LocationList temp;
483
484                 for (i = locations.begin(); i != locations.end(); ++i) {
485                         if ((*i)->start() >= timespan->get_start() && (*i)->end() <= timespan->get_end() && (*i)->is_cd_marker() && !(*i)->is_session_range()) {
486                                 temp.push_back (*i);
487                         }
488                 }
489
490                 if (temp.empty()) {
491                         // TODO One index marker for whole thing
492                         return;
493                 }
494
495                 LocationSortByStart cmp;
496                 temp.sort (cmp);
497                 Locations::LocationList::const_iterator nexti;
498
499                 /* Start actual marker stuff */
500
501                 framepos_t last_end_time = timespan->get_start();
502                 status.track_position = 0;
503
504                 for (i = temp.begin(); i != temp.end(); ++i) {
505
506                         status.marker = *i;
507
508                         if ((*i)->start() < last_end_time) {
509                                 if ((*i)->is_mark()) {
510                                         /* Index within track */
511
512                                         status.index_position = (*i)->start() - timespan->get_start();
513                                         (this->*index_func) (status);
514                                 }
515
516                                 continue;
517                         }
518
519                         /* A track, defined by a cd range marker or a cd location marker outside of a cd range */
520
521                         status.track_position = last_end_time - timespan->get_start();
522                         status.track_start_frame = (*i)->start() - timespan->get_start();  // everything before this is the pregap
523                         status.track_duration = 0;
524
525                         if ((*i)->is_mark()) {
526                                 // a mark track location needs to look ahead to the next marker's start to determine length
527                                 nexti = i;
528                                 ++nexti;
529
530                                 if (nexti != temp.end()) {
531                                         status.track_duration = (*nexti)->start() - last_end_time;
532
533                                         last_end_time = (*nexti)->start();
534                                 } else {
535                                         // this was the last marker, use timespan end
536                                         status.track_duration = timespan->get_end() - last_end_time;
537
538                                         last_end_time = timespan->get_end();
539                                 }
540                         } else {
541                                 // range
542                                 status.track_duration = (*i)->end() - last_end_time;
543
544                                 last_end_time = (*i)->end();
545                         }
546
547                         (this->*track_func) (status);
548                 }
549
550         } catch (std::exception& e) {
551                 error << string_compose (_("an error occurred while writing a TOC/CUE file: %1"), e.what()) << endmsg;
552                 ::g_unlink (filepath.c_str());
553         } catch (Glib::Exception& e) {
554                 error << string_compose (_("an error occurred while writing a TOC/CUE file: %1"), e.what()) << endmsg;
555                 ::g_unlink (filepath.c_str());
556         }
557 }
558
559 string
560 ExportHandler::get_cd_marker_filename(std::string filename, CDMarkerFormat format)
561 {
562         /* do not strip file suffix because there may be more than one format,
563            and we do not want the CD marker file from one format to overwrite
564            another (e.g. foo.wav.cue > foo.aiff.cue)
565         */
566
567         switch (format) {
568         case CDMarkerTOC:
569                 return filename + ".toc";
570         case CDMarkerCUE:
571                 return filename + ".cue";
572         case MP4Chaps:
573         {
574                 unsigned lastdot = filename.find_last_of('.');
575                 return filename.substr(0,lastdot) + ".chapters.txt";
576         }
577         default:
578                 return filename + ".marker"; // Should not be reached when actually creating a file
579         }
580 }
581
582 void
583 ExportHandler::write_cue_header (CDMarkerStatus & status)
584 {
585         string title = status.timespan->name().compare ("Session") ? status.timespan->name() : (string) session.name();
586
587         // Album metadata
588         string barcode      = SessionMetadata::Metadata()->barcode();
589         string album_artist = SessionMetadata::Metadata()->album_artist();
590         string album_title  = SessionMetadata::Metadata()->album();
591
592         status.out << "REM Cue file generated by " << PROGRAM_NAME << endl;
593
594         if (barcode != "")
595                 status.out << "CATALOG " << barcode << endl;
596
597         if (album_artist != "")
598                 status.out << "PERFORMER " << cue_escape_cdtext (album_artist) << endl;
599
600         if (album_title != "")
601                 title = album_title;
602
603         status.out << "TITLE " << cue_escape_cdtext (title) << endl;
604
605         /*  The original cue sheet spec mentions five file types
606                 WAVE, AIFF,
607                 BINARY   = "header-less" audio (44.1 kHz, 16 Bit, little endian),
608                 MOTOROLA = "header-less" audio (44.1 kHz, 16 Bit, big endian),
609                 and MP3
610
611                 We try to use these file types whenever appropriate and
612                 default to our own names otherwise.
613         */
614         status.out << "FILE \"" << Glib::path_get_basename(status.filename) << "\" ";
615         if (!status.format->format_name().compare ("WAV")  || !status.format->format_name().compare ("BWF")) {
616                 status.out  << "WAVE";
617         } else if (status.format->format_id() == ExportFormatBase::F_RAW &&
618                    status.format->sample_format() == ExportFormatBase::SF_16 &&
619                    status.format->sample_rate() == ExportFormatBase::SR_44_1) {
620                 // Format is RAW 16bit 44.1kHz
621                 if (status.format->endianness() == ExportFormatBase::E_Little) {
622                         status.out << "BINARY";
623                 } else {
624                         status.out << "MOTOROLA";
625                 }
626         } else {
627                 // no special case for AIFF format it's name is already "AIFF"
628                 status.out << status.format->format_name();
629         }
630         status.out << endl;
631 }
632
633 void
634 ExportHandler::write_toc_header (CDMarkerStatus & status)
635 {
636         string title = status.timespan->name().compare ("Session") ? status.timespan->name() : (string) session.name();
637
638         // Album metadata
639         string barcode      = SessionMetadata::Metadata()->barcode();
640         string album_artist = SessionMetadata::Metadata()->album_artist();
641         string album_title  = SessionMetadata::Metadata()->album();
642
643         if (barcode != "")
644                 status.out << "CATALOG \"" << barcode << "\"" << endl;
645
646         if (album_title != "")
647                 title = album_title;
648
649         status.out << "CD_DA" << endl;
650         status.out << "CD_TEXT {" << endl << "  LANGUAGE_MAP {" << endl << "    0 : EN" << endl << "  }" << endl;
651         status.out << "  LANGUAGE 0 {" << endl << "    TITLE " << toc_escape_cdtext (title) << endl ;
652         status.out << "    PERFORMER " << toc_escape_cdtext (album_artist) << endl;
653         status.out << "  }" << endl << "}" << endl;
654 }
655
656 void
657 ExportHandler::write_mp4ch_header (CDMarkerStatus & status)
658 {
659         status.out << "00:00:00.000 Intro" << endl;
660 }
661
662 void
663 ExportHandler::write_track_info_cue (CDMarkerStatus & status)
664 {
665         gchar buf[18];
666
667         snprintf (buf, sizeof(buf), "  TRACK %02d AUDIO", status.track_number);
668         status.out << buf << endl;
669
670         status.out << "    FLAGS" ;
671         if (status.marker->cd_info.find("scms") != status.marker->cd_info.end())  {
672                 status.out << " SCMS ";
673         } else {
674                 status.out << " DCP ";
675         }
676
677         if (status.marker->cd_info.find("preemph") != status.marker->cd_info.end())  {
678                 status.out << " PRE";
679         }
680         status.out << endl;
681
682         if (status.marker->cd_info.find("isrc") != status.marker->cd_info.end())  {
683                 status.out << "    ISRC " << status.marker->cd_info["isrc"] << endl;
684         }
685
686         if (status.marker->name() != "") {
687                 status.out << "    TITLE " << cue_escape_cdtext (status.marker->name()) << endl;
688         }
689
690         if (status.marker->cd_info.find("performer") != status.marker->cd_info.end()) {
691                 status.out <<  "    PERFORMER " << cue_escape_cdtext (status.marker->cd_info["performer"]) << endl;
692         }
693
694         if (status.marker->cd_info.find("composer") != status.marker->cd_info.end()) {
695                 status.out << "    SONGWRITER " << cue_escape_cdtext (status.marker->cd_info["composer"]) << endl;
696         }
697
698         if (status.track_position != status.track_start_frame) {
699                 frames_to_cd_frames_string (buf, status.track_position);
700                 status.out << "    INDEX 00" << buf << endl;
701         }
702
703         frames_to_cd_frames_string (buf, status.track_start_frame);
704         status.out << "    INDEX 01" << buf << endl;
705
706         status.index_number = 2;
707         status.track_number++;
708 }
709
710 void
711 ExportHandler::write_track_info_toc (CDMarkerStatus & status)
712 {
713         gchar buf[18];
714
715         status.out << endl << "TRACK AUDIO" << endl;
716
717         if (status.marker->cd_info.find("scms") != status.marker->cd_info.end())  {
718                 status.out << "NO ";
719         }
720         status.out << "COPY" << endl;
721
722         if (status.marker->cd_info.find("preemph") != status.marker->cd_info.end())  {
723                 status.out << "PRE_EMPHASIS" << endl;
724         } else {
725                 status.out << "NO PRE_EMPHASIS" << endl;
726         }
727
728         if (status.marker->cd_info.find("isrc") != status.marker->cd_info.end())  {
729                 status.out << "ISRC \"" << status.marker->cd_info["isrc"] << "\"" << endl;
730         }
731
732         status.out << "CD_TEXT {" << endl << "  LANGUAGE 0 {" << endl;
733         status.out << "     TITLE " << toc_escape_cdtext (status.marker->name()) << endl;
734
735         status.out << "     PERFORMER ";
736         if (status.marker->cd_info.find("performer") != status.marker->cd_info.end()) {
737                 status.out << toc_escape_cdtext (status.marker->cd_info["performer"]) << endl;
738         } else {
739                 status.out << "\"\"" << endl;
740         }
741
742         if (status.marker->cd_info.find("composer") != status.marker->cd_info.end()) {
743                 status.out  << "     SONGWRITER " << toc_escape_cdtext (status.marker->cd_info["composer"]) << endl;
744         }
745
746         if (status.marker->cd_info.find("isrc") != status.marker->cd_info.end()) {
747                 status.out  << "     ISRC \"";
748                 status.out << status.marker->cd_info["isrc"].substr(0,2) << "-";
749                 status.out << status.marker->cd_info["isrc"].substr(2,3) << "-";
750                 status.out << status.marker->cd_info["isrc"].substr(5,2) << "-";
751                 status.out << status.marker->cd_info["isrc"].substr(7,5) << "\"" << endl;
752         }
753
754         status.out << "  }" << endl << "}" << endl;
755
756         frames_to_cd_frames_string (buf, status.track_position);
757         status.out << "FILE " << toc_escape_filename (status.filename) << ' ' << buf;
758
759         frames_to_cd_frames_string (buf, status.track_duration);
760         status.out << buf << endl;
761
762         frames_to_cd_frames_string (buf, status.track_start_frame - status.track_position);
763         status.out << "START" << buf << endl;
764 }
765
766 void ExportHandler::write_track_info_mp4ch (CDMarkerStatus & status)
767 {
768         gchar buf[18];
769
770         frames_to_chapter_marks_string(buf, status.track_start_frame);
771         status.out << buf << " " << status.marker->name() << endl;
772 }
773
774 void
775 ExportHandler::write_index_info_cue (CDMarkerStatus & status)
776 {
777         gchar buf[18];
778
779         snprintf (buf, sizeof(buf), "    INDEX %02d", cue_indexnum);
780         status.out << buf;
781         frames_to_cd_frames_string (buf, status.index_position);
782         status.out << buf << endl;
783
784         cue_indexnum++;
785 }
786
787 void
788 ExportHandler::write_index_info_toc (CDMarkerStatus & status)
789 {
790         gchar buf[18];
791
792         frames_to_cd_frames_string (buf, status.index_position - status.track_position);
793         status.out << "INDEX" << buf << endl;
794 }
795
796 void
797 ExportHandler::write_index_info_mp4ch (CDMarkerStatus & status)
798 {
799 }
800
801 void
802 ExportHandler::frames_to_cd_frames_string (char* buf, framepos_t when)
803 {
804         framecnt_t remainder;
805         framecnt_t fr = session.nominal_frame_rate();
806         int mins, secs, frames;
807
808         mins = when / (60 * fr);
809         remainder = when - (mins * 60 * fr);
810         secs = remainder / fr;
811         remainder -= secs * fr;
812         frames = remainder / (fr / 75);
813         sprintf (buf, " %02d:%02d:%02d", mins, secs, frames);
814 }
815
816 void
817 ExportHandler::frames_to_chapter_marks_string (char* buf, framepos_t when)
818 {
819         framecnt_t remainder;
820         framecnt_t fr = session.nominal_frame_rate();
821         int hours, mins, secs, msecs;
822
823         hours = when / (3600 * fr);
824         remainder = when - (hours * 3600 * fr);
825         mins = remainder / (60 * fr);
826         remainder -= mins * 60 * fr;
827         secs = remainder / fr;
828         remainder -= secs * fr;
829         msecs = (remainder * 1000) / fr;
830         sprintf (buf, "%02d:%02d:%02d.%03d", hours, mins, secs, msecs);
831 }
832
833 std::string
834 ExportHandler::toc_escape_cdtext (const std::string& txt)
835 {
836         Glib::ustring check (txt);
837         std::string out;
838         std::string latin1_txt;
839         char buf[5];
840
841         try {
842                 latin1_txt = Glib::convert_with_fallback (txt, "ISO-8859-1", "UTF-8", "_");
843         } catch (Glib::ConvertError& err) {
844                 throw Glib::ConvertError (err.code(), string_compose (_("Cannot convert %1 to Latin-1 text"), txt));
845         }
846
847         out = '"';
848
849         for (std::string::const_iterator c = latin1_txt.begin(); c != latin1_txt.end(); ++c) {
850
851                 if ((*c) == '"') {
852                         out += "\\\"";
853                 } else if ((*c) == '\\') {
854                         out += "\\134";
855                 } else if (isprint (*c)) {
856                         out += *c;
857                 } else {
858                         snprintf (buf, sizeof (buf), "\\%03o", (int) (unsigned char) *c);
859                         out += buf;
860                 }
861         }
862
863         out += '"';
864
865         return out;
866 }
867
868 std::string
869 ExportHandler::toc_escape_filename (const std::string& txt)
870 {
871         std::string out;
872
873         out = '"';
874
875         // We iterate byte-wise not character-wise over a UTF-8 string here,
876         // because we only want to translate backslashes and double quotes
877         for (std::string::const_iterator c = txt.begin(); c != txt.end(); ++c) {
878
879                 if (*c == '"') {
880                         out += "\\\"";
881                 } else if (*c == '\\') {
882                         out += "\\134";
883                 } else {
884                         out += *c;
885                 }
886         }
887
888         out += '"';
889
890         return out;
891 }
892
893 std::string
894 ExportHandler::cue_escape_cdtext (const std::string& txt)
895 {
896         std::string latin1_txt;
897         std::string out;
898
899         try {
900                 latin1_txt = Glib::convert (txt, "ISO-8859-1", "UTF-8");
901         } catch (Glib::ConvertError& err) {
902                 throw Glib::ConvertError (err.code(), string_compose (_("Cannot convert %1 to Latin-1 text"), txt));
903         }
904
905         // does not do much mor than UTF-8 to Latin1 translation yet, but
906         // that may have to change if cue parsers in burning programs change
907         out = '"' + latin1_txt + '"';
908
909         return out;
910 }
911
912 } // namespace ARDOUR