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"
23 #include "pbd/gstdio_compat.h"
25 #include <glibmm/convert.h>
27 #include "pbd/convert.h"
29 #include "ardour/audiofile_tagger.h"
30 #include "ardour/debug.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/soundcloud_upload.h"
38 #include "ardour/system_exec.h"
39 #include "pbd/openuri.h"
40 #include "pbd/basename.h"
41 #include "ardour/session_metadata.h"
51 /*** ExportElementFactory ***/
53 ExportElementFactory::ExportElementFactory (Session & session) :
59 ExportElementFactory::~ExportElementFactory ()
65 ExportElementFactory::add_timespan ()
67 return ExportTimespanPtr (new ExportTimespan (session.get_export_status(), session.frame_rate()));
70 ExportChannelConfigPtr
71 ExportElementFactory::add_channel_config ()
73 return ExportChannelConfigPtr (new ExportChannelConfiguration (session));
77 ExportElementFactory::add_format ()
79 return ExportFormatSpecPtr (new ExportFormatSpecification (session));
83 ExportElementFactory::add_format (XMLNode const & state)
85 return ExportFormatSpecPtr (new ExportFormatSpecification (session, state));
89 ExportElementFactory::add_format_copy (ExportFormatSpecPtr other)
91 return ExportFormatSpecPtr (new ExportFormatSpecification (*other));
95 ExportElementFactory::add_filename ()
97 return ExportFilenamePtr (new ExportFilename (session));
101 ExportElementFactory::add_filename_copy (ExportFilenamePtr other)
103 return ExportFilenamePtr (new ExportFilename (*other));
106 /*** ExportHandler ***/
108 ExportHandler::ExportHandler (Session & session)
109 : ExportElementFactory (session)
111 , graph_builder (new ExportGraphBuilder (session))
112 , export_status (session.get_export_status ())
113 , normalizing (false)
119 ExportHandler::~ExportHandler ()
121 graph_builder->cleanup (export_status->aborted () );
124 /** Add an export to the `to-do' list */
126 ExportHandler::add_export_config (ExportTimespanPtr timespan, ExportChannelConfigPtr channel_config,
127 ExportFormatSpecPtr format, ExportFilenamePtr filename,
128 BroadcastInfoPtr broadcast_info)
130 FileSpec spec (channel_config, format, filename, broadcast_info);
131 config_map.insert (make_pair (timespan, spec));
137 ExportHandler::do_export ()
139 /* Count timespans */
141 export_status->init();
142 std::set<ExportTimespanPtr> timespan_set;
143 for (ConfigMap::iterator it = config_map.begin(); it != config_map.end(); ++it) {
144 bool new_timespan = timespan_set.insert (it->first).second;
146 export_status->total_frames += it->first->get_length();
149 export_status->total_timespans = timespan_set.size();
151 if (export_status->total_timespans > 1) {
152 // always include timespan if there's more than one.
153 for (ConfigMap::iterator it = config_map.begin(); it != config_map.end(); ++it) {
154 FileSpec & spec = it->second;
155 spec.filename->include_timespan = true;
161 Glib::Threads::Mutex::Lock l (export_status->lock());
166 ExportHandler::start_timespan ()
168 export_status->timespan++;
170 if (config_map.empty()) {
171 // freewheeling has to be stopped from outside the process cycle
172 export_status->set_running (false);
176 /* finish_timespan pops the config_map entry that has been done, so
177 this is the timespan to do this time
179 current_timespan = config_map.begin()->first;
181 export_status->total_frames_current_timespan = current_timespan->get_length();
182 export_status->timespan_name = current_timespan->name();
183 export_status->processed_frames_current_timespan = 0;
185 /* Register file configurations to graph builder */
187 /* Here's the config_map entries that use this timespan */
188 timespan_bounds = config_map.equal_range (current_timespan);
189 graph_builder->reset ();
190 graph_builder->set_current_timespan (current_timespan);
191 handle_duplicate_format_extensions();
192 for (ConfigMap::iterator it = timespan_bounds.first; it != timespan_bounds.second; ++it) {
193 // Filenames can be shared across timespans
194 FileSpec & spec = it->second;
195 spec.filename->set_timespan (it->first);
196 graph_builder->add_config (spec);
202 session.ProcessExport.connect_same_thread (process_connection, boost::bind (&ExportHandler::process, this, _1));
203 process_position = current_timespan->get_start();
204 session.start_audio_export (process_position);
208 ExportHandler::handle_duplicate_format_extensions()
210 typedef std::map<std::string, int> ExtCountMap;
213 for (ConfigMap::iterator it = timespan_bounds.first; it != timespan_bounds.second; ++it) {
214 counts[it->second.format->extension()]++;
217 bool duplicates_found = false;
218 for (ExtCountMap::iterator it = counts.begin(); it != counts.end(); ++it) {
219 if (it->second > 1) { duplicates_found = true; }
222 // Set this always, as the filenames are shared...
223 for (ConfigMap::iterator it = timespan_bounds.first; it != timespan_bounds.second; ++it) {
224 it->second.filename->include_format_name = duplicates_found;
229 ExportHandler::process (framecnt_t frames)
231 if (!export_status->running ()) {
233 } else if (normalizing) {
234 Glib::Threads::Mutex::Lock l (export_status->lock());
235 return process_normalize ();
237 export_status->active_job = ExportStatus::Exporting;
238 Glib::Threads::Mutex::Lock l (export_status->lock());
239 return process_timespan (frames);
244 ExportHandler::process_timespan (framecnt_t frames)
246 /* update position */
248 framecnt_t frames_to_read = 0;
249 framepos_t const end = current_timespan->get_end();
251 bool const last_cycle = (process_position + frames >= end);
254 frames_to_read = end - process_position;
255 export_status->stop = true;
257 frames_to_read = frames;
260 process_position += frames_to_read;
261 export_status->processed_frames += frames_to_read;
262 export_status->processed_frames_current_timespan += frames_to_read;
264 /* Do actual processing */
265 int ret = graph_builder->process (frames_to_read, last_cycle);
267 /* Start normalizing if necessary */
269 normalizing = graph_builder->will_normalize();
271 export_status->total_normalize_cycles = graph_builder->get_normalize_cycle_count();
272 export_status->current_normalize_cycle = 0;
283 ExportHandler::process_normalize ()
285 if (graph_builder->process_normalize ()) {
287 export_status->active_job = ExportStatus::Exporting;
289 export_status->active_job = ExportStatus::Normalizing;
292 export_status->current_normalize_cycle++;
298 ExportHandler::command_output(std::string output, size_t size)
300 std::cerr << "command: " << size << ", " << output << std::endl;
301 info << output << endmsg;
305 ExportHandler::finish_timespan ()
307 graph_builder->get_analysis_results (export_status->result_map);
309 while (config_map.begin() != timespan_bounds.second) {
311 ExportFormatSpecPtr fmt = config_map.begin()->second.format;
312 std::string filename = config_map.begin()->second.filename->get_path(fmt);
313 if (fmt->with_cue()) {
314 export_cd_marker_file (current_timespan, fmt, filename, CDMarkerCUE);
317 if (fmt->with_toc()) {
318 export_cd_marker_file (current_timespan, fmt, filename, CDMarkerTOC);
321 if (fmt->with_mp4chaps()) {
322 export_cd_marker_file (current_timespan, fmt, filename, MP4Chaps);
325 /* close file first, otherwise TagLib enounters an ERROR_SHARING_VIOLATION
326 * The process cannot access the file because it is being used.
327 * ditto for post-export and upload.
329 graph_builder->reset ();
332 /* TODO: check Umlauts and encoding in filename.
333 * TagLib eventually calls CreateFileA(),
335 export_status->active_job = ExportStatus::Tagging;
336 AudiofileTagger::tag_file(filename, *SessionMetadata::Metadata());
339 if (!fmt->command().empty()) {
341 #if 0 // would be nicer with C++11 initialiser...
342 std::map<char, std::string> subs {
344 { 'd', Glib::path_get_dirname(filename) + G_DIR_SEPARATOR },
345 { 'b', PBD::basename_nosuffix(filename) },
349 export_status->active_job = ExportStatus::Command;
350 PBD::ScopedConnection command_connection;
351 std::map<char, std::string> subs;
352 subs.insert (std::pair<char, std::string> ('f', filename));
353 subs.insert (std::pair<char, std::string> ('d', Glib::path_get_dirname (filename) + G_DIR_SEPARATOR));
354 subs.insert (std::pair<char, std::string> ('b', PBD::basename_nosuffix (filename)));
355 subs.insert (std::pair<char, std::string> ('s', session.path ()));
356 subs.insert (std::pair<char, std::string> ('n', session.name ()));
358 ARDOUR::SystemExec *se = new ARDOUR::SystemExec(fmt->command(), subs);
359 info << "Post-export command line : {" << se->to_s () << "}" << endmsg;
360 se->ReadStdout.connect_same_thread(command_connection, boost::bind(&ExportHandler::command_output, this, _1, _2));
361 int ret = se->start (2);
363 // successfully started
364 while (se->is_running ()) {
365 // wait for system exec to terminate
369 error << "Post-export command FAILED with Error: " << ret << endmsg;
374 if (fmt->soundcloud_upload()) {
375 SoundcloudUploader *soundcloud_uploader = new SoundcloudUploader;
376 std::string token = soundcloud_uploader->Get_Auth_Token(soundcloud_username, soundcloud_password);
377 DEBUG_TRACE (DEBUG::Soundcloud, string_compose(
378 "uploading %1 - username=%2, password=%3, token=%4",
379 filename, soundcloud_username, soundcloud_password, token) );
380 std::string path = soundcloud_uploader->Upload (
382 PBD::basename_nosuffix(filename), // title
384 soundcloud_make_public,
385 soundcloud_downloadable,
388 if (path.length() != 0) {
389 info << string_compose ( _("File %1 uploaded to %2"), filename, path) << endmsg;
390 if (soundcloud_open_page) {
391 DEBUG_TRACE (DEBUG::Soundcloud, string_compose ("opening %1", path) );
392 open_uri(path.c_str()); // open the soundcloud website to the new file
395 error << _("upload to Soundcloud failed. Perhaps your email or password are incorrect?\n") << endmsg;
397 delete soundcloud_uploader;
399 config_map.erase (config_map.begin());
405 /*** CD Marker stuff ***/
407 struct LocationSortByStart {
408 bool operator() (Location *a, Location *b) {
409 return a->start() < b->start();
414 ExportHandler::export_cd_marker_file (ExportTimespanPtr timespan, ExportFormatSpecPtr file_format,
415 std::string filename, CDMarkerFormat format)
417 string filepath = get_cd_marker_filename(filename, format);
420 void (ExportHandler::*header_func) (CDMarkerStatus &);
421 void (ExportHandler::*track_func) (CDMarkerStatus &);
422 void (ExportHandler::*index_func) (CDMarkerStatus &);
426 header_func = &ExportHandler::write_toc_header;
427 track_func = &ExportHandler::write_track_info_toc;
428 index_func = &ExportHandler::write_index_info_toc;
431 header_func = &ExportHandler::write_cue_header;
432 track_func = &ExportHandler::write_track_info_cue;
433 index_func = &ExportHandler::write_index_info_cue;
436 header_func = &ExportHandler::write_mp4ch_header;
437 track_func = &ExportHandler::write_track_info_mp4ch;
438 index_func = &ExportHandler::write_index_info_mp4ch;
444 CDMarkerStatus status (filepath, timespan, file_format, filename);
446 (this->*header_func) (status);
448 /* Get locations and sort */
450 Locations::LocationList const & locations (session.locations()->list());
451 Locations::LocationList::const_iterator i;
452 Locations::LocationList temp;
454 for (i = locations.begin(); i != locations.end(); ++i) {
455 if ((*i)->start() >= timespan->get_start() && (*i)->end() <= timespan->get_end() && (*i)->is_cd_marker() && !(*i)->is_session_range()) {
461 // TODO One index marker for whole thing
465 LocationSortByStart cmp;
467 Locations::LocationList::const_iterator nexti;
469 /* Start actual marker stuff */
471 framepos_t last_end_time = timespan->get_start();
472 status.track_position = 0;
474 for (i = temp.begin(); i != temp.end(); ++i) {
478 if ((*i)->start() < last_end_time) {
479 if ((*i)->is_mark()) {
480 /* Index within track */
482 status.index_position = (*i)->start() - timespan->get_start();
483 (this->*index_func) (status);
489 /* A track, defined by a cd range marker or a cd location marker outside of a cd range */
491 status.track_position = last_end_time - timespan->get_start();
492 status.track_start_frame = (*i)->start() - timespan->get_start(); // everything before this is the pregap
493 status.track_duration = 0;
495 if ((*i)->is_mark()) {
496 // a mark track location needs to look ahead to the next marker's start to determine length
500 if (nexti != temp.end()) {
501 status.track_duration = (*nexti)->start() - last_end_time;
503 last_end_time = (*nexti)->start();
505 // this was the last marker, use timespan end
506 status.track_duration = timespan->get_end() - last_end_time;
508 last_end_time = timespan->get_end();
512 status.track_duration = (*i)->end() - last_end_time;
514 last_end_time = (*i)->end();
517 (this->*track_func) (status);
520 } catch (std::exception& e) {
521 error << string_compose (_("an error occurred while writing a TOC/CUE file: %1"), e.what()) << endmsg;
522 ::g_unlink (filepath.c_str());
523 } catch (Glib::Exception& e) {
524 error << string_compose (_("an error occurred while writing a TOC/CUE file: %1"), e.what()) << endmsg;
525 ::g_unlink (filepath.c_str());
530 ExportHandler::get_cd_marker_filename(std::string filename, CDMarkerFormat format)
532 /* do not strip file suffix because there may be more than one format,
533 and we do not want the CD marker file from one format to overwrite
534 another (e.g. foo.wav.cue > foo.aiff.cue)
539 return filename + ".toc";
541 return filename + ".cue";
544 unsigned lastdot = filename.find_last_of('.');
545 return filename.substr(0,lastdot) + ".chapters.txt";
548 return filename + ".marker"; // Should not be reached when actually creating a file
553 ExportHandler::write_cue_header (CDMarkerStatus & status)
555 string title = status.timespan->name().compare ("Session") ? status.timespan->name() : (string) session.name();
558 string barcode = SessionMetadata::Metadata()->barcode();
559 string album_artist = SessionMetadata::Metadata()->album_artist();
560 string album_title = SessionMetadata::Metadata()->album();
562 status.out << "REM Cue file generated by " << PROGRAM_NAME << endl;
565 status.out << "CATALOG " << barcode << endl;
567 if (album_artist != "")
568 status.out << "PERFORMER " << cue_escape_cdtext (album_artist) << endl;
570 if (album_title != "")
573 status.out << "TITLE " << cue_escape_cdtext (title) << endl;
575 /* The original cue sheet spec mentions five file types
577 BINARY = "header-less" audio (44.1 kHz, 16 Bit, little endian),
578 MOTOROLA = "header-less" audio (44.1 kHz, 16 Bit, big endian),
581 We try to use these file types whenever appropriate and
582 default to our own names otherwise.
584 status.out << "FILE \"" << Glib::path_get_basename(status.filename) << "\" ";
585 if (!status.format->format_name().compare ("WAV") || !status.format->format_name().compare ("BWF")) {
586 status.out << "WAVE";
587 } else if (status.format->format_id() == ExportFormatBase::F_RAW &&
588 status.format->sample_format() == ExportFormatBase::SF_16 &&
589 status.format->sample_rate() == ExportFormatBase::SR_44_1) {
590 // Format is RAW 16bit 44.1kHz
591 if (status.format->endianness() == ExportFormatBase::E_Little) {
592 status.out << "BINARY";
594 status.out << "MOTOROLA";
597 // no special case for AIFF format it's name is already "AIFF"
598 status.out << status.format->format_name();
604 ExportHandler::write_toc_header (CDMarkerStatus & status)
606 string title = status.timespan->name().compare ("Session") ? status.timespan->name() : (string) session.name();
609 string barcode = SessionMetadata::Metadata()->barcode();
610 string album_artist = SessionMetadata::Metadata()->album_artist();
611 string album_title = SessionMetadata::Metadata()->album();
614 status.out << "CATALOG \"" << barcode << "\"" << endl;
616 if (album_title != "")
619 status.out << "CD_DA" << endl;
620 status.out << "CD_TEXT {" << endl << " LANGUAGE_MAP {" << endl << " 0 : EN" << endl << " }" << endl;
621 status.out << " LANGUAGE 0 {" << endl << " TITLE " << toc_escape_cdtext (title) << endl ;
622 status.out << " PERFORMER " << toc_escape_cdtext (album_artist) << endl;
623 status.out << " }" << endl << "}" << endl;
627 ExportHandler::write_mp4ch_header (CDMarkerStatus & status)
629 status.out << "00:00:00.000 Intro" << endl;
633 ExportHandler::write_track_info_cue (CDMarkerStatus & status)
637 snprintf (buf, sizeof(buf), " TRACK %02d AUDIO", status.track_number);
638 status.out << buf << endl;
640 status.out << " FLAGS" ;
641 if (status.marker->cd_info.find("scms") != status.marker->cd_info.end()) {
642 status.out << " SCMS ";
644 status.out << " DCP ";
647 if (status.marker->cd_info.find("preemph") != status.marker->cd_info.end()) {
648 status.out << " PRE";
652 if (status.marker->cd_info.find("isrc") != status.marker->cd_info.end()) {
653 status.out << " ISRC " << status.marker->cd_info["isrc"] << endl;
656 if (status.marker->name() != "") {
657 status.out << " TITLE " << cue_escape_cdtext (status.marker->name()) << endl;
660 if (status.marker->cd_info.find("performer") != status.marker->cd_info.end()) {
661 status.out << " PERFORMER " << cue_escape_cdtext (status.marker->cd_info["performer"]) << endl;
664 if (status.marker->cd_info.find("composer") != status.marker->cd_info.end()) {
665 status.out << " SONGWRITER " << cue_escape_cdtext (status.marker->cd_info["composer"]) << endl;
668 if (status.track_position != status.track_start_frame) {
669 frames_to_cd_frames_string (buf, status.track_position);
670 status.out << " INDEX 00" << buf << endl;
673 frames_to_cd_frames_string (buf, status.track_start_frame);
674 status.out << " INDEX 01" << buf << endl;
676 status.index_number = 2;
677 status.track_number++;
681 ExportHandler::write_track_info_toc (CDMarkerStatus & status)
685 status.out << endl << "TRACK AUDIO" << endl;
687 if (status.marker->cd_info.find("scms") != status.marker->cd_info.end()) {
690 status.out << "COPY" << endl;
692 if (status.marker->cd_info.find("preemph") != status.marker->cd_info.end()) {
693 status.out << "PRE_EMPHASIS" << endl;
695 status.out << "NO PRE_EMPHASIS" << endl;
698 if (status.marker->cd_info.find("isrc") != status.marker->cd_info.end()) {
699 status.out << "ISRC \"" << status.marker->cd_info["isrc"] << "\"" << endl;
702 status.out << "CD_TEXT {" << endl << " LANGUAGE 0 {" << endl;
703 status.out << " TITLE " << toc_escape_cdtext (status.marker->name()) << endl;
705 status.out << " PERFORMER ";
706 if (status.marker->cd_info.find("performer") != status.marker->cd_info.end()) {
707 status.out << toc_escape_cdtext (status.marker->cd_info["performer"]) << endl;
709 status.out << "\"\"" << endl;
712 if (status.marker->cd_info.find("composer") != status.marker->cd_info.end()) {
713 status.out << " SONGWRITER " << toc_escape_cdtext (status.marker->cd_info["composer"]) << endl;
716 if (status.marker->cd_info.find("isrc") != status.marker->cd_info.end()) {
717 status.out << " ISRC \"";
718 status.out << status.marker->cd_info["isrc"].substr(0,2) << "-";
719 status.out << status.marker->cd_info["isrc"].substr(2,3) << "-";
720 status.out << status.marker->cd_info["isrc"].substr(5,2) << "-";
721 status.out << status.marker->cd_info["isrc"].substr(7,5) << "\"" << endl;
724 status.out << " }" << endl << "}" << endl;
726 frames_to_cd_frames_string (buf, status.track_position);
727 status.out << "FILE " << toc_escape_filename (status.filename) << ' ' << buf;
729 frames_to_cd_frames_string (buf, status.track_duration);
730 status.out << buf << endl;
732 frames_to_cd_frames_string (buf, status.track_start_frame - status.track_position);
733 status.out << "START" << buf << endl;
736 void ExportHandler::write_track_info_mp4ch (CDMarkerStatus & status)
740 frames_to_chapter_marks_string(buf, status.track_start_frame);
741 status.out << buf << " " << status.marker->name() << endl;
745 ExportHandler::write_index_info_cue (CDMarkerStatus & status)
749 snprintf (buf, sizeof(buf), " INDEX %02d", cue_indexnum);
751 frames_to_cd_frames_string (buf, status.index_position);
752 status.out << buf << endl;
758 ExportHandler::write_index_info_toc (CDMarkerStatus & status)
762 frames_to_cd_frames_string (buf, status.index_position - status.track_position);
763 status.out << "INDEX" << buf << endl;
767 ExportHandler::write_index_info_mp4ch (CDMarkerStatus & status)
772 ExportHandler::frames_to_cd_frames_string (char* buf, framepos_t when)
774 framecnt_t remainder;
775 framecnt_t fr = session.nominal_frame_rate();
776 int mins, secs, frames;
778 mins = when / (60 * fr);
779 remainder = when - (mins * 60 * fr);
780 secs = remainder / fr;
781 remainder -= secs * fr;
782 frames = remainder / (fr / 75);
783 sprintf (buf, " %02d:%02d:%02d", mins, secs, frames);
787 ExportHandler::frames_to_chapter_marks_string (char* buf, framepos_t when)
789 framecnt_t remainder;
790 framecnt_t fr = session.nominal_frame_rate();
791 int hours, mins, secs, msecs;
793 hours = when / (3600 * fr);
794 remainder = when - (hours * 3600 * fr);
795 mins = remainder / (60 * fr);
796 remainder -= mins * 60 * fr;
797 secs = remainder / fr;
798 remainder -= secs * fr;
799 msecs = (remainder * 1000) / fr;
800 sprintf (buf, "%02d:%02d:%02d.%03d", hours, mins, secs, msecs);
804 ExportHandler::toc_escape_cdtext (const std::string& txt)
806 Glib::ustring check (txt);
808 std::string latin1_txt;
812 latin1_txt = Glib::convert (txt, "ISO-8859-1", "UTF-8");
813 } catch (Glib::ConvertError& err) {
814 throw Glib::ConvertError (err.code(), string_compose (_("Cannot convert %1 to Latin-1 text"), txt));
819 for (std::string::const_iterator c = latin1_txt.begin(); c != latin1_txt.end(); ++c) {
823 } else if ((*c) == '\\') {
825 } else if (isprint (*c)) {
828 snprintf (buf, sizeof (buf), "\\%03o", (int) (unsigned char) *c);
839 ExportHandler::toc_escape_filename (const std::string& txt)
845 // We iterate byte-wise not character-wise over a UTF-8 string here,
846 // because we only want to translate backslashes and double quotes
847 for (std::string::const_iterator c = txt.begin(); c != txt.end(); ++c) {
851 } else if (*c == '\\') {
864 ExportHandler::cue_escape_cdtext (const std::string& txt)
866 std::string latin1_txt;
870 latin1_txt = Glib::convert (txt, "ISO-8859-1", "UTF-8");
871 } catch (Glib::ConvertError& err) {
872 throw Glib::ConvertError (err.code(), string_compose (_("Cannot convert %1 to Latin-1 text"), txt));
875 // does not do much mor than UTF-8 to Latin1 translation yet, but
876 // that may have to change if cue parsers in burning programs change
877 out = '"' + latin1_txt + '"';
882 } // namespace ARDOUR