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