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 for (ConfigMap::iterator it = timespan_bounds.first; it != timespan_bounds.second; ++it) {
175 // Filenames can be shared across timespans
176 FileSpec & spec = it->second;
177 spec.filename->set_timespan (it->first);
178 graph_builder->add_config (spec);
184 session.ProcessExport.connect_same_thread (process_connection, boost::bind (&ExportHandler::process, this, _1));
185 process_position = current_timespan->get_start();
186 session.start_audio_export (process_position);
190 ExportHandler::process (framecnt_t frames)
192 if (!export_status->running) {
194 } else if (normalizing) {
195 return process_normalize ();
197 return process_timespan (frames);
202 ExportHandler::process_timespan (framecnt_t frames)
204 /* update position */
206 framecnt_t frames_to_read = 0;
207 framepos_t const end = current_timespan->get_end();
209 bool const last_cycle = (process_position + frames >= end);
212 frames_to_read = end - process_position;
213 export_status->stop = true;
215 frames_to_read = frames;
218 process_position += frames_to_read;
219 export_status->processed_frames += frames_to_read;
220 export_status->processed_frames_current_timespan += frames_to_read;
222 /* Do actual processing */
223 int ret = graph_builder->process (frames_to_read, last_cycle);
225 /* Start normalizing if necessary */
227 normalizing = graph_builder->will_normalize();
229 export_status->total_normalize_cycles = graph_builder->get_normalize_cycle_count();
230 export_status->current_normalize_cycle = 0;
241 ExportHandler::process_normalize ()
243 if (graph_builder->process_normalize ()) {
245 export_status->normalizing = false;
247 export_status->normalizing = true;
250 export_status->current_normalize_cycle++;
256 ExportHandler::finish_timespan ()
258 while (config_map.begin() != timespan_bounds.second) {
260 ExportFormatSpecPtr fmt = config_map.begin()->second.format;
262 if (fmt->with_cue()) {
263 export_cd_marker_file (current_timespan, fmt, config_map.begin()->second.filename->get_path(fmt), CDMarkerCUE);
266 if (fmt->with_toc()) {
267 export_cd_marker_file (current_timespan, fmt, config_map.begin()->second.filename->get_path(fmt), CDMarkerTOC);
270 config_map.erase (config_map.begin());
276 /*** CD Marker sutff ***/
278 struct LocationSortByStart {
279 bool operator() (Location *a, Location *b) {
280 return a->start() < b->start();
285 ExportHandler::export_cd_marker_file (ExportTimespanPtr timespan, ExportFormatSpecPtr file_format,
286 std::string filename, CDMarkerFormat format)
288 string filepath = get_cd_marker_filename(filename, format);
291 void (ExportHandler::*header_func) (CDMarkerStatus &);
292 void (ExportHandler::*track_func) (CDMarkerStatus &);
293 void (ExportHandler::*index_func) (CDMarkerStatus &);
297 header_func = &ExportHandler::write_toc_header;
298 track_func = &ExportHandler::write_track_info_toc;
299 index_func = &ExportHandler::write_index_info_toc;
302 header_func = &ExportHandler::write_cue_header;
303 track_func = &ExportHandler::write_track_info_cue;
304 index_func = &ExportHandler::write_index_info_cue;
310 CDMarkerStatus status (filepath, timespan, file_format, filename);
313 error << string_compose(_("Editor: cannot open \"%1\" as export file for CD marker file"), filepath) << endmsg;
317 (this->*header_func) (status);
319 /* Get locations and sort */
321 Locations::LocationList const & locations (session.locations()->list());
322 Locations::LocationList::const_iterator i;
323 Locations::LocationList temp;
325 for (i = locations.begin(); i != locations.end(); ++i) {
326 if ((*i)->start() >= timespan->get_start() && (*i)->end() <= timespan->get_end() && (*i)->is_cd_marker() && !(*i)->is_session_range()) {
332 // TODO One index marker for whole thing
336 LocationSortByStart cmp;
338 Locations::LocationList::const_iterator nexti;
340 /* Start actual marker stuff */
342 framepos_t last_end_time = timespan->get_start(), last_start_time = timespan->get_start();
343 status.track_position = last_start_time - timespan->get_start();
345 for (i = temp.begin(); i != temp.end(); ++i) {
349 if ((*i)->start() < last_end_time) {
350 if ((*i)->is_mark()) {
351 /* Index within track */
353 status.index_position = (*i)->start() - timespan->get_start();
354 (this->*index_func) (status);
360 /* A track, defined by a cd range marker or a cd location marker outside of a cd range */
362 status.track_position = last_end_time - timespan->get_start();
363 status.track_start_frame = (*i)->start() - timespan->get_start(); // everything before this is the pregap
364 status.track_duration = 0;
366 if ((*i)->is_mark()) {
367 // a mark track location needs to look ahead to the next marker's start to determine length
371 if (nexti != temp.end()) {
372 status.track_duration = (*nexti)->start() - last_end_time;
374 last_start_time = (*i)->start();
375 last_end_time = (*nexti)->start();
377 // this was the last marker, use timespan end
378 status.track_duration = timespan->get_end() - last_end_time;
380 last_start_time = (*i)->start();
381 last_end_time = timespan->get_end();
385 status.track_duration = (*i)->end() - last_end_time;
387 last_start_time = (*i)->start();
388 last_end_time = (*i)->end();
391 (this->*track_func) (status);
394 } catch (std::exception& e) {
395 error << string_compose (_("an error occured while writing a TOC/CUE file: %1"), e.what()) << endmsg;
396 ::unlink (filepath.c_str());
397 } catch (Glib::Exception& e) {
398 error << string_compose (_("an error occured while writing a TOC/CUE file: %1"), e.what()) << endmsg;
399 ::unlink (filepath.c_str());
404 ExportHandler::get_cd_marker_filename(std::string filename, CDMarkerFormat format)
406 /* do not strip file suffix because there may be more than one format,
407 and we do not want the CD marker file from one format to overwrite
408 another (e.g. foo.wav.cue > foo.aiff.cue)
413 return filename + ".toc";
415 return filename + ".cue";
417 return filename + ".marker"; // Should not be reached when actually creating a file
422 ExportHandler::write_cue_header (CDMarkerStatus & status)
424 string title = status.timespan->name().compare ("Session") ? status.timespan->name() : (string) session.name();
426 status.out << "REM Cue file generated by " << PROGRAM_NAME << endl;
427 status.out << "TITLE " << cue_escape_cdtext (title) << endl;
429 /* The original cue sheet sepc metions five file types
431 BINARY = "header-less" audio (44.1 kHz, 16 Bit, little endian),
432 MOTOROLA = "header-less" audio (44.1 kHz, 16 Bit, big endian),
435 We try to use these file types whenever appropriate and
436 default to our own names otherwise.
438 status.out << "FILE \"" << Glib::path_get_basename(status.filename) << "\" ";
439 if (!status.format->format_name().compare ("WAV") || !status.format->format_name().compare ("BWF")) {
440 status.out << "WAVE";
441 } else if (status.format->format_id() == ExportFormatBase::F_RAW &&
442 status.format->sample_format() == ExportFormatBase::SF_16 &&
443 status.format->sample_rate() == ExportFormatBase::SR_44_1) {
444 // Format is RAW 16bit 44.1kHz
445 if (status.format->endianness() == ExportFormatBase::E_Little) {
446 status.out << "BINARY";
448 status.out << "MOTOROLA";
451 // no special case for AIFF format it's name is already "AIFF"
452 status.out << status.format->format_name();
458 ExportHandler::write_toc_header (CDMarkerStatus & status)
460 string title = status.timespan->name().compare ("Session") ? status.timespan->name() : (string) session.name();
462 status.out << "CD_DA" << endl;
463 status.out << "CD_TEXT {" << endl << " LANGUAGE_MAP {" << endl << " 0 : EN" << endl << " }" << endl;
464 status.out << " LANGUAGE 0 {" << endl << " TITLE " << toc_escape_cdtext (title) << endl ;
465 status.out << " PERFORMER \"\"" << endl << " }" << endl << "}" << endl;
469 ExportHandler::write_track_info_cue (CDMarkerStatus & status)
473 snprintf (buf, sizeof(buf), " TRACK %02d AUDIO", status.track_number);
474 status.out << buf << endl;
476 status.out << " FLAGS" ;
477 if (status.marker->cd_info.find("scms") != status.marker->cd_info.end()) {
478 status.out << " SCMS ";
480 status.out << " DCP ";
483 if (status.marker->cd_info.find("preemph") != status.marker->cd_info.end()) {
484 status.out << " PRE";
488 if (status.marker->cd_info.find("isrc") != status.marker->cd_info.end()) {
489 status.out << " ISRC " << status.marker->cd_info["isrc"] << endl;
492 if (status.marker->name() != "") {
493 status.out << " TITLE " << cue_escape_cdtext (status.marker->name()) << endl;
496 if (status.marker->cd_info.find("performer") != status.marker->cd_info.end()) {
497 status.out << " PERFORMER " << cue_escape_cdtext (status.marker->cd_info["performer"]) << endl;
500 if (status.marker->cd_info.find("composer") != status.marker->cd_info.end()) {
501 status.out << " SONGWRITER " << cue_escape_cdtext (status.marker->cd_info["composer"]) << endl;
504 if (status.track_position != status.track_start_frame) {
505 frames_to_cd_frames_string (buf, status.track_position);
506 status.out << " INDEX 00" << buf << endl;
509 frames_to_cd_frames_string (buf, status.track_start_frame);
510 status.out << " INDEX 01" << buf << endl;
512 status.index_number = 2;
513 status.track_number++;
517 ExportHandler::write_track_info_toc (CDMarkerStatus & status)
521 status.out << endl << "TRACK AUDIO" << endl;
523 if (status.marker->cd_info.find("scms") != status.marker->cd_info.end()) {
526 status.out << "COPY" << endl;
528 if (status.marker->cd_info.find("preemph") != status.marker->cd_info.end()) {
529 status.out << "PRE_EMPHASIS" << endl;
531 status.out << "NO PRE_EMPHASIS" << endl;
534 if (status.marker->cd_info.find("isrc") != status.marker->cd_info.end()) {
535 status.out << "ISRC \"" << status.marker->cd_info["isrc"] << "\"" << endl;
538 status.out << "CD_TEXT {" << endl << " LANGUAGE 0 {" << endl;
539 status.out << " TITLE " << toc_escape_cdtext (status.marker->name()) << endl;
541 status.out << " PERFORMER ";
542 if (status.marker->cd_info.find("performer") != status.marker->cd_info.end()) {
543 status.out << toc_escape_cdtext (status.marker->cd_info["performer"]) << endl;
545 status.out << "\"\"" << endl;
548 if (status.marker->cd_info.find("composer") != status.marker->cd_info.end()) {
549 status.out << " SONGWRITER " << toc_escape_cdtext (status.marker->cd_info["composer"]) << endl;
552 if (status.marker->cd_info.find("isrc") != status.marker->cd_info.end()) {
553 status.out << " ISRC \"";
554 status.out << status.marker->cd_info["isrc"].substr(0,2) << "-";
555 status.out << status.marker->cd_info["isrc"].substr(2,3) << "-";
556 status.out << status.marker->cd_info["isrc"].substr(5,2) << "-";
557 status.out << status.marker->cd_info["isrc"].substr(7,5) << "\"" << endl;
560 status.out << " }" << endl << "}" << endl;
562 frames_to_cd_frames_string (buf, status.track_position);
563 status.out << "FILE " << toc_escape_filename (status.filename) << ' ' << buf;
565 frames_to_cd_frames_string (buf, status.track_duration);
566 status.out << buf << endl;
568 frames_to_cd_frames_string (buf, status.track_start_frame - status.track_position);
569 status.out << "START" << buf << endl;
573 ExportHandler::write_index_info_cue (CDMarkerStatus & status)
577 snprintf (buf, sizeof(buf), " INDEX %02d", cue_indexnum);
579 frames_to_cd_frames_string (buf, status.index_position);
580 status.out << buf << endl;
586 ExportHandler::write_index_info_toc (CDMarkerStatus & status)
590 frames_to_cd_frames_string (buf, status.index_position - status.track_position);
591 status.out << "INDEX" << buf << endl;
595 ExportHandler::frames_to_cd_frames_string (char* buf, framepos_t when)
597 framecnt_t remainder;
598 framecnt_t fr = session.nominal_frame_rate();
599 int mins, secs, frames;
601 mins = when / (60 * fr);
602 remainder = when - (mins * 60 * fr);
603 secs = remainder / fr;
604 remainder -= secs * fr;
605 frames = remainder / (fr / 75);
606 sprintf (buf, " %02d:%02d:%02d", mins, secs, frames);
610 ExportHandler::toc_escape_cdtext (const std::string& txt)
612 Glib::ustring check (txt);
614 std::string latin1_txt;
618 latin1_txt = Glib::convert (txt, "ISO-8859-1", "UTF-8");
619 } catch (Glib::ConvertError& err) {
620 throw Glib::ConvertError (err.code(), string_compose (_("Cannot convert %1 to Latin-1 text"), txt));
625 for (std::string::const_iterator c = latin1_txt.begin(); c != latin1_txt.end(); ++c) {
629 } else if ((*c) == '\\') {
631 } else if (isprint (*c)) {
634 snprintf (buf, sizeof (buf), "\\%03o", (int) (unsigned char) *c);
645 ExportHandler::toc_escape_filename (const std::string& txt)
651 // We iterate byte-wise not character-wise over a UTF-8 string here,
652 // because we only want to translate backslashes and double quotes
653 for (std::string::const_iterator c = txt.begin(); c != txt.end(); ++c) {
657 } else if (*c == '\\') {
670 ExportHandler::cue_escape_cdtext (const std::string& txt)
672 std::string latin1_txt;
676 latin1_txt = Glib::convert (txt, "ISO-8859-1", "UTF-8");
677 } catch (Glib::ConvertError& err) {
678 throw Glib::ConvertError (err.code(), string_compose (_("Cannot convert %1 to Latin-1 text"), txt));
681 // does not do much mor than UTF-8 to Latin1 translation yet, but
682 // that may have to change if cue parsers in burning programs change
683 out = '"' + latin1_txt + '"';
688 } // namespace ARDOUR