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