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