2 Copyright (C) 2008-2009 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"
23 #include <glib/gstdio.h>
25 #include <glibmm/convert.h>
27 #include "pbd/convert.h"
29 #include "ardour/audiofile_tagger.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/session_metadata.h"
46 /*** ExportElementFactory ***/
48 ExportElementFactory::ExportElementFactory (Session & session) :
54 ExportElementFactory::~ExportElementFactory ()
60 ExportElementFactory::add_timespan ()
62 return ExportTimespanPtr (new ExportTimespan (session.get_export_status(), session.frame_rate()));
65 ExportChannelConfigPtr
66 ExportElementFactory::add_channel_config ()
68 return ExportChannelConfigPtr (new ExportChannelConfiguration (session));
72 ExportElementFactory::add_format ()
74 return ExportFormatSpecPtr (new ExportFormatSpecification (session));
78 ExportElementFactory::add_format (XMLNode const & state)
80 return ExportFormatSpecPtr (new ExportFormatSpecification (session, state));
84 ExportElementFactory::add_format_copy (ExportFormatSpecPtr other)
86 return ExportFormatSpecPtr (new ExportFormatSpecification (*other));
90 ExportElementFactory::add_filename ()
92 return ExportFilenamePtr (new ExportFilename (session));
96 ExportElementFactory::add_filename_copy (ExportFilenamePtr other)
98 return ExportFilenamePtr (new ExportFilename (*other));
101 /*** ExportHandler ***/
103 ExportHandler::ExportHandler (Session & session)
104 : ExportElementFactory (session)
106 , graph_builder (new ExportGraphBuilder (session))
107 , export_status (session.get_export_status ())
108 , normalizing (false)
114 ExportHandler::~ExportHandler ()
116 // TODO remove files that were written but not finished
119 /** Add an export to the `to-do' list */
121 ExportHandler::add_export_config (ExportTimespanPtr timespan, ExportChannelConfigPtr channel_config,
122 ExportFormatSpecPtr format, ExportFilenamePtr filename,
123 BroadcastInfoPtr broadcast_info)
125 FileSpec spec (channel_config, format, filename, broadcast_info);
126 config_map.insert (make_pair (timespan, spec));
132 ExportHandler::do_export ()
134 /* Count timespans */
136 export_status->init();
137 std::set<ExportTimespanPtr> timespan_set;
138 for (ConfigMap::iterator it = config_map.begin(); it != config_map.end(); ++it) {
139 bool new_timespan = timespan_set.insert (it->first).second;
141 export_status->total_frames += it->first->get_length();
144 export_status->total_timespans = timespan_set.size();
152 ExportHandler::start_timespan ()
154 export_status->timespan++;
156 if (config_map.empty()) {
157 // freewheeling has to be stopped from outside the process cycle
158 export_status->running = false;
162 /* finish_timespan pops the config_map entry that has been done, so
163 this is the timespan to do this time
165 current_timespan = config_map.begin()->first;
167 export_status->total_frames_current_timespan = current_timespan->get_length();
168 export_status->timespan_name = current_timespan->name();
169 export_status->processed_frames_current_timespan = 0;
171 /* Register file configurations to graph builder */
173 /* Here's the config_map entries that use this timespan */
174 timespan_bounds = config_map.equal_range (current_timespan);
175 graph_builder->reset ();
176 graph_builder->set_current_timespan (current_timespan);
177 handle_duplicate_format_extensions();
178 for (ConfigMap::iterator it = timespan_bounds.first; it != timespan_bounds.second; ++it) {
179 // Filenames can be shared across timespans
180 FileSpec & spec = it->second;
181 spec.filename->set_timespan (it->first);
182 graph_builder->add_config (spec);
188 session.ProcessExport.connect_same_thread (process_connection, boost::bind (&ExportHandler::process, this, _1));
189 process_position = current_timespan->get_start();
190 session.start_audio_export (process_position);
194 ExportHandler::handle_duplicate_format_extensions()
196 typedef std::map<std::string, int> ExtCountMap;
199 for (ConfigMap::iterator it = timespan_bounds.first; it != timespan_bounds.second; ++it) {
200 counts[it->second.format->extension()]++;
203 bool duplicates_found = false;
204 for (ExtCountMap::iterator it = counts.begin(); it != counts.end(); ++it) {
205 if (it->second > 1) { duplicates_found = true; }
208 // Set this always, as the filenames are shared...
209 for (ConfigMap::iterator it = timespan_bounds.first; it != timespan_bounds.second; ++it) {
210 it->second.filename->include_format_name = duplicates_found;
215 ExportHandler::process (framecnt_t frames)
217 if (!export_status->running) {
219 } else if (normalizing) {
220 return process_normalize ();
222 return process_timespan (frames);
227 ExportHandler::process_timespan (framecnt_t frames)
229 /* update position */
231 framecnt_t frames_to_read = 0;
232 framepos_t const end = current_timespan->get_end();
234 bool const last_cycle = (process_position + frames >= end);
237 frames_to_read = end - process_position;
238 export_status->stop = true;
240 frames_to_read = frames;
243 process_position += frames_to_read;
244 export_status->processed_frames += frames_to_read;
245 export_status->processed_frames_current_timespan += frames_to_read;
247 /* Do actual processing */
248 int ret = graph_builder->process (frames_to_read, last_cycle);
250 /* Start normalizing if necessary */
252 normalizing = graph_builder->will_normalize();
254 export_status->total_normalize_cycles = graph_builder->get_normalize_cycle_count();
255 export_status->current_normalize_cycle = 0;
266 ExportHandler::process_normalize ()
268 if (graph_builder->process_normalize ()) {
270 export_status->normalizing = false;
272 export_status->normalizing = true;
275 export_status->current_normalize_cycle++;
281 ExportHandler::finish_timespan ()
283 while (config_map.begin() != timespan_bounds.second) {
285 ExportFormatSpecPtr fmt = config_map.begin()->second.format;
286 std::string filename = config_map.begin()->second.filename->get_path(fmt);
288 if (fmt->with_cue()) {
289 export_cd_marker_file (current_timespan, fmt, filename, CDMarkerCUE);
292 if (fmt->with_toc()) {
293 export_cd_marker_file (current_timespan, fmt, filename, CDMarkerTOC);
297 AudiofileTagger::tag_file(filename, *SessionMetadata::Metadata());
300 config_map.erase (config_map.begin());
306 /*** CD Marker sutff ***/
308 struct LocationSortByStart {
309 bool operator() (Location *a, Location *b) {
310 return a->start() < b->start();
315 ExportHandler::export_cd_marker_file (ExportTimespanPtr timespan, ExportFormatSpecPtr file_format,
316 std::string filename, CDMarkerFormat format)
318 string filepath = get_cd_marker_filename(filename, format);
321 void (ExportHandler::*header_func) (CDMarkerStatus &);
322 void (ExportHandler::*track_func) (CDMarkerStatus &);
323 void (ExportHandler::*index_func) (CDMarkerStatus &);
327 header_func = &ExportHandler::write_toc_header;
328 track_func = &ExportHandler::write_track_info_toc;
329 index_func = &ExportHandler::write_index_info_toc;
332 header_func = &ExportHandler::write_cue_header;
333 track_func = &ExportHandler::write_track_info_cue;
334 index_func = &ExportHandler::write_index_info_cue;
340 CDMarkerStatus status (filepath, timespan, file_format, filename);
343 error << string_compose(_("Editor: cannot open \"%1\" as export file for CD marker file"), filepath) << endmsg;
347 (this->*header_func) (status);
349 /* Get locations and sort */
351 Locations::LocationList const & locations (session.locations()->list());
352 Locations::LocationList::const_iterator i;
353 Locations::LocationList temp;
355 for (i = locations.begin(); i != locations.end(); ++i) {
356 if ((*i)->start() >= timespan->get_start() && (*i)->end() <= timespan->get_end() && (*i)->is_cd_marker() && !(*i)->is_session_range()) {
362 // TODO One index marker for whole thing
366 LocationSortByStart cmp;
368 Locations::LocationList::const_iterator nexti;
370 /* Start actual marker stuff */
372 framepos_t last_end_time = timespan->get_start(), last_start_time = timespan->get_start();
373 status.track_position = last_start_time - timespan->get_start();
375 for (i = temp.begin(); i != temp.end(); ++i) {
379 if ((*i)->start() < last_end_time) {
380 if ((*i)->is_mark()) {
381 /* Index within track */
383 status.index_position = (*i)->start() - timespan->get_start();
384 (this->*index_func) (status);
390 /* A track, defined by a cd range marker or a cd location marker outside of a cd range */
392 status.track_position = last_end_time - timespan->get_start();
393 status.track_start_frame = (*i)->start() - timespan->get_start(); // everything before this is the pregap
394 status.track_duration = 0;
396 if ((*i)->is_mark()) {
397 // a mark track location needs to look ahead to the next marker's start to determine length
401 if (nexti != temp.end()) {
402 status.track_duration = (*nexti)->start() - last_end_time;
404 last_start_time = (*i)->start();
405 last_end_time = (*nexti)->start();
407 // this was the last marker, use timespan end
408 status.track_duration = timespan->get_end() - last_end_time;
410 last_start_time = (*i)->start();
411 last_end_time = timespan->get_end();
415 status.track_duration = (*i)->end() - last_end_time;
417 last_start_time = (*i)->start();
418 last_end_time = (*i)->end();
421 (this->*track_func) (status);
424 } catch (std::exception& e) {
425 error << string_compose (_("an error occured while writing a TOC/CUE file: %1"), e.what()) << endmsg;
426 ::g_unlink (filepath.c_str());
427 } catch (Glib::Exception& e) {
428 error << string_compose (_("an error occured while writing a TOC/CUE file: %1"), e.what()) << endmsg;
429 ::g_unlink (filepath.c_str());
434 ExportHandler::get_cd_marker_filename(std::string filename, CDMarkerFormat format)
436 /* do not strip file suffix because there may be more than one format,
437 and we do not want the CD marker file from one format to overwrite
438 another (e.g. foo.wav.cue > foo.aiff.cue)
443 return filename + ".toc";
445 return filename + ".cue";
447 return filename + ".marker"; // Should not be reached when actually creating a file
452 ExportHandler::write_cue_header (CDMarkerStatus & status)
454 string title = status.timespan->name().compare ("Session") ? status.timespan->name() : (string) session.name();
456 status.out << "REM Cue file generated by " << PROGRAM_NAME << endl;
457 status.out << "TITLE " << cue_escape_cdtext (title) << endl;
459 /* The original cue sheet sepc metions five file types
461 BINARY = "header-less" audio (44.1 kHz, 16 Bit, little endian),
462 MOTOROLA = "header-less" audio (44.1 kHz, 16 Bit, big endian),
465 We try to use these file types whenever appropriate and
466 default to our own names otherwise.
468 status.out << "FILE \"" << Glib::path_get_basename(status.filename) << "\" ";
469 if (!status.format->format_name().compare ("WAV") || !status.format->format_name().compare ("BWF")) {
470 status.out << "WAVE";
471 } else if (status.format->format_id() == ExportFormatBase::F_RAW &&
472 status.format->sample_format() == ExportFormatBase::SF_16 &&
473 status.format->sample_rate() == ExportFormatBase::SR_44_1) {
474 // Format is RAW 16bit 44.1kHz
475 if (status.format->endianness() == ExportFormatBase::E_Little) {
476 status.out << "BINARY";
478 status.out << "MOTOROLA";
481 // no special case for AIFF format it's name is already "AIFF"
482 status.out << status.format->format_name();
488 ExportHandler::write_toc_header (CDMarkerStatus & status)
490 string title = status.timespan->name().compare ("Session") ? status.timespan->name() : (string) session.name();
492 status.out << "CD_DA" << endl;
493 status.out << "CD_TEXT {" << endl << " LANGUAGE_MAP {" << endl << " 0 : EN" << endl << " }" << endl;
494 status.out << " LANGUAGE 0 {" << endl << " TITLE " << toc_escape_cdtext (title) << endl ;
495 status.out << " PERFORMER \"\"" << endl << " }" << endl << "}" << endl;
499 ExportHandler::write_track_info_cue (CDMarkerStatus & status)
503 snprintf (buf, sizeof(buf), " TRACK %02d AUDIO", status.track_number);
504 status.out << buf << endl;
506 status.out << " FLAGS" ;
507 if (status.marker->cd_info.find("scms") != status.marker->cd_info.end()) {
508 status.out << " SCMS ";
510 status.out << " DCP ";
513 if (status.marker->cd_info.find("preemph") != status.marker->cd_info.end()) {
514 status.out << " PRE";
518 if (status.marker->cd_info.find("isrc") != status.marker->cd_info.end()) {
519 status.out << " ISRC " << status.marker->cd_info["isrc"] << endl;
522 if (status.marker->name() != "") {
523 status.out << " TITLE " << cue_escape_cdtext (status.marker->name()) << endl;
526 if (status.marker->cd_info.find("performer") != status.marker->cd_info.end()) {
527 status.out << " PERFORMER " << cue_escape_cdtext (status.marker->cd_info["performer"]) << endl;
530 if (status.marker->cd_info.find("composer") != status.marker->cd_info.end()) {
531 status.out << " SONGWRITER " << cue_escape_cdtext (status.marker->cd_info["composer"]) << endl;
534 if (status.track_position != status.track_start_frame) {
535 frames_to_cd_frames_string (buf, status.track_position);
536 status.out << " INDEX 00" << buf << endl;
539 frames_to_cd_frames_string (buf, status.track_start_frame);
540 status.out << " INDEX 01" << buf << endl;
542 status.index_number = 2;
543 status.track_number++;
547 ExportHandler::write_track_info_toc (CDMarkerStatus & status)
551 status.out << endl << "TRACK AUDIO" << endl;
553 if (status.marker->cd_info.find("scms") != status.marker->cd_info.end()) {
556 status.out << "COPY" << endl;
558 if (status.marker->cd_info.find("preemph") != status.marker->cd_info.end()) {
559 status.out << "PRE_EMPHASIS" << endl;
561 status.out << "NO PRE_EMPHASIS" << endl;
564 if (status.marker->cd_info.find("isrc") != status.marker->cd_info.end()) {
565 status.out << "ISRC \"" << status.marker->cd_info["isrc"] << "\"" << endl;
568 status.out << "CD_TEXT {" << endl << " LANGUAGE 0 {" << endl;
569 status.out << " TITLE " << toc_escape_cdtext (status.marker->name()) << endl;
571 status.out << " PERFORMER ";
572 if (status.marker->cd_info.find("performer") != status.marker->cd_info.end()) {
573 status.out << toc_escape_cdtext (status.marker->cd_info["performer"]) << endl;
575 status.out << "\"\"" << endl;
578 if (status.marker->cd_info.find("composer") != status.marker->cd_info.end()) {
579 status.out << " SONGWRITER " << toc_escape_cdtext (status.marker->cd_info["composer"]) << endl;
582 if (status.marker->cd_info.find("isrc") != status.marker->cd_info.end()) {
583 status.out << " ISRC \"";
584 status.out << status.marker->cd_info["isrc"].substr(0,2) << "-";
585 status.out << status.marker->cd_info["isrc"].substr(2,3) << "-";
586 status.out << status.marker->cd_info["isrc"].substr(5,2) << "-";
587 status.out << status.marker->cd_info["isrc"].substr(7,5) << "\"" << endl;
590 status.out << " }" << endl << "}" << endl;
592 frames_to_cd_frames_string (buf, status.track_position);
593 status.out << "FILE " << toc_escape_filename (status.filename) << ' ' << buf;
595 frames_to_cd_frames_string (buf, status.track_duration);
596 status.out << buf << endl;
598 frames_to_cd_frames_string (buf, status.track_start_frame - status.track_position);
599 status.out << "START" << buf << endl;
603 ExportHandler::write_index_info_cue (CDMarkerStatus & status)
607 snprintf (buf, sizeof(buf), " INDEX %02d", cue_indexnum);
609 frames_to_cd_frames_string (buf, status.index_position);
610 status.out << buf << endl;
616 ExportHandler::write_index_info_toc (CDMarkerStatus & status)
620 frames_to_cd_frames_string (buf, status.index_position - status.track_position);
621 status.out << "INDEX" << buf << endl;
625 ExportHandler::frames_to_cd_frames_string (char* buf, framepos_t when)
627 framecnt_t remainder;
628 framecnt_t fr = session.nominal_frame_rate();
629 int mins, secs, frames;
631 mins = when / (60 * fr);
632 remainder = when - (mins * 60 * fr);
633 secs = remainder / fr;
634 remainder -= secs * fr;
635 frames = remainder / (fr / 75);
636 sprintf (buf, " %02d:%02d:%02d", mins, secs, frames);
640 ExportHandler::toc_escape_cdtext (const std::string& txt)
642 Glib::ustring check (txt);
644 std::string latin1_txt;
648 latin1_txt = Glib::convert (txt, "ISO-8859-1", "UTF-8");
649 } catch (Glib::ConvertError& err) {
650 throw Glib::ConvertError (err.code(), string_compose (_("Cannot convert %1 to Latin-1 text"), txt));
655 for (std::string::const_iterator c = latin1_txt.begin(); c != latin1_txt.end(); ++c) {
659 } else if ((*c) == '\\') {
661 } else if (isprint (*c)) {
664 snprintf (buf, sizeof (buf), "\\%03o", (int) (unsigned char) *c);
675 ExportHandler::toc_escape_filename (const std::string& txt)
681 // We iterate byte-wise not character-wise over a UTF-8 string here,
682 // because we only want to translate backslashes and double quotes
683 for (std::string::const_iterator c = txt.begin(); c != txt.end(); ++c) {
687 } else if (*c == '\\') {
700 ExportHandler::cue_escape_cdtext (const std::string& txt)
702 std::string latin1_txt;
706 latin1_txt = Glib::convert (txt, "ISO-8859-1", "UTF-8");
707 } catch (Glib::ConvertError& err) {
708 throw Glib::ConvertError (err.code(), string_compose (_("Cannot convert %1 to Latin-1 text"), txt));
711 // does not do much mor than UTF-8 to Latin1 translation yet, but
712 // that may have to change if cue parsers in burning programs change
713 out = '"' + latin1_txt + '"';
718 } // namespace ARDOUR