add 0.5 second sleep after closing JACK connection so that next startup/connect is...
[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 <glibmm.h>
24 #include <glibmm/convert.h>
25
26 #include "pbd/convert.h"
27
28 #include "ardour/export_graph_builder.h"
29 #include "ardour/export_timespan.h"
30 #include "ardour/export_channel_configuration.h"
31 #include "ardour/export_status.h"
32 #include "ardour/export_format_specification.h"
33 #include "ardour/export_filename.h"
34
35 #include "i18n.h"
36
37 using namespace std;
38 using namespace PBD;
39
40 namespace ARDOUR
41 {
42
43 /*** ExportElementFactory ***/
44
45 ExportElementFactory::ExportElementFactory (Session & session) :
46   session (session)
47 {
48
49 }
50
51 ExportElementFactory::~ExportElementFactory ()
52 {
53
54 }
55
56 ExportTimespanPtr
57 ExportElementFactory::add_timespan ()
58 {
59         return ExportTimespanPtr (new ExportTimespan (session.get_export_status(), session.frame_rate()));
60 }
61
62 ExportChannelConfigPtr
63 ExportElementFactory::add_channel_config ()
64 {
65         return ExportChannelConfigPtr (new ExportChannelConfiguration (session));
66 }
67
68 ExportFormatSpecPtr
69 ExportElementFactory::add_format ()
70 {
71         return ExportFormatSpecPtr (new ExportFormatSpecification (session));
72 }
73
74 ExportFormatSpecPtr
75 ExportElementFactory::add_format (XMLNode const & state)
76 {
77         return ExportFormatSpecPtr (new ExportFormatSpecification (session, state));
78 }
79
80 ExportFormatSpecPtr
81 ExportElementFactory::add_format_copy (ExportFormatSpecPtr other)
82 {
83         return ExportFormatSpecPtr (new ExportFormatSpecification (*other));
84 }
85
86 ExportFilenamePtr
87 ExportElementFactory::add_filename ()
88 {
89         return ExportFilenamePtr (new ExportFilename (session));
90 }
91
92 ExportFilenamePtr
93 ExportElementFactory::add_filename_copy (ExportFilenamePtr other)
94 {
95         return ExportFilenamePtr (new ExportFilename (*other));
96 }
97
98 /*** ExportHandler ***/
99
100 ExportHandler::ExportHandler (Session & session)
101   : ExportElementFactory (session)
102   , session (session)
103   , graph_builder (new ExportGraphBuilder (session))
104   , export_status (session.get_export_status ())
105   , normalizing (false)
106   , cue_tracknum (0)
107   , cue_indexnum (0)
108 {
109 }
110
111 ExportHandler::~ExportHandler ()
112 {
113         // TODO remove files that were written but not finished
114 }
115
116 /** Add an export to the `to-do' list */
117 bool
118 ExportHandler::add_export_config (ExportTimespanPtr timespan, ExportChannelConfigPtr channel_config,
119                                   ExportFormatSpecPtr format, ExportFilenamePtr filename,
120                                   BroadcastInfoPtr broadcast_info)
121 {
122         FileSpec spec (channel_config, format, filename, broadcast_info);
123         config_map.insert (make_pair (timespan, spec));
124
125         return true;
126 }
127
128 void
129 ExportHandler::do_export ()
130 {
131         /* Count timespans */
132
133         export_status->init();
134         std::set<ExportTimespanPtr> timespan_set;
135         for (ConfigMap::iterator it = config_map.begin(); it != config_map.end(); ++it) {
136                 bool new_timespan = timespan_set.insert (it->first).second;
137                 if (new_timespan) {
138                         export_status->total_frames += it->first->get_length();
139                 }
140         }
141         export_status->total_timespans = timespan_set.size();
142
143         /* Start export */
144
145         start_timespan ();
146 }
147
148 void
149 ExportHandler::start_timespan ()
150 {
151         export_status->timespan++;
152
153         if (config_map.empty()) {
154                 // freewheeling has to be stopped from outside the process cycle
155                 export_status->running = false;
156                 return;
157         }
158
159         /* finish_timespan pops the config_map entry that has been done, so
160            this is the timespan to do this time
161         */
162         current_timespan = config_map.begin()->first;
163         
164         export_status->total_frames_current_timespan = current_timespan->get_length();
165         export_status->timespan_name = current_timespan->name();
166         export_status->processed_frames_current_timespan = 0;
167
168         /* Register file configurations to graph builder */
169
170         /* Here's the config_map entries that use this timespan */
171         timespan_bounds = config_map.equal_range (current_timespan);
172         graph_builder->reset ();
173         graph_builder->set_current_timespan (current_timespan);
174         handle_duplicate_format_extensions();
175         for (ConfigMap::iterator it = timespan_bounds.first; it != timespan_bounds.second; ++it) {
176                 // Filenames can be shared across timespans
177                 FileSpec & spec = it->second;
178                 spec.filename->set_timespan (it->first);
179                 graph_builder->add_config (spec);
180         }
181
182         /* start export */
183
184         normalizing = false;
185         session.ProcessExport.connect_same_thread (process_connection, boost::bind (&ExportHandler::process, this, _1));
186         process_position = current_timespan->get_start();
187         session.start_audio_export (process_position);
188 }
189
190 void
191 ExportHandler::handle_duplicate_format_extensions()
192 {
193         typedef std::map<std::string, int> ExtCountMap;
194
195         ExtCountMap counts;
196         for (ConfigMap::iterator it = timespan_bounds.first; it != timespan_bounds.second; ++it) {
197                 counts[it->second.format->extension()]++;
198         }
199
200         bool duplicates_found = false;
201         for (ExtCountMap::iterator it = counts.begin(); it != counts.end(); ++it) {
202                 if (it->second > 1) { duplicates_found = true; }
203         }
204
205         // Set this always, as the filenames are shared...
206         for (ConfigMap::iterator it = timespan_bounds.first; it != timespan_bounds.second; ++it) {
207                 it->second.filename->include_format_name = duplicates_found;
208         }
209 }
210
211 int
212 ExportHandler::process (framecnt_t frames)
213 {
214         if (!export_status->running) {
215                 return 0;
216         } else if (normalizing) {
217                 return process_normalize ();
218         } else {
219                 return process_timespan (frames);
220         }
221 }
222
223 int
224 ExportHandler::process_timespan (framecnt_t frames)
225 {
226         /* update position */
227
228         framecnt_t frames_to_read = 0;
229         framepos_t const end = current_timespan->get_end();
230
231         bool const last_cycle = (process_position + frames >= end);
232
233         if (last_cycle) {
234                 frames_to_read = end - process_position;
235                 export_status->stop = true;
236         } else {
237                 frames_to_read = frames;
238         }
239
240         process_position += frames_to_read;
241         export_status->processed_frames += frames_to_read;
242         export_status->processed_frames_current_timespan += frames_to_read;
243
244         /* Do actual processing */
245         int ret = graph_builder->process (frames_to_read, last_cycle);
246
247         /* Start normalizing if necessary */
248         if (last_cycle) {
249                 normalizing = graph_builder->will_normalize();
250                 if (normalizing) {
251                         export_status->total_normalize_cycles = graph_builder->get_normalize_cycle_count();
252                         export_status->current_normalize_cycle = 0;
253                 } else {
254                         finish_timespan ();
255                         return 0;
256                 }
257         }
258
259         return ret;
260 }
261
262 int
263 ExportHandler::process_normalize ()
264 {
265         if (graph_builder->process_normalize ()) {
266                 finish_timespan ();
267                 export_status->normalizing = false;
268         } else {
269                 export_status->normalizing = true;
270         }
271
272         export_status->current_normalize_cycle++;
273
274         return 0;
275 }
276
277 void
278 ExportHandler::finish_timespan ()
279 {
280         while (config_map.begin() != timespan_bounds.second) {
281
282                 ExportFormatSpecPtr fmt = config_map.begin()->second.format;
283
284                 if (fmt->with_cue()) {
285                         export_cd_marker_file (current_timespan, fmt, config_map.begin()->second.filename->get_path(fmt), CDMarkerCUE);
286                 } 
287
288                 if (fmt->with_toc()) {
289                         export_cd_marker_file (current_timespan, fmt, config_map.begin()->second.filename->get_path(fmt), CDMarkerTOC);
290                 }
291
292                 config_map.erase (config_map.begin());
293         }
294
295         start_timespan ();
296 }
297
298 /*** CD Marker sutff ***/
299
300 struct LocationSortByStart {
301     bool operator() (Location *a, Location *b) {
302             return a->start() < b->start();
303     }
304 };
305
306 void
307 ExportHandler::export_cd_marker_file (ExportTimespanPtr timespan, ExportFormatSpecPtr file_format,
308                                       std::string filename, CDMarkerFormat format)
309 {
310         string filepath = get_cd_marker_filename(filename, format);
311
312         try {
313                 void (ExportHandler::*header_func) (CDMarkerStatus &);
314                 void (ExportHandler::*track_func) (CDMarkerStatus &);
315                 void (ExportHandler::*index_func) (CDMarkerStatus &);
316
317                 switch (format) {
318                 case CDMarkerTOC:
319                         header_func = &ExportHandler::write_toc_header;
320                         track_func = &ExportHandler::write_track_info_toc;
321                         index_func = &ExportHandler::write_index_info_toc;
322                         break;
323                 case CDMarkerCUE:
324                         header_func = &ExportHandler::write_cue_header;
325                         track_func = &ExportHandler::write_track_info_cue;
326                         index_func = &ExportHandler::write_index_info_cue;
327                         break;
328                 default:
329                         return;
330                 }
331
332                 CDMarkerStatus status (filepath, timespan, file_format, filename);
333
334                 if (!status.out) {
335                         error << string_compose(_("Editor: cannot open \"%1\" as export file for CD marker file"), filepath) << endmsg;
336                         return;
337                 }
338
339                 (this->*header_func) (status);
340
341                 /* Get locations and sort */
342
343                 Locations::LocationList const & locations (session.locations()->list());
344                 Locations::LocationList::const_iterator i;
345                 Locations::LocationList temp;
346
347                 for (i = locations.begin(); i != locations.end(); ++i) {
348                         if ((*i)->start() >= timespan->get_start() && (*i)->end() <= timespan->get_end() && (*i)->is_cd_marker() && !(*i)->is_session_range()) {
349                                 temp.push_back (*i);
350                         }
351                 }
352
353                 if (temp.empty()) {
354                         // TODO One index marker for whole thing
355                         return;
356                 }
357
358                 LocationSortByStart cmp;
359                 temp.sort (cmp);
360                 Locations::LocationList::const_iterator nexti;
361
362                 /* Start actual marker stuff */
363
364                 framepos_t last_end_time = timespan->get_start(), last_start_time = timespan->get_start();
365                 status.track_position = last_start_time - timespan->get_start();
366
367                 for (i = temp.begin(); i != temp.end(); ++i) {
368
369                         status.marker = *i;
370
371                         if ((*i)->start() < last_end_time) {
372                                 if ((*i)->is_mark()) {
373                                         /* Index within track */
374
375                                         status.index_position = (*i)->start() - timespan->get_start();
376                                         (this->*index_func) (status);
377                                 }
378
379                                 continue;
380                         }
381
382                         /* A track, defined by a cd range marker or a cd location marker outside of a cd range */
383
384                         status.track_position = last_end_time - timespan->get_start();
385                         status.track_start_frame = (*i)->start() - timespan->get_start();  // everything before this is the pregap
386                         status.track_duration = 0;
387
388                         if ((*i)->is_mark()) {
389                                 // a mark track location needs to look ahead to the next marker's start to determine length
390                                 nexti = i;
391                                 ++nexti;
392
393                                 if (nexti != temp.end()) {
394                                         status.track_duration = (*nexti)->start() - last_end_time;
395
396                                         last_start_time = (*i)->start();
397                                         last_end_time = (*nexti)->start();
398                                 } else {
399                                         // this was the last marker, use timespan end
400                                         status.track_duration = timespan->get_end() - last_end_time;
401
402                                         last_start_time = (*i)->start();
403                                         last_end_time = timespan->get_end();
404                                 }
405                         } else {
406                                 // range
407                                 status.track_duration = (*i)->end() - last_end_time;
408
409                                 last_start_time = (*i)->start();
410                                 last_end_time = (*i)->end();
411                         }
412
413                         (this->*track_func) (status);
414                 }
415
416         } catch (std::exception& e) {
417                 error << string_compose (_("an error occured while writing a TOC/CUE file: %1"), e.what()) << endmsg;
418                 ::unlink (filepath.c_str());
419         } catch (Glib::Exception& e) {
420                 error << string_compose (_("an error occured while writing a TOC/CUE file: %1"), e.what()) << endmsg;
421                 ::unlink (filepath.c_str());
422         }
423 }
424
425 string
426 ExportHandler::get_cd_marker_filename(std::string filename, CDMarkerFormat format)
427 {
428         /* do not strip file suffix because there may be more than one format, 
429            and we do not want the CD marker file from one format to overwrite
430            another (e.g. foo.wav.cue > foo.aiff.cue)
431         */
432
433         switch (format) {
434           case CDMarkerTOC:
435                 return filename + ".toc";
436           case CDMarkerCUE:
437                 return filename + ".cue";
438           default:
439                 return filename + ".marker"; // Should not be reached when actually creating a file
440         }
441 }
442
443 void
444 ExportHandler::write_cue_header (CDMarkerStatus & status)
445 {
446         string title = status.timespan->name().compare ("Session") ? status.timespan->name() : (string) session.name();
447
448         status.out << "REM Cue file generated by " << PROGRAM_NAME << endl;
449         status.out << "TITLE " << cue_escape_cdtext (title) << endl;
450
451         /*  The original cue sheet sepc metions five file types
452                 WAVE, AIFF,
453                 BINARY   = "header-less" audio (44.1 kHz, 16 Bit, little endian),
454                 MOTOROLA = "header-less" audio (44.1 kHz, 16 Bit, big endian),
455                 and MP3
456                 
457                 We try to use these file types whenever appropriate and 
458                 default to our own names otherwise.
459         */
460         status.out << "FILE \"" << Glib::path_get_basename(status.filename) << "\" ";
461         if (!status.format->format_name().compare ("WAV")  || !status.format->format_name().compare ("BWF")) {
462                 status.out  << "WAVE";
463         } else if (status.format->format_id() == ExportFormatBase::F_RAW &&
464                    status.format->sample_format() == ExportFormatBase::SF_16 &&
465                    status.format->sample_rate() == ExportFormatBase::SR_44_1) {
466                 // Format is RAW 16bit 44.1kHz
467                 if (status.format->endianness() == ExportFormatBase::E_Little) {
468                         status.out << "BINARY";
469                 } else {
470                         status.out << "MOTOROLA";
471                 }
472         } else {
473                 // no special case for AIFF format it's name is already "AIFF"
474                 status.out << status.format->format_name();
475         }
476         status.out << endl;
477 }
478
479 void
480 ExportHandler::write_toc_header (CDMarkerStatus & status)
481 {
482         string title = status.timespan->name().compare ("Session") ? status.timespan->name() : (string) session.name();
483
484         status.out << "CD_DA" << endl;
485         status.out << "CD_TEXT {" << endl << "  LANGUAGE_MAP {" << endl << "    0 : EN" << endl << "  }" << endl;
486         status.out << "  LANGUAGE 0 {" << endl << "    TITLE " << toc_escape_cdtext (title) << endl ;
487         status.out << "    PERFORMER \"\"" << endl << "  }" << endl << "}" << endl;
488 }
489
490 void
491 ExportHandler::write_track_info_cue (CDMarkerStatus & status)
492 {
493         gchar buf[18];
494
495         snprintf (buf, sizeof(buf), "  TRACK %02d AUDIO", status.track_number);
496         status.out << buf << endl;
497
498         status.out << "    FLAGS" ;
499         if (status.marker->cd_info.find("scms") != status.marker->cd_info.end())  {
500                 status.out << " SCMS ";
501         } else {
502                 status.out << " DCP ";
503         }
504
505         if (status.marker->cd_info.find("preemph") != status.marker->cd_info.end())  {
506                 status.out << " PRE";
507         }
508         status.out << endl;
509
510         if (status.marker->cd_info.find("isrc") != status.marker->cd_info.end())  {
511                 status.out << "    ISRC " << status.marker->cd_info["isrc"] << endl;
512         }
513
514         if (status.marker->name() != "") {
515                 status.out << "    TITLE " << cue_escape_cdtext (status.marker->name()) << endl;
516         }
517
518         if (status.marker->cd_info.find("performer") != status.marker->cd_info.end()) {
519                 status.out <<  "    PERFORMER " << cue_escape_cdtext (status.marker->cd_info["performer"]) << endl;
520         }
521
522         if (status.marker->cd_info.find("composer") != status.marker->cd_info.end()) {
523                 status.out << "    SONGWRITER " << cue_escape_cdtext (status.marker->cd_info["composer"]) << endl;
524         }
525
526         if (status.track_position != status.track_start_frame) {
527                 frames_to_cd_frames_string (buf, status.track_position);
528                 status.out << "    INDEX 00" << buf << endl;
529         }
530
531         frames_to_cd_frames_string (buf, status.track_start_frame);
532         status.out << "    INDEX 01" << buf << endl;
533
534         status.index_number = 2;
535         status.track_number++;
536 }
537
538 void
539 ExportHandler::write_track_info_toc (CDMarkerStatus & status)
540 {
541         gchar buf[18];
542
543         status.out << endl << "TRACK AUDIO" << endl;
544
545         if (status.marker->cd_info.find("scms") != status.marker->cd_info.end())  {
546                 status.out << "NO ";
547         }
548         status.out << "COPY" << endl;
549
550         if (status.marker->cd_info.find("preemph") != status.marker->cd_info.end())  {
551                 status.out << "PRE_EMPHASIS" << endl;
552         } else {
553                 status.out << "NO PRE_EMPHASIS" << endl;
554         }
555
556         if (status.marker->cd_info.find("isrc") != status.marker->cd_info.end())  {
557                 status.out << "ISRC \"" << status.marker->cd_info["isrc"] << "\"" << endl;
558         }
559
560         status.out << "CD_TEXT {" << endl << "  LANGUAGE 0 {" << endl;
561         status.out << "     TITLE " << toc_escape_cdtext (status.marker->name()) << endl;
562         
563         status.out << "     PERFORMER ";
564         if (status.marker->cd_info.find("performer") != status.marker->cd_info.end()) {
565                 status.out << toc_escape_cdtext (status.marker->cd_info["performer"]) << endl;
566         } else {
567                 status.out << "\"\"" << endl;
568         }
569         
570         if (status.marker->cd_info.find("composer") != status.marker->cd_info.end()) {
571                 status.out  << "     SONGWRITER " << toc_escape_cdtext (status.marker->cd_info["composer"]) << endl;
572         }
573
574         if (status.marker->cd_info.find("isrc") != status.marker->cd_info.end()) {
575                 status.out  << "     ISRC \"";
576                 status.out << status.marker->cd_info["isrc"].substr(0,2) << "-";
577                 status.out << status.marker->cd_info["isrc"].substr(2,3) << "-";
578                 status.out << status.marker->cd_info["isrc"].substr(5,2) << "-";
579                 status.out << status.marker->cd_info["isrc"].substr(7,5) << "\"" << endl;
580         }
581
582         status.out << "  }" << endl << "}" << endl;
583
584         frames_to_cd_frames_string (buf, status.track_position);
585         status.out << "FILE " << toc_escape_filename (status.filename) << ' ' << buf;
586
587         frames_to_cd_frames_string (buf, status.track_duration);
588         status.out << buf << endl;
589
590         frames_to_cd_frames_string (buf, status.track_start_frame - status.track_position);
591         status.out << "START" << buf << endl;
592 }
593
594 void
595 ExportHandler::write_index_info_cue (CDMarkerStatus & status)
596 {
597         gchar buf[18];
598
599         snprintf (buf, sizeof(buf), "    INDEX %02d", cue_indexnum);
600         status.out << buf;
601         frames_to_cd_frames_string (buf, status.index_position);
602         status.out << buf << endl;
603
604         cue_indexnum++;
605 }
606
607 void
608 ExportHandler::write_index_info_toc (CDMarkerStatus & status)
609 {
610         gchar buf[18];
611
612         frames_to_cd_frames_string (buf, status.index_position - status.track_position);
613         status.out << "INDEX" << buf << endl;
614 }
615
616 void
617 ExportHandler::frames_to_cd_frames_string (char* buf, framepos_t when)
618 {
619         framecnt_t remainder;
620         framecnt_t fr = session.nominal_frame_rate();
621         int mins, secs, frames;
622
623         mins = when / (60 * fr);
624         remainder = when - (mins * 60 * fr);
625         secs = remainder / fr;
626         remainder -= secs * fr;
627         frames = remainder / (fr / 75);
628         sprintf (buf, " %02d:%02d:%02d", mins, secs, frames);
629 }
630
631 std::string
632 ExportHandler::toc_escape_cdtext (const std::string& txt)
633 {
634         Glib::ustring check (txt);
635         std::string out;
636         std::string latin1_txt;
637         char buf[5];
638
639         try {
640                 latin1_txt = Glib::convert (txt, "ISO-8859-1", "UTF-8");
641         } catch (Glib::ConvertError& err) {
642                 throw Glib::ConvertError (err.code(), string_compose (_("Cannot convert %1 to Latin-1 text"), txt));
643         }
644
645         out = '"';
646
647         for (std::string::const_iterator c = latin1_txt.begin(); c != latin1_txt.end(); ++c) {
648
649                 if ((*c) == '"') {
650                         out += "\\\"";
651                 } else if ((*c) == '\\') {
652                         out += "\\134";
653                 } else if (isprint (*c)) {
654                         out += *c;
655                 } else {
656                         snprintf (buf, sizeof (buf), "\\%03o", (int) (unsigned char) *c);
657                         out += buf;
658                 }
659         }
660         
661         out += '"';
662
663         return out;
664 }
665
666 std::string
667 ExportHandler::toc_escape_filename (const std::string& txt)
668 {
669         std::string out;
670
671         out = '"';
672
673         // We iterate byte-wise not character-wise over a UTF-8 string here,
674         // because we only want to translate backslashes and double quotes
675         for (std::string::const_iterator c = txt.begin(); c != txt.end(); ++c) {
676
677                 if (*c == '"') {
678                         out += "\\\"";
679                 } else if (*c == '\\') {
680                         out += "\\134";
681                 } else {
682                         out += *c;
683                 }
684         }
685         
686         out += '"';
687
688         return out;
689 }
690
691 std::string
692 ExportHandler::cue_escape_cdtext (const std::string& txt)
693 {
694         std::string latin1_txt;
695         std::string out;
696         
697         try {
698                 latin1_txt = Glib::convert (txt, "ISO-8859-1", "UTF-8");
699         } catch (Glib::ConvertError& err) {
700                 throw Glib::ConvertError (err.code(), string_compose (_("Cannot convert %1 to Latin-1 text"), txt));
701         }
702         
703         // does not do much mor than UTF-8 to Latin1 translation yet, but
704         // that may have to change if cue parsers in burning programs change 
705         out = '"' + latin1_txt + '"';
706
707         return out;
708 }
709
710 } // namespace ARDOUR