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