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