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