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/export_graph_builder.h"
29 #include "ardour/export_timespan.h"
30 #include "ardour/export_channel_configuration.h"
31 #include "ardour/export_status.h"
32 #include "ardour/export_format_specification.h"
33 #include "ardour/export_filename.h"
43 /*** ExportElementFactory ***/
45 ExportElementFactory::ExportElementFactory (Session & session) :
51 ExportElementFactory::~ExportElementFactory ()
57 ExportElementFactory::add_timespan ()
59 return ExportTimespanPtr (new ExportTimespan (session.get_export_status(), session.frame_rate()));
62 ExportChannelConfigPtr
63 ExportElementFactory::add_channel_config ()
65 return ExportChannelConfigPtr (new ExportChannelConfiguration (session));
69 ExportElementFactory::add_format ()
71 return ExportFormatSpecPtr (new ExportFormatSpecification (session));
75 ExportElementFactory::add_format (XMLNode const & state)
77 return ExportFormatSpecPtr (new ExportFormatSpecification (session, state));
81 ExportElementFactory::add_format_copy (ExportFormatSpecPtr other)
83 return ExportFormatSpecPtr (new ExportFormatSpecification (*other));
87 ExportElementFactory::add_filename ()
89 return ExportFilenamePtr (new ExportFilename (session));
93 ExportElementFactory::add_filename_copy (ExportFilenamePtr other)
95 return ExportFilenamePtr (new ExportFilename (*other));
98 /*** ExportHandler ***/
100 ExportHandler::ExportHandler (Session & session)
101 : ExportElementFactory (session)
103 , graph_builder (new ExportGraphBuilder (session))
104 , export_status (session.get_export_status ())
105 , normalizing (false)
111 ExportHandler::~ExportHandler ()
113 // TODO remove files that were written but not finished
116 /** Add an export to the `to-do' list */
118 ExportHandler::add_export_config (ExportTimespanPtr timespan, ExportChannelConfigPtr channel_config,
119 ExportFormatSpecPtr format, ExportFilenamePtr filename,
120 BroadcastInfoPtr broadcast_info)
122 FileSpec spec (channel_config, format, filename, broadcast_info);
123 config_map.insert (make_pair (timespan, spec));
129 ExportHandler::do_export ()
131 /* Count timespans */
133 export_status->init();
134 std::set<ExportTimespanPtr> timespan_set;
135 for (ConfigMap::iterator it = config_map.begin(); it != config_map.end(); ++it) {
136 bool new_timespan = timespan_set.insert (it->first).second;
138 export_status->total_frames += it->first->get_length();
141 export_status->total_timespans = timespan_set.size();
149 ExportHandler::start_timespan ()
151 export_status->timespan++;
153 if (config_map.empty()) {
154 // freewheeling has to be stopped from outside the process cycle
155 export_status->running = false;
159 /* finish_timespan pops the config_map entry that has been done, so
160 this is the timespan to do this time
162 current_timespan = config_map.begin()->first;
164 export_status->total_frames_current_timespan = current_timespan->get_length();
165 export_status->timespan_name = current_timespan->name();
166 export_status->processed_frames_current_timespan = 0;
168 /* Register file configurations to graph builder */
170 /* Here's the config_map entries that use this timespan */
171 timespan_bounds = config_map.equal_range (current_timespan);
172 graph_builder->reset ();
173 graph_builder->set_current_timespan (current_timespan);
174 handle_duplicate_format_extensions();
175 for (ConfigMap::iterator it = timespan_bounds.first; it != timespan_bounds.second; ++it) {
176 // Filenames can be shared across timespans
177 FileSpec & spec = it->second;
178 spec.filename->set_timespan (it->first);
179 graph_builder->add_config (spec);
185 session.ProcessExport.connect_same_thread (process_connection, boost::bind (&ExportHandler::process, this, _1));
186 process_position = current_timespan->get_start();
187 session.start_audio_export (process_position);
191 ExportHandler::handle_duplicate_format_extensions()
193 typedef std::map<std::string, int> ExtCountMap;
196 for (ConfigMap::iterator it = timespan_bounds.first; it != timespan_bounds.second; ++it) {
197 counts[it->second.format->extension()]++;
200 bool duplicates_found = false;
201 for (ExtCountMap::iterator it = counts.begin(); it != counts.end(); ++it) {
202 if (it->second > 1) { duplicates_found = true; }
205 // Set this always, as the filenames are shared...
206 for (ConfigMap::iterator it = timespan_bounds.first; it != timespan_bounds.second; ++it) {
207 it->second.filename->include_format_name = duplicates_found;
212 ExportHandler::process (framecnt_t frames)
214 if (!export_status->running) {
216 } else if (normalizing) {
217 return process_normalize ();
219 return process_timespan (frames);
224 ExportHandler::process_timespan (framecnt_t frames)
226 /* update position */
228 framecnt_t frames_to_read = 0;
229 framepos_t const end = current_timespan->get_end();
231 bool const last_cycle = (process_position + frames >= end);
234 frames_to_read = end - process_position;
235 export_status->stop = true;
237 frames_to_read = frames;
240 process_position += frames_to_read;
241 export_status->processed_frames += frames_to_read;
242 export_status->processed_frames_current_timespan += frames_to_read;
244 /* Do actual processing */
245 int ret = graph_builder->process (frames_to_read, last_cycle);
247 /* Start normalizing if necessary */
249 normalizing = graph_builder->will_normalize();
251 export_status->total_normalize_cycles = graph_builder->get_normalize_cycle_count();
252 export_status->current_normalize_cycle = 0;
263 ExportHandler::process_normalize ()
265 if (graph_builder->process_normalize ()) {
267 export_status->normalizing = false;
269 export_status->normalizing = true;
272 export_status->current_normalize_cycle++;
278 ExportHandler::finish_timespan ()
280 while (config_map.begin() != timespan_bounds.second) {
282 ExportFormatSpecPtr fmt = config_map.begin()->second.format;
284 if (fmt->with_cue()) {
285 export_cd_marker_file (current_timespan, fmt, config_map.begin()->second.filename->get_path(fmt), CDMarkerCUE);
288 if (fmt->with_toc()) {
289 export_cd_marker_file (current_timespan, fmt, config_map.begin()->second.filename->get_path(fmt), CDMarkerTOC);
292 config_map.erase (config_map.begin());
298 /*** CD Marker sutff ***/
300 struct LocationSortByStart {
301 bool operator() (Location *a, Location *b) {
302 return a->start() < b->start();
307 ExportHandler::export_cd_marker_file (ExportTimespanPtr timespan, ExportFormatSpecPtr file_format,
308 std::string filename, CDMarkerFormat format)
310 string filepath = get_cd_marker_filename(filename, format);
313 void (ExportHandler::*header_func) (CDMarkerStatus &);
314 void (ExportHandler::*track_func) (CDMarkerStatus &);
315 void (ExportHandler::*index_func) (CDMarkerStatus &);
319 header_func = &ExportHandler::write_toc_header;
320 track_func = &ExportHandler::write_track_info_toc;
321 index_func = &ExportHandler::write_index_info_toc;
324 header_func = &ExportHandler::write_cue_header;
325 track_func = &ExportHandler::write_track_info_cue;
326 index_func = &ExportHandler::write_index_info_cue;
332 CDMarkerStatus status (filepath, timespan, file_format, filename);
335 error << string_compose(_("Editor: cannot open \"%1\" as export file for CD marker file"), filepath) << endmsg;
339 (this->*header_func) (status);
341 /* Get locations and sort */
343 Locations::LocationList const & locations (session.locations()->list());
344 Locations::LocationList::const_iterator i;
345 Locations::LocationList temp;
347 for (i = locations.begin(); i != locations.end(); ++i) {
348 if ((*i)->start() >= timespan->get_start() && (*i)->end() <= timespan->get_end() && (*i)->is_cd_marker() && !(*i)->is_session_range()) {
354 // TODO One index marker for whole thing
358 LocationSortByStart cmp;
360 Locations::LocationList::const_iterator nexti;
362 /* Start actual marker stuff */
364 framepos_t last_end_time = timespan->get_start(), last_start_time = timespan->get_start();
365 status.track_position = last_start_time - timespan->get_start();
367 for (i = temp.begin(); i != temp.end(); ++i) {
371 if ((*i)->start() < last_end_time) {
372 if ((*i)->is_mark()) {
373 /* Index within track */
375 status.index_position = (*i)->start() - timespan->get_start();
376 (this->*index_func) (status);
382 /* A track, defined by a cd range marker or a cd location marker outside of a cd range */
384 status.track_position = last_end_time - timespan->get_start();
385 status.track_start_frame = (*i)->start() - timespan->get_start(); // everything before this is the pregap
386 status.track_duration = 0;
388 if ((*i)->is_mark()) {
389 // a mark track location needs to look ahead to the next marker's start to determine length
393 if (nexti != temp.end()) {
394 status.track_duration = (*nexti)->start() - last_end_time;
396 last_start_time = (*i)->start();
397 last_end_time = (*nexti)->start();
399 // this was the last marker, use timespan end
400 status.track_duration = timespan->get_end() - last_end_time;
402 last_start_time = (*i)->start();
403 last_end_time = timespan->get_end();
407 status.track_duration = (*i)->end() - last_end_time;
409 last_start_time = (*i)->start();
410 last_end_time = (*i)->end();
413 (this->*track_func) (status);
416 } catch (std::exception& e) {
417 error << string_compose (_("an error occured while writing a TOC/CUE file: %1"), e.what()) << endmsg;
418 ::unlink (filepath.c_str());
419 } catch (Glib::Exception& e) {
420 error << string_compose (_("an error occured while writing a TOC/CUE file: %1"), e.what()) << endmsg;
421 ::unlink (filepath.c_str());
426 ExportHandler::get_cd_marker_filename(std::string filename, CDMarkerFormat format)
428 /* do not strip file suffix because there may be more than one format,
429 and we do not want the CD marker file from one format to overwrite
430 another (e.g. foo.wav.cue > foo.aiff.cue)
435 return filename + ".toc";
437 return filename + ".cue";
439 return filename + ".marker"; // Should not be reached when actually creating a file
444 ExportHandler::write_cue_header (CDMarkerStatus & status)
446 string title = status.timespan->name().compare ("Session") ? status.timespan->name() : (string) session.name();
448 status.out << "REM Cue file generated by " << PROGRAM_NAME << endl;
449 status.out << "TITLE " << cue_escape_cdtext (title) << endl;
451 /* The original cue sheet sepc metions five file types
453 BINARY = "header-less" audio (44.1 kHz, 16 Bit, little endian),
454 MOTOROLA = "header-less" audio (44.1 kHz, 16 Bit, big endian),
457 We try to use these file types whenever appropriate and
458 default to our own names otherwise.
460 status.out << "FILE \"" << Glib::path_get_basename(status.filename) << "\" ";
461 if (!status.format->format_name().compare ("WAV") || !status.format->format_name().compare ("BWF")) {
462 status.out << "WAVE";
463 } else if (status.format->format_id() == ExportFormatBase::F_RAW &&
464 status.format->sample_format() == ExportFormatBase::SF_16 &&
465 status.format->sample_rate() == ExportFormatBase::SR_44_1) {
466 // Format is RAW 16bit 44.1kHz
467 if (status.format->endianness() == ExportFormatBase::E_Little) {
468 status.out << "BINARY";
470 status.out << "MOTOROLA";
473 // no special case for AIFF format it's name is already "AIFF"
474 status.out << status.format->format_name();
480 ExportHandler::write_toc_header (CDMarkerStatus & status)
482 string title = status.timespan->name().compare ("Session") ? status.timespan->name() : (string) session.name();
484 status.out << "CD_DA" << endl;
485 status.out << "CD_TEXT {" << endl << " LANGUAGE_MAP {" << endl << " 0 : EN" << endl << " }" << endl;
486 status.out << " LANGUAGE 0 {" << endl << " TITLE " << toc_escape_cdtext (title) << endl ;
487 status.out << " PERFORMER \"\"" << endl << " }" << endl << "}" << endl;
491 ExportHandler::write_track_info_cue (CDMarkerStatus & status)
495 snprintf (buf, sizeof(buf), " TRACK %02d AUDIO", status.track_number);
496 status.out << buf << endl;
498 status.out << " FLAGS" ;
499 if (status.marker->cd_info.find("scms") != status.marker->cd_info.end()) {
500 status.out << " SCMS ";
502 status.out << " DCP ";
505 if (status.marker->cd_info.find("preemph") != status.marker->cd_info.end()) {
506 status.out << " PRE";
510 if (status.marker->cd_info.find("isrc") != status.marker->cd_info.end()) {
511 status.out << " ISRC " << status.marker->cd_info["isrc"] << endl;
514 if (status.marker->name() != "") {
515 status.out << " TITLE " << cue_escape_cdtext (status.marker->name()) << endl;
518 if (status.marker->cd_info.find("performer") != status.marker->cd_info.end()) {
519 status.out << " PERFORMER " << cue_escape_cdtext (status.marker->cd_info["performer"]) << endl;
522 if (status.marker->cd_info.find("composer") != status.marker->cd_info.end()) {
523 status.out << " SONGWRITER " << cue_escape_cdtext (status.marker->cd_info["composer"]) << endl;
526 if (status.track_position != status.track_start_frame) {
527 frames_to_cd_frames_string (buf, status.track_position);
528 status.out << " INDEX 00" << buf << endl;
531 frames_to_cd_frames_string (buf, status.track_start_frame);
532 status.out << " INDEX 01" << buf << endl;
534 status.index_number = 2;
535 status.track_number++;
539 ExportHandler::write_track_info_toc (CDMarkerStatus & status)
543 status.out << endl << "TRACK AUDIO" << endl;
545 if (status.marker->cd_info.find("scms") != status.marker->cd_info.end()) {
548 status.out << "COPY" << endl;
550 if (status.marker->cd_info.find("preemph") != status.marker->cd_info.end()) {
551 status.out << "PRE_EMPHASIS" << endl;
553 status.out << "NO PRE_EMPHASIS" << endl;
556 if (status.marker->cd_info.find("isrc") != status.marker->cd_info.end()) {
557 status.out << "ISRC \"" << status.marker->cd_info["isrc"] << "\"" << endl;
560 status.out << "CD_TEXT {" << endl << " LANGUAGE 0 {" << endl;
561 status.out << " TITLE " << toc_escape_cdtext (status.marker->name()) << endl;
563 status.out << " PERFORMER ";
564 if (status.marker->cd_info.find("performer") != status.marker->cd_info.end()) {
565 status.out << toc_escape_cdtext (status.marker->cd_info["performer"]) << endl;
567 status.out << "\"\"" << endl;
570 if (status.marker->cd_info.find("composer") != status.marker->cd_info.end()) {
571 status.out << " SONGWRITER " << toc_escape_cdtext (status.marker->cd_info["composer"]) << endl;
574 if (status.marker->cd_info.find("isrc") != status.marker->cd_info.end()) {
575 status.out << " ISRC \"";
576 status.out << status.marker->cd_info["isrc"].substr(0,2) << "-";
577 status.out << status.marker->cd_info["isrc"].substr(2,3) << "-";
578 status.out << status.marker->cd_info["isrc"].substr(5,2) << "-";
579 status.out << status.marker->cd_info["isrc"].substr(7,5) << "\"" << endl;
582 status.out << " }" << endl << "}" << endl;
584 frames_to_cd_frames_string (buf, status.track_position);
585 status.out << "FILE " << toc_escape_filename (status.filename) << ' ' << buf;
587 frames_to_cd_frames_string (buf, status.track_duration);
588 status.out << buf << endl;
590 frames_to_cd_frames_string (buf, status.track_start_frame - status.track_position);
591 status.out << "START" << buf << endl;
595 ExportHandler::write_index_info_cue (CDMarkerStatus & status)
599 snprintf (buf, sizeof(buf), " INDEX %02d", cue_indexnum);
601 frames_to_cd_frames_string (buf, status.index_position);
602 status.out << buf << endl;
608 ExportHandler::write_index_info_toc (CDMarkerStatus & status)
612 frames_to_cd_frames_string (buf, status.index_position - status.track_position);
613 status.out << "INDEX" << buf << endl;
617 ExportHandler::frames_to_cd_frames_string (char* buf, framepos_t when)
619 framecnt_t remainder;
620 framecnt_t fr = session.nominal_frame_rate();
621 int mins, secs, frames;
623 mins = when / (60 * fr);
624 remainder = when - (mins * 60 * fr);
625 secs = remainder / fr;
626 remainder -= secs * fr;
627 frames = remainder / (fr / 75);
628 sprintf (buf, " %02d:%02d:%02d", mins, secs, frames);
632 ExportHandler::toc_escape_cdtext (const std::string& txt)
634 Glib::ustring check (txt);
636 std::string latin1_txt;
640 latin1_txt = Glib::convert (txt, "ISO-8859-1", "UTF-8");
641 } catch (Glib::ConvertError& err) {
642 throw Glib::ConvertError (err.code(), string_compose (_("Cannot convert %1 to Latin-1 text"), txt));
647 for (std::string::const_iterator c = latin1_txt.begin(); c != latin1_txt.end(); ++c) {
651 } else if ((*c) == '\\') {
653 } else if (isprint (*c)) {
656 snprintf (buf, sizeof (buf), "\\%03o", (int) (unsigned char) *c);
667 ExportHandler::toc_escape_filename (const std::string& txt)
673 // We iterate byte-wise not character-wise over a UTF-8 string here,
674 // because we only want to translate backslashes and double quotes
675 for (std::string::const_iterator c = txt.begin(); c != txt.end(); ++c) {
679 } else if (*c == '\\') {
692 ExportHandler::cue_escape_cdtext (const std::string& txt)
694 std::string latin1_txt;
698 latin1_txt = Glib::convert (txt, "ISO-8859-1", "UTF-8");
699 } catch (Glib::ConvertError& err) {
700 throw Glib::ConvertError (err.code(), string_compose (_("Cannot convert %1 to Latin-1 text"), txt));
703 // does not do much mor than UTF-8 to Latin1 translation yet, but
704 // that may have to change if cue parsers in burning programs change
705 out = '"' + latin1_txt + '"';
710 } // namespace ARDOUR