Fix #4094: show total export progress instead of per timespan progress
[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
25 #include "pbd/convert.h"
26 #include "pbd/filesystem.h"
27
28 #include "ardour/ardour.h"
29 #include "ardour/configuration.h"
30 #include "ardour/export_graph_builder.h"
31 #include "ardour/export_timespan.h"
32 #include "ardour/export_channel_configuration.h"
33 #include "ardour/export_status.h"
34 #include "ardour/export_format_specification.h"
35 #include "ardour/export_filename.h"
36 #include "ardour/export_failed.h"
37
38 #include "i18n.h"
39
40 using namespace std;
41 using namespace PBD;
42
43 namespace ARDOUR
44 {
45
46 /*** ExportElementFactory ***/
47
48 ExportElementFactory::ExportElementFactory (Session & session) :
49   session (session)
50 {
51
52 }
53
54 ExportElementFactory::~ExportElementFactory ()
55 {
56
57 }
58
59 ExportTimespanPtr
60 ExportElementFactory::add_timespan ()
61 {
62         return ExportTimespanPtr (new ExportTimespan (session.get_export_status(), session.frame_rate()));
63 }
64
65 ExportChannelConfigPtr
66 ExportElementFactory::add_channel_config ()
67 {
68         return ExportChannelConfigPtr (new ExportChannelConfiguration (session));
69 }
70
71 ExportFormatSpecPtr
72 ExportElementFactory::add_format ()
73 {
74         return ExportFormatSpecPtr (new ExportFormatSpecification (session));
75 }
76
77 ExportFormatSpecPtr
78 ExportElementFactory::add_format (XMLNode const & state)
79 {
80         return ExportFormatSpecPtr (new ExportFormatSpecification (session, state));
81 }
82
83 ExportFormatSpecPtr
84 ExportElementFactory::add_format_copy (ExportFormatSpecPtr other)
85 {
86         return ExportFormatSpecPtr (new ExportFormatSpecification (*other));
87 }
88
89 ExportFilenamePtr
90 ExportElementFactory::add_filename ()
91 {
92         return ExportFilenamePtr (new ExportFilename (session));
93 }
94
95 ExportFilenamePtr
96 ExportElementFactory::add_filename_copy (ExportFilenamePtr other)
97 {
98         return ExportFilenamePtr (new ExportFilename (*other));
99 }
100
101 /*** ExportHandler ***/
102
103 ExportHandler::ExportHandler (Session & session)
104   : ExportElementFactory (session)
105   , session (session)
106   , graph_builder (new ExportGraphBuilder (session))
107   , export_status (session.get_export_status ())
108   , realtime (false)
109   , normalizing (false)
110   , cue_tracknum (0)
111   , cue_indexnum (0)
112 {
113 }
114
115 ExportHandler::~ExportHandler ()
116 {
117         // TODO remove files that were written but not finsihed
118 }
119
120 bool
121 ExportHandler::add_export_config (ExportTimespanPtr timespan, ExportChannelConfigPtr channel_config,
122                                   ExportFormatSpecPtr format, ExportFilenamePtr filename,
123                                   BroadcastInfoPtr broadcast_info)
124 {
125         FileSpec spec (channel_config, format, filename, broadcast_info);
126         ConfigPair pair (timespan, spec);
127         config_map.insert (pair);
128
129         return true;
130 }
131
132 void
133 ExportHandler::do_export (bool rt)
134 {
135         /* Count timespans */
136
137         export_status->init();
138         std::set<ExportTimespanPtr> timespan_set;
139         for (ConfigMap::iterator it = config_map.begin(); it != config_map.end(); ++it) {
140                 timespan_set.insert (it->first);
141                 export_status->total_frames += it->first->get_length();
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
164         /* Register file configurations to graph builder */
165
166         timespan_bounds = config_map.equal_range (current_timespan);
167         graph_builder->reset ();
168         graph_builder->set_current_timespan (current_timespan);
169         for (ConfigMap::iterator it = timespan_bounds.first; it != timespan_bounds.second; ++it) {
170                 // Filenames can be shared across timespans
171                 FileSpec & spec = it->second;
172                 spec.filename->set_timespan (it->first);
173                 graph_builder->add_config (spec);
174         }
175
176         /* start export */
177
178         normalizing = false;
179         session.ProcessExport.connect_same_thread (process_connection, boost::bind (&ExportHandler::process, this, _1));
180         process_position = current_timespan->get_start();
181         session.start_audio_export (process_position, realtime);
182 }
183
184 int
185 ExportHandler::process (framecnt_t frames)
186 {
187         if (!export_status->running) {
188                 return 0;
189         } else if (normalizing) {
190                 return process_normalize ();
191         } else {
192                 return process_timespan (frames);
193         }
194 }
195
196 int
197 ExportHandler::process_timespan (framecnt_t frames)
198 {
199         /* update position */
200
201         framecnt_t frames_to_read = 0;
202         framepos_t const end = current_timespan->get_end();
203
204         bool const last_cycle = (process_position + frames >= end);
205
206         if (last_cycle) {
207                 frames_to_read = end - process_position;
208                 export_status->stop = true;
209                 normalizing = true;
210         } else {
211                 frames_to_read = frames;
212         }
213
214         process_position += frames_to_read;
215         export_status->processed_frames += frames_to_read;
216         export_status->progress = (float) export_status->processed_frames / export_status->total_frames;
217
218         /* Do actual processing */
219
220         return graph_builder->process (frames_to_read, last_cycle);
221 }
222
223 int
224 ExportHandler::process_normalize ()
225 {
226         if (graph_builder->process_normalize ()) {
227                 finish_timespan ();
228                 export_status->normalizing = false;
229         } else {
230                 export_status->normalizing = true;
231         }
232
233         return 0;
234 }
235
236 void
237 ExportHandler::finish_timespan ()
238 {
239         while (config_map.begin() != timespan_bounds.second) {
240                 config_map.erase (config_map.begin());
241         }
242
243         start_timespan ();
244 }
245
246 /*** CD Marker sutff ***/
247
248 struct LocationSortByStart {
249     bool operator() (Location *a, Location *b) {
250             return a->start() < b->start();
251     }
252 };
253
254 void
255 ExportHandler::export_cd_marker_file (ExportTimespanPtr timespan, ExportFormatSpecPtr file_format,
256                                       std::string filename, CDMarkerFormat format)
257 {
258         string filepath;
259         string basename = Glib::path_get_basename(filename);
260
261         size_t ext_pos = basename.rfind('.');
262         if (ext_pos != string::npos) {
263                 basename = basename.substr(0, ext_pos); /* strip file extension, if there is one */
264         }
265
266         void (ExportHandler::*header_func) (CDMarkerStatus &);
267         void (ExportHandler::*track_func) (CDMarkerStatus &);
268         void (ExportHandler::*index_func) (CDMarkerStatus &);
269
270         switch (format) {
271           case CDMarkerTOC:
272                 filepath = Glib::build_filename(Glib::path_get_dirname(filename), basename + ".toc");
273                 header_func = &ExportHandler::write_toc_header;
274                 track_func = &ExportHandler::write_track_info_toc;
275                 index_func = &ExportHandler::write_index_info_toc;
276                 break;
277           case CDMarkerCUE:
278                 filepath = Glib::build_filename(Glib::path_get_dirname(filename), basename + ".cue");
279                 header_func = &ExportHandler::write_cue_header;
280                 track_func = &ExportHandler::write_track_info_cue;
281                 index_func = &ExportHandler::write_index_info_cue;
282                 break;
283           default:
284                 return;
285         }
286
287         CDMarkerStatus status (filepath, timespan, file_format, filename);
288
289         if (!status.out) {
290                 error << string_compose(_("Editor: cannot open \"%1\" as export file for CD marker file"), filepath) << endmsg;
291                 return;
292         }
293
294         (this->*header_func) (status);
295
296         /* Get locations and sort */
297
298         Locations::LocationList const & locations (session.locations()->list());
299         Locations::LocationList::const_iterator i;
300         Locations::LocationList temp;
301
302         for (i = locations.begin(); i != locations.end(); ++i) {
303                 if ((*i)->start() >= timespan->get_start() && (*i)->end() <= timespan->get_end() && (*i)->is_cd_marker() && !(*i)->is_session_range()) {
304                         temp.push_back (*i);
305                 }
306         }
307
308         if (temp.empty()) {
309                 // TODO One index marker for whole thing
310                 return;
311         }
312
313         LocationSortByStart cmp;
314         temp.sort (cmp);
315         Locations::LocationList::const_iterator nexti;
316
317         /* Start actual marker stuff */
318
319         framepos_t last_end_time = timespan->get_start(), last_start_time = timespan->get_start();
320         status.track_position = last_start_time - timespan->get_start();
321
322         for (i = temp.begin(); i != temp.end(); ++i) {
323
324                 status.marker = *i;
325
326                 if ((*i)->start() < last_end_time) {
327                         if ((*i)->is_mark()) {
328                                 /* Index within track */
329
330                                 status.index_position = (*i)->start() - timespan->get_start();
331                                 (this->*index_func) (status);
332                         }
333
334                         continue;
335                 }
336
337                 /* A track, defined by a cd range marker or a cd location marker outside of a cd range */
338
339                 status.track_position = last_end_time - timespan->get_start();
340                 status.track_start_frame = (*i)->start() - timespan->get_start();  // everything before this is the pregap
341                 status.track_duration = 0;
342
343                 if ((*i)->is_mark()) {
344                         // a mark track location needs to look ahead to the next marker's start to determine length
345                         nexti = i;
346                         ++nexti;
347
348                         if (nexti != temp.end()) {
349                                 status.track_duration = (*nexti)->start() - last_end_time;
350
351                                 last_start_time = (*i)->start();
352                                 last_end_time = (*nexti)->start();
353                         } else {
354                                 // this was the last marker, use timespan end
355                                 status.track_duration = timespan->get_end() - last_end_time;
356
357                                 last_start_time = (*i)->start();
358                                 last_end_time = timespan->get_end();
359                         }
360                 } else {
361                         // range
362                         status.track_duration = (*i)->end() - last_end_time;
363
364                         last_start_time = (*i)->start();
365                         last_end_time = (*i)->end();
366                 }
367
368                 (this->*track_func) (status);
369         }
370 }
371
372 void
373 ExportHandler::write_cue_header (CDMarkerStatus & status)
374 {
375         string title = status.timespan->name().compare ("Session") ? status.timespan->name() : (string) session.name();
376
377         status.out << "REM Cue file generated by Ardour" << endl;
378         status.out << "TITLE \"" << title << "\"" << endl;
379
380         /*  The cue sheet syntax has originally five file types:
381                 WAVE     : 44.1 kHz, 16 Bit (little endian)
382                 AIFF     : 44.1 kHz, 16 Bit (big endian)
383                 BINARY   : 44.1 kHz, 16 Bit (little endian)
384                 MOTOROLA : 44.1 kHz, 16 Bit (big endian)
385                 MP3
386
387                 We want to use cue sheets not only as CD images but also as general playlyist
388                 format, thus for WAVE and AIFF we don't care if it's really 44.1 kHz/16 Bit, the
389                 soundfile's header shows it anyway.  But for the raw formats, i.e. BINARY
390                 and MOTOROLA we do care, because no header would tell us about a different format.
391
392                 For all other formats we just make up our own file type.  MP3 is not supported
393                 at the moment.
394         */
395
396         status.out << "FILE \"" << Glib::path_get_basename(status.filename) << "\" ";
397         if (!status.format->format_name().compare ("WAV")) {
398                 status.out  << "WAVE";
399         } else if (status.format->format_id() == ExportFormatBase::F_RAW &&
400                    status.format->sample_format() == ExportFormatBase::SF_16 &&
401                    status.format->sample_rate() == ExportFormatBase::SR_44_1) {
402                 // Format is RAW 16bit 44.1kHz
403                 if (status.format->endianness() == ExportFormatBase::E_Little) {
404                         status.out << "BINARY";
405                 } else {
406                         status.out << "MOTOROLA";
407                 }
408         } else {
409                 // AIFF should return "AIFF"
410                 status.out << status.format->format_name();
411         }
412         status.out << endl;
413 }
414
415 void
416 ExportHandler::write_toc_header (CDMarkerStatus & status)
417 {
418         string title = status.timespan->name().compare ("Session") ? status.timespan->name() : (string) session.name();
419
420         status.out << "CD_DA" << endl;
421         status.out << "CD_TEXT {" << endl << "  LANGUAGE_MAP {" << endl << "    0 : EN" << endl << "  }" << endl;
422         status.out << "  LANGUAGE 0 {" << endl << "    TITLE \"" << title << "\"" << endl << "  }" << endl << "}" << endl;
423 }
424
425 void
426 ExportHandler::write_track_info_cue (CDMarkerStatus & status)
427 {
428         gchar buf[18];
429
430         snprintf (buf, sizeof(buf), "  TRACK %02d AUDIO", status.track_number);
431         status.out << buf << endl;
432
433         status.out << "    FLAGS" ;
434         if (status.marker->cd_info.find("scms") != status.marker->cd_info.end())  {
435                 status.out << " SCMS ";
436         } else {
437                 status.out << " DCP ";
438         }
439
440         if (status.marker->cd_info.find("preemph") != status.marker->cd_info.end())  {
441                 status.out << " PRE";
442         }
443         status.out << endl;
444
445         if (status.marker->cd_info.find("isrc") != status.marker->cd_info.end())  {
446                 status.out << "    ISRC " << status.marker->cd_info["isrc"] << endl;
447
448         }
449         if (status.marker->name() != "") {
450                 status.out << "    TITLE \"" << status.marker->name() << "\"" << endl;
451         }
452
453         if (status.marker->cd_info.find("performer") != status.marker->cd_info.end()) {
454                 status.out << "    PERFORMER \"" <<  status.marker->cd_info["performer"] << "\"" << endl;
455         }
456
457         if (status.marker->cd_info.find("string_composer") != status.marker->cd_info.end()) {
458                 status.out << "    SONGWRITER \"" << status.marker->cd_info["string_composer"]  << "\"" << endl;
459         }
460
461         if (status.track_position != status.track_start_frame) {
462                 frames_to_cd_frames_string (buf, status.track_position);
463                 status.out << "    INDEX 00" << buf << endl;
464         }
465
466         frames_to_cd_frames_string (buf, status.track_start_frame);
467         status.out << "    INDEX 01" << buf << endl;
468
469         status.index_number = 2;
470         status.track_number++;
471 }
472
473 void
474 ExportHandler::write_track_info_toc (CDMarkerStatus & status)
475 {
476         gchar buf[18];
477
478         status.out << endl << "TRACK AUDIO" << endl;
479
480         if (status.marker->cd_info.find("scms") != status.marker->cd_info.end())  {
481                 status.out << "NO ";
482         }
483         status.out << "COPY" << endl;
484
485         if (status.marker->cd_info.find("preemph") != status.marker->cd_info.end())  {
486                 status.out << "PRE_EMPHASIS" << endl;
487         } else {
488                 status.out << "NO PRE_EMPHASIS" << endl;
489         }
490
491         if (status.marker->cd_info.find("isrc") != status.marker->cd_info.end())  {
492                 status.out << "ISRC \"" << status.marker->cd_info["isrc"] << "\"" << endl;
493         }
494
495         status.out << "CD_TEXT {" << endl << "  LANGUAGE 0 {" << endl << "     TITLE \"" << status.marker->name() << "\"" << endl;
496         if (status.marker->cd_info.find("performer") != status.marker->cd_info.end()) {
497                 status.out << "     PERFORMER \"" << status.marker->cd_info["performer"]  << "\"" << endl;
498         }
499         if (status.marker->cd_info.find("string_composer") != status.marker->cd_info.end()) {
500                 status.out  << "     COMPOSER \"" << status.marker->cd_info["string_composer"] << "\"" << endl;
501         }
502
503         if (status.marker->cd_info.find("isrc") != status.marker->cd_info.end()) {
504                 status.out  << "     ISRC \"";
505                 status.out << status.marker->cd_info["isrc"].substr(0,2) << "-";
506                 status.out << status.marker->cd_info["isrc"].substr(2,3) << "-";
507                 status.out << status.marker->cd_info["isrc"].substr(5,2) << "-";
508                 status.out << status.marker->cd_info["isrc"].substr(7,5) << "\"" << endl;
509         }
510
511         status.out << "  }" << endl << "}" << endl;
512
513         frames_to_cd_frames_string (buf, status.track_position);
514         status.out << "FILE \"" << status.filename << "\" " << buf;
515
516         frames_to_cd_frames_string (buf, status.track_duration);
517         status.out << buf << endl;
518
519         frames_to_cd_frames_string (buf, status.track_start_frame - status.track_position);
520         status.out << "START" << buf << endl;
521 }
522
523 void
524 ExportHandler::write_index_info_cue (CDMarkerStatus & status)
525 {
526         gchar buf[18];
527
528         snprintf (buf, sizeof(buf), "    INDEX %02d", cue_indexnum);
529         status.out << buf;
530         frames_to_cd_frames_string (buf, status.index_position);
531         status.out << buf << endl;
532
533         cue_indexnum++;
534 }
535
536 void
537 ExportHandler::write_index_info_toc (CDMarkerStatus & status)
538 {
539         gchar buf[18];
540
541         frames_to_cd_frames_string (buf, status.index_position - status.track_position);
542         status.out << "INDEX" << buf << endl;
543 }
544
545 void
546 ExportHandler::frames_to_cd_frames_string (char* buf, framepos_t when)
547 {
548         framecnt_t remainder;
549         framecnt_t fr = session.nominal_frame_rate();
550         int mins, secs, frames;
551
552         mins = when / (60 * fr);
553         remainder = when - (mins * 60 * fr);
554         secs = remainder / fr;
555         remainder -= secs * fr;
556         frames = remainder / (fr / 75);
557         sprintf (buf, " %02d:%02d:%02d", mins, secs, frames);
558 }
559
560 } // namespace ARDOUR