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"
24 #include <glibmm/convert.h>
26 #include "pbd/convert.h"
28 #include "ardour/audiofile_tagger.h"
29 #include "ardour/export_graph_builder.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/session_metadata.h"
45 /*** ExportElementFactory ***/
47 ExportElementFactory::ExportElementFactory (Session & session) :
53 ExportElementFactory::~ExportElementFactory ()
59 ExportElementFactory::add_timespan ()
61 return ExportTimespanPtr (new ExportTimespan (session.get_export_status(), session.frame_rate()));
64 ExportChannelConfigPtr
65 ExportElementFactory::add_channel_config ()
67 return ExportChannelConfigPtr (new ExportChannelConfiguration (session));
71 ExportElementFactory::add_format ()
73 return ExportFormatSpecPtr (new ExportFormatSpecification (session));
77 ExportElementFactory::add_format (XMLNode const & state)
79 return ExportFormatSpecPtr (new ExportFormatSpecification (session, state));
83 ExportElementFactory::add_format_copy (ExportFormatSpecPtr other)
85 return ExportFormatSpecPtr (new ExportFormatSpecification (*other));
89 ExportElementFactory::add_filename ()
91 return ExportFilenamePtr (new ExportFilename (session));
95 ExportElementFactory::add_filename_copy (ExportFilenamePtr other)
97 return ExportFilenamePtr (new ExportFilename (*other));
100 /*** ExportHandler ***/
102 ExportHandler::ExportHandler (Session & session)
103 : ExportElementFactory (session)
105 , graph_builder (new ExportGraphBuilder (session))
106 , export_status (session.get_export_status ())
107 , normalizing (false)
113 ExportHandler::~ExportHandler ()
115 // TODO remove files that were written but not finished
118 /** Add an export to the `to-do' list */
120 ExportHandler::add_export_config (ExportTimespanPtr timespan, ExportChannelConfigPtr channel_config,
121 ExportFormatSpecPtr format, ExportFilenamePtr filename,
122 BroadcastInfoPtr broadcast_info)
124 FileSpec spec (channel_config, format, filename, broadcast_info);
125 config_map.insert (make_pair (timespan, spec));
131 ExportHandler::do_export ()
133 /* Count timespans */
135 export_status->init();
136 std::set<ExportTimespanPtr> timespan_set;
137 for (ConfigMap::iterator it = config_map.begin(); it != config_map.end(); ++it) {
138 bool new_timespan = timespan_set.insert (it->first).second;
140 export_status->total_frames += it->first->get_length();
143 export_status->total_timespans = timespan_set.size();
151 ExportHandler::start_timespan ()
153 export_status->timespan++;
155 if (config_map.empty()) {
156 // freewheeling has to be stopped from outside the process cycle
157 export_status->running = false;
161 /* finish_timespan pops the config_map entry that has been done, so
162 this is the timespan to do this time
164 current_timespan = config_map.begin()->first;
166 export_status->total_frames_current_timespan = current_timespan->get_length();
167 export_status->timespan_name = current_timespan->name();
168 export_status->processed_frames_current_timespan = 0;
170 /* Register file configurations to graph builder */
172 /* Here's the config_map entries that use this timespan */
173 timespan_bounds = config_map.equal_range (current_timespan);
174 graph_builder->reset ();
175 graph_builder->set_current_timespan (current_timespan);
176 handle_duplicate_format_extensions();
177 for (ConfigMap::iterator it = timespan_bounds.first; it != timespan_bounds.second; ++it) {
178 // Filenames can be shared across timespans
179 FileSpec & spec = it->second;
180 spec.filename->set_timespan (it->first);
181 graph_builder->add_config (spec);
187 session.ProcessExport.connect_same_thread (process_connection, boost::bind (&ExportHandler::process, this, _1));
188 process_position = current_timespan->get_start();
189 session.start_audio_export (process_position);
193 ExportHandler::handle_duplicate_format_extensions()
195 typedef std::map<std::string, int> ExtCountMap;
198 for (ConfigMap::iterator it = timespan_bounds.first; it != timespan_bounds.second; ++it) {
199 counts[it->second.format->extension()]++;
202 bool duplicates_found = false;
203 for (ExtCountMap::iterator it = counts.begin(); it != counts.end(); ++it) {
204 if (it->second > 1) { duplicates_found = true; }
207 // Set this always, as the filenames are shared...
208 for (ConfigMap::iterator it = timespan_bounds.first; it != timespan_bounds.second; ++it) {
209 it->second.filename->include_format_name = duplicates_found;
214 ExportHandler::process (framecnt_t frames)
216 if (!export_status->running) {
218 } else if (normalizing) {
219 return process_normalize ();
221 return process_timespan (frames);
226 ExportHandler::process_timespan (framecnt_t frames)
228 /* update position */
230 framecnt_t frames_to_read = 0;
231 framepos_t const end = current_timespan->get_end();
233 bool const last_cycle = (process_position + frames >= end);
236 frames_to_read = end - process_position;
237 export_status->stop = true;
239 frames_to_read = frames;
242 process_position += frames_to_read;
243 export_status->processed_frames += frames_to_read;
244 export_status->processed_frames_current_timespan += frames_to_read;
246 /* Do actual processing */
247 int ret = graph_builder->process (frames_to_read, last_cycle);
249 /* Start normalizing if necessary */
251 normalizing = graph_builder->will_normalize();
253 export_status->total_normalize_cycles = graph_builder->get_normalize_cycle_count();
254 export_status->current_normalize_cycle = 0;
265 ExportHandler::process_normalize ()
267 if (graph_builder->process_normalize ()) {
269 export_status->normalizing = false;
271 export_status->normalizing = true;
274 export_status->current_normalize_cycle++;
280 ExportHandler::finish_timespan ()
282 while (config_map.begin() != timespan_bounds.second) {
284 ExportFormatSpecPtr fmt = config_map.begin()->second.format;
285 std::string filename = config_map.begin()->second.filename->get_path(fmt);
287 if (fmt->with_cue()) {
288 export_cd_marker_file (current_timespan, fmt, filename, CDMarkerCUE);
291 if (fmt->with_toc()) {
292 export_cd_marker_file (current_timespan, fmt, filename, CDMarkerTOC);
296 AudiofileTagger::tag_file(filename, *SessionMetadata::Metadata());
299 config_map.erase (config_map.begin());
305 /*** CD Marker sutff ***/
307 struct LocationSortByStart {
308 bool operator() (Location *a, Location *b) {
309 return a->start() < b->start();
314 ExportHandler::export_cd_marker_file (ExportTimespanPtr timespan, ExportFormatSpecPtr file_format,
315 std::string filename, CDMarkerFormat format)
317 string filepath = get_cd_marker_filename(filename, format);
320 void (ExportHandler::*header_func) (CDMarkerStatus &);
321 void (ExportHandler::*track_func) (CDMarkerStatus &);
322 void (ExportHandler::*index_func) (CDMarkerStatus &);
326 header_func = &ExportHandler::write_toc_header;
327 track_func = &ExportHandler::write_track_info_toc;
328 index_func = &ExportHandler::write_index_info_toc;
331 header_func = &ExportHandler::write_cue_header;
332 track_func = &ExportHandler::write_track_info_cue;
333 index_func = &ExportHandler::write_index_info_cue;
339 CDMarkerStatus status (filepath, timespan, file_format, filename);
342 error << string_compose(_("Editor: cannot open \"%1\" as export file for CD marker file"), filepath) << endmsg;
346 (this->*header_func) (status);
348 /* Get locations and sort */
350 Locations::LocationList const & locations (session.locations()->list());
351 Locations::LocationList::const_iterator i;
352 Locations::LocationList temp;
354 for (i = locations.begin(); i != locations.end(); ++i) {
355 if ((*i)->start() >= timespan->get_start() && (*i)->end() <= timespan->get_end() && (*i)->is_cd_marker() && !(*i)->is_session_range()) {
361 // TODO One index marker for whole thing
365 LocationSortByStart cmp;
367 Locations::LocationList::const_iterator nexti;
369 /* Start actual marker stuff */
371 framepos_t last_end_time = timespan->get_start(), last_start_time = timespan->get_start();
372 status.track_position = last_start_time - timespan->get_start();
374 for (i = temp.begin(); i != temp.end(); ++i) {
378 if ((*i)->start() < last_end_time) {
379 if ((*i)->is_mark()) {
380 /* Index within track */
382 status.index_position = (*i)->start() - timespan->get_start();
383 (this->*index_func) (status);
389 /* A track, defined by a cd range marker or a cd location marker outside of a cd range */
391 status.track_position = last_end_time - timespan->get_start();
392 status.track_start_frame = (*i)->start() - timespan->get_start(); // everything before this is the pregap
393 status.track_duration = 0;
395 if ((*i)->is_mark()) {
396 // a mark track location needs to look ahead to the next marker's start to determine length
400 if (nexti != temp.end()) {
401 status.track_duration = (*nexti)->start() - last_end_time;
403 last_start_time = (*i)->start();
404 last_end_time = (*nexti)->start();
406 // this was the last marker, use timespan end
407 status.track_duration = timespan->get_end() - last_end_time;
409 last_start_time = (*i)->start();
410 last_end_time = timespan->get_end();
414 status.track_duration = (*i)->end() - last_end_time;
416 last_start_time = (*i)->start();
417 last_end_time = (*i)->end();
420 (this->*track_func) (status);
423 } catch (std::exception& e) {
424 error << string_compose (_("an error occured while writing a TOC/CUE file: %1"), e.what()) << endmsg;
425 ::unlink (filepath.c_str());
426 } catch (Glib::Exception& e) {
427 error << string_compose (_("an error occured while writing a TOC/CUE file: %1"), e.what()) << endmsg;
428 ::unlink (filepath.c_str());
433 ExportHandler::get_cd_marker_filename(std::string filename, CDMarkerFormat format)
435 /* do not strip file suffix because there may be more than one format,
436 and we do not want the CD marker file from one format to overwrite
437 another (e.g. foo.wav.cue > foo.aiff.cue)
442 return filename + ".toc";
444 return filename + ".cue";
446 return filename + ".marker"; // Should not be reached when actually creating a file
451 ExportHandler::write_cue_header (CDMarkerStatus & status)
453 string title = status.timespan->name().compare ("Session") ? status.timespan->name() : (string) session.name();
455 status.out << "REM Cue file generated by " << PROGRAM_NAME << endl;
456 status.out << "TITLE " << cue_escape_cdtext (title) << endl;
458 /* The original cue sheet sepc metions five file types
460 BINARY = "header-less" audio (44.1 kHz, 16 Bit, little endian),
461 MOTOROLA = "header-less" audio (44.1 kHz, 16 Bit, big endian),
464 We try to use these file types whenever appropriate and
465 default to our own names otherwise.
467 status.out << "FILE \"" << Glib::path_get_basename(status.filename) << "\" ";
468 if (!status.format->format_name().compare ("WAV") || !status.format->format_name().compare ("BWF")) {
469 status.out << "WAVE";
470 } else if (status.format->format_id() == ExportFormatBase::F_RAW &&
471 status.format->sample_format() == ExportFormatBase::SF_16 &&
472 status.format->sample_rate() == ExportFormatBase::SR_44_1) {
473 // Format is RAW 16bit 44.1kHz
474 if (status.format->endianness() == ExportFormatBase::E_Little) {
475 status.out << "BINARY";
477 status.out << "MOTOROLA";
480 // no special case for AIFF format it's name is already "AIFF"
481 status.out << status.format->format_name();
487 ExportHandler::write_toc_header (CDMarkerStatus & status)
489 string title = status.timespan->name().compare ("Session") ? status.timespan->name() : (string) session.name();
491 status.out << "CD_DA" << endl;
492 status.out << "CD_TEXT {" << endl << " LANGUAGE_MAP {" << endl << " 0 : EN" << endl << " }" << endl;
493 status.out << " LANGUAGE 0 {" << endl << " TITLE " << toc_escape_cdtext (title) << endl ;
494 status.out << " PERFORMER \"\"" << endl << " }" << endl << "}" << endl;
498 ExportHandler::write_track_info_cue (CDMarkerStatus & status)
502 snprintf (buf, sizeof(buf), " TRACK %02d AUDIO", status.track_number);
503 status.out << buf << endl;
505 status.out << " FLAGS" ;
506 if (status.marker->cd_info.find("scms") != status.marker->cd_info.end()) {
507 status.out << " SCMS ";
509 status.out << " DCP ";
512 if (status.marker->cd_info.find("preemph") != status.marker->cd_info.end()) {
513 status.out << " PRE";
517 if (status.marker->cd_info.find("isrc") != status.marker->cd_info.end()) {
518 status.out << " ISRC " << status.marker->cd_info["isrc"] << endl;
521 if (status.marker->name() != "") {
522 status.out << " TITLE " << cue_escape_cdtext (status.marker->name()) << endl;
525 if (status.marker->cd_info.find("performer") != status.marker->cd_info.end()) {
526 status.out << " PERFORMER " << cue_escape_cdtext (status.marker->cd_info["performer"]) << endl;
529 if (status.marker->cd_info.find("composer") != status.marker->cd_info.end()) {
530 status.out << " SONGWRITER " << cue_escape_cdtext (status.marker->cd_info["composer"]) << endl;
533 if (status.track_position != status.track_start_frame) {
534 frames_to_cd_frames_string (buf, status.track_position);
535 status.out << " INDEX 00" << buf << endl;
538 frames_to_cd_frames_string (buf, status.track_start_frame);
539 status.out << " INDEX 01" << buf << endl;
541 status.index_number = 2;
542 status.track_number++;
546 ExportHandler::write_track_info_toc (CDMarkerStatus & status)
550 status.out << endl << "TRACK AUDIO" << endl;
552 if (status.marker->cd_info.find("scms") != status.marker->cd_info.end()) {
555 status.out << "COPY" << endl;
557 if (status.marker->cd_info.find("preemph") != status.marker->cd_info.end()) {
558 status.out << "PRE_EMPHASIS" << endl;
560 status.out << "NO PRE_EMPHASIS" << endl;
563 if (status.marker->cd_info.find("isrc") != status.marker->cd_info.end()) {
564 status.out << "ISRC \"" << status.marker->cd_info["isrc"] << "\"" << endl;
567 status.out << "CD_TEXT {" << endl << " LANGUAGE 0 {" << endl;
568 status.out << " TITLE " << toc_escape_cdtext (status.marker->name()) << endl;
570 status.out << " PERFORMER ";
571 if (status.marker->cd_info.find("performer") != status.marker->cd_info.end()) {
572 status.out << toc_escape_cdtext (status.marker->cd_info["performer"]) << endl;
574 status.out << "\"\"" << endl;
577 if (status.marker->cd_info.find("composer") != status.marker->cd_info.end()) {
578 status.out << " SONGWRITER " << toc_escape_cdtext (status.marker->cd_info["composer"]) << endl;
581 if (status.marker->cd_info.find("isrc") != status.marker->cd_info.end()) {
582 status.out << " ISRC \"";
583 status.out << status.marker->cd_info["isrc"].substr(0,2) << "-";
584 status.out << status.marker->cd_info["isrc"].substr(2,3) << "-";
585 status.out << status.marker->cd_info["isrc"].substr(5,2) << "-";
586 status.out << status.marker->cd_info["isrc"].substr(7,5) << "\"" << endl;
589 status.out << " }" << endl << "}" << endl;
591 frames_to_cd_frames_string (buf, status.track_position);
592 status.out << "FILE " << toc_escape_filename (status.filename) << ' ' << buf;
594 frames_to_cd_frames_string (buf, status.track_duration);
595 status.out << buf << endl;
597 frames_to_cd_frames_string (buf, status.track_start_frame - status.track_position);
598 status.out << "START" << buf << endl;
602 ExportHandler::write_index_info_cue (CDMarkerStatus & status)
606 snprintf (buf, sizeof(buf), " INDEX %02d", cue_indexnum);
608 frames_to_cd_frames_string (buf, status.index_position);
609 status.out << buf << endl;
615 ExportHandler::write_index_info_toc (CDMarkerStatus & status)
619 frames_to_cd_frames_string (buf, status.index_position - status.track_position);
620 status.out << "INDEX" << buf << endl;
624 ExportHandler::frames_to_cd_frames_string (char* buf, framepos_t when)
626 framecnt_t remainder;
627 framecnt_t fr = session.nominal_frame_rate();
628 int mins, secs, frames;
630 mins = when / (60 * fr);
631 remainder = when - (mins * 60 * fr);
632 secs = remainder / fr;
633 remainder -= secs * fr;
634 frames = remainder / (fr / 75);
635 sprintf (buf, " %02d:%02d:%02d", mins, secs, frames);
639 ExportHandler::toc_escape_cdtext (const std::string& txt)
641 Glib::ustring check (txt);
643 std::string latin1_txt;
647 latin1_txt = Glib::convert (txt, "ISO-8859-1", "UTF-8");
648 } catch (Glib::ConvertError& err) {
649 throw Glib::ConvertError (err.code(), string_compose (_("Cannot convert %1 to Latin-1 text"), txt));
654 for (std::string::const_iterator c = latin1_txt.begin(); c != latin1_txt.end(); ++c) {
658 } else if ((*c) == '\\') {
660 } else if (isprint (*c)) {
663 snprintf (buf, sizeof (buf), "\\%03o", (int) (unsigned char) *c);
674 ExportHandler::toc_escape_filename (const std::string& txt)
680 // We iterate byte-wise not character-wise over a UTF-8 string here,
681 // because we only want to translate backslashes and double quotes
682 for (std::string::const_iterator c = txt.begin(); c != txt.end(); ++c) {
686 } else if (*c == '\\') {
699 ExportHandler::cue_escape_cdtext (const std::string& txt)
701 std::string latin1_txt;
705 latin1_txt = Glib::convert (txt, "ISO-8859-1", "UTF-8");
706 } catch (Glib::ConvertError& err) {
707 throw Glib::ConvertError (err.code(), string_compose (_("Cannot convert %1 to Latin-1 text"), txt));
710 // does not do much mor than UTF-8 to Latin1 translation yet, but
711 // that may have to change if cue parsers in burning programs change
712 out = '"' + latin1_txt + '"';
717 } // namespace ARDOUR