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