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"
37 #include "pbd/system_exec.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 ())
109 , normalizing (false)
115 ExportHandler::~ExportHandler ()
117 // TODO remove files that were written but not finished
120 /** Add an export to the `to-do' list */
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 config_map.insert (make_pair (timespan, spec));
133 ExportHandler::do_export ()
135 /* Count timespans */
137 export_status->init();
138 std::set<ExportTimespanPtr> timespan_set;
139 for (ConfigMap::iterator it = config_map.begin(); it != config_map.end(); ++it) {
140 bool new_timespan = timespan_set.insert (it->first).second;
142 export_status->total_frames += it->first->get_length();
145 export_status->total_timespans = timespan_set.size();
153 ExportHandler::start_timespan ()
155 export_status->timespan++;
157 if (config_map.empty()) {
158 // freewheeling has to be stopped from outside the process cycle
159 export_status->running = false;
163 /* finish_timespan pops the config_map entry that has been done, so
164 this is the timespan to do this time
166 current_timespan = config_map.begin()->first;
168 export_status->total_frames_current_timespan = current_timespan->get_length();
169 export_status->timespan_name = current_timespan->name();
170 export_status->processed_frames_current_timespan = 0;
172 /* Register file configurations to graph builder */
174 /* Here's the config_map entries that use this timespan */
175 timespan_bounds = config_map.equal_range (current_timespan);
176 graph_builder->reset ();
177 graph_builder->set_current_timespan (current_timespan);
178 handle_duplicate_format_extensions();
179 for (ConfigMap::iterator it = timespan_bounds.first; it != timespan_bounds.second; ++it) {
180 // Filenames can be shared across timespans
181 FileSpec & spec = it->second;
182 spec.filename->set_timespan (it->first);
183 graph_builder->add_config (spec);
189 session.ProcessExport.connect_same_thread (process_connection, boost::bind (&ExportHandler::process, this, _1));
190 process_position = current_timespan->get_start();
191 session.start_audio_export (process_position);
195 ExportHandler::handle_duplicate_format_extensions()
197 typedef std::map<std::string, int> ExtCountMap;
200 for (ConfigMap::iterator it = timespan_bounds.first; it != timespan_bounds.second; ++it) {
201 counts[it->second.format->extension()]++;
204 bool duplicates_found = false;
205 for (ExtCountMap::iterator it = counts.begin(); it != counts.end(); ++it) {
206 if (it->second > 1) { duplicates_found = true; }
209 // Set this always, as the filenames are shared...
210 for (ConfigMap::iterator it = timespan_bounds.first; it != timespan_bounds.second; ++it) {
211 it->second.filename->include_format_name = duplicates_found;
216 ExportHandler::process (framecnt_t frames)
218 if (!export_status->running) {
220 } else if (normalizing) {
221 return process_normalize ();
223 return process_timespan (frames);
228 ExportHandler::process_timespan (framecnt_t frames)
230 /* update position */
232 framecnt_t frames_to_read = 0;
233 framepos_t const end = current_timespan->get_end();
235 bool const last_cycle = (process_position + frames >= end);
238 frames_to_read = end - process_position;
239 export_status->stop = true;
241 frames_to_read = frames;
244 process_position += frames_to_read;
245 export_status->processed_frames += frames_to_read;
246 export_status->processed_frames_current_timespan += frames_to_read;
248 /* Do actual processing */
249 int ret = graph_builder->process (frames_to_read, last_cycle);
251 /* Start normalizing if necessary */
253 normalizing = graph_builder->will_normalize();
255 export_status->total_normalize_cycles = graph_builder->get_normalize_cycle_count();
256 export_status->current_normalize_cycle = 0;
267 ExportHandler::process_normalize ()
269 if (graph_builder->process_normalize ()) {
271 export_status->normalizing = false;
273 export_status->normalizing = true;
276 export_status->current_normalize_cycle++;
282 ExportHandler::command_output(std::string output, size_t size)
284 std::cerr << "command: " << size << ", " << output << std::endl;
285 info << output << endmsg;
289 ExportHandler::finish_timespan ()
291 while (config_map.begin() != timespan_bounds.second) {
293 ExportFormatSpecPtr fmt = config_map.begin()->second.format;
294 std::string filepath = config_map.begin()->second.filename->get_path(fmt);
296 if (fmt->with_cue()) {
297 export_cd_marker_file (current_timespan, fmt, filepath, CDMarkerCUE);
300 if (fmt->with_toc()) {
301 export_cd_marker_file (current_timespan, fmt, filepath, CDMarkerTOC);
304 if (!fmt->command().empty()) {
306 #if 0 // would be nicer with C++11 initialiser...
307 std::map<char, std::string> subs {
309 { 'd', Glib::path_get_dirname(filepath) },
310 { 'b', PBD::basename_nosuffix(filepath) },
311 { 'u', upload_username },
312 { 'p', upload_password}
316 PBD::ScopedConnection command_connection;
317 std::map<char, std::string> subs;
318 subs.insert (std::pair<char, std::string> ('f', filepath));
319 subs.insert (std::pair<char, std::string> ('d', Glib::path_get_dirname(filepath)));
320 subs.insert (std::pair<char, std::string> ('b', PBD::basename_nosuffix(filepath)));
321 subs.insert (std::pair<char, std::string> ('u', upload_username));
322 subs.insert (std::pair<char, std::string> ('p', upload_password));
325 std::cerr << "running command: " << fmt->command() << "..." << std::endl;
326 SystemExec *se = new SystemExec(fmt->command(), subs);
327 se->ReadStdout.connect_same_thread(command_connection, boost::bind(&ExportHandler::command_output, this, _1, _2));
328 if (se->start (2) == 0) {
329 // successfully started
330 std::cerr << "started!" << std::endl;
331 while (se->is_running ()) {
332 // wait for system exec to terminate
333 // std::cerr << "waiting..." << std::endl;
337 std::cerr << "done! deleting..." << std::endl;
342 SoundcloudUploader *soundcloud_uploader = new SoundcloudUploader;
343 std::string token = soundcloud_uploader->Get_Auth_Token(upload_username, upload_password);
346 << filepath << std::endl
347 << "username = " << upload_username
348 << ", password = " << upload_password
349 << " - token = " << token << " ..."
351 std::string path = soundcloud_uploader->Upload (
353 PBD::basename_nosuffix(filepath), // title
358 if (path.length() != 0) {
360 std::cerr << "opening " << path << " ..." << std::endl;
361 open_uri(path.c_str()); // open the soundcloud website to the new file
364 error << _("upload to Soundcloud failed. Perhaps your email or password are incorrect?\n") << endmsg;
366 delete soundcloud_uploader;
368 config_map.erase (config_map.begin());
374 /*** CD Marker stuff ***/
376 struct LocationSortByStart {
377 bool operator() (Location *a, Location *b) {
378 return a->start() < b->start();
383 ExportHandler::export_cd_marker_file (ExportTimespanPtr timespan, ExportFormatSpecPtr file_format,
384 std::string filename, CDMarkerFormat format)
386 string filepath = get_cd_marker_filename(filename, format);
389 void (ExportHandler::*header_func) (CDMarkerStatus &);
390 void (ExportHandler::*track_func) (CDMarkerStatus &);
391 void (ExportHandler::*index_func) (CDMarkerStatus &);
395 header_func = &ExportHandler::write_toc_header;
396 track_func = &ExportHandler::write_track_info_toc;
397 index_func = &ExportHandler::write_index_info_toc;
400 header_func = &ExportHandler::write_cue_header;
401 track_func = &ExportHandler::write_track_info_cue;
402 index_func = &ExportHandler::write_index_info_cue;
408 CDMarkerStatus status (filepath, timespan, file_format, filename);
411 error << string_compose(_("Editor: cannot open \"%1\" as export file for CD marker file"), filepath) << endmsg;
415 (this->*header_func) (status);
417 /* Get locations and sort */
419 Locations::LocationList const & locations (session.locations()->list());
420 Locations::LocationList::const_iterator i;
421 Locations::LocationList temp;
423 for (i = locations.begin(); i != locations.end(); ++i) {
424 if ((*i)->start() >= timespan->get_start() && (*i)->end() <= timespan->get_end() && (*i)->is_cd_marker() && !(*i)->is_session_range()) {
430 // TODO One index marker for whole thing
434 LocationSortByStart cmp;
436 Locations::LocationList::const_iterator nexti;
438 /* Start actual marker stuff */
440 framepos_t last_end_time = timespan->get_start(), last_start_time = timespan->get_start();
441 status.track_position = last_start_time - timespan->get_start();
443 for (i = temp.begin(); i != temp.end(); ++i) {
447 if ((*i)->start() < last_end_time) {
448 if ((*i)->is_mark()) {
449 /* Index within track */
451 status.index_position = (*i)->start() - timespan->get_start();
452 (this->*index_func) (status);
458 /* A track, defined by a cd range marker or a cd location marker outside of a cd range */
460 status.track_position = last_end_time - timespan->get_start();
461 status.track_start_frame = (*i)->start() - timespan->get_start(); // everything before this is the pregap
462 status.track_duration = 0;
464 if ((*i)->is_mark()) {
465 // a mark track location needs to look ahead to the next marker's start to determine length
469 if (nexti != temp.end()) {
470 status.track_duration = (*nexti)->start() - last_end_time;
472 last_start_time = (*i)->start();
473 last_end_time = (*nexti)->start();
475 // this was the last marker, use timespan end
476 status.track_duration = timespan->get_end() - last_end_time;
478 last_start_time = (*i)->start();
479 last_end_time = timespan->get_end();
483 status.track_duration = (*i)->end() - last_end_time;
485 last_start_time = (*i)->start();
486 last_end_time = (*i)->end();
489 (this->*track_func) (status);
492 } catch (std::exception& e) {
493 error << string_compose (_("an error occured while writing a TOC/CUE file: %1"), e.what()) << endmsg;
494 ::unlink (filepath.c_str());
495 } catch (Glib::Exception& e) {
496 error << string_compose (_("an error occured while writing a TOC/CUE file: %1"), e.what()) << endmsg;
497 ::unlink (filepath.c_str());
502 ExportHandler::get_cd_marker_filename(std::string filename, CDMarkerFormat format)
504 /* do not strip file suffix because there may be more than one format,
505 and we do not want the CD marker file from one format to overwrite
506 another (e.g. foo.wav.cue > foo.aiff.cue)
511 return filename + ".toc";
513 return filename + ".cue";
515 return filename + ".marker"; // Should not be reached when actually creating a file
520 ExportHandler::write_cue_header (CDMarkerStatus & status)
522 string title = status.timespan->name().compare ("Session") ? status.timespan->name() : (string) session.name();
524 status.out << "REM Cue file generated by " << PROGRAM_NAME << endl;
525 status.out << "TITLE " << cue_escape_cdtext (title) << endl;
527 /* The original cue sheet sepc metions five file types
529 BINARY = "header-less" audio (44.1 kHz, 16 Bit, little endian),
530 MOTOROLA = "header-less" audio (44.1 kHz, 16 Bit, big endian),
533 We try to use these file types whenever appropriate and
534 default to our own names otherwise.
536 status.out << "FILE \"" << Glib::path_get_basename(status.filename) << "\" ";
537 if (!status.format->format_name().compare ("WAV") || !status.format->format_name().compare ("BWF")) {
538 status.out << "WAVE";
539 } else if (status.format->format_id() == ExportFormatBase::F_RAW &&
540 status.format->sample_format() == ExportFormatBase::SF_16 &&
541 status.format->sample_rate() == ExportFormatBase::SR_44_1) {
542 // Format is RAW 16bit 44.1kHz
543 if (status.format->endianness() == ExportFormatBase::E_Little) {
544 status.out << "BINARY";
546 status.out << "MOTOROLA";
549 // no special case for AIFF format it's name is already "AIFF"
550 status.out << status.format->format_name();
556 ExportHandler::write_toc_header (CDMarkerStatus & status)
558 string title = status.timespan->name().compare ("Session") ? status.timespan->name() : (string) session.name();
560 status.out << "CD_DA" << endl;
561 status.out << "CD_TEXT {" << endl << " LANGUAGE_MAP {" << endl << " 0 : EN" << endl << " }" << endl;
562 status.out << " LANGUAGE 0 {" << endl << " TITLE " << toc_escape_cdtext (title) << endl ;
563 status.out << " PERFORMER \"\"" << endl << " }" << endl << "}" << endl;
567 ExportHandler::write_track_info_cue (CDMarkerStatus & status)
571 snprintf (buf, sizeof(buf), " TRACK %02d AUDIO", status.track_number);
572 status.out << buf << endl;
574 status.out << " FLAGS" ;
575 if (status.marker->cd_info.find("scms") != status.marker->cd_info.end()) {
576 status.out << " SCMS ";
578 status.out << " DCP ";
581 if (status.marker->cd_info.find("preemph") != status.marker->cd_info.end()) {
582 status.out << " PRE";
586 if (status.marker->cd_info.find("isrc") != status.marker->cd_info.end()) {
587 status.out << " ISRC " << status.marker->cd_info["isrc"] << endl;
590 if (status.marker->name() != "") {
591 status.out << " TITLE " << cue_escape_cdtext (status.marker->name()) << endl;
594 if (status.marker->cd_info.find("performer") != status.marker->cd_info.end()) {
595 status.out << " PERFORMER " << cue_escape_cdtext (status.marker->cd_info["performer"]) << endl;
598 if (status.marker->cd_info.find("composer") != status.marker->cd_info.end()) {
599 status.out << " SONGWRITER " << cue_escape_cdtext (status.marker->cd_info["composer"]) << endl;
602 if (status.track_position != status.track_start_frame) {
603 frames_to_cd_frames_string (buf, status.track_position);
604 status.out << " INDEX 00" << buf << endl;
607 frames_to_cd_frames_string (buf, status.track_start_frame);
608 status.out << " INDEX 01" << buf << endl;
610 status.index_number = 2;
611 status.track_number++;
615 ExportHandler::write_track_info_toc (CDMarkerStatus & status)
619 status.out << endl << "TRACK AUDIO" << endl;
621 if (status.marker->cd_info.find("scms") != status.marker->cd_info.end()) {
624 status.out << "COPY" << endl;
626 if (status.marker->cd_info.find("preemph") != status.marker->cd_info.end()) {
627 status.out << "PRE_EMPHASIS" << endl;
629 status.out << "NO PRE_EMPHASIS" << endl;
632 if (status.marker->cd_info.find("isrc") != status.marker->cd_info.end()) {
633 status.out << "ISRC \"" << status.marker->cd_info["isrc"] << "\"" << endl;
636 status.out << "CD_TEXT {" << endl << " LANGUAGE 0 {" << endl;
637 status.out << " TITLE " << toc_escape_cdtext (status.marker->name()) << endl;
639 status.out << " PERFORMER ";
640 if (status.marker->cd_info.find("performer") != status.marker->cd_info.end()) {
641 status.out << toc_escape_cdtext (status.marker->cd_info["performer"]) << endl;
643 status.out << "\"\"" << endl;
646 if (status.marker->cd_info.find("composer") != status.marker->cd_info.end()) {
647 status.out << " SONGWRITER " << toc_escape_cdtext (status.marker->cd_info["composer"]) << endl;
650 if (status.marker->cd_info.find("isrc") != status.marker->cd_info.end()) {
651 status.out << " ISRC \"";
652 status.out << status.marker->cd_info["isrc"].substr(0,2) << "-";
653 status.out << status.marker->cd_info["isrc"].substr(2,3) << "-";
654 status.out << status.marker->cd_info["isrc"].substr(5,2) << "-";
655 status.out << status.marker->cd_info["isrc"].substr(7,5) << "\"" << endl;
658 status.out << " }" << endl << "}" << endl;
660 frames_to_cd_frames_string (buf, status.track_position);
661 status.out << "FILE " << toc_escape_filename (status.filename) << ' ' << buf;
663 frames_to_cd_frames_string (buf, status.track_duration);
664 status.out << buf << endl;
666 frames_to_cd_frames_string (buf, status.track_start_frame - status.track_position);
667 status.out << "START" << buf << endl;
671 ExportHandler::write_index_info_cue (CDMarkerStatus & status)
675 snprintf (buf, sizeof(buf), " INDEX %02d", cue_indexnum);
677 frames_to_cd_frames_string (buf, status.index_position);
678 status.out << buf << endl;
684 ExportHandler::write_index_info_toc (CDMarkerStatus & status)
688 frames_to_cd_frames_string (buf, status.index_position - status.track_position);
689 status.out << "INDEX" << buf << endl;
693 ExportHandler::frames_to_cd_frames_string (char* buf, framepos_t when)
695 framecnt_t remainder;
696 framecnt_t fr = session.nominal_frame_rate();
697 int mins, secs, frames;
699 mins = when / (60 * fr);
700 remainder = when - (mins * 60 * fr);
701 secs = remainder / fr;
702 remainder -= secs * fr;
703 frames = remainder / (fr / 75);
704 sprintf (buf, " %02d:%02d:%02d", mins, secs, frames);
708 ExportHandler::toc_escape_cdtext (const std::string& txt)
710 Glib::ustring check (txt);
712 std::string latin1_txt;
716 latin1_txt = Glib::convert (txt, "ISO-8859-1", "UTF-8");
717 } catch (Glib::ConvertError& err) {
718 throw Glib::ConvertError (err.code(), string_compose (_("Cannot convert %1 to Latin-1 text"), txt));
723 for (std::string::const_iterator c = latin1_txt.begin(); c != latin1_txt.end(); ++c) {
727 } else if ((*c) == '\\') {
729 } else if (isprint (*c)) {
732 snprintf (buf, sizeof (buf), "\\%03o", (int) (unsigned char) *c);
743 ExportHandler::toc_escape_filename (const std::string& txt)
749 // We iterate byte-wise not character-wise over a UTF-8 string here,
750 // because we only want to translate backslashes and double quotes
751 for (std::string::const_iterator c = txt.begin(); c != txt.end(); ++c) {
755 } else if (*c == '\\') {
768 ExportHandler::cue_escape_cdtext (const std::string& txt)
770 std::string latin1_txt;
774 latin1_txt = Glib::convert (txt, "ISO-8859-1", "UTF-8");
775 } catch (Glib::ConvertError& err) {
776 throw Glib::ConvertError (err.code(), string_compose (_("Cannot convert %1 to Latin-1 text"), txt));
779 // does not do much mor than UTF-8 to Latin1 translation yet, but
780 // that may have to change if cue parsers in burning programs change
781 out = '"' + latin1_txt + '"';
786 } // namespace ARDOUR