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"
34 #include "ardour/soundcloud_upload.h"
35 #include "pbd/openuri.h"
36 #include "pbd/basename.h"
46 /*** ExportElementFactory ***/
48 ExportElementFactory::ExportElementFactory (Session & session) :
54 ExportElementFactory::~ExportElementFactory ()
60 ExportElementFactory::add_timespan ()
62 return ExportTimespanPtr (new ExportTimespan (session.get_export_status(), session.frame_rate()));
65 ExportChannelConfigPtr
66 ExportElementFactory::add_channel_config ()
68 return ExportChannelConfigPtr (new ExportChannelConfiguration (session));
72 ExportElementFactory::add_format ()
74 return ExportFormatSpecPtr (new ExportFormatSpecification (session));
78 ExportElementFactory::add_format (XMLNode const & state)
80 return ExportFormatSpecPtr (new ExportFormatSpecification (session, state));
84 ExportElementFactory::add_format_copy (ExportFormatSpecPtr other)
86 return ExportFormatSpecPtr (new ExportFormatSpecification (*other));
90 ExportElementFactory::add_filename ()
92 return ExportFilenamePtr (new ExportFilename (session));
96 ExportElementFactory::add_filename_copy (ExportFilenamePtr other)
98 return ExportFilenamePtr (new ExportFilename (*other));
101 /*** ExportHandler ***/
103 ExportHandler::ExportHandler (Session & session)
104 : ExportElementFactory (session)
106 , graph_builder (new ExportGraphBuilder (session))
107 , export_status (session.get_export_status ())
108 , normalizing (false)
114 ExportHandler::~ExportHandler ()
116 // TODO remove files that were written but not finished
119 /** Add an export to the `to-do' list */
121 ExportHandler::add_export_config (ExportTimespanPtr timespan, ExportChannelConfigPtr channel_config,
122 ExportFormatSpecPtr format, ExportFilenamePtr filename,
123 BroadcastInfoPtr broadcast_info)
125 FileSpec spec (channel_config, format, filename, broadcast_info);
126 config_map.insert (make_pair (timespan, spec));
132 ExportHandler::do_export ()
134 /* Count timespans */
136 export_status->init();
137 std::set<ExportTimespanPtr> timespan_set;
138 for (ConfigMap::iterator it = config_map.begin(); it != config_map.end(); ++it) {
139 bool new_timespan = timespan_set.insert (it->first).second;
141 export_status->total_frames += it->first->get_length();
144 export_status->total_timespans = timespan_set.size();
152 ExportHandler::start_timespan ()
154 export_status->timespan++;
156 if (config_map.empty()) {
157 // freewheeling has to be stopped from outside the process cycle
158 export_status->running = false;
162 /* finish_timespan pops the config_map entry that has been done, so
163 this is the timespan to do this time
165 current_timespan = config_map.begin()->first;
167 export_status->total_frames_current_timespan = current_timespan->get_length();
168 export_status->timespan_name = current_timespan->name();
169 export_status->processed_frames_current_timespan = 0;
171 /* Register file configurations to graph builder */
173 /* Here's the config_map entries that use this timespan */
174 timespan_bounds = config_map.equal_range (current_timespan);
175 graph_builder->reset ();
176 graph_builder->set_current_timespan (current_timespan);
177 handle_duplicate_format_extensions();
178 for (ConfigMap::iterator it = timespan_bounds.first; it != timespan_bounds.second; ++it) {
179 // Filenames can be shared across timespans
180 FileSpec & spec = it->second;
181 spec.filename->set_timespan (it->first);
182 graph_builder->add_config (spec);
188 session.ProcessExport.connect_same_thread (process_connection, boost::bind (&ExportHandler::process, this, _1));
189 process_position = current_timespan->get_start();
190 session.start_audio_export (process_position);
194 ExportHandler::handle_duplicate_format_extensions()
196 typedef std::map<std::string, int> ExtCountMap;
199 for (ConfigMap::iterator it = timespan_bounds.first; it != timespan_bounds.second; ++it) {
200 counts[it->second.format->extension()]++;
203 bool duplicates_found = false;
204 for (ExtCountMap::iterator it = counts.begin(); it != counts.end(); ++it) {
205 if (it->second > 1) { duplicates_found = true; }
208 // Set this always, as the filenames are shared...
209 for (ConfigMap::iterator it = timespan_bounds.first; it != timespan_bounds.second; ++it) {
210 it->second.filename->include_format_name = duplicates_found;
215 ExportHandler::process (framecnt_t frames)
217 if (!export_status->running) {
219 } else if (normalizing) {
220 return process_normalize ();
222 return process_timespan (frames);
227 ExportHandler::process_timespan (framecnt_t frames)
229 /* update position */
231 framecnt_t frames_to_read = 0;
232 framepos_t const end = current_timespan->get_end();
234 bool const last_cycle = (process_position + frames >= end);
237 frames_to_read = end - process_position;
238 export_status->stop = true;
240 frames_to_read = frames;
243 process_position += frames_to_read;
244 export_status->processed_frames += frames_to_read;
245 export_status->processed_frames_current_timespan += frames_to_read;
247 /* Do actual processing */
248 int ret = graph_builder->process (frames_to_read, last_cycle);
250 /* Start normalizing if necessary */
252 normalizing = graph_builder->will_normalize();
254 export_status->total_normalize_cycles = graph_builder->get_normalize_cycle_count();
255 export_status->current_normalize_cycle = 0;
266 ExportHandler::process_normalize ()
268 if (graph_builder->process_normalize ()) {
270 export_status->normalizing = false;
272 export_status->normalizing = true;
275 export_status->current_normalize_cycle++;
281 ExportHandler::finish_timespan ()
283 while (config_map.begin() != timespan_bounds.second) {
285 ExportFormatSpecPtr fmt = config_map.begin()->second.format;
286 std::string filepath = config_map.begin()->second.filename->get_path(fmt);
288 if (fmt->with_cue()) {
289 export_cd_marker_file (current_timespan, fmt, filepath, CDMarkerCUE);
292 if (fmt->with_toc()) {
293 export_cd_marker_file (current_timespan, fmt, filepath, CDMarkerTOC);
296 if (!fmt->command().empty()) {
297 std::string command = string_compose(fmt->command(),
299 Glib::path_get_dirname(filepath),
300 PBD::basename_nosuffix(filepath)
302 std::cerr << "running command: " << command << "..." << std::endl;
303 system(command.c_str());
307 SoundcloudUploader *soundcloud_uploader = new SoundcloudUploader;
308 std::string token = soundcloud_uploader->Get_Auth_Token(upload_username, upload_password);
311 << filepath << std::endl
312 << "username = " << upload_username
313 << ", password = " << upload_password
314 << " - token = " << token << " ..."
316 std::string path = soundcloud_uploader->Upload (
318 PBD::basename_nosuffix(filepath), // title
323 if (path.length() != 0) {
325 std::cerr << "opening " << path << " ..." << std::endl;
326 open_uri(path.c_str()); // open the soundcloud website to the new file
329 error << _("upload to Soundcloud failed. Perhaps your email or password are incorrect?\n") << endmsg;
331 delete soundcloud_uploader;
333 config_map.erase (config_map.begin());
339 /*** CD Marker stuff ***/
341 struct LocationSortByStart {
342 bool operator() (Location *a, Location *b) {
343 return a->start() < b->start();
348 ExportHandler::export_cd_marker_file (ExportTimespanPtr timespan, ExportFormatSpecPtr file_format,
349 std::string filename, CDMarkerFormat format)
351 string filepath = get_cd_marker_filename(filename, format);
354 void (ExportHandler::*header_func) (CDMarkerStatus &);
355 void (ExportHandler::*track_func) (CDMarkerStatus &);
356 void (ExportHandler::*index_func) (CDMarkerStatus &);
360 header_func = &ExportHandler::write_toc_header;
361 track_func = &ExportHandler::write_track_info_toc;
362 index_func = &ExportHandler::write_index_info_toc;
365 header_func = &ExportHandler::write_cue_header;
366 track_func = &ExportHandler::write_track_info_cue;
367 index_func = &ExportHandler::write_index_info_cue;
373 CDMarkerStatus status (filepath, timespan, file_format, filename);
376 error << string_compose(_("Editor: cannot open \"%1\" as export file for CD marker file"), filepath) << endmsg;
380 (this->*header_func) (status);
382 /* Get locations and sort */
384 Locations::LocationList const & locations (session.locations()->list());
385 Locations::LocationList::const_iterator i;
386 Locations::LocationList temp;
388 for (i = locations.begin(); i != locations.end(); ++i) {
389 if ((*i)->start() >= timespan->get_start() && (*i)->end() <= timespan->get_end() && (*i)->is_cd_marker() && !(*i)->is_session_range()) {
395 // TODO One index marker for whole thing
399 LocationSortByStart cmp;
401 Locations::LocationList::const_iterator nexti;
403 /* Start actual marker stuff */
405 framepos_t last_end_time = timespan->get_start(), last_start_time = timespan->get_start();
406 status.track_position = last_start_time - timespan->get_start();
408 for (i = temp.begin(); i != temp.end(); ++i) {
412 if ((*i)->start() < last_end_time) {
413 if ((*i)->is_mark()) {
414 /* Index within track */
416 status.index_position = (*i)->start() - timespan->get_start();
417 (this->*index_func) (status);
423 /* A track, defined by a cd range marker or a cd location marker outside of a cd range */
425 status.track_position = last_end_time - timespan->get_start();
426 status.track_start_frame = (*i)->start() - timespan->get_start(); // everything before this is the pregap
427 status.track_duration = 0;
429 if ((*i)->is_mark()) {
430 // a mark track location needs to look ahead to the next marker's start to determine length
434 if (nexti != temp.end()) {
435 status.track_duration = (*nexti)->start() - last_end_time;
437 last_start_time = (*i)->start();
438 last_end_time = (*nexti)->start();
440 // this was the last marker, use timespan end
441 status.track_duration = timespan->get_end() - last_end_time;
443 last_start_time = (*i)->start();
444 last_end_time = timespan->get_end();
448 status.track_duration = (*i)->end() - last_end_time;
450 last_start_time = (*i)->start();
451 last_end_time = (*i)->end();
454 (this->*track_func) (status);
457 } catch (std::exception& e) {
458 error << string_compose (_("an error occured while writing a TOC/CUE file: %1"), e.what()) << endmsg;
459 ::unlink (filepath.c_str());
460 } catch (Glib::Exception& e) {
461 error << string_compose (_("an error occured while writing a TOC/CUE file: %1"), e.what()) << endmsg;
462 ::unlink (filepath.c_str());
467 ExportHandler::get_cd_marker_filename(std::string filename, CDMarkerFormat format)
469 /* do not strip file suffix because there may be more than one format,
470 and we do not want the CD marker file from one format to overwrite
471 another (e.g. foo.wav.cue > foo.aiff.cue)
476 return filename + ".toc";
478 return filename + ".cue";
480 return filename + ".marker"; // Should not be reached when actually creating a file
485 ExportHandler::write_cue_header (CDMarkerStatus & status)
487 string title = status.timespan->name().compare ("Session") ? status.timespan->name() : (string) session.name();
489 status.out << "REM Cue file generated by " << PROGRAM_NAME << endl;
490 status.out << "TITLE " << cue_escape_cdtext (title) << endl;
492 /* The original cue sheet sepc metions five file types
494 BINARY = "header-less" audio (44.1 kHz, 16 Bit, little endian),
495 MOTOROLA = "header-less" audio (44.1 kHz, 16 Bit, big endian),
498 We try to use these file types whenever appropriate and
499 default to our own names otherwise.
501 status.out << "FILE \"" << Glib::path_get_basename(status.filename) << "\" ";
502 if (!status.format->format_name().compare ("WAV") || !status.format->format_name().compare ("BWF")) {
503 status.out << "WAVE";
504 } else if (status.format->format_id() == ExportFormatBase::F_RAW &&
505 status.format->sample_format() == ExportFormatBase::SF_16 &&
506 status.format->sample_rate() == ExportFormatBase::SR_44_1) {
507 // Format is RAW 16bit 44.1kHz
508 if (status.format->endianness() == ExportFormatBase::E_Little) {
509 status.out << "BINARY";
511 status.out << "MOTOROLA";
514 // no special case for AIFF format it's name is already "AIFF"
515 status.out << status.format->format_name();
521 ExportHandler::write_toc_header (CDMarkerStatus & status)
523 string title = status.timespan->name().compare ("Session") ? status.timespan->name() : (string) session.name();
525 status.out << "CD_DA" << endl;
526 status.out << "CD_TEXT {" << endl << " LANGUAGE_MAP {" << endl << " 0 : EN" << endl << " }" << endl;
527 status.out << " LANGUAGE 0 {" << endl << " TITLE " << toc_escape_cdtext (title) << endl ;
528 status.out << " PERFORMER \"\"" << endl << " }" << endl << "}" << endl;
532 ExportHandler::write_track_info_cue (CDMarkerStatus & status)
536 snprintf (buf, sizeof(buf), " TRACK %02d AUDIO", status.track_number);
537 status.out << buf << endl;
539 status.out << " FLAGS" ;
540 if (status.marker->cd_info.find("scms") != status.marker->cd_info.end()) {
541 status.out << " SCMS ";
543 status.out << " DCP ";
546 if (status.marker->cd_info.find("preemph") != status.marker->cd_info.end()) {
547 status.out << " PRE";
551 if (status.marker->cd_info.find("isrc") != status.marker->cd_info.end()) {
552 status.out << " ISRC " << status.marker->cd_info["isrc"] << endl;
555 if (status.marker->name() != "") {
556 status.out << " TITLE " << cue_escape_cdtext (status.marker->name()) << endl;
559 if (status.marker->cd_info.find("performer") != status.marker->cd_info.end()) {
560 status.out << " PERFORMER " << cue_escape_cdtext (status.marker->cd_info["performer"]) << endl;
563 if (status.marker->cd_info.find("composer") != status.marker->cd_info.end()) {
564 status.out << " SONGWRITER " << cue_escape_cdtext (status.marker->cd_info["composer"]) << endl;
567 if (status.track_position != status.track_start_frame) {
568 frames_to_cd_frames_string (buf, status.track_position);
569 status.out << " INDEX 00" << buf << endl;
572 frames_to_cd_frames_string (buf, status.track_start_frame);
573 status.out << " INDEX 01" << buf << endl;
575 status.index_number = 2;
576 status.track_number++;
580 ExportHandler::write_track_info_toc (CDMarkerStatus & status)
584 status.out << endl << "TRACK AUDIO" << endl;
586 if (status.marker->cd_info.find("scms") != status.marker->cd_info.end()) {
589 status.out << "COPY" << endl;
591 if (status.marker->cd_info.find("preemph") != status.marker->cd_info.end()) {
592 status.out << "PRE_EMPHASIS" << endl;
594 status.out << "NO PRE_EMPHASIS" << endl;
597 if (status.marker->cd_info.find("isrc") != status.marker->cd_info.end()) {
598 status.out << "ISRC \"" << status.marker->cd_info["isrc"] << "\"" << endl;
601 status.out << "CD_TEXT {" << endl << " LANGUAGE 0 {" << endl;
602 status.out << " TITLE " << toc_escape_cdtext (status.marker->name()) << endl;
604 status.out << " PERFORMER ";
605 if (status.marker->cd_info.find("performer") != status.marker->cd_info.end()) {
606 status.out << toc_escape_cdtext (status.marker->cd_info["performer"]) << endl;
608 status.out << "\"\"" << endl;
611 if (status.marker->cd_info.find("composer") != status.marker->cd_info.end()) {
612 status.out << " SONGWRITER " << toc_escape_cdtext (status.marker->cd_info["composer"]) << endl;
615 if (status.marker->cd_info.find("isrc") != status.marker->cd_info.end()) {
616 status.out << " ISRC \"";
617 status.out << status.marker->cd_info["isrc"].substr(0,2) << "-";
618 status.out << status.marker->cd_info["isrc"].substr(2,3) << "-";
619 status.out << status.marker->cd_info["isrc"].substr(5,2) << "-";
620 status.out << status.marker->cd_info["isrc"].substr(7,5) << "\"" << endl;
623 status.out << " }" << endl << "}" << endl;
625 frames_to_cd_frames_string (buf, status.track_position);
626 status.out << "FILE " << toc_escape_filename (status.filename) << ' ' << buf;
628 frames_to_cd_frames_string (buf, status.track_duration);
629 status.out << buf << endl;
631 frames_to_cd_frames_string (buf, status.track_start_frame - status.track_position);
632 status.out << "START" << buf << endl;
636 ExportHandler::write_index_info_cue (CDMarkerStatus & status)
640 snprintf (buf, sizeof(buf), " INDEX %02d", cue_indexnum);
642 frames_to_cd_frames_string (buf, status.index_position);
643 status.out << buf << endl;
649 ExportHandler::write_index_info_toc (CDMarkerStatus & status)
653 frames_to_cd_frames_string (buf, status.index_position - status.track_position);
654 status.out << "INDEX" << buf << endl;
658 ExportHandler::frames_to_cd_frames_string (char* buf, framepos_t when)
660 framecnt_t remainder;
661 framecnt_t fr = session.nominal_frame_rate();
662 int mins, secs, frames;
664 mins = when / (60 * fr);
665 remainder = when - (mins * 60 * fr);
666 secs = remainder / fr;
667 remainder -= secs * fr;
668 frames = remainder / (fr / 75);
669 sprintf (buf, " %02d:%02d:%02d", mins, secs, frames);
673 ExportHandler::toc_escape_cdtext (const std::string& txt)
675 Glib::ustring check (txt);
677 std::string latin1_txt;
681 latin1_txt = Glib::convert (txt, "ISO-8859-1", "UTF-8");
682 } catch (Glib::ConvertError& err) {
683 throw Glib::ConvertError (err.code(), string_compose (_("Cannot convert %1 to Latin-1 text"), txt));
688 for (std::string::const_iterator c = latin1_txt.begin(); c != latin1_txt.end(); ++c) {
692 } else if ((*c) == '\\') {
694 } else if (isprint (*c)) {
697 snprintf (buf, sizeof (buf), "\\%03o", (int) (unsigned char) *c);
708 ExportHandler::toc_escape_filename (const std::string& txt)
714 // We iterate byte-wise not character-wise over a UTF-8 string here,
715 // because we only want to translate backslashes and double quotes
716 for (std::string::const_iterator c = txt.begin(); c != txt.end(); ++c) {
720 } else if (*c == '\\') {
733 ExportHandler::cue_escape_cdtext (const std::string& txt)
735 std::string latin1_txt;
739 latin1_txt = Glib::convert (txt, "ISO-8859-1", "UTF-8");
740 } catch (Glib::ConvertError& err) {
741 throw Glib::ConvertError (err.code(), string_compose (_("Cannot convert %1 to Latin-1 text"), txt));
744 // does not do much mor than UTF-8 to Latin1 translation yet, but
745 // that may have to change if cue parsers in burning programs change
746 out = '"' + latin1_txt + '"';
751 } // namespace ARDOUR