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/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"
44 /*** ExportElementFactory ***/
46 ExportElementFactory::ExportElementFactory (Session & session) :
52 ExportElementFactory::~ExportElementFactory ()
58 ExportElementFactory::add_timespan ()
60 return ExportTimespanPtr (new ExportTimespan (session.get_export_status(), session.frame_rate()));
63 ExportChannelConfigPtr
64 ExportElementFactory::add_channel_config ()
66 return ExportChannelConfigPtr (new ExportChannelConfiguration (session));
70 ExportElementFactory::add_format ()
72 return ExportFormatSpecPtr (new ExportFormatSpecification (session));
76 ExportElementFactory::add_format (XMLNode const & state)
78 return ExportFormatSpecPtr (new ExportFormatSpecification (session, state));
82 ExportElementFactory::add_format_copy (ExportFormatSpecPtr other)
84 return ExportFormatSpecPtr (new ExportFormatSpecification (*other));
88 ExportElementFactory::add_filename ()
90 return ExportFilenamePtr (new ExportFilename (session));
94 ExportElementFactory::add_filename_copy (ExportFilenamePtr other)
96 return ExportFilenamePtr (new ExportFilename (*other));
99 /*** ExportHandler ***/
101 ExportHandler::ExportHandler (Session & session)
102 : ExportElementFactory (session)
104 , graph_builder (new ExportGraphBuilder (session))
105 , export_status (session.get_export_status ())
106 , normalizing (false)
112 ExportHandler::~ExportHandler ()
114 // TODO remove files that were written but not finished
117 /** Add an export to the `to-do' list */
119 ExportHandler::add_export_config (ExportTimespanPtr timespan, ExportChannelConfigPtr channel_config,
120 ExportFormatSpecPtr format, ExportFilenamePtr filename,
121 BroadcastInfoPtr broadcast_info)
123 FileSpec spec (channel_config, format, filename, broadcast_info);
124 config_map.insert (make_pair (timespan, spec));
130 ExportHandler::do_export ()
132 /* Count timespans */
134 export_status->init();
135 std::set<ExportTimespanPtr> timespan_set;
136 for (ConfigMap::iterator it = config_map.begin(); it != config_map.end(); ++it) {
137 bool new_timespan = timespan_set.insert (it->first).second;
139 export_status->total_frames += it->first->get_length();
142 export_status->total_timespans = timespan_set.size();
150 ExportHandler::start_timespan ()
152 export_status->timespan++;
154 if (config_map.empty()) {
155 // freewheeling has to be stopped from outside the process cycle
156 export_status->running = false;
160 /* finish_timespan pops the config_map entry that has been done, so
161 this is the timespan to do this time
163 current_timespan = config_map.begin()->first;
165 export_status->total_frames_current_timespan = current_timespan->get_length();
166 export_status->timespan_name = current_timespan->name();
167 export_status->processed_frames_current_timespan = 0;
169 /* Register file configurations to graph builder */
171 /* Here's the config_map entries that use this timespan */
172 timespan_bounds = config_map.equal_range (current_timespan);
173 graph_builder->reset ();
174 graph_builder->set_current_timespan (current_timespan);
175 handle_duplicate_format_extensions();
176 for (ConfigMap::iterator it = timespan_bounds.first; it != timespan_bounds.second; ++it) {
177 // Filenames can be shared across timespans
178 FileSpec & spec = it->second;
179 spec.filename->set_timespan (it->first);
180 graph_builder->add_config (spec);
186 session.ProcessExport.connect_same_thread (process_connection, boost::bind (&ExportHandler::process, this, _1));
187 process_position = current_timespan->get_start();
188 session.start_audio_export (process_position);
192 ExportHandler::handle_duplicate_format_extensions()
194 typedef std::map<std::string, int> ExtCountMap;
197 for (ConfigMap::iterator it = timespan_bounds.first; it != timespan_bounds.second; ++it) {
198 counts[it->second.format->extension()]++;
201 bool duplicates_found = false;
202 for (ExtCountMap::iterator it = counts.begin(); it != counts.end(); ++it) {
203 if (it->second > 1) { duplicates_found = true; }
206 // Set this always, as the filenames are shared...
207 for (ConfigMap::iterator it = timespan_bounds.first; it != timespan_bounds.second; ++it) {
208 it->second.filename->include_format_name = duplicates_found;
213 ExportHandler::process (framecnt_t frames)
215 if (!export_status->running) {
217 } else if (normalizing) {
218 return process_normalize ();
220 return process_timespan (frames);
225 ExportHandler::process_timespan (framecnt_t frames)
227 /* update position */
229 framecnt_t frames_to_read = 0;
230 framepos_t const end = current_timespan->get_end();
232 bool const last_cycle = (process_position + frames >= end);
235 frames_to_read = end - process_position;
236 export_status->stop = true;
238 frames_to_read = frames;
241 process_position += frames_to_read;
242 export_status->processed_frames += frames_to_read;
243 export_status->processed_frames_current_timespan += frames_to_read;
245 /* Do actual processing */
246 int ret = graph_builder->process (frames_to_read, last_cycle);
248 /* Start normalizing if necessary */
250 normalizing = graph_builder->will_normalize();
252 export_status->total_normalize_cycles = graph_builder->get_normalize_cycle_count();
253 export_status->current_normalize_cycle = 0;
264 ExportHandler::process_normalize ()
266 if (graph_builder->process_normalize ()) {
268 export_status->normalizing = false;
270 export_status->normalizing = true;
273 export_status->current_normalize_cycle++;
279 ExportHandler::finish_timespan ()
281 while (config_map.begin() != timespan_bounds.second) {
283 ExportFormatSpecPtr fmt = config_map.begin()->second.format;
285 if (fmt->with_cue()) {
286 export_cd_marker_file (current_timespan, fmt, config_map.begin()->second.filename->get_path(fmt), CDMarkerCUE);
289 if (fmt->with_toc()) {
290 export_cd_marker_file (current_timespan, fmt, config_map.begin()->second.filename->get_path(fmt), CDMarkerTOC);
293 config_map.erase (config_map.begin());
299 /*** CD Marker sutff ***/
301 struct LocationSortByStart {
302 bool operator() (Location *a, Location *b) {
303 return a->start() < b->start();
308 ExportHandler::export_cd_marker_file (ExportTimespanPtr timespan, ExportFormatSpecPtr file_format,
309 std::string filename, CDMarkerFormat format)
311 string filepath = get_cd_marker_filename(filename, format);
314 void (ExportHandler::*header_func) (CDMarkerStatus &);
315 void (ExportHandler::*track_func) (CDMarkerStatus &);
316 void (ExportHandler::*index_func) (CDMarkerStatus &);
320 header_func = &ExportHandler::write_toc_header;
321 track_func = &ExportHandler::write_track_info_toc;
322 index_func = &ExportHandler::write_index_info_toc;
325 header_func = &ExportHandler::write_cue_header;
326 track_func = &ExportHandler::write_track_info_cue;
327 index_func = &ExportHandler::write_index_info_cue;
333 CDMarkerStatus status (filepath, timespan, file_format, filename);
336 error << string_compose(_("Editor: cannot open \"%1\" as export file for CD marker file"), filepath) << endmsg;
340 (this->*header_func) (status);
342 /* Get locations and sort */
344 Locations::LocationList const & locations (session.locations()->list());
345 Locations::LocationList::const_iterator i;
346 Locations::LocationList temp;
348 for (i = locations.begin(); i != locations.end(); ++i) {
349 if ((*i)->start() >= timespan->get_start() && (*i)->end() <= timespan->get_end() && (*i)->is_cd_marker() && !(*i)->is_session_range()) {
355 // TODO One index marker for whole thing
359 LocationSortByStart cmp;
361 Locations::LocationList::const_iterator nexti;
363 /* Start actual marker stuff */
365 framepos_t last_end_time = timespan->get_start(), last_start_time = timespan->get_start();
366 status.track_position = last_start_time - timespan->get_start();
368 for (i = temp.begin(); i != temp.end(); ++i) {
372 if ((*i)->start() < last_end_time) {
373 if ((*i)->is_mark()) {
374 /* Index within track */
376 status.index_position = (*i)->start() - timespan->get_start();
377 (this->*index_func) (status);
383 /* A track, defined by a cd range marker or a cd location marker outside of a cd range */
385 status.track_position = last_end_time - timespan->get_start();
386 status.track_start_frame = (*i)->start() - timespan->get_start(); // everything before this is the pregap
387 status.track_duration = 0;
389 if ((*i)->is_mark()) {
390 // a mark track location needs to look ahead to the next marker's start to determine length
394 if (nexti != temp.end()) {
395 status.track_duration = (*nexti)->start() - last_end_time;
397 last_start_time = (*i)->start();
398 last_end_time = (*nexti)->start();
400 // this was the last marker, use timespan end
401 status.track_duration = timespan->get_end() - last_end_time;
403 last_start_time = (*i)->start();
404 last_end_time = timespan->get_end();
408 status.track_duration = (*i)->end() - last_end_time;
410 last_start_time = (*i)->start();
411 last_end_time = (*i)->end();
414 (this->*track_func) (status);
417 } catch (std::exception& e) {
418 error << string_compose (_("an error occured while writing a TOC/CUE file: %1"), e.what()) << endmsg;
419 ::g_unlink (filepath.c_str());
420 } catch (Glib::Exception& e) {
421 error << string_compose (_("an error occured while writing a TOC/CUE file: %1"), e.what()) << endmsg;
422 ::g_unlink (filepath.c_str());
427 ExportHandler::get_cd_marker_filename(std::string filename, CDMarkerFormat format)
429 /* do not strip file suffix because there may be more than one format,
430 and we do not want the CD marker file from one format to overwrite
431 another (e.g. foo.wav.cue > foo.aiff.cue)
436 return filename + ".toc";
438 return filename + ".cue";
440 return filename + ".marker"; // Should not be reached when actually creating a file
445 ExportHandler::write_cue_header (CDMarkerStatus & status)
447 string title = status.timespan->name().compare ("Session") ? status.timespan->name() : (string) session.name();
449 status.out << "REM Cue file generated by " << PROGRAM_NAME << endl;
450 status.out << "TITLE " << cue_escape_cdtext (title) << endl;
452 /* The original cue sheet sepc metions five file types
454 BINARY = "header-less" audio (44.1 kHz, 16 Bit, little endian),
455 MOTOROLA = "header-less" audio (44.1 kHz, 16 Bit, big endian),
458 We try to use these file types whenever appropriate and
459 default to our own names otherwise.
461 status.out << "FILE \"" << Glib::path_get_basename(status.filename) << "\" ";
462 if (!status.format->format_name().compare ("WAV") || !status.format->format_name().compare ("BWF")) {
463 status.out << "WAVE";
464 } else if (status.format->format_id() == ExportFormatBase::F_RAW &&
465 status.format->sample_format() == ExportFormatBase::SF_16 &&
466 status.format->sample_rate() == ExportFormatBase::SR_44_1) {
467 // Format is RAW 16bit 44.1kHz
468 if (status.format->endianness() == ExportFormatBase::E_Little) {
469 status.out << "BINARY";
471 status.out << "MOTOROLA";
474 // no special case for AIFF format it's name is already "AIFF"
475 status.out << status.format->format_name();
481 ExportHandler::write_toc_header (CDMarkerStatus & status)
483 string title = status.timespan->name().compare ("Session") ? status.timespan->name() : (string) session.name();
485 status.out << "CD_DA" << endl;
486 status.out << "CD_TEXT {" << endl << " LANGUAGE_MAP {" << endl << " 0 : EN" << endl << " }" << endl;
487 status.out << " LANGUAGE 0 {" << endl << " TITLE " << toc_escape_cdtext (title) << endl ;
488 status.out << " PERFORMER \"\"" << endl << " }" << endl << "}" << endl;
492 ExportHandler::write_track_info_cue (CDMarkerStatus & status)
496 snprintf (buf, sizeof(buf), " TRACK %02d AUDIO", status.track_number);
497 status.out << buf << endl;
499 status.out << " FLAGS" ;
500 if (status.marker->cd_info.find("scms") != status.marker->cd_info.end()) {
501 status.out << " SCMS ";
503 status.out << " DCP ";
506 if (status.marker->cd_info.find("preemph") != status.marker->cd_info.end()) {
507 status.out << " PRE";
511 if (status.marker->cd_info.find("isrc") != status.marker->cd_info.end()) {
512 status.out << " ISRC " << status.marker->cd_info["isrc"] << endl;
515 if (status.marker->name() != "") {
516 status.out << " TITLE " << cue_escape_cdtext (status.marker->name()) << endl;
519 if (status.marker->cd_info.find("performer") != status.marker->cd_info.end()) {
520 status.out << " PERFORMER " << cue_escape_cdtext (status.marker->cd_info["performer"]) << endl;
523 if (status.marker->cd_info.find("composer") != status.marker->cd_info.end()) {
524 status.out << " SONGWRITER " << cue_escape_cdtext (status.marker->cd_info["composer"]) << endl;
527 if (status.track_position != status.track_start_frame) {
528 frames_to_cd_frames_string (buf, status.track_position);
529 status.out << " INDEX 00" << buf << endl;
532 frames_to_cd_frames_string (buf, status.track_start_frame);
533 status.out << " INDEX 01" << buf << endl;
535 status.index_number = 2;
536 status.track_number++;
540 ExportHandler::write_track_info_toc (CDMarkerStatus & status)
544 status.out << endl << "TRACK AUDIO" << endl;
546 if (status.marker->cd_info.find("scms") != status.marker->cd_info.end()) {
549 status.out << "COPY" << endl;
551 if (status.marker->cd_info.find("preemph") != status.marker->cd_info.end()) {
552 status.out << "PRE_EMPHASIS" << endl;
554 status.out << "NO PRE_EMPHASIS" << endl;
557 if (status.marker->cd_info.find("isrc") != status.marker->cd_info.end()) {
558 status.out << "ISRC \"" << status.marker->cd_info["isrc"] << "\"" << endl;
561 status.out << "CD_TEXT {" << endl << " LANGUAGE 0 {" << endl;
562 status.out << " TITLE " << toc_escape_cdtext (status.marker->name()) << endl;
564 status.out << " PERFORMER ";
565 if (status.marker->cd_info.find("performer") != status.marker->cd_info.end()) {
566 status.out << toc_escape_cdtext (status.marker->cd_info["performer"]) << endl;
568 status.out << "\"\"" << endl;
571 if (status.marker->cd_info.find("composer") != status.marker->cd_info.end()) {
572 status.out << " SONGWRITER " << toc_escape_cdtext (status.marker->cd_info["composer"]) << endl;
575 if (status.marker->cd_info.find("isrc") != status.marker->cd_info.end()) {
576 status.out << " ISRC \"";
577 status.out << status.marker->cd_info["isrc"].substr(0,2) << "-";
578 status.out << status.marker->cd_info["isrc"].substr(2,3) << "-";
579 status.out << status.marker->cd_info["isrc"].substr(5,2) << "-";
580 status.out << status.marker->cd_info["isrc"].substr(7,5) << "\"" << endl;
583 status.out << " }" << endl << "}" << endl;
585 frames_to_cd_frames_string (buf, status.track_position);
586 status.out << "FILE " << toc_escape_filename (status.filename) << ' ' << buf;
588 frames_to_cd_frames_string (buf, status.track_duration);
589 status.out << buf << endl;
591 frames_to_cd_frames_string (buf, status.track_start_frame - status.track_position);
592 status.out << "START" << buf << endl;
596 ExportHandler::write_index_info_cue (CDMarkerStatus & status)
600 snprintf (buf, sizeof(buf), " INDEX %02d", cue_indexnum);
602 frames_to_cd_frames_string (buf, status.index_position);
603 status.out << buf << endl;
609 ExportHandler::write_index_info_toc (CDMarkerStatus & status)
613 frames_to_cd_frames_string (buf, status.index_position - status.track_position);
614 status.out << "INDEX" << buf << endl;
618 ExportHandler::frames_to_cd_frames_string (char* buf, framepos_t when)
620 framecnt_t remainder;
621 framecnt_t fr = session.nominal_frame_rate();
622 int mins, secs, frames;
624 mins = when / (60 * fr);
625 remainder = when - (mins * 60 * fr);
626 secs = remainder / fr;
627 remainder -= secs * fr;
628 frames = remainder / (fr / 75);
629 sprintf (buf, " %02d:%02d:%02d", mins, secs, frames);
633 ExportHandler::toc_escape_cdtext (const std::string& txt)
635 Glib::ustring check (txt);
637 std::string latin1_txt;
641 latin1_txt = Glib::convert (txt, "ISO-8859-1", "UTF-8");
642 } catch (Glib::ConvertError& err) {
643 throw Glib::ConvertError (err.code(), string_compose (_("Cannot convert %1 to Latin-1 text"), txt));
648 for (std::string::const_iterator c = latin1_txt.begin(); c != latin1_txt.end(); ++c) {
652 } else if ((*c) == '\\') {
654 } else if (isprint (*c)) {
657 snprintf (buf, sizeof (buf), "\\%03o", (int) (unsigned char) *c);
668 ExportHandler::toc_escape_filename (const std::string& txt)
674 // We iterate byte-wise not character-wise over a UTF-8 string here,
675 // because we only want to translate backslashes and double quotes
676 for (std::string::const_iterator c = txt.begin(); c != txt.end(); ++c) {
680 } else if (*c == '\\') {
693 ExportHandler::cue_escape_cdtext (const std::string& txt)
695 std::string latin1_txt;
699 latin1_txt = Glib::convert (txt, "ISO-8859-1", "UTF-8");
700 } catch (Glib::ConvertError& err) {
701 throw Glib::ConvertError (err.code(), string_compose (_("Cannot convert %1 to Latin-1 text"), txt));
704 // does not do much mor than UTF-8 to Latin1 translation yet, but
705 // that may have to change if cue parsers in burning programs change
706 out = '"' + latin1_txt + '"';
711 } // namespace ARDOUR