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