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"
27 #include "pbd/filesystem.h"
29 #include "ardour/ardour.h"
30 #include "ardour/configuration.h"
31 #include "ardour/export_graph_builder.h"
32 #include "ardour/export_timespan.h"
33 #include "ardour/export_channel_configuration.h"
34 #include "ardour/export_status.h"
35 #include "ardour/export_format_specification.h"
36 #include "ardour/export_filename.h"
37 #include "ardour/export_failed.h"
47 /*** ExportElementFactory ***/
49 ExportElementFactory::ExportElementFactory (Session & session) :
55 ExportElementFactory::~ExportElementFactory ()
61 ExportElementFactory::add_timespan ()
63 return ExportTimespanPtr (new ExportTimespan (session.get_export_status(), session.frame_rate()));
66 ExportChannelConfigPtr
67 ExportElementFactory::add_channel_config ()
69 return ExportChannelConfigPtr (new ExportChannelConfiguration (session));
73 ExportElementFactory::add_format ()
75 return ExportFormatSpecPtr (new ExportFormatSpecification (session));
79 ExportElementFactory::add_format (XMLNode const & state)
81 return ExportFormatSpecPtr (new ExportFormatSpecification (session, state));
85 ExportElementFactory::add_format_copy (ExportFormatSpecPtr other)
87 return ExportFormatSpecPtr (new ExportFormatSpecification (*other));
91 ExportElementFactory::add_filename ()
93 return ExportFilenamePtr (new ExportFilename (session));
97 ExportElementFactory::add_filename_copy (ExportFilenamePtr other)
99 return ExportFilenamePtr (new ExportFilename (*other));
102 /*** ExportHandler ***/
104 ExportHandler::ExportHandler (Session & session)
105 : ExportElementFactory (session)
107 , graph_builder (new ExportGraphBuilder (session))
108 , export_status (session.get_export_status ())
110 , normalizing (false)
116 ExportHandler::~ExportHandler ()
118 // TODO remove files that were written but not finsihed
122 ExportHandler::add_export_config (ExportTimespanPtr timespan, ExportChannelConfigPtr channel_config,
123 ExportFormatSpecPtr format, ExportFilenamePtr filename,
124 BroadcastInfoPtr broadcast_info)
126 FileSpec spec (channel_config, format, filename, broadcast_info);
127 ConfigPair pair (timespan, spec);
128 config_map.insert (pair);
134 ExportHandler::do_export (bool rt)
136 /* Count timespans */
138 export_status->init();
139 std::set<ExportTimespanPtr> timespan_set;
140 for (ConfigMap::iterator it = config_map.begin(); it != config_map.end(); ++it) {
141 bool new_timespan = timespan_set.insert (it->first).second;
143 export_status->total_frames += it->first->get_length();
146 export_status->total_timespans = timespan_set.size();
155 ExportHandler::start_timespan ()
157 export_status->timespan++;
159 if (config_map.empty()) {
160 // freewheeling has to be stopped from outside the process cycle
161 export_status->running = false;
165 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 timespan_bounds = config_map.equal_range (current_timespan);
173 graph_builder->reset ();
174 graph_builder->set_current_timespan (current_timespan);
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, realtime);
191 ExportHandler::process (framecnt_t frames)
193 if (!export_status->running) {
195 } else if (normalizing) {
196 return process_normalize ();
198 return process_timespan (frames);
203 ExportHandler::process_timespan (framecnt_t frames)
205 /* update position */
207 framecnt_t frames_to_read = 0;
208 framepos_t const end = current_timespan->get_end();
210 bool const last_cycle = (process_position + frames >= end);
213 frames_to_read = end - process_position;
214 export_status->stop = true;
216 frames_to_read = frames;
219 process_position += frames_to_read;
220 export_status->processed_frames += frames_to_read;
221 export_status->processed_frames_current_timespan += frames_to_read;
223 /* Do actual processing */
224 int ret = graph_builder->process (frames_to_read, last_cycle);
226 /* Start normalizing if necessary */
228 normalizing = graph_builder->will_normalize();
230 export_status->total_normalize_cycles = graph_builder->get_normalize_cycle_count();
231 export_status->current_normalize_cycle = 0;
242 ExportHandler::process_normalize ()
244 if (graph_builder->process_normalize ()) {
246 export_status->normalizing = false;
248 export_status->normalizing = true;
251 export_status->current_normalize_cycle++;
257 ExportHandler::finish_timespan ()
259 while (config_map.begin() != timespan_bounds.second) {
261 ExportFormatSpecPtr fmt = config_map.begin()->second.format;
263 if (fmt->with_cue()) {
264 export_cd_marker_file (current_timespan, fmt, config_map.begin()->second.filename->get_path(fmt), CDMarkerCUE);
267 if (fmt->with_toc()) {
268 export_cd_marker_file (current_timespan, fmt, config_map.begin()->second.filename->get_path(fmt), CDMarkerTOC);
271 config_map.erase (config_map.begin());
277 /*** CD Marker sutff ***/
279 struct LocationSortByStart {
280 bool operator() (Location *a, Location *b) {
281 return a->start() < b->start();
286 ExportHandler::export_cd_marker_file (ExportTimespanPtr timespan, ExportFormatSpecPtr file_format,
287 std::string filename, CDMarkerFormat format)
289 string filepath = get_cd_marker_filename(filename, format);
292 void (ExportHandler::*header_func) (CDMarkerStatus &);
293 void (ExportHandler::*track_func) (CDMarkerStatus &);
294 void (ExportHandler::*index_func) (CDMarkerStatus &);
298 header_func = &ExportHandler::write_toc_header;
299 track_func = &ExportHandler::write_track_info_toc;
300 index_func = &ExportHandler::write_index_info_toc;
303 header_func = &ExportHandler::write_cue_header;
304 track_func = &ExportHandler::write_track_info_cue;
305 index_func = &ExportHandler::write_index_info_cue;
311 CDMarkerStatus status (filepath, timespan, file_format, filename);
314 error << string_compose(_("Editor: cannot open \"%1\" as export file for CD marker file"), filepath) << endmsg;
318 (this->*header_func) (status);
320 /* Get locations and sort */
322 Locations::LocationList const & locations (session.locations()->list());
323 Locations::LocationList::const_iterator i;
324 Locations::LocationList temp;
326 for (i = locations.begin(); i != locations.end(); ++i) {
327 if ((*i)->start() >= timespan->get_start() && (*i)->end() <= timespan->get_end() && (*i)->is_cd_marker() && !(*i)->is_session_range()) {
333 // TODO One index marker for whole thing
337 LocationSortByStart cmp;
339 Locations::LocationList::const_iterator nexti;
341 /* Start actual marker stuff */
343 framepos_t last_end_time = timespan->get_start(), last_start_time = timespan->get_start();
344 status.track_position = last_start_time - timespan->get_start();
346 for (i = temp.begin(); i != temp.end(); ++i) {
350 if ((*i)->start() < last_end_time) {
351 if ((*i)->is_mark()) {
352 /* Index within track */
354 status.index_position = (*i)->start() - timespan->get_start();
355 (this->*index_func) (status);
361 /* A track, defined by a cd range marker or a cd location marker outside of a cd range */
363 status.track_position = last_end_time - timespan->get_start();
364 status.track_start_frame = (*i)->start() - timespan->get_start(); // everything before this is the pregap
365 status.track_duration = 0;
367 if ((*i)->is_mark()) {
368 // a mark track location needs to look ahead to the next marker's start to determine length
372 if (nexti != temp.end()) {
373 status.track_duration = (*nexti)->start() - last_end_time;
375 last_start_time = (*i)->start();
376 last_end_time = (*nexti)->start();
378 // this was the last marker, use timespan end
379 status.track_duration = timespan->get_end() - last_end_time;
381 last_start_time = (*i)->start();
382 last_end_time = timespan->get_end();
386 status.track_duration = (*i)->end() - last_end_time;
388 last_start_time = (*i)->start();
389 last_end_time = (*i)->end();
392 (this->*track_func) (status);
395 } catch (std::exception& e) {
396 error << string_compose (_("an error occured while writing a TOC/CUE file: %1"), e.what()) << endmsg;
397 ::unlink (filepath.c_str());
398 } catch (Glib::Exception& e) {
399 error << string_compose (_("an error occured while writing a TOC/CUE file: %1"), e.what()) << endmsg;
400 ::unlink (filepath.c_str());
405 ExportHandler::get_cd_marker_filename(std::string filename, CDMarkerFormat format)
407 /* do not strip file suffix because there may be more than one format,
408 and we do not want the CD marker file from one format to overwrite
409 another (e.g. foo.wav.cue > foo.aiff.cue)
414 return filename + ".toc";
416 return filename + ".cue";
418 return filename + ".marker"; // Should not be reached when actually creating a file
423 ExportHandler::write_cue_header (CDMarkerStatus & status)
425 string title = status.timespan->name().compare ("Session") ? status.timespan->name() : (string) session.name();
427 status.out << "REM Cue file generated by " << PROGRAM_NAME << endl;
428 status.out << "TITLE " << cue_escape_cdtext (title) << endl;
430 /* The original cue sheet sepc metions five file types
432 BINARY = "header-less" audio (44.1 kHz, 16 Bit, little endian),
433 MOTOROLA = "header-less" audio (44.1 kHz, 16 Bit, big endian),
436 We try to use these file types whenever appropriate and
437 default to our own names otherwise.
439 status.out << "FILE \"" << Glib::path_get_basename(status.filename) << "\" ";
440 if (!status.format->format_name().compare ("WAV") || !status.format->format_name().compare ("BWF")) {
441 status.out << "WAVE";
442 } else if (status.format->format_id() == ExportFormatBase::F_RAW &&
443 status.format->sample_format() == ExportFormatBase::SF_16 &&
444 status.format->sample_rate() == ExportFormatBase::SR_44_1) {
445 // Format is RAW 16bit 44.1kHz
446 if (status.format->endianness() == ExportFormatBase::E_Little) {
447 status.out << "BINARY";
449 status.out << "MOTOROLA";
452 // no special case for AIFF format it's name is already "AIFF"
453 status.out << status.format->format_name();
459 ExportHandler::write_toc_header (CDMarkerStatus & status)
461 string title = status.timespan->name().compare ("Session") ? status.timespan->name() : (string) session.name();
463 status.out << "CD_DA" << endl;
464 status.out << "CD_TEXT {" << endl << " LANGUAGE_MAP {" << endl << " 0 : EN" << endl << " }" << endl;
465 status.out << " LANGUAGE 0 {" << endl << " TITLE " << toc_escape_cdtext (title) << endl ;
466 status.out << " PERFORMER \"\"" << endl << " }" << endl << "}" << endl;
470 ExportHandler::write_track_info_cue (CDMarkerStatus & status)
474 snprintf (buf, sizeof(buf), " TRACK %02d AUDIO", status.track_number);
475 status.out << buf << endl;
477 status.out << " FLAGS" ;
478 if (status.marker->cd_info.find("scms") != status.marker->cd_info.end()) {
479 status.out << " SCMS ";
481 status.out << " DCP ";
484 if (status.marker->cd_info.find("preemph") != status.marker->cd_info.end()) {
485 status.out << " PRE";
489 if (status.marker->cd_info.find("isrc") != status.marker->cd_info.end()) {
490 status.out << " ISRC " << status.marker->cd_info["isrc"] << endl;
493 if (status.marker->name() != "") {
494 status.out << " TITLE " << cue_escape_cdtext (status.marker->name()) << endl;
497 if (status.marker->cd_info.find("performer") != status.marker->cd_info.end()) {
498 status.out << " PERFORMER " << cue_escape_cdtext (status.marker->cd_info["performer"]) << endl;
501 if (status.marker->cd_info.find("composer") != status.marker->cd_info.end()) {
502 status.out << " SONGWRITER " << cue_escape_cdtext (status.marker->cd_info["composer"]) << endl;
505 if (status.track_position != status.track_start_frame) {
506 frames_to_cd_frames_string (buf, status.track_position);
507 status.out << " INDEX 00" << buf << endl;
510 frames_to_cd_frames_string (buf, status.track_start_frame);
511 status.out << " INDEX 01" << buf << endl;
513 status.index_number = 2;
514 status.track_number++;
518 ExportHandler::write_track_info_toc (CDMarkerStatus & status)
522 status.out << endl << "TRACK AUDIO" << endl;
524 if (status.marker->cd_info.find("scms") != status.marker->cd_info.end()) {
527 status.out << "COPY" << endl;
529 if (status.marker->cd_info.find("preemph") != status.marker->cd_info.end()) {
530 status.out << "PRE_EMPHASIS" << endl;
532 status.out << "NO PRE_EMPHASIS" << endl;
535 if (status.marker->cd_info.find("isrc") != status.marker->cd_info.end()) {
536 status.out << "ISRC \"" << status.marker->cd_info["isrc"] << "\"" << endl;
539 status.out << "CD_TEXT {" << endl << " LANGUAGE 0 {" << endl;
540 status.out << " TITLE " << toc_escape_cdtext (status.marker->name()) << endl;
542 status.out << " PERFORMER ";
543 if (status.marker->cd_info.find("performer") != status.marker->cd_info.end()) {
544 status.out << toc_escape_cdtext (status.marker->cd_info["performer"]) << endl;
546 status.out << "\"\"" << endl;
549 if (status.marker->cd_info.find("composer") != status.marker->cd_info.end()) {
550 status.out << " SONGWRITER " << toc_escape_cdtext (status.marker->cd_info["composer"]) << endl;
553 if (status.marker->cd_info.find("isrc") != status.marker->cd_info.end()) {
554 status.out << " ISRC \"";
555 status.out << status.marker->cd_info["isrc"].substr(0,2) << "-";
556 status.out << status.marker->cd_info["isrc"].substr(2,3) << "-";
557 status.out << status.marker->cd_info["isrc"].substr(5,2) << "-";
558 status.out << status.marker->cd_info["isrc"].substr(7,5) << "\"" << endl;
561 status.out << " }" << endl << "}" << endl;
563 frames_to_cd_frames_string (buf, status.track_position);
564 status.out << "FILE " << toc_escape_filename (status.filename) << ' ' << buf;
566 frames_to_cd_frames_string (buf, status.track_duration);
567 status.out << buf << endl;
569 frames_to_cd_frames_string (buf, status.track_start_frame - status.track_position);
570 status.out << "START" << buf << endl;
574 ExportHandler::write_index_info_cue (CDMarkerStatus & status)
578 snprintf (buf, sizeof(buf), " INDEX %02d", cue_indexnum);
580 frames_to_cd_frames_string (buf, status.index_position);
581 status.out << buf << endl;
587 ExportHandler::write_index_info_toc (CDMarkerStatus & status)
591 frames_to_cd_frames_string (buf, status.index_position - status.track_position);
592 status.out << "INDEX" << buf << endl;
596 ExportHandler::frames_to_cd_frames_string (char* buf, framepos_t when)
598 framecnt_t remainder;
599 framecnt_t fr = session.nominal_frame_rate();
600 int mins, secs, frames;
602 mins = when / (60 * fr);
603 remainder = when - (mins * 60 * fr);
604 secs = remainder / fr;
605 remainder -= secs * fr;
606 frames = remainder / (fr / 75);
607 sprintf (buf, " %02d:%02d:%02d", mins, secs, frames);
611 ExportHandler::toc_escape_cdtext (const std::string& txt)
613 Glib::ustring check (txt);
615 std::string latin1_txt;
619 latin1_txt = Glib::convert (txt, "ISO-8859-1", "UTF-8");
620 } catch (Glib::ConvertError& err) {
621 throw Glib::ConvertError (err.code(), string_compose (_("Cannot convert %1 to Latin-1 text"), txt));
626 for (std::string::const_iterator c = latin1_txt.begin(); c != latin1_txt.end(); ++c) {
630 } else if ((*c) == '\\') {
632 } else if (isprint (*c)) {
635 snprintf (buf, sizeof (buf), "\\%03o", (int) (unsigned char) *c);
646 ExportHandler::toc_escape_filename (const std::string& txt)
652 // We iterate byte-wise not character-wise over a UTF-8 string here,
653 // because we only want to translate backslashes and double quotes
654 for (std::string::const_iterator c = txt.begin(); c != txt.end(); ++c) {
658 } else if (*c == '\\') {
671 ExportHandler::cue_escape_cdtext (const std::string& txt)
673 std::string latin1_txt;
677 latin1_txt = Glib::convert (txt, "ISO-8859-1", "UTF-8");
678 } catch (Glib::ConvertError& err) {
679 throw Glib::ConvertError (err.code(), string_compose (_("Cannot convert %1 to Latin-1 text"), txt));
682 // does not do much mor than UTF-8 to Latin1 translation yet, but
683 // that may have to change if cue parsers in burning programs change
684 out = '"' + latin1_txt + '"';
689 } // namespace ARDOUR