2 Copyright (C) 2008 Paul Davis
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.
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.
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.
21 #include "ardour/export_handler.h"
25 #include "pbd/convert.h"
26 #include "pbd/filesystem.h"
28 #include "ardour/ardour.h"
29 #include "ardour/configuration.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 #include "ardour/export_processor.h"
36 #include "ardour/export_failed.h"
44 /*** ExportElementFactory ***/
46 ExportElementFactory::ExportElementFactory (Session & session) :
52 ExportElementFactory::~ExportElementFactory ()
57 ExportElementFactory::TimespanPtr
58 ExportElementFactory::add_timespan ()
60 return TimespanPtr (new ExportTimespan (session.get_export_status(), session.frame_rate()));
63 ExportElementFactory::ChannelConfigPtr
64 ExportElementFactory::add_channel_config ()
66 return ChannelConfigPtr (new ExportChannelConfiguration (session));
69 ExportElementFactory::FormatPtr
70 ExportElementFactory::add_format ()
72 return FormatPtr (new ExportFormatSpecification (session));
75 ExportElementFactory::FormatPtr
76 ExportElementFactory::add_format (XMLNode const & state)
78 return FormatPtr (new ExportFormatSpecification (session, state));
81 ExportElementFactory::FormatPtr
82 ExportElementFactory::add_format_copy (FormatPtr other)
84 return FormatPtr (new ExportFormatSpecification (*other));
87 ExportElementFactory::FilenamePtr
88 ExportElementFactory::add_filename ()
90 return FilenamePtr (new ExportFilename (session));
93 ExportElementFactory::FilenamePtr
94 ExportElementFactory::add_filename_copy (FilenamePtr other)
96 return FilenamePtr (new ExportFilename (*other));
99 /*** ExportHandler ***/
101 ExportHandler::ExportHandler (Session & session)
102 : ExportElementFactory (session)
104 , export_status (session.get_export_status ())
107 processor.reset (new ExportProcessor (session));
109 ExportProcessor::WritingFile.connect (files_written_connection, boost::bind (&ExportHandler::add_file, this, _1));
112 ExportHandler::~ExportHandler ()
114 if (export_status->aborted()) {
115 for (std::list<Glib::ustring>::iterator it = files_written.begin(); it != files_written.end(); ++it) {
116 sys::remove (sys::path (*it));
120 channel_config_connection.disconnect();
121 files_written_connection.disconnect();
125 ExportHandler::add_file (const Glib::ustring& str)
127 files_written.push_back (str);
131 ExportHandler::add_export_config (TimespanPtr timespan, ChannelConfigPtr channel_config, FormatPtr format, FilenamePtr filename)
133 FileSpec spec (channel_config, format, filename);
134 ConfigPair pair (timespan, spec);
135 config_map.insert (pair);
140 /// Starts exporting the registered configurations
141 /** The following happens, when do_export is called:
142 * 1. Session is prepared in do_export
143 * 2. start_timespan is called, which then registers all necessary channel configs to a timespan
144 * 3. The timespan reads each unique channel into a tempfile and calls Session::stop_export when the end is reached
145 * 4. stop_export emits ExportReadFinished after stopping the transport, this ends up calling finish_timespan
146 * 5. finish_timespan registers all the relevant formats and filenames to relevant channel configurations
147 * 6. finish_timespan does a manual call to timespan_thread_finished, which gets the next channel configuration
148 * for the current timespan, calling write_files for it
149 * 7. write_files writes the actual export files, composing them from the individual channels from tempfiles and
150 * emits FilesWritten when it is done, which ends up calling timespan_thread_finished
151 * 8. Once all channel configs are written, a new timespan is started by calling start_timespan
152 * 9. When all timespans are written the session is taken out of export.
156 ExportHandler::do_export (bool rt)
158 /* Count timespans */
160 export_status->init();
161 std::set<TimespanPtr> timespan_set;
162 for (ConfigMap::iterator it = config_map.begin(); it != config_map.end(); ++it) {
163 timespan_set.insert (it->first);
165 export_status->total_timespans = timespan_set.size();
171 session.ExportReadFinished.connect (export_read_finished_connection, boost::bind (&ExportHandler::finish_timespan, this));
175 struct LocationSortByStart {
176 bool operator() (Location *a, Location *b) {
177 return a->start() < b->start();
182 ExportHandler::export_cd_marker_file (TimespanPtr timespan, FormatPtr file_format, std::string filename, CDMarkerFormat format)
185 string basename = Glib::path_get_basename(filename);
187 size_t ext_pos = basename.rfind('.');
188 if (ext_pos != string::npos) {
189 basename = basename.substr(0, ext_pos); /* strip file extension, if there is one */
192 void (ExportHandler::*header_func) (CDMarkerStatus &);
193 void (ExportHandler::*track_func) (CDMarkerStatus &);
194 void (ExportHandler::*index_func) (CDMarkerStatus &);
198 filepath = Glib::build_filename(Glib::path_get_dirname(filename), basename + ".toc");
199 header_func = &ExportHandler::write_toc_header;
200 track_func = &ExportHandler::write_track_info_toc;
201 index_func = &ExportHandler::write_index_info_toc;
204 filepath = Glib::build_filename(Glib::path_get_dirname(filename), basename + ".cue");
205 header_func = &ExportHandler::write_cue_header;
206 track_func = &ExportHandler::write_track_info_cue;
207 index_func = &ExportHandler::write_index_info_cue;
213 CDMarkerStatus status (filepath, timespan, file_format, filename);
216 error << string_compose(_("Editor: cannot open \"%1\" as export file for CD marker file"), filepath) << endmsg;
220 (this->*header_func) (status);
222 /* Get locations and sort */
224 Locations::LocationList const & locations (session.locations()->list());
225 Locations::LocationList::const_iterator i;
226 Locations::LocationList temp;
228 for (i = locations.begin(); i != locations.end(); ++i) {
229 if ((*i)->start() >= timespan->get_start() && (*i)->end() <= timespan->get_end() && (*i)->is_cd_marker() && !(*i)->is_end()) {
235 // TODO One index marker for whole thing
239 LocationSortByStart cmp;
241 Locations::LocationList::const_iterator nexti;
243 /* Start actual marker stuff */
245 nframes_t last_end_time = timespan->get_start(), last_start_time = timespan->get_start();
246 status.track_position = last_start_time - timespan->get_start();
248 for (i = temp.begin(); i != temp.end(); ++i) {
252 if ((*i)->start() < last_end_time) {
253 if ((*i)->is_mark()) {
254 /* Index within track */
256 status.index_position = (*i)->start() - timespan->get_start();
257 (this->*index_func) (status);
263 /* A track, defined by a cd range marker or a cd location marker outside of a cd range */
265 status.track_position = last_end_time - timespan->get_start();
266 status.track_start_frame = (*i)->start() - timespan->get_start(); // everything before this is the pregap
267 status.track_duration = 0;
269 if ((*i)->is_mark()) {
270 // a mark track location needs to look ahead to the next marker's start to determine length
274 if (nexti != temp.end()) {
275 status.track_duration = (*nexti)->start() - last_end_time;
277 last_start_time = (*i)->start();
278 last_end_time = (*nexti)->start();
280 // this was the last marker, use timespan end
281 status.track_duration = timespan->get_end() - last_end_time;
283 last_start_time = (*i)->start();
284 last_end_time = timespan->get_end();
288 status.track_duration = (*i)->end() - last_end_time;
290 last_start_time = (*i)->start();
291 last_end_time = (*i)->end();
294 (this->*track_func) (status);
299 ExportHandler::write_cue_header (CDMarkerStatus & status)
301 Glib::ustring title = status.timespan->name().compare ("Session") ? status.timespan->name() : (Glib::ustring) session.name();
303 status.out << "REM Cue file generated by Ardour" << endl;
304 status.out << "TITLE \"" << title << "\"" << endl;
306 /* The cue sheet syntax has originally five file types:
307 WAVE : 44.1 kHz, 16 Bit (little endian)
308 AIFF : 44.1 kHz, 16 Bit (big endian)
309 BINARY : 44.1 kHz, 16 Bit (little endian)
310 MOTOROLA : 44.1 kHz, 16 Bit (big endian)
313 We want to use cue sheets not only as CD images but also as general playlyist
314 format, thus for WAVE and AIFF we don't care if it's really 44.1 kHz/16 Bit, the
315 soundfile's header shows it anyway. But for the raw formats, i.e. BINARY
316 and MOTOROLA we do care, because no header would tell us about a different format.
318 For all other formats we just make up our own file type. MP3 is not supported
322 status.out << "FILE \"" << Glib::path_get_basename(status.filename) << "\" ";
323 if (!status.format->format_name().compare ("WAV")) {
324 status.out << "WAVE";
325 } else if (status.format->format_name() == ExportFormatBase::F_RAW &&
326 status.format->sample_format() == ExportFormatBase::SF_16 &&
327 status.format->sample_rate() == ExportFormatBase::SR_44_1) {
328 // Format is RAW 16bit 44.1kHz
329 if (status.format->endianness() == ExportFormatBase::E_Little) {
330 status.out << "BINARY";
332 status.out << "MOTOROLA";
335 // AIFF should return "AIFF"
336 status.out << status.format->format_name();
342 ExportHandler::write_toc_header (CDMarkerStatus & status)
344 Glib::ustring title = status.timespan->name().compare ("Session") ? status.timespan->name() : (Glib::ustring) session.name();
346 status.out << "CD_DA" << endl;
347 status.out << "CD_TEXT {" << endl << " LANGUAGE_MAP {" << endl << " 0 : EN" << endl << " }" << endl;
348 status.out << " LANGUAGE 0 {" << endl << " TITLE \"" << title << "\"" << endl << " }" << endl << "}" << endl;
352 ExportHandler::write_track_info_cue (CDMarkerStatus & status)
356 snprintf (buf, sizeof(buf), " TRACK %02d AUDIO", status.track_number);
357 status.out << buf << endl;
359 status.out << " FLAGS" ;
360 if (status.marker->cd_info.find("scms") != status.marker->cd_info.end()) {
361 status.out << " SCMS ";
363 status.out << " DCP ";
366 if (status.marker->cd_info.find("preemph") != status.marker->cd_info.end()) {
367 status.out << " PRE";
371 if (status.marker->cd_info.find("isrc") != status.marker->cd_info.end()) {
372 status.out << " ISRC " << status.marker->cd_info["isrc"] << endl;
375 if (status.marker->name() != "") {
376 status.out << " TITLE \"" << status.marker->name() << "\"" << endl;
379 if (status.marker->cd_info.find("performer") != status.marker->cd_info.end()) {
380 status.out << " PERFORMER \"" << status.marker->cd_info["performer"] << "\"" << endl;
383 if (status.marker->cd_info.find("string_composer") != status.marker->cd_info.end()) {
384 status.out << " SONGWRITER \"" << status.marker->cd_info["string_composer"] << "\"" << endl;
387 if (status.track_position != status.track_start_frame) {
388 frames_to_cd_frames_string (buf, status.track_position);
389 status.out << " INDEX 00" << buf << endl;
392 frames_to_cd_frames_string (buf, status.track_start_frame);
393 status.out << " INDEX 01" << buf << endl;
395 status.index_number = 2;
396 status.track_number++;
400 ExportHandler::write_track_info_toc (CDMarkerStatus & status)
404 status.out << endl << "TRACK AUDIO" << endl;
406 if (status.marker->cd_info.find("scms") != status.marker->cd_info.end()) {
409 status.out << "COPY" << endl;
411 if (status.marker->cd_info.find("preemph") != status.marker->cd_info.end()) {
412 status.out << "PRE_EMPHASIS" << endl;
414 status.out << "NO PRE_EMPHASIS" << endl;
417 if (status.marker->cd_info.find("isrc") != status.marker->cd_info.end()) {
418 status.out << "ISRC \"" << status.marker->cd_info["isrc"] << "\"" << endl;
421 status.out << "CD_TEXT {" << endl << " LANGUAGE 0 {" << endl << " TITLE \"" << status.marker->name() << "\"" << endl;
422 if (status.marker->cd_info.find("performer") != status.marker->cd_info.end()) {
423 status.out << " PERFORMER \"" << status.marker->cd_info["performer"] << "\"" << endl;
425 if (status.marker->cd_info.find("string_composer") != status.marker->cd_info.end()) {
426 status.out << " COMPOSER \"" << status.marker->cd_info["string_composer"] << "\"" << endl;
429 if (status.marker->cd_info.find("isrc") != status.marker->cd_info.end()) {
430 status.out << " ISRC \"";
431 status.out << status.marker->cd_info["isrc"].substr(0,2) << "-";
432 status.out << status.marker->cd_info["isrc"].substr(2,3) << "-";
433 status.out << status.marker->cd_info["isrc"].substr(5,2) << "-";
434 status.out << status.marker->cd_info["isrc"].substr(7,5) << "\"" << endl;
437 status.out << " }" << endl << "}" << endl;
439 frames_to_cd_frames_string (buf, status.track_position);
440 status.out << "FILE \"" << status.filename << "\" " << buf;
442 frames_to_cd_frames_string (buf, status.track_duration);
443 status.out << buf << endl;
445 frames_to_cd_frames_string (buf, status.track_start_frame - status.track_position);
446 status.out << "START" << buf << endl;
450 ExportHandler::write_index_info_cue (CDMarkerStatus & status)
454 snprintf (buf, sizeof(buf), " INDEX %02d", cue_indexnum);
456 frames_to_cd_frames_string (buf, status.index_position);
457 status.out << buf << endl;
463 ExportHandler::write_index_info_toc (CDMarkerStatus & status)
467 frames_to_cd_frames_string (buf, status.index_position - status.track_position);
468 status.out << "INDEX" << buf << endl;
472 ExportHandler::frames_to_cd_frames_string (char* buf, nframes_t when)
475 nframes_t fr = session.nominal_frame_rate();
476 int mins, secs, frames;
478 mins = when / (60 * fr);
479 remainder = when - (mins * 60 * fr);
480 secs = remainder / fr;
481 remainder -= secs * fr;
482 frames = remainder / (fr / 75);
483 sprintf (buf, " %02d:%02d:%02d", mins, secs, frames);
487 ExportHandler::start_timespan ()
489 export_status->timespan++;
491 if (config_map.empty()) {
492 export_status->finish ();
496 current_timespan = config_map.begin()->first;
498 /* Register channel configs with timespan */
500 timespan_bounds = config_map.equal_range (current_timespan);
502 for (ConfigMap::iterator it = timespan_bounds.first; it != timespan_bounds.second; ++it) {
503 it->second.channel_config->register_with_timespan (current_timespan);
506 /* connect stuff and start export */
508 session.ProcessExport.connect (current_timespan->process_connection, boost::bind (&ExportTimespan::process, current_timespan, _1));
509 session.start_audio_export (current_timespan->get_start(), realtime);
513 ExportHandler::finish_timespan ()
515 current_timespan->process_connection.disconnect ();
517 /* Register formats and filenames to relevant channel configs */
519 export_status->total_formats = 0;
520 export_status->format = 0;
522 for (ConfigMap::iterator it = timespan_bounds.first; it != timespan_bounds.second; ++it) {
524 export_status->total_formats++;
528 it->second.filename->set_timespan (current_timespan);
529 it->second.filename->set_channel_config (it->second.channel_config);
531 /* Do actual registration */
533 ChannelConfigPtr chan_config = it->second.channel_config;
534 chan_config->register_file_config (it->second.format, it->second.filename);
537 /* Start writing files by doing a manual call to timespan_thread_finished */
539 current_map_it = timespan_bounds.first;
540 timespan_thread_finished ();
544 ExportHandler::timespan_thread_finished ()
546 channel_config_connection.disconnect();
547 export_read_finished_connection.disconnect ();
549 if (current_map_it != timespan_bounds.second) {
551 /* Get next configuration as long as no new export process is started */
553 ChannelConfigPtr cc = current_map_it->second.channel_config;
554 while (!cc->write_files(processor)) {
558 if (current_map_it == timespan_bounds.second) {
560 /* reached end of bounds, this call will end up in the else block below */
562 timespan_thread_finished ();
566 cc = current_map_it->second.channel_config;
569 cc->FilesWritten.connect (channel_config_connection, boost::bind (&ExportHandler::timespan_thread_finished, this));
572 } else { /* All files are written from current timespan, reset timespan and start new */
574 /* Unregister configs and remove configs with this timespan */
576 for (ConfigMap::iterator it = timespan_bounds.first; it != timespan_bounds.second;) {
577 it->second.channel_config->unregister_all ();
579 ConfigMap::iterator to_erase = it;
581 config_map.erase (to_erase);
584 /* Start new timespan */
591 } // namespace ARDOUR